From 49e3ebd528bd54f70afcbc91405913c7711858ef Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Mon, 14 Oct 2019 16:52:06 +0200 Subject: [PATCH 01/71] move pytest, mypy and flake8 config to setup.cfg --- pytest.ini | 5 ----- mypy.ini => setup.cfg | 20 ++++++++++++++++++++ tox.ini | 2 +- 3 files changed, 21 insertions(+), 6 deletions(-) delete mode 100644 pytest.ini rename mypy.ini => setup.cfg (75%) diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index b014ae3831..0000000000 --- a/pytest.ini +++ /dev/null @@ -1,5 +0,0 @@ -[pytest] -log_cli = 1 -log_cli_level = DEBUG -log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s) -log_cli_date_format=%Y-%m-%d %H:%M:%S diff --git a/mypy.ini b/setup.cfg similarity index 75% rename from mypy.ini rename to setup.cfg index 461ccb7ac3..d32ac4817a 100644 --- a/mypy.ini +++ b/setup.cfg @@ -1,3 +1,23 @@ +[bdist_wheel] +universal = 1 + +[pytest] +log_cli = 1 +log_cli_level = DEBUG +log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s) +log_cli_date_format=%Y-%m-%d %H:%M:%S + + +[flake8] +paths=aea,examples,packages,scripts,tests +exclude=.md, + *_pb2.py, + aea/__init__.py, + aea/cli/__init__.py, + tests/common/oef_search_pluto_scripts, + scripts/oef/launch.py +ignore = E501,E701 + # Global options: [mypy] diff --git a/tox.ini b/tox.ini index 7e7254eac1..8673016b87 100644 --- a/tox.ini +++ b/tox.ini @@ -20,7 +20,7 @@ basepython = python3.7 deps = flake8 flake8-docstrings pydocstyle==3.0.0 -commands = flake8 aea examples packages scripts tests --exclude=.md,*_pb2.py,aea/__init__.py,aea/cli/__init__.py,tests/common//oef_search_pluto_scripts,scripts/oef/launch.py --ignore=E501,E701 +commands = flake8 aea examples packages scripts tests [testenv:mypy] basepython = python3.7 From aea813a305fe64150c3a977bd02b30c5b4cc1708 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Mon, 14 Oct 2019 18:19:10 +0200 Subject: [PATCH 02/71] fix pytest config. --- pytest.ini | 5 +++++ setup.cfg | 7 ------- 2 files changed, 5 insertions(+), 7 deletions(-) create mode 100644 pytest.ini diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000000..b014ae3831 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +log_cli = 1 +log_cli_level = DEBUG +log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s) +log_cli_date_format=%Y-%m-%d %H:%M:%S diff --git a/setup.cfg b/setup.cfg index d32ac4817a..3e01630647 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,13 +1,6 @@ [bdist_wheel] universal = 1 -[pytest] -log_cli = 1 -log_cli_level = DEBUG -log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s) -log_cli_date_format=%Y-%m-%d %H:%M:%S - - [flake8] paths=aea,examples,packages,scripts,tests exclude=.md, From 7414b2093f51cbba4b2b0ec02282d48deadc23ed Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Tue, 15 Oct 2019 13:09:44 +0100 Subject: [PATCH 03/71] fixes setup.py dependency references --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1546365c9b..12d3852e45 100644 --- a/setup.py +++ b/setup.py @@ -68,7 +68,7 @@ def get_all_extras() -> Dict: cli_gui = [ "flask", - "connexion[swagger-ui] @ git+https://github.com/neverpanic/connexion.git@jsonschema-3#egg=connexion[swagger-ui]" + "connexion[swagger-ui]" ] cli_deps = [ @@ -130,6 +130,9 @@ def get_all_extras() -> Dict: *all_extras.get("cli", []), *all_extras.get("oef_connection", []), ], + dependency_links=[ + 'git+https://github.com/neverpanic/connexion.git@jsonschema-3#egg=connexion[swagger-ui]', + ], tests_require=["tox"], extras_require=all_extras, entry_points={ From 946eb2e9058006b6a1e171c7ca18171a424018f3 Mon Sep 17 00:00:00 2001 From: Aristotelis Date: Tue, 15 Oct 2019 13:28:35 +0100 Subject: [PATCH 04/71] Commit before debugging --- m_agent/aea-config.yaml | 29 + m_agent/connections/__init__.py | 0 m_agent/connections/oef/__init__.py | 21 + m_agent/connections/oef/connection.py | 604 ++++++++++++++++++ m_agent/connections/oef/connection.yaml | 13 + m_agent/default_private_key.pem | 6 + m_agent/eth_private_key.txt | 1 + m_agent/fet_private_key.txt | 1 + m_agent/protocols/__init__.py | 0 m_agent/protocols/default/__init__.py | 21 + m_agent/protocols/default/message.py | 59 ++ m_agent/protocols/default/protocol.yaml | 5 + m_agent/protocols/default/serialization.py | 71 ++ m_agent/protocols/fipa/README.md | 6 + m_agent/protocols/fipa/__init__.py | 21 + m_agent/protocols/fipa/fipa.proto | 48 ++ m_agent/protocols/fipa/fipa_pb2.py | 525 +++++++++++++++ m_agent/protocols/fipa/message.py | 99 +++ m_agent/protocols/fipa/protocol.yaml | 7 + m_agent/protocols/fipa/serialization.py | 143 +++++ m_agent/skills/__init__.py | 0 m_agent/skills/error/__init__.py | 20 + m_agent/skills/error/behaviours.py | 50 ++ m_agent/skills/error/handlers.py | 131 ++++ m_agent/skills/error/skill.yaml | 14 + m_agent/skills/error/tasks.py | 51 ++ m_agent/skills/weather_station/__init__.py | 20 + m_agent/skills/weather_station/behaviours.py | 84 +++ .../weather_station/db_communication.py | 70 ++ .../dummy_weather_station_data.db | Bin 0 -> 8192 bytes .../dummy_weather_station_data.py | 122 ++++ m_agent/skills/weather_station/handlers.py | 165 +++++ m_agent/skills/weather_station/skill.yaml | 25 + m_agent/skills/weather_station/tasks.py | 50 ++ .../weather_station_data_model.py | 37 ++ 35 files changed, 2519 insertions(+) create mode 100644 m_agent/aea-config.yaml create mode 100644 m_agent/connections/__init__.py create mode 100644 m_agent/connections/oef/__init__.py create mode 100644 m_agent/connections/oef/connection.py create mode 100644 m_agent/connections/oef/connection.yaml create mode 100644 m_agent/default_private_key.pem create mode 100644 m_agent/eth_private_key.txt create mode 100644 m_agent/fet_private_key.txt create mode 100644 m_agent/protocols/__init__.py create mode 100644 m_agent/protocols/default/__init__.py create mode 100644 m_agent/protocols/default/message.py create mode 100644 m_agent/protocols/default/protocol.yaml create mode 100644 m_agent/protocols/default/serialization.py create mode 100644 m_agent/protocols/fipa/README.md create mode 100644 m_agent/protocols/fipa/__init__.py create mode 100644 m_agent/protocols/fipa/fipa.proto create mode 100644 m_agent/protocols/fipa/fipa_pb2.py create mode 100644 m_agent/protocols/fipa/message.py create mode 100644 m_agent/protocols/fipa/protocol.yaml create mode 100644 m_agent/protocols/fipa/serialization.py create mode 100644 m_agent/skills/__init__.py create mode 100644 m_agent/skills/error/__init__.py create mode 100644 m_agent/skills/error/behaviours.py create mode 100644 m_agent/skills/error/handlers.py create mode 100644 m_agent/skills/error/skill.yaml create mode 100644 m_agent/skills/error/tasks.py create mode 100644 m_agent/skills/weather_station/__init__.py create mode 100644 m_agent/skills/weather_station/behaviours.py create mode 100644 m_agent/skills/weather_station/db_communication.py create mode 100644 m_agent/skills/weather_station/dummy_weather_station_data.db create mode 100644 m_agent/skills/weather_station/dummy_weather_station_data.py create mode 100644 m_agent/skills/weather_station/handlers.py create mode 100644 m_agent/skills/weather_station/skill.yaml create mode 100644 m_agent/skills/weather_station/tasks.py create mode 100644 m_agent/skills/weather_station/weather_station_data_model.py diff --git a/m_agent/aea-config.yaml b/m_agent/aea-config.yaml new file mode 100644 index 0000000000..f9879e08c5 --- /dev/null +++ b/m_agent/aea-config.yaml @@ -0,0 +1,29 @@ +aea_version: 0.1.6 +agent_name: m_agent +authors: '' +connections: +- oef +default_connection: oef +license: '' +logging_config: + disable_existing_loggers: false + version: 1 +private_key_paths: +- private_key_path: + ledger: default + path: default_private_key.pem +- private_key_path: + ledger: fetchai + path: fet_private_key.txt +- private_key_path: + ledger: ethereum + path: eth_private_key.txt +protocols: +- default +- fipa +registry_path: ../packages +skills: +- error +- weather_station +url: '' +version: v1 diff --git a/m_agent/connections/__init__.py b/m_agent/connections/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/m_agent/connections/oef/__init__.py b/m_agent/connections/oef/__init__.py new file mode 100644 index 0000000000..21ee4d83df --- /dev/null +++ b/m_agent/connections/oef/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Implementation of the OEF connection.""" diff --git a/m_agent/connections/oef/connection.py b/m_agent/connections/oef/connection.py new file mode 100644 index 0000000000..7f489b867f --- /dev/null +++ b/m_agent/connections/oef/connection.py @@ -0,0 +1,604 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Extension to the OEF Python SDK.""" +import datetime +import logging +import pickle +from queue import Empty, Queue +from threading import Thread +from typing import List, Dict, Optional, cast + +import oef +from oef.agents import OEFAgent +from oef.core import AsyncioCore +from oef.messages import CFP_TYPES, PROPOSE_TYPES +from oef.query import ( + Query as OEFQuery, + ConstraintExpr as OEFConstraintExpr, + And as OEFAnd, + Or as OEFOr, + Not as OEFNot, + Constraint as OEFConstraint, + ConstraintType as OEFConstraintType, Eq, NotEq, Lt, LtEq, Gt, GtEq, Range, In, NotIn) +from oef.schema import Description as OEFDescription, DataModel as OEFDataModel, AttributeSchema as OEFAttribute + +from aea.configurations.base import ConnectionConfig +from aea.connections.base import Channel, Connection +from aea.mail.base import MailBox, Envelope +from aea.protocols.fipa.message import FIPAMessage +from aea.protocols.fipa.serialization import FIPASerializer +from aea.protocols.oef.message import OEFMessage +from aea.protocols.oef.models import Description, Attribute, DataModel, Query, ConstraintExpr, And, Or, Not, Constraint, \ + ConstraintType, ConstraintTypes +from aea.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF + +logger = logging.getLogger(__name__) + + +STUB_MESSSAGE_ID = 0 +STUB_DIALOGUE_ID = 0 + + +class OEFObjectTranslator: + """Translate our OEF object to object of OEF SDK classes.""" + + @classmethod + def to_oef_description(cls, desc: Description) -> OEFDescription: + """From our description to OEF description.""" + oef_data_model = cls.to_oef_data_model(desc.data_model) if desc.data_model is not None else None + return OEFDescription(desc.values, oef_data_model) + + @classmethod + def to_oef_data_model(cls, data_model: DataModel) -> OEFDataModel: + """From our data model to OEF data model.""" + oef_attributes = [cls.to_oef_attribute(attribute) for attribute in data_model.attributes] + return OEFDataModel(data_model.name, oef_attributes, data_model.description) + + @classmethod + def to_oef_attribute(cls, attribute: Attribute) -> OEFAttribute: + """From our attribute to OEF attribute.""" + return OEFAttribute(attribute.name, attribute.type, attribute.is_required, attribute.description) + + @classmethod + def to_oef_query(cls, query: Query) -> OEFQuery: + """From our query to OEF query.""" + oef_data_model = cls.to_oef_data_model(query.model) if query.model is not None else None + constraints = [cls.to_oef_constraint_expr(c) for c in query.constraints] + return OEFQuery(constraints, oef_data_model) + + @classmethod + def to_oef_constraint_expr(cls, constraint_expr: ConstraintExpr) -> OEFConstraintExpr: + """From our constraint expression to the OEF constraint expression.""" + if isinstance(constraint_expr, And): + return OEFAnd([cls.to_oef_constraint_expr(c) for c in constraint_expr.constraints]) + elif isinstance(constraint_expr, Or): + return OEFOr([cls.to_oef_constraint_expr(c) for c in constraint_expr.constraints]) + elif isinstance(constraint_expr, Not): + return OEFNot(cls.to_oef_constraint_expr(constraint_expr.constraint)) + elif isinstance(constraint_expr, Constraint): + oef_constraint_type = cls.to_oef_constraint_type(constraint_expr.constraint_type) + return OEFConstraint(constraint_expr.attribute_name, oef_constraint_type) + else: + raise ValueError("Constraint expression not supported.") + + @classmethod + def to_oef_constraint_type(cls, constraint_type: ConstraintType) -> OEFConstraintType: + """From our constraint type to OEF constraint type.""" + value = constraint_type.value + if constraint_type.type == ConstraintTypes.EQUAL: + return Eq(value) + elif constraint_type.type == ConstraintTypes.NOT_EQUAL: + return NotEq(value) + elif constraint_type.type == ConstraintTypes.LESS_THAN: + return Lt(value) + elif constraint_type.type == ConstraintTypes.LESS_THAN_EQ: + return LtEq(value) + elif constraint_type.type == ConstraintTypes.GREATER_THAN: + return Gt(value) + elif constraint_type.type == ConstraintTypes.GREATER_THAN_EQ: + return GtEq(value) + elif constraint_type.type == ConstraintTypes.WITHIN: + return Range(value) + elif constraint_type.type == ConstraintTypes.IN: + return In(value) + elif constraint_type.type == ConstraintTypes.NOT_IN: + return NotIn(value) + else: + raise ValueError("Constraint type not recognized.") + + @classmethod + def from_oef_description(cls, oef_desc: OEFDescription) -> Description: + """From an OEF description to our description.""" + data_model = cls.from_oef_data_model(oef_desc.data_model) if oef_desc.data_model is not None else None + return Description(oef_desc.values, data_model=data_model) + + @classmethod + def from_oef_data_model(cls, oef_data_model: OEFDataModel) -> DataModel: + """From an OEF data model to our data model.""" + attributes = [cls.from_oef_attribute(oef_attribute) for oef_attribute in oef_data_model.attribute_schemas] + return DataModel(oef_data_model.name, attributes, oef_data_model.description) + + @classmethod + def from_oef_attribute(cls, oef_attribute: OEFAttribute) -> Attribute: + """From an OEF attribute to our attribute.""" + return Attribute(oef_attribute.name, oef_attribute.type, oef_attribute.required, oef_attribute.description) + + @classmethod + def from_oef_query(cls, oef_query: OEFQuery) -> Query: + """From our query to OrOEF query.""" + data_model = cls.from_oef_data_model(oef_query.model) if oef_query.model is not None else None + constraints = [cls.from_oef_constraint_expr(c) for c in oef_query.constraints] + return Query(constraints, data_model) + + @classmethod + def from_oef_constraint_expr(cls, oef_constraint_expr: OEFConstraintExpr) -> ConstraintExpr: + """From our query to OEF query.""" + if isinstance(oef_constraint_expr, OEFAnd): + return And([cls.from_oef_constraint_expr(c) for c in oef_constraint_expr.constraints]) + elif isinstance(oef_constraint_expr, OEFOr): + return Or([cls.from_oef_constraint_expr(c) for c in oef_constraint_expr.constraints]) + elif isinstance(oef_constraint_expr, OEFNot): + return Not(cls.from_oef_constraint_expr(oef_constraint_expr.constraint)) + elif isinstance(oef_constraint_expr, OEFConstraint): + constraint_type = cls.from_oef_constraint_type(oef_constraint_expr.constraint) + return Constraint(oef_constraint_expr.attribute_name, constraint_type) + else: + raise ValueError("OEF Constraint not supported.") + + @classmethod + def from_oef_constraint_type(cls, constraint_type: OEFConstraintType) -> ConstraintType: + """From OEF constraint type to our constraint type.""" + if isinstance(constraint_type, Eq): + return ConstraintType(ConstraintTypes.EQUAL, constraint_type.value) + elif isinstance(constraint_type, NotEq): + return ConstraintType(ConstraintTypes.NOT_EQUAL, constraint_type.value) + elif isinstance(constraint_type, Lt): + return ConstraintType(ConstraintTypes.LESS_THAN, constraint_type.value) + elif isinstance(constraint_type, LtEq): + return ConstraintType(ConstraintTypes.LESS_THAN_EQ, constraint_type.value) + elif isinstance(constraint_type, Gt): + return ConstraintType(ConstraintTypes.GREATER_THAN, constraint_type.value) + elif isinstance(constraint_type, GtEq): + return ConstraintType(ConstraintTypes.GREATER_THAN_EQ, constraint_type.value) + elif isinstance(constraint_type, Range): + return ConstraintType(ConstraintTypes.WITHIN, constraint_type.values) + elif isinstance(constraint_type, In): + return ConstraintType(ConstraintTypes.IN, constraint_type.values) + elif isinstance(constraint_type, NotIn): + return ConstraintType(ConstraintTypes.NOT_IN, constraint_type.values) + else: + raise ValueError("Constraint type not recognized.") + + +class MailStats(object): + """The MailStats class tracks statistics on messages processed by MailBox.""" + + def __init__(self) -> None: + """ + Instantiate mail stats. + + :return: None + """ + self._search_count = 0 + self._search_start_time = {} # type: Dict[int, datetime.datetime] + self._search_timedelta = {} # type: Dict[int, float] + self._search_result_counts = {} # type: Dict[int, int] + + @property + def search_count(self) -> int: + """Get the search count.""" + return self._search_count + + def search_start(self, search_id: int) -> None: + """ + Add a search id and start time. + + :param search_id: the search id + + :return: None + """ + assert search_id not in self._search_start_time + self._search_count += 1 + self._search_start_time[search_id] = datetime.datetime.now() + + def search_end(self, search_id: int, nb_search_results: int) -> None: + """ + Add end time for a search id. + + :param search_id: the search id + :param nb_search_results: the number of agents returned in the search result + + :return: None + """ + assert search_id in self._search_start_time + assert search_id not in self._search_timedelta + self._search_timedelta[search_id] = (datetime.datetime.now() - self._search_start_time[search_id]).total_seconds() * 1000 + self._search_result_counts[search_id] = nb_search_results + + +class OEFChannel(OEFAgent, Channel): + """The OEFChannel connects the OEF Agent with the connection.""" + + def __init__(self, public_key: str, oef_addr: str, oef_port: int, core: AsyncioCore, in_queue: Queue): + """ + Initialize. + + :param public_key: the public key of the agent. + :param oef_addr: the OEF IP address. + :param oef_port: the OEF port. + :param in_queue: the in queue. + """ + super().__init__(public_key, oef_addr=oef_addr, oef_port=oef_port, core=core) + self.in_queue = in_queue + self.mail_stats = MailStats() + + def on_message(self, msg_id: int, dialogue_id: int, origin: str, content: bytes) -> None: + """ + On message event handler. + + :param msg_id: the message id. + :param dialogue_id: the dialogue id. + :param origin: the public key of the sender. + :param content: the bytes content. + :return: None + """ + # We are not using the 'origin' parameter because 'content' contains a serialized instance of 'Envelope', + # hence it already contains the address of the sender. + envelope = Envelope.decode(content) + self.in_queue.put(envelope) + + def on_cfp(self, msg_id: int, dialogue_id: int, origin: str, target: int, query: CFP_TYPES) -> None: + """ + On cfp event handler. + + :param msg_id: the message id. + :param dialogue_id: the dialogue id. + :param origin: the public key of the sender. + :param target: the message target. + :param query: the query. + :return: None + """ + try: + query = pickle.loads(query) + except Exception: + pass + msg = FIPAMessage(message_id=msg_id, + dialogue_id=dialogue_id, + target=target, + performative=FIPAMessage.Performative.CFP, + query=query if query != b"" else None) + msg_bytes = FIPASerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_propose(self, msg_id: int, dialogue_id: int, origin: str, target: int, b_proposals: PROPOSE_TYPES) -> None: + """ + On propose event handler. + + :param msg_id: the message id. + :param dialogue_id: the dialogue id. + :param origin: the public key of the sender. + :param target: the message target. + :param b_proposals: the proposals. + :return: None + """ + if type(b_proposals) == bytes: + proposals = pickle.loads(b_proposals) # type: List[Description] + else: + raise ValueError("No support for non-bytes proposals.") + + msg = FIPAMessage(message_id=msg_id, + dialogue_id=dialogue_id, + target=target, + performative=FIPAMessage.Performative.PROPOSE, + proposal=proposals) + msg_bytes = FIPASerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_accept(self, msg_id: int, dialogue_id: int, origin: str, target: int) -> None: + """ + On accept event handler. + + :param msg_id: the message id. + :param dialogue_id: the dialogue id. + :param origin: the public key of the sender. + :param target: the message target. + :return: None + """ + performative = FIPAMessage.Performative.MATCH_ACCEPT if msg_id == 4 and target == 3 else FIPAMessage.Performative.ACCEPT + msg = FIPAMessage(message_id=msg_id, + dialogue_id=dialogue_id, + target=target, + performative=performative) + msg_bytes = FIPASerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_decline(self, msg_id: int, dialogue_id: int, origin: str, target: int) -> None: + """ + On decline event handler. + + :param msg_id: the message id. + :param dialogue_id: the dialogue id. + :param origin: the public key of the sender. + :param target: the message target. + :return: None + """ + msg = FIPAMessage(message_id=msg_id, + dialogue_id=dialogue_id, + target=target, + performative=FIPAMessage.Performative.DECLINE) + msg_bytes = FIPASerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_search_result(self, search_id: int, agents: List[str]) -> None: + """ + On accept event handler. + + :param search_id: the search id. + :param agents: the list of agents. + :return: None + """ + self.mail_stats.search_end(search_id, len(agents)) + msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_RESULT, id=search_id, agents=agents) + msg_bytes = OEFSerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_oef_error(self, answer_id: int, operation: oef.messages.OEFErrorOperation) -> None: + """ + On oef error event handler. + + :param answer_id: the answer id. + :param operation: the error operation. + :return: None + """ + try: + operation = OEFMessage.OEFErrorOperation(operation) + except ValueError: + operation = OEFMessage.OEFErrorOperation.OTHER + + msg = OEFMessage(oef_type=OEFMessage.Type.OEF_ERROR, id=answer_id, operation=operation) + msg_bytes = OEFSerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_dialogue_error(self, answer_id: int, dialogue_id: int, origin: str) -> None: + """ + On dialogue error event handler. + + :param answer_id: the answer id. + :param dialogue_id: the dialogue id. + :param origin: the message sender. + :return: None + """ + msg = OEFMessage(oef_type=OEFMessage.Type.DIALOGUE_ERROR, + id=answer_id, + dialogue_id=dialogue_id, + origin=origin) + msg_bytes = OEFSerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def send(self, envelope: Envelope) -> None: + """ + Send message handler. + + :param envelope: the message. + :return: None + """ + if envelope.protocol_id == "default": + self.send_default_message(envelope) + elif envelope.protocol_id == "fipa": + self.send_fipa_message(envelope) + elif envelope.protocol_id == "oef": + self.send_oef_message(envelope) + elif envelope.protocol_id == "tac": + self.send_default_message(envelope) + else: + logger.error("This envelope cannot be sent: protocol_id={}".format(envelope.protocol_id)) + raise ValueError("Cannot send message.") + + def send_default_message(self, envelope: Envelope): + """Send a 'default' message.""" + self.send_message(STUB_MESSSAGE_ID, STUB_DIALOGUE_ID, envelope.to, envelope.encode()) + + def send_fipa_message(self, envelope: Envelope) -> None: + """ + Send fipa message handler. + + :param envelope: the message. + :return: None + """ + fipa_message = FIPASerializer().decode(envelope.message) + id = fipa_message.get("message_id") + dialogue_id = fipa_message.get("dialogue_id") + destination = envelope.to + target = fipa_message.get("target") + performative = FIPAMessage.Performative(fipa_message.get("performative")) + if performative == FIPAMessage.Performative.CFP: + query = fipa_message.get("query") + query = b"" if query is None else query + if type(query) == Query: + query = pickle.dumps(query) + self.send_cfp(id, dialogue_id, destination, target, query) + elif performative == FIPAMessage.Performative.PROPOSE: + proposal = cast(List[Description], fipa_message.get("proposal")) + proposal_b = pickle.dumps(proposal) # type: bytes + self.send_propose(id, dialogue_id, destination, target, proposal_b) + elif performative == FIPAMessage.Performative.ACCEPT: + self.send_accept(id, dialogue_id, destination, target) + elif performative == FIPAMessage.Performative.MATCH_ACCEPT: + self.send_accept(id, dialogue_id, destination, target) + elif performative == FIPAMessage.Performative.DECLINE: + self.send_decline(id, dialogue_id, destination, target) + else: + raise ValueError("OEF FIPA message not recognized.") + + def send_oef_message(self, envelope: Envelope) -> None: + """ + Send oef message handler. + + :param envelope: the message. + :return: None + """ + oef_message = OEFSerializer().decode(envelope.message) + oef_type = OEFMessage.Type(oef_message.get("type")) + oef_msg_id = cast(int, oef_message.get("id")) + if oef_type == OEFMessage.Type.REGISTER_SERVICE: + service_description = cast(Description, oef_message.get("service_description")) + service_id = cast(int, oef_message.get("service_id")) + oef_service_description = OEFObjectTranslator.to_oef_description(service_description) + self.register_service(oef_msg_id, oef_service_description, service_id) + elif oef_type == OEFMessage.Type.UNREGISTER_SERVICE: + service_description = cast(Description, oef_message.get("service_description")) + service_id = cast(int, oef_message.get("service_id")) + oef_service_description = OEFObjectTranslator.to_oef_description(service_description) + self.unregister_service(oef_msg_id, oef_service_description, service_id) + elif oef_type == OEFMessage.Type.SEARCH_AGENTS: + query = cast(Query, oef_message.get("query")) + oef_query = OEFObjectTranslator.to_oef_query(query) + self.search_agents(oef_msg_id, oef_query) + elif oef_type == OEFMessage.Type.SEARCH_SERVICES: + query = cast(Query, oef_message.get("query")) + oef_query = OEFObjectTranslator.to_oef_query(query) + self.mail_stats.search_start(oef_msg_id) + self.search_services(oef_msg_id, oef_query) + else: + raise ValueError("OEF request not recognized.") + + +class OEFConnection(Connection): + """The OEFConnection connects the to the mailbox.""" + + def __init__(self, public_key: str, oef_addr: str, oef_port: int = 10000): + """ + Initialize. + + :param public_key: the public key of the agent. + :param oef_addr: the OEF IP address. + :param oef_port: the OEF port. + """ + super().__init__() + core = AsyncioCore(logger=logger) + self._core = core # type: AsyncioCore + self.channel = OEFChannel(public_key, oef_addr, oef_port, core=core, in_queue=self.in_queue) + + self._stopped = True + self._connected = False + self.out_thread = None # type: Optional[Thread] + + @property + def is_established(self) -> bool: + """Get the connection status.""" + return self._connected + + def _fetch(self) -> None: + """ + Fetch the messages from the outqueue and send them. + + :return: None + """ + while self._connected: + try: + msg = self.out_queue.get(block=True, timeout=1.0) + self.send(msg) + except Empty: + pass + + def connect(self) -> None: + """ + Connect to the channel. + + :return: None + :raises ConnectionError if the connection to the OEF fails. + """ + if self._stopped and not self._connected: + self._stopped = False + self._core.run_threaded() + try: + if not self.channel.connect(): + raise ConnectionError("Cannot connect to OEFChannel.") + self._connected = True + self.out_thread = Thread(target=self._fetch) + self.out_thread.start() + except ConnectionError as e: + self._core.stop() + raise e + + def disconnect(self) -> None: + """ + Disconnect from the channel. + + :return: None + """ + assert self.out_thread is not None, "Call connect before disconnect." + if not self._stopped and self._connected: + self._connected = False + self.out_thread.join() + self.out_thread = None + self.channel.disconnect() + self._core.stop() + self._stopped = True + + def send(self, envelope: Envelope): + """ + Send messages. + + :return: None + """ + if self._connected: + self.channel.send(envelope) + + @classmethod + def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection': + """ + Get the OEF connection from the connection configuration. + + :param public_key: the public key of the agent. + :param connection_configuration: the connection configuration object. + :return: the connection object + """ + oef_addr = cast(str, connection_configuration.config.get("addr")) + oef_port = cast(int, connection_configuration.config.get("port")) + return OEFConnection(public_key, oef_addr, oef_port) + + +class OEFMailBox(MailBox): + """The OEF mail box.""" + + def __init__(self, public_key: str, oef_addr: str, oef_port: int = 10000): + """ + Initialize. + + :param public_key: the public key of the agent. + :param oef_addr: the OEF IP address. + :param oef_port: the OEF port. + """ + connection = OEFConnection(public_key, oef_addr, oef_port) + super().__init__(connection) + + @property + def mail_stats(self) -> MailStats: + """Get the mail stats object.""" + return self._connection.channel.mail_stats # type: ignore diff --git a/m_agent/connections/oef/connection.yaml b/m_agent/connections/oef/connection.yaml new file mode 100644 index 0000000000..774c2721c8 --- /dev/null +++ b/m_agent/connections/oef/connection.yaml @@ -0,0 +1,13 @@ +name: oef +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +class_name: OEFConnection +supported_protocols: ["oef"] +config: + addr: ${OEF_ADDR:127.0.0.1} + port: ${OEF_PORT:10000} +dependencies: + - colorlog + - oef \ No newline at end of file diff --git a/m_agent/default_private_key.pem b/m_agent/default_private_key.pem new file mode 100644 index 0000000000..79808c7224 --- /dev/null +++ b/m_agent/default_private_key.pem @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDCz6NgtmkEIocXBXJLFAFQCuhgzf+pTiaDbcdzq0wG9OiJpTUrdMbRJ +tTdIRzR812+gBwYFK4EEACKhZANiAARQlMkwlZash82ceV4TJPH6g+SENb42ocJP +ypEPsgRc4jQ3rsOEo8zmBnwueW3lj8VwRQdaW//mzWNtEA0e3f1zt2SpUFBdHJEs +5dWQGPosfxTA8VzTnrqFg+1jFZeY5Rc= +-----END EC PRIVATE KEY----- diff --git a/m_agent/eth_private_key.txt b/m_agent/eth_private_key.txt new file mode 100644 index 0000000000..dceb71fbe4 --- /dev/null +++ b/m_agent/eth_private_key.txt @@ -0,0 +1 @@ +0x1e91910c28a467f6a57f2d44409b2ee7d32fe194eae7b2084526cecd6fe3e753 \ No newline at end of file diff --git a/m_agent/fet_private_key.txt b/m_agent/fet_private_key.txt new file mode 100644 index 0000000000..966b91fd73 --- /dev/null +++ b/m_agent/fet_private_key.txt @@ -0,0 +1 @@ +6135b9666fe3078154a880d07c766b18f22fdc173b5cc8eba9e4805f95d39e05 \ No newline at end of file diff --git a/m_agent/protocols/__init__.py b/m_agent/protocols/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/m_agent/protocols/default/__init__.py b/m_agent/protocols/default/__init__.py new file mode 100644 index 0000000000..52e51b51e3 --- /dev/null +++ b/m_agent/protocols/default/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the support resources for the default protocol.""" diff --git a/m_agent/protocols/default/message.py b/m_agent/protocols/default/message.py new file mode 100644 index 0000000000..475714a2f0 --- /dev/null +++ b/m_agent/protocols/default/message.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the default message definition.""" +from enum import Enum +from typing import Optional + +from aea.protocols.base import Message + + +class DefaultMessage(Message): + """The Default message class.""" + + protocol_id = "default" + + class Type(Enum): + """Default message types.""" + + BYTES = "bytes" + ERROR = "error" + + def __str__(self): + """Get the string representation.""" + return self.value + + class ErrorCode(Enum): + """The error codes.""" + + UNSUPPORTED_PROTOCOL = -10001 + DECODING_ERROR = -10002 + INVALID_MESSAGE = -10003 + UNSUPPORTED_SKILL = -10004 + + def __init__(self, type: Optional[Type] = None, + **kwargs): + """ + Initialize. + + :param type: the type. + """ + super().__init__(type=type, **kwargs) + assert self.check_consistency(), "DefaultMessage initialization inconsistent." diff --git a/m_agent/protocols/default/protocol.yaml b/m_agent/protocols/default/protocol.yaml new file mode 100644 index 0000000000..6e9fd1dc97 --- /dev/null +++ b/m_agent/protocols/default/protocol.yaml @@ -0,0 +1,5 @@ +name: 'default' +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" diff --git a/m_agent/protocols/default/serialization.py b/m_agent/protocols/default/serialization.py new file mode 100644 index 0000000000..080b8f386b --- /dev/null +++ b/m_agent/protocols/default/serialization.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Serialization module for the default protocol.""" +import base64 +import json +from typing import cast + +from aea.protocols.base import Message +from aea.protocols.base import Serializer +from aea.protocols.default.message import DefaultMessage + + +class DefaultSerializer(Serializer): + """Serialization for the 'default' protocol.""" + + def encode(self, msg: Message) -> bytes: + """Encode a 'default' message into bytes.""" + body = {} # Dict[str, Any] + + msg_type = DefaultMessage.Type(msg.get("type")) + body["type"] = str(msg_type.value) + + if msg_type == DefaultMessage.Type.BYTES: + content = cast(bytes, msg.get("content")) + body["content"] = base64.b64encode(content).decode("utf-8") + elif msg_type == DefaultMessage.Type.ERROR: + body["error_code"] = cast(str, msg.get("error_code")) + body["error_msg"] = cast(str, msg.get("error_msg")) + body["error_data"] = cast(str, msg.get("error_data")) + else: + raise ValueError("Type not recognized.") + + bytes_msg = json.dumps(body).encode("utf-8") + return bytes_msg + + def decode(self, obj: bytes) -> Message: + """Decode bytes into a 'default' message.""" + json_body = json.loads(obj.decode("utf-8")) + body = {} + + msg_type = DefaultMessage.Type(json_body["type"]) + body["type"] = msg_type + if msg_type == DefaultMessage.Type.BYTES: + content = base64.b64decode(json_body["content"].encode("utf-8")) + body["content"] = content # type: ignore + elif msg_type == DefaultMessage.Type.ERROR: + body["error_code"] = json_body["error_code"] + body["error_msg"] = json_body["error_msg"] + body["error_data"] = json_body["error_data"] + else: + raise ValueError("Type not recognized.") + + return DefaultMessage(type=msg_type, body=body) diff --git a/m_agent/protocols/fipa/README.md b/m_agent/protocols/fipa/README.md new file mode 100644 index 0000000000..70b80ac297 --- /dev/null +++ b/m_agent/protocols/fipa/README.md @@ -0,0 +1,6 @@ +# fipa protocol + +## To update the `fipa_pb2.py` file + + cd aea/protocols/fipa + protoc --python_out=. fipa.proto \ No newline at end of file diff --git a/m_agent/protocols/fipa/__init__.py b/m_agent/protocols/fipa/__init__.py new file mode 100644 index 0000000000..88af132fb8 --- /dev/null +++ b/m_agent/protocols/fipa/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the support resources for the FIPA protocol.""" diff --git a/m_agent/protocols/fipa/fipa.proto b/m_agent/protocols/fipa/fipa.proto new file mode 100644 index 0000000000..98a387d176 --- /dev/null +++ b/m_agent/protocols/fipa/fipa.proto @@ -0,0 +1,48 @@ +syntax = "proto3"; + +package fetch.aea.fipa; + +message FIPAMessage{ + + message CFP{ + message Nothing { + } + oneof query{ + bytes bytes = 1; + Nothing nothing = 2; + bytes query_bytes = 3; + } + } + message Propose{ + repeated bytes proposal = 1; + } + message Accept{} + + message MatchAccept{} + + message Accept_W_Address{ + string address = 1; + } + + message MatchAccept_W_Address{ + string address = 1; + } + message Decline{} + message Inform{ + bytes bytes = 1; + } + + int32 message_id = 1; + int32 dialogue_id = 2; + int32 target = 3; + oneof performative{ + CFP cfp = 4; + Propose propose = 5; + Accept accept = 6; + MatchAccept match_accept = 7; + Decline decline = 8; + Inform inform = 9; + Accept_W_Address accept_w_address = 10; + MatchAccept_W_Address match_accept_w_address = 11; + } +} diff --git a/m_agent/protocols/fipa/fipa_pb2.py b/m_agent/protocols/fipa/fipa_pb2.py new file mode 100644 index 0000000000..5c4736d708 --- /dev/null +++ b/m_agent/protocols/fipa/fipa_pb2.py @@ -0,0 +1,525 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: fipa.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='fipa.proto', + package='fetch.aea.fipa', + syntax='proto3', + serialized_options=None, + serialized_pb=_b('\n\nfipa.proto\x12\x0e\x66\x65tch.aea.fipa\"\xea\x06\n\x0b\x46IPAMessage\x12\x12\n\nmessage_id\x18\x01 \x01(\x05\x12\x13\n\x0b\x64ialogue_id\x18\x02 \x01(\x05\x12\x0e\n\x06target\x18\x03 \x01(\x05\x12.\n\x03\x63\x66p\x18\x04 \x01(\x0b\x32\x1f.fetch.aea.fipa.FIPAMessage.CFPH\x00\x12\x36\n\x07propose\x18\x05 \x01(\x0b\x32#.fetch.aea.fipa.FIPAMessage.ProposeH\x00\x12\x34\n\x06\x61\x63\x63\x65pt\x18\x06 \x01(\x0b\x32\".fetch.aea.fipa.FIPAMessage.AcceptH\x00\x12?\n\x0cmatch_accept\x18\x07 \x01(\x0b\x32\'.fetch.aea.fipa.FIPAMessage.MatchAcceptH\x00\x12\x36\n\x07\x64\x65\x63line\x18\x08 \x01(\x0b\x32#.fetch.aea.fipa.FIPAMessage.DeclineH\x00\x12\x34\n\x06inform\x18\t \x01(\x0b\x32\".fetch.aea.fipa.FIPAMessage.InformH\x00\x12H\n\x10\x61\x63\x63\x65pt_w_address\x18\n \x01(\x0b\x32,.fetch.aea.fipa.FIPAMessage.Accept_W_AddressH\x00\x12S\n\x16match_accept_w_address\x18\x0b \x01(\x0b\x32\x31.fetch.aea.fipa.FIPAMessage.MatchAccept_W_AddressH\x00\x1a}\n\x03\x43\x46P\x12\x0f\n\x05\x62ytes\x18\x01 \x01(\x0cH\x00\x12:\n\x07nothing\x18\x02 \x01(\x0b\x32\'.fetch.aea.fipa.FIPAMessage.CFP.NothingH\x00\x12\x15\n\x0bquery_bytes\x18\x03 \x01(\x0cH\x00\x1a\t\n\x07NothingB\x07\n\x05query\x1a\x1b\n\x07Propose\x12\x10\n\x08proposal\x18\x01 \x03(\x0c\x1a\x08\n\x06\x41\x63\x63\x65pt\x1a\r\n\x0bMatchAccept\x1a#\n\x10\x41\x63\x63\x65pt_W_Address\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x1a(\n\x15MatchAccept_W_Address\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x1a\t\n\x07\x44\x65\x63line\x1a\x17\n\x06Inform\x12\r\n\x05\x62ytes\x18\x01 \x01(\x0c\x42\x0e\n\x0cperformativeb\x06proto3') +) + + + + +_FIPAMESSAGE_CFP_NOTHING = _descriptor.Descriptor( + name='Nothing', + full_name='fetch.aea.fipa.FIPAMessage.CFP.Nothing', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=702, + serialized_end=711, +) + +_FIPAMESSAGE_CFP = _descriptor.Descriptor( + name='CFP', + full_name='fetch.aea.fipa.FIPAMessage.CFP', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='bytes', full_name='fetch.aea.fipa.FIPAMessage.CFP.bytes', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='nothing', full_name='fetch.aea.fipa.FIPAMessage.CFP.nothing', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='query_bytes', full_name='fetch.aea.fipa.FIPAMessage.CFP.query_bytes', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_FIPAMESSAGE_CFP_NOTHING, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='query', full_name='fetch.aea.fipa.FIPAMessage.CFP.query', + index=0, containing_type=None, fields=[]), + ], + serialized_start=595, + serialized_end=720, +) + +_FIPAMESSAGE_PROPOSE = _descriptor.Descriptor( + name='Propose', + full_name='fetch.aea.fipa.FIPAMessage.Propose', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='proposal', full_name='fetch.aea.fipa.FIPAMessage.Propose.proposal', index=0, + number=1, type=12, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=722, + serialized_end=749, +) + +_FIPAMESSAGE_ACCEPT = _descriptor.Descriptor( + name='Accept', + full_name='fetch.aea.fipa.FIPAMessage.Accept', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=751, + serialized_end=759, +) + +_FIPAMESSAGE_MATCHACCEPT = _descriptor.Descriptor( + name='MatchAccept', + full_name='fetch.aea.fipa.FIPAMessage.MatchAccept', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=761, + serialized_end=774, +) + +_FIPAMESSAGE_ACCEPT_W_ADDRESS = _descriptor.Descriptor( + name='Accept_W_Address', + full_name='fetch.aea.fipa.FIPAMessage.Accept_W_Address', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='address', full_name='fetch.aea.fipa.FIPAMessage.Accept_W_Address.address', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=776, + serialized_end=811, +) + +_FIPAMESSAGE_MATCHACCEPT_W_ADDRESS = _descriptor.Descriptor( + name='MatchAccept_W_Address', + full_name='fetch.aea.fipa.FIPAMessage.MatchAccept_W_Address', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='address', full_name='fetch.aea.fipa.FIPAMessage.MatchAccept_W_Address.address', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=813, + serialized_end=853, +) + +_FIPAMESSAGE_DECLINE = _descriptor.Descriptor( + name='Decline', + full_name='fetch.aea.fipa.FIPAMessage.Decline', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=855, + serialized_end=864, +) + +_FIPAMESSAGE_INFORM = _descriptor.Descriptor( + name='Inform', + full_name='fetch.aea.fipa.FIPAMessage.Inform', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='bytes', full_name='fetch.aea.fipa.FIPAMessage.Inform.bytes', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=866, + serialized_end=889, +) + +_FIPAMESSAGE = _descriptor.Descriptor( + name='FIPAMessage', + full_name='fetch.aea.fipa.FIPAMessage', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='message_id', full_name='fetch.aea.fipa.FIPAMessage.message_id', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='dialogue_id', full_name='fetch.aea.fipa.FIPAMessage.dialogue_id', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='target', full_name='fetch.aea.fipa.FIPAMessage.target', index=2, + number=3, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='cfp', full_name='fetch.aea.fipa.FIPAMessage.cfp', index=3, + number=4, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='propose', full_name='fetch.aea.fipa.FIPAMessage.propose', index=4, + number=5, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='accept', full_name='fetch.aea.fipa.FIPAMessage.accept', index=5, + number=6, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='match_accept', full_name='fetch.aea.fipa.FIPAMessage.match_accept', index=6, + number=7, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='decline', full_name='fetch.aea.fipa.FIPAMessage.decline', index=7, + number=8, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='inform', full_name='fetch.aea.fipa.FIPAMessage.inform', index=8, + number=9, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='accept_w_address', full_name='fetch.aea.fipa.FIPAMessage.accept_w_address', index=9, + number=10, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='match_accept_w_address', full_name='fetch.aea.fipa.FIPAMessage.match_accept_w_address', index=10, + number=11, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_FIPAMESSAGE_CFP, _FIPAMESSAGE_PROPOSE, _FIPAMESSAGE_ACCEPT, _FIPAMESSAGE_MATCHACCEPT, _FIPAMESSAGE_ACCEPT_W_ADDRESS, _FIPAMESSAGE_MATCHACCEPT_W_ADDRESS, _FIPAMESSAGE_DECLINE, _FIPAMESSAGE_INFORM, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='performative', full_name='fetch.aea.fipa.FIPAMessage.performative', + index=0, containing_type=None, fields=[]), + ], + serialized_start=31, + serialized_end=905, +) + +_FIPAMESSAGE_CFP_NOTHING.containing_type = _FIPAMESSAGE_CFP +_FIPAMESSAGE_CFP.fields_by_name['nothing'].message_type = _FIPAMESSAGE_CFP_NOTHING +_FIPAMESSAGE_CFP.containing_type = _FIPAMESSAGE +_FIPAMESSAGE_CFP.oneofs_by_name['query'].fields.append( + _FIPAMESSAGE_CFP.fields_by_name['bytes']) +_FIPAMESSAGE_CFP.fields_by_name['bytes'].containing_oneof = _FIPAMESSAGE_CFP.oneofs_by_name['query'] +_FIPAMESSAGE_CFP.oneofs_by_name['query'].fields.append( + _FIPAMESSAGE_CFP.fields_by_name['nothing']) +_FIPAMESSAGE_CFP.fields_by_name['nothing'].containing_oneof = _FIPAMESSAGE_CFP.oneofs_by_name['query'] +_FIPAMESSAGE_CFP.oneofs_by_name['query'].fields.append( + _FIPAMESSAGE_CFP.fields_by_name['query_bytes']) +_FIPAMESSAGE_CFP.fields_by_name['query_bytes'].containing_oneof = _FIPAMESSAGE_CFP.oneofs_by_name['query'] +_FIPAMESSAGE_PROPOSE.containing_type = _FIPAMESSAGE +_FIPAMESSAGE_ACCEPT.containing_type = _FIPAMESSAGE +_FIPAMESSAGE_MATCHACCEPT.containing_type = _FIPAMESSAGE +_FIPAMESSAGE_ACCEPT_W_ADDRESS.containing_type = _FIPAMESSAGE +_FIPAMESSAGE_MATCHACCEPT_W_ADDRESS.containing_type = _FIPAMESSAGE +_FIPAMESSAGE_DECLINE.containing_type = _FIPAMESSAGE +_FIPAMESSAGE_INFORM.containing_type = _FIPAMESSAGE +_FIPAMESSAGE.fields_by_name['cfp'].message_type = _FIPAMESSAGE_CFP +_FIPAMESSAGE.fields_by_name['propose'].message_type = _FIPAMESSAGE_PROPOSE +_FIPAMESSAGE.fields_by_name['accept'].message_type = _FIPAMESSAGE_ACCEPT +_FIPAMESSAGE.fields_by_name['match_accept'].message_type = _FIPAMESSAGE_MATCHACCEPT +_FIPAMESSAGE.fields_by_name['decline'].message_type = _FIPAMESSAGE_DECLINE +_FIPAMESSAGE.fields_by_name['inform'].message_type = _FIPAMESSAGE_INFORM +_FIPAMESSAGE.fields_by_name['accept_w_address'].message_type = _FIPAMESSAGE_ACCEPT_W_ADDRESS +_FIPAMESSAGE.fields_by_name['match_accept_w_address'].message_type = _FIPAMESSAGE_MATCHACCEPT_W_ADDRESS +_FIPAMESSAGE.oneofs_by_name['performative'].fields.append( + _FIPAMESSAGE.fields_by_name['cfp']) +_FIPAMESSAGE.fields_by_name['cfp'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] +_FIPAMESSAGE.oneofs_by_name['performative'].fields.append( + _FIPAMESSAGE.fields_by_name['propose']) +_FIPAMESSAGE.fields_by_name['propose'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] +_FIPAMESSAGE.oneofs_by_name['performative'].fields.append( + _FIPAMESSAGE.fields_by_name['accept']) +_FIPAMESSAGE.fields_by_name['accept'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] +_FIPAMESSAGE.oneofs_by_name['performative'].fields.append( + _FIPAMESSAGE.fields_by_name['match_accept']) +_FIPAMESSAGE.fields_by_name['match_accept'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] +_FIPAMESSAGE.oneofs_by_name['performative'].fields.append( + _FIPAMESSAGE.fields_by_name['decline']) +_FIPAMESSAGE.fields_by_name['decline'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] +_FIPAMESSAGE.oneofs_by_name['performative'].fields.append( + _FIPAMESSAGE.fields_by_name['inform']) +_FIPAMESSAGE.fields_by_name['inform'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] +_FIPAMESSAGE.oneofs_by_name['performative'].fields.append( + _FIPAMESSAGE.fields_by_name['accept_w_address']) +_FIPAMESSAGE.fields_by_name['accept_w_address'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] +_FIPAMESSAGE.oneofs_by_name['performative'].fields.append( + _FIPAMESSAGE.fields_by_name['match_accept_w_address']) +_FIPAMESSAGE.fields_by_name['match_accept_w_address'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] +DESCRIPTOR.message_types_by_name['FIPAMessage'] = _FIPAMESSAGE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +FIPAMessage = _reflection.GeneratedProtocolMessageType('FIPAMessage', (_message.Message,), dict( + + CFP = _reflection.GeneratedProtocolMessageType('CFP', (_message.Message,), dict( + + Nothing = _reflection.GeneratedProtocolMessageType('Nothing', (_message.Message,), dict( + DESCRIPTOR = _FIPAMESSAGE_CFP_NOTHING, + __module__ = 'fipa_pb2' + # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.CFP.Nothing) + )) + , + DESCRIPTOR = _FIPAMESSAGE_CFP, + __module__ = 'fipa_pb2' + # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.CFP) + )) + , + + Propose = _reflection.GeneratedProtocolMessageType('Propose', (_message.Message,), dict( + DESCRIPTOR = _FIPAMESSAGE_PROPOSE, + __module__ = 'fipa_pb2' + # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.Propose) + )) + , + + Accept = _reflection.GeneratedProtocolMessageType('Accept', (_message.Message,), dict( + DESCRIPTOR = _FIPAMESSAGE_ACCEPT, + __module__ = 'fipa_pb2' + # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.Accept) + )) + , + + MatchAccept = _reflection.GeneratedProtocolMessageType('MatchAccept', (_message.Message,), dict( + DESCRIPTOR = _FIPAMESSAGE_MATCHACCEPT, + __module__ = 'fipa_pb2' + # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.MatchAccept) + )) + , + + Accept_W_Address = _reflection.GeneratedProtocolMessageType('Accept_W_Address', (_message.Message,), dict( + DESCRIPTOR = _FIPAMESSAGE_ACCEPT_W_ADDRESS, + __module__ = 'fipa_pb2' + # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.Accept_W_Address) + )) + , + + MatchAccept_W_Address = _reflection.GeneratedProtocolMessageType('MatchAccept_W_Address', (_message.Message,), dict( + DESCRIPTOR = _FIPAMESSAGE_MATCHACCEPT_W_ADDRESS, + __module__ = 'fipa_pb2' + # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.MatchAccept_W_Address) + )) + , + + Decline = _reflection.GeneratedProtocolMessageType('Decline', (_message.Message,), dict( + DESCRIPTOR = _FIPAMESSAGE_DECLINE, + __module__ = 'fipa_pb2' + # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.Decline) + )) + , + + Inform = _reflection.GeneratedProtocolMessageType('Inform', (_message.Message,), dict( + DESCRIPTOR = _FIPAMESSAGE_INFORM, + __module__ = 'fipa_pb2' + # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.Inform) + )) + , + DESCRIPTOR = _FIPAMESSAGE, + __module__ = 'fipa_pb2' + # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage) + )) +_sym_db.RegisterMessage(FIPAMessage) +_sym_db.RegisterMessage(FIPAMessage.CFP) +_sym_db.RegisterMessage(FIPAMessage.CFP.Nothing) +_sym_db.RegisterMessage(FIPAMessage.Propose) +_sym_db.RegisterMessage(FIPAMessage.Accept) +_sym_db.RegisterMessage(FIPAMessage.MatchAccept) +_sym_db.RegisterMessage(FIPAMessage.Accept_W_Address) +_sym_db.RegisterMessage(FIPAMessage.MatchAccept_W_Address) +_sym_db.RegisterMessage(FIPAMessage.Decline) +_sym_db.RegisterMessage(FIPAMessage.Inform) + + +# @@protoc_insertion_point(module_scope) diff --git a/m_agent/protocols/fipa/message.py b/m_agent/protocols/fipa/message.py new file mode 100644 index 0000000000..d776c12aeb --- /dev/null +++ b/m_agent/protocols/fipa/message.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the FIPA message definition.""" +from enum import Enum +from typing import Optional, Union + +from aea.protocols.base import Message +from aea.protocols.oef.models import Description, Query + + +class FIPAMessage(Message): + """The FIPA message class.""" + + protocol_id = "fipa" + + class Performative(Enum): + """FIPA performatives.""" + + CFP = "cfp" + PROPOSE = "propose" + ACCEPT = "accept" + MATCH_ACCEPT = "match_accept" + DECLINE = "decline" + INFORM = "inform" + ACCEPT_W_ADDRESS = "accept_w_address" + MATCH_ACCEPT_W_ADDRESS = "match_accept_w_address" + + def __str__(self): + """Get string representation.""" + return self.value + + def __init__(self, message_id: Optional[int] = None, + dialogue_id: Optional[int] = None, + target: Optional[int] = None, + performative: Optional[Union[str, Performative]] = None, + **kwargs): + """ + Initialize. + + :param message_id: the message id. + :param dialogue_id: the dialogue id. + :param target: the message target. + :param performative: the message performative. + """ + super().__init__(message_id=message_id, + dialogue_id=dialogue_id, + target=target, + performative=FIPAMessage.Performative(performative), + **kwargs) + assert self.check_consistency(), "FIPAMessage initialization inconsistent." + + def check_consistency(self) -> bool: + """Check that the data is consistent.""" + try: + assert self.is_set("message_id") + assert self.is_set("dialogue_id") + assert self.is_set("target") + performative = FIPAMessage.Performative(self.get("performative")) + if performative == FIPAMessage.Performative.CFP: + query = self.get("query") + assert isinstance(query, Query) or isinstance(query, bytes) or query is None + elif performative == FIPAMessage.Performative.PROPOSE: + proposal = self.get("proposal") + assert type(proposal) == list and all(isinstance(d, Description) or type(d) == bytes for d in proposal) # type: ignore + elif performative == FIPAMessage.Performative.ACCEPT \ + or performative == FIPAMessage.Performative.MATCH_ACCEPT \ + or performative == FIPAMessage.Performative.DECLINE: + pass # pragma: no cover + elif performative == FIPAMessage.Performative.ACCEPT_W_ADDRESS\ + or performative == FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS: + assert self.is_set("address") + elif performative == FIPAMessage.Performative.INFORM: + data = self.get("data") + assert isinstance(data, bytes) + else: + raise ValueError("Performative not recognized.") + + except (AssertionError, ValueError, KeyError): + return False + + return True diff --git a/m_agent/protocols/fipa/protocol.yaml b/m_agent/protocols/fipa/protocol.yaml new file mode 100644 index 0000000000..a92f5d07fd --- /dev/null +++ b/m_agent/protocols/fipa/protocol.yaml @@ -0,0 +1,7 @@ +name: 'fipa' +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +dependencies: + - protobuf diff --git a/m_agent/protocols/fipa/serialization.py b/m_agent/protocols/fipa/serialization.py new file mode 100644 index 0000000000..ee300b64ea --- /dev/null +++ b/m_agent/protocols/fipa/serialization.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Serialization for the FIPA protocol.""" +import pickle +from typing import cast + +from aea.protocols.base import Message +from aea.protocols.base import Serializer +from aea.protocols.fipa import fipa_pb2 +from aea.protocols.fipa.message import FIPAMessage +from aea.protocols.oef.models import Description, Query + + +class FIPASerializer(Serializer): + """Serialization for the FIPA protocol.""" + + def encode(self, msg: Message) -> bytes: + """Encode a FIPA message into bytes.""" + fipa_msg = fipa_pb2.FIPAMessage() + fipa_msg.message_id = msg.get("message_id") + fipa_msg.dialogue_id = msg.get("dialogue_id") + fipa_msg.target = msg.get("target") + + performative_id = FIPAMessage.Performative(msg.get("performative")) + if performative_id == FIPAMessage.Performative.CFP: + performative = fipa_pb2.FIPAMessage.CFP() # type: ignore + query = msg.get("query") + if query is None or query == b"": + nothing = fipa_pb2.FIPAMessage.CFP.Nothing() # type: ignore + performative.nothing.CopyFrom(nothing) + elif type(query) == Query: + query = pickle.dumps(query) + performative.query_bytes = query + elif type(query) == bytes: + performative.bytes = query + else: + raise ValueError("Query type not supported: {}".format(type(query))) + fipa_msg.cfp.CopyFrom(performative) + elif performative_id == FIPAMessage.Performative.PROPOSE: + performative = fipa_pb2.FIPAMessage.Propose() # type: ignore + proposal = cast(Description, msg.get("proposal")) + p_array_bytes = [pickle.dumps(p) for p in proposal] + performative.proposal.extend(p_array_bytes) + fipa_msg.propose.CopyFrom(performative) + elif performative_id == FIPAMessage.Performative.ACCEPT: + performative = fipa_pb2.FIPAMessage.Accept() # type: ignore + fipa_msg.accept.CopyFrom(performative) + elif performative_id == FIPAMessage.Performative.MATCH_ACCEPT: + performative = fipa_pb2.FIPAMessage.MatchAccept() # type: ignore + fipa_msg.match_accept.CopyFrom(performative) + elif performative_id == FIPAMessage.Performative.ACCEPT_W_ADDRESS: + performative = fipa_pb2.FIPAMessage.Accept_W_Address() # type: ignore + address = msg.get("address") + if type(address) == str: + performative.address = address + fipa_msg.accept_w_address.CopyFrom(performative) + elif performative_id == FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS: + performative = fipa_pb2.FIPAMessage.MatchAccept_W_Address() # type: ignore + address = msg.get("address") + if type(address) == str: + performative.address = address + fipa_msg.match_accept_w_address.CopyFrom(performative) + elif performative_id == FIPAMessage.Performative.DECLINE: + performative = fipa_pb2.FIPAMessage.Decline() # type: ignore + fipa_msg.decline.CopyFrom(performative) + elif performative_id == FIPAMessage.Performative.INFORM: + performative = fipa_pb2.FIPAMessage.Inform() # type: ignore + data = msg.get("data") + data_bytes = pickle.dumps(data) + performative.bytes = data_bytes + fipa_msg.inform.CopyFrom(performative) + else: + raise ValueError("Performative not valid: {}".format(performative_id)) + + fipa_bytes = fipa_msg.SerializeToString() + return fipa_bytes + + def decode(self, obj: bytes) -> Message: + """Decode bytes into a FIPA message.""" + fipa_pb = fipa_pb2.FIPAMessage() + fipa_pb.ParseFromString(obj) + message_id = fipa_pb.message_id + dialogue_id = fipa_pb.dialogue_id + target = fipa_pb.target + + performative = fipa_pb.WhichOneof("performative") + performative_id = FIPAMessage.Performative(str(performative)) + performative_content = dict() + if performative_id == FIPAMessage.Performative.CFP: + query_type = fipa_pb.cfp.WhichOneof("query") + if query_type == "nothing": + query = None + elif query_type == "query_bytes": + query = pickle.loads(fipa_pb.cfp.query_bytes) + elif query_type == "bytes": + query = fipa_pb.cfp.bytes + else: + raise ValueError("Query type not recognized.") + performative_content["query"] = query + elif performative_id == FIPAMessage.Performative.PROPOSE: + descriptions = [] + for p_bytes in fipa_pb.propose.proposal: + p = pickle.loads(p_bytes) # type: Description + descriptions.append(p) + performative_content["proposal"] = descriptions + elif performative_id == FIPAMessage.Performative.ACCEPT: + pass + elif performative_id == FIPAMessage.Performative.MATCH_ACCEPT: + pass + elif performative_id == FIPAMessage.Performative.ACCEPT_W_ADDRESS: + address = fipa_pb.accept_w_address.address + performative_content['address'] = address + elif performative_id == FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS: + address = fipa_pb.match_accept_w_address.address + performative_content['address'] = address + elif performative_id == FIPAMessage.Performative.DECLINE: + pass + elif performative_id == FIPAMessage.Performative.INFORM: + data = pickle.loads(fipa_pb.inform.bytes) + performative_content["data"] = data + else: + raise ValueError("Performative not valid: {}.".format(performative)) + + return FIPAMessage(message_id=message_id, dialogue_id=dialogue_id, target=target, + performative=performative, **performative_content) diff --git a/m_agent/skills/__init__.py b/m_agent/skills/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/m_agent/skills/error/__init__.py b/m_agent/skills/error/__init__.py new file mode 100644 index 0000000000..96c80ac32c --- /dev/null +++ b/m_agent/skills/error/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the implementation of the error skill.""" diff --git a/m_agent/skills/error/behaviours.py b/m_agent/skills/error/behaviours.py new file mode 100644 index 0000000000..556ee98ca7 --- /dev/null +++ b/m_agent/skills/error/behaviours.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains the error behaviours.""" + +from aea.skills.base import Behaviour + + +class ErrorBehaviour(Behaviour): + """This class implements the error behaviour.""" + + def setup(self) -> None: + """ + Implement the setup. + + :return: None + """ + pass + + def act(self) -> None: + """ + Implement the act. + + :return: None + """ + pass + + def teardown(self) -> None: + """ + Implement the task teardown. + + :return: None + """ + pass diff --git a/m_agent/skills/error/handlers.py b/m_agent/skills/error/handlers.py new file mode 100644 index 0000000000..098a61eced --- /dev/null +++ b/m_agent/skills/error/handlers.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains the implementation of the handler for the 'default' protocol.""" +import base64 +import logging +from typing import Optional + +from aea.configurations.base import ProtocolId +from aea.mail.base import Envelope +from aea.protocols.base import Message, Protocol +from aea.protocols.default.message import DefaultMessage +from aea.protocols.default.serialization import DefaultSerializer +from aea.skills.base import Handler + +logger = logging.getLogger(__name__) + + +class ErrorHandler(Handler): + """This class implements the error handler.""" + + SUPPORTED_PROTOCOL = 'default' # type: Optional[ProtocolId] + + def setup(self) -> None: + """ + Implement the setup. + + :return: None + """ + pass + + def handle(self, message: Message, sender: str) -> None: + """ + Implement the reaction to an envelope. + + :param message: the message + :param sender: the sender + """ + pass + + def teardown(self) -> None: + """ + Implement the handler teardown. + + :return: None + """ + pass + + def send_unsupported_protocol(self, envelope: Envelope) -> None: + """ + Handle the received envelope in case the protocol is not supported. + + :param envelope: the envelope + :return: None + """ + logger.warning("Unsupported protocol: {}".format(envelope.protocol_id)) + reply = DefaultMessage(type=DefaultMessage.Type.ERROR, + error_code=DefaultMessage.ErrorCode.UNSUPPORTED_PROTOCOL.value, + error_msg="Unsupported protocol.", + error_data={"protocol_id": envelope.protocol_id}) + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, + message=DefaultSerializer().encode(reply)) + + def send_decoding_error(self, envelope: Envelope) -> None: + """ + Handle a decoding error. + + :param envelope: the envelope + :return: None + """ + logger.warning("Decoding error: {}.".format(envelope)) + encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") + reply = DefaultMessage(type=DefaultMessage.Type.ERROR, + error_code=DefaultMessage.ErrorCode.DECODING_ERROR.value, + error_msg="Decoding error.", + error_data={"envelope": encoded_envelope}) + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, + message=DefaultSerializer().encode(reply)) + + def send_invalid_message(self, envelope: Envelope) -> None: + """ + Handle an message that is invalid wrt a protocol. + + :param envelope: the envelope + :return: None + """ + logger.warning("Invalid message wrt protocol: {}.".format(envelope.protocol_id)) + encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") + reply = DefaultMessage(type=DefaultMessage.Type.ERROR, + error_code=DefaultMessage.ErrorCode.INVALID_MESSAGE.value, + error_msg="Invalid message.", + error_data={"envelope": encoded_envelope}) + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, + message=DefaultSerializer().encode(reply)) + + def send_unsupported_skill(self, envelope: Envelope, protocol: Protocol) -> None: + """ + Handle the received envelope in case the skill is not supported. + + :param envelope: the envelope + :param protocol: the protocol + :return: None + """ + logger.warning("Cannot handle envelope: no handler registered for the protocol '{}'.".format(protocol.id)) + encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") + reply = DefaultMessage(type=DefaultMessage.Type.ERROR, + error_code=DefaultMessage.ErrorCode.UNSUPPORTED_SKILL.value, + error_msg="Unsupported skill.", + error_data={"envelope": encoded_envelope}) + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, + message=DefaultSerializer().encode(reply)) diff --git a/m_agent/skills/error/skill.yaml b/m_agent/skills/error/skill.yaml new file mode 100644 index 0000000000..70e9ceda1d --- /dev/null +++ b/m_agent/skills/error/skill.yaml @@ -0,0 +1,14 @@ +name: error +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +behaviours: [] +handlers: + - handler: + class_name: ErrorHandler + args: + foo: bar +tasks: [] +shared_classes: [] +protocols: ['default'] diff --git a/m_agent/skills/error/tasks.py b/m_agent/skills/error/tasks.py new file mode 100644 index 0000000000..8922217537 --- /dev/null +++ b/m_agent/skills/error/tasks.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains the implementation of the error tasks.""" + +from aea.skills.base import Task + + +class ErrorTask(Task): + """This class implements the error task.""" + + def setup(self) -> None: + """ + Implement the setup. + + :return: None + """ + pass + + def execute(self) -> None: + """ + Implement the task execution. + + :param envelope: the envelope + :return: None + """ + pass + + def teardown(self) -> None: + """ + Implement the task teardown. + + :return: None + """ + pass diff --git a/m_agent/skills/weather_station/__init__.py b/m_agent/skills/weather_station/__init__.py new file mode 100644 index 0000000000..81d567366d --- /dev/null +++ b/m_agent/skills/weather_station/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the implementation of the default skill.""" diff --git a/m_agent/skills/weather_station/behaviours.py b/m_agent/skills/weather_station/behaviours.py new file mode 100644 index 0000000000..4bce9b9bce --- /dev/null +++ b/m_agent/skills/weather_station/behaviours.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains a scaffold of a behaviour.""" + +import logging + +from aea.skills.base import Behaviour +from typing import TYPE_CHECKING +from aea.protocols.oef.models import Description +from aea.protocols.oef.message import OEFMessage +from aea.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF + +if TYPE_CHECKING: + from packages.skills.weather_station.weather_station_data_model import WEATHER_STATION_DATAMODEL, SCHEME, SERVICE_ID +else: + from weather_station_skill.weather_station_data_model import WEATHER_STATION_DATAMODEL, SCHEME, SERVICE_ID + +logger = logging.getLogger("aea.weather_station_skill") + +REGISTER_ID = 1 + + +class MyWeatherBehaviour(Behaviour): + """This class scaffolds a behaviour.""" + + def __init__(self, **kwargs): + """Initialise the behaviour.""" + super().__init__(**kwargs) + self.registered = False + self.data_model = WEATHER_STATION_DATAMODEL() + self.scheme = SCHEME + + def setup(self) -> None: + """ + Implement the setup. + + :return: None + """ + print(self.context.agent_addresses) + + def act(self) -> None: + """ + Implement the act. + + :return: None + """ + if not self.registered: + desc = Description(self.scheme, data_model=self.data_model) + msg = OEFMessage(oef_type=OEFMessage.Type.REGISTER_SERVICE, + id=REGISTER_ID, + service_description=desc, + service_id=SERVICE_ID) + msg_bytes = OEFSerializer().encode(msg) + self.context.outbox.put_message(to=DEFAULT_OEF, + sender=self.context.agent_public_key, + protocol_id=OEFMessage.protocol_id, + message=msg_bytes) + logger.info("[{}]: registered! My public key is : {}".format(self.context.agent_name, self.context.agent_public_key)) + self.registered = True + + def teardown(self) -> None: + """ + Implement the task teardown. + + :return: None + """ + pass diff --git a/m_agent/skills/weather_station/db_communication.py b/m_agent/skills/weather_station/db_communication.py new file mode 100644 index 0000000000..2dfe79061a --- /dev/null +++ b/m_agent/skills/weather_station/db_communication.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains the Database Communication for the weather agent.""" + +import datetime +import os.path +import sqlite3 +from typing import Dict, cast + +my_path = os.path.dirname(__file__) + +DB_SOURCE = os.path.join(my_path, 'dummy_weather_station_data.db') + + +class DBCommunication: + """A class to communicate with a database.""" + + def __init__(self): + """ + Initialize the database communication. + + :param source: the source + """ + self.source = DB_SOURCE + + def db_connection(self) -> sqlite3.Connection: + """ + Get db connection. + + :return: the db connection + """ + con = sqlite3.connect(self.source) + return con + + def get_data_for_specific_dates(self, start_date: str, end_date: str) -> Dict[str, int]: + """ + Get data for specific dates. + + :param start_date: the start date + :param end_date: the end date + :return: the data + """ + con = self.db_connection() + cur = con.cursor() + start_dt = datetime.datetime.strptime(start_date, '%d/%m/%Y') + start = start_dt.strftime('%s') + end_dt = datetime.datetime.strptime(end_date, '%d/%m/%Y') + end = end_dt.strftime('%s') + cur.execute("SELECT * FROM data WHERE idx BETWEEN ? AND ?", (str(start), str(end))) + data = cast(Dict[str, int], cur.fetchall()) + cur.close() + con.close() + return data diff --git a/m_agent/skills/weather_station/dummy_weather_station_data.db b/m_agent/skills/weather_station/dummy_weather_station_data.db new file mode 100644 index 0000000000000000000000000000000000000000..ff0d00d99ac64c48c8e6ec126477e1013c67f479 GIT binary patch literal 8192 zcmeH~J8UCG7{|Ty+J|@T#Ez4T@3_p^@jAKZI^J=dyByqu_<0U}4 zUL`^ZiGuPPS_%pzD368~DkvzAf)*MoIugEhGP9fNauO2Fjy$rR{pFi){@?%mqTIc` zzC9Z9cOLIN9E^A~Gm$Zj%yrH)nanC|1Y0cwJ`nW<-lOj?TC15$XTJ4-GQ3|h@Jd&z zK&n8hK&n8hK&n8hK&n8hK&n8hK&rrhq`)T^4RfYcGCsRD8r*v@+!~Aq(MGrKtlijH z;~O__uCMXvCw#%-FIt0pyPHpThP%6aJ3|fu)-SyHceaKP1|OajzQ6ZybNkWD(2w^< zC&jn7KHwW`Z*QC+>7BvJvltB@K6#0x$M{QR{r>i&tf8MeaS6k<%QBnXH>=UazFb2$k5WfwCl+DQcOoBq-f(pr(7 zQjQ?Xmt({>@II=(u92WK>mV#zF<}^xiKr{0EjtBDOf)AoVp$M`)qRZwy&_{g)PP~w zkgI=Q0n4&CRbaMgCI|>Y=4+Y=x@AVQOe`glNHm>D3xecL56?9;VnwQ<`l5=`3c9nd zDFO|E^q`37C_u_yagq{t_OeDS8*(-0U)2cc>hMeYxdDnD;JQ{Ul>O5VCE{XCM28@; zpdy5j#e_wvxNrS7NFX-oUY81K$bK0tBI*eOf}l!Q6G3m(F z#bLD-6{01(Q^;cCm6(WZLWMG`Y(eW42xk0D0)bO#MF#=9mY8jd(+L9ZLVeW~QU<*O zp_K5lM&P>mfclqJCScgI=P)s=iLp;;t*wcmQ-Ur){8R#gu3*|H5)3<=$Ar(s#NkX^ zRY96C1Z;}MbWDuNsN7FDaw}nG{VzL@|j# zi|R=s8$st(o;s8l5(rcnS8k{%0ZcU)j5L`bpgg3mOfA8&QN>k7#5n;!NN--U|6Bd7Pf9yE;Up0l7*%~C7_lY7Ow4VKL~hz=LRBbi1^t4X zCBo9g;hig2)Pr@Gs0jv79&yrkgAeD457@}MuNsf%6w;KvQ+W#stO-aK@8haY7F;`>jV21n3(o{>SkQ$2(VyaG2(~Rv Wf}4bqM8LMS>*}1Ppg)7Li~j|ndVIV9 literal 0 HcmV?d00001 diff --git a/m_agent/skills/weather_station/dummy_weather_station_data.py b/m_agent/skills/weather_station/dummy_weather_station_data.py new file mode 100644 index 0000000000..126b091e3d --- /dev/null +++ b/m_agent/skills/weather_station/dummy_weather_station_data.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains dummy weather station data.""" + +import datetime +import logging +import os.path +import random +import sqlite3 +import time +from typing import Dict, Union + +logger = logging.getLogger("aea.weather_station_skill") + +my_path = os.path.dirname(__file__) + +DB_SOURCE = os.path.join(my_path, 'dummy_weather_station_data.db') + +# Checking if the database exists +con = sqlite3.connect(DB_SOURCE) +cur = con.cursor() + +cur.close() +con.commit() +con.close() + +# Create a table if it doesn't exist' +command = (''' CREATE TABLE IF NOT EXISTS data ( + abs_pressure REAL, + delay REAL, + hum_in REAL, + hum_out REAL, + idx TEXT, + rain REAL, + temp_in REAL, + temp_out REAL, + wind_ave REAL, + wind_dir REAL, + wind_gust REAL)''') + +con = sqlite3.connect(DB_SOURCE) +cur = con.cursor() +cur.execute(command) +cur.close() +con.commit() +if con is not None: + logger.info("Wheather station: I closed the db after checking it is populated!") + con.close() + + +class Forecast(): + """Represents a whether forecast.""" + + def add_data(self, tagged_data: Dict[str, Union[int, datetime.datetime]]) -> None: + """ + Add data to the forecast. + + :param tagged_data: the data dictionary + :return: None + """ + con = sqlite3.connect(DB_SOURCE) + cur = con.cursor() + cur.execute('''INSERT INTO data(abs_pressure, + delay, + hum_in, + hum_out, + idx, + rain, + temp_in, + temp_out, + wind_ave, + wind_dir, + wind_gust) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', + (tagged_data['abs_pressure'], + tagged_data['delay'], + tagged_data['hum_in'], + tagged_data['hum_out'], + datetime.datetime.now().strftime('%s'), + tagged_data['rain'], + tagged_data['temp_in'], + tagged_data['temp_out'], + tagged_data['wind_ave'], + tagged_data['wind_dir'], + tagged_data['wind_gust'])) + logger.info("Wheather station: I added data in the db!") + cur.close() + con.commit() + con.close() + + def generate(self): + """Generate weather data.""" + while True: + dict_of_data = {'abs_pressure': random.randrange(1022.0, 1025, 1), 'delay': random.randint(2, 7), + 'hum_in': random.randrange(33.0, 40.0, 1), 'hum_out': random.randrange(33.0, 80.0, 1), + 'idx': datetime.datetime.now(), 'rain': random.randrange(70.0, 74.0, 1), + 'temp_in': random.randrange(18, 28, 1), 'temp_out': random.randrange(2, 20, 1), + 'wind_ave': random.randrange(0, 10, 1), 'wind_dir': random.randrange(0, 14, 1), + 'wind_gust': random.randrange(1, 7, 1)} # type: Dict[str, Union[int, datetime.datetime]] + self.add_data(dict_of_data) + time.sleep(5) + + +if __name__ == '__main__': + a = Forecast() + a.generate() diff --git a/m_agent/skills/weather_station/handlers.py b/m_agent/skills/weather_station/handlers.py new file mode 100644 index 0000000000..cc0c478fe4 --- /dev/null +++ b/m_agent/skills/weather_station/handlers.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains a scaffold of a handler.""" + +import logging +import json +import time +from typing import Any, Dict, List, Optional, Union, cast, TYPE_CHECKING + +from aea.configurations.base import ProtocolId +from aea.protocols.base import Message +from aea.protocols.default.message import DefaultMessage +from aea.protocols.default.serialization import DefaultSerializer +from aea.protocols.fipa.message import FIPAMessage +from aea.protocols.fipa.serialization import FIPASerializer +from aea.protocols.oef.models import Description +from aea.skills.base import Handler + +if TYPE_CHECKING: + from packages.skills.weather_station.db_communication import DBCommunication +else: + from weather_station_skill.db_communication import DBCommunication + +logger = logging.getLogger("aea.weather_station_skill") + +DATE_ONE = "3/10/2019" +DATE_TWO = "15/10/2019" + + +class MyWeatherHandler(Handler): + """This class scaffolds a handler.""" + + SUPPORTED_PROTOCOL = FIPAMessage.protocol_id # type: Optional[ProtocolId] + + def __init__(self, **kwargs): + """Initialise the behaviour.""" + super().__init__(**kwargs) + self.fet_price = 0.002 + self.db = DBCommunication() + self.fetched_data = [] + + def setup(self) -> None: + """Implement the setup for the handler.""" + pass + + def handle(self, message: Message, sender: str) -> None: + """ + Implement the reaction to an message. + + :param message: the message + :param sender: the sender + :return: None + """ + fipa_msg = cast(FIPAMessage, message) + msg_performative = FIPAMessage.Performative(fipa_msg.get('performative')) + message_id = cast(int, fipa_msg.get('id')) + dialogue_id = cast(int, fipa_msg.get('dialogue_id')) + + if msg_performative == FIPAMessage.Performative.CFP: + self.handle_cfp(fipa_msg, sender, message_id, dialogue_id) + elif msg_performative == FIPAMessage.Performative.ACCEPT: + self.handle_accept(sender) + + def teardown(self) -> None: + """ + Implement the handler teardown. + + :return: None + """ + pass + + def handle_cfp(self, msg: FIPAMessage, sender: str, message_id: int, dialogue_id: int) -> None: + """ + Handle the CFP calls. + + :param msg: the message + :param sender: the sender + :param message_id: the message id + :param dialogue_id: the dialogue id + :return: None + """ + new_message_id = message_id + 1 + new_target = message_id + fetched_data = self.db.get_data_for_specific_dates(DATE_ONE, DATE_TWO) + + if len(fetched_data) >= 1: + self.fetched_data = fetched_data + total_price = self.fet_price * len(fetched_data) + proposal = [Description({"Rows": len(fetched_data), + "Price": total_price})] + logger.info("[{}]: sending sender={} a proposal at price={}".format(self.context.agent_name, sender, total_price)) + proposal_msg = FIPAMessage(message_id=new_message_id, + dialogue_id=dialogue_id, + target=new_target, + performative=FIPAMessage.Performative.PROPOSE, + proposal=proposal) + self.context.outbox.put_message(to=sender, + sender=self.context.agent_public_key, + protocol_id=FIPAMessage.protocol_id, + message=FIPASerializer().encode(proposal_msg)) + else: + logger.info("[{}]: declined the CFP from sender={}".format(self.context.agent_name, sender)) + decline_msg = FIPAMessage(message_id=new_message_id, + dialogue_id=dialogue_id, + target=new_target, + performative=FIPAMessage.Performative.DECLINE) + self.context.outbox.put_message(to=sender, + sender=self.context.agent_public_key, + protocol_id=FIPAMessage.protocol_id, + message=FIPASerializer().encode(decline_msg)) + + def handle_accept(self, sender: str) -> None: + """ + Handle the Accept Calls. + + :param sender: the sender + :return: None + """ + command = {} # type: Dict[str, Union[str, List[Any]]] + command['Command'] = "success" + command['fetched_data'] = [] + counter = 0 + for items in self.fetched_data: + dict_of_data = {} + dict_of_data['abs_pressure'] = items[0] + dict_of_data['delay'] = items[1] + dict_of_data['hum_in'] = items[2] + dict_of_data['hum_out'] = items[3] + dict_of_data['idx'] = time.ctime(int(items[4])) + dict_of_data['rain'] = items[5] + dict_of_data['temp_in'] = items[6] + dict_of_data['temp_out'] = items[7] + dict_of_data['wind_ave'] = items[8] + dict_of_data['wind_dir'] = items[9] + dict_of_data['wind_gust'] = items[10] + command['fetched_data'].append(dict_of_data) # type: ignore + counter += 1 + if counter == 10: + break + json_data = json.dumps(command) + json_bytes = json_data.encode("utf-8") + logger.info("[{}]: handling accept and sending weather data to sender={}".format(self.context.agent_name, sender)) + data_msg = DefaultMessage( + type=DefaultMessage.Type.BYTES, content=json_bytes) + self.context.outbox.put_message(to=sender, + sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, + message=DefaultSerializer().encode(data_msg)) diff --git a/m_agent/skills/weather_station/skill.yaml b/m_agent/skills/weather_station/skill.yaml new file mode 100644 index 0000000000..4630a25e9f --- /dev/null +++ b/m_agent/skills/weather_station/skill.yaml @@ -0,0 +1,25 @@ +name: weather_station +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +behaviours: + - behaviour: + class_name: MyWeatherBehaviour + args: + foo: bar +handlers: + - handler: + class_name: MyWeatherHandler + args: + foo: bar +tasks: + - task: + class_name: MyWeatherTask + args: + foo: bar +shared_classes: [] +protocols: ['fipa'] + + + diff --git a/m_agent/skills/weather_station/tasks.py b/m_agent/skills/weather_station/tasks.py new file mode 100644 index 0000000000..9458f5b11e --- /dev/null +++ b/m_agent/skills/weather_station/tasks.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains a scaffold of a task.""" + +from aea.skills.base import Task + + +class MyWeatherTask(Task): + """This class scaffolds a task.""" + + def setup(self) -> None: + """ + Implement the setup. + + :return: None + """ + pass + + def execute(self) -> None: + """ + Implement the task execution. + + :return: None + """ + pass + + def teardown(self) -> None: + """ + Implement the task teardown. + + :return: None + """ + pass diff --git a/m_agent/skills/weather_station/weather_station_data_model.py b/m_agent/skills/weather_station/weather_station_data_model.py new file mode 100644 index 0000000000..43783bb541 --- /dev/null +++ b/m_agent/skills/weather_station/weather_station_data_model.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains the dataModel for the weather agent.""" + +from aea.protocols.oef.models import DataModel, Attribute + +SCHEME = {'country': "UK", 'city': "Cambridge"} +SERVICE_ID = "WeatherData" + + +class WEATHER_STATION_DATAMODEL (DataModel): + """Data model for the weather Agent.""" + + def __init__(self): + """Initialise the dataModel.""" + self.ATTRIBUTE_COUNTRY = Attribute("country", str, True) + self.ATTRIBUTE_CITY = Attribute("city", str, True) + + super().__init__("weather_station_datamodel", [self.ATTRIBUTE_COUNTRY, + self.ATTRIBUTE_CITY]) From 42d5082a13769953cae304f5a9378b96733bfc84 Mon Sep 17 00:00:00 2001 From: Aristotelis Date: Tue, 15 Oct 2019 13:29:10 +0100 Subject: [PATCH 05/71] deleted unwanted agent --- aea/configurations/base.py | 1 + aea/context/base.py | 6 + aea/crypto/helpers.py | 14 +- aea/skills/base.py | 5 + m_agent/aea-config.yaml | 29 - m_agent/connections/__init__.py | 0 m_agent/connections/oef/__init__.py | 21 - m_agent/connections/oef/connection.py | 604 ------------------ m_agent/connections/oef/connection.yaml | 13 - m_agent/default_private_key.pem | 6 - m_agent/eth_private_key.txt | 1 - m_agent/fet_private_key.txt | 1 - m_agent/protocols/__init__.py | 0 m_agent/protocols/default/__init__.py | 21 - m_agent/protocols/default/message.py | 59 -- m_agent/protocols/default/protocol.yaml | 5 - m_agent/protocols/default/serialization.py | 71 -- m_agent/protocols/fipa/README.md | 6 - m_agent/protocols/fipa/__init__.py | 21 - m_agent/protocols/fipa/fipa.proto | 48 -- m_agent/protocols/fipa/fipa_pb2.py | 525 --------------- m_agent/protocols/fipa/message.py | 99 --- m_agent/protocols/fipa/protocol.yaml | 7 - m_agent/protocols/fipa/serialization.py | 143 ----- m_agent/skills/__init__.py | 0 m_agent/skills/error/__init__.py | 20 - m_agent/skills/error/behaviours.py | 50 -- m_agent/skills/error/handlers.py | 131 ---- m_agent/skills/error/skill.yaml | 14 - m_agent/skills/error/tasks.py | 51 -- m_agent/skills/weather_station/__init__.py | 20 - m_agent/skills/weather_station/behaviours.py | 84 --- .../weather_station/db_communication.py | 70 -- .../dummy_weather_station_data.db | Bin 8192 -> 0 bytes .../dummy_weather_station_data.py | 122 ---- m_agent/skills/weather_station/handlers.py | 165 ----- m_agent/skills/weather_station/skill.yaml | 25 - m_agent/skills/weather_station/tasks.py | 50 -- .../weather_station_data_model.py | 37 -- 39 files changed, 22 insertions(+), 2523 deletions(-) delete mode 100644 m_agent/aea-config.yaml delete mode 100644 m_agent/connections/__init__.py delete mode 100644 m_agent/connections/oef/__init__.py delete mode 100644 m_agent/connections/oef/connection.py delete mode 100644 m_agent/connections/oef/connection.yaml delete mode 100644 m_agent/default_private_key.pem delete mode 100644 m_agent/eth_private_key.txt delete mode 100644 m_agent/fet_private_key.txt delete mode 100644 m_agent/protocols/__init__.py delete mode 100644 m_agent/protocols/default/__init__.py delete mode 100644 m_agent/protocols/default/message.py delete mode 100644 m_agent/protocols/default/protocol.yaml delete mode 100644 m_agent/protocols/default/serialization.py delete mode 100644 m_agent/protocols/fipa/README.md delete mode 100644 m_agent/protocols/fipa/__init__.py delete mode 100644 m_agent/protocols/fipa/fipa.proto delete mode 100644 m_agent/protocols/fipa/fipa_pb2.py delete mode 100644 m_agent/protocols/fipa/message.py delete mode 100644 m_agent/protocols/fipa/protocol.yaml delete mode 100644 m_agent/protocols/fipa/serialization.py delete mode 100644 m_agent/skills/__init__.py delete mode 100644 m_agent/skills/error/__init__.py delete mode 100644 m_agent/skills/error/behaviours.py delete mode 100644 m_agent/skills/error/handlers.py delete mode 100644 m_agent/skills/error/skill.yaml delete mode 100644 m_agent/skills/error/tasks.py delete mode 100644 m_agent/skills/weather_station/__init__.py delete mode 100644 m_agent/skills/weather_station/behaviours.py delete mode 100644 m_agent/skills/weather_station/db_communication.py delete mode 100644 m_agent/skills/weather_station/dummy_weather_station_data.db delete mode 100644 m_agent/skills/weather_station/dummy_weather_station_data.py delete mode 100644 m_agent/skills/weather_station/handlers.py delete mode 100644 m_agent/skills/weather_station/skill.yaml delete mode 100644 m_agent/skills/weather_station/tasks.py delete mode 100644 m_agent/skills/weather_station/weather_station_data_model.py diff --git a/aea/configurations/base.py b/aea/configurations/base.py index 584932a7c5..3b51b6d54d 100644 --- a/aea/configurations/base.py +++ b/aea/configurations/base.py @@ -435,6 +435,7 @@ def __init__(self, self.url = url self.registry_path = registry_path self.private_key_paths = CRUDCollection[PrivateKeyPathConfig]() + self.addresses = None #type: Dict[str, str] private_key_paths = private_key_paths if private_key_paths is not None else {} for ledger, path in private_key_paths.items(): diff --git a/aea/context/base.py b/aea/context/base.py index 7ea75091d0..fee338e189 100644 --- a/aea/context/base.py +++ b/aea/context/base.py @@ -31,6 +31,7 @@ class AgentContext: def __init__(self, agent_name: str, public_keys: Dict[str, str], + addresses: Dict[str, str], outbox: OutBox, decision_maker_message_queue: Queue, ownership_state: OwnershipState, @@ -50,6 +51,7 @@ def __init__(self, agent_name: str, """ self._agent_name = agent_name self._public_keys = public_keys + self._addresses = addresses self._outbox = outbox self._decision_maker_message_queue = decision_maker_message_queue self._ownership_state = ownership_state @@ -65,6 +67,10 @@ def agent_name(self) -> str: def public_keys(self) -> Dict[str, str]: """Get public keys.""" return self._public_keys + @property + def addresses(self) -> Dict[str, str]: + """Get addresses.""" + return self._addresses @property def public_key(self) -> str: diff --git a/aea/crypto/helpers.py b/aea/crypto/helpers.py index f7a063a39a..de9b2a9fee 100644 --- a/aea/crypto/helpers.py +++ b/aea/crypto/helpers.py @@ -19,13 +19,13 @@ # ------------------------------------------------------------------------------ """Module wrapping the helpers of public and private key cryptography.""" -from typing import cast +from typing import cast, Dict from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption import logging from pathlib import Path -from fetchai.ledger.crypto import Entity # type: ignore +from fetchai.ledger.crypto import Entity, Address # type: ignore from eth_account import Account # type: ignore from aea.crypto.base import DefaultCrypto @@ -78,8 +78,15 @@ def _verify_or_create_private_keys(ctx: Context) -> None: fetchai_private_key_path = FETCHAI_PRIVATE_KEY_FILE fetchai_private_key_config = PrivateKeyPathConfig(FETCHAI, fetchai_private_key_path) aea_conf.private_key_paths.create(fetchai_private_key_config.ledger, fetchai_private_key_config) + aea_conf.addresses = cast(Dict[str, str], (FETCHAI, Address(entity).to_hex())) else: + path = Path(FETCHAI_PRIVATE_KEY_FILE) fetchai_private_key_config = cast(PrivateKeyPathConfig, fetchai_private_key_config) + with open(path, "r") as file: + pk = file.read() + entity = Entity.from_hex(pk) + adr = Address(entity).to_hex() + aea_conf.addresses = cast(Dict[str, str], (FETCHAI, adr)) try: _try_validate_fet_private_key_path(fetchai_private_key_config.path) except FileNotFoundError: @@ -95,6 +102,7 @@ def _verify_or_create_private_keys(ctx: Context) -> None: ethereum_private_key_path = ETHEREUM_PRIVATE_KEY_FILE ethereum_private_key_config = PrivateKeyPathConfig(ETHEREUM, ethereum_private_key_path) aea_conf.private_key_paths.create(ethereum_private_key_config.ledger, ethereum_private_key_config) + aea_conf.addresses = cast(Dict[str, str], (ETHEREUM, account.address)) else: ethereum_private_key_config = cast(PrivateKeyPathConfig, ethereum_private_key_config) try: @@ -145,7 +153,6 @@ def _try_validate_fet_private_key_path(private_key_path: str) -> None: :raises: an exception if the private key is invalid. """ try: - # TODO :Change this to match the enity.fromhex() with open(private_key_path, "r") as key: data = key.read() Entity.from_hex(data) @@ -163,7 +170,6 @@ def _try_validate_ethereum_private_key_path(private_key_path: str) -> None: :raises: an exception if the private key is invalid. """ try: - # TODO :Change this to match the Account.fromhex() with open(private_key_path, "r") as key: data = key.read() Account.from_key(data) diff --git a/aea/skills/base.py b/aea/skills/base.py index 85cb2caebc..840c41e31c 100644 --- a/aea/skills/base.py +++ b/aea/skills/base.py @@ -65,6 +65,11 @@ def agent_public_keys(self) -> Dict[str, str]: """Get public keys.""" return self._agent_context.public_keys + @property + def agent_addresses(self) -> Dict[str, str]: + """Get addresses.""" + return self._agent_context.addresses + @property def outbox(self) -> OutBox: """Get outbox.""" diff --git a/m_agent/aea-config.yaml b/m_agent/aea-config.yaml deleted file mode 100644 index f9879e08c5..0000000000 --- a/m_agent/aea-config.yaml +++ /dev/null @@ -1,29 +0,0 @@ -aea_version: 0.1.6 -agent_name: m_agent -authors: '' -connections: -- oef -default_connection: oef -license: '' -logging_config: - disable_existing_loggers: false - version: 1 -private_key_paths: -- private_key_path: - ledger: default - path: default_private_key.pem -- private_key_path: - ledger: fetchai - path: fet_private_key.txt -- private_key_path: - ledger: ethereum - path: eth_private_key.txt -protocols: -- default -- fipa -registry_path: ../packages -skills: -- error -- weather_station -url: '' -version: v1 diff --git a/m_agent/connections/__init__.py b/m_agent/connections/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/m_agent/connections/oef/__init__.py b/m_agent/connections/oef/__init__.py deleted file mode 100644 index 21ee4d83df..0000000000 --- a/m_agent/connections/oef/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Implementation of the OEF connection.""" diff --git a/m_agent/connections/oef/connection.py b/m_agent/connections/oef/connection.py deleted file mode 100644 index 7f489b867f..0000000000 --- a/m_agent/connections/oef/connection.py +++ /dev/null @@ -1,604 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Extension to the OEF Python SDK.""" -import datetime -import logging -import pickle -from queue import Empty, Queue -from threading import Thread -from typing import List, Dict, Optional, cast - -import oef -from oef.agents import OEFAgent -from oef.core import AsyncioCore -from oef.messages import CFP_TYPES, PROPOSE_TYPES -from oef.query import ( - Query as OEFQuery, - ConstraintExpr as OEFConstraintExpr, - And as OEFAnd, - Or as OEFOr, - Not as OEFNot, - Constraint as OEFConstraint, - ConstraintType as OEFConstraintType, Eq, NotEq, Lt, LtEq, Gt, GtEq, Range, In, NotIn) -from oef.schema import Description as OEFDescription, DataModel as OEFDataModel, AttributeSchema as OEFAttribute - -from aea.configurations.base import ConnectionConfig -from aea.connections.base import Channel, Connection -from aea.mail.base import MailBox, Envelope -from aea.protocols.fipa.message import FIPAMessage -from aea.protocols.fipa.serialization import FIPASerializer -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.models import Description, Attribute, DataModel, Query, ConstraintExpr, And, Or, Not, Constraint, \ - ConstraintType, ConstraintTypes -from aea.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF - -logger = logging.getLogger(__name__) - - -STUB_MESSSAGE_ID = 0 -STUB_DIALOGUE_ID = 0 - - -class OEFObjectTranslator: - """Translate our OEF object to object of OEF SDK classes.""" - - @classmethod - def to_oef_description(cls, desc: Description) -> OEFDescription: - """From our description to OEF description.""" - oef_data_model = cls.to_oef_data_model(desc.data_model) if desc.data_model is not None else None - return OEFDescription(desc.values, oef_data_model) - - @classmethod - def to_oef_data_model(cls, data_model: DataModel) -> OEFDataModel: - """From our data model to OEF data model.""" - oef_attributes = [cls.to_oef_attribute(attribute) for attribute in data_model.attributes] - return OEFDataModel(data_model.name, oef_attributes, data_model.description) - - @classmethod - def to_oef_attribute(cls, attribute: Attribute) -> OEFAttribute: - """From our attribute to OEF attribute.""" - return OEFAttribute(attribute.name, attribute.type, attribute.is_required, attribute.description) - - @classmethod - def to_oef_query(cls, query: Query) -> OEFQuery: - """From our query to OEF query.""" - oef_data_model = cls.to_oef_data_model(query.model) if query.model is not None else None - constraints = [cls.to_oef_constraint_expr(c) for c in query.constraints] - return OEFQuery(constraints, oef_data_model) - - @classmethod - def to_oef_constraint_expr(cls, constraint_expr: ConstraintExpr) -> OEFConstraintExpr: - """From our constraint expression to the OEF constraint expression.""" - if isinstance(constraint_expr, And): - return OEFAnd([cls.to_oef_constraint_expr(c) for c in constraint_expr.constraints]) - elif isinstance(constraint_expr, Or): - return OEFOr([cls.to_oef_constraint_expr(c) for c in constraint_expr.constraints]) - elif isinstance(constraint_expr, Not): - return OEFNot(cls.to_oef_constraint_expr(constraint_expr.constraint)) - elif isinstance(constraint_expr, Constraint): - oef_constraint_type = cls.to_oef_constraint_type(constraint_expr.constraint_type) - return OEFConstraint(constraint_expr.attribute_name, oef_constraint_type) - else: - raise ValueError("Constraint expression not supported.") - - @classmethod - def to_oef_constraint_type(cls, constraint_type: ConstraintType) -> OEFConstraintType: - """From our constraint type to OEF constraint type.""" - value = constraint_type.value - if constraint_type.type == ConstraintTypes.EQUAL: - return Eq(value) - elif constraint_type.type == ConstraintTypes.NOT_EQUAL: - return NotEq(value) - elif constraint_type.type == ConstraintTypes.LESS_THAN: - return Lt(value) - elif constraint_type.type == ConstraintTypes.LESS_THAN_EQ: - return LtEq(value) - elif constraint_type.type == ConstraintTypes.GREATER_THAN: - return Gt(value) - elif constraint_type.type == ConstraintTypes.GREATER_THAN_EQ: - return GtEq(value) - elif constraint_type.type == ConstraintTypes.WITHIN: - return Range(value) - elif constraint_type.type == ConstraintTypes.IN: - return In(value) - elif constraint_type.type == ConstraintTypes.NOT_IN: - return NotIn(value) - else: - raise ValueError("Constraint type not recognized.") - - @classmethod - def from_oef_description(cls, oef_desc: OEFDescription) -> Description: - """From an OEF description to our description.""" - data_model = cls.from_oef_data_model(oef_desc.data_model) if oef_desc.data_model is not None else None - return Description(oef_desc.values, data_model=data_model) - - @classmethod - def from_oef_data_model(cls, oef_data_model: OEFDataModel) -> DataModel: - """From an OEF data model to our data model.""" - attributes = [cls.from_oef_attribute(oef_attribute) for oef_attribute in oef_data_model.attribute_schemas] - return DataModel(oef_data_model.name, attributes, oef_data_model.description) - - @classmethod - def from_oef_attribute(cls, oef_attribute: OEFAttribute) -> Attribute: - """From an OEF attribute to our attribute.""" - return Attribute(oef_attribute.name, oef_attribute.type, oef_attribute.required, oef_attribute.description) - - @classmethod - def from_oef_query(cls, oef_query: OEFQuery) -> Query: - """From our query to OrOEF query.""" - data_model = cls.from_oef_data_model(oef_query.model) if oef_query.model is not None else None - constraints = [cls.from_oef_constraint_expr(c) for c in oef_query.constraints] - return Query(constraints, data_model) - - @classmethod - def from_oef_constraint_expr(cls, oef_constraint_expr: OEFConstraintExpr) -> ConstraintExpr: - """From our query to OEF query.""" - if isinstance(oef_constraint_expr, OEFAnd): - return And([cls.from_oef_constraint_expr(c) for c in oef_constraint_expr.constraints]) - elif isinstance(oef_constraint_expr, OEFOr): - return Or([cls.from_oef_constraint_expr(c) for c in oef_constraint_expr.constraints]) - elif isinstance(oef_constraint_expr, OEFNot): - return Not(cls.from_oef_constraint_expr(oef_constraint_expr.constraint)) - elif isinstance(oef_constraint_expr, OEFConstraint): - constraint_type = cls.from_oef_constraint_type(oef_constraint_expr.constraint) - return Constraint(oef_constraint_expr.attribute_name, constraint_type) - else: - raise ValueError("OEF Constraint not supported.") - - @classmethod - def from_oef_constraint_type(cls, constraint_type: OEFConstraintType) -> ConstraintType: - """From OEF constraint type to our constraint type.""" - if isinstance(constraint_type, Eq): - return ConstraintType(ConstraintTypes.EQUAL, constraint_type.value) - elif isinstance(constraint_type, NotEq): - return ConstraintType(ConstraintTypes.NOT_EQUAL, constraint_type.value) - elif isinstance(constraint_type, Lt): - return ConstraintType(ConstraintTypes.LESS_THAN, constraint_type.value) - elif isinstance(constraint_type, LtEq): - return ConstraintType(ConstraintTypes.LESS_THAN_EQ, constraint_type.value) - elif isinstance(constraint_type, Gt): - return ConstraintType(ConstraintTypes.GREATER_THAN, constraint_type.value) - elif isinstance(constraint_type, GtEq): - return ConstraintType(ConstraintTypes.GREATER_THAN_EQ, constraint_type.value) - elif isinstance(constraint_type, Range): - return ConstraintType(ConstraintTypes.WITHIN, constraint_type.values) - elif isinstance(constraint_type, In): - return ConstraintType(ConstraintTypes.IN, constraint_type.values) - elif isinstance(constraint_type, NotIn): - return ConstraintType(ConstraintTypes.NOT_IN, constraint_type.values) - else: - raise ValueError("Constraint type not recognized.") - - -class MailStats(object): - """The MailStats class tracks statistics on messages processed by MailBox.""" - - def __init__(self) -> None: - """ - Instantiate mail stats. - - :return: None - """ - self._search_count = 0 - self._search_start_time = {} # type: Dict[int, datetime.datetime] - self._search_timedelta = {} # type: Dict[int, float] - self._search_result_counts = {} # type: Dict[int, int] - - @property - def search_count(self) -> int: - """Get the search count.""" - return self._search_count - - def search_start(self, search_id: int) -> None: - """ - Add a search id and start time. - - :param search_id: the search id - - :return: None - """ - assert search_id not in self._search_start_time - self._search_count += 1 - self._search_start_time[search_id] = datetime.datetime.now() - - def search_end(self, search_id: int, nb_search_results: int) -> None: - """ - Add end time for a search id. - - :param search_id: the search id - :param nb_search_results: the number of agents returned in the search result - - :return: None - """ - assert search_id in self._search_start_time - assert search_id not in self._search_timedelta - self._search_timedelta[search_id] = (datetime.datetime.now() - self._search_start_time[search_id]).total_seconds() * 1000 - self._search_result_counts[search_id] = nb_search_results - - -class OEFChannel(OEFAgent, Channel): - """The OEFChannel connects the OEF Agent with the connection.""" - - def __init__(self, public_key: str, oef_addr: str, oef_port: int, core: AsyncioCore, in_queue: Queue): - """ - Initialize. - - :param public_key: the public key of the agent. - :param oef_addr: the OEF IP address. - :param oef_port: the OEF port. - :param in_queue: the in queue. - """ - super().__init__(public_key, oef_addr=oef_addr, oef_port=oef_port, core=core) - self.in_queue = in_queue - self.mail_stats = MailStats() - - def on_message(self, msg_id: int, dialogue_id: int, origin: str, content: bytes) -> None: - """ - On message event handler. - - :param msg_id: the message id. - :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. - :param content: the bytes content. - :return: None - """ - # We are not using the 'origin' parameter because 'content' contains a serialized instance of 'Envelope', - # hence it already contains the address of the sender. - envelope = Envelope.decode(content) - self.in_queue.put(envelope) - - def on_cfp(self, msg_id: int, dialogue_id: int, origin: str, target: int, query: CFP_TYPES) -> None: - """ - On cfp event handler. - - :param msg_id: the message id. - :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. - :param target: the message target. - :param query: the query. - :return: None - """ - try: - query = pickle.loads(query) - except Exception: - pass - msg = FIPAMessage(message_id=msg_id, - dialogue_id=dialogue_id, - target=target, - performative=FIPAMessage.Performative.CFP, - query=query if query != b"" else None) - msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_propose(self, msg_id: int, dialogue_id: int, origin: str, target: int, b_proposals: PROPOSE_TYPES) -> None: - """ - On propose event handler. - - :param msg_id: the message id. - :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. - :param target: the message target. - :param b_proposals: the proposals. - :return: None - """ - if type(b_proposals) == bytes: - proposals = pickle.loads(b_proposals) # type: List[Description] - else: - raise ValueError("No support for non-bytes proposals.") - - msg = FIPAMessage(message_id=msg_id, - dialogue_id=dialogue_id, - target=target, - performative=FIPAMessage.Performative.PROPOSE, - proposal=proposals) - msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_accept(self, msg_id: int, dialogue_id: int, origin: str, target: int) -> None: - """ - On accept event handler. - - :param msg_id: the message id. - :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. - :param target: the message target. - :return: None - """ - performative = FIPAMessage.Performative.MATCH_ACCEPT if msg_id == 4 and target == 3 else FIPAMessage.Performative.ACCEPT - msg = FIPAMessage(message_id=msg_id, - dialogue_id=dialogue_id, - target=target, - performative=performative) - msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_decline(self, msg_id: int, dialogue_id: int, origin: str, target: int) -> None: - """ - On decline event handler. - - :param msg_id: the message id. - :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. - :param target: the message target. - :return: None - """ - msg = FIPAMessage(message_id=msg_id, - dialogue_id=dialogue_id, - target=target, - performative=FIPAMessage.Performative.DECLINE) - msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_search_result(self, search_id: int, agents: List[str]) -> None: - """ - On accept event handler. - - :param search_id: the search id. - :param agents: the list of agents. - :return: None - """ - self.mail_stats.search_end(search_id, len(agents)) - msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_RESULT, id=search_id, agents=agents) - msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_oef_error(self, answer_id: int, operation: oef.messages.OEFErrorOperation) -> None: - """ - On oef error event handler. - - :param answer_id: the answer id. - :param operation: the error operation. - :return: None - """ - try: - operation = OEFMessage.OEFErrorOperation(operation) - except ValueError: - operation = OEFMessage.OEFErrorOperation.OTHER - - msg = OEFMessage(oef_type=OEFMessage.Type.OEF_ERROR, id=answer_id, operation=operation) - msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_dialogue_error(self, answer_id: int, dialogue_id: int, origin: str) -> None: - """ - On dialogue error event handler. - - :param answer_id: the answer id. - :param dialogue_id: the dialogue id. - :param origin: the message sender. - :return: None - """ - msg = OEFMessage(oef_type=OEFMessage.Type.DIALOGUE_ERROR, - id=answer_id, - dialogue_id=dialogue_id, - origin=origin) - msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def send(self, envelope: Envelope) -> None: - """ - Send message handler. - - :param envelope: the message. - :return: None - """ - if envelope.protocol_id == "default": - self.send_default_message(envelope) - elif envelope.protocol_id == "fipa": - self.send_fipa_message(envelope) - elif envelope.protocol_id == "oef": - self.send_oef_message(envelope) - elif envelope.protocol_id == "tac": - self.send_default_message(envelope) - else: - logger.error("This envelope cannot be sent: protocol_id={}".format(envelope.protocol_id)) - raise ValueError("Cannot send message.") - - def send_default_message(self, envelope: Envelope): - """Send a 'default' message.""" - self.send_message(STUB_MESSSAGE_ID, STUB_DIALOGUE_ID, envelope.to, envelope.encode()) - - def send_fipa_message(self, envelope: Envelope) -> None: - """ - Send fipa message handler. - - :param envelope: the message. - :return: None - """ - fipa_message = FIPASerializer().decode(envelope.message) - id = fipa_message.get("message_id") - dialogue_id = fipa_message.get("dialogue_id") - destination = envelope.to - target = fipa_message.get("target") - performative = FIPAMessage.Performative(fipa_message.get("performative")) - if performative == FIPAMessage.Performative.CFP: - query = fipa_message.get("query") - query = b"" if query is None else query - if type(query) == Query: - query = pickle.dumps(query) - self.send_cfp(id, dialogue_id, destination, target, query) - elif performative == FIPAMessage.Performative.PROPOSE: - proposal = cast(List[Description], fipa_message.get("proposal")) - proposal_b = pickle.dumps(proposal) # type: bytes - self.send_propose(id, dialogue_id, destination, target, proposal_b) - elif performative == FIPAMessage.Performative.ACCEPT: - self.send_accept(id, dialogue_id, destination, target) - elif performative == FIPAMessage.Performative.MATCH_ACCEPT: - self.send_accept(id, dialogue_id, destination, target) - elif performative == FIPAMessage.Performative.DECLINE: - self.send_decline(id, dialogue_id, destination, target) - else: - raise ValueError("OEF FIPA message not recognized.") - - def send_oef_message(self, envelope: Envelope) -> None: - """ - Send oef message handler. - - :param envelope: the message. - :return: None - """ - oef_message = OEFSerializer().decode(envelope.message) - oef_type = OEFMessage.Type(oef_message.get("type")) - oef_msg_id = cast(int, oef_message.get("id")) - if oef_type == OEFMessage.Type.REGISTER_SERVICE: - service_description = cast(Description, oef_message.get("service_description")) - service_id = cast(int, oef_message.get("service_id")) - oef_service_description = OEFObjectTranslator.to_oef_description(service_description) - self.register_service(oef_msg_id, oef_service_description, service_id) - elif oef_type == OEFMessage.Type.UNREGISTER_SERVICE: - service_description = cast(Description, oef_message.get("service_description")) - service_id = cast(int, oef_message.get("service_id")) - oef_service_description = OEFObjectTranslator.to_oef_description(service_description) - self.unregister_service(oef_msg_id, oef_service_description, service_id) - elif oef_type == OEFMessage.Type.SEARCH_AGENTS: - query = cast(Query, oef_message.get("query")) - oef_query = OEFObjectTranslator.to_oef_query(query) - self.search_agents(oef_msg_id, oef_query) - elif oef_type == OEFMessage.Type.SEARCH_SERVICES: - query = cast(Query, oef_message.get("query")) - oef_query = OEFObjectTranslator.to_oef_query(query) - self.mail_stats.search_start(oef_msg_id) - self.search_services(oef_msg_id, oef_query) - else: - raise ValueError("OEF request not recognized.") - - -class OEFConnection(Connection): - """The OEFConnection connects the to the mailbox.""" - - def __init__(self, public_key: str, oef_addr: str, oef_port: int = 10000): - """ - Initialize. - - :param public_key: the public key of the agent. - :param oef_addr: the OEF IP address. - :param oef_port: the OEF port. - """ - super().__init__() - core = AsyncioCore(logger=logger) - self._core = core # type: AsyncioCore - self.channel = OEFChannel(public_key, oef_addr, oef_port, core=core, in_queue=self.in_queue) - - self._stopped = True - self._connected = False - self.out_thread = None # type: Optional[Thread] - - @property - def is_established(self) -> bool: - """Get the connection status.""" - return self._connected - - def _fetch(self) -> None: - """ - Fetch the messages from the outqueue and send them. - - :return: None - """ - while self._connected: - try: - msg = self.out_queue.get(block=True, timeout=1.0) - self.send(msg) - except Empty: - pass - - def connect(self) -> None: - """ - Connect to the channel. - - :return: None - :raises ConnectionError if the connection to the OEF fails. - """ - if self._stopped and not self._connected: - self._stopped = False - self._core.run_threaded() - try: - if not self.channel.connect(): - raise ConnectionError("Cannot connect to OEFChannel.") - self._connected = True - self.out_thread = Thread(target=self._fetch) - self.out_thread.start() - except ConnectionError as e: - self._core.stop() - raise e - - def disconnect(self) -> None: - """ - Disconnect from the channel. - - :return: None - """ - assert self.out_thread is not None, "Call connect before disconnect." - if not self._stopped and self._connected: - self._connected = False - self.out_thread.join() - self.out_thread = None - self.channel.disconnect() - self._core.stop() - self._stopped = True - - def send(self, envelope: Envelope): - """ - Send messages. - - :return: None - """ - if self._connected: - self.channel.send(envelope) - - @classmethod - def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection': - """ - Get the OEF connection from the connection configuration. - - :param public_key: the public key of the agent. - :param connection_configuration: the connection configuration object. - :return: the connection object - """ - oef_addr = cast(str, connection_configuration.config.get("addr")) - oef_port = cast(int, connection_configuration.config.get("port")) - return OEFConnection(public_key, oef_addr, oef_port) - - -class OEFMailBox(MailBox): - """The OEF mail box.""" - - def __init__(self, public_key: str, oef_addr: str, oef_port: int = 10000): - """ - Initialize. - - :param public_key: the public key of the agent. - :param oef_addr: the OEF IP address. - :param oef_port: the OEF port. - """ - connection = OEFConnection(public_key, oef_addr, oef_port) - super().__init__(connection) - - @property - def mail_stats(self) -> MailStats: - """Get the mail stats object.""" - return self._connection.channel.mail_stats # type: ignore diff --git a/m_agent/connections/oef/connection.yaml b/m_agent/connections/oef/connection.yaml deleted file mode 100644 index 774c2721c8..0000000000 --- a/m_agent/connections/oef/connection.yaml +++ /dev/null @@ -1,13 +0,0 @@ -name: oef -authors: Fetch.AI Limited -version: 0.1.0 -license: Apache 2.0 -url: "" -class_name: OEFConnection -supported_protocols: ["oef"] -config: - addr: ${OEF_ADDR:127.0.0.1} - port: ${OEF_PORT:10000} -dependencies: - - colorlog - - oef \ No newline at end of file diff --git a/m_agent/default_private_key.pem b/m_agent/default_private_key.pem deleted file mode 100644 index 79808c7224..0000000000 --- a/m_agent/default_private_key.pem +++ /dev/null @@ -1,6 +0,0 @@ ------BEGIN EC PRIVATE KEY----- -MIGkAgEBBDCz6NgtmkEIocXBXJLFAFQCuhgzf+pTiaDbcdzq0wG9OiJpTUrdMbRJ -tTdIRzR812+gBwYFK4EEACKhZANiAARQlMkwlZash82ceV4TJPH6g+SENb42ocJP -ypEPsgRc4jQ3rsOEo8zmBnwueW3lj8VwRQdaW//mzWNtEA0e3f1zt2SpUFBdHJEs -5dWQGPosfxTA8VzTnrqFg+1jFZeY5Rc= ------END EC PRIVATE KEY----- diff --git a/m_agent/eth_private_key.txt b/m_agent/eth_private_key.txt deleted file mode 100644 index dceb71fbe4..0000000000 --- a/m_agent/eth_private_key.txt +++ /dev/null @@ -1 +0,0 @@ -0x1e91910c28a467f6a57f2d44409b2ee7d32fe194eae7b2084526cecd6fe3e753 \ No newline at end of file diff --git a/m_agent/fet_private_key.txt b/m_agent/fet_private_key.txt deleted file mode 100644 index 966b91fd73..0000000000 --- a/m_agent/fet_private_key.txt +++ /dev/null @@ -1 +0,0 @@ -6135b9666fe3078154a880d07c766b18f22fdc173b5cc8eba9e4805f95d39e05 \ No newline at end of file diff --git a/m_agent/protocols/__init__.py b/m_agent/protocols/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/m_agent/protocols/default/__init__.py b/m_agent/protocols/default/__init__.py deleted file mode 100644 index 52e51b51e3..0000000000 --- a/m_agent/protocols/default/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the support resources for the default protocol.""" diff --git a/m_agent/protocols/default/message.py b/m_agent/protocols/default/message.py deleted file mode 100644 index 475714a2f0..0000000000 --- a/m_agent/protocols/default/message.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the default message definition.""" -from enum import Enum -from typing import Optional - -from aea.protocols.base import Message - - -class DefaultMessage(Message): - """The Default message class.""" - - protocol_id = "default" - - class Type(Enum): - """Default message types.""" - - BYTES = "bytes" - ERROR = "error" - - def __str__(self): - """Get the string representation.""" - return self.value - - class ErrorCode(Enum): - """The error codes.""" - - UNSUPPORTED_PROTOCOL = -10001 - DECODING_ERROR = -10002 - INVALID_MESSAGE = -10003 - UNSUPPORTED_SKILL = -10004 - - def __init__(self, type: Optional[Type] = None, - **kwargs): - """ - Initialize. - - :param type: the type. - """ - super().__init__(type=type, **kwargs) - assert self.check_consistency(), "DefaultMessage initialization inconsistent." diff --git a/m_agent/protocols/default/protocol.yaml b/m_agent/protocols/default/protocol.yaml deleted file mode 100644 index 6e9fd1dc97..0000000000 --- a/m_agent/protocols/default/protocol.yaml +++ /dev/null @@ -1,5 +0,0 @@ -name: 'default' -authors: Fetch.AI Limited -version: 0.1.0 -license: Apache 2.0 -url: "" diff --git a/m_agent/protocols/default/serialization.py b/m_agent/protocols/default/serialization.py deleted file mode 100644 index 080b8f386b..0000000000 --- a/m_agent/protocols/default/serialization.py +++ /dev/null @@ -1,71 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Serialization module for the default protocol.""" -import base64 -import json -from typing import cast - -from aea.protocols.base import Message -from aea.protocols.base import Serializer -from aea.protocols.default.message import DefaultMessage - - -class DefaultSerializer(Serializer): - """Serialization for the 'default' protocol.""" - - def encode(self, msg: Message) -> bytes: - """Encode a 'default' message into bytes.""" - body = {} # Dict[str, Any] - - msg_type = DefaultMessage.Type(msg.get("type")) - body["type"] = str(msg_type.value) - - if msg_type == DefaultMessage.Type.BYTES: - content = cast(bytes, msg.get("content")) - body["content"] = base64.b64encode(content).decode("utf-8") - elif msg_type == DefaultMessage.Type.ERROR: - body["error_code"] = cast(str, msg.get("error_code")) - body["error_msg"] = cast(str, msg.get("error_msg")) - body["error_data"] = cast(str, msg.get("error_data")) - else: - raise ValueError("Type not recognized.") - - bytes_msg = json.dumps(body).encode("utf-8") - return bytes_msg - - def decode(self, obj: bytes) -> Message: - """Decode bytes into a 'default' message.""" - json_body = json.loads(obj.decode("utf-8")) - body = {} - - msg_type = DefaultMessage.Type(json_body["type"]) - body["type"] = msg_type - if msg_type == DefaultMessage.Type.BYTES: - content = base64.b64decode(json_body["content"].encode("utf-8")) - body["content"] = content # type: ignore - elif msg_type == DefaultMessage.Type.ERROR: - body["error_code"] = json_body["error_code"] - body["error_msg"] = json_body["error_msg"] - body["error_data"] = json_body["error_data"] - else: - raise ValueError("Type not recognized.") - - return DefaultMessage(type=msg_type, body=body) diff --git a/m_agent/protocols/fipa/README.md b/m_agent/protocols/fipa/README.md deleted file mode 100644 index 70b80ac297..0000000000 --- a/m_agent/protocols/fipa/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# fipa protocol - -## To update the `fipa_pb2.py` file - - cd aea/protocols/fipa - protoc --python_out=. fipa.proto \ No newline at end of file diff --git a/m_agent/protocols/fipa/__init__.py b/m_agent/protocols/fipa/__init__.py deleted file mode 100644 index 88af132fb8..0000000000 --- a/m_agent/protocols/fipa/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the support resources for the FIPA protocol.""" diff --git a/m_agent/protocols/fipa/fipa.proto b/m_agent/protocols/fipa/fipa.proto deleted file mode 100644 index 98a387d176..0000000000 --- a/m_agent/protocols/fipa/fipa.proto +++ /dev/null @@ -1,48 +0,0 @@ -syntax = "proto3"; - -package fetch.aea.fipa; - -message FIPAMessage{ - - message CFP{ - message Nothing { - } - oneof query{ - bytes bytes = 1; - Nothing nothing = 2; - bytes query_bytes = 3; - } - } - message Propose{ - repeated bytes proposal = 1; - } - message Accept{} - - message MatchAccept{} - - message Accept_W_Address{ - string address = 1; - } - - message MatchAccept_W_Address{ - string address = 1; - } - message Decline{} - message Inform{ - bytes bytes = 1; - } - - int32 message_id = 1; - int32 dialogue_id = 2; - int32 target = 3; - oneof performative{ - CFP cfp = 4; - Propose propose = 5; - Accept accept = 6; - MatchAccept match_accept = 7; - Decline decline = 8; - Inform inform = 9; - Accept_W_Address accept_w_address = 10; - MatchAccept_W_Address match_accept_w_address = 11; - } -} diff --git a/m_agent/protocols/fipa/fipa_pb2.py b/m_agent/protocols/fipa/fipa_pb2.py deleted file mode 100644 index 5c4736d708..0000000000 --- a/m_agent/protocols/fipa/fipa_pb2.py +++ /dev/null @@ -1,525 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: fipa.proto - -import sys -_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection -from google.protobuf import symbol_database as _symbol_database -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='fipa.proto', - package='fetch.aea.fipa', - syntax='proto3', - serialized_options=None, - serialized_pb=_b('\n\nfipa.proto\x12\x0e\x66\x65tch.aea.fipa\"\xea\x06\n\x0b\x46IPAMessage\x12\x12\n\nmessage_id\x18\x01 \x01(\x05\x12\x13\n\x0b\x64ialogue_id\x18\x02 \x01(\x05\x12\x0e\n\x06target\x18\x03 \x01(\x05\x12.\n\x03\x63\x66p\x18\x04 \x01(\x0b\x32\x1f.fetch.aea.fipa.FIPAMessage.CFPH\x00\x12\x36\n\x07propose\x18\x05 \x01(\x0b\x32#.fetch.aea.fipa.FIPAMessage.ProposeH\x00\x12\x34\n\x06\x61\x63\x63\x65pt\x18\x06 \x01(\x0b\x32\".fetch.aea.fipa.FIPAMessage.AcceptH\x00\x12?\n\x0cmatch_accept\x18\x07 \x01(\x0b\x32\'.fetch.aea.fipa.FIPAMessage.MatchAcceptH\x00\x12\x36\n\x07\x64\x65\x63line\x18\x08 \x01(\x0b\x32#.fetch.aea.fipa.FIPAMessage.DeclineH\x00\x12\x34\n\x06inform\x18\t \x01(\x0b\x32\".fetch.aea.fipa.FIPAMessage.InformH\x00\x12H\n\x10\x61\x63\x63\x65pt_w_address\x18\n \x01(\x0b\x32,.fetch.aea.fipa.FIPAMessage.Accept_W_AddressH\x00\x12S\n\x16match_accept_w_address\x18\x0b \x01(\x0b\x32\x31.fetch.aea.fipa.FIPAMessage.MatchAccept_W_AddressH\x00\x1a}\n\x03\x43\x46P\x12\x0f\n\x05\x62ytes\x18\x01 \x01(\x0cH\x00\x12:\n\x07nothing\x18\x02 \x01(\x0b\x32\'.fetch.aea.fipa.FIPAMessage.CFP.NothingH\x00\x12\x15\n\x0bquery_bytes\x18\x03 \x01(\x0cH\x00\x1a\t\n\x07NothingB\x07\n\x05query\x1a\x1b\n\x07Propose\x12\x10\n\x08proposal\x18\x01 \x03(\x0c\x1a\x08\n\x06\x41\x63\x63\x65pt\x1a\r\n\x0bMatchAccept\x1a#\n\x10\x41\x63\x63\x65pt_W_Address\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x1a(\n\x15MatchAccept_W_Address\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x1a\t\n\x07\x44\x65\x63line\x1a\x17\n\x06Inform\x12\r\n\x05\x62ytes\x18\x01 \x01(\x0c\x42\x0e\n\x0cperformativeb\x06proto3') -) - - - - -_FIPAMESSAGE_CFP_NOTHING = _descriptor.Descriptor( - name='Nothing', - full_name='fetch.aea.fipa.FIPAMessage.CFP.Nothing', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=702, - serialized_end=711, -) - -_FIPAMESSAGE_CFP = _descriptor.Descriptor( - name='CFP', - full_name='fetch.aea.fipa.FIPAMessage.CFP', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='bytes', full_name='fetch.aea.fipa.FIPAMessage.CFP.bytes', index=0, - number=1, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='nothing', full_name='fetch.aea.fipa.FIPAMessage.CFP.nothing', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='query_bytes', full_name='fetch.aea.fipa.FIPAMessage.CFP.query_bytes', index=2, - number=3, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[_FIPAMESSAGE_CFP_NOTHING, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='query', full_name='fetch.aea.fipa.FIPAMessage.CFP.query', - index=0, containing_type=None, fields=[]), - ], - serialized_start=595, - serialized_end=720, -) - -_FIPAMESSAGE_PROPOSE = _descriptor.Descriptor( - name='Propose', - full_name='fetch.aea.fipa.FIPAMessage.Propose', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='proposal', full_name='fetch.aea.fipa.FIPAMessage.Propose.proposal', index=0, - number=1, type=12, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=722, - serialized_end=749, -) - -_FIPAMESSAGE_ACCEPT = _descriptor.Descriptor( - name='Accept', - full_name='fetch.aea.fipa.FIPAMessage.Accept', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=751, - serialized_end=759, -) - -_FIPAMESSAGE_MATCHACCEPT = _descriptor.Descriptor( - name='MatchAccept', - full_name='fetch.aea.fipa.FIPAMessage.MatchAccept', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=761, - serialized_end=774, -) - -_FIPAMESSAGE_ACCEPT_W_ADDRESS = _descriptor.Descriptor( - name='Accept_W_Address', - full_name='fetch.aea.fipa.FIPAMessage.Accept_W_Address', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='address', full_name='fetch.aea.fipa.FIPAMessage.Accept_W_Address.address', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=776, - serialized_end=811, -) - -_FIPAMESSAGE_MATCHACCEPT_W_ADDRESS = _descriptor.Descriptor( - name='MatchAccept_W_Address', - full_name='fetch.aea.fipa.FIPAMessage.MatchAccept_W_Address', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='address', full_name='fetch.aea.fipa.FIPAMessage.MatchAccept_W_Address.address', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=813, - serialized_end=853, -) - -_FIPAMESSAGE_DECLINE = _descriptor.Descriptor( - name='Decline', - full_name='fetch.aea.fipa.FIPAMessage.Decline', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=855, - serialized_end=864, -) - -_FIPAMESSAGE_INFORM = _descriptor.Descriptor( - name='Inform', - full_name='fetch.aea.fipa.FIPAMessage.Inform', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='bytes', full_name='fetch.aea.fipa.FIPAMessage.Inform.bytes', index=0, - number=1, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=866, - serialized_end=889, -) - -_FIPAMESSAGE = _descriptor.Descriptor( - name='FIPAMessage', - full_name='fetch.aea.fipa.FIPAMessage', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='message_id', full_name='fetch.aea.fipa.FIPAMessage.message_id', index=0, - number=1, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='dialogue_id', full_name='fetch.aea.fipa.FIPAMessage.dialogue_id', index=1, - number=2, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='target', full_name='fetch.aea.fipa.FIPAMessage.target', index=2, - number=3, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='cfp', full_name='fetch.aea.fipa.FIPAMessage.cfp', index=3, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='propose', full_name='fetch.aea.fipa.FIPAMessage.propose', index=4, - number=5, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='accept', full_name='fetch.aea.fipa.FIPAMessage.accept', index=5, - number=6, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='match_accept', full_name='fetch.aea.fipa.FIPAMessage.match_accept', index=6, - number=7, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='decline', full_name='fetch.aea.fipa.FIPAMessage.decline', index=7, - number=8, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='inform', full_name='fetch.aea.fipa.FIPAMessage.inform', index=8, - number=9, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='accept_w_address', full_name='fetch.aea.fipa.FIPAMessage.accept_w_address', index=9, - number=10, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='match_accept_w_address', full_name='fetch.aea.fipa.FIPAMessage.match_accept_w_address', index=10, - number=11, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[_FIPAMESSAGE_CFP, _FIPAMESSAGE_PROPOSE, _FIPAMESSAGE_ACCEPT, _FIPAMESSAGE_MATCHACCEPT, _FIPAMESSAGE_ACCEPT_W_ADDRESS, _FIPAMESSAGE_MATCHACCEPT_W_ADDRESS, _FIPAMESSAGE_DECLINE, _FIPAMESSAGE_INFORM, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='performative', full_name='fetch.aea.fipa.FIPAMessage.performative', - index=0, containing_type=None, fields=[]), - ], - serialized_start=31, - serialized_end=905, -) - -_FIPAMESSAGE_CFP_NOTHING.containing_type = _FIPAMESSAGE_CFP -_FIPAMESSAGE_CFP.fields_by_name['nothing'].message_type = _FIPAMESSAGE_CFP_NOTHING -_FIPAMESSAGE_CFP.containing_type = _FIPAMESSAGE -_FIPAMESSAGE_CFP.oneofs_by_name['query'].fields.append( - _FIPAMESSAGE_CFP.fields_by_name['bytes']) -_FIPAMESSAGE_CFP.fields_by_name['bytes'].containing_oneof = _FIPAMESSAGE_CFP.oneofs_by_name['query'] -_FIPAMESSAGE_CFP.oneofs_by_name['query'].fields.append( - _FIPAMESSAGE_CFP.fields_by_name['nothing']) -_FIPAMESSAGE_CFP.fields_by_name['nothing'].containing_oneof = _FIPAMESSAGE_CFP.oneofs_by_name['query'] -_FIPAMESSAGE_CFP.oneofs_by_name['query'].fields.append( - _FIPAMESSAGE_CFP.fields_by_name['query_bytes']) -_FIPAMESSAGE_CFP.fields_by_name['query_bytes'].containing_oneof = _FIPAMESSAGE_CFP.oneofs_by_name['query'] -_FIPAMESSAGE_PROPOSE.containing_type = _FIPAMESSAGE -_FIPAMESSAGE_ACCEPT.containing_type = _FIPAMESSAGE -_FIPAMESSAGE_MATCHACCEPT.containing_type = _FIPAMESSAGE -_FIPAMESSAGE_ACCEPT_W_ADDRESS.containing_type = _FIPAMESSAGE -_FIPAMESSAGE_MATCHACCEPT_W_ADDRESS.containing_type = _FIPAMESSAGE -_FIPAMESSAGE_DECLINE.containing_type = _FIPAMESSAGE -_FIPAMESSAGE_INFORM.containing_type = _FIPAMESSAGE -_FIPAMESSAGE.fields_by_name['cfp'].message_type = _FIPAMESSAGE_CFP -_FIPAMESSAGE.fields_by_name['propose'].message_type = _FIPAMESSAGE_PROPOSE -_FIPAMESSAGE.fields_by_name['accept'].message_type = _FIPAMESSAGE_ACCEPT -_FIPAMESSAGE.fields_by_name['match_accept'].message_type = _FIPAMESSAGE_MATCHACCEPT -_FIPAMESSAGE.fields_by_name['decline'].message_type = _FIPAMESSAGE_DECLINE -_FIPAMESSAGE.fields_by_name['inform'].message_type = _FIPAMESSAGE_INFORM -_FIPAMESSAGE.fields_by_name['accept_w_address'].message_type = _FIPAMESSAGE_ACCEPT_W_ADDRESS -_FIPAMESSAGE.fields_by_name['match_accept_w_address'].message_type = _FIPAMESSAGE_MATCHACCEPT_W_ADDRESS -_FIPAMESSAGE.oneofs_by_name['performative'].fields.append( - _FIPAMESSAGE.fields_by_name['cfp']) -_FIPAMESSAGE.fields_by_name['cfp'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] -_FIPAMESSAGE.oneofs_by_name['performative'].fields.append( - _FIPAMESSAGE.fields_by_name['propose']) -_FIPAMESSAGE.fields_by_name['propose'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] -_FIPAMESSAGE.oneofs_by_name['performative'].fields.append( - _FIPAMESSAGE.fields_by_name['accept']) -_FIPAMESSAGE.fields_by_name['accept'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] -_FIPAMESSAGE.oneofs_by_name['performative'].fields.append( - _FIPAMESSAGE.fields_by_name['match_accept']) -_FIPAMESSAGE.fields_by_name['match_accept'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] -_FIPAMESSAGE.oneofs_by_name['performative'].fields.append( - _FIPAMESSAGE.fields_by_name['decline']) -_FIPAMESSAGE.fields_by_name['decline'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] -_FIPAMESSAGE.oneofs_by_name['performative'].fields.append( - _FIPAMESSAGE.fields_by_name['inform']) -_FIPAMESSAGE.fields_by_name['inform'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] -_FIPAMESSAGE.oneofs_by_name['performative'].fields.append( - _FIPAMESSAGE.fields_by_name['accept_w_address']) -_FIPAMESSAGE.fields_by_name['accept_w_address'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] -_FIPAMESSAGE.oneofs_by_name['performative'].fields.append( - _FIPAMESSAGE.fields_by_name['match_accept_w_address']) -_FIPAMESSAGE.fields_by_name['match_accept_w_address'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] -DESCRIPTOR.message_types_by_name['FIPAMessage'] = _FIPAMESSAGE -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -FIPAMessage = _reflection.GeneratedProtocolMessageType('FIPAMessage', (_message.Message,), dict( - - CFP = _reflection.GeneratedProtocolMessageType('CFP', (_message.Message,), dict( - - Nothing = _reflection.GeneratedProtocolMessageType('Nothing', (_message.Message,), dict( - DESCRIPTOR = _FIPAMESSAGE_CFP_NOTHING, - __module__ = 'fipa_pb2' - # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.CFP.Nothing) - )) - , - DESCRIPTOR = _FIPAMESSAGE_CFP, - __module__ = 'fipa_pb2' - # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.CFP) - )) - , - - Propose = _reflection.GeneratedProtocolMessageType('Propose', (_message.Message,), dict( - DESCRIPTOR = _FIPAMESSAGE_PROPOSE, - __module__ = 'fipa_pb2' - # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.Propose) - )) - , - - Accept = _reflection.GeneratedProtocolMessageType('Accept', (_message.Message,), dict( - DESCRIPTOR = _FIPAMESSAGE_ACCEPT, - __module__ = 'fipa_pb2' - # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.Accept) - )) - , - - MatchAccept = _reflection.GeneratedProtocolMessageType('MatchAccept', (_message.Message,), dict( - DESCRIPTOR = _FIPAMESSAGE_MATCHACCEPT, - __module__ = 'fipa_pb2' - # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.MatchAccept) - )) - , - - Accept_W_Address = _reflection.GeneratedProtocolMessageType('Accept_W_Address', (_message.Message,), dict( - DESCRIPTOR = _FIPAMESSAGE_ACCEPT_W_ADDRESS, - __module__ = 'fipa_pb2' - # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.Accept_W_Address) - )) - , - - MatchAccept_W_Address = _reflection.GeneratedProtocolMessageType('MatchAccept_W_Address', (_message.Message,), dict( - DESCRIPTOR = _FIPAMESSAGE_MATCHACCEPT_W_ADDRESS, - __module__ = 'fipa_pb2' - # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.MatchAccept_W_Address) - )) - , - - Decline = _reflection.GeneratedProtocolMessageType('Decline', (_message.Message,), dict( - DESCRIPTOR = _FIPAMESSAGE_DECLINE, - __module__ = 'fipa_pb2' - # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.Decline) - )) - , - - Inform = _reflection.GeneratedProtocolMessageType('Inform', (_message.Message,), dict( - DESCRIPTOR = _FIPAMESSAGE_INFORM, - __module__ = 'fipa_pb2' - # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.Inform) - )) - , - DESCRIPTOR = _FIPAMESSAGE, - __module__ = 'fipa_pb2' - # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage) - )) -_sym_db.RegisterMessage(FIPAMessage) -_sym_db.RegisterMessage(FIPAMessage.CFP) -_sym_db.RegisterMessage(FIPAMessage.CFP.Nothing) -_sym_db.RegisterMessage(FIPAMessage.Propose) -_sym_db.RegisterMessage(FIPAMessage.Accept) -_sym_db.RegisterMessage(FIPAMessage.MatchAccept) -_sym_db.RegisterMessage(FIPAMessage.Accept_W_Address) -_sym_db.RegisterMessage(FIPAMessage.MatchAccept_W_Address) -_sym_db.RegisterMessage(FIPAMessage.Decline) -_sym_db.RegisterMessage(FIPAMessage.Inform) - - -# @@protoc_insertion_point(module_scope) diff --git a/m_agent/protocols/fipa/message.py b/m_agent/protocols/fipa/message.py deleted file mode 100644 index d776c12aeb..0000000000 --- a/m_agent/protocols/fipa/message.py +++ /dev/null @@ -1,99 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the FIPA message definition.""" -from enum import Enum -from typing import Optional, Union - -from aea.protocols.base import Message -from aea.protocols.oef.models import Description, Query - - -class FIPAMessage(Message): - """The FIPA message class.""" - - protocol_id = "fipa" - - class Performative(Enum): - """FIPA performatives.""" - - CFP = "cfp" - PROPOSE = "propose" - ACCEPT = "accept" - MATCH_ACCEPT = "match_accept" - DECLINE = "decline" - INFORM = "inform" - ACCEPT_W_ADDRESS = "accept_w_address" - MATCH_ACCEPT_W_ADDRESS = "match_accept_w_address" - - def __str__(self): - """Get string representation.""" - return self.value - - def __init__(self, message_id: Optional[int] = None, - dialogue_id: Optional[int] = None, - target: Optional[int] = None, - performative: Optional[Union[str, Performative]] = None, - **kwargs): - """ - Initialize. - - :param message_id: the message id. - :param dialogue_id: the dialogue id. - :param target: the message target. - :param performative: the message performative. - """ - super().__init__(message_id=message_id, - dialogue_id=dialogue_id, - target=target, - performative=FIPAMessage.Performative(performative), - **kwargs) - assert self.check_consistency(), "FIPAMessage initialization inconsistent." - - def check_consistency(self) -> bool: - """Check that the data is consistent.""" - try: - assert self.is_set("message_id") - assert self.is_set("dialogue_id") - assert self.is_set("target") - performative = FIPAMessage.Performative(self.get("performative")) - if performative == FIPAMessage.Performative.CFP: - query = self.get("query") - assert isinstance(query, Query) or isinstance(query, bytes) or query is None - elif performative == FIPAMessage.Performative.PROPOSE: - proposal = self.get("proposal") - assert type(proposal) == list and all(isinstance(d, Description) or type(d) == bytes for d in proposal) # type: ignore - elif performative == FIPAMessage.Performative.ACCEPT \ - or performative == FIPAMessage.Performative.MATCH_ACCEPT \ - or performative == FIPAMessage.Performative.DECLINE: - pass # pragma: no cover - elif performative == FIPAMessage.Performative.ACCEPT_W_ADDRESS\ - or performative == FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS: - assert self.is_set("address") - elif performative == FIPAMessage.Performative.INFORM: - data = self.get("data") - assert isinstance(data, bytes) - else: - raise ValueError("Performative not recognized.") - - except (AssertionError, ValueError, KeyError): - return False - - return True diff --git a/m_agent/protocols/fipa/protocol.yaml b/m_agent/protocols/fipa/protocol.yaml deleted file mode 100644 index a92f5d07fd..0000000000 --- a/m_agent/protocols/fipa/protocol.yaml +++ /dev/null @@ -1,7 +0,0 @@ -name: 'fipa' -authors: Fetch.AI Limited -version: 0.1.0 -license: Apache 2.0 -url: "" -dependencies: - - protobuf diff --git a/m_agent/protocols/fipa/serialization.py b/m_agent/protocols/fipa/serialization.py deleted file mode 100644 index ee300b64ea..0000000000 --- a/m_agent/protocols/fipa/serialization.py +++ /dev/null @@ -1,143 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Serialization for the FIPA protocol.""" -import pickle -from typing import cast - -from aea.protocols.base import Message -from aea.protocols.base import Serializer -from aea.protocols.fipa import fipa_pb2 -from aea.protocols.fipa.message import FIPAMessage -from aea.protocols.oef.models import Description, Query - - -class FIPASerializer(Serializer): - """Serialization for the FIPA protocol.""" - - def encode(self, msg: Message) -> bytes: - """Encode a FIPA message into bytes.""" - fipa_msg = fipa_pb2.FIPAMessage() - fipa_msg.message_id = msg.get("message_id") - fipa_msg.dialogue_id = msg.get("dialogue_id") - fipa_msg.target = msg.get("target") - - performative_id = FIPAMessage.Performative(msg.get("performative")) - if performative_id == FIPAMessage.Performative.CFP: - performative = fipa_pb2.FIPAMessage.CFP() # type: ignore - query = msg.get("query") - if query is None or query == b"": - nothing = fipa_pb2.FIPAMessage.CFP.Nothing() # type: ignore - performative.nothing.CopyFrom(nothing) - elif type(query) == Query: - query = pickle.dumps(query) - performative.query_bytes = query - elif type(query) == bytes: - performative.bytes = query - else: - raise ValueError("Query type not supported: {}".format(type(query))) - fipa_msg.cfp.CopyFrom(performative) - elif performative_id == FIPAMessage.Performative.PROPOSE: - performative = fipa_pb2.FIPAMessage.Propose() # type: ignore - proposal = cast(Description, msg.get("proposal")) - p_array_bytes = [pickle.dumps(p) for p in proposal] - performative.proposal.extend(p_array_bytes) - fipa_msg.propose.CopyFrom(performative) - elif performative_id == FIPAMessage.Performative.ACCEPT: - performative = fipa_pb2.FIPAMessage.Accept() # type: ignore - fipa_msg.accept.CopyFrom(performative) - elif performative_id == FIPAMessage.Performative.MATCH_ACCEPT: - performative = fipa_pb2.FIPAMessage.MatchAccept() # type: ignore - fipa_msg.match_accept.CopyFrom(performative) - elif performative_id == FIPAMessage.Performative.ACCEPT_W_ADDRESS: - performative = fipa_pb2.FIPAMessage.Accept_W_Address() # type: ignore - address = msg.get("address") - if type(address) == str: - performative.address = address - fipa_msg.accept_w_address.CopyFrom(performative) - elif performative_id == FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS: - performative = fipa_pb2.FIPAMessage.MatchAccept_W_Address() # type: ignore - address = msg.get("address") - if type(address) == str: - performative.address = address - fipa_msg.match_accept_w_address.CopyFrom(performative) - elif performative_id == FIPAMessage.Performative.DECLINE: - performative = fipa_pb2.FIPAMessage.Decline() # type: ignore - fipa_msg.decline.CopyFrom(performative) - elif performative_id == FIPAMessage.Performative.INFORM: - performative = fipa_pb2.FIPAMessage.Inform() # type: ignore - data = msg.get("data") - data_bytes = pickle.dumps(data) - performative.bytes = data_bytes - fipa_msg.inform.CopyFrom(performative) - else: - raise ValueError("Performative not valid: {}".format(performative_id)) - - fipa_bytes = fipa_msg.SerializeToString() - return fipa_bytes - - def decode(self, obj: bytes) -> Message: - """Decode bytes into a FIPA message.""" - fipa_pb = fipa_pb2.FIPAMessage() - fipa_pb.ParseFromString(obj) - message_id = fipa_pb.message_id - dialogue_id = fipa_pb.dialogue_id - target = fipa_pb.target - - performative = fipa_pb.WhichOneof("performative") - performative_id = FIPAMessage.Performative(str(performative)) - performative_content = dict() - if performative_id == FIPAMessage.Performative.CFP: - query_type = fipa_pb.cfp.WhichOneof("query") - if query_type == "nothing": - query = None - elif query_type == "query_bytes": - query = pickle.loads(fipa_pb.cfp.query_bytes) - elif query_type == "bytes": - query = fipa_pb.cfp.bytes - else: - raise ValueError("Query type not recognized.") - performative_content["query"] = query - elif performative_id == FIPAMessage.Performative.PROPOSE: - descriptions = [] - for p_bytes in fipa_pb.propose.proposal: - p = pickle.loads(p_bytes) # type: Description - descriptions.append(p) - performative_content["proposal"] = descriptions - elif performative_id == FIPAMessage.Performative.ACCEPT: - pass - elif performative_id == FIPAMessage.Performative.MATCH_ACCEPT: - pass - elif performative_id == FIPAMessage.Performative.ACCEPT_W_ADDRESS: - address = fipa_pb.accept_w_address.address - performative_content['address'] = address - elif performative_id == FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS: - address = fipa_pb.match_accept_w_address.address - performative_content['address'] = address - elif performative_id == FIPAMessage.Performative.DECLINE: - pass - elif performative_id == FIPAMessage.Performative.INFORM: - data = pickle.loads(fipa_pb.inform.bytes) - performative_content["data"] = data - else: - raise ValueError("Performative not valid: {}.".format(performative)) - - return FIPAMessage(message_id=message_id, dialogue_id=dialogue_id, target=target, - performative=performative, **performative_content) diff --git a/m_agent/skills/__init__.py b/m_agent/skills/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/m_agent/skills/error/__init__.py b/m_agent/skills/error/__init__.py deleted file mode 100644 index 96c80ac32c..0000000000 --- a/m_agent/skills/error/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the implementation of the error skill.""" diff --git a/m_agent/skills/error/behaviours.py b/m_agent/skills/error/behaviours.py deleted file mode 100644 index 556ee98ca7..0000000000 --- a/m_agent/skills/error/behaviours.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains the error behaviours.""" - -from aea.skills.base import Behaviour - - -class ErrorBehaviour(Behaviour): - """This class implements the error behaviour.""" - - def setup(self) -> None: - """ - Implement the setup. - - :return: None - """ - pass - - def act(self) -> None: - """ - Implement the act. - - :return: None - """ - pass - - def teardown(self) -> None: - """ - Implement the task teardown. - - :return: None - """ - pass diff --git a/m_agent/skills/error/handlers.py b/m_agent/skills/error/handlers.py deleted file mode 100644 index 098a61eced..0000000000 --- a/m_agent/skills/error/handlers.py +++ /dev/null @@ -1,131 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains the implementation of the handler for the 'default' protocol.""" -import base64 -import logging -from typing import Optional - -from aea.configurations.base import ProtocolId -from aea.mail.base import Envelope -from aea.protocols.base import Message, Protocol -from aea.protocols.default.message import DefaultMessage -from aea.protocols.default.serialization import DefaultSerializer -from aea.skills.base import Handler - -logger = logging.getLogger(__name__) - - -class ErrorHandler(Handler): - """This class implements the error handler.""" - - SUPPORTED_PROTOCOL = 'default' # type: Optional[ProtocolId] - - def setup(self) -> None: - """ - Implement the setup. - - :return: None - """ - pass - - def handle(self, message: Message, sender: str) -> None: - """ - Implement the reaction to an envelope. - - :param message: the message - :param sender: the sender - """ - pass - - def teardown(self) -> None: - """ - Implement the handler teardown. - - :return: None - """ - pass - - def send_unsupported_protocol(self, envelope: Envelope) -> None: - """ - Handle the received envelope in case the protocol is not supported. - - :param envelope: the envelope - :return: None - """ - logger.warning("Unsupported protocol: {}".format(envelope.protocol_id)) - reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.UNSUPPORTED_PROTOCOL.value, - error_msg="Unsupported protocol.", - error_data={"protocol_id": envelope.protocol_id}) - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, - protocol_id=DefaultMessage.protocol_id, - message=DefaultSerializer().encode(reply)) - - def send_decoding_error(self, envelope: Envelope) -> None: - """ - Handle a decoding error. - - :param envelope: the envelope - :return: None - """ - logger.warning("Decoding error: {}.".format(envelope)) - encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") - reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.DECODING_ERROR.value, - error_msg="Decoding error.", - error_data={"envelope": encoded_envelope}) - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, - protocol_id=DefaultMessage.protocol_id, - message=DefaultSerializer().encode(reply)) - - def send_invalid_message(self, envelope: Envelope) -> None: - """ - Handle an message that is invalid wrt a protocol. - - :param envelope: the envelope - :return: None - """ - logger.warning("Invalid message wrt protocol: {}.".format(envelope.protocol_id)) - encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") - reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.INVALID_MESSAGE.value, - error_msg="Invalid message.", - error_data={"envelope": encoded_envelope}) - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, - protocol_id=DefaultMessage.protocol_id, - message=DefaultSerializer().encode(reply)) - - def send_unsupported_skill(self, envelope: Envelope, protocol: Protocol) -> None: - """ - Handle the received envelope in case the skill is not supported. - - :param envelope: the envelope - :param protocol: the protocol - :return: None - """ - logger.warning("Cannot handle envelope: no handler registered for the protocol '{}'.".format(protocol.id)) - encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") - reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.UNSUPPORTED_SKILL.value, - error_msg="Unsupported skill.", - error_data={"envelope": encoded_envelope}) - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, - protocol_id=DefaultMessage.protocol_id, - message=DefaultSerializer().encode(reply)) diff --git a/m_agent/skills/error/skill.yaml b/m_agent/skills/error/skill.yaml deleted file mode 100644 index 70e9ceda1d..0000000000 --- a/m_agent/skills/error/skill.yaml +++ /dev/null @@ -1,14 +0,0 @@ -name: error -authors: Fetch.AI Limited -version: 0.1.0 -license: Apache 2.0 -url: "" -behaviours: [] -handlers: - - handler: - class_name: ErrorHandler - args: - foo: bar -tasks: [] -shared_classes: [] -protocols: ['default'] diff --git a/m_agent/skills/error/tasks.py b/m_agent/skills/error/tasks.py deleted file mode 100644 index 8922217537..0000000000 --- a/m_agent/skills/error/tasks.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains the implementation of the error tasks.""" - -from aea.skills.base import Task - - -class ErrorTask(Task): - """This class implements the error task.""" - - def setup(self) -> None: - """ - Implement the setup. - - :return: None - """ - pass - - def execute(self) -> None: - """ - Implement the task execution. - - :param envelope: the envelope - :return: None - """ - pass - - def teardown(self) -> None: - """ - Implement the task teardown. - - :return: None - """ - pass diff --git a/m_agent/skills/weather_station/__init__.py b/m_agent/skills/weather_station/__init__.py deleted file mode 100644 index 81d567366d..0000000000 --- a/m_agent/skills/weather_station/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the implementation of the default skill.""" diff --git a/m_agent/skills/weather_station/behaviours.py b/m_agent/skills/weather_station/behaviours.py deleted file mode 100644 index 4bce9b9bce..0000000000 --- a/m_agent/skills/weather_station/behaviours.py +++ /dev/null @@ -1,84 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains a scaffold of a behaviour.""" - -import logging - -from aea.skills.base import Behaviour -from typing import TYPE_CHECKING -from aea.protocols.oef.models import Description -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF - -if TYPE_CHECKING: - from packages.skills.weather_station.weather_station_data_model import WEATHER_STATION_DATAMODEL, SCHEME, SERVICE_ID -else: - from weather_station_skill.weather_station_data_model import WEATHER_STATION_DATAMODEL, SCHEME, SERVICE_ID - -logger = logging.getLogger("aea.weather_station_skill") - -REGISTER_ID = 1 - - -class MyWeatherBehaviour(Behaviour): - """This class scaffolds a behaviour.""" - - def __init__(self, **kwargs): - """Initialise the behaviour.""" - super().__init__(**kwargs) - self.registered = False - self.data_model = WEATHER_STATION_DATAMODEL() - self.scheme = SCHEME - - def setup(self) -> None: - """ - Implement the setup. - - :return: None - """ - print(self.context.agent_addresses) - - def act(self) -> None: - """ - Implement the act. - - :return: None - """ - if not self.registered: - desc = Description(self.scheme, data_model=self.data_model) - msg = OEFMessage(oef_type=OEFMessage.Type.REGISTER_SERVICE, - id=REGISTER_ID, - service_description=desc, - service_id=SERVICE_ID) - msg_bytes = OEFSerializer().encode(msg) - self.context.outbox.put_message(to=DEFAULT_OEF, - sender=self.context.agent_public_key, - protocol_id=OEFMessage.protocol_id, - message=msg_bytes) - logger.info("[{}]: registered! My public key is : {}".format(self.context.agent_name, self.context.agent_public_key)) - self.registered = True - - def teardown(self) -> None: - """ - Implement the task teardown. - - :return: None - """ - pass diff --git a/m_agent/skills/weather_station/db_communication.py b/m_agent/skills/weather_station/db_communication.py deleted file mode 100644 index 2dfe79061a..0000000000 --- a/m_agent/skills/weather_station/db_communication.py +++ /dev/null @@ -1,70 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains the Database Communication for the weather agent.""" - -import datetime -import os.path -import sqlite3 -from typing import Dict, cast - -my_path = os.path.dirname(__file__) - -DB_SOURCE = os.path.join(my_path, 'dummy_weather_station_data.db') - - -class DBCommunication: - """A class to communicate with a database.""" - - def __init__(self): - """ - Initialize the database communication. - - :param source: the source - """ - self.source = DB_SOURCE - - def db_connection(self) -> sqlite3.Connection: - """ - Get db connection. - - :return: the db connection - """ - con = sqlite3.connect(self.source) - return con - - def get_data_for_specific_dates(self, start_date: str, end_date: str) -> Dict[str, int]: - """ - Get data for specific dates. - - :param start_date: the start date - :param end_date: the end date - :return: the data - """ - con = self.db_connection() - cur = con.cursor() - start_dt = datetime.datetime.strptime(start_date, '%d/%m/%Y') - start = start_dt.strftime('%s') - end_dt = datetime.datetime.strptime(end_date, '%d/%m/%Y') - end = end_dt.strftime('%s') - cur.execute("SELECT * FROM data WHERE idx BETWEEN ? AND ?", (str(start), str(end))) - data = cast(Dict[str, int], cur.fetchall()) - cur.close() - con.close() - return data diff --git a/m_agent/skills/weather_station/dummy_weather_station_data.db b/m_agent/skills/weather_station/dummy_weather_station_data.db deleted file mode 100644 index ff0d00d99ac64c48c8e6ec126477e1013c67f479..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8192 zcmeH~J8UCG7{|Ty+J|@T#Ez4T@3_p^@jAKZI^J=dyByqu_<0U}4 zUL`^ZiGuPPS_%pzD368~DkvzAf)*MoIugEhGP9fNauO2Fjy$rR{pFi){@?%mqTIc` zzC9Z9cOLIN9E^A~Gm$Zj%yrH)nanC|1Y0cwJ`nW<-lOj?TC15$XTJ4-GQ3|h@Jd&z zK&n8hK&n8hK&n8hK&n8hK&n8hK&rrhq`)T^4RfYcGCsRD8r*v@+!~Aq(MGrKtlijH z;~O__uCMXvCw#%-FIt0pyPHpThP%6aJ3|fu)-SyHceaKP1|OajzQ6ZybNkWD(2w^< zC&jn7KHwW`Z*QC+>7BvJvltB@K6#0x$M{QR{r>i&tf8MeaS6k<%QBnXH>=UazFb2$k5WfwCl+DQcOoBq-f(pr(7 zQjQ?Xmt({>@II=(u92WK>mV#zF<}^xiKr{0EjtBDOf)AoVp$M`)qRZwy&_{g)PP~w zkgI=Q0n4&CRbaMgCI|>Y=4+Y=x@AVQOe`glNHm>D3xecL56?9;VnwQ<`l5=`3c9nd zDFO|E^q`37C_u_yagq{t_OeDS8*(-0U)2cc>hMeYxdDnD;JQ{Ul>O5VCE{XCM28@; zpdy5j#e_wvxNrS7NFX-oUY81K$bK0tBI*eOf}l!Q6G3m(F z#bLD-6{01(Q^;cCm6(WZLWMG`Y(eW42xk0D0)bO#MF#=9mY8jd(+L9ZLVeW~QU<*O zp_K5lM&P>mfclqJCScgI=P)s=iLp;;t*wcmQ-Ur){8R#gu3*|H5)3<=$Ar(s#NkX^ zRY96C1Z;}MbWDuNsN7FDaw}nG{VzL@|j# zi|R=s8$st(o;s8l5(rcnS8k{%0ZcU)j5L`bpgg3mOfA8&QN>k7#5n;!NN--U|6Bd7Pf9yE;Up0l7*%~C7_lY7Ow4VKL~hz=LRBbi1^t4X zCBo9g;hig2)Pr@Gs0jv79&yrkgAeD457@}MuNsf%6w;KvQ+W#stO-aK@8haY7F;`>jV21n3(o{>SkQ$2(VyaG2(~Rv Wf}4bqM8LMS>*}1Ppg)7Li~j|ndVIV9 diff --git a/m_agent/skills/weather_station/dummy_weather_station_data.py b/m_agent/skills/weather_station/dummy_weather_station_data.py deleted file mode 100644 index 126b091e3d..0000000000 --- a/m_agent/skills/weather_station/dummy_weather_station_data.py +++ /dev/null @@ -1,122 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains dummy weather station data.""" - -import datetime -import logging -import os.path -import random -import sqlite3 -import time -from typing import Dict, Union - -logger = logging.getLogger("aea.weather_station_skill") - -my_path = os.path.dirname(__file__) - -DB_SOURCE = os.path.join(my_path, 'dummy_weather_station_data.db') - -# Checking if the database exists -con = sqlite3.connect(DB_SOURCE) -cur = con.cursor() - -cur.close() -con.commit() -con.close() - -# Create a table if it doesn't exist' -command = (''' CREATE TABLE IF NOT EXISTS data ( - abs_pressure REAL, - delay REAL, - hum_in REAL, - hum_out REAL, - idx TEXT, - rain REAL, - temp_in REAL, - temp_out REAL, - wind_ave REAL, - wind_dir REAL, - wind_gust REAL)''') - -con = sqlite3.connect(DB_SOURCE) -cur = con.cursor() -cur.execute(command) -cur.close() -con.commit() -if con is not None: - logger.info("Wheather station: I closed the db after checking it is populated!") - con.close() - - -class Forecast(): - """Represents a whether forecast.""" - - def add_data(self, tagged_data: Dict[str, Union[int, datetime.datetime]]) -> None: - """ - Add data to the forecast. - - :param tagged_data: the data dictionary - :return: None - """ - con = sqlite3.connect(DB_SOURCE) - cur = con.cursor() - cur.execute('''INSERT INTO data(abs_pressure, - delay, - hum_in, - hum_out, - idx, - rain, - temp_in, - temp_out, - wind_ave, - wind_dir, - wind_gust) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', - (tagged_data['abs_pressure'], - tagged_data['delay'], - tagged_data['hum_in'], - tagged_data['hum_out'], - datetime.datetime.now().strftime('%s'), - tagged_data['rain'], - tagged_data['temp_in'], - tagged_data['temp_out'], - tagged_data['wind_ave'], - tagged_data['wind_dir'], - tagged_data['wind_gust'])) - logger.info("Wheather station: I added data in the db!") - cur.close() - con.commit() - con.close() - - def generate(self): - """Generate weather data.""" - while True: - dict_of_data = {'abs_pressure': random.randrange(1022.0, 1025, 1), 'delay': random.randint(2, 7), - 'hum_in': random.randrange(33.0, 40.0, 1), 'hum_out': random.randrange(33.0, 80.0, 1), - 'idx': datetime.datetime.now(), 'rain': random.randrange(70.0, 74.0, 1), - 'temp_in': random.randrange(18, 28, 1), 'temp_out': random.randrange(2, 20, 1), - 'wind_ave': random.randrange(0, 10, 1), 'wind_dir': random.randrange(0, 14, 1), - 'wind_gust': random.randrange(1, 7, 1)} # type: Dict[str, Union[int, datetime.datetime]] - self.add_data(dict_of_data) - time.sleep(5) - - -if __name__ == '__main__': - a = Forecast() - a.generate() diff --git a/m_agent/skills/weather_station/handlers.py b/m_agent/skills/weather_station/handlers.py deleted file mode 100644 index cc0c478fe4..0000000000 --- a/m_agent/skills/weather_station/handlers.py +++ /dev/null @@ -1,165 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains a scaffold of a handler.""" - -import logging -import json -import time -from typing import Any, Dict, List, Optional, Union, cast, TYPE_CHECKING - -from aea.configurations.base import ProtocolId -from aea.protocols.base import Message -from aea.protocols.default.message import DefaultMessage -from aea.protocols.default.serialization import DefaultSerializer -from aea.protocols.fipa.message import FIPAMessage -from aea.protocols.fipa.serialization import FIPASerializer -from aea.protocols.oef.models import Description -from aea.skills.base import Handler - -if TYPE_CHECKING: - from packages.skills.weather_station.db_communication import DBCommunication -else: - from weather_station_skill.db_communication import DBCommunication - -logger = logging.getLogger("aea.weather_station_skill") - -DATE_ONE = "3/10/2019" -DATE_TWO = "15/10/2019" - - -class MyWeatherHandler(Handler): - """This class scaffolds a handler.""" - - SUPPORTED_PROTOCOL = FIPAMessage.protocol_id # type: Optional[ProtocolId] - - def __init__(self, **kwargs): - """Initialise the behaviour.""" - super().__init__(**kwargs) - self.fet_price = 0.002 - self.db = DBCommunication() - self.fetched_data = [] - - def setup(self) -> None: - """Implement the setup for the handler.""" - pass - - def handle(self, message: Message, sender: str) -> None: - """ - Implement the reaction to an message. - - :param message: the message - :param sender: the sender - :return: None - """ - fipa_msg = cast(FIPAMessage, message) - msg_performative = FIPAMessage.Performative(fipa_msg.get('performative')) - message_id = cast(int, fipa_msg.get('id')) - dialogue_id = cast(int, fipa_msg.get('dialogue_id')) - - if msg_performative == FIPAMessage.Performative.CFP: - self.handle_cfp(fipa_msg, sender, message_id, dialogue_id) - elif msg_performative == FIPAMessage.Performative.ACCEPT: - self.handle_accept(sender) - - def teardown(self) -> None: - """ - Implement the handler teardown. - - :return: None - """ - pass - - def handle_cfp(self, msg: FIPAMessage, sender: str, message_id: int, dialogue_id: int) -> None: - """ - Handle the CFP calls. - - :param msg: the message - :param sender: the sender - :param message_id: the message id - :param dialogue_id: the dialogue id - :return: None - """ - new_message_id = message_id + 1 - new_target = message_id - fetched_data = self.db.get_data_for_specific_dates(DATE_ONE, DATE_TWO) - - if len(fetched_data) >= 1: - self.fetched_data = fetched_data - total_price = self.fet_price * len(fetched_data) - proposal = [Description({"Rows": len(fetched_data), - "Price": total_price})] - logger.info("[{}]: sending sender={} a proposal at price={}".format(self.context.agent_name, sender, total_price)) - proposal_msg = FIPAMessage(message_id=new_message_id, - dialogue_id=dialogue_id, - target=new_target, - performative=FIPAMessage.Performative.PROPOSE, - proposal=proposal) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, - protocol_id=FIPAMessage.protocol_id, - message=FIPASerializer().encode(proposal_msg)) - else: - logger.info("[{}]: declined the CFP from sender={}".format(self.context.agent_name, sender)) - decline_msg = FIPAMessage(message_id=new_message_id, - dialogue_id=dialogue_id, - target=new_target, - performative=FIPAMessage.Performative.DECLINE) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, - protocol_id=FIPAMessage.protocol_id, - message=FIPASerializer().encode(decline_msg)) - - def handle_accept(self, sender: str) -> None: - """ - Handle the Accept Calls. - - :param sender: the sender - :return: None - """ - command = {} # type: Dict[str, Union[str, List[Any]]] - command['Command'] = "success" - command['fetched_data'] = [] - counter = 0 - for items in self.fetched_data: - dict_of_data = {} - dict_of_data['abs_pressure'] = items[0] - dict_of_data['delay'] = items[1] - dict_of_data['hum_in'] = items[2] - dict_of_data['hum_out'] = items[3] - dict_of_data['idx'] = time.ctime(int(items[4])) - dict_of_data['rain'] = items[5] - dict_of_data['temp_in'] = items[6] - dict_of_data['temp_out'] = items[7] - dict_of_data['wind_ave'] = items[8] - dict_of_data['wind_dir'] = items[9] - dict_of_data['wind_gust'] = items[10] - command['fetched_data'].append(dict_of_data) # type: ignore - counter += 1 - if counter == 10: - break - json_data = json.dumps(command) - json_bytes = json_data.encode("utf-8") - logger.info("[{}]: handling accept and sending weather data to sender={}".format(self.context.agent_name, sender)) - data_msg = DefaultMessage( - type=DefaultMessage.Type.BYTES, content=json_bytes) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, - protocol_id=DefaultMessage.protocol_id, - message=DefaultSerializer().encode(data_msg)) diff --git a/m_agent/skills/weather_station/skill.yaml b/m_agent/skills/weather_station/skill.yaml deleted file mode 100644 index 4630a25e9f..0000000000 --- a/m_agent/skills/weather_station/skill.yaml +++ /dev/null @@ -1,25 +0,0 @@ -name: weather_station -authors: Fetch.AI Limited -version: 0.1.0 -license: Apache 2.0 -url: "" -behaviours: - - behaviour: - class_name: MyWeatherBehaviour - args: - foo: bar -handlers: - - handler: - class_name: MyWeatherHandler - args: - foo: bar -tasks: - - task: - class_name: MyWeatherTask - args: - foo: bar -shared_classes: [] -protocols: ['fipa'] - - - diff --git a/m_agent/skills/weather_station/tasks.py b/m_agent/skills/weather_station/tasks.py deleted file mode 100644 index 9458f5b11e..0000000000 --- a/m_agent/skills/weather_station/tasks.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains a scaffold of a task.""" - -from aea.skills.base import Task - - -class MyWeatherTask(Task): - """This class scaffolds a task.""" - - def setup(self) -> None: - """ - Implement the setup. - - :return: None - """ - pass - - def execute(self) -> None: - """ - Implement the task execution. - - :return: None - """ - pass - - def teardown(self) -> None: - """ - Implement the task teardown. - - :return: None - """ - pass diff --git a/m_agent/skills/weather_station/weather_station_data_model.py b/m_agent/skills/weather_station/weather_station_data_model.py deleted file mode 100644 index 43783bb541..0000000000 --- a/m_agent/skills/weather_station/weather_station_data_model.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains the dataModel for the weather agent.""" - -from aea.protocols.oef.models import DataModel, Attribute - -SCHEME = {'country': "UK", 'city': "Cambridge"} -SERVICE_ID = "WeatherData" - - -class WEATHER_STATION_DATAMODEL (DataModel): - """Data model for the weather Agent.""" - - def __init__(self): - """Initialise the dataModel.""" - self.ATTRIBUTE_COUNTRY = Attribute("country", str, True) - self.ATTRIBUTE_CITY = Attribute("city", str, True) - - super().__init__("weather_station_datamodel", [self.ATTRIBUTE_COUNTRY, - self.ATTRIBUTE_CITY]) From 0c92bf81b7ddca2e9eebc5f46e49273b19230e24 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Tue, 15 Oct 2019 13:55:38 +0100 Subject: [PATCH 06/71] Fixes version on setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 12d3852e45..608d394bc1 100644 --- a/setup.py +++ b/setup.py @@ -68,7 +68,7 @@ def get_all_extras() -> Dict: cli_gui = [ "flask", - "connexion[swagger-ui]" + "connexion[swagger-ui]==2018.0.dev1" ] cli_deps = [ From 666105ae88f4f205d431e5c0360b204f220d9452 Mon Sep 17 00:00:00 2001 From: Aristotelis Date: Tue, 15 Oct 2019 14:00:23 +0100 Subject: [PATCH 07/71] Exposed the addresses to the context of an agent --- aea/aea.py | 1 + aea/crypto/base.py | 17 +++++++++++++++++ aea/crypto/ethereum_base.py | 2 +- aea/crypto/fetchai_base.py | 2 +- aea/crypto/helpers.py | 12 ++---------- aea/crypto/wallet.py | 7 +++++++ 6 files changed, 29 insertions(+), 12 deletions(-) diff --git a/aea/aea.py b/aea/aea.py index 2a49d5ec0f..973286d2e4 100644 --- a/aea/aea.py +++ b/aea/aea.py @@ -67,6 +67,7 @@ def __init__(self, name: str, self._decision_maker = DecisionMaker(self.max_reactions, self.outbox) self._context = AgentContext(self.name, self.wallet.public_keys, + self.wallet.crypto_addresses, self.outbox, self.decision_maker.message_queue, self.decision_maker.ownership_state, diff --git a/aea/crypto/base.py b/aea/crypto/base.py index d1a28c9a0b..b87599c981 100644 --- a/aea/crypto/base.py +++ b/aea/crypto/base.py @@ -56,6 +56,14 @@ def public_key(self) -> str: :return: a public key string """ + @abstractmethod + def address(self) -> str: + """ + Returns the address. + + :return: an address string + """ + class DefaultCrypto(Crypto): @@ -91,6 +99,15 @@ def public_key_pem(self) -> bytes: """ return self._public_key_pem + @property + def address(self) -> str: + """ + Return a 219 character public key in base58 format. + + :return: a public key string in base58 format + """ + return self._public_key_b58 + @property def fingerprint(self) -> str: """ diff --git a/aea/crypto/ethereum_base.py b/aea/crypto/ethereum_base.py index c17e412115..97b9703856 100644 --- a/aea/crypto/ethereum_base.py +++ b/aea/crypto/ethereum_base.py @@ -58,7 +58,7 @@ def public_key(self) -> str: return self._public_key @property - def display_address(self) -> str: + def address(self) -> str: """ Return the display_address for the key pair. diff --git a/aea/crypto/fetchai_base.py b/aea/crypto/fetchai_base.py index d14d26afca..88aeed1aea 100644 --- a/aea/crypto/fetchai_base.py +++ b/aea/crypto/fetchai_base.py @@ -60,7 +60,7 @@ def private_key(self) -> str: return self._entity.private_key_hex @property - def display_address(self) -> str: + def address(self) -> str: """ Return the display_address for the key pair. diff --git a/aea/crypto/helpers.py b/aea/crypto/helpers.py index de9b2a9fee..497339b80c 100644 --- a/aea/crypto/helpers.py +++ b/aea/crypto/helpers.py @@ -19,13 +19,13 @@ # ------------------------------------------------------------------------------ """Module wrapping the helpers of public and private key cryptography.""" -from typing import cast, Dict +from typing import cast from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption import logging from pathlib import Path -from fetchai.ledger.crypto import Entity, Address # type: ignore +from fetchai.ledger.crypto import Entity # type: ignore from eth_account import Account # type: ignore from aea.crypto.base import DefaultCrypto @@ -78,15 +78,8 @@ def _verify_or_create_private_keys(ctx: Context) -> None: fetchai_private_key_path = FETCHAI_PRIVATE_KEY_FILE fetchai_private_key_config = PrivateKeyPathConfig(FETCHAI, fetchai_private_key_path) aea_conf.private_key_paths.create(fetchai_private_key_config.ledger, fetchai_private_key_config) - aea_conf.addresses = cast(Dict[str, str], (FETCHAI, Address(entity).to_hex())) else: - path = Path(FETCHAI_PRIVATE_KEY_FILE) fetchai_private_key_config = cast(PrivateKeyPathConfig, fetchai_private_key_config) - with open(path, "r") as file: - pk = file.read() - entity = Entity.from_hex(pk) - adr = Address(entity).to_hex() - aea_conf.addresses = cast(Dict[str, str], (FETCHAI, adr)) try: _try_validate_fet_private_key_path(fetchai_private_key_config.path) except FileNotFoundError: @@ -102,7 +95,6 @@ def _verify_or_create_private_keys(ctx: Context) -> None: ethereum_private_key_path = ETHEREUM_PRIVATE_KEY_FILE ethereum_private_key_config = PrivateKeyPathConfig(ETHEREUM, ethereum_private_key_path) aea_conf.private_key_paths.create(ethereum_private_key_config.ledger, ethereum_private_key_config) - aea_conf.addresses = cast(Dict[str, str], (ETHEREUM, account.address)) else: ethereum_private_key_config = cast(PrivateKeyPathConfig, ethereum_private_key_config) try: diff --git a/aea/crypto/wallet.py b/aea/crypto/wallet.py index 9a2cc1c79a..6c655762b0 100644 --- a/aea/crypto/wallet.py +++ b/aea/crypto/wallet.py @@ -42,6 +42,7 @@ def __init__(self, private_key_paths: Dict[str, str]): """ crypto_objects = {} # type: Dict[str, Crypto] public_keys = {} # type: Dict[str, str] + addresses = {} # type: Dict[str, str] for identifier, path in private_key_paths.items(): if identifier == DEFAULT: crypto_objects[identifier] = DefaultCrypto(path) @@ -53,9 +54,11 @@ def __init__(self, private_key_paths: Dict[str, str]): ValueError("Unsupported identifier in private key paths.") crypto = cast(Crypto, crypto_objects.get(identifier)) public_keys[identifier] = cast(str, crypto.public_key) + addresses[identifier] = cast(str, crypto.address) self._crypto_objects = crypto_objects self._public_keys = public_keys + self._crypto_addresses = addresses @property def public_keys(self): @@ -66,3 +69,7 @@ def public_keys(self): def crypto_objects(self): """Get the crypto objects (key pair).""" return self._crypto_objects + + @property + def crypto_addresses(self): + return self._crypto_addresses From 2b153044632118067f11520648dfb8f42f241a95 Mon Sep 17 00:00:00 2001 From: Diarmid Campbell Date: Tue, 15 Oct 2019 14:08:49 +0100 Subject: [PATCH 08/71] turned out reloading as it was happening spuriously, fixed bug where atdout was not beign piped through to gui and fixed from grammar issues in the ui --- aea/cli_gui/__init__.py | 17 ++++++++++------- aea/cli_gui/templates/home.html | 6 +++--- aea/cli_gui/templates/home.js | 2 +- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/aea/cli_gui/__init__.py b/aea/cli_gui/__init__.py index fefba0c360..02fed521b0 100644 --- a/aea/cli_gui/__init__.py +++ b/aea/cli_gui/__init__.py @@ -23,6 +23,7 @@ from enum import Enum import glob import io +import logging import os import subprocess import threading @@ -76,7 +77,7 @@ def read_description(dir_name, yaml_name): if "description" in yaml_data: return yaml_data["description"] except yaml.YAMLError as exc: - print(exc) + logging.error(exc) return "Placeholder description" @@ -201,7 +202,9 @@ def _call_aea(param_list, dir): def _call_aea_async(param_list, dir): old_cwd = os.getcwd() os.chdir(dir) - ret = subprocess.Popen(param_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + env = os.environ.copy() + env["PYTHONUNBUFFERED"] = "1" + ret = subprocess.Popen(param_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) os.chdir(old_cwd) return ret @@ -303,7 +306,7 @@ def start_agent(agent_id): def _read_tty(process, str_list): for line in io.TextIOWrapper(process.stdout, encoding="utf-8"): - # print(line) + logging.info("stdout: " + line.replace("\n", "")) str_list.append(line) str_list.append("process terminated\n") @@ -311,7 +314,7 @@ def _read_tty(process, str_list): def _read_error(process, str_list): for line in io.TextIOWrapper(process.stderr, encoding="utf-8"): - # print("Error:" + line) + logging.error("stderr: " + line.replace("\n", "")) str_list.append(line) str_list.append("process terminated\n") @@ -380,7 +383,7 @@ def get_process_status(process_id) -> ProcessState: def _kill_running_oef_nodes(): - print("Kill off any existing OEF nodes which are running...") + logging.info("Kill off any existing OEF nodes which are running...") subprocess.call(['docker', 'kill', oef_node_name]) @@ -399,7 +402,7 @@ def run(): @app.route('/') def home(): - """Respond to browser URL: localhost:5000/ .""" + """Respond to browser URL: localhost:5000/.""" return flask.render_template('home.html', len=len(elements), htmlElements=elements) @app.route('/static/js/home.js') @@ -413,7 +416,7 @@ def favicon(): return flask.send_from_directory( os.path.join(app.root_path, 'static'), 'favicon.ico', mimetype='image/vnd.microsoft.icon') - app.run(host='127.0.0.1', port=8080, debug=True) + app.run(host='127.0.0.1', port=8080, debug=False) # If we're running in stand alone mode, run the application diff --git a/aea/cli_gui/templates/home.html b/aea/cli_gui/templates/home.html index 9fa5e80562..5f2dfbb3f7 100644 --- a/aea/cli_gui/templates/home.html +++ b/aea/cli_gui/templates/home.html @@ -51,7 +51,7 @@

Selected Agent: NONE
- + @@ -80,14 +80,14 @@

Selected {{htmlElements[i][1]}}:
- +

-

Running NONE Agent

+

Running "NONE" Agent

Agent Status: NONE

diff --git a/aea/cli_gui/templates/home.js b/aea/cli_gui/templates/home.js index 67c6c832dc..405e783d19 100644 --- a/aea/cli_gui/templates/home.js +++ b/aea/cli_gui/templates/home.js @@ -618,7 +618,7 @@ class Controller{ } } if (agentSelectionId != "NONE"){ - $('.localItemHeading').html(agentSelectionId + "'s"); + $('.localItemHeading').html(agentSelectionId); } else{ $('.localItemHeading').html("Local"); From f853ecad2f5013db637eab3a9b840e6f5eda4a49 Mon Sep 17 00:00:00 2001 From: Aristotelis Date: Tue, 15 Oct 2019 14:11:33 +0100 Subject: [PATCH 09/71] Chagnes based on the tests --- aea/configurations/base.py | 2 +- aea/context/base.py | 1 + aea/crypto/base.py | 3 +-- aea/crypto/wallet.py | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/aea/configurations/base.py b/aea/configurations/base.py index 3b51b6d54d..b106ce8136 100644 --- a/aea/configurations/base.py +++ b/aea/configurations/base.py @@ -435,7 +435,7 @@ def __init__(self, self.url = url self.registry_path = registry_path self.private_key_paths = CRUDCollection[PrivateKeyPathConfig]() - self.addresses = None #type: Dict[str, str] + self.addresses = {} # type: Dict[str, str] private_key_paths = private_key_paths if private_key_paths is not None else {} for ledger, path in private_key_paths.items(): diff --git a/aea/context/base.py b/aea/context/base.py index fee338e189..4cb7765fe0 100644 --- a/aea/context/base.py +++ b/aea/context/base.py @@ -67,6 +67,7 @@ def agent_name(self) -> str: def public_keys(self) -> Dict[str, str]: """Get public keys.""" return self._public_keys + @property def addresses(self) -> Dict[str, str]: """Get addresses.""" diff --git a/aea/crypto/base.py b/aea/crypto/base.py index b87599c981..7bd92f15b6 100644 --- a/aea/crypto/base.py +++ b/aea/crypto/base.py @@ -59,13 +59,12 @@ def public_key(self) -> str: @abstractmethod def address(self) -> str: """ - Returns the address. + Return the address. :return: an address string """ - class DefaultCrypto(Crypto): """Class wrapping the public and private key cryptography.""" diff --git a/aea/crypto/wallet.py b/aea/crypto/wallet.py index 6c655762b0..60fde022b2 100644 --- a/aea/crypto/wallet.py +++ b/aea/crypto/wallet.py @@ -72,4 +72,5 @@ def crypto_objects(self): @property def crypto_addresses(self): + """Get the crypt addresses.""" return self._crypto_addresses From 1eff0f299b26569f17afa0ebe0fd396b826263e9 Mon Sep 17 00:00:00 2001 From: Diarmid Campbell Date: Tue, 15 Oct 2019 14:14:01 +0100 Subject: [PATCH 10/71] fixed flake8 issue --- aea/cli_gui/__main__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aea/cli_gui/__main__.py b/aea/cli_gui/__main__.py index 3e1d1e6900..9dd8cbe954 100644 --- a/aea/cli_gui/__main__.py +++ b/aea/cli_gui/__main__.py @@ -23,4 +23,3 @@ # If we're running in stand alone mode, run the application if __name__ == '__main__': aea.cli_gui.run() - From da3bed72f9d6e1e09b0b4e734db68f870f4b00f6 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 15 Oct 2019 15:14:34 +0200 Subject: [PATCH 11/71] make 'cli_gui' depend on 'cli' and not the other way around. --- aea/cli/__main__.py | 2 +- setup.py | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/aea/cli/__main__.py b/aea/cli/__main__.py index 47bf932582..ce357cf095 100755 --- a/aea/cli/__main__.py +++ b/aea/cli/__main__.py @@ -40,7 +40,6 @@ from aea.cli.scaffold import scaffold from aea.cli.search import search from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE, AgentConfig -import aea.cli_gui DEFAULT_CONNECTION = "oef" DEFAULT_SKILL = "error" @@ -127,6 +126,7 @@ def freeze(ctx: Context): @pass_ctx def gui(ctx: Context): """Run the CLI GUI.""" + import aea.cli_gui logger.info("Running the GUI.....(press Ctrl+C to exit)") aea.cli_gui.run() diff --git a/setup.py b/setup.py index 608d394bc1..436a3c2abe 100644 --- a/setup.py +++ b/setup.py @@ -66,21 +66,21 @@ def get_all_extras() -> Dict: *ethereum_deps ] - cli_gui = [ - "flask", - "connexion[swagger-ui]==2018.0.dev1" - ] - cli_deps = [ "click", "click_log", "PyYAML", "jsonschema", "python-dotenv", - *cli_gui, *crypto_deps ] + cli_gui = [ + "flask", + "connexion[swagger-ui] @ git+https://github.com/neverpanic/connexion.git@jsonschema-3#egg=connexion[swagger-ui]", + *cli_deps + ] + extras = { "cli": cli_deps, "cli_gui": cli_gui, @@ -130,9 +130,6 @@ def get_all_extras() -> Dict: *all_extras.get("cli", []), *all_extras.get("oef_connection", []), ], - dependency_links=[ - 'git+https://github.com/neverpanic/connexion.git@jsonschema-3#egg=connexion[swagger-ui]', - ], tests_require=["tox"], extras_require=all_extras, entry_points={ From 3ab6d6e448279f769e7841b098ad5dd1e4dafc5d Mon Sep 17 00:00:00 2001 From: Aristotelis Date: Tue, 15 Oct 2019 14:31:20 +0100 Subject: [PATCH 12/71] Changes based on the PR comments --- aea/aea.py | 2 +- aea/configurations/base.py | 1 - aea/context/base.py | 5 +++++ aea/crypto/ethereum_base.py | 2 +- aea/crypto/fetchai_base.py | 2 +- aea/crypto/wallet.py | 6 +++--- aea/skills/base.py | 5 +++++ 7 files changed, 16 insertions(+), 7 deletions(-) diff --git a/aea/aea.py b/aea/aea.py index 973286d2e4..8b60f61c37 100644 --- a/aea/aea.py +++ b/aea/aea.py @@ -67,7 +67,7 @@ def __init__(self, name: str, self._decision_maker = DecisionMaker(self.max_reactions, self.outbox) self._context = AgentContext(self.name, self.wallet.public_keys, - self.wallet.crypto_addresses, + self.wallet.addresses, self.outbox, self.decision_maker.message_queue, self.decision_maker.ownership_state, diff --git a/aea/configurations/base.py b/aea/configurations/base.py index b106ce8136..584932a7c5 100644 --- a/aea/configurations/base.py +++ b/aea/configurations/base.py @@ -435,7 +435,6 @@ def __init__(self, self.url = url self.registry_path = registry_path self.private_key_paths = CRUDCollection[PrivateKeyPathConfig]() - self.addresses = {} # type: Dict[str, str] private_key_paths = private_key_paths if private_key_paths is not None else {} for ledger, path in private_key_paths.items(): diff --git a/aea/context/base.py b/aea/context/base.py index 4cb7765fe0..a6cd06b4ab 100644 --- a/aea/context/base.py +++ b/aea/context/base.py @@ -73,6 +73,11 @@ def addresses(self) -> Dict[str, str]: """Get addresses.""" return self._addresses + @property + def address(self) -> str: + """Get the defualt address.""" + return self._addresses['default'] + @property def public_key(self) -> str: """Get the default public key.""" diff --git a/aea/crypto/ethereum_base.py b/aea/crypto/ethereum_base.py index 97b9703856..a75b3d2adb 100644 --- a/aea/crypto/ethereum_base.py +++ b/aea/crypto/ethereum_base.py @@ -60,7 +60,7 @@ def public_key(self) -> str: @property def address(self) -> str: """ - Return the display_address for the key pair. + Return the address for the key pair. :return: a display_address str """ diff --git a/aea/crypto/fetchai_base.py b/aea/crypto/fetchai_base.py index 88aeed1aea..00650fc299 100644 --- a/aea/crypto/fetchai_base.py +++ b/aea/crypto/fetchai_base.py @@ -62,7 +62,7 @@ def private_key(self) -> str: @property def address(self) -> str: """ - Return the display_address for the key pair. + Return the address for the key pair. :return: a display_address str """ diff --git a/aea/crypto/wallet.py b/aea/crypto/wallet.py index 60fde022b2..f50d26c223 100644 --- a/aea/crypto/wallet.py +++ b/aea/crypto/wallet.py @@ -58,7 +58,7 @@ def __init__(self, private_key_paths: Dict[str, str]): self._crypto_objects = crypto_objects self._public_keys = public_keys - self._crypto_addresses = addresses + self._addresses = addresses @property def public_keys(self): @@ -71,6 +71,6 @@ def crypto_objects(self): return self._crypto_objects @property - def crypto_addresses(self): + def addresses(self): """Get the crypt addresses.""" - return self._crypto_addresses + return self._addresses diff --git a/aea/skills/base.py b/aea/skills/base.py index 840c41e31c..dc591830c0 100644 --- a/aea/skills/base.py +++ b/aea/skills/base.py @@ -70,6 +70,11 @@ def agent_addresses(self) -> Dict[str, str]: """Get addresses.""" return self._agent_context.addresses + @property + def agent_address(self) -> str: + """ Get address""" + return self._agent_context.address + @property def outbox(self) -> OutBox: """Get outbox.""" From 74d7a3219b2ec09c5d7291b1d646b3d2b30816e4 Mon Sep 17 00:00:00 2001 From: Aristotelis Date: Tue, 15 Oct 2019 14:35:13 +0100 Subject: [PATCH 13/71] Changes based on the Test --- aea/skills/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aea/skills/base.py b/aea/skills/base.py index dc591830c0..186a6d9c64 100644 --- a/aea/skills/base.py +++ b/aea/skills/base.py @@ -72,8 +72,8 @@ def agent_addresses(self) -> Dict[str, str]: @property def agent_address(self) -> str: - """ Get address""" - return self._agent_context.address + """Get address.""" + return self._agent_context.address @property def outbox(self) -> OutBox: From c2d77312e8954dc2adb9ff2d7a0bb15cb91b3f82 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 15 Oct 2019 17:21:24 +0200 Subject: [PATCH 14/71] Put log messages to stdout with default config. this change implied removing the 'click_log' dependency and reimplementing part of the functionalities that the package provided. --- Pipfile | 3 +- Pipfile.lock | 80 ++++++++++++++++++----------------------- aea/cli/__main__.py | 4 +-- aea/cli/common.py | 4 +-- aea/cli/loggers.py | 87 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 127 insertions(+), 51 deletions(-) create mode 100644 aea/cli/loggers.py diff --git a/Pipfile b/Pipfile index 4d0507dae3..3cc92f43a0 100644 --- a/Pipfile +++ b/Pipfile @@ -28,13 +28,12 @@ base58 = "*" docker = "*" click = "*" pyyaml = ">=4.2b1" -click-log = "*" oef = {index = "test-pypi",version = "==0.6.10"} colorlog = "*" jsonschema = "*" protobuf = "*" flask = "*" -connexion = {git = "https://github.com/neverpanic/connexion.git", editable = true, ref = "jsonschema-3"} +connexion = {git = "https://github.com/neverpanic/connexion.git",editable = true,ref = "jsonschema-3"} watchdog = "*" python-dotenv = "*" fetchai-ledger-api = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 3917e231cd..26fa2b2a07 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f1e84c9d1a2d43fb29b340be13d1731f89700fe09e4afc06222ef2e60efa40a9" + "sha256": "eeff9998b99b29ddb9eafda38d9f37c6493fc54c6a63333cdb8945c31e7f30f9" }, "pipfile-spec": 6, "requires": { @@ -44,10 +44,10 @@ }, "attrs": { "hashes": [ - "sha256:ec20e7a4825331c1b5ebf261d111e16fa9612c1f7a5e1f884f12bd53a664dfd2", - "sha256:f913492e1663d3c36f502e5e9ba6cd13cf19d7fab50aa13239e420fef95e1396" + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" ], - "version": "==19.2.0" + "version": "==19.3.0" }, "base58": { "hashes": [ @@ -67,36 +67,34 @@ }, "cffi": { "hashes": [ - "sha256:041c81822e9f84b1d9c401182e174996f0bae9991f33725d059b771744290774", - "sha256:046ef9a22f5d3eed06334d01b1e836977eeef500d9b78e9ef693f9380ad0b83d", - "sha256:066bc4c7895c91812eff46f4b1c285220947d4aa46fa0a2651ff85f2afae9c90", - "sha256:066c7ff148ae33040c01058662d6752fd73fbc8e64787229ea8498c7d7f4041b", - "sha256:2444d0c61f03dcd26dbf7600cf64354376ee579acad77aef459e34efcb438c63", - "sha256:300832850b8f7967e278870c5d51e3819b9aad8f0a2c8dbe39ab11f119237f45", - "sha256:34c77afe85b6b9e967bd8154e3855e847b70ca42043db6ad17f26899a3df1b25", - "sha256:46de5fa00f7ac09f020729148ff632819649b3e05a007d286242c4882f7b1dc3", - "sha256:4aa8ee7ba27c472d429b980c51e714a24f47ca296d53f4d7868075b175866f4b", - "sha256:4d0004eb4351e35ed950c14c11e734182591465a33e960a4ab5e8d4f04d72647", - "sha256:4e3d3f31a1e202b0f5a35ba3bc4eb41e2fc2b11c1eff38b362de710bcffb5016", - "sha256:50bec6d35e6b1aaeb17f7c4e2b9374ebf95a8975d57863546fa83e8d31bdb8c4", - "sha256:55cad9a6df1e2a1d62063f79d0881a414a906a6962bc160ac968cc03ed3efcfb", - "sha256:5662ad4e4e84f1eaa8efce5da695c5d2e229c563f9d5ce5b0113f71321bcf753", - "sha256:59b4dc008f98fc6ee2bb4fd7fc786a8d70000d058c2bbe2698275bc53a8d3fa7", - "sha256:73e1ffefe05e4ccd7bcea61af76f36077b914f92b76f95ccf00b0c1b9186f3f9", - "sha256:a1f0fd46eba2d71ce1589f7e50a9e2ffaeb739fb2c11e8192aa2b45d5f6cc41f", - "sha256:a2e85dc204556657661051ff4bab75a84e968669765c8a2cd425918699c3d0e8", - "sha256:a5457d47dfff24882a21492e5815f891c0ca35fefae8aa742c6c263dac16ef1f", - "sha256:a8dccd61d52a8dae4a825cdbb7735da530179fea472903eb871a5513b5abbfdc", - "sha256:ae61af521ed676cf16ae94f30fe202781a38d7178b6b4ab622e4eec8cefaff42", - "sha256:b012a5edb48288f77a63dba0840c92d0504aa215612da4541b7b42d849bc83a3", - "sha256:d2c5cfa536227f57f97c92ac30c8109688ace8fa4ac086d19d0af47d134e2909", - "sha256:d42b5796e20aacc9d15e66befb7a345454eef794fdb0737d1af593447c6c8f45", - "sha256:dee54f5d30d775f525894d67b1495625dd9322945e7fee00731952e0368ff42d", - "sha256:e070535507bd6aa07124258171be2ee8dfc19119c28ca94c9dfb7efd23564512", - "sha256:e1ff2748c84d97b065cc95429814cdba39bcbd77c9c85c89344b317dc0d9cbff", - "sha256:ed851c75d1e0e043cbf5ca9a8e1b13c4c90f3fbd863dacb01c0808e2b5204201" - ], - "version": "==1.12.3" + "sha256:1112d2fc92a867a6103bce6740a549e74b1d320cf28875609f6e93857eee4f2d", + "sha256:1b9ab50c74e075bd2ae489853c5f7f592160b379df53b7f72befcbe145475a36", + "sha256:24eff2997436b6156c2f30bed215c782b1d8fd8c6a704206053c79af95962e45", + "sha256:2eff642fbc9877a6449026ad66bf37c73bf4232505fb557168ba5c502f95999b", + "sha256:362e896cea1249ed5c2a81cf6477fabd9e1a5088aa7ea08358a4c6b0998294d2", + "sha256:40eddb3589f382cb950f2dcf1c39c9b8d7bd5af20665ce273815b0d24635008b", + "sha256:5ed40760976f6b8613d4a0db5e423673ca162d4ed6c9ed92d1f4e58a47ee01b5", + "sha256:6f19c9df4785305669335b934c852133faed913c0faa63056248168966f7a7d5", + "sha256:719537b4c5cd5218f0f47826dd705fb7a21d83824920088c4214794457113f3f", + "sha256:7b0e337a70e58f1a36fb483fd63880c9e74f1db5c532b4082bceac83df1523fa", + "sha256:853376efeeb8a4ae49a737d5d30f5db8cdf01d9319695719c4af126488df5a6a", + "sha256:85bbf77ffd12985d76a69d2feb449e35ecdcb4fc54a5f087d2bd54158ae5bb0c", + "sha256:8978115c6f0b0ce5880bc21c967c65058be8a15f1b81aa5fdbdcbea0e03952d1", + "sha256:8f7eec920bc83692231d7306b3e311586c2e340db2dc734c43c37fbf9c981d24", + "sha256:8fe230f612c18af1df6f348d02d682fe2c28ca0a6c3856c99599cdacae7cf226", + "sha256:b57e1c8bcdd7340e9c9d09613b5e7fdd0c600be142f04e2cc1cc8cb7c0b43529", + "sha256:ba956c9b44646bc1852db715b4a252e52a8f5a4009b57f1dac48ba3203a7bde1", + "sha256:ca42034c11eb447497ea0e7b855d87ccc2aebc1e253c22e7d276b8599c112a27", + "sha256:dc9b2003e9a62bbe0c84a04c61b0329e86fccd85134a78d7aca373bbbf788165", + "sha256:e77cd105b19b8cd721d101687fcf665fd1553eb7b57556a1ef0d453b6fc42faa", + "sha256:f56dff1bd81022f1c980754ec721fb8da56192b026f17f0f99b965da5ab4fbd2", + "sha256:fa4cc13c03ea1d0d37ce8528e0ecc988d2365e8ac64d8d86cafab4038cb4ce89", + "sha256:fa8cf1cb974a9f5911d2a0303f6adc40625c05578d8e7ff5d313e1e27850bd59", + "sha256:fb003019f06d5fc0aa4738492ad8df1fa343b8a37cbcf634018ad78575d185df", + "sha256:fd409b7778167c3bcc836484a8f49c0e0b93d3e745d975749f83aa5d18a5822f", + "sha256:fe5d65a3ee38122003245a82303d11ac05ff36531a8f5ce4bc7d4bbc012797e1" + ], + "version": "==1.13.0" }, "chardet": { "hashes": [ @@ -113,14 +111,6 @@ "index": "pypi", "version": "==7.0" }, - "click-log": { - "hashes": [ - "sha256:16fd1ca3fc6b16c98cea63acf1ab474ea8e676849dc669d86afafb0ed7003124", - "sha256:eee14dc37cdf3072158570f00406572f9e03e414accdccfccd4c538df9ae322c" - ], - "index": "pypi", - "version": "==0.3.2" - }, "clickclick": { "hashes": [ "sha256:4a890aaa9c3990cfabd446294eb34e3dc89701101ac7b41c1bff85fc210f6d23", @@ -647,10 +637,10 @@ }, "attrs": { "hashes": [ - "sha256:ec20e7a4825331c1b5ebf261d111e16fa9612c1f7a5e1f884f12bd53a664dfd2", - "sha256:f913492e1663d3c36f502e5e9ba6cd13cf19d7fab50aa13239e420fef95e1396" + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" ], - "version": "==19.2.0" + "version": "==19.3.0" }, "certifi": { "hashes": [ diff --git a/aea/cli/__main__.py b/aea/cli/__main__.py index 47bf932582..ff178f3e8c 100755 --- a/aea/cli/__main__.py +++ b/aea/cli/__main__.py @@ -26,7 +26,6 @@ from typing import cast import click -import click_log from click import pass_context from jsonschema import ValidationError @@ -35,6 +34,7 @@ from aea.cli.common import Context, pass_ctx, logger, _try_to_load_agent_config, DEFAULT_REGISTRY_PATH from aea.cli.install import install from aea.cli.list import list as _list +from aea.cli.loggers import simple_verbosity_option from aea.cli.remove import remove from aea.cli.run import run from aea.cli.scaffold import scaffold @@ -49,7 +49,7 @@ @click.group() @click.version_option('0.1.0') @click.pass_context -@click_log.simple_verbosity_option(logger, default="INFO") +@simple_verbosity_option(logger, default="INFO") def cli(ctx) -> None: """Command-line tool for setting up an Autonomous Economic Agent.""" ctx.obj = Context(cwd=".") diff --git a/aea/cli/common.py b/aea/cli/common.py index 9307db7b0f..79ad7a04ee 100644 --- a/aea/cli/common.py +++ b/aea/cli/common.py @@ -28,16 +28,16 @@ from typing import Dict, List, cast import click -import click_log import jsonschema # type: ignore from dotenv import load_dotenv +from aea.cli.loggers import default_logging_config from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE, AgentConfig, SkillConfig, ConnectionConfig, ProtocolConfig, \ DEFAULT_PROTOCOL_CONFIG_FILE, DEFAULT_CONNECTION_CONFIG_FILE, DEFAULT_SKILL_CONFIG_FILE from aea.configurations.loader import ConfigLoader logger = logging.getLogger("aea") -logger = click_log.basic_config(logger=logger) +logger = default_logging_config(logger) DEFAULT_REGISTRY_PATH = "../packages" diff --git a/aea/cli/loggers.py b/aea/cli/loggers.py new file mode 100644 index 0000000000..5bbb326277 --- /dev/null +++ b/aea/cli/loggers.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Helpers for the logging module.""" +import logging +import sys + +import click + + +class ColorFormatter(logging.Formatter): + """The default formatter for cli output.""" + + colors = { + 'error': dict(fg='red'), + 'exception': dict(fg='red'), + 'critical': dict(fg='red'), + 'debug': dict(fg='blue'), + 'info': dict(fg='green'), + 'warning': dict(fg='yellow') + } + + def format(self, record): + if not record.exc_info: + level = record.levelname.lower() + msg = record.getMessage() + if level in self.colors: + prefix = click.style('{}: '.format(level), + **self.colors[level]) + msg = '\n'.join(prefix + x for x in msg.splitlines()) + return msg + return logging.Formatter.format(self, record) + + +def simple_verbosity_option(logger=None, *names, **kwargs): + """ + A decorator that adds a `--verbosity, -v` option to the decorated + command. + + Name can be configured through ``*names``. Keyword arguments are passed to + the underlying ``click.option`` decorator. + """ + if not names: + names = ['--verbosity', '-v'] + + kwargs.setdefault('default', 'INFO') + kwargs.setdefault('metavar', 'LVL') + kwargs.setdefault('expose_value', False) + kwargs.setdefault('help', 'Either CRITICAL, ERROR, WARNING, INFO or DEBUG') + kwargs.setdefault('is_eager', True) + + def decorator(f): + def _set_level(ctx, param, value): + x = getattr(logging, value.upper(), None) + if x is None: + raise click.BadParameter( + 'Must be CRITICAL, ERROR, WARNING, INFO or DEBUG, not {}' + ) + logger.setLevel(x) + + return click.option(*names, callback=_set_level, **kwargs)(f) + return decorator + + +def default_logging_config(logger): + """Set up the default handler and formatter on the given logger.""" + default_handler = logging.StreamHandler(stream=sys.stdout) + default_handler.formatter = ColorFormatter() + logger.handlers = [default_handler] + logger.propagate = True + return logger From a8d03a8255cde604412a04cb53cb7b3e60fda9f0 Mon Sep 17 00:00:00 2001 From: Aristotelis Date: Tue, 15 Oct 2019 16:22:41 +0100 Subject: [PATCH 15/71] 100% coverage ---> aea/crypto/ethereum_base.py --- aea/crypto/ethereum_base.py | 2 +- tests/data/eth_private_key.txt | 1 + tests/data/fet_private_key.txt | 1 + tests/test_crypto/test_ethereum_base.py | 49 +++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 tests/data/eth_private_key.txt create mode 100644 tests/data/fet_private_key.txt create mode 100644 tests/test_crypto/test_ethereum_base.py diff --git a/aea/crypto/ethereum_base.py b/aea/crypto/ethereum_base.py index c17e412115..0eb32b88a7 100644 --- a/aea/crypto/ethereum_base.py +++ b/aea/crypto/ethereum_base.py @@ -83,7 +83,7 @@ def _load_private_key_from_path(self, file_name) -> Account: else: account = self._generate_private_key() return account - except IOError as e: + except IOError as e: # pragma: no cover logger.exception(str(e)) def sign_transaction(self, message: str) -> bytes: diff --git a/tests/data/eth_private_key.txt b/tests/data/eth_private_key.txt new file mode 100644 index 0000000000..97d4098c2c --- /dev/null +++ b/tests/data/eth_private_key.txt @@ -0,0 +1 @@ +0x0c70c25dc9fcb75ae5122eca8a750403ff5420236a6475ac25d9504d6318b6eb \ No newline at end of file diff --git a/tests/data/fet_private_key.txt b/tests/data/fet_private_key.txt new file mode 100644 index 0000000000..3bc75d8591 --- /dev/null +++ b/tests/data/fet_private_key.txt @@ -0,0 +1 @@ +ccf59158ee8c5bc1aa5422f511f64d7ed55106a38806259568d39b158a1719e7 \ No newline at end of file diff --git a/tests/test_crypto/test_ethereum_base.py b/tests/test_crypto/test_ethereum_base.py new file mode 100644 index 0000000000..28b8b0daef --- /dev/null +++ b/tests/test_crypto/test_ethereum_base.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the tests of the ethereum module.""" +import pytest + +from aea.crypto.ethereum_base import EthCrypto, EthCryptoError +from ..conftest import ROOT_DIR + +PRIVATE_KEY_PATH = ROOT_DIR + "/tests/data/eth_private_key.txt" + + +def test_creation(): + """Test the creation of the crypto_objects""" + assert EthCrypto(), "Managed to initialise the eth_account" + assert EthCrypto(PRIVATE_KEY_PATH), "Managed to load the eth private key" + assert EthCrypto("./"), "Managed to create a new eth private key" + + +def test_initialization(): + """Test the initialisation of the variables.""" + account = EthCrypto() + assert account.display_address is not None, "After creation the display address must not be None" + assert account._bytes_representation is not None, "After creation the bytes_representation of the " \ + "address must not be None" + assert account.public_key is not None, "After creation the public key must no be None" + + +def test_sign_transaction(): + """Test the signing function for the eth_crypto.""" + account = EthCrypto(PRIVATE_KEY_PATH) + sign_bytes = account.sign_transaction('Hello') + assert len(sign_bytes) > 0, "The len(signature) must not be 0" From 897d666c440093966f28035d478d7918330a27d3 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 15 Oct 2019 17:26:59 +0200 Subject: [PATCH 16/71] remove click_log dep from setup.py --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 1546365c9b..cabfc8cbb9 100644 --- a/setup.py +++ b/setup.py @@ -73,7 +73,6 @@ def get_all_extras() -> Dict: cli_deps = [ "click", - "click_log", "PyYAML", "jsonschema", "python-dotenv", From b9dcfd2823b55712c730becf4a24528c643caa28 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 15 Oct 2019 17:29:30 +0200 Subject: [PATCH 17/71] fix code style in aea.cli.loggers module. --- aea/cli/loggers.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/aea/cli/loggers.py b/aea/cli/loggers.py index 5bbb326277..4d6923dc09 100644 --- a/aea/cli/loggers.py +++ b/aea/cli/loggers.py @@ -37,6 +37,7 @@ class ColorFormatter(logging.Formatter): } def format(self, record): + """Format the log message.""" if not record.exc_info: level = record.levelname.lower() msg = record.getMessage() @@ -49,12 +50,10 @@ def format(self, record): def simple_verbosity_option(logger=None, *names, **kwargs): - """ - A decorator that adds a `--verbosity, -v` option to the decorated - command. + """A decorator that adds a `--verbosity, -v` option to the decoratedcommand. - Name can be configured through ``*names``. Keyword arguments are passed to - the underlying ``click.option`` decorator. + Name can be configured through `*names`. Keyword arguments are passed to + the underlying `click.option` decorator. """ if not names: names = ['--verbosity', '-v'] From 472fe99be8ec001e1be34907fd10671ffb626c13 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Tue, 15 Oct 2019 16:44:30 +0100 Subject: [PATCH 18/71] Updates docs to incliude a skill developing guide, fixes #193 --- aea/connections/local/connection.yaml | 8 +- aea/connections/oef/connection.yaml | 8 +- aea/connections/scaffold/connection.yaml | 2 +- aea/connections/stub/connection.yaml | 1 + aea/protocols/default/protocol.yaml | 2 +- aea/protocols/fipa/protocol.yaml | 2 +- aea/protocols/oef/protocol.yaml | 2 +- aea/protocols/scaffold/protocol.yaml | 2 +- aea/skills/error/skill.yaml | 1 + aea/skills/scaffold/skill.yaml | 1 + docs/file-structure.md | 2 +- docs/index.md | 26 ++- docs/skill-guide.md | 230 +++++++++++++++++++++++ docs/skill.md | 2 +- docs/steps.md | 2 +- mkdocs.yml | 3 +- 16 files changed, 267 insertions(+), 27 deletions(-) create mode 100644 docs/skill-guide.md diff --git a/aea/connections/local/connection.yaml b/aea/connections/local/connection.yaml index f3b11a9197..8f3a799e2b 100644 --- a/aea/connections/local/connection.yaml +++ b/aea/connections/local/connection.yaml @@ -3,7 +3,11 @@ authors: Fetch.AI Limited version: 0.1.0 license: Apache 2.0 url: "" +description: "The local connection provides a stub for an OEF node." class_name: OEFLocalConnection -supported_protocols: ["oef"] +supported_protocols: + - default + - oef + - fipa + - tac config: {} -description: "local connection description [Fill in]" \ No newline at end of file diff --git a/aea/connections/oef/connection.yaml b/aea/connections/oef/connection.yaml index 5a8cca4134..6a69ed33d1 100644 --- a/aea/connections/oef/connection.yaml +++ b/aea/connections/oef/connection.yaml @@ -3,12 +3,16 @@ authors: Fetch.AI Limited version: 0.1.0 license: Apache 2.0 url: "" +description: "The oef connection provides a wrapper around the OEF sdk." class_name: OEFConnection -supported_protocols: ["oef"] +supported_protocols: + - default + - oef + - fipa + - tac config: addr: ${OEF_ADDR:127.0.0.1} port: ${OEF_PORT:10000} dependencies: - colorlog - oef -description: "oef connection description [Fill in]" \ No newline at end of file diff --git a/aea/connections/scaffold/connection.yaml b/aea/connections/scaffold/connection.yaml index fa38ffb42c..fa1f7f05e9 100644 --- a/aea/connections/scaffold/connection.yaml +++ b/aea/connections/scaffold/connection.yaml @@ -3,8 +3,8 @@ authors: Fetch.AI Limited version: 0.1.0 license: Apache 2.0 url: "" +description: "The scaffold connection provides a scaffold for a connection to be implemented by the developer." class_name: MyScaffoldConnection supported_protocols: [] config: foo: bar -description: "default connection description [Fill in]" \ No newline at end of file diff --git a/aea/connections/stub/connection.yaml b/aea/connections/stub/connection.yaml index 1a2532340b..bbe1d6268c 100644 --- a/aea/connections/stub/connection.yaml +++ b/aea/connections/stub/connection.yaml @@ -3,6 +3,7 @@ authors: Fetch.AI Limited version: 0.1.0 license: Apache 2.0 url: "" +description: "The stub connection implements a connection stub which reads/writes messages from/to file." class_name: StubConnection supported_protocols: - oef diff --git a/aea/protocols/default/protocol.yaml b/aea/protocols/default/protocol.yaml index 4323f92d70..66d97cfc83 100644 --- a/aea/protocols/default/protocol.yaml +++ b/aea/protocols/default/protocol.yaml @@ -3,4 +3,4 @@ authors: Fetch.AI Limited version: 0.1.0 license: Apache 2.0 url: "" -description: "default protocol description [Fill in]" \ No newline at end of file +description: "The default protocol allows for any bytes message." \ No newline at end of file diff --git a/aea/protocols/fipa/protocol.yaml b/aea/protocols/fipa/protocol.yaml index a1d6856356..f9197cdcb6 100644 --- a/aea/protocols/fipa/protocol.yaml +++ b/aea/protocols/fipa/protocol.yaml @@ -5,4 +5,4 @@ license: Apache 2.0 url: "" dependencies: - protobuf -description: "fipa protocol description [Fill in]" +description: "The fipa protocol implements the FIPA ACL." diff --git a/aea/protocols/oef/protocol.yaml b/aea/protocols/oef/protocol.yaml index 918c9a101e..83130abea0 100644 --- a/aea/protocols/oef/protocol.yaml +++ b/aea/protocols/oef/protocol.yaml @@ -6,4 +6,4 @@ url: "" dependencies: - colorlog - oef -description: "oef protocol description [Fill in]" \ No newline at end of file +description: "The oef protocol implements the OEF specific messages." diff --git a/aea/protocols/scaffold/protocol.yaml b/aea/protocols/scaffold/protocol.yaml index 33766e9c37..62dc31060d 100644 --- a/aea/protocols/scaffold/protocol.yaml +++ b/aea/protocols/scaffold/protocol.yaml @@ -3,4 +3,4 @@ authors: Fetch.AI Limited version: 0.1.0 license: Apache 2.0 url: "" -description: "scaffold protocol description [Fill in]" \ No newline at end of file +description: "The scaffold protocol scaffolds a protocol to be implemented by the developer." diff --git a/aea/skills/error/skill.yaml b/aea/skills/error/skill.yaml index e6f13e68b2..d17d7e42aa 100644 --- a/aea/skills/error/skill.yaml +++ b/aea/skills/error/skill.yaml @@ -3,6 +3,7 @@ authors: Fetch.AI Limited version: 0.1.0 license: Apache 2.0 url: "" +description: "The error skill implements basic error handling required by all AEAs." behaviours: [] handlers: - handler: diff --git a/aea/skills/scaffold/skill.yaml b/aea/skills/scaffold/skill.yaml index 20b12d01ac..d8212cd2f6 100644 --- a/aea/skills/scaffold/skill.yaml +++ b/aea/skills/scaffold/skill.yaml @@ -3,6 +3,7 @@ authors: Fetch.AI Limited version: 0.1.0 license: Apache 2.0 url: "" +description: "The scaffold skill is a scaffold for your own skill implementation." behaviours: - behaviour: class_name: MyScaffoldBehaviour diff --git a/docs/file-structure.md b/docs/file-structure.md index 87d6433f08..637791355c 100644 --- a/docs/file-structure.md +++ b/docs/file-structure.md @@ -9,7 +9,7 @@ The CLI tool provides a way to scaffold out the required directory structure for ``` bash agent_name/ aea-config.yaml YAML configuration of the agent - priv.pem The private key file + private_key.pem The private key file connections/ Directory containing all the supported connections connection_1/ First connection ... ... diff --git a/docs/index.md b/docs/index.md index 38b2132e40..a790cbe125 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,9 +7,19 @@ The framework is super modular, easily extensible, and highly composable. The AEA framework attempts to make agent development as straightforward as web development using popular web frameworks. +## AEA definition + +An autonomous economic agent (AEA) is an intelligent agent whose goal is generating economic value for its owner. + +The AEA super power is their ability to autonomously acquire new skills. + +AEAs achieve their goals with the help of the Fetch.AI OEF and the Fetch.AI Ledger. Third party systems, such as Ethereum, may also allow AEA integration. As such, AEAs bridge web 2 and web 3. + + ## Our vision -The AEA framework has two focused commercial roles. +The AEA framework has two commercial roles. + ### Open source technology @@ -27,25 +37,13 @@ AEA users are, among others: * Crypto passionates * Web developers + ### Platform for start ups By operating as a platform for start ups, we envisage the AEA framework to be in a continuous growth pattern. With start up grants we will kick start solutions while testing product-problem fit and identifying our user base. - - - -## Agents - -An autonomous economic agent (AEA) is an intelligent agent whose goal is generating economic value for its owner. - -The AEA super power is the ability to autonomously acquire new skills. - -AEAs achieve their goals with the help of the Fetch.AI OEF and the Fetch.AI Ledger. - -Third party systems, such as Ethereum, may also allow AEA integration. - !!! Note diff --git a/docs/skill-guide.md b/docs/skill-guide.md new file mode 100644 index 0000000000..1a13b31b61 --- /dev/null +++ b/docs/skill-guide.md @@ -0,0 +1,230 @@ +The scaffolding tool allows you create the folder structure required for a skill. + +!!! Note + Before developing your first skill, please read the skill guide. + +## Step 1: Setup + +Ensure, you have followed the preliminaries. We will first create an agent and add a scaffold skill, which we call `my_search`: + +``` bash +aea create my_agent && cd my_agent +aea scaffold skill my_search +``` + +## Step 2: Develop a Behaviour + +A `Behaviour` class contains the business logic specific to initial actions initiated by the agent rather than reactions to other events. + +In this example, we implement a simple search behaviour. Each time, `act()` gets called, we will send a search request to the OEF. + +``` python +from aea.protocols.oef.message import OEFMessage +from aea.protocols.oef.models import Query, Constraint, ConstraintType +from aea.protocols.oef.serialization import DEFAULT_OEF, OEFSerializer +from aea.skills.base import Behaviour + + +class MySearchBehaviour(Behaviour): + """This class provides a simple search behaviour.""" + + def __init__(self, **kwargs): + """Initialize the search behaviour.""" + super().__init__(**kwargs) + self.sent_search_count = 0 + + def setup(self) -> None: + """ + Implement the setup. + + :return: None + """ + pass + + + def act(self) -> None: + """ + Implement the act. + + :return: None + """ + self.sent_search_count += 1 + search_constraints = [Constraint("country", + ConstraintType("==", "UK"))] + search_query_w_empty_model = Query(search_constraints, model=None) + search_request = OEFMessage(oef_type=OEFMessage.Type.SEARCH_SERVICES, + id=self.sent_search_count, + query=search_query_w_empty_model) + self.context.outbox.put_message(to=DEFAULT_OEF, + sender=self.context.agent_address, + protocol_id=OEFMessage.protocol_id, + message=OEFSerializer().encode(search_request)) + + def teardown(self) -> None: + """ + Implement the task teardown. + + :return: None + """ + pass +``` + +Searches are proactive and as such well placed in a `Behaviour`. + + +## Step 3: Develop a Handler + +So far, we have tasked the agent with sending search requests to the OEF. However, we have no way of handling the responses sent to the agent by the OEF at the moment. The agent would simply respond to the OEF via the default `error` skill which sends all unrecognized envelopes back to the sender. + +Let us now implement a handler to deal with the incoming search responses. + + +``` python +import logging + +from aea.protocols.oef.message import OEFMessage +from aea.protocols.oef.serialization import OEFSerializer +from aea.skills.base import Handler + +logger = logging.getLogger("aea.my_search_skill") + + +class MySearchHandler(Handler): + """This class provides a simple search handler.""" + + SUPPORTED_PROTOCOL = OEFMessage.protocol_id + + def __init__(self, **kwargs): + """Initialize the handler.""" + super().__init__(**kwargs) + self.received_search_count = 0 + + def setup(self) -> None: + """Set up the handler.""" + pass + + def handle(self, message: OEFMessage, sender: str) -> None: + """ + Handle the message. + + :param message: the message. + :param sender: the sender. + :return: None + """ + msg_type = OEFMessage.Type(message.get("type")) + + if msg_type is OEFMessage.Type.SEARCH_RESULT: + nb_agents_found = len(message.get("agents")) + logger.info("[{}]: found number of agents={}".format(self.context.agent_name, nb_agents_found)) + self.received_search_count += 1 + + def teardown(self) -> None: + """ + Teardown the handler. + + :return: None + """ + pass +``` + +We create a handler which is registered for the `oef` protocol. Whenever it receives a search result, we log the number of agents returned in the search - the agents matching the search query - and update the counter of received searches. + +Note, how the handler simply reacts to incoming events (i.e. messages). It could initiate further actions, however, they are still reactions to the upstream search event. + + +## Step 4: Develop a Task + +We have implemented a behaviour and a handler. We conclude by implementing a task. Here we can implement background logic. We will implement a trivial check on the difference between the amount of search requests sent and responses received. + + +```python +import logging + +from aea.skills.base import Task + +logger = logging.getLogger("aea.my_search_skill") + + +class MySearchTask(Task): + """This class scaffolds a task.""" + + def setup(self) -> None: + """ + Implement the setup. + + :return: None + """ + pass + + def execute(self) -> None: + """ + Implement the task execution. + + :param envelope: the envelope + :return: None + """ + my_search_behaviour = self.context.behaviours[0] + my_search_task = self.context.tasks[0] + my_search_behaviour + logger.info("[{}]: number of search requests sent={} vs. number of search responses received={}".format(self.context.agent_name, + my_search_behaviour.sent_search_count, + my_search_task.received_search_count) + ) + + def teardown(self) -> None: + """ + Implement the task teardown. + + :return: None + """ + pass +``` + +Note, how we have access to other objects in the skill via `self.context`. + + +## Step 5: Create the config file + +Based on our skill components above, we create the following config file: + +```yaml +name: my_search +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +description: 'A simple search skill utilising the OEF.' +behaviours: + - behaviour: + class_name: MySearchBehaviour + args: {} +handlers: + - handler: + class_name: MySearchHandler + args: {} +tasks: + - task: + class_name: MySearchTask + args: {} +shared_classes: [] +protocols: ["oef"] +dependencies: [] +``` + +## Step 6: Run the agent + +We first start an oef node (see the connection section for more details) in a separate terminal window: + +```bash +python scripts/oef/launch.py -c ./scripts/oef/launch_config.json +``` + +We can then launch our agent: +```bash +aea run +``` + +## Now it's your turn: + +We hope this step by step introduction has helped you to develop your own skill. We are excited to see what you will build. + diff --git a/docs/skill.md b/docs/skill.md index 96b2c46e5a..d0c42a6326 100644 --- a/docs/skill.md +++ b/docs/skill.md @@ -5,7 +5,7 @@ When you add a skill with the CLI, a directory is created which includes modules ## Context -The skill has a `context` object which is shared by all `Handler`, `Behaviour`, and `Task` objects. The skill context also has a link to the agent context. The agent context provides read access to agent specific information like the private key of the agent, its preferences and ownership state. It also provides access to the `OutBox`. +The skill has a `context` object which is shared by all `Handler`, `Behaviour`, and `Task` objects. The skill context also has a link to the agent context. The agent context provides read access to agent specific information like the public key and address of the agent, its preferences and ownership state. It also provides access to the `OutBox`. This means it is possible to, at any point, grab the `context` and have access to the code in other parts of the skill and the agent. diff --git a/docs/steps.md b/docs/steps.md index e8ee892a02..625a6e80a1 100644 --- a/docs/steps.md +++ b/docs/steps.md @@ -12,7 +12,7 @@
  • Set up your skills.
  • Code the protocols.
  • Add the connections.
  • -
  • Scaffold any of the above resources with the scaffolding tool.
  • +
  • Scaffold any of the above resources with the scaffolding tool. This guide shows you step by step how to develop a skill.
  • Now, build and run your agent using the quick start guide.
  • diff --git a/mkdocs.yml b/mkdocs.yml index 4669ed2f84..547bd35eee 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -21,15 +21,16 @@ nav: - Welcome: - "What is the AEA framework?": 'index.md' - "Version": 'version.md' + - AEA quick start: 'quickstart.md' - Architecture: - "Design principles": 'design-principles.md' - "Architectural diagram": 'diagram.md' - "Core components": 'core-components.md' - "File structure": 'file-structure.md' - - AEA quick start: 'quickstart.md' - Developer guide: - "Step by step": 'steps.md' - "Skill": 'skill.md' + - "Build your own skill": 'skill-guide.md' - "Protocol": 'protocol.md' - "Connection": 'connection.md' - "Scaffolding": 'scaffolding.md' From 68ea707de984d1a697ed9d39a407164cb2d5e3d2 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 15 Oct 2019 17:47:55 +0200 Subject: [PATCH 19/71] fix help message of 'verbosity' option. --- aea/cli/__main__.py | 4 ++-- aea/cli/loggers.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/aea/cli/__main__.py b/aea/cli/__main__.py index ff178f3e8c..fac8e51cc4 100755 --- a/aea/cli/__main__.py +++ b/aea/cli/__main__.py @@ -40,7 +40,6 @@ from aea.cli.scaffold import scaffold from aea.cli.search import search from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE, AgentConfig -import aea.cli_gui DEFAULT_CONNECTION = "oef" DEFAULT_SKILL = "error" @@ -48,8 +47,8 @@ @click.group() @click.version_option('0.1.0') -@click.pass_context @simple_verbosity_option(logger, default="INFO") +@click.pass_context def cli(ctx) -> None: """Command-line tool for setting up an Autonomous Economic Agent.""" ctx.obj = Context(cwd=".") @@ -127,6 +126,7 @@ def freeze(ctx: Context): @pass_ctx def gui(ctx: Context): """Run the CLI GUI.""" + import aea.cli_gui logger.info("Running the GUI.....(press Ctrl+C to exit)") aea.cli_gui.run() diff --git a/aea/cli/loggers.py b/aea/cli/loggers.py index 4d6923dc09..8160c47e06 100644 --- a/aea/cli/loggers.py +++ b/aea/cli/loggers.py @@ -24,6 +24,9 @@ import click +LOG_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + + class ColorFormatter(logging.Formatter): """The default formatter for cli output.""" @@ -59,9 +62,10 @@ def simple_verbosity_option(logger=None, *names, **kwargs): names = ['--verbosity', '-v'] kwargs.setdefault('default', 'INFO') + kwargs.setdefault('type', click.Choice(LOG_LEVELS, case_sensitive=False)) kwargs.setdefault('metavar', 'LVL') kwargs.setdefault('expose_value', False) - kwargs.setdefault('help', 'Either CRITICAL, ERROR, WARNING, INFO or DEBUG') + kwargs.setdefault('help', 'One of {}'.format(", ".join(LOG_LEVELS))) kwargs.setdefault('is_eager', True) def decorator(f): From 0c534f1c5be04c694052db25c9d18bedcaaa13a2 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 15 Oct 2019 18:03:10 +0200 Subject: [PATCH 20/71] add OFF and NOTSET level to 'verbosity' option. --- aea/cli/loggers.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/aea/cli/loggers.py b/aea/cli/loggers.py index 8160c47e06..2088ac5c0e 100644 --- a/aea/cli/loggers.py +++ b/aea/cli/loggers.py @@ -23,8 +23,10 @@ import click +OFF = 100 +logging.addLevelName(OFF, "OFF") -LOG_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] +LOG_LEVELS = ["NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "OFF"] class ColorFormatter(logging.Formatter): @@ -70,12 +72,8 @@ def simple_verbosity_option(logger=None, *names, **kwargs): def decorator(f): def _set_level(ctx, param, value): - x = getattr(logging, value.upper(), None) - if x is None: - raise click.BadParameter( - 'Must be CRITICAL, ERROR, WARNING, INFO or DEBUG, not {}' - ) - logger.setLevel(x) + level = logging.getLevelName(value) + logger.setLevel(level) return click.option(*names, callback=_set_level, **kwargs)(f) return decorator From b8f01d107c2b2cd76dac37547540d77ca3baeb78 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Tue, 15 Oct 2019 17:10:51 +0100 Subject: [PATCH 21/71] Adds fixes to various description fields in config files. --- aea/skills/error/skill.yaml | 1 - aea/skills/scaffold/skill.yaml | 1 - packages/connections/gym/connection.yaml | 2 +- packages/protocols/gym/protocol.yaml | 2 +- packages/protocols/tac/protocol.yaml | 2 +- packages/skills/echo/skill.yaml | 2 +- packages/skills/fipa_negotiation/skill.yaml | 2 +- packages/skills/gym/skill.yaml | 2 +- packages/skills/tac/skill.yaml | 2 +- packages/skills/weather_client/skill.yaml | 2 +- packages/skills/weather_station/skill.yaml | 4 +--- 11 files changed, 9 insertions(+), 13 deletions(-) diff --git a/aea/skills/error/skill.yaml b/aea/skills/error/skill.yaml index d17d7e42aa..33806a3345 100644 --- a/aea/skills/error/skill.yaml +++ b/aea/skills/error/skill.yaml @@ -13,4 +13,3 @@ handlers: tasks: [] shared_classes: [] protocols: ['default'] -description: "Error skill description [Fill in]" diff --git a/aea/skills/scaffold/skill.yaml b/aea/skills/scaffold/skill.yaml index d8212cd2f6..8854ac25e8 100644 --- a/aea/skills/scaffold/skill.yaml +++ b/aea/skills/scaffold/skill.yaml @@ -25,4 +25,3 @@ shared_classes: args: foo: bar protocols: [] -description: "Scaffold skill description [Fill in]" diff --git a/packages/connections/gym/connection.yaml b/packages/connections/gym/connection.yaml index fc939f10d8..c845fc7097 100644 --- a/packages/connections/gym/connection.yaml +++ b/packages/connections/gym/connection.yaml @@ -2,6 +2,7 @@ name: gym authors: Fetch.AI Limited version: 0.1.0 license: Apache 2.0 +description: "The gym connection wraps an OpenAI gym." url: "" class_name: GymConnection supported_protocols: ["gym"] @@ -9,4 +10,3 @@ config: env: '' # put here the dotted path to your Gym Environment class. dependencies: - gym -description: "gym connection description [Fill in]" \ No newline at end of file diff --git a/packages/protocols/gym/protocol.yaml b/packages/protocols/gym/protocol.yaml index 4b978f58fa..7b10d5429b 100644 --- a/packages/protocols/gym/protocol.yaml +++ b/packages/protocols/gym/protocol.yaml @@ -3,4 +3,4 @@ authors: Fetch.AI Limited version: 0.1.0 license: Apache 2.0 url: "" -description: "gym protocol description [Fill in]" \ No newline at end of file +description: "The gym protocol implements the messages an agent needs to engage with a gym connection." \ No newline at end of file diff --git a/packages/protocols/tac/protocol.yaml b/packages/protocols/tac/protocol.yaml index 0775107d8a..435e33b383 100644 --- a/packages/protocols/tac/protocol.yaml +++ b/packages/protocols/tac/protocol.yaml @@ -3,4 +3,4 @@ authors: Fetch.AI Limited version: 0.1.0 license: Apache 2.0 url: "" -description: "tac protocol description [Fill in]" \ No newline at end of file +description: "The tac protocol implements the messages an AEA needs to participate in the TAC." \ No newline at end of file diff --git a/packages/skills/echo/skill.yaml b/packages/skills/echo/skill.yaml index 5b13134cf9..57b568ac77 100644 --- a/packages/skills/echo/skill.yaml +++ b/packages/skills/echo/skill.yaml @@ -2,6 +2,7 @@ name: echo authors: Fetch.AI Limited version: 0.1.0 license: Apache 2.0 +description: "The echo skill implements simple echo functionality." url: "" behaviours: - behaviour: @@ -23,4 +24,3 @@ tasks: shared_classes: [] protocols: ["default"] dependencies: [] -description: "Echo skill description [Fill in]" \ No newline at end of file diff --git a/packages/skills/fipa_negotiation/skill.yaml b/packages/skills/fipa_negotiation/skill.yaml index efdf9140ff..d57c3468fc 100644 --- a/packages/skills/fipa_negotiation/skill.yaml +++ b/packages/skills/fipa_negotiation/skill.yaml @@ -2,6 +2,7 @@ name: 'fipa_negotiation' authors: Fetch.AI Limited version: 0.1.0 license: Apache 2.0 +description: "The fipa skill implements the logic for an AEA to do fipa negotiation." url: "" behaviours: - behaviour: @@ -28,4 +29,3 @@ shared_classes: args: pending_transaction_timeout: 30 protocols: ['oef', 'fipa'] -description: "fipa skill description [Fill in]" \ No newline at end of file diff --git a/packages/skills/gym/skill.yaml b/packages/skills/gym/skill.yaml index 1c4cad6c70..8ddda18fe9 100644 --- a/packages/skills/gym/skill.yaml +++ b/packages/skills/gym/skill.yaml @@ -2,6 +2,7 @@ name: gym authors: Fetch.AI Limited version: 0.1.0 license: Apache 2.0 +description: "The gym skill wraps an RL agent." url: "" behaviours: - behaviour: @@ -21,4 +22,3 @@ shared_classes: [] protocols: ["gym"] dependencies: - gym -description: "Gym skill description [Fill in]" \ No newline at end of file diff --git a/packages/skills/tac/skill.yaml b/packages/skills/tac/skill.yaml index 009b154cce..6feeb7f172 100644 --- a/packages/skills/tac/skill.yaml +++ b/packages/skills/tac/skill.yaml @@ -3,6 +3,7 @@ authors: Fetch.AI Limited version: 0.1.0 license: Apache 2.0 url: "" +description: "The tac skill implements the logic for an AEA to participate in the TAC." behaviours: - behaviour: class_name: TACBehaviour @@ -28,4 +29,3 @@ shared_classes: args: expected_version_id: 1 protocols: ['oef', 'tac'] -description: "tac skill description [Fill in]" \ No newline at end of file diff --git a/packages/skills/weather_client/skill.yaml b/packages/skills/weather_client/skill.yaml index 54646f1802..23e0939389 100644 --- a/packages/skills/weather_client/skill.yaml +++ b/packages/skills/weather_client/skill.yaml @@ -3,6 +3,7 @@ authors: Fetch.AI Limited version: 0.1.0 license: Apache 2.0 url: "" +description: "The weather client skill looks for weather stations to buy weather data from." behaviours: - behaviour: class_name: MyBuyBehaviour @@ -28,4 +29,3 @@ tasks: foo: bar shared_classes: [] protocols: ['fipa','default','oef'] -description: "weather_station_client skill description [Fill in]" \ No newline at end of file diff --git a/packages/skills/weather_station/skill.yaml b/packages/skills/weather_station/skill.yaml index 547761c33d..c087a4156a 100644 --- a/packages/skills/weather_station/skill.yaml +++ b/packages/skills/weather_station/skill.yaml @@ -3,6 +3,7 @@ authors: Fetch.AI Limited version: 0.1.0 license: Apache 2.0 url: "" +description: "The weather station skill offers weather data for sale." behaviours: - behaviour: class_name: MyWeatherBehaviour @@ -20,6 +21,3 @@ tasks: foo: bar shared_classes: [] protocols: ['fipa'] -description: "weather_station skill description [Fill in]" - - From f6166c980a2755651872d13cff69e95b7577d02f Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 15 Oct 2019 18:13:06 +0200 Subject: [PATCH 22/71] fix cli tests the latest changes on the logging system of the CLI made the tests to fail. Switch off all the log messages in the tests. --- tests/conftest.py | 1 + .../test_commands/test_add/test_connection.py | 19 ++++++++++--------- .../test_commands/test_add/test_protocol.py | 19 ++++++++++--------- .../test_commands/test_add/test_skill.py | 18 +++++++++--------- tests/test_cli/test_commands/test_create.py | 10 +++++----- tests/test_cli/test_commands/test_delete.py | 7 ++++--- .../test_remove/test_connection.py | 17 +++++++++-------- .../test_remove/test_protocol.py | 17 +++++++++-------- .../test_commands/test_remove/test_skill.py | 18 +++++++++--------- 9 files changed, 66 insertions(+), 60 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 372731bb4f..8ad1304cb1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,6 +35,7 @@ CUR_PATH = os.path.dirname(inspect.getfile(inspect.currentframe())) # type: ignore ROOT_DIR = os.path.join(CUR_PATH, "..") +CLI_LOG_OPTION = ["-v", "OFF"] CONFIGURATION_SCHEMA_DIR = os.path.join(ROOT_DIR, "aea", "configurations", "schemas") AGENT_CONFIGURATION_SCHEMA = os.path.join(CONFIGURATION_SCHEMA_DIR, "aea-config_schema.json") diff --git a/tests/test_cli/test_commands/test_add/test_connection.py b/tests/test_cli/test_commands/test_add/test_connection.py index 0022c3a566..44e74f3729 100644 --- a/tests/test_cli/test_commands/test_add/test_connection.py +++ b/tests/test_cli/test_commands/test_add/test_connection.py @@ -31,6 +31,7 @@ import aea.cli.common import aea.configurations.base from aea.cli import cli +from tests.conftest import CLI_LOG_OPTION class TestAddConnectionFailsWhenConnectionAlreadyExists: @@ -48,14 +49,14 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() os.chdir(cls.t) - result = cls.runner.invoke(cli, ["create", cls.agent_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) assert result.exit_code == 0 os.chdir(cls.agent_name) # add connection first time - result = cls.runner.invoke(cli, ["add", "connection", cls.connection_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", cls.connection_name]) assert result.exit_code == 0 # add connection again - cls.result = cls.runner.invoke(cli, ["add", "connection", cls.connection_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", cls.connection_name]) def test_exit_code_equal_to_minus_1(self): """Test that the exit code is equal to minus 1.""" @@ -94,10 +95,10 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() os.chdir(cls.t) - result = cls.runner.invoke(cli, ["create", cls.agent_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) assert result.exit_code == 0 os.chdir(cls.agent_name) - cls.result = cls.runner.invoke(cli, ["add", "connection", cls.connection_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", cls.connection_name]) def test_exit_code_equal_to_minus_1(self): """Test that the exit code is equal to minus 1.""" @@ -136,7 +137,7 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() os.chdir(cls.t) - result = cls.runner.invoke(cli, ["create", cls.agent_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) assert result.exit_code == 0 # change the serialization of the AgentConfig class so to make the parsing to fail. @@ -145,7 +146,7 @@ def setup_class(cls): cls.patch.__enter__() os.chdir(cls.agent_name) - cls.result = cls.runner.invoke(cli, ["add", "connection", cls.connection_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", cls.connection_name]) def test_exit_code_equal_to_minus_1(self): """Test that the exit code is equal to minus 1.""" @@ -184,12 +185,12 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() os.chdir(cls.t) - result = cls.runner.invoke(cli, ["create", cls.agent_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) assert result.exit_code == 0 os.chdir(cls.agent_name) Path("connections", cls.connection_name).mkdir(parents=True, exist_ok=True) - cls.result = cls.runner.invoke(cli, ["add", "connection", cls.connection_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", cls.connection_name]) def test_exit_code_equal_to_minus_1(self): """Test that the exit code is equal to minus 1.""" diff --git a/tests/test_cli/test_commands/test_add/test_protocol.py b/tests/test_cli/test_commands/test_add/test_protocol.py index f89043dc71..e34e3a77a8 100644 --- a/tests/test_cli/test_commands/test_add/test_protocol.py +++ b/tests/test_cli/test_commands/test_add/test_protocol.py @@ -31,6 +31,7 @@ import aea.cli.common import aea.configurations.base from aea.cli import cli +from tests.conftest import CLI_LOG_OPTION class TestAddProtocolFailsWhenProtocolAlreadyExists: @@ -48,12 +49,12 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() os.chdir(cls.t) - result = cls.runner.invoke(cli, ["create", cls.agent_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) assert result.exit_code == 0 os.chdir(cls.agent_name) - result = cls.runner.invoke(cli, ["add", "protocol", cls.protocol_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "protocol", cls.protocol_name]) assert result.exit_code == 0 - cls.result = cls.runner.invoke(cli, ["add", "protocol", cls.protocol_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "protocol", cls.protocol_name]) def test_exit_code_equal_to_minus_1(self): """Test that the exit code is equal to minus 1.""" @@ -92,10 +93,10 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() os.chdir(cls.t) - result = cls.runner.invoke(cli, ["create", cls.agent_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) assert result.exit_code == 0 os.chdir(cls.agent_name) - cls.result = cls.runner.invoke(cli, ["add", "protocol", cls.protocol_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "protocol", cls.protocol_name]) def test_exit_code_equal_to_minus_1(self): """Test that the exit code is equal to minus 1.""" @@ -134,7 +135,7 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() os.chdir(cls.t) - result = cls.runner.invoke(cli, ["create", cls.agent_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) assert result.exit_code == 0 # change the serialization of the ProtocolConfig class so to make the parsing to fail. @@ -143,7 +144,7 @@ def setup_class(cls): cls.patch.__enter__() os.chdir(cls.agent_name) - cls.result = cls.runner.invoke(cli, ["add", "protocol", cls.protocol_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "protocol", cls.protocol_name]) def test_exit_code_equal_to_minus_1(self): """Test that the exit code is equal to minus 1.""" @@ -182,12 +183,12 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() os.chdir(cls.t) - result = cls.runner.invoke(cli, ["create", cls.agent_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) assert result.exit_code == 0 os.chdir(cls.agent_name) Path("protocols", cls.protocol_name).mkdir(parents=True, exist_ok=True) - cls.result = cls.runner.invoke(cli, ["add", "protocol", cls.protocol_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "protocol", cls.protocol_name]) def test_exit_code_equal_to_minus_1(self): """Test that the exit code is equal to minus 1.""" diff --git a/tests/test_cli/test_commands/test_add/test_skill.py b/tests/test_cli/test_commands/test_add/test_skill.py index df6087e7f7..9a2cf79830 100644 --- a/tests/test_cli/test_commands/test_add/test_skill.py +++ b/tests/test_cli/test_commands/test_add/test_skill.py @@ -32,7 +32,7 @@ import aea.cli.common from aea.cli import cli from aea.configurations.base import AgentConfig, DEFAULT_AEA_CONFIG_FILE -from ....conftest import ROOT_DIR +from ....conftest import ROOT_DIR, CLI_LOG_OPTION class TestAddSkillFailsWhenSkillAlreadyExists: @@ -50,7 +50,7 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() os.chdir(cls.t) - result = cls.runner.invoke(cli, ["create", cls.agent_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) # this also by default adds the oef connection and error skill assert result.exit_code == 0 os.chdir(cls.agent_name) @@ -61,7 +61,7 @@ def setup_class(cls): yaml.safe_dump(config.json, open(DEFAULT_AEA_CONFIG_FILE, "w")) # add the error skill again - cls.result = cls.runner.invoke(cli, ["add", "skill", cls.skill_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "skill", cls.skill_name]) def test_exit_code_equal_to_minus_1(self): """Test that the exit code is equal to minus 1.""" @@ -100,10 +100,10 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() os.chdir(cls.t) - result = cls.runner.invoke(cli, ["create", cls.agent_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) assert result.exit_code == 0 os.chdir(cls.agent_name) - cls.result = cls.runner.invoke(cli, ["add", "skill", cls.skill_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "skill", cls.skill_name]) def test_exit_code_equal_to_minus_1(self): """Test that the exit code is equal to minus 1.""" @@ -142,7 +142,7 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() os.chdir(cls.t) - result = cls.runner.invoke(cli, ["create", cls.agent_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) assert result.exit_code == 0 os.chdir(cls.agent_name) @@ -156,7 +156,7 @@ def setup_class(cls): side_effect=ValidationError("test error message")) cls.patch.__enter__() - cls.result = cls.runner.invoke(cli, ["add", "skill", cls.skill_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "skill", cls.skill_name]) def test_exit_code_equal_to_minus_1(self): """Test that the exit code is equal to minus 1.""" @@ -195,7 +195,7 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() os.chdir(cls.t) - result = cls.runner.invoke(cli, ["create", cls.agent_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) assert result.exit_code == 0 os.chdir(cls.agent_name) @@ -205,7 +205,7 @@ def setup_class(cls): yaml.safe_dump(config.json, open(DEFAULT_AEA_CONFIG_FILE, "w")) Path("skills", cls.skill_name).mkdir(parents=True, exist_ok=True) - cls.result = cls.runner.invoke(cli, ["add", "skill", cls.skill_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "skill", cls.skill_name]) def test_exit_code_equal_to_minus_1(self): """Test that the exit code is equal to minus 1.""" diff --git a/tests/test_cli/test_commands/test_create.py b/tests/test_cli/test_commands/test_create.py index 8447755486..100e33d077 100644 --- a/tests/test_cli/test_commands/test_create.py +++ b/tests/test_cli/test_commands/test_create.py @@ -39,7 +39,7 @@ from aea.cli import cli from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE from aea.configurations.loader import ConfigLoader -from ...conftest import AGENT_CONFIGURATION_SCHEMA, ROOT_DIR, CONFIGURATION_SCHEMA_DIR +from ...conftest import AGENT_CONFIGURATION_SCHEMA, ROOT_DIR, CONFIGURATION_SCHEMA_DIR, CLI_LOG_OPTION class TestCreate: @@ -57,7 +57,7 @@ def setup_class(cls): cls.cwd = os.getcwd() cls.t = tempfile.mkdtemp() os.chdir(cls.t) - cls.result = cls.runner.invoke(cli, ["create", cls.agent_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) def _load_config_file(self) -> Dict: """Load a config file.""" @@ -191,7 +191,7 @@ def setup_class(cls): # create a directory with the agent name -> make 'aea create fail. os.mkdir(cls.agent_name) - cls.result = cls.runner.invoke(cli, ["create", cls.agent_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) def test_exit_code_equal_to_minus_1(self): """Test that the error code is equal to -1.""" @@ -233,7 +233,7 @@ def setup_class(cls): cls.t = tempfile.mkdtemp() os.chdir(cls.t) - cls.result = cls.runner.invoke(cli, ["create", cls.agent_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) def test_exit_code_equal_to_minus_1(self): """Test that the error code is equal to -1.""" @@ -271,7 +271,7 @@ def setup_class(cls): cls.t = tempfile.mkdtemp() os.chdir(cls.t) - cls.result = cls.runner.invoke(cli, ["create", cls.agent_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) def test_exit_code_equal_to_minus_1(self): """Test that the error code is equal to -1.""" diff --git a/tests/test_cli/test_commands/test_delete.py b/tests/test_cli/test_commands/test_delete.py index d3d993b084..aee3255f98 100644 --- a/tests/test_cli/test_commands/test_delete.py +++ b/tests/test_cli/test_commands/test_delete.py @@ -29,6 +29,7 @@ import aea import aea.cli.common from aea.cli import cli +from tests.conftest import CLI_LOG_OPTION class TestDelete: @@ -43,8 +44,8 @@ def setup_class(cls): cls.t = tempfile.mkdtemp() os.chdir(cls.t) - cls.runner.invoke(cli, ["create", cls.agent_name]) - cls.result = cls.runner.invoke(cli, ["delete", cls.agent_name]) + cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "delete", cls.agent_name]) def test_exit_code_equal_to_zero(self): """Assert that the exit code is equal to zero (i.e. success).""" @@ -81,7 +82,7 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() # agent's directory does not exist -> command will fail. - cls.result = cls.runner.invoke(cli, ["delete", cls.agent_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "delete", cls.agent_name]) def test_exit_code_equal_to_minus_1(self): """Test that the error code is equal to -1.""" diff --git a/tests/test_cli/test_commands/test_remove/test_connection.py b/tests/test_cli/test_commands/test_remove/test_connection.py index ad7d034a60..57408da27c 100644 --- a/tests/test_cli/test_commands/test_remove/test_connection.py +++ b/tests/test_cli/test_commands/test_remove/test_connection.py @@ -32,6 +32,7 @@ import aea.configurations.base from aea.cli import cli from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE +from tests.conftest import CLI_LOG_OPTION class TestRemoveConnection: @@ -49,12 +50,12 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() os.chdir(cls.t) - result = cls.runner.invoke(cli, ["create", cls.agent_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) assert result.exit_code == 0 os.chdir(cls.agent_name) - result = cls.runner.invoke(cli, ["add", "connection", cls.connection_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", cls.connection_name]) assert result.exit_code == 0 - cls.result = cls.runner.invoke(cli, ["remove", "connection", cls.connection_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "remove", "connection", cls.connection_name]) def test_exit_code_equal_to_zero(self): """Test that the exit code is equal to minus 1.""" @@ -94,11 +95,11 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() os.chdir(cls.t) - result = cls.runner.invoke(cli, ["create", cls.agent_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) assert result.exit_code == 0 os.chdir(cls.agent_name) - cls.result = cls.runner.invoke(cli, ["remove", "connection", cls.connection_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "remove", "connection", cls.connection_name]) def test_exit_code_equal_to_minus_1(self): """Test that the exit code is equal to minus 1.""" @@ -137,16 +138,16 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() os.chdir(cls.t) - result = cls.runner.invoke(cli, ["create", cls.agent_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) assert result.exit_code == 0 os.chdir(cls.agent_name) - result = cls.runner.invoke(cli, ["add", "connection", cls.connection_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", cls.connection_name]) assert result.exit_code == 0 cls.patch = unittest.mock.patch("shutil.rmtree", side_effect=BaseException("an exception")) cls.patch.__enter__() - cls.result = cls.runner.invoke(cli, ["remove", "connection", cls.connection_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "remove", "connection", cls.connection_name]) def test_exit_code_equal_to_minus_1(self): """Test that the exit code is equal to minus 1.""" diff --git a/tests/test_cli/test_commands/test_remove/test_protocol.py b/tests/test_cli/test_commands/test_remove/test_protocol.py index 6703a0f38d..ba486fdcca 100644 --- a/tests/test_cli/test_commands/test_remove/test_protocol.py +++ b/tests/test_cli/test_commands/test_remove/test_protocol.py @@ -32,6 +32,7 @@ import aea.configurations.base from aea.cli import cli from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE +from tests.conftest import CLI_LOG_OPTION class TestRemoveProtocol: @@ -49,12 +50,12 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() os.chdir(cls.t) - result = cls.runner.invoke(cli, ["create", cls.agent_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) assert result.exit_code == 0 os.chdir(cls.agent_name) - result = cls.runner.invoke(cli, ["add", "protocol", cls.protocol_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "protocol", cls.protocol_name]) assert result.exit_code == 0 - cls.result = cls.runner.invoke(cli, ["remove", "protocol", cls.protocol_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "remove", "protocol", cls.protocol_name]) def test_exit_code_equal_to_zero(self): """Test that the exit code is equal to minus 1.""" @@ -94,11 +95,11 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() os.chdir(cls.t) - result = cls.runner.invoke(cli, ["create", cls.agent_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) assert result.exit_code == 0 os.chdir(cls.agent_name) - cls.result = cls.runner.invoke(cli, ["remove", "protocol", cls.protocol_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "remove", "protocol", cls.protocol_name]) def test_exit_code_equal_to_minus_1(self): """Test that the exit code is equal to minus 1.""" @@ -137,16 +138,16 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() os.chdir(cls.t) - result = cls.runner.invoke(cli, ["create", cls.agent_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) assert result.exit_code == 0 os.chdir(cls.agent_name) - result = cls.runner.invoke(cli, ["add", "protocol", cls.protocol_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "protocol", cls.protocol_name]) assert result.exit_code == 0 cls.patch = unittest.mock.patch("shutil.rmtree", side_effect=BaseException("an exception")) cls.patch.__enter__() - cls.result = cls.runner.invoke(cli, ["remove", "protocol", cls.protocol_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "remove", "protocol", cls.protocol_name]) def test_exit_code_equal_to_minus_1(self): """Test that the exit code is equal to minus 1.""" diff --git a/tests/test_cli/test_commands/test_remove/test_skill.py b/tests/test_cli/test_commands/test_remove/test_skill.py index 2cd0a55a09..71d2094747 100644 --- a/tests/test_cli/test_commands/test_remove/test_skill.py +++ b/tests/test_cli/test_commands/test_remove/test_skill.py @@ -32,7 +32,7 @@ import aea.configurations.base from aea.cli import cli from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE, AgentConfig -from ....conftest import ROOT_DIR +from ....conftest import ROOT_DIR, CLI_LOG_OPTION class TestRemoveSkill: @@ -50,7 +50,7 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() os.chdir(cls.t) - result = cls.runner.invoke(cli, ["create", cls.agent_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) assert result.exit_code == 0 os.chdir(cls.agent_name) @@ -59,9 +59,9 @@ def setup_class(cls): config.registry_path = os.path.join(ROOT_DIR, "packages") yaml.safe_dump(config.json, open(DEFAULT_AEA_CONFIG_FILE, "w")) - result = cls.runner.invoke(cli, ["add", "skill", cls.skill_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "skill", cls.skill_name]) assert result.exit_code == 0 - cls.result = cls.runner.invoke(cli, ["remove", "skill", cls.skill_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "remove", "skill", cls.skill_name]) def test_exit_code_equal_to_zero(self): """Test that the exit code is equal to minus 1.""" @@ -101,11 +101,11 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() os.chdir(cls.t) - result = cls.runner.invoke(cli, ["create", cls.agent_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) assert result.exit_code == 0 os.chdir(cls.agent_name) - cls.result = cls.runner.invoke(cli, ["remove", "skill", cls.skill_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "remove", "skill", cls.skill_name]) def test_exit_code_equal_to_minus_1(self): """Test that the exit code is equal to minus 1.""" @@ -144,7 +144,7 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() os.chdir(cls.t) - result = cls.runner.invoke(cli, ["create", cls.agent_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) assert result.exit_code == 0 os.chdir(cls.agent_name) @@ -153,13 +153,13 @@ def setup_class(cls): config.registry_path = os.path.join(ROOT_DIR, "packages") yaml.safe_dump(config.json, open(DEFAULT_AEA_CONFIG_FILE, "w")) - result = cls.runner.invoke(cli, ["add", "skill", cls.skill_name]) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "skill", cls.skill_name]) assert result.exit_code == 0 cls.patch = unittest.mock.patch("shutil.rmtree", side_effect=BaseException("an exception")) cls.patch.__enter__() - cls.result = cls.runner.invoke(cli, ["remove", "skill", cls.skill_name]) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "remove", "skill", cls.skill_name]) def test_exit_code_equal_to_minus_1(self): """Test that the exit code is equal to minus 1.""" From 71ec58aaa4875c4ffd64451febf5c23e28d0d2a4 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 15 Oct 2019 18:17:04 +0200 Subject: [PATCH 23/71] fix docstring in 'simple_verbosity_option' function. --- aea/cli/loggers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aea/cli/loggers.py b/aea/cli/loggers.py index 2088ac5c0e..fe6d6f81e3 100644 --- a/aea/cli/loggers.py +++ b/aea/cli/loggers.py @@ -55,7 +55,7 @@ def format(self, record): def simple_verbosity_option(logger=None, *names, **kwargs): - """A decorator that adds a `--verbosity, -v` option to the decoratedcommand. + """Add a decorator that adds a `--verbosity, -v` option to the decorated command. Name can be configured through `*names`. Keyword arguments are passed to the underlying `click.option` decorator. From 3af1eb6a1bfbb37f252396f1bf8f6ba7fc58e45c Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 15 Oct 2019 18:24:18 +0200 Subject: [PATCH 24/71] try to fix flaky 'test_aea.test_handle' test --- tests/test_aea.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_aea.py b/tests/test_aea.py index d492677e65..332e656c73 100644 --- a/tests/test_aea.py +++ b/tests/test_aea.py @@ -138,7 +138,7 @@ def test_handle(): try: t.start() agent.mailbox.inbox._queue.put(envelope) - env = agent.mailbox.outbox._queue.get(block=True, timeout=5.0) + env = agent.mailbox.outbox._queue.get(block=True, timeout=10.0) assert env.protocol_id == "default", \ "The envelope is not the expected protocol (Unsupported protocol)" From d38f840c14464064220f6e3dc730028cb0acfb7a Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Wed, 16 Oct 2019 09:09:06 +0100 Subject: [PATCH 25/71] Addresses PR comments --- docs/skill-guide.md | 60 +++++++++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/docs/skill-guide.md b/docs/skill-guide.md index 1a13b31b61..610d9ea4dc 100644 --- a/docs/skill-guide.md +++ b/docs/skill-guide.md @@ -3,27 +3,36 @@ The scaffolding tool allows you create the folder !!! Note Before developing your first skill, please read the skill guide. + ## Step 1: Setup -Ensure, you have followed the preliminaries. We will first create an agent and add a scaffold skill, which we call `my_search`: +Ensure, you have followed the preliminaries and installation. We will first create an agent and add a scaffold skill, which we call `my_search`: ``` bash aea create my_agent && cd my_agent aea scaffold skill my_search ``` +In the following steps, we will replace each one of the scaffolded `Behaviour`, `Handler` and `Task` in `my_agent/skills/my_search` with our implementation. + + ## Step 2: Develop a Behaviour A `Behaviour` class contains the business logic specific to initial actions initiated by the agent rather than reactions to other events. -In this example, we implement a simple search behaviour. Each time, `act()` gets called, we will send a search request to the OEF. +In this example, we implement a simple search behaviour. Each time, `act()` gets called by the main agent loop, we will send a search request to the OEF. ``` python +import logging +import time + from aea.protocols.oef.message import OEFMessage from aea.protocols.oef.models import Query, Constraint, ConstraintType from aea.protocols.oef.serialization import DEFAULT_OEF, OEFSerializer from aea.skills.base import Behaviour +logger = logging.getLogger("aea.my_search_skill") + class MySearchBehaviour(Behaviour): """This class provides a simple search behaviour.""" @@ -39,8 +48,7 @@ class MySearchBehaviour(Behaviour): :return: None """ - pass - + logger.info("[{}]: setting up MySearchBehaviour".format(self.context.agent_name)) def act(self) -> None: """ @@ -48,6 +56,7 @@ class MySearchBehaviour(Behaviour): :return: None """ + time.sleep(1) # to slow down the agent self.sent_search_count += 1 search_constraints = [Constraint("country", ConstraintType("==", "UK"))] @@ -55,6 +64,7 @@ class MySearchBehaviour(Behaviour): search_request = OEFMessage(oef_type=OEFMessage.Type.SEARCH_SERVICES, id=self.sent_search_count, query=search_query_w_empty_model) + logger.info("[{}]: sending search request to OEF, search_count={}".format(self.context.agent_name, self.sent_search_count)) self.context.outbox.put_message(to=DEFAULT_OEF, sender=self.context.agent_address, protocol_id=OEFMessage.protocol_id, @@ -66,11 +76,13 @@ class MySearchBehaviour(Behaviour): :return: None """ - pass + logger.info("[{}]: tearing down MySearchBehaviour".format(self.context.agent_name)) ``` Searches are proactive and as such well placed in a `Behaviour`. +We place this code in `my_agent/skills/my_search/behaviours.py`. + ## Step 3: Develop a Handler @@ -101,7 +113,7 @@ class MySearchHandler(Handler): def setup(self) -> None: """Set up the handler.""" - pass + logger.info("[{}]: setting up MySearchHandler".format(self.context.agent_name)) def handle(self, message: OEFMessage, sender: str) -> None: """ @@ -114,9 +126,9 @@ class MySearchHandler(Handler): msg_type = OEFMessage.Type(message.get("type")) if msg_type is OEFMessage.Type.SEARCH_RESULT: - nb_agents_found = len(message.get("agents")) - logger.info("[{}]: found number of agents={}".format(self.context.agent_name, nb_agents_found)) self.received_search_count += 1 + nb_agents_found = len(message.get("agents")) + logger.info("[{}]: found number of agents={}, received search count={}".format(self.context.agent_name, nb_agents_found, self.received_search_count)) def teardown(self) -> None: """ @@ -124,13 +136,15 @@ class MySearchHandler(Handler): :return: None """ - pass + logger.info("[{}]: tearing down MySearchHandler".format(self.context.agent_name)) ``` We create a handler which is registered for the `oef` protocol. Whenever it receives a search result, we log the number of agents returned in the search - the agents matching the search query - and update the counter of received searches. Note, how the handler simply reacts to incoming events (i.e. messages). It could initiate further actions, however, they are still reactions to the upstream search event. +We place this code in `my_agent/skills/my_search/handlers.py`. + ## Step 4: Develop a Task @@ -154,7 +168,7 @@ class MySearchTask(Task): :return: None """ - pass + logger.info("[{}]: setting up MySearchTask".format(self.context.agent_name)) def execute(self) -> None: """ @@ -164,11 +178,10 @@ class MySearchTask(Task): :return: None """ my_search_behaviour = self.context.behaviours[0] - my_search_task = self.context.tasks[0] - my_search_behaviour + my_search_handler = self.context.handlers[0] logger.info("[{}]: number of search requests sent={} vs. number of search responses received={}".format(self.context.agent_name, - my_search_behaviour.sent_search_count, - my_search_task.received_search_count) + my_search_behaviour.sent_search_count, + my_search_handler.received_search_count) ) def teardown(self) -> None: @@ -177,11 +190,13 @@ class MySearchTask(Task): :return: None """ - pass + logger.info("[{}]: tearing down MySearchTask".format(self.context.agent_name)) ``` Note, how we have access to other objects in the skill via `self.context`. +We place this code in `my_agent/skills/my_search/tasks.py`. + ## Step 5: Create the config file @@ -211,7 +226,17 @@ protocols: ["oef"] dependencies: [] ``` -## Step 6: Run the agent +We place this code in `my_agent/skills/my_search/skill.yaml`. + + +## Step 6: Add the oef protocol + +Our agent does not have the oef protocol yet. Hence, we add it like so: +```bash +aea add protocol oef +``` + +## Step 7: Run the agent We first start an oef node (see the connection section for more details) in a separate terminal window: @@ -224,6 +249,9 @@ We can then launch our agent: aea run ``` +Stop the agent with `CTRL + C`. + + ## Now it's your turn: We hope this step by step introduction has helped you to develop your own skill. We are excited to see what you will build. From 48372bd6dfaefb9335275a0153f4e299210c361e Mon Sep 17 00:00:00 2001 From: Diarmid Campbell Date: Wed, 16 Oct 2019 10:10:14 +0100 Subject: [PATCH 26/71] fixed issue with gui lists not updating correctly sometimes --- aea/cli_gui/__init__.py | 52 ++++++++++++++++++----------------- aea/cli_gui/templates/home.js | 15 +++++----- 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/aea/cli_gui/__init__.py b/aea/cli_gui/__init__.py index 02fed521b0..78fcaf0060 100644 --- a/aea/cli_gui/__init__.py +++ b/aea/cli_gui/__init__.py @@ -64,6 +64,7 @@ class ProcessState(Enum): oef_node_name = "aea_local_oef_node" max_log_lines = 100 +lock = threading.Lock() def read_description(dir_name, yaml_name): @@ -99,9 +100,7 @@ def is_item_dir(dir_name, item_type): def get_agents(): """Return list of all local agents.""" - agent_dir = os.path.join(os.getcwd(), args.agent_dir) - - file_list = glob.glob(os.path.join(agent_dir, '*')) + file_list = glob.glob(os.path.join(flask.app.agents_dir, '*')) agent_list = [] @@ -115,8 +114,7 @@ def get_agents(): def get_registered_items(item_type): """Return list of all protocols, connections or skills in the registry.""" - agent_dir = os.path.join(os.getcwd(), args.agent_dir) - item_dir = os.path.join(agent_dir, "packages/" + item_type + "s") + item_dir = os.path.join(flask.app.agents_dir, "packages/" + item_type + "s") file_list = glob.glob(os.path.join(item_dir, '*')) @@ -133,7 +131,7 @@ def get_registered_items(item_type): def create_agent(agent_id): """Create a new AEA project.""" - if _call_aea(["aea", "create", agent_id], args.agent_dir) == 0: + if _call_aea(["aea", "create", agent_id], flask.app.agents_dir) == 0: return agent_id, 201 # 201 (Created) else: return {"detail": "Failed to create Agent {} - a folder of this name may exist already".format(agent_id)}, 400 # 400 Bad request @@ -141,7 +139,7 @@ def create_agent(agent_id): def delete_agent(agent_id): """Delete an existing AEA project.""" - if _call_aea(["aea", "delete", agent_id], args.agent_dir) == 0: + if _call_aea(["aea", "delete", agent_id], flask.app.agents_dir) == 0: return 'Agent {} deleted'.format(agent_id), 200 # 200 (OK) else: return {"detail": "Failed to delete Agent {} - it ay not exist".format(agent_id)}, 400 # 400 Bad request @@ -149,7 +147,7 @@ def delete_agent(agent_id): def add_item(agent_id, item_type, item_id): """Add a protocol, skill or connection to the register to a local agent.""" - agent_dir = os.path.join(args.agent_dir, agent_id) + agent_dir = os.path.join(flask.app.agents_dir, agent_id) if _call_aea(["aea", "add", item_type, item_id], agent_dir) == 0: return agent_id, 201 # 200 (OK) else: @@ -158,7 +156,7 @@ def add_item(agent_id, item_type, item_id): def remove_local_item(agent_id, item_type, item_id): """Remove a protocol, skill or connection from a local agent.""" - agent_dir = os.path.join(args.agent_dir, agent_id) + agent_dir = os.path.join(flask.app.agents_dir, agent_id) if _call_aea(["aea", "remove", item_type, item_id], agent_dir) == 0: return agent_id, 201 # 200 (OK) else: @@ -167,7 +165,7 @@ def remove_local_item(agent_id, item_type, item_id): def get_local_items(agent_id, item_type): """Return a list of protocols, skills or connections supported by a local agent.""" - items_dir = os.path.join(os.path.join(args.agent_dir, agent_id), item_type + "s") + items_dir = os.path.join(os.path.join(flask.app.agents_dir, agent_id), item_type + "s") file_list = glob.glob(os.path.join(items_dir, '*')) @@ -184,7 +182,7 @@ def get_local_items(agent_id, item_type): def scaffold_item(agent_id, item_type, item_id): """Scaffold a moslty empty item on an agent (either protocol, skill or connection).""" - agent_dir = os.path.join(args.agent_dir, agent_id) + agent_dir = os.path.join(flask.app.agents_dir, agent_id) if _call_aea(["aea", "scaffold", item_type, item_id], agent_dir) == 0: return agent_id, 201 # 200 (OK) else: @@ -192,20 +190,24 @@ def scaffold_item(agent_id, item_type, item_id): def _call_aea(param_list, dir): - old_cwd = os.getcwd() - os.chdir(dir) - ret = subprocess.call(param_list) - os.chdir(old_cwd) + # Should lock here to prevet multiple calls coming in at once and changing the current working directory weirdly + with lock: + old_cwd = os.getcwd() + os.chdir(dir) + ret = subprocess.call(param_list) + os.chdir(old_cwd) return ret def _call_aea_async(param_list, dir): - old_cwd = os.getcwd() - os.chdir(dir) - env = os.environ.copy() - env["PYTHONUNBUFFERED"] = "1" - ret = subprocess.Popen(param_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) - os.chdir(old_cwd) + # Should lock here to prevet multiple calls coming in at once and changing the current working directory weirdly + with lock: + old_cwd = os.getcwd() + os.chdir(dir) + env = os.environ.copy() + env["PYTHONUNBUFFERED"] = "1" + ret = subprocess.Popen(param_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) + os.chdir(old_cwd) return ret @@ -213,8 +215,6 @@ def start_oef_node(dummy): """Start an OEF node running.""" _kill_running_oef_nodes() - CUR_DIR = os.path.abspath(os.path.dirname(__file__)) - param_list = [ "python", "scripts/oef/launch.py", @@ -224,7 +224,7 @@ def start_oef_node(dummy): "-c", "./scripts/oef/launch_config.json"] - flask.app.oef_process = _call_aea_async(param_list, os.path.join(CUR_DIR, "../../")) + flask.app.oef_process = _call_aea_async(param_list, flask.app.agents_dir) if flask.app.oef_process is not None: flask.app.oef_tty = [] @@ -286,7 +286,7 @@ def start_agent(agent_id): else: return {"detail": "Agent {} is already running".format(agent_id)}, 400 # 400 Bad request - agent_dir = os.path.join(args.agent_dir, agent_id) + agent_dir = os.path.join(flask.app.agents_dir, agent_id) agent_process = _call_aea_async(["aea", "run"], agent_dir) if agent_process is None: return {"detail": "Failed to run agent {}".format(agent_id)}, 400 # 400 Bad request @@ -397,6 +397,8 @@ def run(): flask.app.agent_tty = {} flask.app.agent_error = {} flask.app.ui_is_starting = False + flask.app.agents_dir = os.path.join(os.path.abspath(os.getcwd()), args.agent_dir) + flask.app.module_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), "../../") app.add_api('aea_cli_rest.yaml') diff --git a/aea/cli_gui/templates/home.js b/aea/cli_gui/templates/home.js index 405e783d19..378c5dd050 100644 --- a/aea/cli_gui/templates/home.js +++ b/aea/cli_gui/templates/home.js @@ -337,8 +337,6 @@ class Controller{ } }, 100) - - // Go through each of the element types setting up call-back and table building functions on the // Items which exist var self = this; @@ -365,6 +363,7 @@ class Controller{ if (self.validateId(id)) { self.model.deleteItem(e.data.el, id) + self.view.setSelectedId(e.data.el["combined"], "NONE") } else { alert('Error: Problem with selected id'); } @@ -380,6 +379,10 @@ class Controller{ if (self.validateId(agentId) && self.validateId(itemId) ) { self.model.addItem(e.data.el, agentId, itemId) + self.view.setSelectedId(e.data.el["combined"], "NONE") + var tableBody = $("."+ e.data.el["combined"] +"registeredTable"); + self.clearTable(tableBody); + } else { alert('Error: Problem with one of the selected ids (either agent or ' + element['type']); @@ -394,6 +397,8 @@ class Controller{ if (self.validateId(agentId) && self.validateId(itemId) ) { self.model.removeItem(e.data.el, agentId, itemId) + self.view.setSelectedId(e.data.el["combined"], "NONE") + } else { alert('Error: Problem with one of the selected ids (either agent or ' + element['type']); @@ -465,22 +470,18 @@ class Controller{ this.$event_pump.on('model_'+ combineName + 'DeleteSuccess', {el: element}, function(e, data) { self.model.readData(e.data.el); - self.view.setSelectedId(e.data.el["combined"], "NONE") + self.refreshAgentData(data) self.handleButtonStates() }); this.$event_pump.on('model_'+ combineName + 'AddSuccess', {el: element}, function(e, data) { self.refreshAgentData(data) - self.view.setSelectedId(e.data.el["combined"], "NONE") - var tableBody = $("."+ e.data.el["combined"] +"registeredTable"); - self.clearTable(tableBody); self.handleButtonStates() }); this.$event_pump.on('model_'+ combineName + 'RemoveSuccess', {el: element}, function(e, data) { self.refreshAgentData(data) - self.view.setSelectedId(e.data.el["combined"], "NONE") self.handleButtonStates() }); From b31588860ee0cd487401bdaadb10a956ce57a572 Mon Sep 17 00:00:00 2001 From: Diarmid Campbell Date: Wed, 16 Oct 2019 11:14:48 +0100 Subject: [PATCH 27/71] oef node should now run when isntalled from pip --- aea/cli_gui/__init__.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/aea/cli_gui/__init__.py b/aea/cli_gui/__init__.py index 78fcaf0060..b7f3ee7299 100644 --- a/aea/cli_gui/__init__.py +++ b/aea/cli_gui/__init__.py @@ -19,7 +19,6 @@ """Key pieces of functionality for CLI GUI.""" -import argparse from enum import Enum import glob import io @@ -33,16 +32,6 @@ import yaml -parser = argparse.ArgumentParser(description='Launch the AEA CLI GUI') -parser.add_argument( - '-ad', - '--agent_dir', - default='./', - help='Location of script and package files and where agents will be created (default: ./)' -) -args = None # pragma: no cover - - elements = [['local', 'agent', 'localAgents'], ['registered', 'protocol', 'registeredProtocols'], ['registered', 'connection', 'registeredConections'], @@ -224,7 +213,7 @@ def start_oef_node(dummy): "-c", "./scripts/oef/launch_config.json"] - flask.app.oef_process = _call_aea_async(param_list, flask.app.agents_dir) + flask.app.oef_process = _call_aea_async(param_list, flask.app.module_dir) if flask.app.oef_process is not None: flask.app.oef_tty = [] @@ -397,7 +386,7 @@ def run(): flask.app.agent_tty = {} flask.app.agent_error = {} flask.app.ui_is_starting = False - flask.app.agents_dir = os.path.join(os.path.abspath(os.getcwd()), args.agent_dir) + flask.app.agents_dir = os.path.abspath(os.getcwd()) flask.app.module_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), "../../") app.add_api('aea_cli_rest.yaml') @@ -423,8 +412,5 @@ def favicon(): # If we're running in stand alone mode, run the application if __name__ == '__main__': - args = parser.parse_args() # pragma: no cover run() -else: - args, _ = parser.parse_known_args() From 2e681187d94bec760ec900557cd21a7e9204c5c3 Mon Sep 17 00:00:00 2001 From: Diarmid Campbell Date: Wed, 16 Oct 2019 11:33:45 +0100 Subject: [PATCH 28/71] fixed flake8 test --- aea/cli_gui/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aea/cli_gui/__init__.py b/aea/cli_gui/__init__.py index b7f3ee7299..368b0dedbf 100644 --- a/aea/cli_gui/__init__.py +++ b/aea/cli_gui/__init__.py @@ -413,4 +413,3 @@ def favicon(): # If we're running in stand alone mode, run the application if __name__ == '__main__': run() - From bd47cdd07e08837329d4f12ba76b2e1db0daf9e2 Mon Sep 17 00:00:00 2001 From: Aristotelis Date: Wed, 16 Oct 2019 12:49:08 +0100 Subject: [PATCH 29/71] Changes based on the PR comments --- aea/crypto/wallet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aea/crypto/wallet.py b/aea/crypto/wallet.py index f50d26c223..289b5cc853 100644 --- a/aea/crypto/wallet.py +++ b/aea/crypto/wallet.py @@ -71,6 +71,6 @@ def crypto_objects(self): return self._crypto_objects @property - def addresses(self): - """Get the crypt addresses.""" + def addresses(self) -> Dict[str, str]: + """Get the crypto addresses.""" return self._addresses From 15480b0adf996c0d31174a8f40f45270af6d0438 Mon Sep 17 00:00:00 2001 From: Diarmid Campbell Date: Wed, 16 Oct 2019 13:15:24 +0100 Subject: [PATCH 30/71] fixed starting oef node - which I broke in the last commit --- aea/cli_gui/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aea/cli_gui/__init__.py b/aea/cli_gui/__init__.py index 368b0dedbf..bcaea9821c 100644 --- a/aea/cli_gui/__init__.py +++ b/aea/cli_gui/__init__.py @@ -213,7 +213,7 @@ def start_oef_node(dummy): "-c", "./scripts/oef/launch_config.json"] - flask.app.oef_process = _call_aea_async(param_list, flask.app.module_dir) + flask.app.oef_process = _call_aea_async(param_list, flask.app.agents_dir) if flask.app.oef_process is not None: flask.app.oef_tty = [] From 6a7682db6f529a062d0ff48192289134d2c113c4 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Wed, 16 Oct 2019 14:20:55 +0200 Subject: [PATCH 31/71] 100% on aea.cli.add --- aea/cli/add.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aea/cli/add.py b/aea/cli/add.py index 748cb9198b..65ef0c32de 100644 --- a/aea/cli/add.py +++ b/aea/cli/add.py @@ -130,7 +130,6 @@ def protocol(click_context, protocol_name): except ValidationError as e: logger.error("Protocol configuration file not valid: {}".format(str(e))) exit(-1) - return # copy the protocol package into the agent's supported connections. src = str(Path(os.path.join(registry_path, "protocols", protocol_name)).absolute()) From 81051353ebe51bdc9f8efd8343cbf8beefe020fd Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Wed, 16 Oct 2019 14:54:58 +0200 Subject: [PATCH 32/71] add tests for 'aea freeze'. --- aea/cli/common.py | 2 +- tests/data/dummy_aea/aea-config.yaml | 8 +-- tests/test_cli/test_commands/test_freeze.py | 57 +++++++++++++++++++++ tests/test_cli/test_schema.py | 23 ++++++++- 4 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 tests/test_cli/test_commands/test_freeze.py diff --git a/aea/cli/common.py b/aea/cli/common.py index 79ad7a04ee..8902361188 100644 --- a/aea/cli/common.py +++ b/aea/cli/common.py @@ -106,7 +106,7 @@ def _try_to_load_agent_config(ctx: Context): except FileNotFoundError: logger.error("Agent configuration file '{}' not found in the current directory.".format(DEFAULT_AEA_CONFIG_FILE)) exit(-1) - except jsonschema.exceptions.ValidationError: + except jsonschema.exceptions.ValidationError as e: logger.error("Agent configuration file '{}' is invalid. Please check the documentation.".format(DEFAULT_AEA_CONFIG_FILE)) exit(-1) diff --git a/tests/data/dummy_aea/aea-config.yaml b/tests/data/dummy_aea/aea-config.yaml index 175ff058fa..5d33d13a96 100644 --- a/tests/data/dummy_aea/aea-config.yaml +++ b/tests/data/dummy_aea/aea-config.yaml @@ -1,4 +1,4 @@ -aea_version: 0.1.4 +aea_version: 0.1.7 agent_name: Agent0 authors: Fetch.AI Limited connections: @@ -6,9 +6,9 @@ connections: default_connection: local license: Apache 2.0 private_key_paths: - private_key_path: - ledger: default - path: '' + - private_key_path: + ledger: default + path: '' protocols: - fipa - default diff --git a/tests/test_cli/test_commands/test_freeze.py b/tests/test_cli/test_commands/test_freeze.py new file mode 100644 index 0000000000..5734b91371 --- /dev/null +++ b/tests/test_cli/test_commands/test_freeze.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the `aea create` sub-command.""" +import json +import os +import shutil +from pathlib import Path + +import jsonschema +from click.testing import CliRunner +from jsonschema import Draft4Validator + +from aea.cli import cli +from ...conftest import AGENT_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR, CLI_LOG_OPTION, CUR_PATH + + +class TestFreeze: + """Test that the command 'aea freeze' works as expected.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.schema = json.load(open(AGENT_CONFIGURATION_SCHEMA)) + cls.resolver = jsonschema.RefResolver("file://{}/".format(Path(CONFIGURATION_SCHEMA_DIR).absolute()), cls.schema) + cls.validator = Draft4Validator(cls.schema, resolver=cls.resolver) + + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + os.chdir(Path(CUR_PATH, "data", "dummy_aea")) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "freeze"]) + + def test_correct_output(self, capsys): + """Test that the command has printed the correct output.""" + assert self.result.output == """protobuf\n""" + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) diff --git a/tests/test_cli/test_schema.py b/tests/test_cli/test_schema.py index 870d0a18b9..45828f4280 100644 --- a/tests/test_cli/test_schema.py +++ b/tests/test_cli/test_schema.py @@ -28,7 +28,7 @@ from jsonschema import validate, Draft4Validator # type: ignore from aea.configurations.base import DEFAULT_PROTOCOL_CONFIG_FILE, DEFAULT_CONNECTION_CONFIG_FILE, \ - DEFAULT_SKILL_CONFIG_FILE + DEFAULT_SKILL_CONFIG_FILE, DEFAULT_AEA_CONFIG_FILE from ..conftest import CUR_PATH, ROOT_DIR, AGENT_CONFIGURATION_SCHEMA, SKILL_CONFIGURATION_SCHEMA, \ CONNECTION_CONFIGURATION_SCHEMA, PROTOCOL_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR @@ -59,6 +59,27 @@ def test_validate_agent_config(): validate(instance=agent_config_file, schema=agent_config_schema, resolver=resolver) +class TestAgentSchema: + """Test that the agent configuration validation works.""" + + @classmethod + def setup_class(cls): + """Set up the test class.""" + cls.schema = json.load(open(AGENT_CONFIGURATION_SCHEMA)) + cls.resolver = jsonschema.RefResolver("file://{}/".format(Path(CONFIGURATION_SCHEMA_DIR).absolute()), cls.schema) + cls.validator = Draft4Validator(cls.schema, resolver=cls.resolver) + + @pytest.mark.parametrize("agent_path", + [ + os.path.join(CUR_PATH, "data", "dummy_aea", DEFAULT_AEA_CONFIG_FILE), + os.path.join(CUR_PATH, "data", "aea-config.example.yaml") + ]) + def test_validate_agent_config(self, agent_path): + """Test that the validation of the protocol configuration file in aea/protocols works correctly.""" + protocol_config_file = yaml.safe_load(open(agent_path)) + self.validator.validate(instance=protocol_config_file) + + class TestProtocolsSchema: """Test that the protocol configuration files provided by the framework are compliant to the schema.""" From 64790cef593e9559762552a874e78d92d246827e Mon Sep 17 00:00:00 2001 From: Diarmid Campbell Date: Wed, 16 Oct 2019 14:35:57 +0100 Subject: [PATCH 33/71] added gui instructions --- docs/assets/cli_gui01_clean.png | Bin 0 -> 183696 bytes docs/assets/cli_gui02_sequence.png | Bin 0 -> 175232 bytes docs/assets/cli_gui03_oef_node.png | Bin 0 -> 46111 bytes docs/assets/cli_gui04_new_agent.png | Bin 0 -> 68113 bytes docs/assets/cli_gui05_full_running_agent.png | Bin 0 -> 241343 bytes docs/cli-gui.md | 39 +++++++++++++++++++ 6 files changed, 39 insertions(+) create mode 100644 docs/assets/cli_gui01_clean.png create mode 100644 docs/assets/cli_gui02_sequence.png create mode 100644 docs/assets/cli_gui03_oef_node.png create mode 100644 docs/assets/cli_gui04_new_agent.png create mode 100644 docs/assets/cli_gui05_full_running_agent.png create mode 100644 docs/cli-gui.md diff --git a/docs/assets/cli_gui01_clean.png b/docs/assets/cli_gui01_clean.png new file mode 100644 index 0000000000000000000000000000000000000000..9f650d08bd11a663b4b851f4f87f3c1d51e649c6 GIT binary patch literal 183696 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­>?tQw2(jtp! z!3tPF>`HB|J6a!r=85KoTots&XfzULoNaJ1t&925e90lf_$gd~ z&{9FXTvcBH6>p4vE3##?X@TZ=I>t)QkaUh=Y%>4uL0x9I1yU;nW;k$ipTf)yzkH%s zW5?@{%dpRSzHlyV?M{>*B0jD4yun^!4mLbH{iF1gRcZ1A9qC{alToTaVW3Dl+^5#f z_a1M3b>TSU14jLpyGsoy$fu;v4YE%GnZ?t?iCEEe_ce$44K)Dc+dQo(Uz$HCupta% zuN&v5XAv31s|!l3``94K_v(NZ31T&}TnPOZ^z2zO<&3ggeq(U~g??{)mNR>$?&E2M zPefr6`>cFOQpTbX+epv@46{6x9mYr;cCeFXKFS#uJP~u~8x^ z5>Bw5OE*Ye4n|=jURNQb8lgtYjc_x>*BTVaa-rGZGoYq88)e&@{7Yk3GH}2IqQ7iv zXhU*R)fqKizt0`^Au3K9T@% zV}5{ef%q(t|IF}(meTM1xveip?XXiCTpx;z>YR4BjN!8uNX!9vHD%J~752k^ad>L6 z=BQlBh2o060nJv*yjeyO+z}?m?2E}B-J@lfILCffJt1&F&NVS6HpUk}i=H}wnoKWO14VtFN{TwlFUXmpgd!mLJ#0ebCF z%dpWYrN&_txuRA96|r5PaLwM_dh?ZIYHKlca#K>Ev^cR;r=Y2MXXL(J@~Po&Ynpiy zo4}L8@}8>g=O_J|<&8@})l&qQ%{c9a<>vJ#ljS9MW#*oc{scpZ53~$hUxu50u+G+h zfpE*Jk>~?$!WYDc+Je65)ofdrr`peqNh4#mU;8U)sddcGwbX&54FcX^H)R60=`SwkE)(-Y1I^12HO`Rg*@f>`4`ad0_|-GT{%Ii zX{O&z&fAN<(O@kMMKUd> z$$i4k!hs!~kD)|Bcg!E!Lc^}}Zb~1Wa|J~@V}`^aE5)n6OvNl2r=@A|7u@TbSMeZp zz+>Uy7_7}XqcaC(R_g1fmP^{@$cl+m%(3G5)Y4w)^RoBVh09BLmXKt6`^-Dr zFq1?3oOvVfMGrNf>gabKR*<9 zRkHMhtnJPg{_dvD=T42XeEI0ZhGB?Sp+c@7-4Y z_`Elb*PRnRKKLOM8&g{=MthrD?CY;zMW{4|*D<&DQs&)`GR9OTyV^P;Aw_kK{(?^X zUDeZtrVMr%7V+sQsq}3vZf~}594Aa*GW?tb*VBh&Bo}YqjJe!{ZqX2OG^&kYs1^&u z*Ro5bHuo*sq3`yTMzBjPh5DoUxw#Q2S*jhUZ|2eDxP$Z$eeSJie7vZO9-Qg$IuY^t zag8{s?`iwbq?;;2TJ#o2`Y3t};$QsEJS3Jg`SQa?qxqRw&(edA!rhw`sH^Vr z2+F*P9=$*RzVR0Yb(2nZfZeCe$(ja-OHjntA6DyVLBE6`?6{bx))8~H=YdnHlkSxG zq<6?*_U_=1PQjfuXIE3I80X3WUITNHr-vKUchj!^!9^`2_zZWjdb+&=(p&NqF&3Kw zv-K~2_D!<%vPj`cS^ByTuc7^EHUhc3rgp_cSi*1$;(ABx8SFGKq98`UIb>2`GI>J5rlL&GD%ghq?^}H z*Ro_q6Y30aG};31+*utfg7q}G?{1kNZBsFnm??m@g}L9`r91DTGPX66%lG70M(@y_ zJHK5Dm+#meXRMC@<0g3|Yh{(fJcGg*JvXzx?yO>GdTymZD_j11N{yRB?Y2y(*@RF< z8^vq1LvkM=BOXpE%y{Vf7~{4e@4vk^Sj{s=ECUB4~pt z=YUV|$1_>p8$8bvjLFeJPKmj;U_C2%y2aTz(r)Xu6~}1|Vuc;9?1NaB-a3`Ebh{>W z=R>RK%FrDzlPZ(J3-DmMpv89?4S9vFZe-1yxvOx@1c>C6m}RBxz4o&XC@0&T>xv1P zZ@$SM+3Qql<=A>!bR70Uu$9StvBVyvpY|G((6nvC&G0ckGV)iTgyH61VCeKGE7aG1 z-#5)!$x?SlU$UOFpOmK|xl=Mn`Q!E*q*J7z>pelqg~Cx);g_eLNlBmj$Toetk#S^V zw3uRc74PH>7xATcb!w8;N>N`$Qj-fSI4WEDKZImlP|AXIbaqNa6Su58c!Jw)s5I<> z$^Na-Wz0G!XT~tP%|NYiY(Tb0?e{@7P6nh;H`7Sdfgwliuz+E;cZT#TL>EYH z@X~Jv3*Ad_=2!8l#z9d|=4NKrC}`d8u6O3;bCQ6$36~x|l6y@pJ9Q0LbiY=Nc$!p~ zy&kIA^-L@b#{~4LkjR*K^~51UCg{QuvAeqv>M2#PCrPf7*()o$NiMp3w`Xb|zBVHS zzKc^tyY9}ea|MB)D5opKBK=M;U{1&FjTb0aMlbErKlsS$;)BUB)7D=!<=3hdTKanN z6r%pF{5&My4F@u4kB@C$QLS{Pez5%dT^S~Tg+|NHknL*Xm>{n>Q7(1UKQ-me^)?FA zy``*3@KAd>UA?V$h7)5V5npoH*kAWyXvnlK5tHCJJG;7{oRcnkqZ^tm<-b$KBBLlT zOPwz9Pic7%-OCQwL25K4!#W8U+Mhfp+sz_Ef5C3u&3k;_nnHYuChICqZz;X8@K?h{ zCStNnx$+?yDh)&DYJqpJHC48@qvTc_Q}r)7wc1? zNnvUk0YF0jx_Pw?jq=kQ#ALfL`JmFI!$67FgJ{AzZN12v@H>yc@!AC6mlm1E_9WlL z_=O@-*ei})^p~aHWFCk4;@0z8bMYeaDN*Z1VNgiM$V=jhVg{0ni}qpFMe3BLpxr%A zMf7=`!3iahQ7`#XQ{fWCU8+-;Kd7&NO+J_P^4NQQo!$;FNF^)y?LOd{|6P~Mg=F~6 z<}1?1g_Uj#_l-tE7xc_dv*@LXm;wP<|sH=F(^6AipPG?dVD;O{AN zuA6>8h_e)MF=Rh{srr96?BD5ZEk0t7PMJnoZ3*88Pm=VKHaoJpxw${nQF2aG^e>%b zA%IZW*dDPJRLr9swW#3vp#)sA)W4s9lJ;iNn#h^HU!uyt)%-u1kZhKHy4c~ z3BC$QztHNj|J15y^+>u*GsORm#Y0W4jGdiB&C}D9xb=Ed{5i{UsQfn)onakaHc@@y zHf;r9dkoZU%kdtmvi4;E?m&zrX^K0y+5lP9&1jKQzdtH+gr3Clagi(z@pI)QYs*n$ z@Sz{0&Z+a1>p~_mob2H8QdiFR{E|U)wrGP2NUU)~AK`Jt2H`mAUXfCY+Q=s}SI60t z+ACdjg;X&^{3=NstTa)tCnD zSfsY_GTr2qk*wwW7#2)|GYo0QdM)SOrCjVE%weHwd2|lDc2VcsyGVaWZnNeWD!htM zlvGW6e%xV#O)K;rxhC1$$0y!`5fV))@r5=TFCC43(KQwsy01Pjx3!CS(|p=;v;cYc z?xGaGypa*Ku(((a*r1ijnEKXt(nTgFkI@avR)CjM3<4&+suPlv*^TR660JFwKvhJU;bd;s)HGT@xw%2tBGeAM9S6s z0N|Stg36Ye%x_PYL7tBs}fLef>g;J3pq@*;Jp)-%wm&)| zM_K|w+@?*nUMrOTAd0;M%qw~{;}wFNq_`N-C8MWuYcKUSo?~`c0N6>qsr##Fv3bkX zYs$(Y0&d)@aP_u0mlW!zH?V1K-m-WzVKt$AdWcu_$6#;Te9!N!K5HY^wly_t38*;1 z;kPjZ7MYaC@sQF{`)(j-zfwxu`#JHoV<cA!|k^mwrg4JD&Z*u0 zWQN5$-uZ5gYvAEl-I?VOB7x8=ut(}W`W1DZNsqm+=S98Cyj{pQ`otB7v!k)={We{& z2A8@J9i9HMtwE^k-{!vrp)hSZ{3E4Uf}-!{wCfMC;p*X-&)LVz?2O;s=T?Mi(=Kj1 z9Bl;Pzs_#=_Kueq1x|WZmqS-WgmUM}_Pt~kD#`b9(s%RN5lU_NZ12?pA3@M0L*vQV z021-YEJw^en<)!UdwV`G#YOg8;mBh^_hJ%G)WI{Wsuc3=Y z(&cav?V>WQo!>sDT{q9lAI>8M+DMv2hjdMFyM@R%NDF;Jz|Fy@vC?8epZu@qB?cW2 zM`YK_2`t|xYrLxZq0eOrz@vBS3#^NVK(9uY|kjW)gw_O z6JUYT8E+V=K{H*lq-xXbG|x_zG9_K#YKZceJS#OZti0`)LHAOIUSvX=``4hHy0o@o zvKUAhpN-bzF{pH)9#t-uW!bCyAOz~l^3?C6AKA<#6Jof6ce&wn> zy0HQzSb7V6ylgv0%opD(EtBiYe3N8&tyNVe5Ao!b?2Sv$5phmsN<IKdUf$Zr7iF2S~LaIC8t|i`XL(;$bI?HrAG}j*XIH;s+{djK`!=7FOD?`$!j0%GWp@R?O z=DTuYQsoH8kF)(YUK$sHwqgGq-OGz$DU}Z&UGhXtFOvIBtjPtjUFK?-;gRq{%oaC7PY*!(BlMw7u{lPo}Gz+6T zJK2lJ29(~rkK!T#RN>N2e>+QM!Co+D+xVCyuBi;|@45Uut=pB9FWH#CRelWyUw_@9 z=)73C^m5?6tAlZ)4qsDlyz7KNb(e%E-O!+Wdk_UX|0|pv;iQHtbiMn;%~VRNKY;i#F+V-cSuTgi=s*@eAXv= z_8mu2@3FHklDVHgC3$WKAI*d~1*5OK1P6ONd~Dph#uAfotozFhwK_U0PuwOG- zt_L1%)c6d_u4@>Vlr;H`_MQr3dt>*+vShU@dW5u_Ke&(JgB(U@&S_XyE!+F^fp`$_dax< zw}=YdaeUY%;p0OxXVF_9j&7W0*8z#!5+{E*gdQvKc;4ubg*Yl*z&DyJ-ay34pawdFGHvb*9wQ){T@`gCwY&S~rVn!QPhFB9C?o9~^7vyfSh z!9D%I@07xbE{M9$u`OxHn@1Nf3?IKn%z@D|c3=rWx)2}e^&&NkZU|aFZ@M;K?)*b@ z)iKq+|3n>$!xHblFr9B~+fZU-$=D9@Z*+PT(s&^a|`?z6i z&ehym%w5EPS6!vmYo<5pxpOde)jDBj*I0nX<%&eEoc9#iwjhJZAR1{78bwW;X=y^T zmpYfT2kOmOZ*XX3Lmk&{fV-q54y1e>mfb-^vzgA5W0HiZ;@M#X@#qjQa-;nSt35kS z!Dpb6&13Sz^2zr+mr>S&-}$spk6Nn%T|nbW)V{Lk+~zt{^KgOE^e}e3+`wx)$y5q7 z<%2e?{;6pZ6rpPS)A&`zc3_b2`YCQdiNBSg6~Z9Zu0P(DRf*h1JCCsBcUrY@MsYiG zkWExiYCO#+KJ~QiQ(M>PMa^#-*7MGn6l~+uyJl-wAjC%AFoZ)uPNpmm#x4HuyFjiv z5(bKquq>mQI@fho?hSp3B##$X*4Fc|&($A@u@qY^%5PhDk{nLT3fEdnKB*JN2UW5)Hjtx}W~CRu(|>bVlEtrSzn@@&{|Y9N`lx z3%<<&pRIL-WW^k{g^}t`bTk9x7S!ZGRri79$3d>wmA2wGXL*lr)k`q4^*;ho`JQ4p zGOD`?HCyPq_m#u>ZBK#wm8`Z_J-PDwOv$|F^}UKCH=RZEBlo2w&5v%#5Lm9X++X4A;qYL#R_ep2 z0$T;h_1$6gY+o9Wdn|8X?we+9C+f*7&}FYb z8EGe+ZjOpM-X1SN)H;MTY2MjC47@wKp25Opw9hcVl%65geiRWCDA#um%zH>8;xv|w z;z+N&MGeqiTJq~5j>bZE14*N&SLmrQ(i!99ZOsGgQ@9VQ92RnB3oKa6w`JS$$8CRd zpH#x>IeB?BqQVt^YEPAKM04}_sQcTyT`VJ(OA(1%?Fbjv4q56=+3fabVlDSykG}Od z7Un1$SW%*|=FuS=4U3_xTOUikU-{{gFlkh_w%*A~VRgAdMQZCA!5A3&oKQLN!P@xY z3By>tz{`pgan|ws%&z)u{b!%QFD~ERL-fdSSE4W7ak=9En(#3%{+*DTx$=UX<;0$+ z;M@SytbJgkq*Fi>W&YYqP!_uZXCIY8sb!_1@+#aAF`yL+dyar+uYlbFfur+y|8UkH zrATS=m$gmQWOQ7|S>Tq5@p_YU zrLe;3zRqzhgJ=S1;p`~}O6iU8c5>f(aZ`kLu9)H|i{pry<)!)8n1RWB$R@p*V|e8) zan=?!AptD@=i*?Go^7;ht6rn-6NIVqJ_3-IU?TFd#A@f{c0b=4+U*xwRHmiCeJjxM zgyjf}GH9TkF*i3BDvZUo(i|DZ7Cf@pZ%gn9p-|!X$xSenGb|EG7G7)4Q4STfE-y*G zw=*w~wE~HijAlNW`T82(@*_+NgH4^(N-*+Q>-!b1)tS2T2M?=?@;S)Ik88h*zJn7V z04RLA4S^XxlHpsSg^_y4^`T&5eE?GD-JESmyYu+%lrO?(+pWgKLCuA!6Ru6Z0wKMj zam5|ry+z}Z3q;+0zq74xRbS38H*j#=7*6ac(9P3mX6Im`&7!VJ7ue^W(RZU`%Giz> zqBg+a(T*x_52RXiwg?QTbdj^u-EsIP*-8g)Kk*3BsS}htu01&({p7;$-$B9BGv|L< zqb%KYdIDAUC^_SpZ76@$SNKgF=3ZZ=15?F1{}XNBF5Ijf`oyCVUm3VKvtF69UU_u( z*Z|D65}Vo{PJwlP#E=OkgnNt>s%D?U`X{HqQo#x%v6fqZ&almar)g+HEo5j7#lS16 zP)UerV|vE##C&(;N8wqi6-UE-l~+l|^AohH3b3ZSvB-=_!Ho9hlmZ$?O!p~rQ!6-@ z(-x$Ml&4d9&A(S#ZKtQfpDs8v-alvXYLN-EoL&>c;d)AP61C;dK5IQHhm6YZ3>Ao` zS3)oVsVl=-a&cwKs?&OttVNOpTzbo*l;3n>Zyotm?V*4$c=M=iT$fL_qNdd}q+vc7 z(;3nbQLPicduU5CtQBg;**UHNuNZK;hk}Ygy5Fl9HhLEIX9#tDg=AVF_2iQGB)c5s z%?C@cyWr#R6pYR0A1%U!9fsty1Ggjst?v}SyA|s$s8$FSe*vf&T&30BAJ82@o_}!^ z6I*aT?zb;p_Be@3hp~1>->BmTK}E%%@QIhSoJhLXZz{dc88~+C)2_=ETl!k~=?=wy z-4Rygea>uqP|Kn0at?ohvKlAybn_^(smFadcwq4pDIaQDtU-3>x`V{_oY$t^B$`OtoUB&L!(Dh9Z^l@Ytn)vU*|e8Nj=T2N9Mgea*YBx^4#I>+NSHS-`kA z8xQ67i?v{u;2}}xnJKUAwehlaL^0m>GaTFvXmedFo6v-Ghj0Q45c(JmxD1;BySDpH z!6p7SYhGSjS+5%&&x&UIzW8^x`#&hllP9-vg9W^AxjNEV&~N^+Tk8=%BJ$^$Tv4U; z?$B9ASdQk>pd2S&GXgT|g@{Kx=$?(eH$r7E7a5q*20LD2p0DrOxLl-|5em}k>tiBU zFPT||?Awj&LwXMun;BL~!8K(!S7bT4wg)2N1>d{kS=^OT@r+_x!W^Quy>!}9jGXT_ zZ!@kmo{X29wKOrgqCZ+bMi__VbHIeg7(s0>2N!M5+Y1fgoR-pD+NHJimEi*4{5f<8 z9%T+`tgm?4GIMKYLZJ~i)8rhVIeYk^(WmW#=gWqxf|yBFL9gNKQj%z)#*ga`VAn?9 zuer*T5Ma`4e@@!;X3=8Kqh|NSO7vtOesLU060&u?n?>O@9*6-3Wx z^Z={{wIV^Rf&?khel5==vvr@uQx_@1RB!KBCCEoD^lX5CJrSH)tO5s3>1B77%t5}d zbq7%$uamKdQ_QdUf8b6y%#y@W{UF?tusk7y-83V`gXUhA=%J9>XU(m#S!g3N42!&k*k6BVorF?sHVdS$vMbghEX59m~4lfv+bFNQ)dRcJ#y`bRx zd}pwIrBh08K7UEA+xTa)RS;BFp1zMWC)iPJ-6zL&D@tDg+gG~+?834ZkK{Nr&Q@Zo zXN6OS8ch*Xwx`QKF5`N;a$*xD_As+)ZqadZsYtue^K4x?$Q7I2J$H1yDFI0RB5Spz zylQf_Hla-_=Per?xxWf7^RL*OBV#AZd@?l4^H)7;=lo9V4rMaW97IWD)mPOiV25iN zvQzuiUUwD3JB(`M4u#V5f2C%o`UweaQ3o?auBn3)m+eCMX0L7y4B42ty!3B?fllr} zE}{_?55v~}EZ7%`D}u{#Xx3u-CvBG*WPDH-V;*#xd0x2H64;cz>Tqrdc6VtLoBF_% zO;WXTYqI2mFdX^y43Jsd;&mI`OtCv(s8RBBTi;id(z^J5#eh05C*3ag{IwuUr+Y@y)t@8@*QWlwddU_QOQrb;e+= zZ`RBqY^a4l9fvEbvH^ zCnRjP%C=@2jib`I?AB5aW?4?-Cl0d^g=LL8-?pWZuGQxdM2l}jF{3%yBbP>}Z#Mug z3Gp%8>M+xPH!iZ=vGRmC?cipxlYI2mN2e#z0-{g$WkB#$o|i^omSiS zu@8`)_GN^&l4E@m4pI!vxLs$9?Q1Tsy)Lc}iCrb@Zckm^OY2^?7LvX7t%JBu=3uN1 z36W7uo41vRyEW1eZG*An*?O^n&Vc5l{o{9+b>d;ye^i-8bav1QKnCgsAg>1+W(U_7 z#+|`t6@c=#J?`5CrH7YPI(+ikmzd96ZX8ALqM{NWN5rSYC@EZcvh}n|??RiFvZdU` zKs9R-zjjCae+q|D>^o;f`bmM-9FQKs@gie>ujK(q3TD7CnNkVlfL3y24uWK%q+^)+ zb@T|YHt%U%#R=N>+5l5hw5h)oWv?w7dfX#x3p8T3o~urIV3Isl;oR2D%`&f6@8p*vr`}Z_;y}<3^mSo&t9=2{eSWCKKmIgUUb` zJYF|ZulXr!;P!xaXpm}L+Civiu}Cs0K~_SAIWtc0oQDC@1NRF68+=34OG81P6c5n4 zHI9CVFwV=h9k8G?k+Nq=Uk{vaPRS$pdae(F*e`vPQaB|!&O2W%@$nK(KlOdi*d;GO z27x^x?X3(1%xhm9Vk-&p)!jHJr%NNAL6)CiQ24q>fJGIW1zELTGOE7DnwvxQRAD;H z!7f!xLrt_sy@W2AZ&dqS`u`RE5K)MWModRXf)%HO^i{4MK%8>s zP#^@@Wgk|y1M-M-TyR18UnD-DU$OoViGOkB=7e_ky3*eh(SIQ)uBRl47s?vE>CYij z|EdEV&5@UHO$gaB5k2?Kzk6Q$b=F+-kFxv$(dW?ae~#a$Di`uw0iNJd!0JCg=l5aC zP}P^02kf6{pDT3!4f`%t5pjbq{fZbYI%j71%QbYjfyiB0uE{Ss&sBe3d-EbRG~^Cp zr_uY|cmU1;&Vd2#{uZRa51d5(56t}UPr!jTK&;TgBAa&33iMZyo_df0A;=tzI`>@Y zpJnK*0BheIe_0;+=QhUyvX57LhQsG;gMVB52+;Vn(twXmB>y=|K$kT4ki8#po;c#K zD7^!Yo!z|VC*QqR|Iz(GoI^v}u^(2r|Iykzz(HH{ zcXB~|=csu8g8#YSerrX_Y0_6^FNZA?#A+o}T^UN7nOJU$1x z-`hP*EC~9q?*eGOIte%D*U{0@t#f+rIFuXay*cyTX}l~YZy-$0Hnz_-DD)$JG-qOBB9PX-QL0xGS9c#U^esma z9+vlNkbH9xC5C?R^5sjjz}l=gmv8=7aodcSJ8KXbcS;`E15- zA+irF=xeipR1=xkJ2L!1VwUQgOE(Gu;5h=YA;7BtYc`EruedwrmJlFs@m^)CFsj@A zZ|^7ckvRTGpBpB9;}21n7f|dC{%@1K4Rc8agw5$1PFElX*^_yXBRt2lnNI){{SMH8 z$YRdUjtU3^h1s$$0!Vz>>(9HnPgZ743h0$+p{hgdq{3xDy1Ge|{-oGUbkH8EUn|GX z&BU+Ho@6Uyg37zAP!%$w@o=Hs6dfXqTPbkF*u6D?&~j?+OBMq_(+n?Utg522BdIlL z>&9iHKVKu0z6}AI%VvnPOiAmlWobkvU6}0XTbDt;kIN4=1kOJ5?;m2 z=>S1sZ-@^5&}yswn3xan&b64x5Tx^DC29x)bdkArhMW-G>J^`<|8&NLKtNR{)~c%a zuUS6~V)zZ9&*+&nH=6;r(jb=)wAh_^3%Ab9oo#9VhgfE}*ZL?iOUQBF8X+tRn#BgwfZSTM*lpxv30BbC#_T=(pCEdLWvOVl^@UUTHmET~KNCdL>7s^>2Nh zLlm_pQ<7n+d25IrLJPNP*HHspLhlYpZFkEY&HY43d|3Z3dE{7<#W6gbaW-RGtZ}RL zl(2=^J^^xb0p2RyT7XIHz!}fMp<;h%d+}_BkbRs%?is))&ZYCjFG zcAXU+wrE1EXhunGeY*QalLo}3nFx6v85>)NFb_U>4t&o=rNMI_6Dww)lWdf5ZG1<2Mm3dh?x2)}o-SteILem*wCv0_2WehqumY9-B_Il`)r}4kz}O zWCxTr@Alr$Z=ZY`MV?**wRomnwyZCaxIE!JGK_)lc9;*dRtM&rfPI1*m?S(E_mx5s z4x!VOm~~$&{q@PW+nU#@GM5|jA;O1B-4C@3Wra(Pta)Erp0bxEhQmt@xtH*d?1@G{B7;2D}K*%k`%- zze|38LE1CWZ7ri8-UZE>(|EgDx{b&^U7c-!8_*aj4?BnYbtl|Gj*}Wq)^f}kl&o;s zYd-%$NA(mKH)DoW>-b3N2D5JaoM4QPjNOPO{GeaSKJd9`g}6EG00$s4EA>4|0oE@+ zSF>7&#L0jVBgd(adB??Nz@dHfIK)(ywK80EY!)O-d>S&et-M^c8o68c8o&Of0ey-) z*qbRV_2c?ibcsl?j{z7(IKI8|Z((^tMO!Mbabe)mR<;GAl~kAr{Hyb3bX@n<+7*X= zfOBGdJm7);ndA3^a@hMeAAqgLytTtSsl*ZFvG3f?1fzfM`okzBzRmJqo|Y2NVIp19+(!MKy$%lJ-VQ z6n3361gw?bZbtx%B6hQK;iPB7Yo~5wx~9Z{5$7-LHuquyd=@;Y_Ye?YS8M03K)7%t zI(UB_&!4~BilqIHiAYvQ2HX4_{c(Nx4dvE32Yvnr_sb)%`4Oj<@(<7&1 zSQN1`<*XS?{DK>ayq}e?5X@8oUd{r|-~uuc4j#J7NN1!sujCA9;)7u}kK`pnIUHHt z*b__&gCb;V@(a3sywCdhTQ@|K%+~R+DbX5VZ#;0C({;eJBrfjrL=ZZl=Cf(gX!ob1 zH%+&D+h?TX=oar>)gIW=*qe1MG8#D6KpENvfP+5IZ)>+-doKdGBt$eCZh zvb-@x8W^^(3&3P(b{;QbJmT2fh3|ZT27kaAODBFw~VTC z`@%;lkq`lqPzjYrx*J3RB?T1$=|&pqMo^Sgx@J!4dyTk*9kuj!zTaUtujD zR5DuBCHCX!_`+8O{Z*eXm&=yg&Q_pBS1k(J*_DMk%*H?Md;^ZxRqCd3q^vf2f51P_D2VlpodOi#hdQV%PCXFaRk`S1})MJhw#bXV0a9Pn2hKd?YlG zZh`f%6R*44)6HH*WczCw4b{nCS9@@zu+h zEgxmX5B*8mSOF&lx!cZy2kWp7^kDlTYjt&DezxjuWM8kUH+Zo8WFLbH$N$omMuo zB-6OGLZ=oj=Qwttnep;Bat=LTljH~5AtF1`<999X`|`{8=8VS+j6+-T}o!!$En>h9BZ*=aWcYwu{Y>+|SfA?v||7x%Zg5=C5#3u0%> z6(0O#MJOMa?2;B59(zO!JLhXy*PhW`DjSnKgoQF3DY3aEwIoFUwM(4-{Z1wJ%uoDX z*OF>cZYMF_I*hJ%ZFX&|b3ce~{{hpEuGL^2AFT^?8p@zS-GvB*)|ukR!aGD8;;0AUTB z^xobLqnjGJ*xe_@R=E{Azo#Sh(e-?r!8N|lPy0*m=s!9aw-?Vkex*M@BAN8MJASdV zXsII-V5-Z<1(Rj)LM{Vbk3T%jj2vX0$3B}AbjC^2O3)rksoHOJrwiHjRaRdhBAT`; zpjggP%v>-7P|0cZh0L{HGHO)PigQt(Xa3ct-70I=`GG;dYEejK4URSRmfrZ&{UM`B z#6I%^_TN$DMP={JP9a4~_pPtYIZzX1q}FPj9$eSSnw(Rs=h(`eyO%24!CRAChHsTc7&^mbWE?Z z3JRDT8Lm$?=G!eQcUe}(^gxcM>)@4occHAp57LCQ64ISQl25D5v<|cUFQ2(!J=p4X zUs+jLe)0ZAmoQoM5kYlLdWHc3x)E_m8`O3v>hiXfX9J^Y#`d`tT?xaj=xTGI+HiLW}r|j6<#4C+#d% zj-2?dD(VpT;V;YfcxeY|@z^u*RGvpR!RQC{8zU}3SNJRoGZfKEwbFwzLwRx&`g<>{ ze+Kb-=d~Tp&FZDR6>fQjGnF?dI(rcVkF90{ysG9hSK$a;PQx4=MRiZ`k+AYuAi@)1 zv8q^qQ9?gV!D;X^!Qi{;BlM$#K7Y)*?WImes3&+z@^n^&uS&EJ=?uR_AbJ>nhx_6y zSg&u1Ick_fY0+Qk8Y@#gMJlM`d#{c;Ao|(wW;xp~rpX1yU5O5ljZ&6FD1_$v2Nu(l zR*JGyMn_ZS<9KV0^l_~ZVlgj zwHOB8g4x=D=9u$5#qLMVfj5VOY(7xw*S!B};Xe8!4V&kWJ|k975QKjV?cSGFJH%U*+OMonHyg*;&IfXJ<|^J@&_G$)*y>dr?rN=+kJ>J9P(*xm z?gx@Q)===0{H4u0T(&zoT9v{bFkts$7K&}HGCNZ>z$Xb?-zcI7uH24E9R?&7uux>E zV)t%FeB;pA!OdaV?>q$)giIOf-?P*TW)4@Yck|bOa^q&Id}_{^F(a?uk4osP9LoVy=~ z(-o-{iMhV!4pu=p?y|H07;CV!qvDL#{PyjNGEe(5mE*Wi1e_^FvhNEEjiB;bRrI>;3&(s>)R8@2b z+XUv$;F>D48%LaQim`wELx9eRUp21J{9={=$Ei?3$ba~=VA*8nXJ;DqmV9@|eR*l} z!}f@k2i>g_Vh~|7FPUg25NySoW!s#`-5=#M>_Wnm#|m1G>AK4W!O@*r8g^eLwaV?@ z9{^CVC_YnWd%f$EyWPCrp#9UQb)q6~+Y$a;0$BwRNQ^xtDR3K1=d!aM688`G7<{F) z`4p)fs3=QU%q@{#c3nQFHwzL8!l$>cO;CG)aw0>mz^G>1=Tr0}ZK~RxfP;VBoHHAg zE3S43Q2%lk1o<@-fwgkwWI zL43vgZRyI<;1%pxt(hU?r0w^^HdITNT(4&#M6K_QI!__k0y4N|U2Tv3)WONiPMtr5 zFPl4Pr&DHmKlGH`JTZP~yE&qk@Mt%tR$%9afBBeUl;)gPNA6D{?3GYj+1uJf5qo=1 z9E!JJb_(bu>b2af)NAyA7gqBlh;Td(^AeMB!L`gT*0@{fWA1H?=`BE!{Cs%wiav2o z9p7HE_mS!4>++fb>hYVu zG1)&$|Mx3`XTWz8bn|2U@9X^E8Vc5epI6hh|M>Y|SP*Nb2(D8>GVk`kKjM28HX{c2 z`TwF9_}gJ!%>mMUzKDPR*8gIm#jj!jpLs3W2J832{W>&$Pm?nXTqmbdfa-sLh@IM%xf7b1< zR}&4e2gEp5;(w#2fByuEcQRz0&NfQ^r$qVpM->8L+Y?Q9s{I0z|FL=mSTeAD_cZ?h zq5R9#{GT=}x09lKZB>vK=xvP)<(Ur?-8SwHn*+?A*%`LN-(@uU!;^bUTw{0LF~1~W z`@V7jqE#7SY!!YiDago#F7~F&Mld~`>ASBel93Y;fb?`YWyqW)0=vf(gP?`T+mF)a zPSxHNu50!qZTE*RyDJIw^LTSKfaC#exwKP4jq9!#`T@lG5dd-?i49ot`DyY)#Vr zWaW{5@ZJndB^PmdpyfPEEP!lB_dW{h;*en(l7?`Ma@y{H)DH4bi)xRPV@E(7E>%m^ z6#tZMw|eymP*j$c|Swi^aeY_PTPbpwzbG>$o@%gf7KVbaxf zI0-5MT}ASD56}Tgo)s~5UMZb_4@~_{3dK{K^`1Z)@Vq24)|hyIfsD^G9?I@W(>(dV zM04EwUThJ!ntlIp?&uoaMV{NBn4s@(FY?;+j2(u{lP4e3eGYs-a($TXk!6bjfsxBj zf5v@j2+8Nvx-y@a*)O$~&L`eDC|~i#CNk(vli%rsf5MCtouv|;re0|*wE>;M7F%2Z z5#c+@Z20}3EtK0mUnQy$2bl}oLWVG3J0U@EZ{wCd zV&ryl#TmUh%xM}gM*#s;K%y5SSGS&XCTJILVQF>Z zAbbHy3Tk#_QeC>V*l*-eed6+Pr=TZQmQ*p8>NsI%(vPax+wIc

    BXu3pr31PA4Br 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

    6&2dF(Pc=c*5VNtR`cQxh0w&$(R{ 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

    i@f^lB6oOFoog~;iP7*Je-M9#kO^J{_ust1 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 literal 0 HcmV?d00001 diff --git a/docs/assets/cli_gui02_sequence.png b/docs/assets/cli_gui02_sequence.png new file mode 100644 index 0000000000000000000000000000000000000000..de97be515c117f10b1d2d2bcbe0bde8d46c0c184 GIT binary patch literal 175232 zcmeEt^;ebc*6yN_29ZudX#oN0Zj|nB>F(}ON)QC4LFw*J>6Y%2?ru0YdvD(Tedm|+ z51cgyW5HrQao@A9dClwjBqt+|ibQ|}fk03tB}5b;5V%wDx3TKJuED<^+- zS_NgLgm^0pqq?kkYj?{qH zTz~O9gAWlLC8b3XR*^7JpAi_0YySZs;X8_|ISSiYSs7b9LWJ#&^&O3k$Xv`EP07S1 zrR7xp&~PCTGKi#zppxs%?!1dD_V_iz;X(g5OLyokSZKs_aWd*a0x67W+KjXuHB+RQ zI*mH?q10NXhHyVAzJ-zXM$t;3l4A&M2F740NFF3we!sda?TL8>rSQ%#?qI`B<@`+i@p#jy? z+M@^ZoQyQOdwo}IIsPV?lI{DO8TSbwCfB!3W#azrOkv9}rbHFmb|$+c3!EE;K&A^R zmc|Lo4lue2QVGW6HQZ5sjCWi8k`R*^I1nJF2Xkm%bHW(w)1*z#3zhq6Ziz54C( zctX_WKUl7tk?qqxBpHn~LzO?r)6?X_J~-ia7iJ0A?`RZW%U~z9{Rw0X6+~f;JeQkI zoXH;jmhn>}Y;ZHY2Wn`^dqbvjyQ1ZrwA&iSbPlrJr(M{m9V?S$L)%1F$H!3hjPlL5 zljEv~eDOObfF1QgfxD70oxu8YGHT7Opi30JO1p8H8mOZb%T%=o3roO z(cNt1m@GqMFgm06XORWc&IH}P>7OVKkow#`%yNVG`j1%s4mxpLosla9>Djd&s>f(D~1SIUV zdP-K^cKd$KXEDGCXnuRnDnW%@2JMTeE+^!R6(i%@q!gSE%d`mNND`xMm6jRd~wp>10b?b(r0We9SqNM~|2>jJ4QZ)7b=>8*9nL#T z<#T}FFf_b+Pnmc=vHP%8IQ^h6{^Zv3jd@o;YwU2zTv_v&-jZgSazYS|w+xM1V^y0y z%jPsO>}7>5|7xkAe}KBMkFm^Cgot>s1&)nY7amx)a@M%kX=KghPBoRCnR{?2N%KyU zCDS7!g~|y*rzy;m@2=S_?@kkOSz)5U6Q8>ClFcm7?rw_Eh(_PQ zeAPm45<^nL0RDIPD%o6BR*Qbmvt_==<9Z(QEqHl2H~JuySq4%{y8OL~H^!f@=MSCd zu3jFJCLCs=gtcklB;>5Ey|tSYGc%*f8uCP?Q!Os6uv*(@OJH`uaRll0^HZAGf5wKs zS&b5?rD2`|!4*(1spFZfHxp^p(m<*}X|QrC(-fD`NOn^VePQ1j=6{6|qk=G9xN5ay z0a6f`RjwQKoYWqjW--VFjSltjTSk&{vC@J0fs}M#;=lXFBv}l9%x+I(5YxJR9lO5K zk~=os!Y^6M(2fqN<)q$PX3#(a|#{&^POsV;< z|0)_8eT0S_A0IQB?DGWjvSJVM@jsh&*@cBPupbB$q3{PD#NYbnG7do?p3O!yD#cfr zkS|}q224KOk)9G`1TSZHEyX_+vXkzFiR!M_cf3Dr`11bxebVjBIh2Y&l(r zn%NoN5kTI^ygEMl#d(havUI?Eb9i2Vu*`4M9M%NW#&na+`DoB=D!wSNlofEIey6{G z*H?6n_lVg((%&zXA7phY=8xMtI|oNqv!@U|z(W5((A&_>I@m1Mu0Ps6`8w<(fXA2c zplf8Z24R9L-x$#KeLVAiu1U$!g*4v97)#t>dP6dt(`HnCTyiq8d+=$?gw-sA|1~gs z>x32eFw0MbF>|(qzfX0_Q8vd;KzDQQMUZF*HTqn9_V3b57FcYY7s8u!XXh{6&%^}N zC&sYS3q+AxL7pi}lS6i$=No@ddVvv(Z8m_80DI)lql=;MEg`4yQTcY<2EcdsBXbcQ=N^ zEl^W+R*7znl=#sxF`vgRCYNj-`noQ{;xpV%))O9DwKg5y zJ?{ppMi;Dsa=kEYSqU09^f*1QJNM%?u-r&P1gxz|j*&n3ZIXT~7BSZ#;7i~}$P7mB+R&wM zi-T39{p#-zU7*p3(@ui_`HHl^W5El-c1dsi>zjDfN)8;axs|WZ4D$`VR8$BDK1`$m z?eNQCLwtgEcFaxp>)fN9=3z&ZMf0X3JP>4D&(NGq`2^NIW1hRQudY@TAHaDhZcA1} zjOo6lrbZ3%-6INzptR1R^RfPt1 z;sY$iahW*slM)l|GNux?Q)^SlmDo}(!>daXmKmDOUs_Ta^bX!hKV6fXi{ zQW68S?RbO}Qa4w`*L*ju2P^;h#88eMnwMdejs(94*176xnxiumtm2?2WL>W*os4wL zZ;$M3(`okFXpy|T|Ff^~0)CEG@`vZkDtJH9F^4x^yi}ZrD?9iyJAn&ONpx+O?dt>j zB&}p=sM}-KQ)6@zq!XlJ{AA4a>1KU*bu5T{AR-(rwcL;RFdoT(*i#Pd4a&B9CwArcFCp-W zGv0?7GnE!0!0(_SHTAXn+o!7i8?!lfh9A6-;1KE-7olPgJP~X|ie@=K#btq769XIp!+qYn1pFJUYeRAjh8C1yTd_C0| z?Th=F(idg)nK?O#ch{$aB1V)csh9LVztHnR5AWOboD7&M{&FvoS6}oZW zqpc|AjU?ue%%Qz~O%8MZV5H>#4%q|a-a$jC30J#!2AEV`8(gFTAb`6!!IuV1Zp zIq`qrw)q45zWD*(JPs*_a`}X@xh;~IJ32lU9kMyrbIO~{;Y5IrN@7`*sa;=%b4N<- zMU)-@uSwKp$|RJobSrG5YX8FL$%ZfMT&OL|zhY0JwVjN9vEv}xVjdAlqQUt5%@3ok zPH1~1U!&F2!OL2k+|iR1KX*1d+8aYC&+KRQ?gZxH9_qAOxB*45& z0y40JSt&4jy~T@Nys-KycZ6>e&>{X1d2F&iXju7A83jh06}57+H)$N)Fl8f%&)&kS zLwV(rWC%gEn7Y>s(FC^$I zJB)lGQiW|TA;AlWL8?VVU9*HmZ{CE4HT%CwK47#kl z55pg7>a(li6bDl?l3=N0BjgkkP`hKy8jS)`1m13wAtH6#tM8|q|2{Ef)Di&&4Xi*D zWFaV~wfe=9BNq{I(h#5L{2=ov=P!@cFdHyxwfMmx9JT#68E{JCu*34Xt;N@_cWfSHao*yMG1#=c0-LWivDb%cNFPMnmSOx}Z~Qlt@p zRXEX*npjULlfoG>@`@sD#I%1$R9+;Nud&o%-2B#J;)7P5BmQ#8%fk=To1PZiv-RwJ zH@EK}krjx)wm-&n!vGuZ(p79Yi^z*S4123pdA_>8yKa_~Qw!yESnNu)Y4A&vAoDcU?5r2-@_m7sxV>Nf|pj-`VLP{*beTx+tF7~zq7b*fB%$)EQdX?WtHM~ zO=DzYvU+4EyIkn9uJ`v)e^R_TUfNPiQ|OAa9pdTbb+~dew0wuYzB*F6YJ0RI`XTw& zz)dhT!!@z)1FGBp_w48|38DHF;)dH5@gw;rL$m=`(6Dm_TX(`Y=~nescyBE^UQ2u6 zqdV?=~abNpM6bIr|39}F~6k(H~Bl)J7w zd#U$Mp!F!g?q^J;W?!+T1kH#I^FJ6w7Q=v1*O+UY0LgN?LTrsV2?i>N06NxJ4?(ab zgls1L7Veq6Tr1fNs2H3t5ntq?yWJ%eXhiVjwJ=n(&$E90(*M2Lg<+>h8XkigL#ZAf znR>Ad?dtAsFcT|DO-=1vUhYu2^NsS^uV24Ps;jHj82jEzNxiq>+SV?f6#k{4Tw7RD zf^kl8<30q(D2es?bJ+6kU!`n!BqU_ej+>&LP6k)~geBnS4&%y*6is~H%aE9;H~ym% zluz!7hlW9|PLO4@CzZD!e6W&b5GpmaSFP9`$lr`?TN(8dKV@BWL@8&%I|s*uXYyM} zJ!(inbsGb>_cilTbPs}Lo0|#0{#73Q{@{PgO078Xr z>R6;Hy1j>y=2T7XayHcU$@q^0(5{(JoK}W=7v^=P+$iR?bxLFUcaNi_tW+eWIQ{h0c@jXwJYLebsWX>GgqmCsN&wha zST2WLm~~ABS43c%HAn2T+?yWjRd zIL3BgZ#J{tFrm8T7DlY5Ui2?X`0ISssoQo>o&nI)d zoJ}{vRT`nmK#Y@u9T{BhS0(sDov)Lu86W>+|Y@@B_$IT%w$c?QF>xml3+?^ zN9PU%eVu_yZrE?HNYlq77#`U6I&4Sh7r=lG%|KNDMlOkkjnr}GphVrjGe*l69y8J) z{muA80#hsP-td;AgruY&=tCMCd1JnQjgC%1vyyxU`j7Mx)1`w0bXi&1?lKfXjBUq; zZVLI$d%eAzlgT{~OcJj$v76IvfSOSByM(_kpaE61>D}OVP1XQ!VKa&=6LO?I`EH8NIowQd`>kX!`4GkNza)LYXew z@hx+re#SQw23>>N9CpW)F<%uS5a}P6%lBzyJglyAg*Guyn`tzQg@uLb#RD%XDS3Et zAwK%?esiL-kJ#+Jk`gN<&1O-ja%PblUp{ZNRi>~?%7ZWJiWnLFAsovq*G)T;ob;xc zsOz4ti#f}Sx@njD#T(k4&Iu9uz*aqh(JX1uwGsaO`7;f4HGY16a=A*>^zL z)kmDTbV{qR+1zps9`0>D&jzYFT=sc1U}4EPYFIR?o6zBtp%BoC+nVlfmN%CIeSJTw z?BN!wg!RR*E^ThAZ6MHXZQ)BuO9v$;YF1i*U}MmIKF!GqVhzBkytyKFg6P-;lwTT7 zeGOJLxn^?t%5t=E>|4!Pl-WPqMn*7^;B;R+C6t|;tqtAMk9%B3A58>*DZFIn#P_(^ z6+S(}b2ObKiHt-;ok_95W$ftai^qItD^R%eg|bCP`$DiOwd*$W0=>XLy$$h_FuAYB zf3u$>+~&D?B_D_PZ4GDIVuPClEF4_=C*9{2XqKo{R8$!TI@g z&Ibz-S<;DZt*wrQWAAX{lainxUVF1G9LV*nTh5Zon|XCOBl<%GbGUL+6WmdY1_w>I zSJfFOPluX|x*D#K^;rpqa}5#l$IRj|uU`aQNhe+#E+o0^PetUYO1K1yi@aoHBn?3$ z&7F+Cel%%}F&zZb=n<@USrRf*Ch7nY(7JtLdC#wi(cJJsdn-arqPphOoGGwmf%e7o zTM!5Hxr$3$Yx2>@ocf9=rGGXfACU>`Xf>8;Nj@p7uCyR(x3U!Qinyu$$ z0Xrpha!e70(qU2jfSf`?a(R`VdbIf9tz_)QPp|oTf4)93-of6UX=hwLG?EVuV(aA8 z8Xty3&QW6#cQaYxDJl63hv6D&$6G6;=PQ-|b@&!L?_|Q)>=KtnL2L`@VsB1P&X*Ok zHn2{0_rg4QBz||xlQv{ce0&LqW6$N3U2`g&Hr^!hc}?7Y3ld-L#5?M4KRi03y?HO* z#<;`2BqfyLmWojC)R6|2{*_Vg@A&W9_} zzHkq63<(P>=gcXd+$r=PdQ#WN#Hd6@rdskG+Dd)@kfWVq%HD^N7IVE z^O8Yv@Sd%~^p?IoyickgXuXwG)am?5!w4vrMxMuymM4FsTTI-W4k|ixPZs?$Y*T!G z*#3ufX>p8HEYoPg`EW^){??Ry!F3fiFeC(Y`xZwRi$1G&7wgav;7*bJ_ftkkZu_;L zAif_5=ohgPcyAc~uB{oKJXnccrrV;B@Zd2@CkEf%=22DUNM7aS7~(No!oEq03TkYG zf3fJsf#U%EUbR3>C&jFJH%ZHKwiG#nm%#9PtjE>t7}?g*_V^;s3)A^Xbj`Pv$bpCIc`cjMPhUenaehLmhg$_it=`J0>`O*|QvTva+dKHO z9UDq5>9$aP;iKz(mf?C;#7Gg*)zj&d+j#i@fH8GZA)%M&yYo@{!lYG*>o5WW0*W6} zx!^iPJElqMSPFF_8 z()xPv(P|eA1aL8WtORz~r)IFQuuCf)VG{4(TO96O%Jol6JzwfALs9>BblM%lbh6cT zcw95$YIaLqfo6(iWkw?+m=k+PS)B$6F&Lb9Pe@A4Hn;MiQu}(l>|`FNpK%gH@NPh} zDy>E>Y=Kvp$yCoY&KQtCyatAh8hZ(1l6lV4VA3I!&j7e}MQcsx>oJ;P^Y?jPUu8ag z5Sew}JKR|3AE~n9?t4dI%)_=!^TG9+hNGT?oSGUj{qAOUTY1zCa~cQ+{;dTFbuhuD zsxX~yyFEHOYXxmDv->Ia<@NQCom%@fB(xyXwo`mY3bHhO(3Kxv6BY}D>w<65Rpw@` z$tH6J(I{r?UG=WnI@wj;Vs88;=jKNEWz4=0+@QNG{D#}fH!SS=+?>`((=Lr&t_%WX z%4G>-cg#Exbr+qR`-Mhz9l8|^45r7j|JCd0ugS=e%_BXg-Cd=_lQX}lsOk>gx7jku zf??|e!=+xDIpegadoQ4=sKkj-qL!N9{ihY^rp<^FZ*PqD_@(E~Da(i+cRmx;tp0Hj26DPIP3v<1v z!P+sRxN$T(q0NC$7!uzZB`}KvHT-Qm&+|}TS$l%%{!^*i!wErt#2~`<7yAus)b)5e z^zBL2>*UO?4`$lI=pHq_dtpuYBnas0?1&buDi4+4K{Cf2D>eMZ%FR!IRn?p5DAyGU zz!$4n6oK1pUA+^sShaPlvh~hnG5Yn{j-k%P)+vWKJwf`Na6Wyr^Ip{BsN`qQSGxHz zsI>R-J7&JSdwV14!U$T@5;@0=em`Ovedy9?VQwxuTJF6D`tGG~(@M+R_jzxiAn=)( zAunG7Bm%i8ljOHQIbPb?v67eV`v%3~anA!LY0K;Dp9e2oen;mRUT*mij#R6nQ)Xw8 z)6n2sL2E)mKqn1q?ns6ka+o{7N-y8=`8~`$m63c2sSkpyV*8K}xh^p=F$`4?HxZM= zHS&f=CdQg~Q)@J7iZKi@ONAPhBGuMe6+M{IAxE>piwspaY)9>A!>4zT2MTUxV5@Mu(gT80YfnS+i=O=zKHAFQe35)Is1%MiUhk<#0Qu>{mcO-Wf31j5C5}RB?oJ(>_|-&5H99lEF!{f*k#=Ko44q2m z$BICs`#Ebic2SO+CTxV5;hCX_yW7&hq;5N4*Tkjlk%IYnU^8tb#E;hrAD|((O|mJ9{_wc6Oy4>_Jj+;t1l1{(ATOM(Sm~ zh;HKI;(_PLq5=Yt!6eQA#cUbf1{5xe} zl64R~et7}IxdjEW&%cJq(ZtE+$I9g^)x;zPrzCT?49cM9e(tfeQC8Kyz1?xQoU6c( ziX`y9_&Z$u>&39;2;^L~EI&0PoZj7TXmJa$fu1{2efFF_3Wp8 zbm#UPhXw1B9yh48uajPEv^K?6!tZfy@~Z@kFdWZM4+Ep3&_93voHlNbiGcy}K=1%L z(#Y6Y5Cc}?h1lujUcH^b3C_UL->Fa}`>AnXmx~WPw6|=wG9r9rD zC(;ju|sUeR$AnhbVixJbAF{6H}>0`c8QNrq5~kFqTkS&@CC z*9_WBc)qz<%+5-{{dfMbQ$l7s)Wd~cG4kG!nQBSTCdwq0z=Re9wx*85aOgfjLQ*;) zsyg1Rd5$N{{TTT;y-FdE`iU#BZq+WS{Y0u#q!SRIFJV}2c_sDNXoJjhIL+OvD~yo` z!OHI0ONywK9y{5Dfb)(H4H1%0LLk?>9tCZfYuRWch{+%m1mCGfs@8j&90=r}_eC7> z2PyagYJNqe|Jjt;?r^#n0vI0l#WPZ{v*Xf&agb3m=5+CxDB?^)5CaQu&C> z=ndu6@IWpJ0Mr5zF|oXs778vdE?`ApzJ3kAzrU~Y-bqIr5M6PvI|?OYD7$0ASHPsM zu0CTdO&k<_j|lBJr|}SRaa8uG;p%T1QNu3w_AL#Z2We)CK7c}K^8i^;U;jwk!7z_D z!Db+A+fPri=p10hy(;7QVy%eHRjMy3DeBpVwAkZAg5#Vy6f!A%5#i=UETC_ql1(jo zD=KUC`*%`HfvRr%?~T_hwHx_OTh>!0P{2_V-WuD-&BDwkSO?ym@39oWm--;IGEV8Z zJ5>ryYP?az%K_S)noBx}@9r)U$Rn6a)#WUWm~ttDU6WmeTln})N(qi@yH{$|aZ(%{ z90?C^X-R{02TSQ0WX-qP05KCG7I0FEOWzBo04?pKFi%l9@z!e_8{4AtQm@tLXq2(} zkp;Q*8vWRCHBk@12i%3&K4KJy(!pMd%Wz9$G&d2^uHC*C{a|uf5D3sY!rJzQbY~hL zb1dQf-eF&ByivZ2nKcu*s8S7|gp-Z;lj3tGA3weXUW(3h3Gw&$C!-b7YH-EuiX;x8 z{tQO$mC)DUzkd(?{JAaF=l&VUieMH44fv1C%hf1mFn`WbHpe@Kmk@hK62S}jfG>%O z$n{(wdvbEKnikmi_~R6y5IneZl%*Vz8qhQ0Pdjy&vm{f3%qNUd&zNbflNI*o3I*@3 z(bi#F4#i3QfNP+Ud~PY4_ps5hp4@L-@3HIR)u|?}7Am-(JR4qW9X4gXbUuoi{dV{9 z2+Nd}01!a7$v11gadZi9-QNCSw}~v$uG2e6XZF3x604&i!nZEGJFGnQ~uVi{j7Um4FwslmX(DF9Hj7)^}Y4J-BSH& z@^MAa752}cKNw@V0kt|0uba9D2HGQ=@5RJE;>aZNL}>dQ1S#Z-4BusiptWjqQ*cuu z)ZZ_Jg%{A@mU+XVh*4ahqAWMFJ^@p6(7G{Q%vX=_S1=mLB(>QPdUq{)oM)VQF&qtQ zJ_v54$xM}-_{4457SJ$r!b#u#(3}3Oe0kefe0UYFs}FuR-JXP%?4ha5Q%ukC_;$p2 z{XwvKUE4;pC{vb>XRIP@{5q3|T9=v18wf_KY!)L?3E%0>R#{C|m_0`$;?R?^b6BOU zb6N%zpo@!3h1D#M@0NZWWAm*)TmGa#3?*KK*x&UvA*F=Zy#u|2LZI6Lkh#QSvIxF0 zn7hJU$1Ir)5%Ac#->32BP6QiRt+6{%wgb+5_JsfDmX}OUDRa8nDm%>=#ySyvI3lORMeZ*M1sC zKJ)GsDdS#|7-ADE66*)6A0QQ0n%gYiSR1V%ve4qhgW_0mM zqx-|cV!J(tUvP8fvwpIFv;=c_#vFHYcGTK(e^=r$#7D1Lnzai*1si~H&OXWVaSZYH z^2D8pnP zYfE}jBbj0br#aDaRdBF8kIl4*7zrGt;qq1_pR^ftgt33Ie^O=xDU04_dsb_f&%tz5 zObnL8_aB`FvAtX&Jj;2`EvfB`?Z*z>M|OW1CFr{IlseT{eSZaf)M(W07rZNzzs{fr z3TQCnqoAWh&Y=D=baV{^+4&oEaeDgu?qU%gkZ_NVLPA1d(E{=aU(e<;&-}0ADeM!mG%j|m-QpOqz_Ke0G^q2h2X*vN{FGPv> zJ|D!t2~f!|w66!LCB@C02ObrrviIm1IgDJg(>#8$nV)Ef-BJYjt3Q`hU~{vXh^K-A zmP`s4D#*tsso#A;0kvgk!i$hwIMh%A>?_g<@^FLbi4=d$V-KT1^SRPPDt~k~W_Kap9&Eyv+ zP;D2Y$t$(|B7rC9T7ONR%Y^M1CBCpeZ#U|g z4^wmmSWf-?3*n9J+_2yo&M+zWJ^o4ROztrIN((cF^+FLFUEj`U2>G2HC$k+43@ zLAUg}RGaeD^fa3U6Dj1!HM`(ci=Q0zqbQI9Ano(>bD(R%*__PA)k)-pv6`)_TER5N zLm5ZkM1W-#Hf?Ug5GC7}g3ic{T3z||UM?qgbCeraG5dp%fWT<|I^(B2|Jg`1G+P&! z;^ScDug7b}b7R+C8FDQh9r$ks2id)x-eben)YZ%#&nV_Afg%x<8PG00o2f**yuNJ^ zr~3P8YG|HsG5N;U!KvIG+~ynY&%zQCF5x6>M3Iq`0f&Id-dwnYy1cp~XJki*f*K#v zN-I@VCAc+1B_Y7Aa6D?I>bgh3ji6{E7n>AI-JT5a?Y+v!`+KrcRe0-~4;L;c7dvOjtm|Icz`8fk${n)0JA}GyS?*xgF8<1s+H}LZJOgT_bYBJR zbQKz)GMl1tJh&&p8-5xBg1k|8!`zDfU%N!3>ItMi9UdCm4gWvJ$RZ*}FtLO_v)wvR zJjmnmeRc|3|H(g(Z-gze+)t_Bqi;5p?D+w)!ZhO70&^|k48lSxehevm%Ha2ek*dbqjiul)X0J+}c)DIz8H+3o7P zNeW;|WgAInO<7mUhxqbGn8m{J{D4qQrFO6XJIhp=ahO}JX0 zbH=x6`-nJfAs|%N*X{vv?T0|J6p{S6uj$>7o;Q}ZK9EQSlXGq|Q(*>XrOwV(M7X#n z_t-c%6IVpaqN;hk5}-n%Se{oAq#HJ{R*Y z?1eU9Z*h!aU|@FUQo$yD)vU5~>wDLDvtMuT?cLPj4NO=`{hWY6qj1%%az^TmFhVT< zUgmMyas}kM)s{9+d|-~idZ4SJLE8-MS51Ggw)RR~Ozg)+t>=|}M3nlGq9R)(pZOoE z^z?LlPfyQZT%ed*$XmDi!!_hSW+OJT^?zlG>yVq3Fvj6BZqxksURGB8Vt>BW`|cW^f8)A_k9MZsg|W`%Aab=U z>g2T)*kMOp{eS6>GPwJvr|}syDr&aQfjWAz+{@wW*cccm?a%eA_urN(Cw0m{8CKgY zl1Qx$JWTKH@9V5xgQiWn)_ZTJvQo+}CN?&()UZoMV=-Tq5tP^EsZv86zx(EgdqQL! z`j#Y4hyA)mNl<3~jE*u?b#-(s$EoV%V*f1ys;kX!U7&k}_=q_G9b^p9op<&nW-vYlB#xF1FRErAi?*Q651mT_-Ff{J8%7z01 zi$Sw00j4sBQXde4IOuc>9tXS);9@{L)|~H61x5q_e_n28MwPgoe02Cep08?nb-X4= zjU9K64$vD&73 z9ng!o@2qmWS+=mwcpfY?DqGW0Qd3K>$7Z&eD3C&@eMv!a zP`BtSlrAuo!kho$lS93u(0Fu0f@vv_)eNfx0CGU+as(nqBlc57gjLB&_4%3nNSa{4 z;coemM!{^O2fIzfW5#fK_HVV4W%t#|1}0dkP19B9%QXo{N7knMYl{ltzzFChuzFJO zv51MaY1kw|PXU0B-CFO9le+iuZ^$XZ!%@w5?|>M@f+RUHB_$FVm<)XAurb(ZR^N2B zP8g2I-1f5Db-4|${|6f~z<0&twZS1F6f`tF4YzH`j4geAeGLuf@kFC z8RCDiEamYyw+vfLj2LYCer%DUrKd;cezvV_JPnG18ht94({|AvFKEjEN%&)KZjNhm z7KEGBo!6=-G`M&NSkFKzFbeCGo94}R%}94N1;e-1)m68XN2yxf`+vK-5ru++0tODw z$7rNgv8}BwJeH3rV9WEhizy{tk9(--)=-*9ZVsBGK(bb@-BYpBXmGVF6k{ybX`C^* z0o+S#WdF{19=zH=7t=Z8_}^mS~D%wjhMn3siI17wagWXH&(GenSFc{N-;r4F_Jxpo*Zg6d z0|kIyv#S3VCp;_+8Kif^`^%df_HNF}t~0w#LaMe#LWMVv7%KYNzYD3$6jiFHe`^4O z>eY94o0QtQzdc_W#3B&j;Pe5>q*iPH3V5BlPAV{2aq~R^cu-jc*!Hz-iltoxxN~5h zXP}xv>k;gEoNmgWI5RRb-k#6eOaXbyuaH z8?@RtxNo#t04`rzUiQ;(57q~n;0tAL>rf+KQItJ=0MG-G{8rWVGA{Mr98PbLe=I8V zuQ+HP_iBV#MNZDQ`Tjb;*J|y#CIL#Ylv^oqB5O1qpp}_72;2vTJ6cb)Y3t~K1HA~& z4V(4c)JtDC!|O!ie(`_(4Ffg$0P35rO-wJAjwfe{V+baM(2D|KSZCk+f`EkCm0>ll) z6<&Sc7y!yao~gEpsnP}hx4>f#@HrN_Y7glbvV)4FpUC4Rt1op9r(jWDI zMlge^{5GYaR0MT*OIXfSJWG6MC1hz?Ao}L0++;|lejg-NHTsaCpzja6OH00BZHC|$ z68L@iKn`!DrK&wUJS!yyzraisC)TgNz#Gut$pAuf^*Y%uJ%X?GgqI zfjMH_>s7D6(cm#3tCO5gDA9(k49 z;XBI~fZ}K)CWv-gQ~!)?4;P&*uRM=N%tdw})xzlUcVcXf+;jDp3@de_G{N_Tt%1qp<9m?sQC(mG} z!-S59#=fwH&)`hztCzn*p|2mozE7!4G8eP?SdPJt)()aeH*t_xrLT7@(UK_KJDY5O z8sD_dPcq~#l*d4fRr_#%^M0)f6ft!pf#x)Sw<~t586eFOk3QQNN*w}wG%)r-G*`E| zX?z2C9gg}1@Ylz*v%Ue4?IHf~9E?)?%gVOA_)!O%pDjbH zS`-fEcxOA4Q~>rX-SR$y0!#~}%`o6eu~2|RuDIUa-qtW(>Gdzd=|`}2bgbw_*0ue6 zwD%EDEIsz+tD^z-#W`DnhvGT*7C;ZG)jRL86@HD46;V@DE0wi?jOD8iZJz)mNhm19 zDF4Xf`AQ{Q)jRqa7~ekoHWe2$f^jK~y?E3H`ew0ia|!^9BTZg6gbPC;gBHA=CFXLZ z2E~(|e|CC$1YpJ#pL@5*@>=c*AVBr$txd-nXh8a6X%tTpR6s!gM3G1H0&9qVo2Fj= zQ6&{D+!aKe>v9lofezRf&}{&Kb^-1_OGP897G>M zkl5Mc2X5!xnj+E{0G+?r(9rlk&#vXeckj>nV3n&{oHG8i2U7lVfL7@MkZ6xZDsQ*6 zi-gW;G33!`)i%2MWk5xyhu4W}Q@=#{CttPr^{ZFE!6`1@5lT%;svC#iK~r`ck~F%^rJ-I;WjZfbTEwZw~O;%&`Hp1RN(ZvltnkB%VRD z(=duR7yvIYegiA(ptZSOY7Mvoo@3|WK*7oy3)GN_eI%ZTg1vF|rOf02F|VIZ>Y4yB zjzYxo1>lXj56@zJj5z+TAAlkCq?L6xxcK^4xL3rvI!T(92W%D-*opSpfZPRhzPBcW zMES?hp1+<#$&6J9x z1r35PDD`S^Szex=pNWzmKpidlIPl};g*p&w>Lsco*!~dPj=1xw)KoO_fQAi3b4k=m1F7bf$s^1P>JtPZG#F>f;t8866DVs_jhH zTW9~0sT+s>>#cem)F6}=Paxkd64?Q`X~4X3MF$1}jdk+oz~{lDTt+~^S067p`o|CR z%gf7H(|VA@SWw!TzR{~2DBllqAE*&qxVqMiAIIx@Pivjuo(yR+XjYg;KOPekV^C99 zr@^3E?`##l#|+{vzpk~lb-k>mMIhf8wA&0-RyGq&EN|aR-P*)n{NQrREA_fL7g__V zA<_NC#l_~~cTyPuWKB9+0^KnE@#3j@F;KDom{e^wo}Qj&U-LKySW#^pxJ7C7_wR1n zUC4MK#NLAY%lh!$eqbjBoU+@&?WhB{aQlr~$`sk*EIQ=@y|^cEhK>m+ zAf-$P=$_q^1WxfB^i?oE>rO+f+uKs$9zM^>fDkO?D+bR=PfbnjaAh^yFZwFKarRwW z(gQlDu!2-}PRo>s+YSC#Gec#@eIS9!e%k&&%)NOumu>ex{6SIDoXjbjlp=-9Doq*; znKO$dQ-sXQU5X^7kg-9MnKET6k<4YzoTkDvEA>%Q;j`M%$^-v8d;`dO>{d6eP0 zuJb(iKK5}OdtZCCGwyqMcqlJ~O(Qf}Ow7QvsrGy_u|CP5fK}8|`%ZtNMl!hkzUGv% zY@K>9azp~${*Fk%z)T&(9R)~)ELuIiy~k0|;snDd+{8aMJgg7Ugf-Ntt&H&Qg-%GS z%kiJp(Te(ch%03wqJAeIZV&!x?_E-bUd0Z^Vu$i;y}6xf z+q;gHRl_thI(i2{-Zl}DG)2W}t(Ib8W#yIZ2ij4|Chr1hn6fNMJMPO~2(slG&Izu- z;%Rq6LINoW;i>)BGc{rYJe_G#|H-aJ@p1K@C_IAVfeRN|MGbm(k^r4Kz*MjqqPtJw zVVvYE_V##en3g`{dPh>B-vfO(6#_XZ% zR|;xP{_`#AH)v;-<#A$DD#_`Wj!Ff3O=RExVYfCT? z>wnzu|COJ5fSI^^_pZu{J7-1$wJ4Kn#=V`@x9{Fn?j)z>&qp)G-Q7K;n>oSZXy!ZB zSlZB(y|GH=hw4dYKf$+_jV&fOn0)04wt-TXqyMKKf(N4+Z6?>(>W!y?e_KtN&iv2g<8lGxt>JBrQss(Q_VrVgK)G5CgTHTetrA zZ!lLvW4wfwz_o$G4~G^sISfAUI&o5o<3G=9gUX>Zo850_N9Jx57WH!r_kXJRmhoyo z?c}7{4*$zn2Ajo-{J2-k0;60Tcw1ld$5T{qP@kZKP`KJ-uTB<79+%@0R%mOr`Uh-6XZqeY*C{Rv+G;vK z?}*^H`*iK+*KN7;qo;@mMV_;YTgvG`o7>zTidFUqJ)&W)Oa|F)%}P+a4y&oD-RY}% zBJ2axevoT_Uu|q#ipSz;cPX2QxyDnQ)vH%O5wR#)uON8{B@FQF{2q%46tj;IVQJT} zv7H2Ucl8U>f->WeQoYwobkjSR+`8vgu-yIe6x(V8?)M8*F}^RZf&}Fi7iTYC8QR7N zC&%=7wZchxd0IkEGqDLRL1m-t5a&*vYimq4VxVw{T7GP5I?wWwTxLKqO4d`_Q;@jR z2QuI@Et|VL4+3Zg;A#2FmEGXMFQQ+7R8T3M#qNH#s0lA?O@R^62S-71neOnqI@GGe z5_-FM&kK*-`R@6_n+k{9?{w64Tb;ha_B^sM$Bfti{5F36GNzv1mV@R^X}*N;sNuQ{ zgzd=tI5;@QU6O2AzGQOap`n~(W9e;fmnS{JKA%CFD>nvc??oz1Utk5Ox;8)Z>2q1x z1*C(IA3st6$8CJV;K69t=go~mVU^@WPAA&;padvDz6l$D=pD( zgUjO-zj_-FsS~LfX>3KUgk9eTP?^Lc5BGr9TQj?Y>r)_FvJ^E`={A0!*WFuCf*$@N)Re;$VS zJ+4nXp6V>@KG)P%c0lHq0sMg{wx11MmJevoxO5zx?0W#|(}spTsF%7>0z3yGxS96O zbpYHgy9Tfen-Mi0q6_83>C-R3@AZhqqxgX?oRXRvqnWz> z(4j+6?&&*`lISP%0}Au@81sx z?$s%qK;nbDONN5h?sL-aYuBE?Yxt`*r)f>5j*t*jr+ewj=T67_>+d@-b)LTa?f9Y= zLBkX}!NW(6_{7hFNoETV4>wuJ4vdJ1xbmZFOKcVnoDiHql(lq#Q_Ekp69)kU&F7E<8pFA6X^c)1zpI})#Oe(}1YJZh^;Tb{dnxbvXc*ckjC zjJ}HOg8CliV|#Y)+vgAB>zj6(ap^`~*9p~-?xi=qyi(En6dD>jJTdWBt1jb;5edbR zT2QcZg4w>JYcygiI;sOp=%9YNd{z2;FkA$Gk9coDCrQlY< z0(zhmGQ@Rkla$l_&`_rQ8%tM|t);5>7c>c(l(FI{ukO0{*s3izC-YZSpe_$Q^Vkm9 zN98u4A22^)g;a28I+ur8?Q6}biO-im*@^KKc%>F*?yA==@@7%rnv8|1}xSTE`u*c-#sFWPLTbnx;f_iEu7Zn*a z3|8Q4KX~OW97tt)MgeEIgIs+dm{8k2q5=ScY_sTXmy*ju+`M1xL{lczjnr&;+5gYj~=}O8IEWzU8~GU z^?$wD!zKLb5w8dL@8_fbK{I4#UosxV#Ryjux7JMC9SCw4z$l}5{odDCg8e6U^~V~B zATKqoN&@&uR7b+t_^k&?L^J!^KGMK~$S(|Ax|Z?e$QU7jq3P(j@bHKLSx21}Ud>vw znqJaOCm}L7V5y^62aWVh*ZRJ?_)`cStdiG~G;{6x)i#R;8i!*qbqo%kg=jT{AmupI z9f(%6Ui3^Ejdpzwm2nTpE+7{LR`ZC8NR-Opr@f}i@>M<43!+X2wKv|2WP*L zovnW_I4A;K&@qa!8R>>in*{~$gHW(ROIGZT6^uPwU7=~j=;nx@J$n|8r)vK6i5ExQ z?E343+X&}`+$JE?&GCn`>rn{>yI4-2!`V z>(;G*`STu6xm3M9)dePENw}~m(AY}(tx!>7_Yuwm5@|>`cEWe0NVTcu2z#5rq(mXS ziS+jGLXN9&8D+L%pg>T)?Y_{jXCfmp6eMqm+k$#@0|K>Dx}^?;Sj)ZVLEfQ20~=by zUn&gMk>@-TogEo$6WC|ljlZJc;npRl4(6a0SejnoTi|s z=o8L^#aSkzu=3bQq3|r5LQJo(q2_FY{aB56V2okheY%6e4PfbTf2gG6S2OFgokhMe zS`*)_B4x0B_SY(CXaqS{qB)Wy7mkC=Pmbrk{izIKrjd0uX6B6zzAxVG zG++<7%TqC`DvIuP$6lRV5$;SM8d`H!IqRUf_!B;T!u8X~LqN5vi&2Ybzdv`|ymi?) zz*Whk)P<@V@T_I}gp#b3p5vctwq5N>zcMSqsI08)fM+WO((VOFNH_ZcgM5I+IB)>K zpPGlOw{lXB#%Lrzt1b78_i)nXXlB~7WsBrwgAri`AjpM$Vq<47L0V}@w~E+%njH5` zV@oxvLwo*pcr(0Y7N_aJ`=d33hxjzaUQ1J@Jkhy~oxbNy7;sKIfRS6S7By!l=+Mo$ z6%c|c`tND?$5$biTmln@dAlG%=CYdN5pH{4&4XKb(w){bxNfb#3}5%X05iu!3#~MD z-MmM(nA2S_c@21csWByLMw)86aQpVMF<0Ha<@4)vdbnw*5PrVKpBD{x>f;j<61s*> zTV{+$b`b^xwM>b`TA&{DvmehZLMsLSpb(nX#Bp)C`Pi{-%(e6|jR=9sI-mz@gL!Lq zFl3lDX?c9r2=^ufLE9}MDBk4DCai&bBC>A0@|o-(Gj8(^n0V6AEdE7Mom|X z*FuI3ifD7}o*$xR2`j};+X$;sQyoQEr=?H{6hM_=ca}(e!RVQe6Xb{w76fZ z=Cx|63$>6%uPW2_BLBt}NPDjvZ-HcsMu@G6(@g!Suz?gax0`Q2m~n#Q?#01l`ye%1+z!|{w~GB()5Sl4G(pOYBXa`dj!Y?qxf=y^&7p&` zLoqs9_p>QIF|!=LsH>|N7$&oB^eG&koZLIfN_-auf=s*+Dlr{kZ%R0Z_pDvNo*dL-erNb1$`?Rq ze&wh3%1|#o7SPSks7k{Wjg@+OZ9G%8XoS(p=CP?)*3*@HVLCI{0Xlu}8y5l&Mo}-a zG^y{oM6G`eeJr&Vv#;VGBVCnf_w@H)CRiEx@Z8JOd}(^QkhRD~2nmh8o?0ujF#Ztm z_6ziaRC6Rnd|46dNR2Khs1>9?Y9%0PP*Bj}Qis9jOTQ0A9(B<^u73; zlk3{$>KN7RFH|t3JH7RV^$f?u_WUhx{Q_1o?^4+eid?kyTEXc#+RT|fuyP!U{j%fH zE>%M;Y4-GYpSt?22yS2$k?Ou|2vtm6;x(@;WEMwO!&`u0}HV(KpCNf`NHSU`W;#p>Dq7vIPm}~tx{W<#)<2@v3_kPqXeG$vyB+{ z@qK70QSE`Uaka^h{jFJXs%NHMm=2glPWpev3ZiQ9fh%3utY&ALCHPka;itf|CZ94Q zYid;Rucx4HSkJC7`Rm@jd)xQy@r6BXJvX-kRFUOOOcx;8%UX&@qAX$2645U%R^2s;cKA!n%Nb zPjkR+~q(x_pqj@e_g$!cdd;MlP;1FuvgC!PNYPa#~YrUo&d;bxX%+kpJV| zmhJ3YjBYnwd6{fnL$_T5+I;9`q^YA$AUpgLDmYnWY!PVh;N>FILaRp7lNBI01@y@<5q z2-J1@JkPmijckOp>$u-`9%b5+rvk9kBPwa6 za0?2o9~Xz)-N;#c~LYK(&2LG%nH)tv#JRQr+$JsI?w zq5RqyT!KWjA>lhG|QfUVMgDbe`+q%x+kLJ&=5c62KmU1M_n2rwoZ^XT>hs|}8&B+7 zQ{`|)ZSG&v_Ro(U`HkCGFic#u_-kA2=50%bQu7~}Q!ji0Yr-_ORx6H2VXd~twPUw%eL}ao63s4pHkynrY!?)pW5wx z$u`gKNHb@%&rBAQq;fHiJvM(QB1+MkiEcvk*97KW-Wlos5Q@|-AdAk9j;BRCkM7mG zPL1~vKHqU#@&ZR)X{v306zx`)pf&Xmj6W&7lX6Up-ccJH!*%qttRei+PX|lw6`6!?P0OJMRe3P`DPN^i zWDGmFbO(s3bG1hH3oavO6j-TDmI?6 z*kSWrQd07uU0(qE;hI1etdo-7V!|`dS)RhAA2FBL@*P&&=pxN9n{6c#WL5eiYS^9n zmyBVp+uvK>X5heKy+PB7{tq7)k486t7T89&_r=0YX8dZ&DtU{YJZY|DQg^9~-|Cwt z80IhYmr+@@wQKJE{msfYaracWtDE5Oi6Z)?9NeqD#>0iD!zQvR(-|;!UjmjRr>XtWiZ8+?SBji+N z+FpwMSRRGbw-z8V{@5DA`^HwnHilevWTh<~Ngrltx{ZGryqt|K^SYVyoJ8W8@9XtD zCO>p{&Yfo%8>@+>>(u*8)Z~1i-U-i#(NC`&(Yg4~gAyVS>bbt4`rzJKrRb|CPW*-C zm0tTqbZMURPJ}eZpWk_c3Q#$&vN$mJ^A~*`9C-(s|QKcJ1A9f_XZP-`BZ+x z?FY=PFNR%I8oQ#Bs5UPCXm``zydp2|zmH7U-e`|S3s^|YVHQ0GZhmBJY`5JfjG!t5 z=WwYdOPG1ryj|c?P{e?N>roa%QP`j*edW6x>SZF@TvfUg!1Kw?>yoX(NMc?0l@SMB z*|wq9X6{{#HdpwZ!b$@z0xu^R{C$O)8@p?kq}RceaSag7v?`JYfS$Q}iPt8l+u*F5M@2rDlo+DGLvQf2suWZ*a9b3kv&SOA8N-C!<_1J{ zG;Gc#6rg^>_!2oFz8J|O;1w+LDLV-hlMi(k!+!28t9l$vnO63-Io98EW;X3QH?5i* zM14NkYW1$Uf0HW@GCHo2|Fq=$~Qq;xA*pb0xE#R(Nj|ZbJJ0gBm0#J z6$r*j_a>fC2({~i=yd}l#Kw?hH@?=Zu^%OddbpL1q1fPeHTH;j@T42{2YG8fe)Ont zdZcqXBjYnS*M3tyU^mSDd5@CswWNeZ2-K4X3oy3@2W<_UqM~ z8VU_wzH!9`6K37VPw4NkyssPD_L)z=OSbx0`+!~NC;w2U2v_xtd8?K0V;|@GksU1B zBtxB1K+SkTb>oH&AK`Ft1c8$1{0@8IE~nT7AadBWAMo@WU%a>EsoO3J5e_a7uz}d%Z#f6$C=xO~R-E5yKYy0=^x}8`Cd0z{5SnCL!D- zA&lXU)kwGG0`sqr=YIOhfjeybO)vtx0L6V7;EgxjNT?Y)YSs0Op$@{WI|g7s0v8Jz zB#?{Jg#y#uA;4SIjz>`nw7hdz&&*u-HR{w)(6IYW$|AMi$~-)z5H1-^8W=Fud^QOP z57qJ!EQ!WjGiH>hTZDujfcn_)zUTruuK>g|G3A2MGe$MGgq~gqn`)O@+zA-FXnuTq zyBq!%9afhrZ4HqBbr| z3(>xS4v6m_i^**rK8YPo7B>SYoLLuJSG-oawPK`@p)zs%9w~?K4R`N;C>7doJJ#65 z*l@A8YwxKPk8R90qf#3lJzBavu)~H?~+&ls^z%=$7+s*Q=B+T6|7jX zqcD0kxVYmXQHU2$E8VgjbOV{51_kEh`}fy-!=3NJ;}wei@C0F`8h{uad=aocVNn#M zXSQM(ff(Suc}vzKZnx>{jLf?>ybHTQ%=ZwJhpeI1)xt(8bpHG78GH zqc{~;?Ckc~OOOqs6sxvJ`P?~TmLq(0pbqFIwB2BE=OHMDClwS(Wv3FaMI>Md&?irv zh{UhPNXm1t_~0AGt_^R7TKBQ2$k58lFEEe=pMZw;!O$w4WvCJdV5@Hs3C&wmsRPOxF;WHNth04U!=VBS=$9u+ce94VI&6VPtZW-iBeO z_LY(+;?~2ku=4v`jx1ZTf{xgzIy_fPlOdg$BICXN%#)y)>xc#FT_f-^@4Na#k=QlS zVFoEQ#c>d}aHFcoLy{vO*MnnzFg$Fn3)UN1;~2=Ia#vL8AqZee?o>C(u|Xmh?1X15 zA0y8d62f-0<$>HnFP%Zr+Go$65oaB>uKDsK)IKe2R4xjOeVMqV}c$&8nN{fX0GUAquwOsHy2rXx1O<>sh$Dxx=r0 zUk-6@zlw@Nqhp^ssrX{S5fRALxLG^PIRj!4ia& zikdfgz|o6%S6Er;he%hcordViE^2w&+FB6mLwDoF2;-k$qk05>fM7z#SO)=1*XifB zx&)n5pjXN(Jc$3xui6;eKNf5et6h!Q589VZXty)?Y`34u6C%~vonT39(N1t`OKsd$ zQZ)E66BVMt3Y>+!g-H%<#Ga`^L<+&YPGrVsI2$G{Na-E86Hofg{qNIEztFO<1RAEF zQ-lEpl8H*JnlB#DdJc}u!b3%96GNWHU91L+ho6b~N`bE+G$3FDPHg3VH=yh>AH*c5c)K2d092d?Uuw;0fPEV`Q>L##L2IZ?5xTBIvFRcOIHHa2UKi zBi-O6lrA|d6q=jNzcSa=S}o}0@{P;*bd*4U243RXrYwQu@yRJFme*i>nMjXxaKT^2l~#IMC#?a z{dMcsXx(`$?4Ec^(83$puJoFy!vAx;sA zw}?(GnYxD%;!KcwiT3~zn2f)}Nqn(FOKu5LMNIY^VI1q5;v3jzMvtq!8t(O6hlfZU zm}sN?{f9(-U=^AqC}@K@Q@ZHeAVzo^?@;aZjDfCnbVeb7VJOOKbYN@+>cYjRI!{?r zh^Hb%#C;WSIW?#W$wka*{8gr3M}ZeLomMUH#@YsW(9S< zEbcGfD7;JB&(o8R5Noxyk8m7ejJ)FD5PkS2{|@zaCUZ{9sfy}4--5^Nkp6r#s~ZGTRBhST1|f8Lnw zm>N9hj@m4o>&@fU6?N~oVPW6`CaocG;%e&D@bK*HuGoV-!Ars9 zg-f-sA8Bsdut5$aX0Y&OwWoGS+>Yp02}Wh3I)fkh`xD{Ik7r(KT@mZ@{&w~dX`5?n ziy)aCOey9Z0(uyr6_<>2Bt`@^-pR38EFzSMU(K0gkKaZ>n{%l(+QUAp<2loGT+%jdZ*upThIpP5%xRdsk`)h5YhvOxAARfJ37mMDc! zzsz|WGPvHvwA)NK3wdb-&G7K>&a{=I;oeqtalx6s*^a-=2U3svG*_7EG8sYYlN>AC zFH*T9(RValwiWg;7=)HaBFR#q&)xz^q6r$fa5?A3v3s1K(OVNN?J5q<3*ASb=AVM| zv_#3hr>mpG;H=GD)sfXoiMmpd@($s6`yU!5xD?rbxR}=u?e4{3m%u%Bpli7R;Q&Ps z!Dt)#i_espCu>k%DMvA)k-H1^X2!dBESX`5F(Gqsrf-0x*bQi#QZgokVc(=n1F=sI zLO+R}q;`UP=yZrkdE8XZIfY=c$=S&PR7dk4P^Kiz$~0!L&4=_t+Sqs6yt#^;`x6xk zu=q+krHR9fU}QLO;cck$%x-*GtqtxUchj zPMtiN$!F2fdGg?>giFfn;grWyvVmStUq;(GUU@$j8>6!PwHgfr!+8tFiVDRqx#QYK zKiP)gAJEXAzP?lX2ZO?rpU%Hy(L2nQWC#B1t4~z8cI}`Hb$&l-g+qx6_lq~0IjVtp z0163)#WE6g7=||-k9$ba{&f1I^b_U{N!OvX55rQ5?$jd42W&E};zNeG7mwz(BWR!^ zhXc!2WELJb?=6vwt_~aYX(GfKRAj1#X3zX2M~1TBLr)Q57MP*~B|Xq`NHG1Va!*fh z_alkEZgd<_IHf}+9z^NpIQ6)y={91N_%y_T<4CSPI*F0UZ9=ZISD`e(K4wh(qMIZe z<~t5ycfG{!nnXsX8OG+K$;@jI(0cQ#cx^Aiiqg{IXg*z6yin*g^1Tq2gHedHL`)b) z#Z`@=Kx8xumCs?|+auB`18o(`#v`8ehi#Y=2|?|E`=QF})QbuPjS)pQ+_8u4)@tqw zgj6=kYl4VBvX=U9P{9#|0m(`TSQkGMD8t@Awc&R#f#yP>AaeA6RKT{H@4u8ovcuSg zAOu~4A@SLPcR9^#0BY}XiW#207Xl^&$dNRkmCq7<2+9K0gmS!=F!FB(>JmKTmq?JT zw+_K92HzKuKDUWhOm@2yK1Z)eq2-P|1TXBgGS2z?65hnf3t8qIDjNh-G1u7xy1KfM z%|GvYbRSK;h<7C1!LVl2SHt&OJ<+XJMH~sT$5DGgLh`#_RW{$<@ZgYM>B?M<;%25H z>iSLQVI0rGMHa0edyMk^RfFf}iC#?!%D&Q>X zAudK9BN+w_6~vK46z>S?ycdf8FMvBCyn|!2b1+*1K?&LQsq8n6NH2p;47Xz&;EEgq zaIq85R3%Y244w-i5Oc8GWgc$YJ2;I%Kss!XLJ-2x-z51aqf+cwG55u6F~(f@6GZwa z2Aa^Kl}4J+N8#!dvTBvw?66ty{n*de90>|oj1GK5oYo_WZ)iJ_CaDI-rghl&9yD|f zdG0bGThZYd5gqR2jqa%i?Zil!oG_-r3FWHM;u?&C$^3QKaCDE{v18vjlXum(s)49@ z5f#<({k!tB0pkl7?)&>Iy>e^)D=cmM!}5}n4~EGaQS123B(@o|(j$QpBN9wu@UoQR zl!!TwzEEu)L@H)sU+W?a?(8@!ub_0-Y`EeYe~y0l7}pwU(XVP| zGX5#CpfS-de21(rvI&yDJgPWY&W&KX0&Cb_ZhPogY6;Q9(G){*0c%{vnJdRy}*HLIP83 zN6?!8FgW-#5$V&+8)-56oPru0R*Q3}pkY8M|I&NQV1uIzMwN{ZG}VXx5QQTSb`G@! z?MthE&`qQ9t(3aZM#VTl1p-B}7l7VK(-)rahHn=!GYmL%{7vj)#%a#gwi^xx(Z*u$@*KcJ$=3A+*GCnm;y2GkKl-b6^= z--Nka38dbKxwoH|MMWFp5)LoRS^sNsguXGNF!^I9v|H^UwDv%8@&)UT_m z1PNgA=<=eOc~s{0X4&j8a01++=JDrh?B#a}3o|n?Fd!0?$>xB`TQxKKwM&Z6`t6s= zrIiJB;zyT`(->BKh*J!Dn>?Iyh)Z8FufOz&ULF6mK(=tA^~}R^gKr0|?Y=mu$SnxG z^|KT@`r-`~cZ6D{D0${ofe3KJ2qq^`2_c=m#M;AjLX4zjB7v<0n1&DF(W-KeIys56 zv9YPqjgBfCYzz#^%jhg6Wl3mQm``?M@roa-xurv4Ja`ZqdN(AbNp&h1Hvw>HzN#VB zP8!wWN7C=^s(R&Ay*=@58BDexyHZC5l9S>P>#b>O9fYfXnqXJKXPnpZLzU` z$}g=OGqV0+-QREYq}65m>$`#7BSknYhHyn*!4wKO0F+;OI`6qY&!PDVrl*P3!!dMQ z#^M9ZzXeHL8EB_bY#gY_x%W6Og{#axW0lLe)zUFS*vs!m&2#vAKZp~8xIoR6r$k0Z z8Zz8hjZrQ%ag36G#8}Rz{y^64%BR2`)Q2l@*2vfMC(8uC%cbn^n zwl80lSYF>gK?C_k^{wfb51~JGxt1_jU9lREoYf{%|k7{;|9vc)iYHoRGLzL{UvtQ7fo@Uunf_em~2&8j0 z?7^`WCsn_J|N0B{N=s-Z`uztvNh4vvKBOCBkW%3~chvb>*OqC&i(eckh7L5@f3RJ@ zBJiF1Uy0j2)c5Gb5?0k%2~Gq^Gsd%==x2zE?msFws7NFvUMnH)wKM(Dxp{BNc5-*v ztUK^(#YBIrV{hir=J8Dy&W-K*NiXtS>9#egEo3>^%m4FssZ7V;0{Zrk%zRSWIg#$% z87^$Q)N{vf)CLDvIWKw9+n_7m;nl2ua$!99PqR5LLm9G_Pcc_1X04$`FEurly;EG1 z|8f75+pkla!Z==3e7^3(mL-CxK|Z=BiB>hJflAi^WGX1bqtN6K`^J~t{adp=!3Tl+ zc5^bK%%%E-F!q06dq24Bcekasr~ISMmSNv+<|ixt;|2IVR=f6hYcq04zqF*GTS{1%Qt}kHMpN`Mlw;S-*qQqls{FV0z2`RhB%Dp%} zAO9fa`2QabYx2oN>wq5zEh9#%ZW#7L3SoS1kszv{PZ4(*yjy6f#Oocf79L?Taf3KI zFl-U0c|MTy6X=$i!FNLm{nI{k@3!)YCfs<^UI(kN3NN{%=B*hHs z^}`{RtCVu*Vala|3)iA=8a(mRiMi8z#Jgpe>7^{;-KF+gmVUu8kGv!!2g^28e`Tud zE!*S2?9R!Pcf6lmGu?3L*r6pRZtl}Yu3TbfYGy;do%>c_tb3dAHen`CII!%Zj%~bz zED}GoGY73YTcTpFs8WP{#_Vyq`pYViml^5O$h}Z6wGk! zzV*#E2UIco6Hg+aSUGX<*f@_)P4&XfYyxr-VRr<5T2^8E2 zS39gm{7J17cF>MKgA5_Bc!8OGZ;IZoGi@(&PrbN0&i7}F`seuqp>+%loB`q|jKIF& z({IchAK))(NaqhogP)D5JK)eKhICXZ#0`ssGBPn?3_iPIz#iT>N>ZaaN(+RoBGeB* zfa!p9QD+W=6-IVmE(71KNiI7&gYfGEy8Hv_hDWOVfVdj{?%E%j_5ykOCf=+K2zxpE|#zvZJ;1fZ}n?Q@f{V4(qqHW^2r+_l;w!gb& zyR3Kx1Xjn^mJkBR*SGM~Coi<=2ofuJoZ4=+*hg3zEVUT=t6+B52O=cJJg8i#ktN`z zWI-g{9RM*If%cZFEW69t14jXEB11HAa%RLOhnE@^{93ZwS`w%U)n zgo5+cb4Z=v90xq9|pH@oxD&R^xh=vcC4$k>c{IYkB zX{!c)ra(yMK4{%e+30hWL91);?-JJ<4PdAg4};c$UQiY$5eVLZ23?q+Q4mouU={Cj z+VmYo*9CBZ*fSWLK&#F59_#5uzk1VY6O)(K0@p|9q!bh?4>ygVaT4_YDd1I*Y+xVu zUnpKf!F<7eq)RD!Vr+DjyrRK+qLqS1_L@Y!2xJ6ODa55ixkeAbZbJG|;D^3ib=3R6 zo)%Hv{>TI-spS5RW;w_h0`#&JD99`<$>;Txq$T4uXuxSYU$Sh3+!gH4T$593?c%>VT>k2kpIM=4FRSM5=sUbsJmY#XIK~@QkCL&fPEII z8e-n&-ZHs?g{I};Vx^~>f!9cJg^Y%JAl=O3el4$P- z1T2})d*KRK%8IxU_tcX4wUd}BCAhe%RN>jS?b{!Ogq%V!fLLjSxLa5UJLKNlIimJq zgG2ryTvBOi^&4g$2hx1Vo;P?2bl#U8Ahq#88&ST3RKAFBCz{Pas8vAk9?&_r>-buD zexq^5iL|?fOjR1g*0X^urJTM^RJ1;ZyDTq%`G(oc&*t@(Z`-khBX7xx6DO?u&L8b6 z;(IE8xR)%#98dsA>^;xBM^rQr(Ub&nlgjV~uk;-P0)D8zh#?@1 zyi{^DP#!Ip2zcZb6duHUo23i=ZC})je?UR30}NtnR`fi5fRkSMX$onN9G}l z9U03)LEN|*7#MhEtQe{K3m6;1(x4h9UAR7Ry*)i@eN#2~VhRRhqhvpmq!lk-#pCb_ zv;rBcSZKHfFXBX~0Z6sNNgbJ*`egQ2$nfJbqK<+_B+aZ?Ud(cS0y8;YeHZq!;x}ht zU|^6v{`J&&U#$-=OWe0g;3P;2Jhl}$PJ|1DC=7Q6jdobcMeO~v{ooJ7@84fT!K9#* zM8N}#hQTVgiC9(w4t*Vfk|>UfVcYP~6qi&qUo9=MVP>zA5M6OK%!D z9V~PcUW|5!bRB{%r;547p5R@1-H+56brM0Pka18r7Nq?FX9SgF4`%Ejo8Lr@h^QY7 zRh1wnY_%fJFSt_4xnHR+<6n+bh#&+j3+e8~Y(GrG*u(8IOoL%Ig>}2l7|H7;qTMkN z#ttRz9ayMQJ_=teI4>P?`*51J|HEbg?+2(*%YbZElB3_~wowz^1 zN0aIqBbo#7JfWOA_J%nS^EeqN0zDb89=Zdb2xjj}>{Q>y=SjIyV7$(jVy7x$Xp?87 zxrpUfT@2Ddi)?bEIk??;*}MZ7)JMz;c&13~Bv`>FbI{^llF027`#1|5-Z>;5k&jN& z;B(`#$$Jui? zaiQ3>!=dtOG`AlgYHK5JPw^_~u!g2yLvs+Mdf?$H7IyYN3MaZ8XbvvU!BA(6r+p=R z^4sNAsg1vo=xo3VRv9E~a030T!Z$-z^Xo*nt372EH0MtsxViAx$m;JiW@1Jfb+ zeYkKggeD*6RgU#mN}z|f9h1cEu+C^+^f1EPbnXFph4J$|%X_=%5p<_Q;L1U_g$Z?r z_GL^=S8?^nF)}HpKU~JW)juTUygm_MxKJFQErmYL32xw~WiL!W_fTXrYpH4He3d_Q z#t!+J%Y0(OCUAFx0H`#Vr=@s}p*R0-Z?q!QSp$rGaPujHru?Sy;_A&59KWbvu<2dwygSpyQa|b#U06k_iUM4#P z_<klQ=W**l^pBm#)X2qwQ|WvfE7*2a{9-kq}LY^nf?8 zTfjAHiY^4$w=sLuc#eBva zXwx7ze=lIgWVCmLtswy`dU$kn*F+X*f}#sT%o@7j`^!vHt5fDVsy7z3v;au#^^R57 zg7xytZFT>=>)&S|%!gFzR)UczPWejImT~nXFy05?krX>+DWw<3KL^ezwjfJVS&)PG7PW42z2k?qOTFrQ&Lgk;AGOtqMHml z?}6M`XP+Ec4%ZE^>k{o8|0YZUYe=&Qxl(-)i{O90yr94nyxvH&Z6zNo4^PxEd}Vmi zT9V=VNuJT(E2_)jFkFoh2}|3NPv{dW6qex~7JI=9QNZsn=uyyIRE!Nk1B_~%??D4D zjhU5AEfkKVB_8tfvMIUlaoJR5oja=K_v&5}I zdA?`nc`Ia%6&5JpmVkzX(!Pwee!@2JbUmr67uUcu00mX&`kJ+TQ7|9zE#paF>s!UL zHuDTory)kTuzCQsP*7FiJ(@Iv$5P!f8HkLLAiguw$P0O&zz6G&!!eU~)58YvqEHC0 z4HztHdGDR+7aj^`X7tr;@+vT>Nko>WpfEL|E-nzW095JyFez|2RpR^+@*R9*Dq*kE zEK0FBvuD{U_um=4CJfke=3KF>6B`FK);3tYG0B6#yfHh&|{qYV{5x@@;{Qcpt zyW3PVXa>;F(jcs1U;q2@oQ2Wx}1-KIHgXqx*EA9_YaaN~fG7j0utI#r0gYA~BNyCN>`y=~CV&9-9Yg!?zSnuh)T`P$zIwFiMh&gkb@s)hH9}GW zq(+q@{_iWRy(7sOHTUa({k4Sn!8%8PNd-~1lLk6ur3jbNF$?#;HjIfamyClIOkkv$M?C>SmOYbMlA~7xnK+G?+C_| zAh(l-I3!;RnZE#8yfNbnFEAV)=^8M+wv|#BPgg$7Yn_0lm&{oOK)qD`l8lmtli@ve zzLH9=lGHoE%;OKw{;5#c`XA0Xm=JcPL!4xGdiq`RQHmOI8)VjIvN<|BUVyZWs6lJI z%FAe1<1+w>9s~syLnS5i!;)Kp*zw|oXmfOawt}G- zA!*{tn2eMZS8C(k&lkR)+{`n3zJIlM@Q?X*YbPdn$Y&LUZNlC65G_dXd6MK6%iHX; z>)oTzE*VaqP1ws~zK^Y7WJc<{s#RHCJ$5Ty2LTEV&%JN>sXdtpV81_<4-(Y+LJ=0u z@383^qk18ol$eu-+SJl?E9#!&Fn(!i-9u~EPN>4!a9-&!%+}s$$~}jT{aP0P-uA$a zelJhD#2L*kqPt;Ffdonm3;mS4BpRIE`xXc6B@oQ$5>{=;Fu0qmN_1M)KgT~0`A#?p z@=>hA3zrbe0<`pDxJ%Ads}YjByZpOf_7XrD!pIJ}9a%8!Kw#{7T=XN%p%pF|=65X> zplo4F;2S1pkUvTi#RG&WnF2y07Z&~9 l^zkeX%3d!}XmOn@2-?iKcz6Ep>c7#~v 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 Wyzlw=;4-?W8auUi#Qth{`F{X+VB~86 literal 0 HcmV?d00001 diff --git a/docs/assets/cli_gui03_oef_node.png b/docs/assets/cli_gui03_oef_node.png new file mode 100644 index 0000000000000000000000000000000000000000..69ef896d776ab9e57f1b6cf8739f04eb9110fb44 GIT binary patch literal 46111 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 literal 0 HcmV?d00001 diff --git a/docs/assets/cli_gui04_new_agent.png b/docs/assets/cli_gui04_new_agent.png new file mode 100644 index 0000000000000000000000000000000000000000..0cf5d5659cdcbf1c67c133571807ea70493c4c29 GIT binary patch literal 68113 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%0DDA)gixf;jWc+A^~Q*t80(gNu%RQb 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 z∋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 literal 0 HcmV?d00001 diff --git a/docs/assets/cli_gui05_full_running_agent.png b/docs/assets/cli_gui05_full_running_agent.png new file mode 100644 index 0000000000000000000000000000000000000000..b54f9ba46dce8f7740d561edbfdddc5b975bc6f2 GIT binary patch literal 241343 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 C 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 s_};W-Fq{vK7HnL%y@$p9e![new gui screen](assets/cli_gui02_sequence.png) + +On the left-hand side we can see any agents you have created and any protocols, connections and skills they have. Initially this will be empy - or if you have created an agent using the CLI in the quick-start guide and not deleted it then that should be listed. + +On the right-hand side is the Reqistry which shows all the protocols, connections and skills which are available to you to construct your agents out of. + +To create a new agent follow these steps: +

    ![gui sequence](assets/cli_gui01_clean.png)
    + +1. In the [Create Agent id] box on the left. type the name of your agent - e.g. my_new_agent. This should now be the currently selected agent - but you can click on its name in the list to make sure. +2. Click the [Create Agent] button - the newly created agent should appear in the [Local Agents] table +3. On the right hand side, find the Echo skill and click on it - this will select it +4. Click on the [Add skill] button - which should actually now say "Add echo skill to my_new_agent agent" +5. Start an OEF Node, by clicking on the [Start OEF Node] button. Wait for the text saying "A thing of beauty is a joy forever..." to appear. This shows that the node has started successfully +
    ![start node](assets/cli_gui03_oef_node.png)
    +6. Start the agent running, by clicking on the [start agent] button - you should say the output from the echo agent appearing on the screen +
    ![start agent](assets/cli_gui04_new_agent.png)
    +This is how your whole page should look if you followed the instrucitons corectly +
    ![whole screen running](assets/cli_gui05_full_running_agent.png)
    + From 512831ad80b75546d61eae00a8e4e0070025bccc Mon Sep 17 00:00:00 2001 From: Diarmid Campbell Date: Wed, 16 Oct 2019 14:51:49 +0100 Subject: [PATCH 34/71] added doc to main doc yaml ad fixed some typos --- docs/cli-gui.md | 15 +++++++++------ mkdocs.yml | 1 + 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/cli-gui.md b/docs/cli-gui.md index 67b4a94d51..32988445ea 100644 --- a/docs/cli-gui.md +++ b/docs/cli-gui.md @@ -1,4 +1,4 @@ -The AEA command Line interface (CLI) can also be invoked from a Graphical User Interface (GUI) which can be access from a web browser. +The AEA Command Line Interface (CLI) can also be invoked from a Graphical User Interface (GUI) which can be access from a web browser. These instructions will take you through building an agent, starting an OEF Node and running the agent - all from the GUI. Once you can do this, the other operations should be fairly self-explanatory. @@ -6,9 +6,8 @@ These instructions will take you through building an agent, starting an OEF Node Ensure you have the framework installed and the CLI is working by following the quick-start guide. -## Starting the gui -Go to your working folder (where you want to create new agents). If you followed the quick start guide, this will be in the my_aea directory. -Start the local web-server: +## Starting the GUI +Go to your working folder, where you want to create new agents. If you followed the quick start guide, this will be in the my_aea directory. Start the local web-server: ``` bash aea gui ``` @@ -19,11 +18,11 @@ You should see the following page displayed:
    ![new gui screen](assets/cli_gui02_sequence.png)
    -On the left-hand side we can see any agents you have created and any protocols, connections and skills they have. Initially this will be empy - or if you have created an agent using the CLI in the quick-start guide and not deleted it then that should be listed. +On the left-hand side we can see any agents you have created and any protocols, connections and skills they have. Initially this will be empty - or if you have created an agent using the CLI in the quick-start guide and not deleted it then that should be listed. On the right-hand side is the Reqistry which shows all the protocols, connections and skills which are available to you to construct your agents out of. -To create a new agent follow these steps: +To create a new agent and run it, follow these steps:
    ![gui sequence](assets/cli_gui01_clean.png)
    1. In the [Create Agent id] box on the left. type the name of your agent - e.g. my_new_agent. This should now be the currently selected agent - but you can click on its name in the list to make sure. @@ -31,9 +30,13 @@ To create a new agent follow these steps: 3. On the right hand side, find the Echo skill and click on it - this will select it 4. Click on the [Add skill] button - which should actually now say "Add echo skill to my_new_agent agent" 5. Start an OEF Node, by clicking on the [Start OEF Node] button. Wait for the text saying "A thing of beauty is a joy forever..." to appear. This shows that the node has started successfully +
    ![start node](assets/cli_gui03_oef_node.png)
    + 6. Start the agent running, by clicking on the [start agent] button - you should say the output from the echo agent appearing on the screen +
    ![start agent](assets/cli_gui04_new_agent.png)
    + This is how your whole page should look if you followed the instrucitons corectly
    ![whole screen running](assets/cli_gui05_full_running_agent.png)
    diff --git a/mkdocs.yml b/mkdocs.yml index 4669ed2f84..c4510c2d3e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -37,6 +37,7 @@ nav: - CLI: - "CLI tool": 'cli-how-to.md' - "Commands": 'cli-commands.md' + - "GUI": 'cli-gui.md' - Integration: - "Integrate with third parties": 'integration.md' - Demos: From b9d9b831b987a9ace540be7b7199bb36dbed8ee4 Mon Sep 17 00:00:00 2001 From: Diarmid Campbell Date: Wed, 16 Oct 2019 14:55:25 +0100 Subject: [PATCH 35/71] inserted pics better beteeen numbered list --- docs/cli-gui.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cli-gui.md b/docs/cli-gui.md index 32988445ea..88e69bbf8d 100644 --- a/docs/cli-gui.md +++ b/docs/cli-gui.md @@ -31,11 +31,11 @@ To create a new agent and run it, follow these steps: 4. Click on the [Add skill] button - which should actually now say "Add echo skill to my_new_agent agent" 5. Start an OEF Node, by clicking on the [Start OEF Node] button. Wait for the text saying "A thing of beauty is a joy forever..." to appear. This shows that the node has started successfully -
    ![start node](assets/cli_gui03_oef_node.png)
    +
    ![start node](assets/cli_gui03_oef_node.png)
    6. Start the agent running, by clicking on the [start agent] button - you should say the output from the echo agent appearing on the screen -
    ![start agent](assets/cli_gui04_new_agent.png)
    +
    ![start agent](assets/cli_gui04_new_agent.png)
    This is how your whole page should look if you followed the instrucitons corectly
    ![whole screen running](assets/cli_gui05_full_running_agent.png)
    From 02e162bfb15be2fdb1a96c9cce36295c0de64cb4 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Wed, 16 Oct 2019 16:48:01 +0200 Subject: [PATCH 36/71] add tests for 'aea gui' --- aea/cli/__main__.py | 6 +- tests/conftest.py | 12 ++++ tests/test_cli/test_commands/test_freeze.py | 3 +- tests/test_cli/test_commands/test_gui.py | 66 +++++++++++++++++++++ 4 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 tests/test_cli/test_commands/test_gui.py diff --git a/aea/cli/__main__.py b/aea/cli/__main__.py index fac8e51cc4..7993a13e44 100755 --- a/aea/cli/__main__.py +++ b/aea/cli/__main__.py @@ -126,9 +126,9 @@ def freeze(ctx: Context): @pass_ctx def gui(ctx: Context): """Run the CLI GUI.""" - import aea.cli_gui - logger.info("Running the GUI.....(press Ctrl+C to exit)") - aea.cli_gui.run() + import aea.cli_gui # pragma: no cover + logger.info("Running the GUI.....(press Ctrl+C to exit)") # pragma: no cover + aea.cli_gui.run() # pragma: no cover cli.add_command(add) diff --git a/tests/conftest.py b/tests/conftest.py index 8ad1304cb1..9ad2005969 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,6 +22,7 @@ import inspect import logging import os +import socket import time from threading import Timer from typing import Optional @@ -62,6 +63,17 @@ def oef_port() -> int: return 10000 +def tcpping(ip, port) -> bool: + """Ping TCP port.""" + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + s.connect((ip, int(port))) + s.shutdown(2) + return True + except: + return False + + class OEFHealthCheck(object): """A health check class.""" diff --git a/tests/test_cli/test_commands/test_freeze.py b/tests/test_cli/test_commands/test_freeze.py index 5734b91371..a07892e6ca 100644 --- a/tests/test_cli/test_commands/test_freeze.py +++ b/tests/test_cli/test_commands/test_freeze.py @@ -17,10 +17,9 @@ # # ------------------------------------------------------------------------------ -"""This test module contains the tests for the `aea create` sub-command.""" +"""This test module contains the tests for the `aea freeze` sub-command.""" import json import os -import shutil from pathlib import Path import jsonschema diff --git a/tests/test_cli/test_commands/test_gui.py b/tests/test_cli/test_commands/test_gui.py new file mode 100644 index 0000000000..b4dbd2177e --- /dev/null +++ b/tests/test_cli/test_commands/test_gui.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the `aea gui` sub-command.""" +import json +import os +import shutil +import socket +import subprocess +import sys +import tempfile +import time +from multiprocessing import Process +from pathlib import Path + +import jsonschema +from click.testing import CliRunner +from jsonschema import Draft4Validator + +from aea.cli import cli +from aea.cli.__main__ import gui +from ...conftest import AGENT_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR, CLI_LOG_OPTION, CUR_PATH, tcpping + + +class TestGui: + """Test that the command 'aea gui' works as expected.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.schema = json.load(open(AGENT_CONFIGURATION_SCHEMA)) + cls.resolver = jsonschema.RefResolver("file://{}/".format(Path(CONFIGURATION_SCHEMA_DIR).absolute()), cls.schema) + cls.validator = Draft4Validator(cls.schema, resolver=cls.resolver) + + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + cls.proc = subprocess.Popen(["aea", *CLI_LOG_OPTION, "gui"]) + + def test_gui(self): + """Test that the gui process has been spawned correctly.""" + assert tcpping("localhost", 8080) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.proc.terminate() + cls.proc.wait(2.0) + os.chdir(cls.cwd) From 1f132fb84486f9f7052b5c2a91ad0c2460f5b242 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Wed, 16 Oct 2019 17:00:41 +0200 Subject: [PATCH 37/71] add tests on 'aea list *'. Sort output in alphanumerical order. --- aea/cli/list.py | 6 +- tests/test_cli/test_commands/test_list.py | 108 ++++++++++++++++++++++ 2 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 tests/test_cli/test_commands/test_list.py diff --git a/aea/cli/list.py b/aea/cli/list.py index ac912e125e..42175db694 100644 --- a/aea/cli/list.py +++ b/aea/cli/list.py @@ -35,7 +35,7 @@ def list(ctx: Context): @pass_ctx def connections(ctx: Context): """List all the installed connections.""" - for c in ctx.agent_config.connections: + for c in sorted(ctx.agent_config.connections): print(c) @@ -43,7 +43,7 @@ def connections(ctx: Context): @pass_ctx def protocols(ctx: Context): """List all the installed protocols.""" - for c in ctx.agent_config.protocols: + for c in sorted(ctx.agent_config.protocols): print(c) @@ -51,5 +51,5 @@ def protocols(ctx: Context): @pass_ctx def skills(ctx: Context): """List all the installed skills.""" - for c in ctx.agent_config.skills: + for c in sorted(ctx.agent_config.skills): print(c) diff --git a/tests/test_cli/test_commands/test_list.py b/tests/test_cli/test_commands/test_list.py new file mode 100644 index 0000000000..3ae7deda0b --- /dev/null +++ b/tests/test_cli/test_commands/test_list.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the `aea list` sub-command.""" +import json +import os +from pathlib import Path + +import jsonschema +from click.testing import CliRunner +from jsonschema import Draft4Validator + +from aea.cli import cli +from ...conftest import AGENT_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR, CLI_LOG_OPTION, CUR_PATH + + +class TestListProtocols: + """Test that the command 'aea list protocols' works as expected.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.schema = json.load(open(AGENT_CONFIGURATION_SCHEMA)) + cls.resolver = jsonschema.RefResolver("file://{}/".format(Path(CONFIGURATION_SCHEMA_DIR).absolute()), cls.schema) + cls.validator = Draft4Validator(cls.schema, resolver=cls.resolver) + + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + os.chdir(Path(CUR_PATH, "data", "dummy_aea")) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "list", "protocols"]) + + def test_correct_output(self, capsys): + """Test that the command has printed the correct output.""" + assert self.result.output == "\n".join(["default", "fipa"]) + "\n" + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + + +class TestListConnections: + """Test that the command 'aea list connections' works as expected.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.schema = json.load(open(AGENT_CONFIGURATION_SCHEMA)) + cls.resolver = jsonschema.RefResolver("file://{}/".format(Path(CONFIGURATION_SCHEMA_DIR).absolute()), cls.schema) + cls.validator = Draft4Validator(cls.schema, resolver=cls.resolver) + + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + os.chdir(Path(CUR_PATH, "data", "dummy_aea")) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "list", "connections"]) + + def test_correct_output(self, capsys): + """Test that the command has printed the correct output.""" + assert self.result.output == "\n".join(["local"]) + "\n" + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + + +class TestListSkills: + """Test that the command 'aea list skills' works as expected.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.schema = json.load(open(AGENT_CONFIGURATION_SCHEMA)) + cls.resolver = jsonschema.RefResolver("file://{}/".format(Path(CONFIGURATION_SCHEMA_DIR).absolute()), cls.schema) + cls.validator = Draft4Validator(cls.schema, resolver=cls.resolver) + + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + os.chdir(Path(CUR_PATH, "data", "dummy_aea")) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "list", "skills"]) + + def test_correct_output(self, capsys): + """Test that the command has printed the correct output.""" + assert self.result.output == "\n".join(["dummy", "error"]) + "\n" + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) From ef78e0d6b19650eea11961a907d8ed2017186f52 Mon Sep 17 00:00:00 2001 From: Diarmid Campbell Date: Wed, 16 Oct 2019 16:19:09 +0100 Subject: [PATCH 38/71] typos fixed in gui instructions and images corrected --- docs/cli-gui.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/cli-gui.md b/docs/cli-gui.md index 88e69bbf8d..42cfd6206a 100644 --- a/docs/cli-gui.md +++ b/docs/cli-gui.md @@ -4,7 +4,7 @@ These instructions will take you through building an agent, starting an OEF Node ## Preliminaries -Ensure you have the framework installed and the CLI is working by following the quick-start guide. +Ensure you have the framework installed and the CLI is working by following the [quick-start guide](quickstart.md). ## Starting the GUI Go to your working folder, where you want to create new agents. If you followed the quick start guide, this will be in the my_aea directory. Start the local web-server: @@ -16,14 +16,14 @@ Open this page in a browser: http://127.0.0.1:8080 You should see the following page displayed: -
    ![new gui screen](assets/cli_gui02_sequence.png)
    +
    ![new gui screen](assets/cli_gui01_clean.png)
    On the left-hand side we can see any agents you have created and any protocols, connections and skills they have. Initially this will be empty - or if you have created an agent using the CLI in the quick-start guide and not deleted it then that should be listed. On the right-hand side is the Reqistry which shows all the protocols, connections and skills which are available to you to construct your agents out of. To create a new agent and run it, follow these steps: -
    ![gui sequence](assets/cli_gui01_clean.png)
    +
    ![gui sequence](assets/cli_gui02_sequence.png)
    1. In the [Create Agent id] box on the left. type the name of your agent - e.g. my_new_agent. This should now be the currently selected agent - but you can click on its name in the list to make sure. 2. Click the [Create Agent] button - the newly created agent should appear in the [Local Agents] table @@ -33,10 +33,10 @@ To create a new agent and run it, follow these steps:
    ![start node](assets/cli_gui03_oef_node.png)
    -6. Start the agent running, by clicking on the [start agent] button - you should say the output from the echo agent appearing on the screen +6. Start the agent running, by clicking on the [start agent] button - you should see the output from the echo agent appearing on the screen
    ![start agent](assets/cli_gui04_new_agent.png)
    -This is how your whole page should look if you followed the instrucitons corectly +This is how your whole page should look if you followed the instructions correctly
    ![whole screen running](assets/cli_gui05_full_running_agent.png)
    From 4464d6d23ad998c2810f5680f0cc63d627e27c11 Mon Sep 17 00:00:00 2001 From: Diarmid Campbell Date: Wed, 16 Oct 2019 16:24:59 +0100 Subject: [PATCH 39/71] made address a link --- docs/cli-gui.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cli-gui.md b/docs/cli-gui.md index 42cfd6206a..991663195e 100644 --- a/docs/cli-gui.md +++ b/docs/cli-gui.md @@ -12,7 +12,7 @@ Go to your working folder, where you want to create new agents. If you followed aea gui ``` -Open this page in a browser: http://127.0.0.1:8080 +Open this page in a browser: [http://127.0.0.1:8080](http://127.0.0.1:8080) You should see the following page displayed: From 092f269b3a5821d1660b20a52bc16adee6e54af4 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Wed, 16 Oct 2019 16:37:42 +0100 Subject: [PATCH 40/71] Adds weather station fix --- packages/skills/weather_station/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/skills/weather_station/handlers.py b/packages/skills/weather_station/handlers.py index cc0c478fe4..c255245cce 100644 --- a/packages/skills/weather_station/handlers.py +++ b/packages/skills/weather_station/handlers.py @@ -70,7 +70,7 @@ def handle(self, message: Message, sender: str) -> None: """ fipa_msg = cast(FIPAMessage, message) msg_performative = FIPAMessage.Performative(fipa_msg.get('performative')) - message_id = cast(int, fipa_msg.get('id')) + message_id = cast(int, fipa_msg.get('message_id')) dialogue_id = cast(int, fipa_msg.get('dialogue_id')) if msg_performative == FIPAMessage.Performative.CFP: From 3e3fc65b1b1157e5de095715153ce9b2be7fc0c2 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Wed, 16 Oct 2019 17:59:53 +0200 Subject: [PATCH 41/71] add tests on 'aea search' --- tests/test_cli/test_commands/test_freeze.py | 2 +- tests/test_cli/test_commands/test_list.py | 6 +- tests/test_cli/test_commands/test_search.py | 147 ++++++++++++++++++++ 3 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 tests/test_cli/test_commands/test_search.py diff --git a/tests/test_cli/test_commands/test_freeze.py b/tests/test_cli/test_commands/test_freeze.py index a07892e6ca..504d1dcc64 100644 --- a/tests/test_cli/test_commands/test_freeze.py +++ b/tests/test_cli/test_commands/test_freeze.py @@ -46,7 +46,7 @@ def setup_class(cls): os.chdir(Path(CUR_PATH, "data", "dummy_aea")) cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "freeze"]) - def test_correct_output(self, capsys): + def test_correct_output(self): """Test that the command has printed the correct output.""" assert self.result.output == """protobuf\n""" diff --git a/tests/test_cli/test_commands/test_list.py b/tests/test_cli/test_commands/test_list.py index 3ae7deda0b..4f47e71547 100644 --- a/tests/test_cli/test_commands/test_list.py +++ b/tests/test_cli/test_commands/test_list.py @@ -46,7 +46,7 @@ def setup_class(cls): os.chdir(Path(CUR_PATH, "data", "dummy_aea")) cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "list", "protocols"]) - def test_correct_output(self, capsys): + def test_correct_output(self): """Test that the command has printed the correct output.""" assert self.result.output == "\n".join(["default", "fipa"]) + "\n" @@ -72,7 +72,7 @@ def setup_class(cls): os.chdir(Path(CUR_PATH, "data", "dummy_aea")) cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "list", "connections"]) - def test_correct_output(self, capsys): + def test_correct_output(self): """Test that the command has printed the correct output.""" assert self.result.output == "\n".join(["local"]) + "\n" @@ -98,7 +98,7 @@ def setup_class(cls): os.chdir(Path(CUR_PATH, "data", "dummy_aea")) cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "list", "skills"]) - def test_correct_output(self, capsys): + def test_correct_output(self): """Test that the command has printed the correct output.""" assert self.result.output == "\n".join(["dummy", "error"]) + "\n" diff --git a/tests/test_cli/test_commands/test_search.py b/tests/test_cli/test_commands/test_search.py new file mode 100644 index 0000000000..d35af74203 --- /dev/null +++ b/tests/test_cli/test_commands/test_search.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the `aea search` sub-command.""" +import json +import os +from pathlib import Path + +import jsonschema +from click.testing import CliRunner +from jsonschema import Draft4Validator + +from aea import AEA_DIR +from aea.cli import cli +from aea.cli.common import DEFAULT_REGISTRY_PATH +from ...conftest import AGENT_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR, CLI_LOG_OPTION, CUR_PATH + + +class TestSearchProtocols: + """Test that the command 'aea search protocols' works as expected.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.schema = json.load(open(AGENT_CONFIGURATION_SCHEMA)) + cls.resolver = jsonschema.RefResolver("file://{}/".format(Path(CONFIGURATION_SCHEMA_DIR).absolute()), cls.schema) + cls.validator = Draft4Validator(cls.schema, resolver=cls.resolver) + + cls.cwd = os.getcwd() + cls.runner = CliRunner() + + def test_correct_output_default_registry(self): + """Test that the command has printed the correct output when using the default registry.""" + os.chdir(AEA_DIR) + self.result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "search", "protocols"]) + expected_output = "Available protocols:\n- " + "\n- ".join(["default", "fipa", "gym", "oef", "tac"]) + "\n" + assert self.result.output == expected_output + + def test_correct_output_custom_registry(self): + """Test that the command has printed the correct output when using a custom registry.""" + os.chdir(AEA_DIR) + self.result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "search", "--registry", DEFAULT_REGISTRY_PATH, "protocols"]) + expected_output = "Available protocols:\n- " + "\n- ".join(["default", "fipa", "gym", "oef", "tac"]) + "\n" + assert self.result.output == expected_output + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + + +class TestSearchConnections: + """Test that the command 'aea search connections' works as expected.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.schema = json.load(open(AGENT_CONFIGURATION_SCHEMA)) + cls.resolver = jsonschema.RefResolver("file://{}/".format(Path(CONFIGURATION_SCHEMA_DIR).absolute()), cls.schema) + cls.validator = Draft4Validator(cls.schema, resolver=cls.resolver) + + cls.cwd = os.getcwd() + cls.runner = CliRunner() + + def test_correct_output_default_registry(self): + """Test that the command has printed the correct output when using the default registry.""" + os.chdir(AEA_DIR) + self.result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "search", "connections"]) + expected_output = "Available connections:\n- " + "\n- ".join(["gym", "local", "oef", "stub"]) + "\n" + assert self.result.output == expected_output + + def test_correct_output_custom_registry(self): + """Test that the command has printed the correct output when using a custom registry.""" + os.chdir(AEA_DIR) + self.result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "search", "--registry", DEFAULT_REGISTRY_PATH, "connections"]) + expected_output = "Available connections:\n- " + "\n- ".join(["gym", "local", "oef", "stub"]) + "\n" + assert self.result.output == expected_output + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + + +class TestSearchSkills: + """Test that the command 'aea search skills' works as expected.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.schema = json.load(open(AGENT_CONFIGURATION_SCHEMA)) + cls.resolver = jsonschema.RefResolver("file://{}/".format(Path(CONFIGURATION_SCHEMA_DIR).absolute()), cls.schema) + cls.validator = Draft4Validator(cls.schema, resolver=cls.resolver) + + cls.cwd = os.getcwd() + cls.runner = CliRunner() + + def test_correct_output_default_registry(self): + """Test that the command has printed the correct output when using the default registry.""" + os.chdir(AEA_DIR) + self.result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "search", "skills"]) + expected_output = """Available skills: +- echo +- error +- fipa_negotiation +- gym +- tac +- weather_client +- weather_station +""" + assert self.result.output == expected_output + + def test_correct_output_custom_registry(self): + """Test that the command has printed the correct output when using a custom registry.""" + os.chdir(AEA_DIR) + self.result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "search", "--registry", DEFAULT_REGISTRY_PATH, "skills"]) + expected_output = """Available skills: +- echo +- error +- fipa_negotiation +- gym +- tac +- weather_client +- weather_station +""" + assert self.result.output == expected_output + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) From d3591b0750e14de1627cd97bebe4db01f7b8260a Mon Sep 17 00:00:00 2001 From: Diarmid Campbell Date: Wed, 16 Oct 2019 17:10:29 +0100 Subject: [PATCH 42/71] error now alway shows at bottom of screen --- aea/cli_gui/static/css/home.css | 27 +++++++++++---------------- aea/cli_gui/templates/home.html | 2 +- aea/cli_gui/templates/home.js | 2 +- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/aea/cli_gui/static/css/home.css b/aea/cli_gui/static/css/home.css index bb27d4e055..91facdc8e1 100644 --- a/aea/cli_gui/static/css/home.css +++ b/aea/cli_gui/static/css/home.css @@ -21,13 +21,13 @@ table { width: 100% ; } -td { +.mainTable { vertical-align: top; width: 50%; } .editor { - width: 80%; + width: 90%; margin-left: auto; margin-right: auto; padding: 5px; @@ -49,13 +49,6 @@ button { background-color: #eee; } -.people { - width: 80%; - margin-left: auto; - margin-right: auto; - margin-bottom: 5px; -} - table { width: 100%; border-collapse: collapse; @@ -83,15 +76,17 @@ td { text-align: left; } -.error { - width: 80%; - margin-left: auto; - margin-right: auto; - padding: 5px; +.error{ + position: fixed; /* Sit on top of the page content */ + width: 100%; /* Full width (cover the whole page) */ + height: 10%; /* A bit of the page */ + bottom: 0; + visibility: hidden; border: 1px solid lightgrey; border-radius: 3px; - background-color: #fbb; - visibility: hidden; + background-color: #fbbd; + text-align: center; + font-weight: bold; } diff --git a/aea/cli_gui/templates/home.html b/aea/cli_gui/templates/home.html index 5f2dfbb3f7..7efcf6f887 100644 --- a/aea/cli_gui/templates/home.html +++ b/aea/cli_gui/templates/home.html @@ -12,6 +12,7 @@ +

    Fetch.AI AEA CLI REST API

    Local {{htmlElements[i][1]}}sLocal's {{htmlElements[i][1]}}s
    {{htmlElements[i][1]}} id
    @@ -81,7 +82,6 @@

    Selected {{htmlElements[i][1]}}:
    -

    diff --git a/aea/cli_gui/templates/home.js b/aea/cli_gui/templates/home.js index 378c5dd050..a4bb0c9c50 100644 --- a/aea/cli_gui/templates/home.js +++ b/aea/cli_gui/templates/home.js @@ -314,7 +314,7 @@ class View{ error(error_msg) { $('.error') - .text(error_msg) + .html("
    " + error_msg) .css('visibility', 'visible'); setTimeout(function() { $('.error').css('visibility', 'hidden'); From 8a6cba4e7307d2acc719fda505fde04f04857e14 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Wed, 16 Oct 2019 18:08:43 +0200 Subject: [PATCH 43/71] fix test_gui Add a sleep call to let the server to be operative before testing. --- tests/test_cli/test_commands/test_gui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_cli/test_commands/test_gui.py b/tests/test_cli/test_commands/test_gui.py index b4dbd2177e..f33e729fe7 100644 --- a/tests/test_cli/test_commands/test_gui.py +++ b/tests/test_cli/test_commands/test_gui.py @@ -53,6 +53,7 @@ def setup_class(cls): cls.t = tempfile.mkdtemp() os.chdir(cls.t) cls.proc = subprocess.Popen(["aea", *CLI_LOG_OPTION, "gui"]) + time.sleep(3.0) def test_gui(self): """Test that the gui process has been spawned correctly.""" From 123dd81a8199a014be3c04b61113c73ecb020fef Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Wed, 16 Oct 2019 18:20:32 +0200 Subject: [PATCH 44/71] ignore coverage of exception handling in aea search. --- aea/cli/search.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/aea/cli/search.py b/aea/cli/search.py index 3976bc6ab2..1776fea071 100644 --- a/aea/cli/search.py +++ b/aea/cli/search.py @@ -24,11 +24,12 @@ import os from aea import AEA_DIR -from aea.cli.common import Context, pass_ctx, DEFAULT_REGISTRY_PATH +from aea.cli.common import Context, pass_ctx, DEFAULT_REGISTRY_PATH, logger @click.group() -@click.option("--registry", type=str, default=None, help="Path/URL to the registry.") +@click.option("--registry", type=click.Path(exists=True, file_okay=False, dir_okay=True, readable=True, resolve_path=True), + default=None, help="Path/URL to the registry.") @pass_ctx def search(ctx: Context, registry): """Search for components in the registry. @@ -39,7 +40,8 @@ def search(ctx: Context, registry): """ if registry is None: registry = os.path.join(AEA_DIR, DEFAULT_REGISTRY_PATH) - ctx.set_config("registry", registry) + logger.debug("Using registry {}".format(registry)) + ctx.set_config("registry", str(registry)) @search.command() @@ -54,7 +56,7 @@ def connections(ctx: Context): try: for r in Path(registry).glob("connections/[!_]*[!.py]/"): result.add(r.name) - except Exception: + except Exception: # pragma: no cover pass if "scaffold" in result: result.remove("scaffold") @@ -76,7 +78,7 @@ def protocols(ctx: Context): try: for r in Path(registry).glob("protocols/[!_]*[!.py]/"): result.add(r.name) - except Exception: + except Exception: # pragma: no cover pass if "scaffold" in result: result.remove("scaffold") @@ -98,7 +100,7 @@ def skills(ctx: Context): try: for r in Path(registry).glob("skills/[!_]*[!.py]/"): result.add(r.name) - except Exception: + except Exception: # pragma: no cover pass if "scaffold" in result: result.remove("scaffold") From b48f2830d103da4cdea9309c4bb100d1ecd2438d Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Wed, 16 Oct 2019 18:34:15 +0200 Subject: [PATCH 45/71] remove unused imports in test_gui and test_search --- tests/test_cli/test_commands/test_gui.py | 9 +-------- tests/test_cli/test_commands/test_search.py | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/tests/test_cli/test_commands/test_gui.py b/tests/test_cli/test_commands/test_gui.py index f33e729fe7..9de6949c32 100644 --- a/tests/test_cli/test_commands/test_gui.py +++ b/tests/test_cli/test_commands/test_gui.py @@ -20,22 +20,15 @@ """This test module contains the tests for the `aea gui` sub-command.""" import json import os -import shutil -import socket import subprocess -import sys import tempfile import time -from multiprocessing import Process from pathlib import Path import jsonschema -from click.testing import CliRunner from jsonschema import Draft4Validator -from aea.cli import cli -from aea.cli.__main__ import gui -from ...conftest import AGENT_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR, CLI_LOG_OPTION, CUR_PATH, tcpping +from ...conftest import AGENT_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR, CLI_LOG_OPTION, tcpping class TestGui: diff --git a/tests/test_cli/test_commands/test_search.py b/tests/test_cli/test_commands/test_search.py index d35af74203..e5c5a8f3d5 100644 --- a/tests/test_cli/test_commands/test_search.py +++ b/tests/test_cli/test_commands/test_search.py @@ -29,7 +29,7 @@ from aea import AEA_DIR from aea.cli import cli from aea.cli.common import DEFAULT_REGISTRY_PATH -from ...conftest import AGENT_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR, CLI_LOG_OPTION, CUR_PATH +from ...conftest import AGENT_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR, CLI_LOG_OPTION class TestSearchProtocols: From fc5cddafb255cee8b2ddfa2648306b98b870209d Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Wed, 16 Oct 2019 19:57:47 +0200 Subject: [PATCH 46/71] add tests to 'aea scaffold * *' --- aea/cli/scaffold.py | 59 ++-- .../test_commands/test_scaffold/__init__.py | 20 ++ .../test_scaffold/test_connection.py | 293 +++++++++++++++++ .../test_scaffold/test_protocols.py | 299 +++++++++++++++++ .../test_scaffold/test_skills.py | 311 ++++++++++++++++++ 5 files changed, 945 insertions(+), 37 deletions(-) create mode 100644 tests/test_cli/test_commands/test_scaffold/__init__.py create mode 100644 tests/test_cli/test_commands/test_scaffold/test_connection.py create mode 100644 tests/test_cli/test_commands/test_scaffold/test_protocols.py create mode 100644 tests/test_cli/test_commands/test_scaffold/test_skills.py diff --git a/aea/cli/scaffold.py b/aea/cli/scaffold.py index 1c4d57716c..18c0e6975e 100644 --- a/aea/cli/scaffold.py +++ b/aea/cli/scaffold.py @@ -48,12 +48,10 @@ def connection(ctx: Context, connection_name: str) -> None: if connection_name in ctx.agent_config.connections: logger.error("A connection with name '{}' already exists. Aborting...".format(connection_name)) exit(-1) - return try: - # create the 'connections' folder if it doesn't exist: - if not os.path.exists("connections"): - os.makedirs("connections") + + Path("connections").mkdir(exist_ok=True) # create the connection folder dest = Path(os.path.join("connections", connection_name)) @@ -61,27 +59,24 @@ def connection(ctx: Context, connection_name: str) -> None: # copy the skill package into the agent's supported skills. src = Path(os.path.join(AEA_DIR, "connections", "scaffold")) logger.info("Copying connection modules. src={} dst={}".format(src, dest)) - try: - shutil.copytree(src, dest) - except Exception as e: - logger.error(e) - exit(-1) + + shutil.copytree(src, dest) # add the connection to the configurations. logger.info("Registering the connection into {}".format(DEFAULT_AEA_CONFIG_FILE)) ctx.agent_config.connections.add(connection_name) ctx.agent_loader.dump(ctx.agent_config, open(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w")) - except OSError: + except FileExistsError: logger.error("Directory already exist. Aborting...") exit(-1) except ValidationError as e: - logger.error(str(e)) - shutil.rmtree(connection_name, ignore_errors=True) + logger.error("Error when validating the skill configuration file.") + shutil.rmtree(os.path.join("connections", connection_name), ignore_errors=True) exit(-1) except Exception as e: logger.exception(e) - shutil.rmtree(connection_name, ignore_errors=True) + shutil.rmtree(os.path.join("connections", connection_name), ignore_errors=True) exit(-1) @@ -95,12 +90,10 @@ def protocol(ctx: Context, protocol_name: str): if protocol_name in ctx.agent_config.protocols: logger.error("A protocol with name '{}' already exists. Aborting...".format(protocol_name)) exit(-1) - return try: # create the 'protocols' folder if it doesn't exist: - if not os.path.exists("protocols"): - os.makedirs("protocols") + Path("protocols").mkdir(exist_ok=True) # create the protocol folder dest = Path(os.path.join("protocols", protocol_name)) @@ -108,27 +101,24 @@ def protocol(ctx: Context, protocol_name: str): # copy the skill package into the agent's supported skills. src = Path(os.path.join(AEA_DIR, "protocols", "scaffold")) logger.info("Copying protocol modules. src={} dst={}".format(src, dest)) - try: - shutil.copytree(src, dest) - except Exception as e: - logger.error(e) - exit(-1) + + shutil.copytree(src, dest) # add the protocol to the configurations. logger.info("Registering the protocol into {}".format(DEFAULT_AEA_CONFIG_FILE)) ctx.agent_config.protocols.add(protocol_name) ctx.agent_loader.dump(ctx.agent_config, open(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w")) - except OSError: + except FileExistsError: logger.error("Directory already exist. Aborting...") exit(-1) except ValidationError as e: - logger.error(str(e)) - shutil.rmtree(protocol_name, ignore_errors=True) + logger.error("Error when validating the skill configuration file.") + shutil.rmtree(os.path.join("protocols", protocol_name), ignore_errors=True) exit(-1) except Exception as e: logger.exception(e) - shutil.rmtree(protocol_name, ignore_errors=True) + shutil.rmtree(os.path.join("protocols", protocol_name), ignore_errors=True) exit(-1) @@ -142,12 +132,10 @@ def skill(ctx: Context, skill_name: str): if skill_name in ctx.agent_config.skills: logger.error("A skill with name '{}' already exists. Aborting...".format(skill_name)) exit(-1) - return try: # create the 'skills' folder if it doesn't exist: - if not os.path.exists("skills"): - os.makedirs("skills") + Path("skills").mkdir(exist_ok=True) # create the skill folder dest = Path(os.path.join("skills", skill_name)) @@ -155,25 +143,22 @@ def skill(ctx: Context, skill_name: str): # copy the skill package into the agent's supported skills. src = Path(os.path.join(AEA_DIR, "skills", "scaffold")) logger.info("Copying skill modules. src={} dst={}".format(src, dest)) - try: - shutil.copytree(src, dest) - except Exception as e: - logger.error(e) - exit(-1) + + shutil.copytree(src, dest) # add the skill to the configurations. logger.info("Registering the protocol into {}".format(DEFAULT_AEA_CONFIG_FILE)) ctx.agent_config.skills.add(skill_name) ctx.agent_loader.dump(ctx.agent_config, open(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w")) - except OSError: + except FileExistsError: logger.error("Directory already exist. Aborting...") exit(-1) except ValidationError as e: - logger.error(str(e)) - shutil.rmtree(skill_name, ignore_errors=True) + logger.error("Error when validating the skill configuration file.") + shutil.rmtree(os.path.join("skills", skill_name), ignore_errors=True) exit(-1) except Exception as e: logger.exception(e) - shutil.rmtree(skill_name, ignore_errors=True) + shutil.rmtree(os.path.join("skills", skill_name), ignore_errors=True) exit(-1) diff --git a/tests/test_cli/test_commands/test_scaffold/__init__.py b/tests/test_cli/test_commands/test_scaffold/__init__.py new file mode 100644 index 0000000000..ea1013e183 --- /dev/null +++ b/tests/test_cli/test_commands/test_scaffold/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the `aea scaffold` sub-command.""" diff --git a/tests/test_cli/test_commands/test_scaffold/test_connection.py b/tests/test_cli/test_commands/test_scaffold/test_connection.py new file mode 100644 index 0000000000..d1ec8a27fb --- /dev/null +++ b/tests/test_cli/test_commands/test_scaffold/test_connection.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the `aea scaffold connection` sub-command.""" +import filecmp +import json +import os +import shutil +import tempfile +import unittest.mock +from pathlib import Path + +import jsonschema +import yaml +from click.testing import CliRunner +from jsonschema import ValidationError, Draft4Validator + +from aea import AEA_DIR +import aea.cli.common +import aea.configurations.base +from aea.configurations.base import DEFAULT_CONNECTION_CONFIG_FILE +from aea.cli import cli +from tests.conftest import CLI_LOG_OPTION, CONNECTION_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR + + +class TestScaffoldConnection: + """Test that the command 'aea scaffold connection' works correctly in correct preconditions.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.resource_name = "myresource" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + cls.schema = json.load(open(CONNECTION_CONFIGURATION_SCHEMA)) + cls.resolver = jsonschema.RefResolver("file://{}/".format(Path(CONFIGURATION_SCHEMA_DIR).absolute()), cls.schema) + cls.validator = Draft4Validator(cls.schema, resolver=cls.resolver) + + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + # scaffold connection + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "scaffold", "connection", cls.resource_name]) + + def test_exit_code_equal_to_0(self): + """Test that the exit code is equal to 0.""" + assert self.result.exit_code == 0 + + def test_resource_folder_contains_module_connection(self): + """Test that the resource folder contains scaffold connection.py module.""" + p = Path(self.t, self.agent_name, "connections", self.resource_name, "connection.py") + original = Path(AEA_DIR, "connections", "scaffold", "connection.py") + assert filecmp.cmp(p, original) + + def test_resource_folder_contains_configuration_file(self): + """Test that the resource folder contains a good configuration file.""" + p = Path(self.t, self.agent_name, "connections", self.resource_name, DEFAULT_CONNECTION_CONFIG_FILE) + config_file = yaml.safe_load(open(p)) + self.validator.validate(instance=config_file) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestScaffoldConnectionFailsWhenDirectoryAlreadyExists: + """Test that the command 'aea scaffold connection' fails when a folder with 'scaffold' name already.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.resource_name = "myresource" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + # create a dummy 'myresource' folder + Path(cls.t, cls.agent_name, "connections", cls.resource_name).mkdir(exist_ok=False, parents=True) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "scaffold", "connection", cls.resource_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_error_message_connection_already_existing(self): + """Test that the log error message is fixed. + + The expected message is: 'A connection with name '{connection_name}' already exists. Aborting...' + """ + s = "Directory already exist. Aborting..." + self.mocked_logger_error.assert_called_once_with(s) + + def test_resource_directory_exists(self): + """Test that the resource directory still exists. + + This means that after every failure, we make sure we restore the previous state. + """ + assert Path(self.t, self.agent_name, "connections", self.resource_name).exists() + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestScaffoldConnectionFailsWhenConnectionAlreadyExists: + """Test that the command 'aea add connection' fails when the connection already exists.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.resource_name = "myresource" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + # add connection first time + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "scaffold", "connection", cls.resource_name]) + assert result.exit_code == 0 + # scaffold connection with the same connection name + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "scaffold", "connection", cls.resource_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_error_message_connection_already_existing(self): + """Test that the log error message is fixed. + + The expected message is: 'A connection with name '{connection_name}' already exists. Aborting...' + """ + s = "A connection with name '{}' already exists. Aborting...".format(self.resource_name) + self.mocked_logger_error.assert_called_once_with(s) + + def test_resource_directory_exists(self): + """Test that the resource directory still exists. + + This means that after every failure, we make sure we restore the previous state. + """ + assert Path(self.t, self.agent_name, "connections", self.resource_name).exists() + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestScaffoldConnectionFailsWhenConfigFileIsNotCompliant: + """Test that the command 'aea scaffold connection' fails when the configuration file is not compliant with the schema.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.resource_name = "myresource" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + + # change the dumping of yaml module to raise an exception. + cls.patch = unittest.mock.patch("yaml.safe_dump", side_effect=ValidationError("test error message")) + cls.patch.__enter__() + + os.chdir(cls.agent_name) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "scaffold", "connection", cls.resource_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_configuration_file_not_valid(self): + """Test that the log error message is fixed. + + The expected message is: 'Cannot find connection: '{connection_name}'' + """ + self.mocked_logger_error.assert_called_once_with("Error when validating the skill configuration file.") + + def test_resource_directory_does_not_exists(self): + """Test that the resource directory does not exist. + + This means that after every failure, we make sure we restore the previous state. + """ + assert not Path(self.t, self.agent_name, "connections", self.resource_name).exists() + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestScaffoldConnectionFailsWhenExceptionOccurs: + """Test that the command 'aea scaffold connection' fails when the configuration file is not compliant with the schema.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.resource_name = "myresource" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + + cls.patch = unittest.mock.patch("shutil.copytree", side_effect=Exception("unknwon exception")) + cls.patch.__enter__() + + os.chdir(cls.agent_name) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "scaffold", "connection", cls.resource_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_resource_directory_does_not_exists(self): + """Test that the resource directory does not exist. + + This means that after every failure, we make sure we restore the previous state. + """ + assert not Path(self.t, self.agent_name, "connections", self.resource_name).exists() + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + diff --git a/tests/test_cli/test_commands/test_scaffold/test_protocols.py b/tests/test_cli/test_commands/test_scaffold/test_protocols.py new file mode 100644 index 0000000000..0fae9c4251 --- /dev/null +++ b/tests/test_cli/test_commands/test_scaffold/test_protocols.py @@ -0,0 +1,299 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the `aea scaffold protocol` sub-command.""" +import filecmp +import json +import os +import shutil +import tempfile +import unittest.mock +from pathlib import Path + +import jsonschema +import yaml +from click.testing import CliRunner +from jsonschema import ValidationError, Draft4Validator + +from aea import AEA_DIR +import aea.cli.common +import aea.configurations.base +from aea.configurations.base import DEFAULT_PROTOCOL_CONFIG_FILE +from aea.cli import cli +from tests.conftest import CLI_LOG_OPTION, PROTOCOL_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR + + +class TestScaffoldProtocol: + """Test that the command 'aea scaffold protocol' works correctly in correct preconditions.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.resource_name = "myresource" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + cls.schema = json.load(open(PROTOCOL_CONFIGURATION_SCHEMA)) + cls.resolver = jsonschema.RefResolver("file://{}/".format(Path(CONFIGURATION_SCHEMA_DIR).absolute()), cls.schema) + cls.validator = Draft4Validator(cls.schema, resolver=cls.resolver) + + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + # scaffold protocol + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "scaffold", "protocol", cls.resource_name]) + + def test_exit_code_equal_to_0(self): + """Test that the exit code is equal to 0.""" + assert self.result.exit_code == 0 + + def test_resource_folder_contains_module_message(self): + """Test that the resource folder contains scaffold message.py module.""" + p = Path(self.t, self.agent_name, "protocols", self.resource_name, "message.py") + original = Path(AEA_DIR, "protocols", "scaffold", "message.py") + assert filecmp.cmp(p, original) + + def test_resource_folder_contains_module_protocol(self): + """Test that the resource folder contains scaffold protocol.py module.""" + p = Path(self.t, self.agent_name, "protocols", self.resource_name, "serialization.py") + original = Path(AEA_DIR, "protocols", "scaffold", "serialization.py") + assert filecmp.cmp(p, original) + + def test_resource_folder_contains_configuration_file(self): + """Test that the resource folder contains a good configuration file.""" + p = Path(self.t, self.agent_name, "protocols", self.resource_name, DEFAULT_PROTOCOL_CONFIG_FILE) + config_file = yaml.safe_load(open(p)) + self.validator.validate(instance=config_file) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestScaffoldProtocolFailsWhenDirectoryAlreadyExists: + """Test that the command 'aea scaffold protocol' fails when a folder with 'scaffold' name already.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.resource_name = "myresource" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + # create a dummy 'myresource' folder + Path(cls.t, cls.agent_name, "protocols", cls.resource_name).mkdir(exist_ok=False, parents=True) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "scaffold", "protocol", cls.resource_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_error_message_protocol_already_existing(self): + """Test that the log error message is fixed. + + The expected message is: 'A protocol with name '{protocol_name}' already exists. Aborting...' + """ + s = "Directory already exist. Aborting..." + self.mocked_logger_error.assert_called_once_with(s) + + def test_resource_directory_exists(self): + """Test that the resource directory still exists. + + This means that after every failure, we make sure we restore the previous state. + """ + assert Path(self.t, self.agent_name, "protocols", self.resource_name).exists() + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestScaffoldProtocolFailsWhenProtocolAlreadyExists: + """Test that the command 'aea add protocol' fails when the protocol already exists.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.resource_name = "myresource" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + # add protocol first time + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "scaffold", "protocol", cls.resource_name]) + assert result.exit_code == 0 + # scaffold protocol with the same protocol name + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "scaffold", "protocol", cls.resource_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_error_message_protocol_already_existing(self): + """Test that the log error message is fixed. + + The expected message is: 'A protocol with name '{protocol_name}' already exists. Aborting...' + """ + s = "A protocol with name '{}' already exists. Aborting...".format(self.resource_name) + self.mocked_logger_error.assert_called_once_with(s) + + def test_resource_directory_exists(self): + """Test that the resource directory still exists. + + This means that after every failure, we make sure we restore the previous state. + """ + assert Path(self.t, self.agent_name, "protocols", self.resource_name).exists() + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestScaffoldProtocolFailsWhenConfigFileIsNotCompliant: + """Test that the command 'aea scaffold protocol' fails when the configuration file is not compliant with the schema.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.resource_name = "myresource" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + + # change the dumping of yaml module to raise an exception. + cls.patch = unittest.mock.patch("yaml.safe_dump", side_effect=ValidationError("test error message")) + cls.patch.__enter__() + + os.chdir(cls.agent_name) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "scaffold", "protocol", cls.resource_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_configuration_file_not_valid(self): + """Test that the log error message is fixed. + + The expected message is: 'Cannot find protocol: '{protocol_name}' + """ + self.mocked_logger_error.assert_called_once_with("Error when validating the skill configuration file.") + + def test_resource_directory_does_not_exists(self): + """Test that the resource directory does not exist. + + This means that after every failure, we make sure we restore the previous state. + """ + assert not Path(self.t, self.agent_name, "protocols", self.resource_name).exists() + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestScaffoldProtocolFailsWhenExceptionOccurs: + """Test that the command 'aea scaffold protocol' fails when the configuration file is not compliant with the schema.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.resource_name = "myresource" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + + cls.patch = unittest.mock.patch("shutil.copytree", side_effect=Exception("unknwon exception")) + cls.patch.__enter__() + + os.chdir(cls.agent_name) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "scaffold", "protocol", cls.resource_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_resource_directory_does_not_exists(self): + """Test that the resource directory does not exist. + + This means that after every failure, we make sure we restore the previous state. + """ + assert not Path(self.t, self.agent_name, "protocols", self.resource_name).exists() + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + diff --git a/tests/test_cli/test_commands/test_scaffold/test_skills.py b/tests/test_cli/test_commands/test_scaffold/test_skills.py new file mode 100644 index 0000000000..991dcfc7bf --- /dev/null +++ b/tests/test_cli/test_commands/test_scaffold/test_skills.py @@ -0,0 +1,311 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the `aea scaffold skill` sub-command.""" +import filecmp +import json +import os +import shutil +import tempfile +import unittest.mock +from pathlib import Path + +import jsonschema +import yaml +from click.testing import CliRunner +from jsonschema import ValidationError, Draft4Validator + +from aea import AEA_DIR +import aea.cli.common +import aea.configurations.base +from aea.configurations.base import DEFAULT_SKILL_CONFIG_FILE +from aea.cli import cli +from tests.conftest import CLI_LOG_OPTION, SKILL_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR + + +class TestScaffoldSkill: + """Test that the command 'aea scaffold skill' works correctly in correct preconditions.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.resource_name = "myresource" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + cls.schema = json.load(open(SKILL_CONFIGURATION_SCHEMA)) + cls.resolver = jsonschema.RefResolver("file://{}/".format(Path(CONFIGURATION_SCHEMA_DIR).absolute()), cls.schema) + cls.validator = Draft4Validator(cls.schema, resolver=cls.resolver) + + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + # scaffold skill + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "scaffold", "skill", cls.resource_name]) + + def test_exit_code_equal_to_0(self): + """Test that the exit code is equal to 0.""" + assert self.result.exit_code == 0 + + def test_resource_folder_contains_module_handlers(self): + """Test that the resource folder contains scaffold handlers.py module.""" + p = Path(self.t, self.agent_name, "skills", self.resource_name, "handlers.py") + original = Path(AEA_DIR, "skills", "scaffold", "handlers.py") + assert filecmp.cmp(p, original) + + def test_resource_folder_contains_module_behaviours(self): + """Test that the resource folder contains scaffold behaviours.py module.""" + p = Path(self.t, self.agent_name, "skills", self.resource_name, "behaviours.py") + original = Path(AEA_DIR, "skills", "scaffold", "behaviours.py") + assert filecmp.cmp(p, original) + + def test_resource_folder_contains_module_tasks(self): + """Test that the resource folder contains scaffold tasks.py module.""" + p = Path(self.t, self.agent_name, "skills", self.resource_name, "tasks.py") + original = Path(AEA_DIR, "skills", "scaffold", "tasks.py") + assert filecmp.cmp(p, original) + + def test_resource_folder_contains_module_shared_class(self): + """Test that the resource folder contains scaffold my_shared_class.py.py module.""" + p = Path(self.t, self.agent_name, "skills", self.resource_name, "my_shared_class.py") + original = Path(AEA_DIR, "skills", "scaffold", "my_shared_class.py") + assert filecmp.cmp(p, original) + + def test_resource_folder_contains_configuration_file(self): + """Test that the resource folder contains a good configuration file.""" + p = Path(self.t, self.agent_name, "skills", self.resource_name, DEFAULT_SKILL_CONFIG_FILE) + config_file = yaml.safe_load(open(p)) + self.validator.validate(instance=config_file) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestScaffoldSkillFailsWhenDirectoryAlreadyExists: + """Test that the command 'aea scaffold skill' fails when a folder with 'scaffold' name already.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.resource_name = "myresource" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + # create a dummy 'myresource' folder + Path(cls.t, cls.agent_name, "skills", cls.resource_name).mkdir(exist_ok=False, parents=True) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "scaffold", "skill", cls.resource_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_error_message_skill_already_existing(self): + """Test that the log error message is fixed. + + The expected message is: 'A skill with name '{skill_name}' already exists. Aborting...' + """ + s = "Directory already exist. Aborting..." + self.mocked_logger_error.assert_called_once_with(s) + + def test_resource_directory_exists(self): + """Test that the resource directory still exists. + + This means that after every failure, we make sure we restore the previous state. + """ + assert Path(self.t, self.agent_name, "skills", self.resource_name).exists() + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestScaffoldSkillFailsWhenSkillAlreadyExists: + """Test that the command 'aea add skill' fails when the skill already exists.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.resource_name = "myresource" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + # add skill first time + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "scaffold", "skill", cls.resource_name]) + assert result.exit_code == 0 + # scaffold skill with the same skill name + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "scaffold", "skill", cls.resource_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_error_message_skill_already_existing(self): + """Test that the log error message is fixed. + + The expected message is: 'A skill with name '{skill_name}' already exists. Aborting...' + """ + s = "A skill with name '{}' already exists. Aborting...".format(self.resource_name) + self.mocked_logger_error.assert_called_once_with(s) + + def test_resource_directory_exists(self): + """Test that the resource directory still exists. + + This means that after every failure, we make sure we restore the previous state. + """ + assert Path(self.t, self.agent_name, "skills", self.resource_name).exists() + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestScaffoldSkillFailsWhenConfigFileIsNotCompliant: + """Test that the command 'aea scaffold skill' fails when the configuration file is not compliant with the schema.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.resource_name = "myresource" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + + # change the dumping of yaml module to raise an exception. + cls.patch = unittest.mock.patch("yaml.safe_dump", side_effect=ValidationError("test error message")) + cls.patch.__enter__() + + os.chdir(cls.agent_name) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "scaffold", "skill", cls.resource_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_configuration_file_not_valid(self): + """Test that the log error message is fixed. + + The expected message is: 'Cannot find skill: '{skill_name}' + """ + self.mocked_logger_error.assert_called_once_with("Error when validating the skill configuration file.") + + def test_resource_directory_does_not_exists(self): + """Test that the resource directory does not exist. + + This means that after every failure, we make sure we restore the previous state. + """ + assert not Path(self.t, self.agent_name, "skills", self.resource_name).exists() + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestScaffoldSkillFailsWhenExceptionOccurs: + """Test that the command 'aea scaffold skill' fails when the configuration file is not compliant with the schema.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.resource_name = "myresource" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + + cls.patch = unittest.mock.patch("shutil.copytree", side_effect=Exception("unknwon exception")) + cls.patch.__enter__() + + os.chdir(cls.agent_name) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "scaffold", "skill", cls.resource_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_resource_directory_does_not_exists(self): + """Test that the resource directory does not exist. + + This means that after every failure, we make sure we restore the previous state. + """ + assert not Path(self.t, self.agent_name, "skills", self.resource_name).exists() + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + From 41ecaa0829e6e6a771eba02ba2f02124eb0f1323 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Wed, 16 Oct 2019 20:14:44 +0200 Subject: [PATCH 47/71] exclude skills/ subfolders with name that don't match a Python package name. This is needed because otherwise temporary folders like __pycache__ or .DS_Store are attempted to be parsed. --- aea/cli/common.py | 2 +- aea/registries/base.py | 3 ++- tests/conftest.py | 3 ++- tests/test_cli/test_commands/test_gui.py | 8 ++++++-- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/aea/cli/common.py b/aea/cli/common.py index 8902361188..79ad7a04ee 100644 --- a/aea/cli/common.py +++ b/aea/cli/common.py @@ -106,7 +106,7 @@ def _try_to_load_agent_config(ctx: Context): except FileNotFoundError: logger.error("Agent configuration file '{}' not found in the current directory.".format(DEFAULT_AEA_CONFIG_FILE)) exit(-1) - except jsonschema.exceptions.ValidationError as e: + except jsonschema.exceptions.ValidationError: logger.error("Agent configuration file '{}' is invalid. Please check the documentation.".format(DEFAULT_AEA_CONFIG_FILE)) exit(-1) diff --git a/aea/registries/base.py b/aea/registries/base.py index 9e61a3ce5d..c3da730473 100644 --- a/aea/registries/base.py +++ b/aea/registries/base.py @@ -479,7 +479,8 @@ def populate_skills(self, directory: str, agent_context: AgentContext) -> None: logger.warning("No skill found.") return - skill_directories = [str(x) for x in Path(root_skill_directory).iterdir() if x.is_dir()] + skill_directories = [str(x) for x in Path(root_skill_directory).iterdir() + if x.is_dir() and re.match(PACKAGE_NAME_REGEX, x.name)] logger.debug("Processing the following skill directories: {}".format(pprint.pformat(skill_directories))) for skill_directory in skill_directories: try: diff --git a/tests/conftest.py b/tests/conftest.py index 9ad2005969..754e00c5d3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -70,7 +70,8 @@ def tcpping(ip, port) -> bool: s.connect((ip, int(port))) s.shutdown(2) return True - except: + except Exception as e: + logger.exception(e) return False diff --git a/tests/test_cli/test_commands/test_gui.py b/tests/test_cli/test_commands/test_gui.py index 9de6949c32..cdaa3b1703 100644 --- a/tests/test_cli/test_commands/test_gui.py +++ b/tests/test_cli/test_commands/test_gui.py @@ -26,6 +26,7 @@ from pathlib import Path import jsonschema +import pytest from jsonschema import Draft4Validator from ...conftest import AGENT_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR, CLI_LOG_OPTION, tcpping @@ -48,9 +49,12 @@ def setup_class(cls): cls.proc = subprocess.Popen(["aea", *CLI_LOG_OPTION, "gui"]) time.sleep(3.0) - def test_gui(self): + def test_gui(self,pytestconfig): """Test that the gui process has been spawned correctly.""" - assert tcpping("localhost", 8080) + if pytestconfig.getoption("ci"): + pytest.skip('skipped: CI') + else: + assert tcpping("localhost", 8080) @classmethod def teardown_class(cls): From b5157081c02dc34a2127a661102db3f28e413646 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Wed, 16 Oct 2019 23:07:26 +0200 Subject: [PATCH 48/71] add tests for 'aea install' --- aea/cli/install.py | 3 +- aea/cli/loggers.py | 2 +- tests/test_cli/test_commands/test_freeze.py | 4 + tests/test_cli/test_commands/test_install.py | 92 ++++++++++++++++++++ tests/test_cli/test_commands/test_list.py | 12 +++ 5 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 tests/test_cli/test_commands/test_install.py diff --git a/aea/cli/install.py b/aea/cli/install.py index 0088930f98..24f43fa65a 100644 --- a/aea/cli/install.py +++ b/aea/cli/install.py @@ -44,10 +44,11 @@ def install(ctx: Context, requirement: Optional[str]): dependencies = ctx.get_dependencies() for d in dependencies: - logger.debug("Installing {}...".format(d)) + logger.info("Installing {}...".format(d)) try: subp = subprocess.Popen([sys.executable, "-m", "pip", "install", d]) subp.wait(30.0) + assert subp.returncode == 0 except Exception: logger.error("An error occurred while installing {}. Stopping...".format(d)) exit(-1) diff --git a/aea/cli/loggers.py b/aea/cli/loggers.py index fe6d6f81e3..32beb461ca 100644 --- a/aea/cli/loggers.py +++ b/aea/cli/loggers.py @@ -51,7 +51,7 @@ def format(self, record): **self.colors[level]) msg = '\n'.join(prefix + x for x in msg.splitlines()) return msg - return logging.Formatter.format(self, record) + return logging.Formatter.format(self, record) # pragma: no cover def simple_verbosity_option(logger=None, *names, **kwargs): diff --git a/tests/test_cli/test_commands/test_freeze.py b/tests/test_cli/test_commands/test_freeze.py index 504d1dcc64..0ebf6cba86 100644 --- a/tests/test_cli/test_commands/test_freeze.py +++ b/tests/test_cli/test_commands/test_freeze.py @@ -46,6 +46,10 @@ def setup_class(cls): os.chdir(Path(CUR_PATH, "data", "dummy_aea")) cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "freeze"]) + def test_exit_code_equal_to_zero(self): + """Assert that the exit code is equal to zero (i.e. success).""" + assert self.result.exit_code == 0 + def test_correct_output(self): """Test that the command has printed the correct output.""" assert self.result.output == """protobuf\n""" diff --git a/tests/test_cli/test_commands/test_install.py b/tests/test_cli/test_commands/test_install.py new file mode 100644 index 0000000000..169af7d6c9 --- /dev/null +++ b/tests/test_cli/test_commands/test_install.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the `aea install` sub-command.""" +import os +import shutil +import tempfile +from pathlib import Path + +import unittest.mock + +import yaml +from click.testing import CliRunner + +import aea.cli.common +from aea.cli import cli +from ...conftest import CLI_LOG_OPTION, CUR_PATH + +class TestInstall: + """Test that the command 'aea install' works as expected.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + os.chdir(Path(CUR_PATH, "data", "dummy_aea")) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "install"]) + + def test_exit_code_equal_to_zero(self): + """Assert that the exit code is equal to zero (i.e. success).""" + assert self.result.exit_code == 0 + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + + +class TestInstallFails: + """Test that the command 'aea install' fails when a dependency is not found.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "scaffold", "protocol", "my_protocol"]) + assert result.exit_code == 0 + + config_path = Path("protocols", "my_protocol", "protocol.yaml") + config = yaml.safe_load(open(config_path)) + config.setdefault("dependencies", []).append("this_dependency_does_not_exist") + yaml.safe_dump(config, open(config_path, "w")) + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "install"]) + + def test_exit_code_equal_to_minus_1(self): + """Assert that the exit code is equal to -1 (i.e. failure).""" + assert self.result.exit_code == -1 + + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) diff --git a/tests/test_cli/test_commands/test_list.py b/tests/test_cli/test_commands/test_list.py index 4f47e71547..f88e244287 100644 --- a/tests/test_cli/test_commands/test_list.py +++ b/tests/test_cli/test_commands/test_list.py @@ -46,6 +46,10 @@ def setup_class(cls): os.chdir(Path(CUR_PATH, "data", "dummy_aea")) cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "list", "protocols"]) + def test_exit_code_equal_to_zero(self): + """Assert that the exit code is equal to zero (i.e. success).""" + assert self.result.exit_code == 0 + def test_correct_output(self): """Test that the command has printed the correct output.""" assert self.result.output == "\n".join(["default", "fipa"]) + "\n" @@ -72,6 +76,10 @@ def setup_class(cls): os.chdir(Path(CUR_PATH, "data", "dummy_aea")) cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "list", "connections"]) + def test_exit_code_equal_to_zero(self): + """Assert that the exit code is equal to zero (i.e. success).""" + assert self.result.exit_code == 0 + def test_correct_output(self): """Test that the command has printed the correct output.""" assert self.result.output == "\n".join(["local"]) + "\n" @@ -98,6 +106,10 @@ def setup_class(cls): os.chdir(Path(CUR_PATH, "data", "dummy_aea")) cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "list", "skills"]) + def test_exit_code_equal_to_zero(self): + """Assert that the exit code is equal to zero (i.e. success).""" + assert self.result.exit_code == 0 + def test_correct_output(self): """Test that the command has printed the correct output.""" assert self.result.output == "\n".join(["dummy", "error"]) + "\n" From 14ef35f116c56d045396603c89953bf85b1e1895 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Wed, 16 Oct 2019 23:12:22 +0200 Subject: [PATCH 49/71] minor fixes to the tests. - add '--ci' flag to Jenkinsfile test command - other minor code style errors --- Jenkinsfile | 2 +- aea/cli/scaffold.py | 6 +++--- tests/test_cli/test_commands/test_gui.py | 2 +- .../test_cli/test_commands/test_scaffold/test_connection.py | 1 - .../test_cli/test_commands/test_scaffold/test_protocols.py | 1 - tests/test_cli/test_commands/test_scaffold/test_skills.py | 1 - 6 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index dc5b88ac4b..a730d19161 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -48,7 +48,7 @@ pipeline { stage('Unit Tests: Python 3.7') { steps { - sh 'tox -e py37 -- --no-integration-tests' + sh 'tox -e py37 -- --no-integration-tests --ci' } } // unit tests: python 3.7 diff --git a/aea/cli/scaffold.py b/aea/cli/scaffold.py index 18c0e6975e..a1e065dae8 100644 --- a/aea/cli/scaffold.py +++ b/aea/cli/scaffold.py @@ -70,7 +70,7 @@ def connection(ctx: Context, connection_name: str) -> None: except FileExistsError: logger.error("Directory already exist. Aborting...") exit(-1) - except ValidationError as e: + except ValidationError: logger.error("Error when validating the skill configuration file.") shutil.rmtree(os.path.join("connections", connection_name), ignore_errors=True) exit(-1) @@ -112,7 +112,7 @@ def protocol(ctx: Context, protocol_name: str): except FileExistsError: logger.error("Directory already exist. Aborting...") exit(-1) - except ValidationError as e: + except ValidationError: logger.error("Error when validating the skill configuration file.") shutil.rmtree(os.path.join("protocols", protocol_name), ignore_errors=True) exit(-1) @@ -154,7 +154,7 @@ def skill(ctx: Context, skill_name: str): except FileExistsError: logger.error("Directory already exist. Aborting...") exit(-1) - except ValidationError as e: + except ValidationError: logger.error("Error when validating the skill configuration file.") shutil.rmtree(os.path.join("skills", skill_name), ignore_errors=True) exit(-1) diff --git a/tests/test_cli/test_commands/test_gui.py b/tests/test_cli/test_commands/test_gui.py index cdaa3b1703..9507302ab8 100644 --- a/tests/test_cli/test_commands/test_gui.py +++ b/tests/test_cli/test_commands/test_gui.py @@ -49,7 +49,7 @@ def setup_class(cls): cls.proc = subprocess.Popen(["aea", *CLI_LOG_OPTION, "gui"]) time.sleep(3.0) - def test_gui(self,pytestconfig): + def test_gui(self, pytestconfig): """Test that the gui process has been spawned correctly.""" if pytestconfig.getoption("ci"): pytest.skip('skipped: CI') diff --git a/tests/test_cli/test_commands/test_scaffold/test_connection.py b/tests/test_cli/test_commands/test_scaffold/test_connection.py index d1ec8a27fb..59ba7f67b1 100644 --- a/tests/test_cli/test_commands/test_scaffold/test_connection.py +++ b/tests/test_cli/test_commands/test_scaffold/test_connection.py @@ -290,4 +290,3 @@ def teardown_class(cls): shutil.rmtree(cls.t) except (OSError, IOError): pass - diff --git a/tests/test_cli/test_commands/test_scaffold/test_protocols.py b/tests/test_cli/test_commands/test_scaffold/test_protocols.py index 0fae9c4251..af7d79e0d7 100644 --- a/tests/test_cli/test_commands/test_scaffold/test_protocols.py +++ b/tests/test_cli/test_commands/test_scaffold/test_protocols.py @@ -296,4 +296,3 @@ def teardown_class(cls): shutil.rmtree(cls.t) except (OSError, IOError): pass - diff --git a/tests/test_cli/test_commands/test_scaffold/test_skills.py b/tests/test_cli/test_commands/test_scaffold/test_skills.py index 991dcfc7bf..50f9eafd1e 100644 --- a/tests/test_cli/test_commands/test_scaffold/test_skills.py +++ b/tests/test_cli/test_commands/test_scaffold/test_skills.py @@ -308,4 +308,3 @@ def teardown_class(cls): shutil.rmtree(cls.t) except (OSError, IOError): pass - From 5c957b2520023f66eccd8ab740bd9a1fb22efe15 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Wed, 16 Oct 2019 23:23:09 +0200 Subject: [PATCH 50/71] fix flaky test 'test_aea.test_handle' use Connection APIs instead of private queues of Outbox and Inbox --- tests/test_aea.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_aea.py b/tests/test_aea.py index 332e656c73..cbab7095fb 100644 --- a/tests/test_aea.py +++ b/tests/test_aea.py @@ -118,7 +118,8 @@ def test_handle(): private_key_pem_path = os.path.join(CUR_PATH, "data", "priv.pem") wallet = Wallet({'default': private_key_pem_path}) public_key = wallet.public_keys['default'] - mailbox = MailBox(OEFLocalConnection(public_key, node)) + connection = OEFLocalConnection(public_key, node) + mailbox = MailBox(connection) msg = DefaultMessage(type=DefaultMessage.Type.BYTES, content=b"hello") message_bytes = DefaultSerializer().encode(msg) @@ -137,8 +138,8 @@ def test_handle(): t = Thread(target=agent.start) try: t.start() - agent.mailbox.inbox._queue.put(envelope) - env = agent.mailbox.outbox._queue.get(block=True, timeout=10.0) + connection.in_queue.put(envelope) + env = connection.out_queue.get(block=True, timeout=4.0) assert env.protocol_id == "default", \ "The envelope is not the expected protocol (Unsupported protocol)" @@ -149,7 +150,7 @@ def test_handle(): sender=public_key, protocol_id='default', message=msg) - agent.mailbox.inbox._queue.put(envelope) + connection.in_queue.put(envelope) # UNSUPPORTED SKILL msg = FIPASerializer().encode( FIPAMessage(performative=FIPAMessage.Performative.ACCEPT, @@ -162,7 +163,7 @@ def test_handle(): sender=public_key, protocol_id="fipa", message=msg) - agent.mailbox.inbox._queue.put(envelope) + connection.in_queue.put(envelope) finally: agent.stop() - t.join() + t.join() \ No newline at end of file From 0711036dc9e9e7cc8e084c173ddbf603319cece1 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Wed, 16 Oct 2019 23:37:29 +0200 Subject: [PATCH 51/71] add test on 'aea install -r REQ_FILE' --- tests/data/dummy_aea/requirements.txt | 1 + tests/test_cli/test_commands/test_install.py | 29 ++++++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 tests/data/dummy_aea/requirements.txt diff --git a/tests/data/dummy_aea/requirements.txt b/tests/data/dummy_aea/requirements.txt new file mode 100644 index 0000000000..b0c79cc0ec --- /dev/null +++ b/tests/data/dummy_aea/requirements.txt @@ -0,0 +1 @@ +protobuf diff --git a/tests/test_cli/test_commands/test_install.py b/tests/test_cli/test_commands/test_install.py index 169af7d6c9..4f44a2a449 100644 --- a/tests/test_cli/test_commands/test_install.py +++ b/tests/test_cli/test_commands/test_install.py @@ -19,11 +19,9 @@ """This test module contains the tests for the `aea install` sub-command.""" import os -import shutil import tempfile -from pathlib import Path - import unittest.mock +from pathlib import Path import yaml from click.testing import CliRunner @@ -32,6 +30,7 @@ from aea.cli import cli from ...conftest import CLI_LOG_OPTION, CUR_PATH + class TestInstall: """Test that the command 'aea install' works as expected.""" @@ -39,7 +38,6 @@ class TestInstall: def setup_class(cls): """Set the test up.""" cls.runner = CliRunner() - cls.agent_name = "myagent" cls.cwd = os.getcwd() os.chdir(Path(CUR_PATH, "data", "dummy_aea")) cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "install"]) @@ -54,6 +52,28 @@ def teardown_class(cls): os.chdir(cls.cwd) +class TestInstallFromRequirementFile: + """Test that the command 'aea install --requirement REQ_FILE' works.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.cwd = os.getcwd() + os.chdir(Path(CUR_PATH, "data", "dummy_aea")) + + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "install", "-r", "requirements.txt"]) + + def test_exit_code_equal_to_zero(self): + """Assert that the exit code is equal to zero (i.e. success).""" + assert self.result.exit_code == 0 + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + + class TestInstallFails: """Test that the command 'aea install' fails when a dependency is not found.""" @@ -85,7 +105,6 @@ def test_exit_code_equal_to_minus_1(self): """Assert that the exit code is equal to -1 (i.e. failure).""" assert self.result.exit_code == -1 - @classmethod def teardown_class(cls): """Teardowm the test.""" From a7119f06af11dda7e61236c19d3c652e46ca1377 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Thu, 17 Oct 2019 01:17:08 +0200 Subject: [PATCH 52/71] add 'liveness' object to agent context (#265) --- aea/aea.py | 3 ++- aea/context/base.py | 11 ++++++++++- aea/skills/base.py | 6 ++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/aea/aea.py b/aea/aea.py index 2a49d5ec0f..77bf248d84 100644 --- a/aea/aea.py +++ b/aea/aea.py @@ -71,7 +71,8 @@ def __init__(self, name: str, self.decision_maker.message_queue, self.decision_maker.ownership_state, self.decision_maker.preferences, - self.decision_maker.is_ready_to_pursuit_goals) + self.decision_maker.is_ready_to_pursuit_goals, + self.liveness) self._resources = None # type: Optional[Resources] @property diff --git a/aea/context/base.py b/aea/context/base.py index 7ea75091d0..51cf83542a 100644 --- a/aea/context/base.py +++ b/aea/context/base.py @@ -22,6 +22,7 @@ from queue import Queue from typing import Dict +from aea.agent import Liveness from aea.decision_maker.base import OwnershipState, Preferences from aea.mail.base import OutBox @@ -35,7 +36,8 @@ def __init__(self, agent_name: str, decision_maker_message_queue: Queue, ownership_state: OwnershipState, preferences: Preferences, - is_ready_to_pursuit_goals: bool): + is_ready_to_pursuit_goals: bool, + liveness: Liveness): """ Initialize an agent context. @@ -47,6 +49,7 @@ def __init__(self, agent_name: str, :param ownership_state: the ownership state of the agent :param preferences: the preferences of the agent :param is_ready_to_pursuit_goals: whether the agent is ready to pursuit its goals + :param liveness: the liveness object. """ self._agent_name = agent_name self._public_keys = public_keys @@ -55,6 +58,7 @@ def __init__(self, agent_name: str, self._ownership_state = ownership_state self._preferences = preferences self._is_ready_to_pursuit_goals = is_ready_to_pursuit_goals + self._liveness = liveness @property def agent_name(self) -> str: @@ -95,3 +99,8 @@ def preferences(self) -> Preferences: def is_ready_to_pursuit_goals(self) -> bool: """Get the goal pursuit readiness.""" return self._is_ready_to_pursuit_goals + + @property + def liveness(self) -> Liveness: + """Get the liveness object.""" + return self._liveness diff --git a/aea/skills/base.py b/aea/skills/base.py index 17c97fef29..f1d2e1b0e1 100644 --- a/aea/skills/base.py +++ b/aea/skills/base.py @@ -28,6 +28,7 @@ from queue import Queue from typing import Optional, List, Dict, Any, cast +from aea.agent import Liveness from aea.configurations.base import BehaviourConfig, HandlerConfig, TaskConfig, SharedClassConfig, SkillConfig, ProtocolId, DEFAULT_SKILL_CONFIG_FILE from aea.configurations.loader import ConfigLoader from aea.context.base import AgentContext @@ -90,6 +91,11 @@ def agent_is_ready_to_pursuit_goals(self) -> bool: """Get the goal pursuit readiness.""" return self._agent_context.is_ready_to_pursuit_goals + @property + def liveness(self) -> Liveness: + """Get the liveness object.""" + return self._agent_context.liveness + @property def handlers(self) -> Optional[List['Handler']]: """Get handlers of the skill.""" From 852a02160960ff92004cffbedcb85e421ee2756f Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Thu, 17 Oct 2019 01:17:17 +0200 Subject: [PATCH 53/71] add test on 'aea run' --- tests/data/dummy_aea/aea-config.yaml | 22 ++++-- tests/data/dummy_aea/default_private_key.pem | 6 ++ tests/data/dummy_aea/eth_private_key.txt | 1 + tests/data/dummy_aea/fet_private_key.txt | 1 + tests/data/dummy_skill/behaviours.py | 2 +- tests/data/dummy_skill/handlers.py | 2 +- tests/data/dummy_skill/tasks.py | 2 +- tests/data/stopping_skill/__init__.py | 20 ++++++ tests/data/stopping_skill/behaviours.py | 20 ++++++ tests/data/stopping_skill/handlers.py | 20 ++++++ tests/data/stopping_skill/skill.yaml | 17 +++++ tests/data/stopping_skill/tasks.py | 51 ++++++++++++++ tests/test_aea.py | 2 +- tests/test_cli/test_commands/test_install.py | 3 +- tests/test_cli/test_commands/test_run.py | 72 ++++++++++++++++++++ 15 files changed, 230 insertions(+), 11 deletions(-) create mode 100644 tests/data/dummy_aea/default_private_key.pem create mode 100644 tests/data/dummy_aea/eth_private_key.txt create mode 100644 tests/data/dummy_aea/fet_private_key.txt create mode 100644 tests/data/stopping_skill/__init__.py create mode 100644 tests/data/stopping_skill/behaviours.py create mode 100644 tests/data/stopping_skill/handlers.py create mode 100644 tests/data/stopping_skill/skill.yaml create mode 100644 tests/data/stopping_skill/tasks.py create mode 100644 tests/test_cli/test_commands/test_run.py diff --git a/tests/data/dummy_aea/aea-config.yaml b/tests/data/dummy_aea/aea-config.yaml index 5d33d13a96..d39ba49981 100644 --- a/tests/data/dummy_aea/aea-config.yaml +++ b/tests/data/dummy_aea/aea-config.yaml @@ -4,18 +4,28 @@ authors: Fetch.AI Limited connections: - local default_connection: local +description: dummy_aea agent description [Fill in] license: Apache 2.0 +logging_config: + disable_existing_loggers: false + version: 1 private_key_paths: - - private_key_path: - ledger: default - path: '' +- private_key_path: + ledger: default + path: default_private_key.pem +- private_key_path: + ledger: fetchai + path: fet_private_key.txt +- private_key_path: + ledger: ethereum + path: eth_private_key.txt protocols: -- fipa - default +- fipa registry_path: aea skills: -- error - dummy +- error +- stopping url: '' version: v1 -description: "dummy_aea agent description [Fill in]" \ No newline at end of file diff --git a/tests/data/dummy_aea/default_private_key.pem b/tests/data/dummy_aea/default_private_key.pem new file mode 100644 index 0000000000..344cc23d2c --- /dev/null +++ b/tests/data/dummy_aea/default_private_key.pem @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDAvooixCAY5kO+PORV2oblpbKkPQ6kG41J6PrJvYULOGCltjNnDu7nT +rhpKLSL4zmKgBwYFK4EEACKhZANiAATje8YjXsjyNbOcqsfSGKf7dqncNZ43j79M +Cj0Ez52VcunGktL0mUqa+fVaN9LD+T5TyfyiViw1FzVHTPmqlp6kZVYrH/zJDbVw +dsdooWy3LOfhf8hak4XORcLcdUa22ys= +-----END EC PRIVATE KEY----- diff --git a/tests/data/dummy_aea/eth_private_key.txt b/tests/data/dummy_aea/eth_private_key.txt new file mode 100644 index 0000000000..43bf869846 --- /dev/null +++ b/tests/data/dummy_aea/eth_private_key.txt @@ -0,0 +1 @@ +0xf5c605c8a611aed64099548aae758fcb7cc364cf896236efe3d478a16cfd6b63 \ No newline at end of file diff --git a/tests/data/dummy_aea/fet_private_key.txt b/tests/data/dummy_aea/fet_private_key.txt new file mode 100644 index 0000000000..e23683c0eb --- /dev/null +++ b/tests/data/dummy_aea/fet_private_key.txt @@ -0,0 +1 @@ +3186c61cbd181fcefe65ff836128d8c9511305dd8ff568d999d911a25ce602ec \ No newline at end of file diff --git a/tests/data/dummy_skill/behaviours.py b/tests/data/dummy_skill/behaviours.py index df9944febb..e3482d6869 100644 --- a/tests/data/dummy_skill/behaviours.py +++ b/tests/data/dummy_skill/behaviours.py @@ -17,7 +17,7 @@ # # ------------------------------------------------------------------------------ -"""This module contains the behaviours for the 'echo' skill.""" +"""This module contains the behaviours for the 'dummy' skill.""" from aea.skills.base import Behaviour diff --git a/tests/data/dummy_skill/handlers.py b/tests/data/dummy_skill/handlers.py index 474c483832..7f33301be3 100644 --- a/tests/data/dummy_skill/handlers.py +++ b/tests/data/dummy_skill/handlers.py @@ -17,7 +17,7 @@ # # ------------------------------------------------------------------------------ -"""This module contains the handler for the 'echo' skill.""" +"""This module contains the handler for the 'dummy' skill.""" from aea.protocols.base import Message from aea.skills.base import Handler diff --git a/tests/data/dummy_skill/tasks.py b/tests/data/dummy_skill/tasks.py index 4fb433f31a..a103c16813 100644 --- a/tests/data/dummy_skill/tasks.py +++ b/tests/data/dummy_skill/tasks.py @@ -17,7 +17,7 @@ # # ------------------------------------------------------------------------------ -"""This module contains the tasks for the 'echo' skill.""" +"""This module contains the tasks for the 'dummy' skill.""" from aea.skills.base import Task diff --git a/tests/data/stopping_skill/__init__.py b/tests/data/stopping_skill/__init__.py new file mode 100644 index 0000000000..2d8bbdabc2 --- /dev/null +++ b/tests/data/stopping_skill/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains a dummy skill for an AEA.""" diff --git a/tests/data/stopping_skill/behaviours.py b/tests/data/stopping_skill/behaviours.py new file mode 100644 index 0000000000..b7f424a017 --- /dev/null +++ b/tests/data/stopping_skill/behaviours.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the behaviours for the 'stop' skill.""" diff --git a/tests/data/stopping_skill/handlers.py b/tests/data/stopping_skill/handlers.py new file mode 100644 index 0000000000..4b1dbadb2e --- /dev/null +++ b/tests/data/stopping_skill/handlers.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the handler for the 'stop' skill.""" diff --git a/tests/data/stopping_skill/skill.yaml b/tests/data/stopping_skill/skill.yaml new file mode 100644 index 0000000000..e605d2468b --- /dev/null +++ b/tests/data/stopping_skill/skill.yaml @@ -0,0 +1,17 @@ +name: stopping +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +behaviours: [] +handlers: [] +tasks: + - task: + class_name: StopTask + args: + enabled: true + timeout: 3 +shared_classes: [] +protocols: [] +dependencies: [] +description: "Stop the agent as soon as possible." \ No newline at end of file diff --git a/tests/data/stopping_skill/tasks.py b/tests/data/stopping_skill/tasks.py new file mode 100644 index 0000000000..b823c0e044 --- /dev/null +++ b/tests/data/stopping_skill/tasks.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the tasks for the 'stop' skill.""" +import datetime + +from aea.skills.base import Task + + +class StopTask(Task): + """Dummy task.""" + + def __init__(self, **kwargs): + """Initialize the task.""" + super().__init__(**kwargs) + self.kwargs = kwargs + self.timeout = kwargs.get("timeout", 3.0) + self.enabled = kwargs.get("enabled", False) + + def setup(self) -> None: + """ + Implement the setup. + + :return: None + """ + self.start_time = datetime.datetime.now() + self.end_time = self.start_time + datetime.timedelta(0, self.timeout) + + def execute(self) -> None: + """Execute the task.""" + if self.enabled and datetime.datetime.now() > self.end_time: + self.context.liveness._is_stopped = True + + def teardown(self) -> None: + """Teardown the task.""" diff --git a/tests/test_aea.py b/tests/test_aea.py index cbab7095fb..b14df94db3 100644 --- a/tests/test_aea.py +++ b/tests/test_aea.py @@ -166,4 +166,4 @@ def test_handle(): connection.in_queue.put(envelope) finally: agent.stop() - t.join() \ No newline at end of file + t.join() diff --git a/tests/test_cli/test_commands/test_install.py b/tests/test_cli/test_commands/test_install.py index 4f44a2a449..a38462a4c9 100644 --- a/tests/test_cli/test_commands/test_install.py +++ b/tests/test_cli/test_commands/test_install.py @@ -28,6 +28,7 @@ import aea.cli.common from aea.cli import cli +from aea.configurations.base import DEFAULT_PROTOCOL_CONFIG_FILE from ...conftest import CLI_LOG_OPTION, CUR_PATH @@ -95,7 +96,7 @@ def setup_class(cls): result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "scaffold", "protocol", "my_protocol"]) assert result.exit_code == 0 - config_path = Path("protocols", "my_protocol", "protocol.yaml") + config_path = Path("protocols", "my_protocol", DEFAULT_PROTOCOL_CONFIG_FILE) config = yaml.safe_load(open(config_path)) config.setdefault("dependencies", []).append("this_dependency_does_not_exist") yaml.safe_dump(config, open(config_path, "w")) diff --git a/tests/test_cli/test_commands/test_run.py b/tests/test_cli/test_commands/test_run.py new file mode 100644 index 0000000000..808230410b --- /dev/null +++ b/tests/test_cli/test_commands/test_run.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the `aea run` sub-command.""" +import logging +import os +import shutil +import tempfile +from pathlib import Path + +import yaml +from click.testing import CliRunner + +from aea.cli import cli +from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE +from ...conftest import CLI_LOG_OPTION, CUR_PATH + + +class TestRun: + """Test that the command 'aea run' works as expected.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + + os.chdir(Path(cls.t, cls.agent_name)) + shutil.copytree(Path(CUR_PATH, "data", "stopping_skill"), Path(cls.t, cls.agent_name, "skills", "stopping")) + config_path = Path(cls.t, cls.agent_name, DEFAULT_AEA_CONFIG_FILE) + config = yaml.safe_load(open(config_path)) + config.setdefault("skills", []).append("stopping") + yaml.safe_dump(config, open(config_path, "w")) + + try: + cli.main([*CLI_LOG_OPTION, "run"]) + except SystemExit as e: + cls.exit_code = e.code + + def test_exit_code_equal_to_zero(self): + """Assert that the exit code is equal to zero (i.e. success).""" + assert self.exit_code == 0 + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass From e3bee9c07f4f823c724231f07259b89d3d17f2a1 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Thu, 17 Oct 2019 01:21:56 +0200 Subject: [PATCH 54/71] do minor fixes on tests. --- tests/data/dummy_aea/aea-config.yaml | 1 - tests/test_cli/test_commands/test_run.py | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/data/dummy_aea/aea-config.yaml b/tests/data/dummy_aea/aea-config.yaml index d39ba49981..f10e1c5b75 100644 --- a/tests/data/dummy_aea/aea-config.yaml +++ b/tests/data/dummy_aea/aea-config.yaml @@ -26,6 +26,5 @@ registry_path: aea skills: - dummy - error -- stopping url: '' version: v1 diff --git a/tests/test_cli/test_commands/test_run.py b/tests/test_cli/test_commands/test_run.py index 808230410b..4749100f4b 100644 --- a/tests/test_cli/test_commands/test_run.py +++ b/tests/test_cli/test_commands/test_run.py @@ -18,7 +18,6 @@ # ------------------------------------------------------------------------------ """This test module contains the tests for the `aea run` sub-command.""" -import logging import os import shutil import tempfile From 011c4ce366ec078b43e732252791318d558ebfb8 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Thu, 17 Oct 2019 11:01:07 +0200 Subject: [PATCH 55/71] add more tests on 'aea run' (100%) --- aea/cli/run.py | 18 +- aea/connections/stub/connection.py | 2 +- aea/mail/base.py | 2 +- tests/data/exception_skill/__init__.py | 20 ++ tests/data/exception_skill/behaviours.py | 20 ++ tests/data/exception_skill/handlers.py | 20 ++ tests/data/exception_skill/skill.yaml | 15 + tests/data/exception_skill/tasks.py | 40 +++ tests/data/stopping_skill/__init__.py | 2 +- tests/test_cli/test_commands/test_run.py | 406 ++++++++++++++++++++++- 10 files changed, 533 insertions(+), 12 deletions(-) create mode 100644 tests/data/exception_skill/__init__.py create mode 100644 tests/data/exception_skill/behaviours.py create mode 100644 tests/data/exception_skill/handlers.py create mode 100644 tests/data/exception_skill/skill.yaml create mode 100644 tests/data/exception_skill/tasks.py diff --git a/aea/cli/run.py b/aea/cli/run.py index cee5d04317..59d6886068 100644 --- a/aea/cli/run.py +++ b/aea/cli/run.py @@ -53,17 +53,19 @@ def _setup_connection(connection_name: str, public_key: str, ctx: Context) -> Co if connection_name not in ctx.agent_config.connections: raise AEAConfigException("Connection name '{}' not declared in the configuration file.".format(connection_name)) - connection_config = ctx.connection_loader.load(open(os.path.join(ctx.cwd, "connections", connection_name, "connection.yaml"))) - if connection_config is None: + try: + connection_config = ctx.connection_loader.load(open(os.path.join(ctx.cwd, "connections", connection_name, "connection.yaml"))) + except FileNotFoundError: raise AEAConfigException("Connection config for '{}' not found.".format(connection_name)) - connection_spec = importlib.util.spec_from_file_location(connection_config.name, os.path.join(ctx.cwd, "connections", connection_config.name, "connection.py")) - if connection_spec is None: + try: + connection_spec = importlib.util.spec_from_file_location(connection_config.name, os.path.join(ctx.cwd, "connections", connection_config.name, "connection.py")) + connection_module = importlib.util.module_from_spec(connection_spec) + connection_spec.loader.exec_module(connection_module) # type: ignore + except FileNotFoundError: raise AEAConfigException("Connection '{}' not found.".format(connection_name)) - connection_module = importlib.util.module_from_spec(connection_spec) sys.modules[connection_spec.name + "_connection"] = connection_module - connection_spec.loader.exec_module(connection_module) # type: ignore classes = inspect.getmembers(connection_module, inspect.isclass) connection_classes = list(filter(lambda x: re.match("\\w+Connection", x[0]), classes)) name_to_class = dict(connection_classes) @@ -101,7 +103,6 @@ def run(click_context, connection_name: str, env_file: str, install_deps: bool): except AEAConfigException as e: logger.error(str(e)) exit(-1) - return if install_deps: if Path("requirements.txt").exists(): @@ -114,8 +115,9 @@ def run(click_context, connection_name: str, env_file: str, install_deps: bool): try: agent.start() except KeyboardInterrupt: - logger.info("Interrupted.") + logger.info("Interrupted.") # pragma: no cover except Exception as e: logger.exception(e) + exit(-1) finally: agent.stop() diff --git a/aea/connections/stub/connection.py b/aea/connections/stub/connection.py index f83bd1fa2c..e1cecbe434 100644 --- a/aea/connections/stub/connection.py +++ b/aea/connections/stub/connection.py @@ -137,7 +137,7 @@ def is_established(self) -> bool: def receive(self) -> None: """Receive new messages, if any.""" line = self.input_file.readline() - logger.debug("read line: {}".format(line)) + logger.debug("read line: {!r}".format(line)) while len(line) > 0: self._process_line(line[:-1]) line = self.input_file.readline() diff --git a/aea/mail/base.py b/aea/mail/base.py index 1150767121..bef8da6c6d 100644 --- a/aea/mail/base.py +++ b/aea/mail/base.py @@ -214,7 +214,7 @@ def put(self, envelope: Envelope) -> None: :param envelope: the envelope. :return: None """ - logger.debug("Put an envelope in the queue: to='{}' sender='{}' protocol_id='{}' message='{}'..." + logger.debug("Put an envelope in the queue: to='{}' sender='{}' protocol_id='{}' message='{!r}'..." .format(envelope.to, envelope.sender, envelope.protocol_id, envelope.message)) self._queue.put(envelope) diff --git a/tests/data/exception_skill/__init__.py b/tests/data/exception_skill/__init__.py new file mode 100644 index 0000000000..401e948c59 --- /dev/null +++ b/tests/data/exception_skill/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains a dummy 'exception' skill for an AEA.""" diff --git a/tests/data/exception_skill/behaviours.py b/tests/data/exception_skill/behaviours.py new file mode 100644 index 0000000000..2f2cea5998 --- /dev/null +++ b/tests/data/exception_skill/behaviours.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the behaviours for the 'exception' skill.""" diff --git a/tests/data/exception_skill/handlers.py b/tests/data/exception_skill/handlers.py new file mode 100644 index 0000000000..7bf837fc57 --- /dev/null +++ b/tests/data/exception_skill/handlers.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the handler for the 'exception' skill.""" diff --git a/tests/data/exception_skill/skill.yaml b/tests/data/exception_skill/skill.yaml new file mode 100644 index 0000000000..2454ebe9d6 --- /dev/null +++ b/tests/data/exception_skill/skill.yaml @@ -0,0 +1,15 @@ +name: exception +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +behaviours: [] +handlers: [] +tasks: + - task: + class_name: ExceptionTask + args: {} +shared_classes: [] +protocols: [] +dependencies: [] +description: "Raise an exception, at some point." \ No newline at end of file diff --git a/tests/data/exception_skill/tasks.py b/tests/data/exception_skill/tasks.py new file mode 100644 index 0000000000..2196c20b3d --- /dev/null +++ b/tests/data/exception_skill/tasks.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the tasks for the 'exception' skill.""" + +from aea.skills.base import Task + + +class ExceptionTask(Task): + """Dummy task.""" + + def setup(self) -> None: + """ + Implement the setup. + + :return: None + """ + + def execute(self) -> None: + """Execute the task.""" + raise Exception() + + def teardown(self) -> None: + """Teardown the task.""" diff --git a/tests/data/stopping_skill/__init__.py b/tests/data/stopping_skill/__init__.py index 2d8bbdabc2..47f4b85aa7 100644 --- a/tests/data/stopping_skill/__init__.py +++ b/tests/data/stopping_skill/__init__.py @@ -17,4 +17,4 @@ # # ------------------------------------------------------------------------------ -"""This module contains a dummy skill for an AEA.""" +"""This module contains a dummy 'stopping' skill for an AEA.""" diff --git a/tests/test_cli/test_commands/test_run.py b/tests/test_cli/test_commands/test_run.py index 4749100f4b..986cc04789 100644 --- a/tests/test_cli/test_commands/test_run.py +++ b/tests/test_cli/test_commands/test_run.py @@ -21,13 +21,15 @@ import os import shutil import tempfile +import unittest.mock from pathlib import Path import yaml from click.testing import CliRunner +import aea.cli.common from aea.cli import cli -from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE +from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE, DEFAULT_CONNECTION_CONFIG_FILE from ...conftest import CLI_LOG_OPTION, CUR_PATH @@ -69,3 +71,405 @@ def teardown_class(cls): shutil.rmtree(cls.t) except (OSError, IOError): pass + + +class TestRunFailsWhenExceptionOccursInSkill: + """Test that the command 'aea run' fails when an exception occurs in any skill.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + + os.chdir(Path(cls.t, cls.agent_name)) + shutil.copytree(Path(CUR_PATH, "data", "exception_skill"), Path(cls.t, cls.agent_name, "skills", "exception")) + config_path = Path(cls.t, cls.agent_name, DEFAULT_AEA_CONFIG_FILE) + config = yaml.safe_load(open(config_path)) + config.setdefault("skills", []).append("exception") + yaml.safe_dump(config, open(config_path, "w")) + + try: + cli.main([*CLI_LOG_OPTION, "run"]) + except SystemExit as e: + cls.exit_code = e.code + + def test_exit_code_equal_to_minus_one(self): + """Assert that the exit code is equal to -1 (i.e. failure).""" + assert self.exit_code == -1 + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestRunFailsWhenConfigurationFileNotFound: + """Test that the command 'aea run' fails when the agent configuration file is not found in the current directory.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + Path(cls.t, cls.agent_name, DEFAULT_AEA_CONFIG_FILE).unlink() + + os.chdir(Path(cls.t, cls.agent_name)) + + try: + cli.main([*CLI_LOG_OPTION, "run"]) + except SystemExit as e: + cls.exit_code = e.code + + def test_exit_code_equal_to_minus_one(self): + """Assert that the exit code is equal to -1 (i.e. failure).""" + assert self.exit_code == -1 + + def test_log_error_message(self): + """Test that the log error message is fixed.""" + s = "Agent configuration file '{}' not found in the current directory.".format(DEFAULT_AEA_CONFIG_FILE) + self.mocked_logger_error.assert_called_once_with(s) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestRunFailsWhenConfigurationFileInvalid: + """Test that the command 'aea run' fails when the agent configuration file is invalid.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + + Path(cls.t, cls.agent_name, DEFAULT_AEA_CONFIG_FILE).write_text("") + + os.chdir(Path(cls.t, cls.agent_name)) + + try: + cli.main([*CLI_LOG_OPTION, "run"]) + except SystemExit as e: + cls.exit_code = e.code + + def test_exit_code_equal_to_minus_one(self): + """Assert that the exit code is equal to -1 (i.e. failure).""" + assert self.exit_code == -1 + + def test_log_error_message(self): + """Test that the log error message is fixed.""" + s = "Agent configuration file '{}' is invalid. Please check the documentation.".format(DEFAULT_AEA_CONFIG_FILE) + self.mocked_logger_error.assert_called_once_with(s) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestRunFailsWhenConnectionNotDeclared: + """Test that the command 'aea run --connection' fails when the connection is not declared.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.connection_name = "unknown_connection" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + + os.chdir(Path(cls.t, cls.agent_name)) + + try: + cli.main([*CLI_LOG_OPTION, "run", "--connection", cls.connection_name]) + except SystemExit as e: + cls.exit_code = e.code + + def test_exit_code_equal_to_minus_one(self): + """Assert that the exit code is equal to -1 (i.e. failure).""" + assert self.exit_code == -1 + + def test_log_error_message(self): + """Test that the log error message is fixed.""" + s = "Connection name '{}' not declared in the configuration file.".format(self.connection_name) + self.mocked_logger_error.assert_called_once_with(s) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestRunFailsWhenConnectionConfigFileNotFound: + """Test that the command 'aea run --connection' fails when the connection config file is not found.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.connection_name = "myconnection" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(Path(cls.t, cls.agent_name)) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "scaffold", "connection", cls.connection_name]) + assert result.exit_code == 0 + Path(cls.t, cls.agent_name, "connections", cls.connection_name, DEFAULT_CONNECTION_CONFIG_FILE).unlink() + + try: + cli.main([*CLI_LOG_OPTION, "run", "--connection", cls.connection_name]) + except SystemExit as e: + cls.exit_code = e.code + + def test_exit_code_equal_to_minus_one(self): + """Assert that the exit code is equal to -1 (i.e. failure).""" + assert self.exit_code == -1 + + def test_log_error_message(self): + """Test that the log error message is fixed.""" + s = "Connection config for '{}' not found.".format(self.connection_name) + self.mocked_logger_error.assert_called_once_with(s) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestRunFailsWhenConnectionNotComplete: + """Test that the command 'aea run --connection' fails when the connection.py module is missing.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.connection_name = "stub" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(Path(cls.t, cls.agent_name)) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", cls.connection_name]) + assert result.exit_code == 0 + Path(cls.t, cls.agent_name, "connections", cls.connection_name, "connection.py").unlink() + + try: + cli.main([*CLI_LOG_OPTION, "run", "--connection", cls.connection_name]) + except SystemExit as e: + cls.exit_code = e.code + + def test_exit_code_equal_to_minus_one(self): + """Assert that the exit code is equal to -1 (i.e. failure).""" + assert self.exit_code == -1 + + def test_log_error_message(self): + """Test that the log error message is fixed.""" + s = "Connection '{}' not found.".format(self.connection_name) + self.mocked_logger_error.assert_called_once_with(s) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestRunFailsWhenConnectionClassNotPresent: + """Test that the command 'aea run --connection' fails when the connection class is missing in connection.py.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.connection_name = "stub" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(Path(cls.t, cls.agent_name)) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", cls.connection_name]) + assert result.exit_code == 0 + Path(cls.t, cls.agent_name, "connections", cls.connection_name, "connection.py").write_text("") + + try: + cli.main([*CLI_LOG_OPTION, "run", "--connection", cls.connection_name]) + except SystemExit as e: + cls.exit_code = e.code + + def test_exit_code_equal_to_minus_one(self): + """Assert that the exit code is equal to -1 (i.e. failure).""" + assert self.exit_code == -1 + + def test_log_error_message(self): + """Test that the log error message is fixed.""" + s = "Connection class '{}' not found.".format("StubConnection") + self.mocked_logger_error.assert_called_once_with(s) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestRunWithInstallDeps: + """Test that the command 'aea run --install-deps' does not crash.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.connection_name = "stub" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(Path(cls.t, cls.agent_name)) + shutil.copytree(Path(CUR_PATH, "data", "stopping_skill"), Path(cls.t, cls.agent_name, "skills", "stopping")) + config_path = Path(cls.t, cls.agent_name, DEFAULT_AEA_CONFIG_FILE) + config = yaml.safe_load(open(config_path)) + config.setdefault("skills", []).append("stopping") + yaml.safe_dump(config, open(config_path, "w")) + + try: + cli.main([*CLI_LOG_OPTION, "run", "--install-deps"]) + except SystemExit as e: + cls.exit_code = e.code + + def test_exit_code_equal_to_zero(self): + """Assert that the exit code is equal to zero (i.e. success).""" + assert self.exit_code == 0 + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestRunWithInstallDepsAndRequirementFile: + """Test that the command 'aea run --install-deps' with requirement file does not crash.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.connection_name = "stub" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(Path(cls.t, cls.agent_name)) + + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "freeze"]) + assert result.exit_code == 0 + Path(cls.t, cls.agent_name, "requirements.txt").write_text(result.output) + + shutil.copytree(Path(CUR_PATH, "data", "stopping_skill"), Path(cls.t, cls.agent_name, "skills", "stopping")) + config_path = Path(cls.t, cls.agent_name, DEFAULT_AEA_CONFIG_FILE) + config = yaml.safe_load(open(config_path)) + config.setdefault("skills", []).append("stopping") + yaml.safe_dump(config, open(config_path, "w")) + + try: + cli.main([*CLI_LOG_OPTION, "run", "--install-deps"]) + except SystemExit as e: + cls.exit_code = e.code + + def test_exit_code_equal_to_zero(self): + """Assert that the exit code is equal to zero (i.e. success).""" + assert self.exit_code == 0 + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass From b7f2eaa2c25f9dc415d238b954017d9c79a1a773 Mon Sep 17 00:00:00 2001 From: Aristotelis Date: Thu, 17 Oct 2019 10:12:01 +0100 Subject: [PATCH 56/71] Changes after merge develop --- tests/test_crypto/test_ethereum_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_crypto/test_ethereum_base.py b/tests/test_crypto/test_ethereum_base.py index 28b8b0daef..b93b7793c8 100644 --- a/tests/test_crypto/test_ethereum_base.py +++ b/tests/test_crypto/test_ethereum_base.py @@ -36,7 +36,7 @@ def test_creation(): def test_initialization(): """Test the initialisation of the variables.""" account = EthCrypto() - assert account.display_address is not None, "After creation the display address must not be None" + assert account.address is not None, "After creation the display address must not be None" assert account._bytes_representation is not None, "After creation the bytes_representation of the " \ "address must not be None" assert account.public_key is not None, "After creation the public key must no be None" From efceecb1d9ea7765f82bb856af68a2a7e97cdd84 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Thu, 17 Oct 2019 11:50:37 +0200 Subject: [PATCH 57/71] make tests in test_run use 'local' connection. --- tests/test_cli/test_commands/test_run.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/test_cli/test_commands/test_run.py b/tests/test_cli/test_commands/test_run.py index 986cc04789..690597d957 100644 --- a/tests/test_cli/test_commands/test_run.py +++ b/tests/test_cli/test_commands/test_run.py @@ -48,6 +48,10 @@ def setup_class(cls): assert result.exit_code == 0 os.chdir(Path(cls.t, cls.agent_name)) + + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "local"]) + assert result.exit_code == 0 + shutil.copytree(Path(CUR_PATH, "data", "stopping_skill"), Path(cls.t, cls.agent_name, "skills", "stopping")) config_path = Path(cls.t, cls.agent_name, DEFAULT_AEA_CONFIG_FILE) config = yaml.safe_load(open(config_path)) @@ -55,7 +59,7 @@ def setup_class(cls): yaml.safe_dump(config, open(config_path, "w")) try: - cli.main([*CLI_LOG_OPTION, "run"]) + cli.main([*CLI_LOG_OPTION, "run", "--connection", "local"]) except SystemExit as e: cls.exit_code = e.code @@ -400,7 +404,12 @@ def setup_class(cls): os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) assert result.exit_code == 0 + os.chdir(Path(cls.t, cls.agent_name)) + + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "local"]) + assert result.exit_code == 0 + shutil.copytree(Path(CUR_PATH, "data", "stopping_skill"), Path(cls.t, cls.agent_name, "skills", "stopping")) config_path = Path(cls.t, cls.agent_name, DEFAULT_AEA_CONFIG_FILE) config = yaml.safe_load(open(config_path)) @@ -408,7 +417,7 @@ def setup_class(cls): yaml.safe_dump(config, open(config_path, "w")) try: - cli.main([*CLI_LOG_OPTION, "run", "--install-deps"]) + cli.main([*CLI_LOG_OPTION, "run", "--install-deps", "--connection", "local"]) except SystemExit as e: cls.exit_code = e.code @@ -443,8 +452,12 @@ def setup_class(cls): os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) assert result.exit_code == 0 + os.chdir(Path(cls.t, cls.agent_name)) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "local"]) + assert result.exit_code == 0 + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "freeze"]) assert result.exit_code == 0 Path(cls.t, cls.agent_name, "requirements.txt").write_text(result.output) @@ -456,7 +469,7 @@ def setup_class(cls): yaml.safe_dump(config, open(config_path, "w")) try: - cli.main([*CLI_LOG_OPTION, "run", "--install-deps"]) + cli.main([*CLI_LOG_OPTION, "run", "--install-deps", "--connection", "local"]) except SystemExit as e: cls.exit_code = e.code From 994d8ff29eff2b25c5511762362c478367df5c7b Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Thu, 17 Oct 2019 12:11:31 +0200 Subject: [PATCH 58/71] add dummy connection type for testing. --- aea/cli/common.py | 21 +-- tests/conftest.py | 29 ++++ tests/test_aea.py | 7 +- tests/test_cli/test_commands/test_run.py | 180 ++++++++++++++--------- 4 files changed, 152 insertions(+), 85 deletions(-) diff --git a/aea/cli/common.py b/aea/cli/common.py index 79ad7a04ee..4e22119921 100644 --- a/aea/cli/common.py +++ b/aea/cli/common.py @@ -113,24 +113,17 @@ def _try_to_load_agent_config(ctx: Context): def _try_to_load_protocols(ctx: Context): for protocol_name in ctx.agent_config.protocols: + logger.debug("Processing protocol {}".format(protocol_name)) try: - logger.debug("Processing protocol {}".format(protocol_name)) - protocol_config = ctx.protocol_loader.load(open(os.path.join("protocols", protocol_name, DEFAULT_PROTOCOL_CONFIG_FILE))) - if protocol_config is None: - logger.debug("Protocol configuration file for protocol {} not found.".format(protocol_name)) - exit(-1) - - protocol_spec = importlib.util.spec_from_file_location(protocol_name, os.path.join(ctx.agent_config.registry_path, "protocols", protocol_name, "__init__.py")) - if protocol_spec is None: - logger.warning("Protocol not found in registry.") - continue - - protocol_module = importlib.util.module_from_spec(protocol_spec) - sys.modules[protocol_spec.name + "_protocol"] = protocol_module + ctx.protocol_loader.load(open(os.path.join("protocols", protocol_name, DEFAULT_PROTOCOL_CONFIG_FILE))) except FileNotFoundError: - logger.error("Protocol {} not found in registry".format(protocol_name)) + logger.error("Protocol configuration file for protocol {} not found.".format(protocol_name)) exit(-1) + protocol_spec = importlib.util.spec_from_file_location(protocol_name, os.path.join("protocols", protocol_name, "__init__.py")) + protocol_module = importlib.util.module_from_spec(protocol_spec) + sys.modules[protocol_spec.name + "_protocol"] = protocol_module + def _load_env_file(env_file: str): """ diff --git a/tests/conftest.py b/tests/conftest.py index 754e00c5d3..a461a2826a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -32,6 +32,10 @@ from docker.models.containers import Container from oef.agents import AsyncioCore, OEFAgent +from aea.configurations.base import ConnectionConfig +from aea.connections.base import Connection +from aea.mail.base import Envelope + logger = logging.getLogger(__name__) CUR_PATH = os.path.dirname(inspect.getfile(inspect.currentframe())) # type: ignore @@ -75,6 +79,31 @@ def tcpping(ip, port) -> bool: return False +class DummyConnection(Connection): + """A dummy connection that just stores the messages.""" + + def connect(self): + """Connect.""" + pass + + def disconnect(self): + """Disconnect.""" + pass + + @property + def is_established(self) -> bool: + """Check if the connection is established.""" + return True + + def send(self, envelope: 'Envelope'): + """Send an envelope.""" + self.out_queue.put(envelope) + + @classmethod + def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection': + """Return a connection obj fom a configuration.""" + + class OEFHealthCheck(object): """A health check class.""" diff --git a/tests/test_aea.py b/tests/test_aea.py index b14df94db3..a23e694523 100644 --- a/tests/test_aea.py +++ b/tests/test_aea.py @@ -30,7 +30,7 @@ from aea.protocols.default.serialization import DefaultSerializer from aea.protocols.fipa.message import FIPAMessage from aea.protocols.fipa.serialization import FIPASerializer -from .conftest import CUR_PATH +from .conftest import CUR_PATH, DummyConnection def test_initialise_AEA(): @@ -113,12 +113,11 @@ def test_react(): def test_handle(): """Tests handle method of an agent.""" - node = LocalNode() agent_name = "MyAgent" private_key_pem_path = os.path.join(CUR_PATH, "data", "priv.pem") wallet = Wallet({'default': private_key_pem_path}) public_key = wallet.public_keys['default'] - connection = OEFLocalConnection(public_key, node) + connection = DummyConnection() mailbox = MailBox(connection) msg = DefaultMessage(type=DefaultMessage.Type.BYTES, content=b"hello") @@ -139,7 +138,7 @@ def test_handle(): try: t.start() connection.in_queue.put(envelope) - env = connection.out_queue.get(block=True, timeout=4.0) + env = connection.out_queue.get(block=True, timeout=5.0) assert env.protocol_id == "default", \ "The envelope is not the expected protocol (Unsupported protocol)" diff --git a/tests/test_cli/test_commands/test_run.py b/tests/test_cli/test_commands/test_run.py index 690597d957..7067cf6967 100644 --- a/tests/test_cli/test_commands/test_run.py +++ b/tests/test_cli/test_commands/test_run.py @@ -77,6 +77,106 @@ def teardown_class(cls): pass +class TestRunWithInstallDeps: + """Test that the command 'aea run --install-deps' does not crash.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.connection_name = "stub" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + + os.chdir(Path(cls.t, cls.agent_name)) + + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "local"]) + assert result.exit_code == 0 + + shutil.copytree(Path(CUR_PATH, "data", "stopping_skill"), Path(cls.t, cls.agent_name, "skills", "stopping")) + config_path = Path(cls.t, cls.agent_name, DEFAULT_AEA_CONFIG_FILE) + config = yaml.safe_load(open(config_path)) + config.setdefault("skills", []).append("stopping") + yaml.safe_dump(config, open(config_path, "w")) + + try: + cli.main([*CLI_LOG_OPTION, "run", "--install-deps", "--connection", "local"]) + except SystemExit as e: + cls.exit_code = e.code + + def test_exit_code_equal_to_zero(self): + """Assert that the exit code is equal to zero (i.e. success).""" + assert self.exit_code == 0 + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestRunWithInstallDepsAndRequirementFile: + """Test that the command 'aea run --install-deps' with requirement file does not crash.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.connection_name = "stub" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + + os.chdir(Path(cls.t, cls.agent_name)) + + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "local"]) + assert result.exit_code == 0 + + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "freeze"]) + assert result.exit_code == 0 + Path(cls.t, cls.agent_name, "requirements.txt").write_text(result.output) + + shutil.copytree(Path(CUR_PATH, "data", "stopping_skill"), Path(cls.t, cls.agent_name, "skills", "stopping")) + config_path = Path(cls.t, cls.agent_name, DEFAULT_AEA_CONFIG_FILE) + config = yaml.safe_load(open(config_path)) + config.setdefault("skills", []).append("stopping") + yaml.safe_dump(config, open(config_path, "w")) + + try: + cli.main([*CLI_LOG_OPTION, "run", "--install-deps", "--connection", "local"]) + except SystemExit as e: + cls.exit_code = e.code + + def test_exit_code_equal_to_zero(self): + """Assert that the exit code is equal to zero (i.e. success).""" + assert self.exit_code == 0 + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + class TestRunFailsWhenExceptionOccursInSkill: """Test that the command 'aea run' fails when an exception occurs in any skill.""" @@ -388,15 +488,15 @@ def teardown_class(cls): pass -class TestRunWithInstallDeps: - """Test that the command 'aea run --install-deps' does not crash.""" +class TestRunFailsWhenProtocolConfigFileNotFound: + """Test that the command 'aea run' fails when a protocol configuration file is not found.""" @classmethod def setup_class(cls): """Set the test up.""" cls.runner = CliRunner() cls.agent_name = "myagent" - cls.connection_name = "stub" + cls.connection_name = "local" cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') cls.mocked_logger_error = cls.patch.__enter__() cls.cwd = os.getcwd() @@ -404,78 +504,23 @@ def setup_class(cls): os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) assert result.exit_code == 0 - os.chdir(Path(cls.t, cls.agent_name)) - result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "local"]) - assert result.exit_code == 0 - - shutil.copytree(Path(CUR_PATH, "data", "stopping_skill"), Path(cls.t, cls.agent_name, "skills", "stopping")) - config_path = Path(cls.t, cls.agent_name, DEFAULT_AEA_CONFIG_FILE) - config = yaml.safe_load(open(config_path)) - config.setdefault("skills", []).append("stopping") - yaml.safe_dump(config, open(config_path, "w")) + Path(cls.t, cls.agent_name, "protocols", "default", "protocol.yaml").unlink() try: - cli.main([*CLI_LOG_OPTION, "run", "--install-deps", "--connection", "local"]) + cli.main([*CLI_LOG_OPTION, "run", "--connection", cls.connection_name]) except SystemExit as e: cls.exit_code = e.code - def test_exit_code_equal_to_zero(self): - """Assert that the exit code is equal to zero (i.e. success).""" - assert self.exit_code == 0 - - @classmethod - def teardown_class(cls): - """Teardowm the test.""" - cls.patch.__exit__() - os.chdir(cls.cwd) - try: - shutil.rmtree(cls.t) - except (OSError, IOError): - pass - - -class TestRunWithInstallDepsAndRequirementFile: - """Test that the command 'aea run --install-deps' with requirement file does not crash.""" - - @classmethod - def setup_class(cls): - """Set the test up.""" - cls.runner = CliRunner() - cls.agent_name = "myagent" - cls.connection_name = "stub" - cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') - cls.mocked_logger_error = cls.patch.__enter__() - cls.cwd = os.getcwd() - cls.t = tempfile.mkdtemp() - os.chdir(cls.t) - result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) - assert result.exit_code == 0 - - os.chdir(Path(cls.t, cls.agent_name)) - - result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "local"]) - assert result.exit_code == 0 - - result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "freeze"]) - assert result.exit_code == 0 - Path(cls.t, cls.agent_name, "requirements.txt").write_text(result.output) - - shutil.copytree(Path(CUR_PATH, "data", "stopping_skill"), Path(cls.t, cls.agent_name, "skills", "stopping")) - config_path = Path(cls.t, cls.agent_name, DEFAULT_AEA_CONFIG_FILE) - config = yaml.safe_load(open(config_path)) - config.setdefault("skills", []).append("stopping") - yaml.safe_dump(config, open(config_path, "w")) - - try: - cli.main([*CLI_LOG_OPTION, "run", "--install-deps", "--connection", "local"]) - except SystemExit as e: - cls.exit_code = e.code + def test_exit_code_equal_to_minus_one(self): + """Assert that the exit code is equal to -1 (i.e. failure).""" + assert self.exit_code == -1 - def test_exit_code_equal_to_zero(self): - """Assert that the exit code is equal to zero (i.e. success).""" - assert self.exit_code == 0 + def test_log_error_message(self): + """Test that the log error message is fixed.""" + s = "Protocol configuration file for protocol {} not found.".format("default") + self.mocked_logger_error.assert_called_once_with(s) @classmethod def teardown_class(cls): @@ -486,3 +531,4 @@ def teardown_class(cls): shutil.rmtree(cls.t) except (OSError, IOError): pass + From 4f1be833cda5dfb849893d7bf391e2d47eed9568 Mon Sep 17 00:00:00 2001 From: Aristotelis Date: Thu, 17 Oct 2019 11:37:57 +0100 Subject: [PATCH 59/71] 100% coverage ---> fetchai_base.py --- aea/crypto/fetchai_base.py | 2 +- tests/data/fet_private_key.txt | 1 + tests/test_crypto/test_fetchai_base.py | 45 ++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tests/data/fet_private_key.txt create mode 100644 tests/test_crypto/test_fetchai_base.py diff --git a/aea/crypto/fetchai_base.py b/aea/crypto/fetchai_base.py index 00650fc299..2988ff3ca1 100644 --- a/aea/crypto/fetchai_base.py +++ b/aea/crypto/fetchai_base.py @@ -107,7 +107,7 @@ def _load_private_key_from_path(self, file_name) -> Entity: entity = self._generate_private_key() return entity - except IOError as e: + except IOError as e: # pragma: no cover logger.exception(str(e)) def _generate_private_key(self) -> Entity: diff --git a/tests/data/fet_private_key.txt b/tests/data/fet_private_key.txt new file mode 100644 index 0000000000..9d9a316cc5 --- /dev/null +++ b/tests/data/fet_private_key.txt @@ -0,0 +1 @@ +04247057020fc15e213851d7d9d0f50f09b5bdd47d55ec8849584258980ff709 \ No newline at end of file diff --git a/tests/test_crypto/test_fetchai_base.py b/tests/test_crypto/test_fetchai_base.py new file mode 100644 index 0000000000..ccba2b5b5b --- /dev/null +++ b/tests/test_crypto/test_fetchai_base.py @@ -0,0 +1,45 @@ + +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the tests of the ethereum module.""" + +from aea.crypto.fetchai_base import FetchCrypto +from ..conftest import ROOT_DIR + +PRIVATE_KEY_PATH = ROOT_DIR + "/tests/data/fet_private_key.txt" + + +def test_initialisation(): + """Test the initialisation of the the fet crypto.""" + fet_crypto = FetchCrypto() + assert fet_crypto.public_key is not None, "Public key must not be None after Initialisation" + assert fet_crypto.address is not None, "Address must not be None after Initialisation" + assert fet_crypto.private_key is not None, "Private key must not be None after Initialisation" + assert fet_crypto.address == str(fet_crypto.get_address_from_public_key(fet_crypto.public_key)), \ + "Must generate the same Address" + assert FetchCrypto(PRIVATE_KEY_PATH), "Couldn't load the fet private_key from the path!" + assert FetchCrypto("./"), "Couldn't create a new entity for the given path!" + + +def test_sign_message(): + """Test the signing process.""" + fet_crypto = FetchCrypto() + signature = fet_crypto.sign_transaction(message=b'HelloWorld') + assert len(signature) > 1, "The len(signature) must be more than 0" From 1cbe3da024d3c588da7deda8419b37ae6dcbca4d Mon Sep 17 00:00:00 2001 From: Marco Favorito Date: Thu, 17 Oct 2019 13:45:49 +0200 Subject: [PATCH 60/71] Update test_run.py --- tests/test_cli/test_commands/test_run.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_cli/test_commands/test_run.py b/tests/test_cli/test_commands/test_run.py index 7067cf6967..fcc1f5d15c 100644 --- a/tests/test_cli/test_commands/test_run.py +++ b/tests/test_cli/test_commands/test_run.py @@ -531,4 +531,3 @@ def teardown_class(cls): shutil.rmtree(cls.t) except (OSError, IOError): pass - From ebca8800ff862f55de37e53b3424c70b4efe7523 Mon Sep 17 00:00:00 2001 From: Aristotelis Date: Thu, 17 Oct 2019 14:00:17 +0100 Subject: [PATCH 61/71] Changes based on Flake8 feedback --- tests/test_crypto/test_ethereum_base.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_crypto/test_ethereum_base.py b/tests/test_crypto/test_ethereum_base.py index b93b7793c8..ad666dfdd4 100644 --- a/tests/test_crypto/test_ethereum_base.py +++ b/tests/test_crypto/test_ethereum_base.py @@ -18,16 +18,15 @@ # ------------------------------------------------------------------------------ """This module contains the tests of the ethereum module.""" -import pytest -from aea.crypto.ethereum_base import EthCrypto, EthCryptoError +from aea.crypto.ethereum_base import EthCrypto from ..conftest import ROOT_DIR PRIVATE_KEY_PATH = ROOT_DIR + "/tests/data/eth_private_key.txt" def test_creation(): - """Test the creation of the crypto_objects""" + """Test the creation of the crypto_objects.ls""" assert EthCrypto(), "Managed to initialise the eth_account" assert EthCrypto(PRIVATE_KEY_PATH), "Managed to load the eth private key" assert EthCrypto("./"), "Managed to create a new eth private key" From f98ff96c95480a40839689862a5086e008586925 Mon Sep 17 00:00:00 2001 From: Aristotelis Date: Thu, 17 Oct 2019 14:01:46 +0100 Subject: [PATCH 62/71] Fix typo --- tests/test_crypto/test_ethereum_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_crypto/test_ethereum_base.py b/tests/test_crypto/test_ethereum_base.py index ad666dfdd4..fe094aeddf 100644 --- a/tests/test_crypto/test_ethereum_base.py +++ b/tests/test_crypto/test_ethereum_base.py @@ -26,7 +26,7 @@ def test_creation(): - """Test the creation of the crypto_objects.ls""" + """Test the creation of the crypto_objects.""" assert EthCrypto(), "Managed to initialise the eth_account" assert EthCrypto(PRIVATE_KEY_PATH), "Managed to load the eth private key" assert EthCrypto("./"), "Managed to create a new eth private key" From ef7484a7add82e7c0646c8d46d650d6ed05b650b Mon Sep 17 00:00:00 2001 From: Diarmid Campbell Date: Thu, 17 Oct 2019 16:54:43 +0100 Subject: [PATCH 63/71] more correct way to attach event handlers --- Test/aea-config.yaml | 21 + Test/connections/__init__.py | 0 Test/connections/oef/__init__.py | 21 + Test/connections/oef/connection.py | 604 +++++++++++++ Test/connections/oef/connection.yaml | 18 + Test/protocols/__init__.py | 0 Test/protocols/default/__init__.py | 21 + Test/protocols/default/message.py | 59 ++ Test/protocols/default/protocol.yaml | 6 + Test/protocols/default/serialization.py | 71 ++ Test/protocols/gym/__init__.py | 21 + Test/protocols/gym/message.py | 76 ++ Test/protocols/gym/protocol.yaml | 6 + Test/protocols/gym/serialization.py | 103 +++ Test/protocols/tac/__init__.py | 21 + Test/protocols/tac/message.py | 134 +++ Test/protocols/tac/protocol.yaml | 6 + Test/protocols/tac/serialization.py | 234 +++++ Test/protocols/tac/tac.proto | 102 +++ Test/protocols/tac/tac_pb2.py | 899 +++++++++++++++++++ Test/skills/__init__.py | 0 Test/skills/error/__init__.py | 20 + Test/skills/error/behaviours.py | 50 ++ Test/skills/error/handlers.py | 131 +++ Test/skills/error/skill.yaml | 15 + Test/skills/error/tasks.py | 51 ++ aea/cli_gui/templates/home.js | 5 +- asdsd/aea-config.yaml | 21 + asdsd/connections/__init__.py | 0 asdsd/connections/oef/__init__.py | 21 + asdsd/connections/oef/connection.py | 604 +++++++++++++ asdsd/connections/oef/connection.yaml | 18 + asdsd/protocols/__init__.py | 0 asdsd/protocols/default/__init__.py | 21 + asdsd/protocols/default/message.py | 59 ++ asdsd/protocols/default/protocol.yaml | 6 + asdsd/protocols/default/serialization.py | 71 ++ asdsd/protocols/gym/__init__.py | 21 + asdsd/protocols/gym/message.py | 76 ++ asdsd/protocols/gym/protocol.yaml | 6 + asdsd/protocols/gym/serialization.py | 103 +++ asdsd/protocols/tac/__init__.py | 21 + asdsd/protocols/tac/message.py | 134 +++ asdsd/protocols/tac/protocol.yaml | 6 + asdsd/protocols/tac/serialization.py | 234 +++++ asdsd/protocols/tac/tac.proto | 102 +++ asdsd/protocols/tac/tac_pb2.py | 899 +++++++++++++++++++ asdsd/skills/__init__.py | 0 asdsd/skills/error/__init__.py | 20 + asdsd/skills/error/behaviours.py | 50 ++ asdsd/skills/error/handlers.py | 131 +++ asdsd/skills/error/skill.yaml | 15 + asdsd/skills/error/tasks.py | 51 ++ my_agent/aea-config.yaml | 31 + my_agent/connections/__init__.py | 0 my_agent/connections/oef/__init__.py | 21 + my_agent/connections/oef/connection.py | 604 +++++++++++++ my_agent/connections/oef/connection.yaml | 14 + my_agent/default_private_key.pem | 6 + my_agent/eth_private_key.txt | 1 + my_agent/fet_private_key.txt | 1 + my_agent/protocols/__init__.py | 0 my_agent/protocols/gym/__init__.py | 21 + my_agent/protocols/gym/message.py | 76 ++ my_agent/protocols/gym/protocol.yaml | 6 + my_agent/protocols/gym/serialization.py | 103 +++ my_agent/protocols/oef/__init__.py | 21 + my_agent/protocols/oef/message.py | 125 +++ my_agent/protocols/oef/models.py | 450 ++++++++++ my_agent/protocols/oef/protocol.yaml | 9 + my_agent/protocols/oef/serialization.py | 96 ++ my_agent/protocols/tac/__init__.py | 21 + my_agent/protocols/tac/message.py | 134 +++ my_agent/protocols/tac/protocol.yaml | 6 + my_agent/protocols/tac/serialization.py | 234 +++++ my_agent/protocols/tac/tac.proto | 102 +++ my_agent/protocols/tac/tac_pb2.py | 899 +++++++++++++++++++ my_agent/skills/__init__.py | 0 my_agent/skills/error/__init__.py | 20 + my_agent/skills/error/behaviours.py | 50 ++ my_agent/skills/error/handlers.py | 131 +++ my_agent/skills/error/skill.yaml | 15 + my_agent/skills/error/tasks.py | 51 ++ my_agent/skills/my_search/__init__.py | 20 + my_agent/skills/my_search/behaviours.py | 82 ++ my_agent/skills/my_search/handlers.py | 66 ++ my_agent/skills/my_search/my_shared_class.py | 26 + my_agent/skills/my_search/skill.yaml | 21 + my_agent/skills/my_search/tasks.py | 59 ++ 89 files changed, 8905 insertions(+), 2 deletions(-) create mode 100644 Test/aea-config.yaml create mode 100644 Test/connections/__init__.py create mode 100644 Test/connections/oef/__init__.py create mode 100644 Test/connections/oef/connection.py create mode 100644 Test/connections/oef/connection.yaml create mode 100644 Test/protocols/__init__.py create mode 100644 Test/protocols/default/__init__.py create mode 100644 Test/protocols/default/message.py create mode 100644 Test/protocols/default/protocol.yaml create mode 100644 Test/protocols/default/serialization.py create mode 100644 Test/protocols/gym/__init__.py create mode 100644 Test/protocols/gym/message.py create mode 100644 Test/protocols/gym/protocol.yaml create mode 100644 Test/protocols/gym/serialization.py create mode 100644 Test/protocols/tac/__init__.py create mode 100644 Test/protocols/tac/message.py create mode 100644 Test/protocols/tac/protocol.yaml create mode 100644 Test/protocols/tac/serialization.py create mode 100644 Test/protocols/tac/tac.proto create mode 100644 Test/protocols/tac/tac_pb2.py create mode 100644 Test/skills/__init__.py create mode 100644 Test/skills/error/__init__.py create mode 100644 Test/skills/error/behaviours.py create mode 100644 Test/skills/error/handlers.py create mode 100644 Test/skills/error/skill.yaml create mode 100644 Test/skills/error/tasks.py create mode 100644 asdsd/aea-config.yaml create mode 100644 asdsd/connections/__init__.py create mode 100644 asdsd/connections/oef/__init__.py create mode 100644 asdsd/connections/oef/connection.py create mode 100644 asdsd/connections/oef/connection.yaml create mode 100644 asdsd/protocols/__init__.py create mode 100644 asdsd/protocols/default/__init__.py create mode 100644 asdsd/protocols/default/message.py create mode 100644 asdsd/protocols/default/protocol.yaml create mode 100644 asdsd/protocols/default/serialization.py create mode 100644 asdsd/protocols/gym/__init__.py create mode 100644 asdsd/protocols/gym/message.py create mode 100644 asdsd/protocols/gym/protocol.yaml create mode 100644 asdsd/protocols/gym/serialization.py create mode 100644 asdsd/protocols/tac/__init__.py create mode 100644 asdsd/protocols/tac/message.py create mode 100644 asdsd/protocols/tac/protocol.yaml create mode 100644 asdsd/protocols/tac/serialization.py create mode 100644 asdsd/protocols/tac/tac.proto create mode 100644 asdsd/protocols/tac/tac_pb2.py create mode 100644 asdsd/skills/__init__.py create mode 100644 asdsd/skills/error/__init__.py create mode 100644 asdsd/skills/error/behaviours.py create mode 100644 asdsd/skills/error/handlers.py create mode 100644 asdsd/skills/error/skill.yaml create mode 100644 asdsd/skills/error/tasks.py create mode 100644 my_agent/aea-config.yaml create mode 100644 my_agent/connections/__init__.py create mode 100644 my_agent/connections/oef/__init__.py create mode 100644 my_agent/connections/oef/connection.py create mode 100644 my_agent/connections/oef/connection.yaml create mode 100644 my_agent/default_private_key.pem create mode 100644 my_agent/eth_private_key.txt create mode 100644 my_agent/fet_private_key.txt create mode 100644 my_agent/protocols/__init__.py create mode 100644 my_agent/protocols/gym/__init__.py create mode 100644 my_agent/protocols/gym/message.py create mode 100644 my_agent/protocols/gym/protocol.yaml create mode 100644 my_agent/protocols/gym/serialization.py create mode 100644 my_agent/protocols/oef/__init__.py create mode 100644 my_agent/protocols/oef/message.py create mode 100644 my_agent/protocols/oef/models.py create mode 100644 my_agent/protocols/oef/protocol.yaml create mode 100644 my_agent/protocols/oef/serialization.py create mode 100644 my_agent/protocols/tac/__init__.py create mode 100644 my_agent/protocols/tac/message.py create mode 100644 my_agent/protocols/tac/protocol.yaml create mode 100644 my_agent/protocols/tac/serialization.py create mode 100644 my_agent/protocols/tac/tac.proto create mode 100644 my_agent/protocols/tac/tac_pb2.py create mode 100644 my_agent/skills/__init__.py create mode 100644 my_agent/skills/error/__init__.py create mode 100644 my_agent/skills/error/behaviours.py create mode 100644 my_agent/skills/error/handlers.py create mode 100644 my_agent/skills/error/skill.yaml create mode 100644 my_agent/skills/error/tasks.py create mode 100644 my_agent/skills/my_search/__init__.py create mode 100644 my_agent/skills/my_search/behaviours.py create mode 100644 my_agent/skills/my_search/handlers.py create mode 100644 my_agent/skills/my_search/my_shared_class.py create mode 100644 my_agent/skills/my_search/skill.yaml create mode 100644 my_agent/skills/my_search/tasks.py diff --git a/Test/aea-config.yaml b/Test/aea-config.yaml new file mode 100644 index 0000000000..902c8f8c45 --- /dev/null +++ b/Test/aea-config.yaml @@ -0,0 +1,21 @@ +aea_version: 0.1.7 +agent_name: Test +authors: '' +connections: +- oef +default_connection: oef +description: '' +license: '' +logging_config: + disable_existing_loggers: false + version: 1 +private_key_paths: [] +protocols: +- default +- gym +- tac +registry_path: ../packages +skills: +- error +url: '' +version: v1 diff --git a/Test/connections/__init__.py b/Test/connections/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Test/connections/oef/__init__.py b/Test/connections/oef/__init__.py new file mode 100644 index 0000000000..21ee4d83df --- /dev/null +++ b/Test/connections/oef/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Implementation of the OEF connection.""" diff --git a/Test/connections/oef/connection.py b/Test/connections/oef/connection.py new file mode 100644 index 0000000000..7f489b867f --- /dev/null +++ b/Test/connections/oef/connection.py @@ -0,0 +1,604 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Extension to the OEF Python SDK.""" +import datetime +import logging +import pickle +from queue import Empty, Queue +from threading import Thread +from typing import List, Dict, Optional, cast + +import oef +from oef.agents import OEFAgent +from oef.core import AsyncioCore +from oef.messages import CFP_TYPES, PROPOSE_TYPES +from oef.query import ( + Query as OEFQuery, + ConstraintExpr as OEFConstraintExpr, + And as OEFAnd, + Or as OEFOr, + Not as OEFNot, + Constraint as OEFConstraint, + ConstraintType as OEFConstraintType, Eq, NotEq, Lt, LtEq, Gt, GtEq, Range, In, NotIn) +from oef.schema import Description as OEFDescription, DataModel as OEFDataModel, AttributeSchema as OEFAttribute + +from aea.configurations.base import ConnectionConfig +from aea.connections.base import Channel, Connection +from aea.mail.base import MailBox, Envelope +from aea.protocols.fipa.message import FIPAMessage +from aea.protocols.fipa.serialization import FIPASerializer +from aea.protocols.oef.message import OEFMessage +from aea.protocols.oef.models import Description, Attribute, DataModel, Query, ConstraintExpr, And, Or, Not, Constraint, \ + ConstraintType, ConstraintTypes +from aea.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF + +logger = logging.getLogger(__name__) + + +STUB_MESSSAGE_ID = 0 +STUB_DIALOGUE_ID = 0 + + +class OEFObjectTranslator: + """Translate our OEF object to object of OEF SDK classes.""" + + @classmethod + def to_oef_description(cls, desc: Description) -> OEFDescription: + """From our description to OEF description.""" + oef_data_model = cls.to_oef_data_model(desc.data_model) if desc.data_model is not None else None + return OEFDescription(desc.values, oef_data_model) + + @classmethod + def to_oef_data_model(cls, data_model: DataModel) -> OEFDataModel: + """From our data model to OEF data model.""" + oef_attributes = [cls.to_oef_attribute(attribute) for attribute in data_model.attributes] + return OEFDataModel(data_model.name, oef_attributes, data_model.description) + + @classmethod + def to_oef_attribute(cls, attribute: Attribute) -> OEFAttribute: + """From our attribute to OEF attribute.""" + return OEFAttribute(attribute.name, attribute.type, attribute.is_required, attribute.description) + + @classmethod + def to_oef_query(cls, query: Query) -> OEFQuery: + """From our query to OEF query.""" + oef_data_model = cls.to_oef_data_model(query.model) if query.model is not None else None + constraints = [cls.to_oef_constraint_expr(c) for c in query.constraints] + return OEFQuery(constraints, oef_data_model) + + @classmethod + def to_oef_constraint_expr(cls, constraint_expr: ConstraintExpr) -> OEFConstraintExpr: + """From our constraint expression to the OEF constraint expression.""" + if isinstance(constraint_expr, And): + return OEFAnd([cls.to_oef_constraint_expr(c) for c in constraint_expr.constraints]) + elif isinstance(constraint_expr, Or): + return OEFOr([cls.to_oef_constraint_expr(c) for c in constraint_expr.constraints]) + elif isinstance(constraint_expr, Not): + return OEFNot(cls.to_oef_constraint_expr(constraint_expr.constraint)) + elif isinstance(constraint_expr, Constraint): + oef_constraint_type = cls.to_oef_constraint_type(constraint_expr.constraint_type) + return OEFConstraint(constraint_expr.attribute_name, oef_constraint_type) + else: + raise ValueError("Constraint expression not supported.") + + @classmethod + def to_oef_constraint_type(cls, constraint_type: ConstraintType) -> OEFConstraintType: + """From our constraint type to OEF constraint type.""" + value = constraint_type.value + if constraint_type.type == ConstraintTypes.EQUAL: + return Eq(value) + elif constraint_type.type == ConstraintTypes.NOT_EQUAL: + return NotEq(value) + elif constraint_type.type == ConstraintTypes.LESS_THAN: + return Lt(value) + elif constraint_type.type == ConstraintTypes.LESS_THAN_EQ: + return LtEq(value) + elif constraint_type.type == ConstraintTypes.GREATER_THAN: + return Gt(value) + elif constraint_type.type == ConstraintTypes.GREATER_THAN_EQ: + return GtEq(value) + elif constraint_type.type == ConstraintTypes.WITHIN: + return Range(value) + elif constraint_type.type == ConstraintTypes.IN: + return In(value) + elif constraint_type.type == ConstraintTypes.NOT_IN: + return NotIn(value) + else: + raise ValueError("Constraint type not recognized.") + + @classmethod + def from_oef_description(cls, oef_desc: OEFDescription) -> Description: + """From an OEF description to our description.""" + data_model = cls.from_oef_data_model(oef_desc.data_model) if oef_desc.data_model is not None else None + return Description(oef_desc.values, data_model=data_model) + + @classmethod + def from_oef_data_model(cls, oef_data_model: OEFDataModel) -> DataModel: + """From an OEF data model to our data model.""" + attributes = [cls.from_oef_attribute(oef_attribute) for oef_attribute in oef_data_model.attribute_schemas] + return DataModel(oef_data_model.name, attributes, oef_data_model.description) + + @classmethod + def from_oef_attribute(cls, oef_attribute: OEFAttribute) -> Attribute: + """From an OEF attribute to our attribute.""" + return Attribute(oef_attribute.name, oef_attribute.type, oef_attribute.required, oef_attribute.description) + + @classmethod + def from_oef_query(cls, oef_query: OEFQuery) -> Query: + """From our query to OrOEF query.""" + data_model = cls.from_oef_data_model(oef_query.model) if oef_query.model is not None else None + constraints = [cls.from_oef_constraint_expr(c) for c in oef_query.constraints] + return Query(constraints, data_model) + + @classmethod + def from_oef_constraint_expr(cls, oef_constraint_expr: OEFConstraintExpr) -> ConstraintExpr: + """From our query to OEF query.""" + if isinstance(oef_constraint_expr, OEFAnd): + return And([cls.from_oef_constraint_expr(c) for c in oef_constraint_expr.constraints]) + elif isinstance(oef_constraint_expr, OEFOr): + return Or([cls.from_oef_constraint_expr(c) for c in oef_constraint_expr.constraints]) + elif isinstance(oef_constraint_expr, OEFNot): + return Not(cls.from_oef_constraint_expr(oef_constraint_expr.constraint)) + elif isinstance(oef_constraint_expr, OEFConstraint): + constraint_type = cls.from_oef_constraint_type(oef_constraint_expr.constraint) + return Constraint(oef_constraint_expr.attribute_name, constraint_type) + else: + raise ValueError("OEF Constraint not supported.") + + @classmethod + def from_oef_constraint_type(cls, constraint_type: OEFConstraintType) -> ConstraintType: + """From OEF constraint type to our constraint type.""" + if isinstance(constraint_type, Eq): + return ConstraintType(ConstraintTypes.EQUAL, constraint_type.value) + elif isinstance(constraint_type, NotEq): + return ConstraintType(ConstraintTypes.NOT_EQUAL, constraint_type.value) + elif isinstance(constraint_type, Lt): + return ConstraintType(ConstraintTypes.LESS_THAN, constraint_type.value) + elif isinstance(constraint_type, LtEq): + return ConstraintType(ConstraintTypes.LESS_THAN_EQ, constraint_type.value) + elif isinstance(constraint_type, Gt): + return ConstraintType(ConstraintTypes.GREATER_THAN, constraint_type.value) + elif isinstance(constraint_type, GtEq): + return ConstraintType(ConstraintTypes.GREATER_THAN_EQ, constraint_type.value) + elif isinstance(constraint_type, Range): + return ConstraintType(ConstraintTypes.WITHIN, constraint_type.values) + elif isinstance(constraint_type, In): + return ConstraintType(ConstraintTypes.IN, constraint_type.values) + elif isinstance(constraint_type, NotIn): + return ConstraintType(ConstraintTypes.NOT_IN, constraint_type.values) + else: + raise ValueError("Constraint type not recognized.") + + +class MailStats(object): + """The MailStats class tracks statistics on messages processed by MailBox.""" + + def __init__(self) -> None: + """ + Instantiate mail stats. + + :return: None + """ + self._search_count = 0 + self._search_start_time = {} # type: Dict[int, datetime.datetime] + self._search_timedelta = {} # type: Dict[int, float] + self._search_result_counts = {} # type: Dict[int, int] + + @property + def search_count(self) -> int: + """Get the search count.""" + return self._search_count + + def search_start(self, search_id: int) -> None: + """ + Add a search id and start time. + + :param search_id: the search id + + :return: None + """ + assert search_id not in self._search_start_time + self._search_count += 1 + self._search_start_time[search_id] = datetime.datetime.now() + + def search_end(self, search_id: int, nb_search_results: int) -> None: + """ + Add end time for a search id. + + :param search_id: the search id + :param nb_search_results: the number of agents returned in the search result + + :return: None + """ + assert search_id in self._search_start_time + assert search_id not in self._search_timedelta + self._search_timedelta[search_id] = (datetime.datetime.now() - self._search_start_time[search_id]).total_seconds() * 1000 + self._search_result_counts[search_id] = nb_search_results + + +class OEFChannel(OEFAgent, Channel): + """The OEFChannel connects the OEF Agent with the connection.""" + + def __init__(self, public_key: str, oef_addr: str, oef_port: int, core: AsyncioCore, in_queue: Queue): + """ + Initialize. + + :param public_key: the public key of the agent. + :param oef_addr: the OEF IP address. + :param oef_port: the OEF port. + :param in_queue: the in queue. + """ + super().__init__(public_key, oef_addr=oef_addr, oef_port=oef_port, core=core) + self.in_queue = in_queue + self.mail_stats = MailStats() + + def on_message(self, msg_id: int, dialogue_id: int, origin: str, content: bytes) -> None: + """ + On message event handler. + + :param msg_id: the message id. + :param dialogue_id: the dialogue id. + :param origin: the public key of the sender. + :param content: the bytes content. + :return: None + """ + # We are not using the 'origin' parameter because 'content' contains a serialized instance of 'Envelope', + # hence it already contains the address of the sender. + envelope = Envelope.decode(content) + self.in_queue.put(envelope) + + def on_cfp(self, msg_id: int, dialogue_id: int, origin: str, target: int, query: CFP_TYPES) -> None: + """ + On cfp event handler. + + :param msg_id: the message id. + :param dialogue_id: the dialogue id. + :param origin: the public key of the sender. + :param target: the message target. + :param query: the query. + :return: None + """ + try: + query = pickle.loads(query) + except Exception: + pass + msg = FIPAMessage(message_id=msg_id, + dialogue_id=dialogue_id, + target=target, + performative=FIPAMessage.Performative.CFP, + query=query if query != b"" else None) + msg_bytes = FIPASerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_propose(self, msg_id: int, dialogue_id: int, origin: str, target: int, b_proposals: PROPOSE_TYPES) -> None: + """ + On propose event handler. + + :param msg_id: the message id. + :param dialogue_id: the dialogue id. + :param origin: the public key of the sender. + :param target: the message target. + :param b_proposals: the proposals. + :return: None + """ + if type(b_proposals) == bytes: + proposals = pickle.loads(b_proposals) # type: List[Description] + else: + raise ValueError("No support for non-bytes proposals.") + + msg = FIPAMessage(message_id=msg_id, + dialogue_id=dialogue_id, + target=target, + performative=FIPAMessage.Performative.PROPOSE, + proposal=proposals) + msg_bytes = FIPASerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_accept(self, msg_id: int, dialogue_id: int, origin: str, target: int) -> None: + """ + On accept event handler. + + :param msg_id: the message id. + :param dialogue_id: the dialogue id. + :param origin: the public key of the sender. + :param target: the message target. + :return: None + """ + performative = FIPAMessage.Performative.MATCH_ACCEPT if msg_id == 4 and target == 3 else FIPAMessage.Performative.ACCEPT + msg = FIPAMessage(message_id=msg_id, + dialogue_id=dialogue_id, + target=target, + performative=performative) + msg_bytes = FIPASerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_decline(self, msg_id: int, dialogue_id: int, origin: str, target: int) -> None: + """ + On decline event handler. + + :param msg_id: the message id. + :param dialogue_id: the dialogue id. + :param origin: the public key of the sender. + :param target: the message target. + :return: None + """ + msg = FIPAMessage(message_id=msg_id, + dialogue_id=dialogue_id, + target=target, + performative=FIPAMessage.Performative.DECLINE) + msg_bytes = FIPASerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_search_result(self, search_id: int, agents: List[str]) -> None: + """ + On accept event handler. + + :param search_id: the search id. + :param agents: the list of agents. + :return: None + """ + self.mail_stats.search_end(search_id, len(agents)) + msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_RESULT, id=search_id, agents=agents) + msg_bytes = OEFSerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_oef_error(self, answer_id: int, operation: oef.messages.OEFErrorOperation) -> None: + """ + On oef error event handler. + + :param answer_id: the answer id. + :param operation: the error operation. + :return: None + """ + try: + operation = OEFMessage.OEFErrorOperation(operation) + except ValueError: + operation = OEFMessage.OEFErrorOperation.OTHER + + msg = OEFMessage(oef_type=OEFMessage.Type.OEF_ERROR, id=answer_id, operation=operation) + msg_bytes = OEFSerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_dialogue_error(self, answer_id: int, dialogue_id: int, origin: str) -> None: + """ + On dialogue error event handler. + + :param answer_id: the answer id. + :param dialogue_id: the dialogue id. + :param origin: the message sender. + :return: None + """ + msg = OEFMessage(oef_type=OEFMessage.Type.DIALOGUE_ERROR, + id=answer_id, + dialogue_id=dialogue_id, + origin=origin) + msg_bytes = OEFSerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def send(self, envelope: Envelope) -> None: + """ + Send message handler. + + :param envelope: the message. + :return: None + """ + if envelope.protocol_id == "default": + self.send_default_message(envelope) + elif envelope.protocol_id == "fipa": + self.send_fipa_message(envelope) + elif envelope.protocol_id == "oef": + self.send_oef_message(envelope) + elif envelope.protocol_id == "tac": + self.send_default_message(envelope) + else: + logger.error("This envelope cannot be sent: protocol_id={}".format(envelope.protocol_id)) + raise ValueError("Cannot send message.") + + def send_default_message(self, envelope: Envelope): + """Send a 'default' message.""" + self.send_message(STUB_MESSSAGE_ID, STUB_DIALOGUE_ID, envelope.to, envelope.encode()) + + def send_fipa_message(self, envelope: Envelope) -> None: + """ + Send fipa message handler. + + :param envelope: the message. + :return: None + """ + fipa_message = FIPASerializer().decode(envelope.message) + id = fipa_message.get("message_id") + dialogue_id = fipa_message.get("dialogue_id") + destination = envelope.to + target = fipa_message.get("target") + performative = FIPAMessage.Performative(fipa_message.get("performative")) + if performative == FIPAMessage.Performative.CFP: + query = fipa_message.get("query") + query = b"" if query is None else query + if type(query) == Query: + query = pickle.dumps(query) + self.send_cfp(id, dialogue_id, destination, target, query) + elif performative == FIPAMessage.Performative.PROPOSE: + proposal = cast(List[Description], fipa_message.get("proposal")) + proposal_b = pickle.dumps(proposal) # type: bytes + self.send_propose(id, dialogue_id, destination, target, proposal_b) + elif performative == FIPAMessage.Performative.ACCEPT: + self.send_accept(id, dialogue_id, destination, target) + elif performative == FIPAMessage.Performative.MATCH_ACCEPT: + self.send_accept(id, dialogue_id, destination, target) + elif performative == FIPAMessage.Performative.DECLINE: + self.send_decline(id, dialogue_id, destination, target) + else: + raise ValueError("OEF FIPA message not recognized.") + + def send_oef_message(self, envelope: Envelope) -> None: + """ + Send oef message handler. + + :param envelope: the message. + :return: None + """ + oef_message = OEFSerializer().decode(envelope.message) + oef_type = OEFMessage.Type(oef_message.get("type")) + oef_msg_id = cast(int, oef_message.get("id")) + if oef_type == OEFMessage.Type.REGISTER_SERVICE: + service_description = cast(Description, oef_message.get("service_description")) + service_id = cast(int, oef_message.get("service_id")) + oef_service_description = OEFObjectTranslator.to_oef_description(service_description) + self.register_service(oef_msg_id, oef_service_description, service_id) + elif oef_type == OEFMessage.Type.UNREGISTER_SERVICE: + service_description = cast(Description, oef_message.get("service_description")) + service_id = cast(int, oef_message.get("service_id")) + oef_service_description = OEFObjectTranslator.to_oef_description(service_description) + self.unregister_service(oef_msg_id, oef_service_description, service_id) + elif oef_type == OEFMessage.Type.SEARCH_AGENTS: + query = cast(Query, oef_message.get("query")) + oef_query = OEFObjectTranslator.to_oef_query(query) + self.search_agents(oef_msg_id, oef_query) + elif oef_type == OEFMessage.Type.SEARCH_SERVICES: + query = cast(Query, oef_message.get("query")) + oef_query = OEFObjectTranslator.to_oef_query(query) + self.mail_stats.search_start(oef_msg_id) + self.search_services(oef_msg_id, oef_query) + else: + raise ValueError("OEF request not recognized.") + + +class OEFConnection(Connection): + """The OEFConnection connects the to the mailbox.""" + + def __init__(self, public_key: str, oef_addr: str, oef_port: int = 10000): + """ + Initialize. + + :param public_key: the public key of the agent. + :param oef_addr: the OEF IP address. + :param oef_port: the OEF port. + """ + super().__init__() + core = AsyncioCore(logger=logger) + self._core = core # type: AsyncioCore + self.channel = OEFChannel(public_key, oef_addr, oef_port, core=core, in_queue=self.in_queue) + + self._stopped = True + self._connected = False + self.out_thread = None # type: Optional[Thread] + + @property + def is_established(self) -> bool: + """Get the connection status.""" + return self._connected + + def _fetch(self) -> None: + """ + Fetch the messages from the outqueue and send them. + + :return: None + """ + while self._connected: + try: + msg = self.out_queue.get(block=True, timeout=1.0) + self.send(msg) + except Empty: + pass + + def connect(self) -> None: + """ + Connect to the channel. + + :return: None + :raises ConnectionError if the connection to the OEF fails. + """ + if self._stopped and not self._connected: + self._stopped = False + self._core.run_threaded() + try: + if not self.channel.connect(): + raise ConnectionError("Cannot connect to OEFChannel.") + self._connected = True + self.out_thread = Thread(target=self._fetch) + self.out_thread.start() + except ConnectionError as e: + self._core.stop() + raise e + + def disconnect(self) -> None: + """ + Disconnect from the channel. + + :return: None + """ + assert self.out_thread is not None, "Call connect before disconnect." + if not self._stopped and self._connected: + self._connected = False + self.out_thread.join() + self.out_thread = None + self.channel.disconnect() + self._core.stop() + self._stopped = True + + def send(self, envelope: Envelope): + """ + Send messages. + + :return: None + """ + if self._connected: + self.channel.send(envelope) + + @classmethod + def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection': + """ + Get the OEF connection from the connection configuration. + + :param public_key: the public key of the agent. + :param connection_configuration: the connection configuration object. + :return: the connection object + """ + oef_addr = cast(str, connection_configuration.config.get("addr")) + oef_port = cast(int, connection_configuration.config.get("port")) + return OEFConnection(public_key, oef_addr, oef_port) + + +class OEFMailBox(MailBox): + """The OEF mail box.""" + + def __init__(self, public_key: str, oef_addr: str, oef_port: int = 10000): + """ + Initialize. + + :param public_key: the public key of the agent. + :param oef_addr: the OEF IP address. + :param oef_port: the OEF port. + """ + connection = OEFConnection(public_key, oef_addr, oef_port) + super().__init__(connection) + + @property + def mail_stats(self) -> MailStats: + """Get the mail stats object.""" + return self._connection.channel.mail_stats # type: ignore diff --git a/Test/connections/oef/connection.yaml b/Test/connections/oef/connection.yaml new file mode 100644 index 0000000000..6a69ed33d1 --- /dev/null +++ b/Test/connections/oef/connection.yaml @@ -0,0 +1,18 @@ +name: oef +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +description: "The oef connection provides a wrapper around the OEF sdk." +class_name: OEFConnection +supported_protocols: + - default + - oef + - fipa + - tac +config: + addr: ${OEF_ADDR:127.0.0.1} + port: ${OEF_PORT:10000} +dependencies: + - colorlog + - oef diff --git a/Test/protocols/__init__.py b/Test/protocols/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Test/protocols/default/__init__.py b/Test/protocols/default/__init__.py new file mode 100644 index 0000000000..52e51b51e3 --- /dev/null +++ b/Test/protocols/default/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the support resources for the default protocol.""" diff --git a/Test/protocols/default/message.py b/Test/protocols/default/message.py new file mode 100644 index 0000000000..475714a2f0 --- /dev/null +++ b/Test/protocols/default/message.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the default message definition.""" +from enum import Enum +from typing import Optional + +from aea.protocols.base import Message + + +class DefaultMessage(Message): + """The Default message class.""" + + protocol_id = "default" + + class Type(Enum): + """Default message types.""" + + BYTES = "bytes" + ERROR = "error" + + def __str__(self): + """Get the string representation.""" + return self.value + + class ErrorCode(Enum): + """The error codes.""" + + UNSUPPORTED_PROTOCOL = -10001 + DECODING_ERROR = -10002 + INVALID_MESSAGE = -10003 + UNSUPPORTED_SKILL = -10004 + + def __init__(self, type: Optional[Type] = None, + **kwargs): + """ + Initialize. + + :param type: the type. + """ + super().__init__(type=type, **kwargs) + assert self.check_consistency(), "DefaultMessage initialization inconsistent." diff --git a/Test/protocols/default/protocol.yaml b/Test/protocols/default/protocol.yaml new file mode 100644 index 0000000000..66d97cfc83 --- /dev/null +++ b/Test/protocols/default/protocol.yaml @@ -0,0 +1,6 @@ +name: 'default' +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +description: "The default protocol allows for any bytes message." \ No newline at end of file diff --git a/Test/protocols/default/serialization.py b/Test/protocols/default/serialization.py new file mode 100644 index 0000000000..080b8f386b --- /dev/null +++ b/Test/protocols/default/serialization.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Serialization module for the default protocol.""" +import base64 +import json +from typing import cast + +from aea.protocols.base import Message +from aea.protocols.base import Serializer +from aea.protocols.default.message import DefaultMessage + + +class DefaultSerializer(Serializer): + """Serialization for the 'default' protocol.""" + + def encode(self, msg: Message) -> bytes: + """Encode a 'default' message into bytes.""" + body = {} # Dict[str, Any] + + msg_type = DefaultMessage.Type(msg.get("type")) + body["type"] = str(msg_type.value) + + if msg_type == DefaultMessage.Type.BYTES: + content = cast(bytes, msg.get("content")) + body["content"] = base64.b64encode(content).decode("utf-8") + elif msg_type == DefaultMessage.Type.ERROR: + body["error_code"] = cast(str, msg.get("error_code")) + body["error_msg"] = cast(str, msg.get("error_msg")) + body["error_data"] = cast(str, msg.get("error_data")) + else: + raise ValueError("Type not recognized.") + + bytes_msg = json.dumps(body).encode("utf-8") + return bytes_msg + + def decode(self, obj: bytes) -> Message: + """Decode bytes into a 'default' message.""" + json_body = json.loads(obj.decode("utf-8")) + body = {} + + msg_type = DefaultMessage.Type(json_body["type"]) + body["type"] = msg_type + if msg_type == DefaultMessage.Type.BYTES: + content = base64.b64decode(json_body["content"].encode("utf-8")) + body["content"] = content # type: ignore + elif msg_type == DefaultMessage.Type.ERROR: + body["error_code"] = json_body["error_code"] + body["error_msg"] = json_body["error_msg"] + body["error_data"] = json_body["error_data"] + else: + raise ValueError("Type not recognized.") + + return DefaultMessage(type=msg_type, body=body) diff --git a/Test/protocols/gym/__init__.py b/Test/protocols/gym/__init__.py new file mode 100644 index 0000000000..a1766ab9cd --- /dev/null +++ b/Test/protocols/gym/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the support resources for the Gym protocol.""" diff --git a/Test/protocols/gym/message.py b/Test/protocols/gym/message.py new file mode 100644 index 0000000000..616b3e8226 --- /dev/null +++ b/Test/protocols/gym/message.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the FIPA message definition.""" +from enum import Enum +from typing import Optional, Union + +from aea.protocols.base import Message + + +class GymMessage(Message): + """The Gym message class.""" + + protocol_id = "gym" + + class Performative(Enum): + """Gym performatives.""" + + ACT = 'act' + PERCEPT = 'percept' + RESET = 'reset' + CLOSE = 'close' + + def __str__(self): + """Get string representation.""" + return self.value + + def __init__(self, performative: Optional[Union[str, Performative]] = None, **kwargs): + """ + Initialize. + + :param type: the type. + """ + super().__init__(performative=GymMessage.Performative(performative), **kwargs) + assert self.check_consistency(), "GymMessage initialization inconsistent." + + def check_consistency(self) -> bool: + """Check that the data is consistent.""" + try: + assert self.is_set("performative") + performative = GymMessage.Performative(self.get("performative")) + if performative == GymMessage.Performative.ACT: + assert self.is_set("action") + assert self.is_set("step_id") + elif performative == GymMessage.Performative.PERCEPT: + assert self.is_set("observation") + assert self.is_set("reward") + assert self.is_set("done") + assert self.is_set("info") + assert self.is_set("step_id") + elif performative == GymMessage.Performative.RESET or performative == GymMessage.Performative.CLOSE: + pass + else: + raise ValueError("Performative not recognized.") + + except (AssertionError, ValueError, KeyError): + return False + + return True diff --git a/Test/protocols/gym/protocol.yaml b/Test/protocols/gym/protocol.yaml new file mode 100644 index 0000000000..7b10d5429b --- /dev/null +++ b/Test/protocols/gym/protocol.yaml @@ -0,0 +1,6 @@ +name: gym +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +description: "The gym protocol implements the messages an agent needs to engage with a gym connection." \ No newline at end of file diff --git a/Test/protocols/gym/serialization.py b/Test/protocols/gym/serialization.py new file mode 100644 index 0000000000..6fb14c1354 --- /dev/null +++ b/Test/protocols/gym/serialization.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Serialization for the FIPA protocol.""" +import base64 +import copy +import json +import pickle +from typing import Any, TYPE_CHECKING + +from aea.protocols.base import Message +from aea.protocols.base import Serializer + +if TYPE_CHECKING: + from packages.protocols.gym.message import GymMessage +else: + from gym_protocol.message import GymMessage + + +class GymSerializer(Serializer): + """Serialization for the Gym protocol.""" + + def encode(self, msg: Message) -> bytes: + """ + Decode the message. + + :param msg: the message object + :return: the bytes + """ + performative = GymMessage.Performative(msg.get("performative")) + new_body = copy.copy(msg.body) + new_body["performative"] = performative.value + + if performative == GymMessage.Performative.ACT: + action = msg.body["action"] # type: Any + action_bytes = base64.b64encode(pickle.dumps(action)).decode("utf-8") + new_body["action"] = action_bytes + new_body["step_id"] = msg.body["step_id"] + elif performative == GymMessage.Performative.PERCEPT: + # observation, reward and info are gym implementation specific, done is boolean + observation = msg.body["observation"] # type: Any + observation_bytes = base64.b64encode(pickle.dumps(observation)).decode("utf-8") + new_body["observation"] = observation_bytes + reward = msg.body["reward"] # type: Any + reward_bytes = base64.b64encode(pickle.dumps(reward)).decode("utf-8") + new_body["reward"] = reward_bytes + info = msg.body["info"] # type: Any + info_bytes = base64.b64encode(pickle.dumps(info)).decode("utf-8") + new_body["info"] = info_bytes + new_body["step_id"] = msg.body["step_id"] + + gym_message_bytes = json.dumps(new_body).encode("utf-8") + return gym_message_bytes + + def decode(self, obj: bytes) -> Message: + """ + Decode the message. + + :param obj: the bytes object + :return: the message + """ + json_msg = json.loads(obj.decode("utf-8")) + performative = GymMessage.Performative(json_msg["performative"]) + new_body = copy.copy(json_msg) + new_body["type"] = performative + + if performative == GymMessage.Performative.ACT: + action_bytes = base64.b64decode(json_msg["action"]) + action = pickle.loads(action_bytes) + new_body["action"] = action + new_body["step_id"] = json_msg["step_id"] + elif performative == GymMessage.Performative.PERCEPT: + # observation, reward and info are gym implementation specific, done is boolean + observation_bytes = base64.b64decode(json_msg["observation"]) + observation = pickle.loads(observation_bytes) + new_body["observation"] = observation + reward_bytes = base64.b64decode(json_msg["reward"]) + reward = pickle.loads(reward_bytes) + new_body["reward"] = reward + info_bytes = base64.b64decode(json_msg["info"]) + info = pickle.loads(info_bytes) + new_body["info"] = info + new_body["step_id"] = json_msg["step_id"] + + gym_message = GymMessage(performative=performative, body=new_body) + return gym_message diff --git a/Test/protocols/tac/__init__.py b/Test/protocols/tac/__init__.py new file mode 100644 index 0000000000..430d160a4f --- /dev/null +++ b/Test/protocols/tac/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the support resources for the TAC protocol.""" diff --git a/Test/protocols/tac/message.py b/Test/protocols/tac/message.py new file mode 100644 index 0000000000..07a100d90e --- /dev/null +++ b/Test/protocols/tac/message.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the default message definition.""" +from enum import Enum +from typing import Dict, Optional, cast + +from aea.protocols.base import Message + + +class TACMessage(Message): + """The TAC message class.""" + + protocol_id = "tac" + + class Type(Enum): + """TAC Message types.""" + + REGISTER = "register" + UNREGISTER = "unregister" + TRANSACTION = "transaction" + GET_STATE_UPDATE = "get_state_update" + CANCELLED = "cancelled" + GAME_DATA = "game_data" + TRANSACTION_CONFIRMATION = "transaction_confirmation" + STATE_UPDATE = "state_update" + TAC_ERROR = "tac_error" + + def __str__(self): + """Get string representation.""" + return self.value + + class ErrorCode(Enum): + """This class defines the error codes.""" + + GENERIC_ERROR = 0 + REQUEST_NOT_VALID = 1 + AGENT_PBK_ALREADY_REGISTERED = 2 + AGENT_NAME_ALREADY_REGISTERED = 3 + AGENT_NOT_REGISTERED = 4 + TRANSACTION_NOT_VALID = 5 + TRANSACTION_NOT_MATCHING = 6 + AGENT_NAME_NOT_IN_WHITELIST = 7 + COMPETITION_NOT_RUNNING = 8 + DIALOGUE_INCONSISTENT = 9 + + _from_ec_to_msg = { + ErrorCode.GENERIC_ERROR: "Unexpected error.", + ErrorCode.REQUEST_NOT_VALID: "Request not recognized", + ErrorCode.AGENT_PBK_ALREADY_REGISTERED: "Agent pbk already registered.", + ErrorCode.AGENT_NAME_ALREADY_REGISTERED: "Agent name already registered.", + ErrorCode.AGENT_NOT_REGISTERED: "Agent not registered.", + ErrorCode.TRANSACTION_NOT_VALID: "Error in checking transaction", + ErrorCode.TRANSACTION_NOT_MATCHING: "The transaction request does not match with a previous transaction request with the same id.", + ErrorCode.AGENT_NAME_NOT_IN_WHITELIST: "Agent name not in whitelist.", + ErrorCode.COMPETITION_NOT_RUNNING: "The competition is not running yet.", + ErrorCode.DIALOGUE_INCONSISTENT: "The message is inconsistent with the dialogue." + } # type: Dict[ErrorCode, str] + + def __init__(self, tac_type: Optional[Type] = None, + **kwargs): + """ + Initialize. + + :param tac_type: the type of TAC message. + """ + super().__init__(type=tac_type, **kwargs) + assert self.check_consistency(), "TACMessage initialization inconsistent." + + def check_consistency(self) -> bool: + """Check that the data is consistent.""" + try: + assert self.is_set("type") + tac_type = TACMessage.Type(self.get("type")) + if tac_type == TACMessage.Type.REGISTER: + assert self.is_set("agent_name") + elif tac_type == TACMessage.Type.UNREGISTER: + pass + elif tac_type == TACMessage.Type.TRANSACTION: + assert self.is_set("transaction_id") + assert self.is_set("is_sender_buyer") + assert self.is_set("counterparty") + assert self.is_set("amount") + amount = cast(float, self.get("amount")) + assert amount >= 0.0 + assert self.is_set("quantities_by_good_pbk") + quantities_by_good_pbk = cast(Dict[str, int], self.get("quantities_by_good_pbk")) + assert len(quantities_by_good_pbk.keys()) == len(set(quantities_by_good_pbk.keys())) + assert all(quantity >= 0 for quantity in quantities_by_good_pbk.values()) + elif tac_type == TACMessage.Type.GET_STATE_UPDATE: + pass + elif tac_type == TACMessage.Type.CANCELLED: + pass + elif tac_type == TACMessage.Type.GAME_DATA: + assert self.is_set("money") + assert self.is_set("endowment") + assert self.is_set("utility_params") + assert self.is_set("nb_agents") + assert self.is_set("nb_goods") + assert self.is_set("tx_fee") + assert self.is_set("agent_pbk_to_name") + assert self.is_set("good_pbk_to_name") + elif tac_type == TACMessage.Type.TRANSACTION_CONFIRMATION: + assert self.is_set("transaction_id") + elif tac_type == TACMessage.Type.STATE_UPDATE: + assert self.is_set("initial_state") + assert self.is_set("transactions") + elif tac_type == TACMessage.Type.TAC_ERROR: + assert self.is_set("error_code") + error_code = self.get("error_code") + assert error_code in set(self.ErrorCode) + else: + raise ValueError("Type not recognized.") + except (AssertionError, ValueError): + return False + + return True diff --git a/Test/protocols/tac/protocol.yaml b/Test/protocols/tac/protocol.yaml new file mode 100644 index 0000000000..435e33b383 --- /dev/null +++ b/Test/protocols/tac/protocol.yaml @@ -0,0 +1,6 @@ +name: tac +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +description: "The tac protocol implements the messages an AEA needs to participate in the TAC." \ No newline at end of file diff --git a/Test/protocols/tac/serialization.py b/Test/protocols/tac/serialization.py new file mode 100644 index 0000000000..e4e085d5b0 --- /dev/null +++ b/Test/protocols/tac/serialization.py @@ -0,0 +1,234 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Serialization for the TAC protocol.""" + +from typing import Any, Dict, List, cast, TYPE_CHECKING + +from aea.protocols.base import Message +from aea.protocols.base import Serializer + +if TYPE_CHECKING: + from packages.protocols.tac import tac_pb2 + from packages.protocols.tac.message import TACMessage +else: + import tac_protocol.tac_pb2 as tac_pb2 + from tac_protocol.message import TACMessage + + +def _from_dict_to_pairs(d): + """Convert a flat dictionary into a list of StrStrPair or StrIntPair.""" + result = [] + items = sorted(d.items(), key=lambda pair: pair[0]) + for key, value in items: + if type(value) == int: + pair = tac_pb2.StrIntPair() + elif type(value) == str: + pair = tac_pb2.StrStrPair() + else: + raise ValueError("Either 'int' or 'str', not {}".format(type(value))) + pair.first = key + pair.second = value + result.append(pair) + return result + + +def _from_pairs_to_dict(pairs): + """Convert a list of StrStrPair or StrIntPair into a flat dictionary.""" + result = {} + for pair in pairs: + key = pair.first + value = pair.second + result[key] = value + return result + + +class TACSerializer(Serializer): + """Serialization for the TAC protocol.""" + + def encode(self, msg: Message) -> bytes: + """ + Decode the message. + + :param msg: the message object + :return: the bytes + """ + tac_type = TACMessage.Type(msg.get("type")) + tac_container = tac_pb2.TACMessage() + + if tac_type == TACMessage.Type.REGISTER: + agent_name = msg.get("agent_name") + tac_msg = tac_pb2.TACAgent.Register() # type: ignore + tac_msg.agent_name = agent_name + tac_container.register.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.UNREGISTER: + tac_msg = tac_pb2.TACAgent.Unregister() # type: ignore + tac_container.unregister.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.TRANSACTION: + tac_msg = tac_pb2.TACAgent.Transaction() # type: ignore + tac_msg.transaction_id = msg.get("transaction_id") + tac_msg.is_sender_buyer = msg.get("is_sender_buyer") + tac_msg.counterparty = msg.get("counterparty") + tac_msg.amount = msg.get("amount") + tac_msg.quantities.extend(_from_dict_to_pairs(msg.get("quantities_by_good_pbk"))) + tac_container.transaction.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.GET_STATE_UPDATE: + tac_msg = tac_pb2.TACAgent.GetStateUpdate() # type: ignore + tac_container.get_state_update.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.CANCELLED: + tac_msg = tac_pb2.TACController.Cancelled() # type: ignore + tac_container.cancelled.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.GAME_DATA: + tac_msg = tac_pb2.TACController.GameData() # type: ignore + tac_msg.money = msg.get("money") + tac_msg.endowment.extend(msg.get("endowment")) + tac_msg.utility_params.extend(msg.get("utility_params")) + tac_msg.nb_agents = msg.get("nb_agents") + tac_msg.nb_goods = msg.get("nb_goods") + tac_msg.tx_fee = msg.get("tx_fee") + tac_msg.agent_pbk_to_name.extend(_from_dict_to_pairs(msg.get("agent_pbk_to_name"))) + tac_msg.good_pbk_to_name.extend(_from_dict_to_pairs(msg.get("good_pbk_to_name"))) + tac_container.game_data.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.TRANSACTION_CONFIRMATION: + tac_msg = tac_pb2.TACController.TransactionConfirmation() # type: ignore + tac_msg.transaction_id = msg.get("transaction_id") + tac_container.transaction_confirmation.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.STATE_UPDATE: + tac_msg = tac_pb2.TACController.StateUpdate() # type: ignore + game_data_json = msg.get("initial_state") + game_data = tac_pb2.TACController.GameData() # type: ignore + game_data.money = game_data_json["money"] # type: ignore + game_data.endowment.extend(game_data_json["endowment"]) # type: ignore + game_data.utility_params.extend(game_data_json["utility_params"]) # type: ignore + game_data.nb_agents = game_data_json["nb_agents"] # type: ignore + game_data.nb_goods = game_data_json["nb_goods"] # type: ignore + game_data.tx_fee = game_data_json["tx_fee"] # type: ignore + game_data.agent_pbk_to_name.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["agent_pbk_to_name"]))) # type: ignore + game_data.good_pbk_to_name.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["good_pbk_to_name"]))) # type: ignore + + tac_msg.initial_state.CopyFrom(game_data) + + transactions = [] + msg_transactions = cast(List[Any], msg.get("transactions")) + for t in msg_transactions: + tx = tac_pb2.TACAgent.Transaction() # type: ignore + tx.transaction_id = t.get("transaction_id") + tx.is_sender_buyer = t.get("is_sender_buyer") + tx.counterparty = t.get("counterparty") + tx.amount = t.get("amount") + tx.quantities.extend(_from_dict_to_pairs(t.get("quantities_by_good_pbk"))) + transactions.append(tx) + tac_msg.txs.extend(transactions) + tac_container.state_update.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.TAC_ERROR: + tac_msg = tac_pb2.TACController.Error() # type: ignore + tac_msg.error_code = TACMessage.ErrorCode(msg.get("error_code")).value + if msg.is_set("error_msg"): + tac_msg.error_msg = msg.get("error_msg") + if msg.is_set("details"): + tac_msg.details.update(msg.get("details")) + + tac_container.error.CopyFrom(tac_msg) + else: + raise ValueError("Type not recognized: {}.".format(tac_type)) + + tac_message_bytes = tac_container.SerializeToString() + return tac_message_bytes + + def decode(self, obj: bytes) -> Message: + """ + Decode the message. + + :param obj: the bytes object + :return: the message + """ + tac_container = tac_pb2.TACMessage() + tac_container.ParseFromString(obj) + + new_body = {} # type: Dict[str, Any] + tac_type = tac_container.WhichOneof("content") + + if tac_type == "register": + new_body["type"] = TACMessage.Type.REGISTER + new_body["agent_name"] = tac_container.register.agent_name + elif tac_type == "unregister": + new_body["type"] = TACMessage.Type.UNREGISTER + elif tac_type == "transaction": + new_body["type"] = TACMessage.Type.TRANSACTION + new_body["transaction_id"] = tac_container.transaction.transaction_id + new_body["is_sender_buyer"] = tac_container.transaction.is_sender_buyer + new_body["counterparty"] = tac_container.transaction.counterparty + new_body["amount"] = tac_container.transaction.amount + new_body["quantities_by_good_pbk"] = _from_pairs_to_dict(tac_container.transaction.quantities) + elif tac_type == "get_state_update": + new_body["type"] = TACMessage.Type.GET_STATE_UPDATE + elif tac_type == "cancelled": + new_body["type"] = TACMessage.Type.CANCELLED + elif tac_type == "game_data": + new_body["type"] = TACMessage.Type.GAME_DATA + new_body["money"] = tac_container.game_data.money + new_body["endowment"] = list(tac_container.game_data.endowment) + new_body["utility_params"] = list(tac_container.game_data.utility_params) + new_body["nb_agents"] = tac_container.game_data.nb_agents + new_body["nb_goods"] = tac_container.game_data.nb_goods + new_body["tx_fee"] = tac_container.game_data.tx_fee + new_body["agent_pbk_to_name"] = _from_pairs_to_dict(tac_container.game_data.agent_pbk_to_name) + new_body["good_pbk_to_name"] = _from_pairs_to_dict(tac_container.game_data.good_pbk_to_name) + elif tac_type == "transaction_confirmation": + new_body["type"] = TACMessage.Type.TRANSACTION_CONFIRMATION + new_body["transaction_id"] = tac_container.transaction_confirmation.transaction_id + elif tac_type == "state_update": + new_body["type"] = TACMessage.Type.STATE_UPDATE + game_data = dict( + money=tac_container.state_update.initial_state.money, + endowment=tac_container.state_update.initial_state.endowment, + utility_params=tac_container.state_update.initial_state.utility_params, + nb_agents=tac_container.state_update.initial_state.nb_agents, + nb_goods=tac_container.state_update.initial_state.nb_goods, + tx_fee=tac_container.state_update.initial_state.tx_fee, + agent_pbk_to_name=_from_pairs_to_dict(tac_container.state_update.initial_state.agent_pbk_to_name), + good_pbk_to_name=_from_pairs_to_dict(tac_container.state_update.initial_state.good_pbk_to_name), + ) + new_body["initial_state"] = game_data + transactions = [] + for t in tac_container.state_update.txs: + tx_json = dict( + transaction_id=t.transaction_id, + is_sender_buyer=t.is_sender_buyer, + counterparty=t.counterparty, + amount=t.amount, + quantities_by_good_pbk=_from_pairs_to_dict(t.quantities), + ) + transactions.append(tx_json) + new_body["transactions"] = transactions + elif tac_type == "error": + new_body["type"] = TACMessage.Type.TAC_ERROR + new_body["error_code"] = TACMessage.ErrorCode(tac_container.error.error_code) + if tac_container.error.error_msg: + new_body["error_msg"] = tac_container.error.error_msg + if tac_container.error.details: + new_body["details"] = dict(tac_container.error.details) + else: + raise ValueError("Type not recognized.") + + tac_type = TACMessage.Type(new_body["type"]) + new_body["type"] = tac_type + tac_message = TACMessage(tac_type=tac_type, body=new_body) + return tac_message diff --git a/Test/protocols/tac/tac.proto b/Test/protocols/tac/tac.proto new file mode 100644 index 0000000000..d6a714d425 --- /dev/null +++ b/Test/protocols/tac/tac.proto @@ -0,0 +1,102 @@ +syntax = "proto3"; + +package fetch.oef.pb; + +import "google/protobuf/struct.proto"; + +message StrIntPair { + string first = 1; + int32 second = 2; +} + +message StrStrPair { + string first = 1; + string second = 2; +} + +message TACController { + + message Registered { + } + message Unregistered { + } + message Cancelled { + } + + message GameData { + double money = 1; + repeated int32 endowment = 2; + repeated double utility_params = 3; + int32 nb_agents = 4; + int32 nb_goods = 5; + double tx_fee = 6; + repeated StrStrPair agent_pbk_to_name = 7; + repeated StrStrPair good_pbk_to_name = 8; + } + + message TransactionConfirmation { + string transaction_id = 1; + } + + message StateUpdate { + GameData initial_state = 1; + repeated TACAgent.Transaction txs = 2; + } + + message Error { + enum ErrorCode { + GENERIC_ERROR = 0; + REQUEST_NOT_VALID = 1; + AGENT_PBK_ALREADY_REGISTERED = 2; + AGENT_NAME_ALREADY_REGISTERED = 3; + AGENT_NOT_REGISTERED = 4; + TRANSACTION_NOT_VALID = 5; + TRANSACTION_NOT_MATCHING = 6; + AGENT_NAME_NOT_IN_WHITELIST = 7; + COMPETITION_NOT_RUNNING = 8; + DIALOGUE_INCONSISTENT = 9; + } + + ErrorCode error_code = 1; + string error_msg = 2; + google.protobuf.Struct details = 3; + } + +} + +message TACAgent { + + message Register { + string agent_name = 1; + } + message Unregister { + } + + message Transaction { + string transaction_id = 1; + bool is_sender_buyer = 2; // is the sender of this message a buyer? + string counterparty = 3; + double amount = 4; + repeated StrIntPair quantities = 5; + } + + message GetStateUpdate { + } + +} + +message TACMessage { + oneof content{ + TACAgent.Register register = 1; + TACAgent.Unregister unregister = 2; + TACAgent.Transaction transaction = 3; + TACAgent.GetStateUpdate get_state_update = 4; + TACController.Registered registered = 5; + TACController.Unregistered unregistered = 6; + TACController.Cancelled cancelled = 7; + TACController.GameData game_data = 8; + TACController.TransactionConfirmation transaction_confirmation = 9; + TACController.StateUpdate state_update = 10; + TACController.Error error = 11; + } +} diff --git a/Test/protocols/tac/tac_pb2.py b/Test/protocols/tac/tac_pb2.py new file mode 100644 index 0000000000..5eb63f23f1 --- /dev/null +++ b/Test/protocols/tac/tac_pb2.py @@ -0,0 +1,899 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: tac.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +from google.protobuf import descriptor_pb2 +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='tac.proto', + package='fetch.oef.pb', + syntax='proto3', + serialized_pb=_b('\n\ttac.proto\x12\x0c\x66\x65tch.oef.pb\x1a\x1cgoogle/protobuf/struct.proto\"+\n\nStrIntPair\x12\r\n\x05\x66irst\x18\x01 \x01(\t\x12\x0e\n\x06second\x18\x02 \x01(\x05\"+\n\nStrStrPair\x12\r\n\x05\x66irst\x18\x01 \x01(\t\x12\x0e\n\x06second\x18\x02 \x01(\t\"\x80\x07\n\rTACController\x1a\x0c\n\nRegistered\x1a\x0e\n\x0cUnregistered\x1a\x0b\n\tCancelled\x1a\xe2\x01\n\x08GameData\x12\r\n\x05money\x18\x01 \x01(\x01\x12\x11\n\tendowment\x18\x02 \x03(\x05\x12\x16\n\x0eutility_params\x18\x03 \x03(\x01\x12\x11\n\tnb_agents\x18\x04 \x01(\x05\x12\x10\n\x08nb_goods\x18\x05 \x01(\x05\x12\x0e\n\x06tx_fee\x18\x06 \x01(\x01\x12\x33\n\x11\x61gent_pbk_to_name\x18\x07 \x03(\x0b\x32\x18.fetch.oef.pb.StrStrPair\x12\x32\n\x10good_pbk_to_name\x18\x08 \x03(\x0b\x32\x18.fetch.oef.pb.StrStrPair\x1a\x31\n\x17TransactionConfirmation\x12\x16\n\x0etransaction_id\x18\x01 \x01(\t\x1a{\n\x0bStateUpdate\x12;\n\rinitial_state\x18\x01 \x01(\x0b\x32$.fetch.oef.pb.TACController.GameData\x12/\n\x03txs\x18\x02 \x03(\x0b\x32\".fetch.oef.pb.TACAgent.Transaction\x1a\xae\x03\n\x05\x45rror\x12?\n\nerror_code\x18\x01 \x01(\x0e\x32+.fetch.oef.pb.TACController.Error.ErrorCode\x12\x11\n\terror_msg\x18\x02 \x01(\t\x12(\n\x07\x64\x65tails\x18\x03 \x01(\x0b\x32\x17.google.protobuf.Struct\"\xa6\x02\n\tErrorCode\x12\x11\n\rGENERIC_ERROR\x10\x00\x12\x15\n\x11REQUEST_NOT_VALID\x10\x01\x12 \n\x1c\x41GENT_PBK_ALREADY_REGISTERED\x10\x02\x12!\n\x1d\x41GENT_NAME_ALREADY_REGISTERED\x10\x03\x12\x18\n\x14\x41GENT_NOT_REGISTERED\x10\x04\x12\x19\n\x15TRANSACTION_NOT_VALID\x10\x05\x12\x1c\n\x18TRANSACTION_NOT_MATCHING\x10\x06\x12\x1f\n\x1b\x41GENT_NAME_NOT_IN_WHITELIST\x10\x07\x12\x1b\n\x17\x43OMPETITION_NOT_RUNNING\x10\x08\x12\x19\n\x15\x44IALOGUE_INCONSISTENT\x10\t\"\xdf\x01\n\x08TACAgent\x1a\x1e\n\x08Register\x12\x12\n\nagent_name\x18\x01 \x01(\t\x1a\x0c\n\nUnregister\x1a\x92\x01\n\x0bTransaction\x12\x16\n\x0etransaction_id\x18\x01 \x01(\t\x12\x17\n\x0fis_sender_buyer\x18\x02 \x01(\x08\x12\x14\n\x0c\x63ounterparty\x18\x03 \x01(\t\x12\x0e\n\x06\x61mount\x18\x04 \x01(\x01\x12,\n\nquantities\x18\x05 \x03(\x0b\x32\x18.fetch.oef.pb.StrIntPair\x1a\x10\n\x0eGetStateUpdate\"\xc8\x05\n\nTACMessage\x12\x33\n\x08register\x18\x01 \x01(\x0b\x32\x1f.fetch.oef.pb.TACAgent.RegisterH\x00\x12\x37\n\nunregister\x18\x02 \x01(\x0b\x32!.fetch.oef.pb.TACAgent.UnregisterH\x00\x12\x39\n\x0btransaction\x18\x03 \x01(\x0b\x32\".fetch.oef.pb.TACAgent.TransactionH\x00\x12\x41\n\x10get_state_update\x18\x04 \x01(\x0b\x32%.fetch.oef.pb.TACAgent.GetStateUpdateH\x00\x12<\n\nregistered\x18\x05 \x01(\x0b\x32&.fetch.oef.pb.TACController.RegisteredH\x00\x12@\n\x0cunregistered\x18\x06 \x01(\x0b\x32(.fetch.oef.pb.TACController.UnregisteredH\x00\x12:\n\tcancelled\x18\x07 \x01(\x0b\x32%.fetch.oef.pb.TACController.CancelledH\x00\x12\x39\n\tgame_data\x18\x08 \x01(\x0b\x32$.fetch.oef.pb.TACController.GameDataH\x00\x12W\n\x18transaction_confirmation\x18\t \x01(\x0b\x32\x33.fetch.oef.pb.TACController.TransactionConfirmationH\x00\x12?\n\x0cstate_update\x18\n \x01(\x0b\x32\'.fetch.oef.pb.TACController.StateUpdateH\x00\x12\x32\n\x05\x65rror\x18\x0b \x01(\x0b\x32!.fetch.oef.pb.TACController.ErrorH\x00\x42\t\n\x07\x63ontentb\x06proto3') + , + dependencies=[google_dot_protobuf_dot_struct__pb2.DESCRIPTOR,]) +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + + + +_TACCONTROLLER_ERROR_ERRORCODE = _descriptor.EnumDescriptor( + name='ErrorCode', + full_name='fetch.oef.pb.TACController.Error.ErrorCode', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='GENERIC_ERROR', index=0, number=0, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='REQUEST_NOT_VALID', index=1, number=1, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='AGENT_PBK_ALREADY_REGISTERED', index=2, number=2, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='AGENT_NAME_ALREADY_REGISTERED', index=3, number=3, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='AGENT_NOT_REGISTERED', index=4, number=4, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='TRANSACTION_NOT_VALID', index=5, number=5, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='TRANSACTION_NOT_MATCHING', index=6, number=6, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='AGENT_NAME_NOT_IN_WHITELIST', index=7, number=7, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='COMPETITION_NOT_RUNNING', index=8, number=8, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='DIALOGUE_INCONSISTENT', index=9, number=9, + options=None, + type=None), + ], + containing_type=None, + options=None, + serialized_start=750, + serialized_end=1044, +) +_sym_db.RegisterEnumDescriptor(_TACCONTROLLER_ERROR_ERRORCODE) + + +_STRINTPAIR = _descriptor.Descriptor( + name='StrIntPair', + full_name='fetch.oef.pb.StrIntPair', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='first', full_name='fetch.oef.pb.StrIntPair.first', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='second', full_name='fetch.oef.pb.StrIntPair.second', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=57, + serialized_end=100, +) + + +_STRSTRPAIR = _descriptor.Descriptor( + name='StrStrPair', + full_name='fetch.oef.pb.StrStrPair', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='first', full_name='fetch.oef.pb.StrStrPair.first', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='second', full_name='fetch.oef.pb.StrStrPair.second', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=102, + serialized_end=145, +) + + +_TACCONTROLLER_REGISTERED = _descriptor.Descriptor( + name='Registered', + full_name='fetch.oef.pb.TACController.Registered', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=165, + serialized_end=177, +) + +_TACCONTROLLER_UNREGISTERED = _descriptor.Descriptor( + name='Unregistered', + full_name='fetch.oef.pb.TACController.Unregistered', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=179, + serialized_end=193, +) + +_TACCONTROLLER_CANCELLED = _descriptor.Descriptor( + name='Cancelled', + full_name='fetch.oef.pb.TACController.Cancelled', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=195, + serialized_end=206, +) + +_TACCONTROLLER_GAMEDATA = _descriptor.Descriptor( + name='GameData', + full_name='fetch.oef.pb.TACController.GameData', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='money', full_name='fetch.oef.pb.TACController.GameData.money', index=0, + number=1, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='endowment', full_name='fetch.oef.pb.TACController.GameData.endowment', index=1, + number=2, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='utility_params', full_name='fetch.oef.pb.TACController.GameData.utility_params', index=2, + number=3, type=1, cpp_type=5, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='nb_agents', full_name='fetch.oef.pb.TACController.GameData.nb_agents', index=3, + number=4, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='nb_goods', full_name='fetch.oef.pb.TACController.GameData.nb_goods', index=4, + number=5, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='tx_fee', full_name='fetch.oef.pb.TACController.GameData.tx_fee', index=5, + number=6, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='agent_pbk_to_name', full_name='fetch.oef.pb.TACController.GameData.agent_pbk_to_name', index=6, + number=7, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='good_pbk_to_name', full_name='fetch.oef.pb.TACController.GameData.good_pbk_to_name', index=7, + number=8, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=209, + serialized_end=435, +) + +_TACCONTROLLER_TRANSACTIONCONFIRMATION = _descriptor.Descriptor( + name='TransactionConfirmation', + full_name='fetch.oef.pb.TACController.TransactionConfirmation', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='transaction_id', full_name='fetch.oef.pb.TACController.TransactionConfirmation.transaction_id', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=437, + serialized_end=486, +) + +_TACCONTROLLER_STATEUPDATE = _descriptor.Descriptor( + name='StateUpdate', + full_name='fetch.oef.pb.TACController.StateUpdate', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='initial_state', full_name='fetch.oef.pb.TACController.StateUpdate.initial_state', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='txs', full_name='fetch.oef.pb.TACController.StateUpdate.txs', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=488, + serialized_end=611, +) + +_TACCONTROLLER_ERROR = _descriptor.Descriptor( + name='Error', + full_name='fetch.oef.pb.TACController.Error', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='error_code', full_name='fetch.oef.pb.TACController.Error.error_code', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='error_msg', full_name='fetch.oef.pb.TACController.Error.error_msg', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='details', full_name='fetch.oef.pb.TACController.Error.details', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _TACCONTROLLER_ERROR_ERRORCODE, + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=614, + serialized_end=1044, +) + +_TACCONTROLLER = _descriptor.Descriptor( + name='TACController', + full_name='fetch.oef.pb.TACController', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[_TACCONTROLLER_REGISTERED, _TACCONTROLLER_UNREGISTERED, _TACCONTROLLER_CANCELLED, _TACCONTROLLER_GAMEDATA, _TACCONTROLLER_TRANSACTIONCONFIRMATION, _TACCONTROLLER_STATEUPDATE, _TACCONTROLLER_ERROR, ], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=148, + serialized_end=1044, +) + + +_TACAGENT_REGISTER = _descriptor.Descriptor( + name='Register', + full_name='fetch.oef.pb.TACAgent.Register', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='agent_name', full_name='fetch.oef.pb.TACAgent.Register.agent_name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1059, + serialized_end=1089, +) + +_TACAGENT_UNREGISTER = _descriptor.Descriptor( + name='Unregister', + full_name='fetch.oef.pb.TACAgent.Unregister', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1091, + serialized_end=1103, +) + +_TACAGENT_TRANSACTION = _descriptor.Descriptor( + name='Transaction', + full_name='fetch.oef.pb.TACAgent.Transaction', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='transaction_id', full_name='fetch.oef.pb.TACAgent.Transaction.transaction_id', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='is_sender_buyer', full_name='fetch.oef.pb.TACAgent.Transaction.is_sender_buyer', index=1, + number=2, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='counterparty', full_name='fetch.oef.pb.TACAgent.Transaction.counterparty', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='amount', full_name='fetch.oef.pb.TACAgent.Transaction.amount', index=3, + number=4, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='quantities', full_name='fetch.oef.pb.TACAgent.Transaction.quantities', index=4, + number=5, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1106, + serialized_end=1252, +) + +_TACAGENT_GETSTATEUPDATE = _descriptor.Descriptor( + name='GetStateUpdate', + full_name='fetch.oef.pb.TACAgent.GetStateUpdate', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1254, + serialized_end=1270, +) + +_TACAGENT = _descriptor.Descriptor( + name='TACAgent', + full_name='fetch.oef.pb.TACAgent', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[_TACAGENT_REGISTER, _TACAGENT_UNREGISTER, _TACAGENT_TRANSACTION, _TACAGENT_GETSTATEUPDATE, ], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1047, + serialized_end=1270, +) + + +_TACMESSAGE = _descriptor.Descriptor( + name='TACMessage', + full_name='fetch.oef.pb.TACMessage', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='register', full_name='fetch.oef.pb.TACMessage.register', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='unregister', full_name='fetch.oef.pb.TACMessage.unregister', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='transaction', full_name='fetch.oef.pb.TACMessage.transaction', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='get_state_update', full_name='fetch.oef.pb.TACMessage.get_state_update', index=3, + number=4, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='registered', full_name='fetch.oef.pb.TACMessage.registered', index=4, + number=5, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='unregistered', full_name='fetch.oef.pb.TACMessage.unregistered', index=5, + number=6, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='cancelled', full_name='fetch.oef.pb.TACMessage.cancelled', index=6, + number=7, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='game_data', full_name='fetch.oef.pb.TACMessage.game_data', index=7, + number=8, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='transaction_confirmation', full_name='fetch.oef.pb.TACMessage.transaction_confirmation', index=8, + number=9, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='state_update', full_name='fetch.oef.pb.TACMessage.state_update', index=9, + number=10, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='error', full_name='fetch.oef.pb.TACMessage.error', index=10, + number=11, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='content', full_name='fetch.oef.pb.TACMessage.content', + index=0, containing_type=None, fields=[]), + ], + serialized_start=1273, + serialized_end=1985, +) + +_TACCONTROLLER_REGISTERED.containing_type = _TACCONTROLLER +_TACCONTROLLER_UNREGISTERED.containing_type = _TACCONTROLLER +_TACCONTROLLER_CANCELLED.containing_type = _TACCONTROLLER +_TACCONTROLLER_GAMEDATA.fields_by_name['agent_pbk_to_name'].message_type = _STRSTRPAIR +_TACCONTROLLER_GAMEDATA.fields_by_name['good_pbk_to_name'].message_type = _STRSTRPAIR +_TACCONTROLLER_GAMEDATA.containing_type = _TACCONTROLLER +_TACCONTROLLER_TRANSACTIONCONFIRMATION.containing_type = _TACCONTROLLER +_TACCONTROLLER_STATEUPDATE.fields_by_name['initial_state'].message_type = _TACCONTROLLER_GAMEDATA +_TACCONTROLLER_STATEUPDATE.fields_by_name['txs'].message_type = _TACAGENT_TRANSACTION +_TACCONTROLLER_STATEUPDATE.containing_type = _TACCONTROLLER +_TACCONTROLLER_ERROR.fields_by_name['error_code'].enum_type = _TACCONTROLLER_ERROR_ERRORCODE +_TACCONTROLLER_ERROR.fields_by_name['details'].message_type = google_dot_protobuf_dot_struct__pb2._STRUCT +_TACCONTROLLER_ERROR.containing_type = _TACCONTROLLER +_TACCONTROLLER_ERROR_ERRORCODE.containing_type = _TACCONTROLLER_ERROR +_TACAGENT_REGISTER.containing_type = _TACAGENT +_TACAGENT_UNREGISTER.containing_type = _TACAGENT +_TACAGENT_TRANSACTION.fields_by_name['quantities'].message_type = _STRINTPAIR +_TACAGENT_TRANSACTION.containing_type = _TACAGENT +_TACAGENT_GETSTATEUPDATE.containing_type = _TACAGENT +_TACMESSAGE.fields_by_name['register'].message_type = _TACAGENT_REGISTER +_TACMESSAGE.fields_by_name['unregister'].message_type = _TACAGENT_UNREGISTER +_TACMESSAGE.fields_by_name['transaction'].message_type = _TACAGENT_TRANSACTION +_TACMESSAGE.fields_by_name['get_state_update'].message_type = _TACAGENT_GETSTATEUPDATE +_TACMESSAGE.fields_by_name['registered'].message_type = _TACCONTROLLER_REGISTERED +_TACMESSAGE.fields_by_name['unregistered'].message_type = _TACCONTROLLER_UNREGISTERED +_TACMESSAGE.fields_by_name['cancelled'].message_type = _TACCONTROLLER_CANCELLED +_TACMESSAGE.fields_by_name['game_data'].message_type = _TACCONTROLLER_GAMEDATA +_TACMESSAGE.fields_by_name['transaction_confirmation'].message_type = _TACCONTROLLER_TRANSACTIONCONFIRMATION +_TACMESSAGE.fields_by_name['state_update'].message_type = _TACCONTROLLER_STATEUPDATE +_TACMESSAGE.fields_by_name['error'].message_type = _TACCONTROLLER_ERROR +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['register']) +_TACMESSAGE.fields_by_name['register'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['unregister']) +_TACMESSAGE.fields_by_name['unregister'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['transaction']) +_TACMESSAGE.fields_by_name['transaction'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['get_state_update']) +_TACMESSAGE.fields_by_name['get_state_update'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['registered']) +_TACMESSAGE.fields_by_name['registered'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['unregistered']) +_TACMESSAGE.fields_by_name['unregistered'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['cancelled']) +_TACMESSAGE.fields_by_name['cancelled'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['game_data']) +_TACMESSAGE.fields_by_name['game_data'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['transaction_confirmation']) +_TACMESSAGE.fields_by_name['transaction_confirmation'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['state_update']) +_TACMESSAGE.fields_by_name['state_update'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['error']) +_TACMESSAGE.fields_by_name['error'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +DESCRIPTOR.message_types_by_name['StrIntPair'] = _STRINTPAIR +DESCRIPTOR.message_types_by_name['StrStrPair'] = _STRSTRPAIR +DESCRIPTOR.message_types_by_name['TACController'] = _TACCONTROLLER +DESCRIPTOR.message_types_by_name['TACAgent'] = _TACAGENT +DESCRIPTOR.message_types_by_name['TACMessage'] = _TACMESSAGE + +StrIntPair = _reflection.GeneratedProtocolMessageType('StrIntPair', (_message.Message,), dict( + DESCRIPTOR = _STRINTPAIR, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.StrIntPair) + )) +_sym_db.RegisterMessage(StrIntPair) + +StrStrPair = _reflection.GeneratedProtocolMessageType('StrStrPair', (_message.Message,), dict( + DESCRIPTOR = _STRSTRPAIR, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.StrStrPair) + )) +_sym_db.RegisterMessage(StrStrPair) + +TACController = _reflection.GeneratedProtocolMessageType('TACController', (_message.Message,), dict( + + Registered = _reflection.GeneratedProtocolMessageType('Registered', (_message.Message,), dict( + DESCRIPTOR = _TACCONTROLLER_REGISTERED, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Registered) + )) + , + + Unregistered = _reflection.GeneratedProtocolMessageType('Unregistered', (_message.Message,), dict( + DESCRIPTOR = _TACCONTROLLER_UNREGISTERED, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Unregistered) + )) + , + + Cancelled = _reflection.GeneratedProtocolMessageType('Cancelled', (_message.Message,), dict( + DESCRIPTOR = _TACCONTROLLER_CANCELLED, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Cancelled) + )) + , + + GameData = _reflection.GeneratedProtocolMessageType('GameData', (_message.Message,), dict( + DESCRIPTOR = _TACCONTROLLER_GAMEDATA, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.GameData) + )) + , + + TransactionConfirmation = _reflection.GeneratedProtocolMessageType('TransactionConfirmation', (_message.Message,), dict( + DESCRIPTOR = _TACCONTROLLER_TRANSACTIONCONFIRMATION, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.TransactionConfirmation) + )) + , + + StateUpdate = _reflection.GeneratedProtocolMessageType('StateUpdate', (_message.Message,), dict( + DESCRIPTOR = _TACCONTROLLER_STATEUPDATE, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.StateUpdate) + )) + , + + Error = _reflection.GeneratedProtocolMessageType('Error', (_message.Message,), dict( + DESCRIPTOR = _TACCONTROLLER_ERROR, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Error) + )) + , + DESCRIPTOR = _TACCONTROLLER, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController) + )) +_sym_db.RegisterMessage(TACController) +_sym_db.RegisterMessage(TACController.Registered) +_sym_db.RegisterMessage(TACController.Unregistered) +_sym_db.RegisterMessage(TACController.Cancelled) +_sym_db.RegisterMessage(TACController.GameData) +_sym_db.RegisterMessage(TACController.TransactionConfirmation) +_sym_db.RegisterMessage(TACController.StateUpdate) +_sym_db.RegisterMessage(TACController.Error) + +TACAgent = _reflection.GeneratedProtocolMessageType('TACAgent', (_message.Message,), dict( + + Register = _reflection.GeneratedProtocolMessageType('Register', (_message.Message,), dict( + DESCRIPTOR = _TACAGENT_REGISTER, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.Register) + )) + , + + Unregister = _reflection.GeneratedProtocolMessageType('Unregister', (_message.Message,), dict( + DESCRIPTOR = _TACAGENT_UNREGISTER, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.Unregister) + )) + , + + Transaction = _reflection.GeneratedProtocolMessageType('Transaction', (_message.Message,), dict( + DESCRIPTOR = _TACAGENT_TRANSACTION, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.Transaction) + )) + , + + GetStateUpdate = _reflection.GeneratedProtocolMessageType('GetStateUpdate', (_message.Message,), dict( + DESCRIPTOR = _TACAGENT_GETSTATEUPDATE, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.GetStateUpdate) + )) + , + DESCRIPTOR = _TACAGENT, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent) + )) +_sym_db.RegisterMessage(TACAgent) +_sym_db.RegisterMessage(TACAgent.Register) +_sym_db.RegisterMessage(TACAgent.Unregister) +_sym_db.RegisterMessage(TACAgent.Transaction) +_sym_db.RegisterMessage(TACAgent.GetStateUpdate) + +TACMessage = _reflection.GeneratedProtocolMessageType('TACMessage', (_message.Message,), dict( + DESCRIPTOR = _TACMESSAGE, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACMessage) + )) +_sym_db.RegisterMessage(TACMessage) + + +# @@protoc_insertion_point(module_scope) diff --git a/Test/skills/__init__.py b/Test/skills/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Test/skills/error/__init__.py b/Test/skills/error/__init__.py new file mode 100644 index 0000000000..96c80ac32c --- /dev/null +++ b/Test/skills/error/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the implementation of the error skill.""" diff --git a/Test/skills/error/behaviours.py b/Test/skills/error/behaviours.py new file mode 100644 index 0000000000..556ee98ca7 --- /dev/null +++ b/Test/skills/error/behaviours.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains the error behaviours.""" + +from aea.skills.base import Behaviour + + +class ErrorBehaviour(Behaviour): + """This class implements the error behaviour.""" + + def setup(self) -> None: + """ + Implement the setup. + + :return: None + """ + pass + + def act(self) -> None: + """ + Implement the act. + + :return: None + """ + pass + + def teardown(self) -> None: + """ + Implement the task teardown. + + :return: None + """ + pass diff --git a/Test/skills/error/handlers.py b/Test/skills/error/handlers.py new file mode 100644 index 0000000000..098a61eced --- /dev/null +++ b/Test/skills/error/handlers.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains the implementation of the handler for the 'default' protocol.""" +import base64 +import logging +from typing import Optional + +from aea.configurations.base import ProtocolId +from aea.mail.base import Envelope +from aea.protocols.base import Message, Protocol +from aea.protocols.default.message import DefaultMessage +from aea.protocols.default.serialization import DefaultSerializer +from aea.skills.base import Handler + +logger = logging.getLogger(__name__) + + +class ErrorHandler(Handler): + """This class implements the error handler.""" + + SUPPORTED_PROTOCOL = 'default' # type: Optional[ProtocolId] + + def setup(self) -> None: + """ + Implement the setup. + + :return: None + """ + pass + + def handle(self, message: Message, sender: str) -> None: + """ + Implement the reaction to an envelope. + + :param message: the message + :param sender: the sender + """ + pass + + def teardown(self) -> None: + """ + Implement the handler teardown. + + :return: None + """ + pass + + def send_unsupported_protocol(self, envelope: Envelope) -> None: + """ + Handle the received envelope in case the protocol is not supported. + + :param envelope: the envelope + :return: None + """ + logger.warning("Unsupported protocol: {}".format(envelope.protocol_id)) + reply = DefaultMessage(type=DefaultMessage.Type.ERROR, + error_code=DefaultMessage.ErrorCode.UNSUPPORTED_PROTOCOL.value, + error_msg="Unsupported protocol.", + error_data={"protocol_id": envelope.protocol_id}) + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, + message=DefaultSerializer().encode(reply)) + + def send_decoding_error(self, envelope: Envelope) -> None: + """ + Handle a decoding error. + + :param envelope: the envelope + :return: None + """ + logger.warning("Decoding error: {}.".format(envelope)) + encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") + reply = DefaultMessage(type=DefaultMessage.Type.ERROR, + error_code=DefaultMessage.ErrorCode.DECODING_ERROR.value, + error_msg="Decoding error.", + error_data={"envelope": encoded_envelope}) + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, + message=DefaultSerializer().encode(reply)) + + def send_invalid_message(self, envelope: Envelope) -> None: + """ + Handle an message that is invalid wrt a protocol. + + :param envelope: the envelope + :return: None + """ + logger.warning("Invalid message wrt protocol: {}.".format(envelope.protocol_id)) + encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") + reply = DefaultMessage(type=DefaultMessage.Type.ERROR, + error_code=DefaultMessage.ErrorCode.INVALID_MESSAGE.value, + error_msg="Invalid message.", + error_data={"envelope": encoded_envelope}) + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, + message=DefaultSerializer().encode(reply)) + + def send_unsupported_skill(self, envelope: Envelope, protocol: Protocol) -> None: + """ + Handle the received envelope in case the skill is not supported. + + :param envelope: the envelope + :param protocol: the protocol + :return: None + """ + logger.warning("Cannot handle envelope: no handler registered for the protocol '{}'.".format(protocol.id)) + encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") + reply = DefaultMessage(type=DefaultMessage.Type.ERROR, + error_code=DefaultMessage.ErrorCode.UNSUPPORTED_SKILL.value, + error_msg="Unsupported skill.", + error_data={"envelope": encoded_envelope}) + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, + message=DefaultSerializer().encode(reply)) diff --git a/Test/skills/error/skill.yaml b/Test/skills/error/skill.yaml new file mode 100644 index 0000000000..33806a3345 --- /dev/null +++ b/Test/skills/error/skill.yaml @@ -0,0 +1,15 @@ +name: error +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +description: "The error skill implements basic error handling required by all AEAs." +behaviours: [] +handlers: + - handler: + class_name: ErrorHandler + args: + foo: bar +tasks: [] +shared_classes: [] +protocols: ['default'] diff --git a/Test/skills/error/tasks.py b/Test/skills/error/tasks.py new file mode 100644 index 0000000000..8922217537 --- /dev/null +++ b/Test/skills/error/tasks.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains the implementation of the error tasks.""" + +from aea.skills.base import Task + + +class ErrorTask(Task): + """This class implements the error task.""" + + def setup(self) -> None: + """ + Implement the setup. + + :return: None + """ + pass + + def execute(self) -> None: + """ + Implement the task execution. + + :param envelope: the envelope + :return: None + """ + pass + + def teardown(self) -> None: + """ + Implement the task teardown. + + :return: None + """ + pass diff --git a/aea/cli_gui/templates/home.js b/aea/cli_gui/templates/home.js index a4bb0c9c50..93c4242347 100644 --- a/aea/cli_gui/templates/home.js +++ b/aea/cli_gui/templates/home.js @@ -681,5 +681,6 @@ class Controller{ } - -c = new Controller(new Model(), new View()) +$( document ).ready(function() { + c = new Controller(new Model(), new View()) +}); diff --git a/asdsd/aea-config.yaml b/asdsd/aea-config.yaml new file mode 100644 index 0000000000..a446facf18 --- /dev/null +++ b/asdsd/aea-config.yaml @@ -0,0 +1,21 @@ +aea_version: 0.1.7 +agent_name: asdsd +authors: '' +connections: +- oef +default_connection: oef +description: '' +license: '' +logging_config: + disable_existing_loggers: false + version: 1 +private_key_paths: [] +protocols: +- default +- gym +- tac +registry_path: ../packages +skills: +- error +url: '' +version: v1 diff --git a/asdsd/connections/__init__.py b/asdsd/connections/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/asdsd/connections/oef/__init__.py b/asdsd/connections/oef/__init__.py new file mode 100644 index 0000000000..21ee4d83df --- /dev/null +++ b/asdsd/connections/oef/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Implementation of the OEF connection.""" diff --git a/asdsd/connections/oef/connection.py b/asdsd/connections/oef/connection.py new file mode 100644 index 0000000000..7f489b867f --- /dev/null +++ b/asdsd/connections/oef/connection.py @@ -0,0 +1,604 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Extension to the OEF Python SDK.""" +import datetime +import logging +import pickle +from queue import Empty, Queue +from threading import Thread +from typing import List, Dict, Optional, cast + +import oef +from oef.agents import OEFAgent +from oef.core import AsyncioCore +from oef.messages import CFP_TYPES, PROPOSE_TYPES +from oef.query import ( + Query as OEFQuery, + ConstraintExpr as OEFConstraintExpr, + And as OEFAnd, + Or as OEFOr, + Not as OEFNot, + Constraint as OEFConstraint, + ConstraintType as OEFConstraintType, Eq, NotEq, Lt, LtEq, Gt, GtEq, Range, In, NotIn) +from oef.schema import Description as OEFDescription, DataModel as OEFDataModel, AttributeSchema as OEFAttribute + +from aea.configurations.base import ConnectionConfig +from aea.connections.base import Channel, Connection +from aea.mail.base import MailBox, Envelope +from aea.protocols.fipa.message import FIPAMessage +from aea.protocols.fipa.serialization import FIPASerializer +from aea.protocols.oef.message import OEFMessage +from aea.protocols.oef.models import Description, Attribute, DataModel, Query, ConstraintExpr, And, Or, Not, Constraint, \ + ConstraintType, ConstraintTypes +from aea.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF + +logger = logging.getLogger(__name__) + + +STUB_MESSSAGE_ID = 0 +STUB_DIALOGUE_ID = 0 + + +class OEFObjectTranslator: + """Translate our OEF object to object of OEF SDK classes.""" + + @classmethod + def to_oef_description(cls, desc: Description) -> OEFDescription: + """From our description to OEF description.""" + oef_data_model = cls.to_oef_data_model(desc.data_model) if desc.data_model is not None else None + return OEFDescription(desc.values, oef_data_model) + + @classmethod + def to_oef_data_model(cls, data_model: DataModel) -> OEFDataModel: + """From our data model to OEF data model.""" + oef_attributes = [cls.to_oef_attribute(attribute) for attribute in data_model.attributes] + return OEFDataModel(data_model.name, oef_attributes, data_model.description) + + @classmethod + def to_oef_attribute(cls, attribute: Attribute) -> OEFAttribute: + """From our attribute to OEF attribute.""" + return OEFAttribute(attribute.name, attribute.type, attribute.is_required, attribute.description) + + @classmethod + def to_oef_query(cls, query: Query) -> OEFQuery: + """From our query to OEF query.""" + oef_data_model = cls.to_oef_data_model(query.model) if query.model is not None else None + constraints = [cls.to_oef_constraint_expr(c) for c in query.constraints] + return OEFQuery(constraints, oef_data_model) + + @classmethod + def to_oef_constraint_expr(cls, constraint_expr: ConstraintExpr) -> OEFConstraintExpr: + """From our constraint expression to the OEF constraint expression.""" + if isinstance(constraint_expr, And): + return OEFAnd([cls.to_oef_constraint_expr(c) for c in constraint_expr.constraints]) + elif isinstance(constraint_expr, Or): + return OEFOr([cls.to_oef_constraint_expr(c) for c in constraint_expr.constraints]) + elif isinstance(constraint_expr, Not): + return OEFNot(cls.to_oef_constraint_expr(constraint_expr.constraint)) + elif isinstance(constraint_expr, Constraint): + oef_constraint_type = cls.to_oef_constraint_type(constraint_expr.constraint_type) + return OEFConstraint(constraint_expr.attribute_name, oef_constraint_type) + else: + raise ValueError("Constraint expression not supported.") + + @classmethod + def to_oef_constraint_type(cls, constraint_type: ConstraintType) -> OEFConstraintType: + """From our constraint type to OEF constraint type.""" + value = constraint_type.value + if constraint_type.type == ConstraintTypes.EQUAL: + return Eq(value) + elif constraint_type.type == ConstraintTypes.NOT_EQUAL: + return NotEq(value) + elif constraint_type.type == ConstraintTypes.LESS_THAN: + return Lt(value) + elif constraint_type.type == ConstraintTypes.LESS_THAN_EQ: + return LtEq(value) + elif constraint_type.type == ConstraintTypes.GREATER_THAN: + return Gt(value) + elif constraint_type.type == ConstraintTypes.GREATER_THAN_EQ: + return GtEq(value) + elif constraint_type.type == ConstraintTypes.WITHIN: + return Range(value) + elif constraint_type.type == ConstraintTypes.IN: + return In(value) + elif constraint_type.type == ConstraintTypes.NOT_IN: + return NotIn(value) + else: + raise ValueError("Constraint type not recognized.") + + @classmethod + def from_oef_description(cls, oef_desc: OEFDescription) -> Description: + """From an OEF description to our description.""" + data_model = cls.from_oef_data_model(oef_desc.data_model) if oef_desc.data_model is not None else None + return Description(oef_desc.values, data_model=data_model) + + @classmethod + def from_oef_data_model(cls, oef_data_model: OEFDataModel) -> DataModel: + """From an OEF data model to our data model.""" + attributes = [cls.from_oef_attribute(oef_attribute) for oef_attribute in oef_data_model.attribute_schemas] + return DataModel(oef_data_model.name, attributes, oef_data_model.description) + + @classmethod + def from_oef_attribute(cls, oef_attribute: OEFAttribute) -> Attribute: + """From an OEF attribute to our attribute.""" + return Attribute(oef_attribute.name, oef_attribute.type, oef_attribute.required, oef_attribute.description) + + @classmethod + def from_oef_query(cls, oef_query: OEFQuery) -> Query: + """From our query to OrOEF query.""" + data_model = cls.from_oef_data_model(oef_query.model) if oef_query.model is not None else None + constraints = [cls.from_oef_constraint_expr(c) for c in oef_query.constraints] + return Query(constraints, data_model) + + @classmethod + def from_oef_constraint_expr(cls, oef_constraint_expr: OEFConstraintExpr) -> ConstraintExpr: + """From our query to OEF query.""" + if isinstance(oef_constraint_expr, OEFAnd): + return And([cls.from_oef_constraint_expr(c) for c in oef_constraint_expr.constraints]) + elif isinstance(oef_constraint_expr, OEFOr): + return Or([cls.from_oef_constraint_expr(c) for c in oef_constraint_expr.constraints]) + elif isinstance(oef_constraint_expr, OEFNot): + return Not(cls.from_oef_constraint_expr(oef_constraint_expr.constraint)) + elif isinstance(oef_constraint_expr, OEFConstraint): + constraint_type = cls.from_oef_constraint_type(oef_constraint_expr.constraint) + return Constraint(oef_constraint_expr.attribute_name, constraint_type) + else: + raise ValueError("OEF Constraint not supported.") + + @classmethod + def from_oef_constraint_type(cls, constraint_type: OEFConstraintType) -> ConstraintType: + """From OEF constraint type to our constraint type.""" + if isinstance(constraint_type, Eq): + return ConstraintType(ConstraintTypes.EQUAL, constraint_type.value) + elif isinstance(constraint_type, NotEq): + return ConstraintType(ConstraintTypes.NOT_EQUAL, constraint_type.value) + elif isinstance(constraint_type, Lt): + return ConstraintType(ConstraintTypes.LESS_THAN, constraint_type.value) + elif isinstance(constraint_type, LtEq): + return ConstraintType(ConstraintTypes.LESS_THAN_EQ, constraint_type.value) + elif isinstance(constraint_type, Gt): + return ConstraintType(ConstraintTypes.GREATER_THAN, constraint_type.value) + elif isinstance(constraint_type, GtEq): + return ConstraintType(ConstraintTypes.GREATER_THAN_EQ, constraint_type.value) + elif isinstance(constraint_type, Range): + return ConstraintType(ConstraintTypes.WITHIN, constraint_type.values) + elif isinstance(constraint_type, In): + return ConstraintType(ConstraintTypes.IN, constraint_type.values) + elif isinstance(constraint_type, NotIn): + return ConstraintType(ConstraintTypes.NOT_IN, constraint_type.values) + else: + raise ValueError("Constraint type not recognized.") + + +class MailStats(object): + """The MailStats class tracks statistics on messages processed by MailBox.""" + + def __init__(self) -> None: + """ + Instantiate mail stats. + + :return: None + """ + self._search_count = 0 + self._search_start_time = {} # type: Dict[int, datetime.datetime] + self._search_timedelta = {} # type: Dict[int, float] + self._search_result_counts = {} # type: Dict[int, int] + + @property + def search_count(self) -> int: + """Get the search count.""" + return self._search_count + + def search_start(self, search_id: int) -> None: + """ + Add a search id and start time. + + :param search_id: the search id + + :return: None + """ + assert search_id not in self._search_start_time + self._search_count += 1 + self._search_start_time[search_id] = datetime.datetime.now() + + def search_end(self, search_id: int, nb_search_results: int) -> None: + """ + Add end time for a search id. + + :param search_id: the search id + :param nb_search_results: the number of agents returned in the search result + + :return: None + """ + assert search_id in self._search_start_time + assert search_id not in self._search_timedelta + self._search_timedelta[search_id] = (datetime.datetime.now() - self._search_start_time[search_id]).total_seconds() * 1000 + self._search_result_counts[search_id] = nb_search_results + + +class OEFChannel(OEFAgent, Channel): + """The OEFChannel connects the OEF Agent with the connection.""" + + def __init__(self, public_key: str, oef_addr: str, oef_port: int, core: AsyncioCore, in_queue: Queue): + """ + Initialize. + + :param public_key: the public key of the agent. + :param oef_addr: the OEF IP address. + :param oef_port: the OEF port. + :param in_queue: the in queue. + """ + super().__init__(public_key, oef_addr=oef_addr, oef_port=oef_port, core=core) + self.in_queue = in_queue + self.mail_stats = MailStats() + + def on_message(self, msg_id: int, dialogue_id: int, origin: str, content: bytes) -> None: + """ + On message event handler. + + :param msg_id: the message id. + :param dialogue_id: the dialogue id. + :param origin: the public key of the sender. + :param content: the bytes content. + :return: None + """ + # We are not using the 'origin' parameter because 'content' contains a serialized instance of 'Envelope', + # hence it already contains the address of the sender. + envelope = Envelope.decode(content) + self.in_queue.put(envelope) + + def on_cfp(self, msg_id: int, dialogue_id: int, origin: str, target: int, query: CFP_TYPES) -> None: + """ + On cfp event handler. + + :param msg_id: the message id. + :param dialogue_id: the dialogue id. + :param origin: the public key of the sender. + :param target: the message target. + :param query: the query. + :return: None + """ + try: + query = pickle.loads(query) + except Exception: + pass + msg = FIPAMessage(message_id=msg_id, + dialogue_id=dialogue_id, + target=target, + performative=FIPAMessage.Performative.CFP, + query=query if query != b"" else None) + msg_bytes = FIPASerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_propose(self, msg_id: int, dialogue_id: int, origin: str, target: int, b_proposals: PROPOSE_TYPES) -> None: + """ + On propose event handler. + + :param msg_id: the message id. + :param dialogue_id: the dialogue id. + :param origin: the public key of the sender. + :param target: the message target. + :param b_proposals: the proposals. + :return: None + """ + if type(b_proposals) == bytes: + proposals = pickle.loads(b_proposals) # type: List[Description] + else: + raise ValueError("No support for non-bytes proposals.") + + msg = FIPAMessage(message_id=msg_id, + dialogue_id=dialogue_id, + target=target, + performative=FIPAMessage.Performative.PROPOSE, + proposal=proposals) + msg_bytes = FIPASerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_accept(self, msg_id: int, dialogue_id: int, origin: str, target: int) -> None: + """ + On accept event handler. + + :param msg_id: the message id. + :param dialogue_id: the dialogue id. + :param origin: the public key of the sender. + :param target: the message target. + :return: None + """ + performative = FIPAMessage.Performative.MATCH_ACCEPT if msg_id == 4 and target == 3 else FIPAMessage.Performative.ACCEPT + msg = FIPAMessage(message_id=msg_id, + dialogue_id=dialogue_id, + target=target, + performative=performative) + msg_bytes = FIPASerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_decline(self, msg_id: int, dialogue_id: int, origin: str, target: int) -> None: + """ + On decline event handler. + + :param msg_id: the message id. + :param dialogue_id: the dialogue id. + :param origin: the public key of the sender. + :param target: the message target. + :return: None + """ + msg = FIPAMessage(message_id=msg_id, + dialogue_id=dialogue_id, + target=target, + performative=FIPAMessage.Performative.DECLINE) + msg_bytes = FIPASerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_search_result(self, search_id: int, agents: List[str]) -> None: + """ + On accept event handler. + + :param search_id: the search id. + :param agents: the list of agents. + :return: None + """ + self.mail_stats.search_end(search_id, len(agents)) + msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_RESULT, id=search_id, agents=agents) + msg_bytes = OEFSerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_oef_error(self, answer_id: int, operation: oef.messages.OEFErrorOperation) -> None: + """ + On oef error event handler. + + :param answer_id: the answer id. + :param operation: the error operation. + :return: None + """ + try: + operation = OEFMessage.OEFErrorOperation(operation) + except ValueError: + operation = OEFMessage.OEFErrorOperation.OTHER + + msg = OEFMessage(oef_type=OEFMessage.Type.OEF_ERROR, id=answer_id, operation=operation) + msg_bytes = OEFSerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_dialogue_error(self, answer_id: int, dialogue_id: int, origin: str) -> None: + """ + On dialogue error event handler. + + :param answer_id: the answer id. + :param dialogue_id: the dialogue id. + :param origin: the message sender. + :return: None + """ + msg = OEFMessage(oef_type=OEFMessage.Type.DIALOGUE_ERROR, + id=answer_id, + dialogue_id=dialogue_id, + origin=origin) + msg_bytes = OEFSerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def send(self, envelope: Envelope) -> None: + """ + Send message handler. + + :param envelope: the message. + :return: None + """ + if envelope.protocol_id == "default": + self.send_default_message(envelope) + elif envelope.protocol_id == "fipa": + self.send_fipa_message(envelope) + elif envelope.protocol_id == "oef": + self.send_oef_message(envelope) + elif envelope.protocol_id == "tac": + self.send_default_message(envelope) + else: + logger.error("This envelope cannot be sent: protocol_id={}".format(envelope.protocol_id)) + raise ValueError("Cannot send message.") + + def send_default_message(self, envelope: Envelope): + """Send a 'default' message.""" + self.send_message(STUB_MESSSAGE_ID, STUB_DIALOGUE_ID, envelope.to, envelope.encode()) + + def send_fipa_message(self, envelope: Envelope) -> None: + """ + Send fipa message handler. + + :param envelope: the message. + :return: None + """ + fipa_message = FIPASerializer().decode(envelope.message) + id = fipa_message.get("message_id") + dialogue_id = fipa_message.get("dialogue_id") + destination = envelope.to + target = fipa_message.get("target") + performative = FIPAMessage.Performative(fipa_message.get("performative")) + if performative == FIPAMessage.Performative.CFP: + query = fipa_message.get("query") + query = b"" if query is None else query + if type(query) == Query: + query = pickle.dumps(query) + self.send_cfp(id, dialogue_id, destination, target, query) + elif performative == FIPAMessage.Performative.PROPOSE: + proposal = cast(List[Description], fipa_message.get("proposal")) + proposal_b = pickle.dumps(proposal) # type: bytes + self.send_propose(id, dialogue_id, destination, target, proposal_b) + elif performative == FIPAMessage.Performative.ACCEPT: + self.send_accept(id, dialogue_id, destination, target) + elif performative == FIPAMessage.Performative.MATCH_ACCEPT: + self.send_accept(id, dialogue_id, destination, target) + elif performative == FIPAMessage.Performative.DECLINE: + self.send_decline(id, dialogue_id, destination, target) + else: + raise ValueError("OEF FIPA message not recognized.") + + def send_oef_message(self, envelope: Envelope) -> None: + """ + Send oef message handler. + + :param envelope: the message. + :return: None + """ + oef_message = OEFSerializer().decode(envelope.message) + oef_type = OEFMessage.Type(oef_message.get("type")) + oef_msg_id = cast(int, oef_message.get("id")) + if oef_type == OEFMessage.Type.REGISTER_SERVICE: + service_description = cast(Description, oef_message.get("service_description")) + service_id = cast(int, oef_message.get("service_id")) + oef_service_description = OEFObjectTranslator.to_oef_description(service_description) + self.register_service(oef_msg_id, oef_service_description, service_id) + elif oef_type == OEFMessage.Type.UNREGISTER_SERVICE: + service_description = cast(Description, oef_message.get("service_description")) + service_id = cast(int, oef_message.get("service_id")) + oef_service_description = OEFObjectTranslator.to_oef_description(service_description) + self.unregister_service(oef_msg_id, oef_service_description, service_id) + elif oef_type == OEFMessage.Type.SEARCH_AGENTS: + query = cast(Query, oef_message.get("query")) + oef_query = OEFObjectTranslator.to_oef_query(query) + self.search_agents(oef_msg_id, oef_query) + elif oef_type == OEFMessage.Type.SEARCH_SERVICES: + query = cast(Query, oef_message.get("query")) + oef_query = OEFObjectTranslator.to_oef_query(query) + self.mail_stats.search_start(oef_msg_id) + self.search_services(oef_msg_id, oef_query) + else: + raise ValueError("OEF request not recognized.") + + +class OEFConnection(Connection): + """The OEFConnection connects the to the mailbox.""" + + def __init__(self, public_key: str, oef_addr: str, oef_port: int = 10000): + """ + Initialize. + + :param public_key: the public key of the agent. + :param oef_addr: the OEF IP address. + :param oef_port: the OEF port. + """ + super().__init__() + core = AsyncioCore(logger=logger) + self._core = core # type: AsyncioCore + self.channel = OEFChannel(public_key, oef_addr, oef_port, core=core, in_queue=self.in_queue) + + self._stopped = True + self._connected = False + self.out_thread = None # type: Optional[Thread] + + @property + def is_established(self) -> bool: + """Get the connection status.""" + return self._connected + + def _fetch(self) -> None: + """ + Fetch the messages from the outqueue and send them. + + :return: None + """ + while self._connected: + try: + msg = self.out_queue.get(block=True, timeout=1.0) + self.send(msg) + except Empty: + pass + + def connect(self) -> None: + """ + Connect to the channel. + + :return: None + :raises ConnectionError if the connection to the OEF fails. + """ + if self._stopped and not self._connected: + self._stopped = False + self._core.run_threaded() + try: + if not self.channel.connect(): + raise ConnectionError("Cannot connect to OEFChannel.") + self._connected = True + self.out_thread = Thread(target=self._fetch) + self.out_thread.start() + except ConnectionError as e: + self._core.stop() + raise e + + def disconnect(self) -> None: + """ + Disconnect from the channel. + + :return: None + """ + assert self.out_thread is not None, "Call connect before disconnect." + if not self._stopped and self._connected: + self._connected = False + self.out_thread.join() + self.out_thread = None + self.channel.disconnect() + self._core.stop() + self._stopped = True + + def send(self, envelope: Envelope): + """ + Send messages. + + :return: None + """ + if self._connected: + self.channel.send(envelope) + + @classmethod + def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection': + """ + Get the OEF connection from the connection configuration. + + :param public_key: the public key of the agent. + :param connection_configuration: the connection configuration object. + :return: the connection object + """ + oef_addr = cast(str, connection_configuration.config.get("addr")) + oef_port = cast(int, connection_configuration.config.get("port")) + return OEFConnection(public_key, oef_addr, oef_port) + + +class OEFMailBox(MailBox): + """The OEF mail box.""" + + def __init__(self, public_key: str, oef_addr: str, oef_port: int = 10000): + """ + Initialize. + + :param public_key: the public key of the agent. + :param oef_addr: the OEF IP address. + :param oef_port: the OEF port. + """ + connection = OEFConnection(public_key, oef_addr, oef_port) + super().__init__(connection) + + @property + def mail_stats(self) -> MailStats: + """Get the mail stats object.""" + return self._connection.channel.mail_stats # type: ignore diff --git a/asdsd/connections/oef/connection.yaml b/asdsd/connections/oef/connection.yaml new file mode 100644 index 0000000000..6a69ed33d1 --- /dev/null +++ b/asdsd/connections/oef/connection.yaml @@ -0,0 +1,18 @@ +name: oef +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +description: "The oef connection provides a wrapper around the OEF sdk." +class_name: OEFConnection +supported_protocols: + - default + - oef + - fipa + - tac +config: + addr: ${OEF_ADDR:127.0.0.1} + port: ${OEF_PORT:10000} +dependencies: + - colorlog + - oef diff --git a/asdsd/protocols/__init__.py b/asdsd/protocols/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/asdsd/protocols/default/__init__.py b/asdsd/protocols/default/__init__.py new file mode 100644 index 0000000000..52e51b51e3 --- /dev/null +++ b/asdsd/protocols/default/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the support resources for the default protocol.""" diff --git a/asdsd/protocols/default/message.py b/asdsd/protocols/default/message.py new file mode 100644 index 0000000000..475714a2f0 --- /dev/null +++ b/asdsd/protocols/default/message.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the default message definition.""" +from enum import Enum +from typing import Optional + +from aea.protocols.base import Message + + +class DefaultMessage(Message): + """The Default message class.""" + + protocol_id = "default" + + class Type(Enum): + """Default message types.""" + + BYTES = "bytes" + ERROR = "error" + + def __str__(self): + """Get the string representation.""" + return self.value + + class ErrorCode(Enum): + """The error codes.""" + + UNSUPPORTED_PROTOCOL = -10001 + DECODING_ERROR = -10002 + INVALID_MESSAGE = -10003 + UNSUPPORTED_SKILL = -10004 + + def __init__(self, type: Optional[Type] = None, + **kwargs): + """ + Initialize. + + :param type: the type. + """ + super().__init__(type=type, **kwargs) + assert self.check_consistency(), "DefaultMessage initialization inconsistent." diff --git a/asdsd/protocols/default/protocol.yaml b/asdsd/protocols/default/protocol.yaml new file mode 100644 index 0000000000..66d97cfc83 --- /dev/null +++ b/asdsd/protocols/default/protocol.yaml @@ -0,0 +1,6 @@ +name: 'default' +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +description: "The default protocol allows for any bytes message." \ No newline at end of file diff --git a/asdsd/protocols/default/serialization.py b/asdsd/protocols/default/serialization.py new file mode 100644 index 0000000000..080b8f386b --- /dev/null +++ b/asdsd/protocols/default/serialization.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Serialization module for the default protocol.""" +import base64 +import json +from typing import cast + +from aea.protocols.base import Message +from aea.protocols.base import Serializer +from aea.protocols.default.message import DefaultMessage + + +class DefaultSerializer(Serializer): + """Serialization for the 'default' protocol.""" + + def encode(self, msg: Message) -> bytes: + """Encode a 'default' message into bytes.""" + body = {} # Dict[str, Any] + + msg_type = DefaultMessage.Type(msg.get("type")) + body["type"] = str(msg_type.value) + + if msg_type == DefaultMessage.Type.BYTES: + content = cast(bytes, msg.get("content")) + body["content"] = base64.b64encode(content).decode("utf-8") + elif msg_type == DefaultMessage.Type.ERROR: + body["error_code"] = cast(str, msg.get("error_code")) + body["error_msg"] = cast(str, msg.get("error_msg")) + body["error_data"] = cast(str, msg.get("error_data")) + else: + raise ValueError("Type not recognized.") + + bytes_msg = json.dumps(body).encode("utf-8") + return bytes_msg + + def decode(self, obj: bytes) -> Message: + """Decode bytes into a 'default' message.""" + json_body = json.loads(obj.decode("utf-8")) + body = {} + + msg_type = DefaultMessage.Type(json_body["type"]) + body["type"] = msg_type + if msg_type == DefaultMessage.Type.BYTES: + content = base64.b64decode(json_body["content"].encode("utf-8")) + body["content"] = content # type: ignore + elif msg_type == DefaultMessage.Type.ERROR: + body["error_code"] = json_body["error_code"] + body["error_msg"] = json_body["error_msg"] + body["error_data"] = json_body["error_data"] + else: + raise ValueError("Type not recognized.") + + return DefaultMessage(type=msg_type, body=body) diff --git a/asdsd/protocols/gym/__init__.py b/asdsd/protocols/gym/__init__.py new file mode 100644 index 0000000000..a1766ab9cd --- /dev/null +++ b/asdsd/protocols/gym/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the support resources for the Gym protocol.""" diff --git a/asdsd/protocols/gym/message.py b/asdsd/protocols/gym/message.py new file mode 100644 index 0000000000..616b3e8226 --- /dev/null +++ b/asdsd/protocols/gym/message.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the FIPA message definition.""" +from enum import Enum +from typing import Optional, Union + +from aea.protocols.base import Message + + +class GymMessage(Message): + """The Gym message class.""" + + protocol_id = "gym" + + class Performative(Enum): + """Gym performatives.""" + + ACT = 'act' + PERCEPT = 'percept' + RESET = 'reset' + CLOSE = 'close' + + def __str__(self): + """Get string representation.""" + return self.value + + def __init__(self, performative: Optional[Union[str, Performative]] = None, **kwargs): + """ + Initialize. + + :param type: the type. + """ + super().__init__(performative=GymMessage.Performative(performative), **kwargs) + assert self.check_consistency(), "GymMessage initialization inconsistent." + + def check_consistency(self) -> bool: + """Check that the data is consistent.""" + try: + assert self.is_set("performative") + performative = GymMessage.Performative(self.get("performative")) + if performative == GymMessage.Performative.ACT: + assert self.is_set("action") + assert self.is_set("step_id") + elif performative == GymMessage.Performative.PERCEPT: + assert self.is_set("observation") + assert self.is_set("reward") + assert self.is_set("done") + assert self.is_set("info") + assert self.is_set("step_id") + elif performative == GymMessage.Performative.RESET or performative == GymMessage.Performative.CLOSE: + pass + else: + raise ValueError("Performative not recognized.") + + except (AssertionError, ValueError, KeyError): + return False + + return True diff --git a/asdsd/protocols/gym/protocol.yaml b/asdsd/protocols/gym/protocol.yaml new file mode 100644 index 0000000000..7b10d5429b --- /dev/null +++ b/asdsd/protocols/gym/protocol.yaml @@ -0,0 +1,6 @@ +name: gym +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +description: "The gym protocol implements the messages an agent needs to engage with a gym connection." \ No newline at end of file diff --git a/asdsd/protocols/gym/serialization.py b/asdsd/protocols/gym/serialization.py new file mode 100644 index 0000000000..6fb14c1354 --- /dev/null +++ b/asdsd/protocols/gym/serialization.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Serialization for the FIPA protocol.""" +import base64 +import copy +import json +import pickle +from typing import Any, TYPE_CHECKING + +from aea.protocols.base import Message +from aea.protocols.base import Serializer + +if TYPE_CHECKING: + from packages.protocols.gym.message import GymMessage +else: + from gym_protocol.message import GymMessage + + +class GymSerializer(Serializer): + """Serialization for the Gym protocol.""" + + def encode(self, msg: Message) -> bytes: + """ + Decode the message. + + :param msg: the message object + :return: the bytes + """ + performative = GymMessage.Performative(msg.get("performative")) + new_body = copy.copy(msg.body) + new_body["performative"] = performative.value + + if performative == GymMessage.Performative.ACT: + action = msg.body["action"] # type: Any + action_bytes = base64.b64encode(pickle.dumps(action)).decode("utf-8") + new_body["action"] = action_bytes + new_body["step_id"] = msg.body["step_id"] + elif performative == GymMessage.Performative.PERCEPT: + # observation, reward and info are gym implementation specific, done is boolean + observation = msg.body["observation"] # type: Any + observation_bytes = base64.b64encode(pickle.dumps(observation)).decode("utf-8") + new_body["observation"] = observation_bytes + reward = msg.body["reward"] # type: Any + reward_bytes = base64.b64encode(pickle.dumps(reward)).decode("utf-8") + new_body["reward"] = reward_bytes + info = msg.body["info"] # type: Any + info_bytes = base64.b64encode(pickle.dumps(info)).decode("utf-8") + new_body["info"] = info_bytes + new_body["step_id"] = msg.body["step_id"] + + gym_message_bytes = json.dumps(new_body).encode("utf-8") + return gym_message_bytes + + def decode(self, obj: bytes) -> Message: + """ + Decode the message. + + :param obj: the bytes object + :return: the message + """ + json_msg = json.loads(obj.decode("utf-8")) + performative = GymMessage.Performative(json_msg["performative"]) + new_body = copy.copy(json_msg) + new_body["type"] = performative + + if performative == GymMessage.Performative.ACT: + action_bytes = base64.b64decode(json_msg["action"]) + action = pickle.loads(action_bytes) + new_body["action"] = action + new_body["step_id"] = json_msg["step_id"] + elif performative == GymMessage.Performative.PERCEPT: + # observation, reward and info are gym implementation specific, done is boolean + observation_bytes = base64.b64decode(json_msg["observation"]) + observation = pickle.loads(observation_bytes) + new_body["observation"] = observation + reward_bytes = base64.b64decode(json_msg["reward"]) + reward = pickle.loads(reward_bytes) + new_body["reward"] = reward + info_bytes = base64.b64decode(json_msg["info"]) + info = pickle.loads(info_bytes) + new_body["info"] = info + new_body["step_id"] = json_msg["step_id"] + + gym_message = GymMessage(performative=performative, body=new_body) + return gym_message diff --git a/asdsd/protocols/tac/__init__.py b/asdsd/protocols/tac/__init__.py new file mode 100644 index 0000000000..430d160a4f --- /dev/null +++ b/asdsd/protocols/tac/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the support resources for the TAC protocol.""" diff --git a/asdsd/protocols/tac/message.py b/asdsd/protocols/tac/message.py new file mode 100644 index 0000000000..07a100d90e --- /dev/null +++ b/asdsd/protocols/tac/message.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the default message definition.""" +from enum import Enum +from typing import Dict, Optional, cast + +from aea.protocols.base import Message + + +class TACMessage(Message): + """The TAC message class.""" + + protocol_id = "tac" + + class Type(Enum): + """TAC Message types.""" + + REGISTER = "register" + UNREGISTER = "unregister" + TRANSACTION = "transaction" + GET_STATE_UPDATE = "get_state_update" + CANCELLED = "cancelled" + GAME_DATA = "game_data" + TRANSACTION_CONFIRMATION = "transaction_confirmation" + STATE_UPDATE = "state_update" + TAC_ERROR = "tac_error" + + def __str__(self): + """Get string representation.""" + return self.value + + class ErrorCode(Enum): + """This class defines the error codes.""" + + GENERIC_ERROR = 0 + REQUEST_NOT_VALID = 1 + AGENT_PBK_ALREADY_REGISTERED = 2 + AGENT_NAME_ALREADY_REGISTERED = 3 + AGENT_NOT_REGISTERED = 4 + TRANSACTION_NOT_VALID = 5 + TRANSACTION_NOT_MATCHING = 6 + AGENT_NAME_NOT_IN_WHITELIST = 7 + COMPETITION_NOT_RUNNING = 8 + DIALOGUE_INCONSISTENT = 9 + + _from_ec_to_msg = { + ErrorCode.GENERIC_ERROR: "Unexpected error.", + ErrorCode.REQUEST_NOT_VALID: "Request not recognized", + ErrorCode.AGENT_PBK_ALREADY_REGISTERED: "Agent pbk already registered.", + ErrorCode.AGENT_NAME_ALREADY_REGISTERED: "Agent name already registered.", + ErrorCode.AGENT_NOT_REGISTERED: "Agent not registered.", + ErrorCode.TRANSACTION_NOT_VALID: "Error in checking transaction", + ErrorCode.TRANSACTION_NOT_MATCHING: "The transaction request does not match with a previous transaction request with the same id.", + ErrorCode.AGENT_NAME_NOT_IN_WHITELIST: "Agent name not in whitelist.", + ErrorCode.COMPETITION_NOT_RUNNING: "The competition is not running yet.", + ErrorCode.DIALOGUE_INCONSISTENT: "The message is inconsistent with the dialogue." + } # type: Dict[ErrorCode, str] + + def __init__(self, tac_type: Optional[Type] = None, + **kwargs): + """ + Initialize. + + :param tac_type: the type of TAC message. + """ + super().__init__(type=tac_type, **kwargs) + assert self.check_consistency(), "TACMessage initialization inconsistent." + + def check_consistency(self) -> bool: + """Check that the data is consistent.""" + try: + assert self.is_set("type") + tac_type = TACMessage.Type(self.get("type")) + if tac_type == TACMessage.Type.REGISTER: + assert self.is_set("agent_name") + elif tac_type == TACMessage.Type.UNREGISTER: + pass + elif tac_type == TACMessage.Type.TRANSACTION: + assert self.is_set("transaction_id") + assert self.is_set("is_sender_buyer") + assert self.is_set("counterparty") + assert self.is_set("amount") + amount = cast(float, self.get("amount")) + assert amount >= 0.0 + assert self.is_set("quantities_by_good_pbk") + quantities_by_good_pbk = cast(Dict[str, int], self.get("quantities_by_good_pbk")) + assert len(quantities_by_good_pbk.keys()) == len(set(quantities_by_good_pbk.keys())) + assert all(quantity >= 0 for quantity in quantities_by_good_pbk.values()) + elif tac_type == TACMessage.Type.GET_STATE_UPDATE: + pass + elif tac_type == TACMessage.Type.CANCELLED: + pass + elif tac_type == TACMessage.Type.GAME_DATA: + assert self.is_set("money") + assert self.is_set("endowment") + assert self.is_set("utility_params") + assert self.is_set("nb_agents") + assert self.is_set("nb_goods") + assert self.is_set("tx_fee") + assert self.is_set("agent_pbk_to_name") + assert self.is_set("good_pbk_to_name") + elif tac_type == TACMessage.Type.TRANSACTION_CONFIRMATION: + assert self.is_set("transaction_id") + elif tac_type == TACMessage.Type.STATE_UPDATE: + assert self.is_set("initial_state") + assert self.is_set("transactions") + elif tac_type == TACMessage.Type.TAC_ERROR: + assert self.is_set("error_code") + error_code = self.get("error_code") + assert error_code in set(self.ErrorCode) + else: + raise ValueError("Type not recognized.") + except (AssertionError, ValueError): + return False + + return True diff --git a/asdsd/protocols/tac/protocol.yaml b/asdsd/protocols/tac/protocol.yaml new file mode 100644 index 0000000000..435e33b383 --- /dev/null +++ b/asdsd/protocols/tac/protocol.yaml @@ -0,0 +1,6 @@ +name: tac +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +description: "The tac protocol implements the messages an AEA needs to participate in the TAC." \ No newline at end of file diff --git a/asdsd/protocols/tac/serialization.py b/asdsd/protocols/tac/serialization.py new file mode 100644 index 0000000000..e4e085d5b0 --- /dev/null +++ b/asdsd/protocols/tac/serialization.py @@ -0,0 +1,234 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Serialization for the TAC protocol.""" + +from typing import Any, Dict, List, cast, TYPE_CHECKING + +from aea.protocols.base import Message +from aea.protocols.base import Serializer + +if TYPE_CHECKING: + from packages.protocols.tac import tac_pb2 + from packages.protocols.tac.message import TACMessage +else: + import tac_protocol.tac_pb2 as tac_pb2 + from tac_protocol.message import TACMessage + + +def _from_dict_to_pairs(d): + """Convert a flat dictionary into a list of StrStrPair or StrIntPair.""" + result = [] + items = sorted(d.items(), key=lambda pair: pair[0]) + for key, value in items: + if type(value) == int: + pair = tac_pb2.StrIntPair() + elif type(value) == str: + pair = tac_pb2.StrStrPair() + else: + raise ValueError("Either 'int' or 'str', not {}".format(type(value))) + pair.first = key + pair.second = value + result.append(pair) + return result + + +def _from_pairs_to_dict(pairs): + """Convert a list of StrStrPair or StrIntPair into a flat dictionary.""" + result = {} + for pair in pairs: + key = pair.first + value = pair.second + result[key] = value + return result + + +class TACSerializer(Serializer): + """Serialization for the TAC protocol.""" + + def encode(self, msg: Message) -> bytes: + """ + Decode the message. + + :param msg: the message object + :return: the bytes + """ + tac_type = TACMessage.Type(msg.get("type")) + tac_container = tac_pb2.TACMessage() + + if tac_type == TACMessage.Type.REGISTER: + agent_name = msg.get("agent_name") + tac_msg = tac_pb2.TACAgent.Register() # type: ignore + tac_msg.agent_name = agent_name + tac_container.register.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.UNREGISTER: + tac_msg = tac_pb2.TACAgent.Unregister() # type: ignore + tac_container.unregister.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.TRANSACTION: + tac_msg = tac_pb2.TACAgent.Transaction() # type: ignore + tac_msg.transaction_id = msg.get("transaction_id") + tac_msg.is_sender_buyer = msg.get("is_sender_buyer") + tac_msg.counterparty = msg.get("counterparty") + tac_msg.amount = msg.get("amount") + tac_msg.quantities.extend(_from_dict_to_pairs(msg.get("quantities_by_good_pbk"))) + tac_container.transaction.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.GET_STATE_UPDATE: + tac_msg = tac_pb2.TACAgent.GetStateUpdate() # type: ignore + tac_container.get_state_update.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.CANCELLED: + tac_msg = tac_pb2.TACController.Cancelled() # type: ignore + tac_container.cancelled.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.GAME_DATA: + tac_msg = tac_pb2.TACController.GameData() # type: ignore + tac_msg.money = msg.get("money") + tac_msg.endowment.extend(msg.get("endowment")) + tac_msg.utility_params.extend(msg.get("utility_params")) + tac_msg.nb_agents = msg.get("nb_agents") + tac_msg.nb_goods = msg.get("nb_goods") + tac_msg.tx_fee = msg.get("tx_fee") + tac_msg.agent_pbk_to_name.extend(_from_dict_to_pairs(msg.get("agent_pbk_to_name"))) + tac_msg.good_pbk_to_name.extend(_from_dict_to_pairs(msg.get("good_pbk_to_name"))) + tac_container.game_data.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.TRANSACTION_CONFIRMATION: + tac_msg = tac_pb2.TACController.TransactionConfirmation() # type: ignore + tac_msg.transaction_id = msg.get("transaction_id") + tac_container.transaction_confirmation.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.STATE_UPDATE: + tac_msg = tac_pb2.TACController.StateUpdate() # type: ignore + game_data_json = msg.get("initial_state") + game_data = tac_pb2.TACController.GameData() # type: ignore + game_data.money = game_data_json["money"] # type: ignore + game_data.endowment.extend(game_data_json["endowment"]) # type: ignore + game_data.utility_params.extend(game_data_json["utility_params"]) # type: ignore + game_data.nb_agents = game_data_json["nb_agents"] # type: ignore + game_data.nb_goods = game_data_json["nb_goods"] # type: ignore + game_data.tx_fee = game_data_json["tx_fee"] # type: ignore + game_data.agent_pbk_to_name.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["agent_pbk_to_name"]))) # type: ignore + game_data.good_pbk_to_name.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["good_pbk_to_name"]))) # type: ignore + + tac_msg.initial_state.CopyFrom(game_data) + + transactions = [] + msg_transactions = cast(List[Any], msg.get("transactions")) + for t in msg_transactions: + tx = tac_pb2.TACAgent.Transaction() # type: ignore + tx.transaction_id = t.get("transaction_id") + tx.is_sender_buyer = t.get("is_sender_buyer") + tx.counterparty = t.get("counterparty") + tx.amount = t.get("amount") + tx.quantities.extend(_from_dict_to_pairs(t.get("quantities_by_good_pbk"))) + transactions.append(tx) + tac_msg.txs.extend(transactions) + tac_container.state_update.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.TAC_ERROR: + tac_msg = tac_pb2.TACController.Error() # type: ignore + tac_msg.error_code = TACMessage.ErrorCode(msg.get("error_code")).value + if msg.is_set("error_msg"): + tac_msg.error_msg = msg.get("error_msg") + if msg.is_set("details"): + tac_msg.details.update(msg.get("details")) + + tac_container.error.CopyFrom(tac_msg) + else: + raise ValueError("Type not recognized: {}.".format(tac_type)) + + tac_message_bytes = tac_container.SerializeToString() + return tac_message_bytes + + def decode(self, obj: bytes) -> Message: + """ + Decode the message. + + :param obj: the bytes object + :return: the message + """ + tac_container = tac_pb2.TACMessage() + tac_container.ParseFromString(obj) + + new_body = {} # type: Dict[str, Any] + tac_type = tac_container.WhichOneof("content") + + if tac_type == "register": + new_body["type"] = TACMessage.Type.REGISTER + new_body["agent_name"] = tac_container.register.agent_name + elif tac_type == "unregister": + new_body["type"] = TACMessage.Type.UNREGISTER + elif tac_type == "transaction": + new_body["type"] = TACMessage.Type.TRANSACTION + new_body["transaction_id"] = tac_container.transaction.transaction_id + new_body["is_sender_buyer"] = tac_container.transaction.is_sender_buyer + new_body["counterparty"] = tac_container.transaction.counterparty + new_body["amount"] = tac_container.transaction.amount + new_body["quantities_by_good_pbk"] = _from_pairs_to_dict(tac_container.transaction.quantities) + elif tac_type == "get_state_update": + new_body["type"] = TACMessage.Type.GET_STATE_UPDATE + elif tac_type == "cancelled": + new_body["type"] = TACMessage.Type.CANCELLED + elif tac_type == "game_data": + new_body["type"] = TACMessage.Type.GAME_DATA + new_body["money"] = tac_container.game_data.money + new_body["endowment"] = list(tac_container.game_data.endowment) + new_body["utility_params"] = list(tac_container.game_data.utility_params) + new_body["nb_agents"] = tac_container.game_data.nb_agents + new_body["nb_goods"] = tac_container.game_data.nb_goods + new_body["tx_fee"] = tac_container.game_data.tx_fee + new_body["agent_pbk_to_name"] = _from_pairs_to_dict(tac_container.game_data.agent_pbk_to_name) + new_body["good_pbk_to_name"] = _from_pairs_to_dict(tac_container.game_data.good_pbk_to_name) + elif tac_type == "transaction_confirmation": + new_body["type"] = TACMessage.Type.TRANSACTION_CONFIRMATION + new_body["transaction_id"] = tac_container.transaction_confirmation.transaction_id + elif tac_type == "state_update": + new_body["type"] = TACMessage.Type.STATE_UPDATE + game_data = dict( + money=tac_container.state_update.initial_state.money, + endowment=tac_container.state_update.initial_state.endowment, + utility_params=tac_container.state_update.initial_state.utility_params, + nb_agents=tac_container.state_update.initial_state.nb_agents, + nb_goods=tac_container.state_update.initial_state.nb_goods, + tx_fee=tac_container.state_update.initial_state.tx_fee, + agent_pbk_to_name=_from_pairs_to_dict(tac_container.state_update.initial_state.agent_pbk_to_name), + good_pbk_to_name=_from_pairs_to_dict(tac_container.state_update.initial_state.good_pbk_to_name), + ) + new_body["initial_state"] = game_data + transactions = [] + for t in tac_container.state_update.txs: + tx_json = dict( + transaction_id=t.transaction_id, + is_sender_buyer=t.is_sender_buyer, + counterparty=t.counterparty, + amount=t.amount, + quantities_by_good_pbk=_from_pairs_to_dict(t.quantities), + ) + transactions.append(tx_json) + new_body["transactions"] = transactions + elif tac_type == "error": + new_body["type"] = TACMessage.Type.TAC_ERROR + new_body["error_code"] = TACMessage.ErrorCode(tac_container.error.error_code) + if tac_container.error.error_msg: + new_body["error_msg"] = tac_container.error.error_msg + if tac_container.error.details: + new_body["details"] = dict(tac_container.error.details) + else: + raise ValueError("Type not recognized.") + + tac_type = TACMessage.Type(new_body["type"]) + new_body["type"] = tac_type + tac_message = TACMessage(tac_type=tac_type, body=new_body) + return tac_message diff --git a/asdsd/protocols/tac/tac.proto b/asdsd/protocols/tac/tac.proto new file mode 100644 index 0000000000..d6a714d425 --- /dev/null +++ b/asdsd/protocols/tac/tac.proto @@ -0,0 +1,102 @@ +syntax = "proto3"; + +package fetch.oef.pb; + +import "google/protobuf/struct.proto"; + +message StrIntPair { + string first = 1; + int32 second = 2; +} + +message StrStrPair { + string first = 1; + string second = 2; +} + +message TACController { + + message Registered { + } + message Unregistered { + } + message Cancelled { + } + + message GameData { + double money = 1; + repeated int32 endowment = 2; + repeated double utility_params = 3; + int32 nb_agents = 4; + int32 nb_goods = 5; + double tx_fee = 6; + repeated StrStrPair agent_pbk_to_name = 7; + repeated StrStrPair good_pbk_to_name = 8; + } + + message TransactionConfirmation { + string transaction_id = 1; + } + + message StateUpdate { + GameData initial_state = 1; + repeated TACAgent.Transaction txs = 2; + } + + message Error { + enum ErrorCode { + GENERIC_ERROR = 0; + REQUEST_NOT_VALID = 1; + AGENT_PBK_ALREADY_REGISTERED = 2; + AGENT_NAME_ALREADY_REGISTERED = 3; + AGENT_NOT_REGISTERED = 4; + TRANSACTION_NOT_VALID = 5; + TRANSACTION_NOT_MATCHING = 6; + AGENT_NAME_NOT_IN_WHITELIST = 7; + COMPETITION_NOT_RUNNING = 8; + DIALOGUE_INCONSISTENT = 9; + } + + ErrorCode error_code = 1; + string error_msg = 2; + google.protobuf.Struct details = 3; + } + +} + +message TACAgent { + + message Register { + string agent_name = 1; + } + message Unregister { + } + + message Transaction { + string transaction_id = 1; + bool is_sender_buyer = 2; // is the sender of this message a buyer? + string counterparty = 3; + double amount = 4; + repeated StrIntPair quantities = 5; + } + + message GetStateUpdate { + } + +} + +message TACMessage { + oneof content{ + TACAgent.Register register = 1; + TACAgent.Unregister unregister = 2; + TACAgent.Transaction transaction = 3; + TACAgent.GetStateUpdate get_state_update = 4; + TACController.Registered registered = 5; + TACController.Unregistered unregistered = 6; + TACController.Cancelled cancelled = 7; + TACController.GameData game_data = 8; + TACController.TransactionConfirmation transaction_confirmation = 9; + TACController.StateUpdate state_update = 10; + TACController.Error error = 11; + } +} diff --git a/asdsd/protocols/tac/tac_pb2.py b/asdsd/protocols/tac/tac_pb2.py new file mode 100644 index 0000000000..5eb63f23f1 --- /dev/null +++ b/asdsd/protocols/tac/tac_pb2.py @@ -0,0 +1,899 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: tac.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +from google.protobuf import descriptor_pb2 +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='tac.proto', + package='fetch.oef.pb', + syntax='proto3', + serialized_pb=_b('\n\ttac.proto\x12\x0c\x66\x65tch.oef.pb\x1a\x1cgoogle/protobuf/struct.proto\"+\n\nStrIntPair\x12\r\n\x05\x66irst\x18\x01 \x01(\t\x12\x0e\n\x06second\x18\x02 \x01(\x05\"+\n\nStrStrPair\x12\r\n\x05\x66irst\x18\x01 \x01(\t\x12\x0e\n\x06second\x18\x02 \x01(\t\"\x80\x07\n\rTACController\x1a\x0c\n\nRegistered\x1a\x0e\n\x0cUnregistered\x1a\x0b\n\tCancelled\x1a\xe2\x01\n\x08GameData\x12\r\n\x05money\x18\x01 \x01(\x01\x12\x11\n\tendowment\x18\x02 \x03(\x05\x12\x16\n\x0eutility_params\x18\x03 \x03(\x01\x12\x11\n\tnb_agents\x18\x04 \x01(\x05\x12\x10\n\x08nb_goods\x18\x05 \x01(\x05\x12\x0e\n\x06tx_fee\x18\x06 \x01(\x01\x12\x33\n\x11\x61gent_pbk_to_name\x18\x07 \x03(\x0b\x32\x18.fetch.oef.pb.StrStrPair\x12\x32\n\x10good_pbk_to_name\x18\x08 \x03(\x0b\x32\x18.fetch.oef.pb.StrStrPair\x1a\x31\n\x17TransactionConfirmation\x12\x16\n\x0etransaction_id\x18\x01 \x01(\t\x1a{\n\x0bStateUpdate\x12;\n\rinitial_state\x18\x01 \x01(\x0b\x32$.fetch.oef.pb.TACController.GameData\x12/\n\x03txs\x18\x02 \x03(\x0b\x32\".fetch.oef.pb.TACAgent.Transaction\x1a\xae\x03\n\x05\x45rror\x12?\n\nerror_code\x18\x01 \x01(\x0e\x32+.fetch.oef.pb.TACController.Error.ErrorCode\x12\x11\n\terror_msg\x18\x02 \x01(\t\x12(\n\x07\x64\x65tails\x18\x03 \x01(\x0b\x32\x17.google.protobuf.Struct\"\xa6\x02\n\tErrorCode\x12\x11\n\rGENERIC_ERROR\x10\x00\x12\x15\n\x11REQUEST_NOT_VALID\x10\x01\x12 \n\x1c\x41GENT_PBK_ALREADY_REGISTERED\x10\x02\x12!\n\x1d\x41GENT_NAME_ALREADY_REGISTERED\x10\x03\x12\x18\n\x14\x41GENT_NOT_REGISTERED\x10\x04\x12\x19\n\x15TRANSACTION_NOT_VALID\x10\x05\x12\x1c\n\x18TRANSACTION_NOT_MATCHING\x10\x06\x12\x1f\n\x1b\x41GENT_NAME_NOT_IN_WHITELIST\x10\x07\x12\x1b\n\x17\x43OMPETITION_NOT_RUNNING\x10\x08\x12\x19\n\x15\x44IALOGUE_INCONSISTENT\x10\t\"\xdf\x01\n\x08TACAgent\x1a\x1e\n\x08Register\x12\x12\n\nagent_name\x18\x01 \x01(\t\x1a\x0c\n\nUnregister\x1a\x92\x01\n\x0bTransaction\x12\x16\n\x0etransaction_id\x18\x01 \x01(\t\x12\x17\n\x0fis_sender_buyer\x18\x02 \x01(\x08\x12\x14\n\x0c\x63ounterparty\x18\x03 \x01(\t\x12\x0e\n\x06\x61mount\x18\x04 \x01(\x01\x12,\n\nquantities\x18\x05 \x03(\x0b\x32\x18.fetch.oef.pb.StrIntPair\x1a\x10\n\x0eGetStateUpdate\"\xc8\x05\n\nTACMessage\x12\x33\n\x08register\x18\x01 \x01(\x0b\x32\x1f.fetch.oef.pb.TACAgent.RegisterH\x00\x12\x37\n\nunregister\x18\x02 \x01(\x0b\x32!.fetch.oef.pb.TACAgent.UnregisterH\x00\x12\x39\n\x0btransaction\x18\x03 \x01(\x0b\x32\".fetch.oef.pb.TACAgent.TransactionH\x00\x12\x41\n\x10get_state_update\x18\x04 \x01(\x0b\x32%.fetch.oef.pb.TACAgent.GetStateUpdateH\x00\x12<\n\nregistered\x18\x05 \x01(\x0b\x32&.fetch.oef.pb.TACController.RegisteredH\x00\x12@\n\x0cunregistered\x18\x06 \x01(\x0b\x32(.fetch.oef.pb.TACController.UnregisteredH\x00\x12:\n\tcancelled\x18\x07 \x01(\x0b\x32%.fetch.oef.pb.TACController.CancelledH\x00\x12\x39\n\tgame_data\x18\x08 \x01(\x0b\x32$.fetch.oef.pb.TACController.GameDataH\x00\x12W\n\x18transaction_confirmation\x18\t \x01(\x0b\x32\x33.fetch.oef.pb.TACController.TransactionConfirmationH\x00\x12?\n\x0cstate_update\x18\n \x01(\x0b\x32\'.fetch.oef.pb.TACController.StateUpdateH\x00\x12\x32\n\x05\x65rror\x18\x0b \x01(\x0b\x32!.fetch.oef.pb.TACController.ErrorH\x00\x42\t\n\x07\x63ontentb\x06proto3') + , + dependencies=[google_dot_protobuf_dot_struct__pb2.DESCRIPTOR,]) +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + + + +_TACCONTROLLER_ERROR_ERRORCODE = _descriptor.EnumDescriptor( + name='ErrorCode', + full_name='fetch.oef.pb.TACController.Error.ErrorCode', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='GENERIC_ERROR', index=0, number=0, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='REQUEST_NOT_VALID', index=1, number=1, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='AGENT_PBK_ALREADY_REGISTERED', index=2, number=2, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='AGENT_NAME_ALREADY_REGISTERED', index=3, number=3, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='AGENT_NOT_REGISTERED', index=4, number=4, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='TRANSACTION_NOT_VALID', index=5, number=5, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='TRANSACTION_NOT_MATCHING', index=6, number=6, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='AGENT_NAME_NOT_IN_WHITELIST', index=7, number=7, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='COMPETITION_NOT_RUNNING', index=8, number=8, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='DIALOGUE_INCONSISTENT', index=9, number=9, + options=None, + type=None), + ], + containing_type=None, + options=None, + serialized_start=750, + serialized_end=1044, +) +_sym_db.RegisterEnumDescriptor(_TACCONTROLLER_ERROR_ERRORCODE) + + +_STRINTPAIR = _descriptor.Descriptor( + name='StrIntPair', + full_name='fetch.oef.pb.StrIntPair', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='first', full_name='fetch.oef.pb.StrIntPair.first', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='second', full_name='fetch.oef.pb.StrIntPair.second', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=57, + serialized_end=100, +) + + +_STRSTRPAIR = _descriptor.Descriptor( + name='StrStrPair', + full_name='fetch.oef.pb.StrStrPair', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='first', full_name='fetch.oef.pb.StrStrPair.first', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='second', full_name='fetch.oef.pb.StrStrPair.second', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=102, + serialized_end=145, +) + + +_TACCONTROLLER_REGISTERED = _descriptor.Descriptor( + name='Registered', + full_name='fetch.oef.pb.TACController.Registered', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=165, + serialized_end=177, +) + +_TACCONTROLLER_UNREGISTERED = _descriptor.Descriptor( + name='Unregistered', + full_name='fetch.oef.pb.TACController.Unregistered', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=179, + serialized_end=193, +) + +_TACCONTROLLER_CANCELLED = _descriptor.Descriptor( + name='Cancelled', + full_name='fetch.oef.pb.TACController.Cancelled', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=195, + serialized_end=206, +) + +_TACCONTROLLER_GAMEDATA = _descriptor.Descriptor( + name='GameData', + full_name='fetch.oef.pb.TACController.GameData', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='money', full_name='fetch.oef.pb.TACController.GameData.money', index=0, + number=1, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='endowment', full_name='fetch.oef.pb.TACController.GameData.endowment', index=1, + number=2, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='utility_params', full_name='fetch.oef.pb.TACController.GameData.utility_params', index=2, + number=3, type=1, cpp_type=5, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='nb_agents', full_name='fetch.oef.pb.TACController.GameData.nb_agents', index=3, + number=4, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='nb_goods', full_name='fetch.oef.pb.TACController.GameData.nb_goods', index=4, + number=5, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='tx_fee', full_name='fetch.oef.pb.TACController.GameData.tx_fee', index=5, + number=6, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='agent_pbk_to_name', full_name='fetch.oef.pb.TACController.GameData.agent_pbk_to_name', index=6, + number=7, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='good_pbk_to_name', full_name='fetch.oef.pb.TACController.GameData.good_pbk_to_name', index=7, + number=8, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=209, + serialized_end=435, +) + +_TACCONTROLLER_TRANSACTIONCONFIRMATION = _descriptor.Descriptor( + name='TransactionConfirmation', + full_name='fetch.oef.pb.TACController.TransactionConfirmation', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='transaction_id', full_name='fetch.oef.pb.TACController.TransactionConfirmation.transaction_id', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=437, + serialized_end=486, +) + +_TACCONTROLLER_STATEUPDATE = _descriptor.Descriptor( + name='StateUpdate', + full_name='fetch.oef.pb.TACController.StateUpdate', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='initial_state', full_name='fetch.oef.pb.TACController.StateUpdate.initial_state', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='txs', full_name='fetch.oef.pb.TACController.StateUpdate.txs', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=488, + serialized_end=611, +) + +_TACCONTROLLER_ERROR = _descriptor.Descriptor( + name='Error', + full_name='fetch.oef.pb.TACController.Error', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='error_code', full_name='fetch.oef.pb.TACController.Error.error_code', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='error_msg', full_name='fetch.oef.pb.TACController.Error.error_msg', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='details', full_name='fetch.oef.pb.TACController.Error.details', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _TACCONTROLLER_ERROR_ERRORCODE, + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=614, + serialized_end=1044, +) + +_TACCONTROLLER = _descriptor.Descriptor( + name='TACController', + full_name='fetch.oef.pb.TACController', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[_TACCONTROLLER_REGISTERED, _TACCONTROLLER_UNREGISTERED, _TACCONTROLLER_CANCELLED, _TACCONTROLLER_GAMEDATA, _TACCONTROLLER_TRANSACTIONCONFIRMATION, _TACCONTROLLER_STATEUPDATE, _TACCONTROLLER_ERROR, ], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=148, + serialized_end=1044, +) + + +_TACAGENT_REGISTER = _descriptor.Descriptor( + name='Register', + full_name='fetch.oef.pb.TACAgent.Register', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='agent_name', full_name='fetch.oef.pb.TACAgent.Register.agent_name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1059, + serialized_end=1089, +) + +_TACAGENT_UNREGISTER = _descriptor.Descriptor( + name='Unregister', + full_name='fetch.oef.pb.TACAgent.Unregister', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1091, + serialized_end=1103, +) + +_TACAGENT_TRANSACTION = _descriptor.Descriptor( + name='Transaction', + full_name='fetch.oef.pb.TACAgent.Transaction', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='transaction_id', full_name='fetch.oef.pb.TACAgent.Transaction.transaction_id', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='is_sender_buyer', full_name='fetch.oef.pb.TACAgent.Transaction.is_sender_buyer', index=1, + number=2, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='counterparty', full_name='fetch.oef.pb.TACAgent.Transaction.counterparty', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='amount', full_name='fetch.oef.pb.TACAgent.Transaction.amount', index=3, + number=4, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='quantities', full_name='fetch.oef.pb.TACAgent.Transaction.quantities', index=4, + number=5, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1106, + serialized_end=1252, +) + +_TACAGENT_GETSTATEUPDATE = _descriptor.Descriptor( + name='GetStateUpdate', + full_name='fetch.oef.pb.TACAgent.GetStateUpdate', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1254, + serialized_end=1270, +) + +_TACAGENT = _descriptor.Descriptor( + name='TACAgent', + full_name='fetch.oef.pb.TACAgent', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[_TACAGENT_REGISTER, _TACAGENT_UNREGISTER, _TACAGENT_TRANSACTION, _TACAGENT_GETSTATEUPDATE, ], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1047, + serialized_end=1270, +) + + +_TACMESSAGE = _descriptor.Descriptor( + name='TACMessage', + full_name='fetch.oef.pb.TACMessage', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='register', full_name='fetch.oef.pb.TACMessage.register', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='unregister', full_name='fetch.oef.pb.TACMessage.unregister', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='transaction', full_name='fetch.oef.pb.TACMessage.transaction', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='get_state_update', full_name='fetch.oef.pb.TACMessage.get_state_update', index=3, + number=4, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='registered', full_name='fetch.oef.pb.TACMessage.registered', index=4, + number=5, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='unregistered', full_name='fetch.oef.pb.TACMessage.unregistered', index=5, + number=6, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='cancelled', full_name='fetch.oef.pb.TACMessage.cancelled', index=6, + number=7, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='game_data', full_name='fetch.oef.pb.TACMessage.game_data', index=7, + number=8, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='transaction_confirmation', full_name='fetch.oef.pb.TACMessage.transaction_confirmation', index=8, + number=9, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='state_update', full_name='fetch.oef.pb.TACMessage.state_update', index=9, + number=10, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='error', full_name='fetch.oef.pb.TACMessage.error', index=10, + number=11, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='content', full_name='fetch.oef.pb.TACMessage.content', + index=0, containing_type=None, fields=[]), + ], + serialized_start=1273, + serialized_end=1985, +) + +_TACCONTROLLER_REGISTERED.containing_type = _TACCONTROLLER +_TACCONTROLLER_UNREGISTERED.containing_type = _TACCONTROLLER +_TACCONTROLLER_CANCELLED.containing_type = _TACCONTROLLER +_TACCONTROLLER_GAMEDATA.fields_by_name['agent_pbk_to_name'].message_type = _STRSTRPAIR +_TACCONTROLLER_GAMEDATA.fields_by_name['good_pbk_to_name'].message_type = _STRSTRPAIR +_TACCONTROLLER_GAMEDATA.containing_type = _TACCONTROLLER +_TACCONTROLLER_TRANSACTIONCONFIRMATION.containing_type = _TACCONTROLLER +_TACCONTROLLER_STATEUPDATE.fields_by_name['initial_state'].message_type = _TACCONTROLLER_GAMEDATA +_TACCONTROLLER_STATEUPDATE.fields_by_name['txs'].message_type = _TACAGENT_TRANSACTION +_TACCONTROLLER_STATEUPDATE.containing_type = _TACCONTROLLER +_TACCONTROLLER_ERROR.fields_by_name['error_code'].enum_type = _TACCONTROLLER_ERROR_ERRORCODE +_TACCONTROLLER_ERROR.fields_by_name['details'].message_type = google_dot_protobuf_dot_struct__pb2._STRUCT +_TACCONTROLLER_ERROR.containing_type = _TACCONTROLLER +_TACCONTROLLER_ERROR_ERRORCODE.containing_type = _TACCONTROLLER_ERROR +_TACAGENT_REGISTER.containing_type = _TACAGENT +_TACAGENT_UNREGISTER.containing_type = _TACAGENT +_TACAGENT_TRANSACTION.fields_by_name['quantities'].message_type = _STRINTPAIR +_TACAGENT_TRANSACTION.containing_type = _TACAGENT +_TACAGENT_GETSTATEUPDATE.containing_type = _TACAGENT +_TACMESSAGE.fields_by_name['register'].message_type = _TACAGENT_REGISTER +_TACMESSAGE.fields_by_name['unregister'].message_type = _TACAGENT_UNREGISTER +_TACMESSAGE.fields_by_name['transaction'].message_type = _TACAGENT_TRANSACTION +_TACMESSAGE.fields_by_name['get_state_update'].message_type = _TACAGENT_GETSTATEUPDATE +_TACMESSAGE.fields_by_name['registered'].message_type = _TACCONTROLLER_REGISTERED +_TACMESSAGE.fields_by_name['unregistered'].message_type = _TACCONTROLLER_UNREGISTERED +_TACMESSAGE.fields_by_name['cancelled'].message_type = _TACCONTROLLER_CANCELLED +_TACMESSAGE.fields_by_name['game_data'].message_type = _TACCONTROLLER_GAMEDATA +_TACMESSAGE.fields_by_name['transaction_confirmation'].message_type = _TACCONTROLLER_TRANSACTIONCONFIRMATION +_TACMESSAGE.fields_by_name['state_update'].message_type = _TACCONTROLLER_STATEUPDATE +_TACMESSAGE.fields_by_name['error'].message_type = _TACCONTROLLER_ERROR +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['register']) +_TACMESSAGE.fields_by_name['register'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['unregister']) +_TACMESSAGE.fields_by_name['unregister'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['transaction']) +_TACMESSAGE.fields_by_name['transaction'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['get_state_update']) +_TACMESSAGE.fields_by_name['get_state_update'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['registered']) +_TACMESSAGE.fields_by_name['registered'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['unregistered']) +_TACMESSAGE.fields_by_name['unregistered'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['cancelled']) +_TACMESSAGE.fields_by_name['cancelled'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['game_data']) +_TACMESSAGE.fields_by_name['game_data'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['transaction_confirmation']) +_TACMESSAGE.fields_by_name['transaction_confirmation'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['state_update']) +_TACMESSAGE.fields_by_name['state_update'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['error']) +_TACMESSAGE.fields_by_name['error'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +DESCRIPTOR.message_types_by_name['StrIntPair'] = _STRINTPAIR +DESCRIPTOR.message_types_by_name['StrStrPair'] = _STRSTRPAIR +DESCRIPTOR.message_types_by_name['TACController'] = _TACCONTROLLER +DESCRIPTOR.message_types_by_name['TACAgent'] = _TACAGENT +DESCRIPTOR.message_types_by_name['TACMessage'] = _TACMESSAGE + +StrIntPair = _reflection.GeneratedProtocolMessageType('StrIntPair', (_message.Message,), dict( + DESCRIPTOR = _STRINTPAIR, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.StrIntPair) + )) +_sym_db.RegisterMessage(StrIntPair) + +StrStrPair = _reflection.GeneratedProtocolMessageType('StrStrPair', (_message.Message,), dict( + DESCRIPTOR = _STRSTRPAIR, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.StrStrPair) + )) +_sym_db.RegisterMessage(StrStrPair) + +TACController = _reflection.GeneratedProtocolMessageType('TACController', (_message.Message,), dict( + + Registered = _reflection.GeneratedProtocolMessageType('Registered', (_message.Message,), dict( + DESCRIPTOR = _TACCONTROLLER_REGISTERED, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Registered) + )) + , + + Unregistered = _reflection.GeneratedProtocolMessageType('Unregistered', (_message.Message,), dict( + DESCRIPTOR = _TACCONTROLLER_UNREGISTERED, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Unregistered) + )) + , + + Cancelled = _reflection.GeneratedProtocolMessageType('Cancelled', (_message.Message,), dict( + DESCRIPTOR = _TACCONTROLLER_CANCELLED, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Cancelled) + )) + , + + GameData = _reflection.GeneratedProtocolMessageType('GameData', (_message.Message,), dict( + DESCRIPTOR = _TACCONTROLLER_GAMEDATA, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.GameData) + )) + , + + TransactionConfirmation = _reflection.GeneratedProtocolMessageType('TransactionConfirmation', (_message.Message,), dict( + DESCRIPTOR = _TACCONTROLLER_TRANSACTIONCONFIRMATION, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.TransactionConfirmation) + )) + , + + StateUpdate = _reflection.GeneratedProtocolMessageType('StateUpdate', (_message.Message,), dict( + DESCRIPTOR = _TACCONTROLLER_STATEUPDATE, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.StateUpdate) + )) + , + + Error = _reflection.GeneratedProtocolMessageType('Error', (_message.Message,), dict( + DESCRIPTOR = _TACCONTROLLER_ERROR, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Error) + )) + , + DESCRIPTOR = _TACCONTROLLER, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController) + )) +_sym_db.RegisterMessage(TACController) +_sym_db.RegisterMessage(TACController.Registered) +_sym_db.RegisterMessage(TACController.Unregistered) +_sym_db.RegisterMessage(TACController.Cancelled) +_sym_db.RegisterMessage(TACController.GameData) +_sym_db.RegisterMessage(TACController.TransactionConfirmation) +_sym_db.RegisterMessage(TACController.StateUpdate) +_sym_db.RegisterMessage(TACController.Error) + +TACAgent = _reflection.GeneratedProtocolMessageType('TACAgent', (_message.Message,), dict( + + Register = _reflection.GeneratedProtocolMessageType('Register', (_message.Message,), dict( + DESCRIPTOR = _TACAGENT_REGISTER, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.Register) + )) + , + + Unregister = _reflection.GeneratedProtocolMessageType('Unregister', (_message.Message,), dict( + DESCRIPTOR = _TACAGENT_UNREGISTER, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.Unregister) + )) + , + + Transaction = _reflection.GeneratedProtocolMessageType('Transaction', (_message.Message,), dict( + DESCRIPTOR = _TACAGENT_TRANSACTION, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.Transaction) + )) + , + + GetStateUpdate = _reflection.GeneratedProtocolMessageType('GetStateUpdate', (_message.Message,), dict( + DESCRIPTOR = _TACAGENT_GETSTATEUPDATE, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.GetStateUpdate) + )) + , + DESCRIPTOR = _TACAGENT, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent) + )) +_sym_db.RegisterMessage(TACAgent) +_sym_db.RegisterMessage(TACAgent.Register) +_sym_db.RegisterMessage(TACAgent.Unregister) +_sym_db.RegisterMessage(TACAgent.Transaction) +_sym_db.RegisterMessage(TACAgent.GetStateUpdate) + +TACMessage = _reflection.GeneratedProtocolMessageType('TACMessage', (_message.Message,), dict( + DESCRIPTOR = _TACMESSAGE, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACMessage) + )) +_sym_db.RegisterMessage(TACMessage) + + +# @@protoc_insertion_point(module_scope) diff --git a/asdsd/skills/__init__.py b/asdsd/skills/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/asdsd/skills/error/__init__.py b/asdsd/skills/error/__init__.py new file mode 100644 index 0000000000..96c80ac32c --- /dev/null +++ b/asdsd/skills/error/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the implementation of the error skill.""" diff --git a/asdsd/skills/error/behaviours.py b/asdsd/skills/error/behaviours.py new file mode 100644 index 0000000000..556ee98ca7 --- /dev/null +++ b/asdsd/skills/error/behaviours.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains the error behaviours.""" + +from aea.skills.base import Behaviour + + +class ErrorBehaviour(Behaviour): + """This class implements the error behaviour.""" + + def setup(self) -> None: + """ + Implement the setup. + + :return: None + """ + pass + + def act(self) -> None: + """ + Implement the act. + + :return: None + """ + pass + + def teardown(self) -> None: + """ + Implement the task teardown. + + :return: None + """ + pass diff --git a/asdsd/skills/error/handlers.py b/asdsd/skills/error/handlers.py new file mode 100644 index 0000000000..098a61eced --- /dev/null +++ b/asdsd/skills/error/handlers.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains the implementation of the handler for the 'default' protocol.""" +import base64 +import logging +from typing import Optional + +from aea.configurations.base import ProtocolId +from aea.mail.base import Envelope +from aea.protocols.base import Message, Protocol +from aea.protocols.default.message import DefaultMessage +from aea.protocols.default.serialization import DefaultSerializer +from aea.skills.base import Handler + +logger = logging.getLogger(__name__) + + +class ErrorHandler(Handler): + """This class implements the error handler.""" + + SUPPORTED_PROTOCOL = 'default' # type: Optional[ProtocolId] + + def setup(self) -> None: + """ + Implement the setup. + + :return: None + """ + pass + + def handle(self, message: Message, sender: str) -> None: + """ + Implement the reaction to an envelope. + + :param message: the message + :param sender: the sender + """ + pass + + def teardown(self) -> None: + """ + Implement the handler teardown. + + :return: None + """ + pass + + def send_unsupported_protocol(self, envelope: Envelope) -> None: + """ + Handle the received envelope in case the protocol is not supported. + + :param envelope: the envelope + :return: None + """ + logger.warning("Unsupported protocol: {}".format(envelope.protocol_id)) + reply = DefaultMessage(type=DefaultMessage.Type.ERROR, + error_code=DefaultMessage.ErrorCode.UNSUPPORTED_PROTOCOL.value, + error_msg="Unsupported protocol.", + error_data={"protocol_id": envelope.protocol_id}) + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, + message=DefaultSerializer().encode(reply)) + + def send_decoding_error(self, envelope: Envelope) -> None: + """ + Handle a decoding error. + + :param envelope: the envelope + :return: None + """ + logger.warning("Decoding error: {}.".format(envelope)) + encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") + reply = DefaultMessage(type=DefaultMessage.Type.ERROR, + error_code=DefaultMessage.ErrorCode.DECODING_ERROR.value, + error_msg="Decoding error.", + error_data={"envelope": encoded_envelope}) + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, + message=DefaultSerializer().encode(reply)) + + def send_invalid_message(self, envelope: Envelope) -> None: + """ + Handle an message that is invalid wrt a protocol. + + :param envelope: the envelope + :return: None + """ + logger.warning("Invalid message wrt protocol: {}.".format(envelope.protocol_id)) + encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") + reply = DefaultMessage(type=DefaultMessage.Type.ERROR, + error_code=DefaultMessage.ErrorCode.INVALID_MESSAGE.value, + error_msg="Invalid message.", + error_data={"envelope": encoded_envelope}) + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, + message=DefaultSerializer().encode(reply)) + + def send_unsupported_skill(self, envelope: Envelope, protocol: Protocol) -> None: + """ + Handle the received envelope in case the skill is not supported. + + :param envelope: the envelope + :param protocol: the protocol + :return: None + """ + logger.warning("Cannot handle envelope: no handler registered for the protocol '{}'.".format(protocol.id)) + encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") + reply = DefaultMessage(type=DefaultMessage.Type.ERROR, + error_code=DefaultMessage.ErrorCode.UNSUPPORTED_SKILL.value, + error_msg="Unsupported skill.", + error_data={"envelope": encoded_envelope}) + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, + message=DefaultSerializer().encode(reply)) diff --git a/asdsd/skills/error/skill.yaml b/asdsd/skills/error/skill.yaml new file mode 100644 index 0000000000..33806a3345 --- /dev/null +++ b/asdsd/skills/error/skill.yaml @@ -0,0 +1,15 @@ +name: error +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +description: "The error skill implements basic error handling required by all AEAs." +behaviours: [] +handlers: + - handler: + class_name: ErrorHandler + args: + foo: bar +tasks: [] +shared_classes: [] +protocols: ['default'] diff --git a/asdsd/skills/error/tasks.py b/asdsd/skills/error/tasks.py new file mode 100644 index 0000000000..8922217537 --- /dev/null +++ b/asdsd/skills/error/tasks.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains the implementation of the error tasks.""" + +from aea.skills.base import Task + + +class ErrorTask(Task): + """This class implements the error task.""" + + def setup(self) -> None: + """ + Implement the setup. + + :return: None + """ + pass + + def execute(self) -> None: + """ + Implement the task execution. + + :param envelope: the envelope + :return: None + """ + pass + + def teardown(self) -> None: + """ + Implement the task teardown. + + :return: None + """ + pass diff --git a/my_agent/aea-config.yaml b/my_agent/aea-config.yaml new file mode 100644 index 0000000000..6f5d588acc --- /dev/null +++ b/my_agent/aea-config.yaml @@ -0,0 +1,31 @@ +aea_version: 0.1.7 +agent_name: my_agent +authors: '' +connections: +- oef +default_connection: oef +description: '' +license: '' +logging_config: + disable_existing_loggers: false + version: 1 +private_key_paths: +- private_key_path: + ledger: default + path: default_private_key.pem +- private_key_path: + ledger: fetchai + path: fet_private_key.txt +- private_key_path: + ledger: ethereum + path: eth_private_key.txt +protocols: +- gym +- oef +- tac +registry_path: ../packages +skills: +- error +- my_search +url: '' +version: v1 diff --git a/my_agent/connections/__init__.py b/my_agent/connections/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/my_agent/connections/oef/__init__.py b/my_agent/connections/oef/__init__.py new file mode 100644 index 0000000000..21ee4d83df --- /dev/null +++ b/my_agent/connections/oef/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Implementation of the OEF connection.""" diff --git a/my_agent/connections/oef/connection.py b/my_agent/connections/oef/connection.py new file mode 100644 index 0000000000..7f489b867f --- /dev/null +++ b/my_agent/connections/oef/connection.py @@ -0,0 +1,604 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Extension to the OEF Python SDK.""" +import datetime +import logging +import pickle +from queue import Empty, Queue +from threading import Thread +from typing import List, Dict, Optional, cast + +import oef +from oef.agents import OEFAgent +from oef.core import AsyncioCore +from oef.messages import CFP_TYPES, PROPOSE_TYPES +from oef.query import ( + Query as OEFQuery, + ConstraintExpr as OEFConstraintExpr, + And as OEFAnd, + Or as OEFOr, + Not as OEFNot, + Constraint as OEFConstraint, + ConstraintType as OEFConstraintType, Eq, NotEq, Lt, LtEq, Gt, GtEq, Range, In, NotIn) +from oef.schema import Description as OEFDescription, DataModel as OEFDataModel, AttributeSchema as OEFAttribute + +from aea.configurations.base import ConnectionConfig +from aea.connections.base import Channel, Connection +from aea.mail.base import MailBox, Envelope +from aea.protocols.fipa.message import FIPAMessage +from aea.protocols.fipa.serialization import FIPASerializer +from aea.protocols.oef.message import OEFMessage +from aea.protocols.oef.models import Description, Attribute, DataModel, Query, ConstraintExpr, And, Or, Not, Constraint, \ + ConstraintType, ConstraintTypes +from aea.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF + +logger = logging.getLogger(__name__) + + +STUB_MESSSAGE_ID = 0 +STUB_DIALOGUE_ID = 0 + + +class OEFObjectTranslator: + """Translate our OEF object to object of OEF SDK classes.""" + + @classmethod + def to_oef_description(cls, desc: Description) -> OEFDescription: + """From our description to OEF description.""" + oef_data_model = cls.to_oef_data_model(desc.data_model) if desc.data_model is not None else None + return OEFDescription(desc.values, oef_data_model) + + @classmethod + def to_oef_data_model(cls, data_model: DataModel) -> OEFDataModel: + """From our data model to OEF data model.""" + oef_attributes = [cls.to_oef_attribute(attribute) for attribute in data_model.attributes] + return OEFDataModel(data_model.name, oef_attributes, data_model.description) + + @classmethod + def to_oef_attribute(cls, attribute: Attribute) -> OEFAttribute: + """From our attribute to OEF attribute.""" + return OEFAttribute(attribute.name, attribute.type, attribute.is_required, attribute.description) + + @classmethod + def to_oef_query(cls, query: Query) -> OEFQuery: + """From our query to OEF query.""" + oef_data_model = cls.to_oef_data_model(query.model) if query.model is not None else None + constraints = [cls.to_oef_constraint_expr(c) for c in query.constraints] + return OEFQuery(constraints, oef_data_model) + + @classmethod + def to_oef_constraint_expr(cls, constraint_expr: ConstraintExpr) -> OEFConstraintExpr: + """From our constraint expression to the OEF constraint expression.""" + if isinstance(constraint_expr, And): + return OEFAnd([cls.to_oef_constraint_expr(c) for c in constraint_expr.constraints]) + elif isinstance(constraint_expr, Or): + return OEFOr([cls.to_oef_constraint_expr(c) for c in constraint_expr.constraints]) + elif isinstance(constraint_expr, Not): + return OEFNot(cls.to_oef_constraint_expr(constraint_expr.constraint)) + elif isinstance(constraint_expr, Constraint): + oef_constraint_type = cls.to_oef_constraint_type(constraint_expr.constraint_type) + return OEFConstraint(constraint_expr.attribute_name, oef_constraint_type) + else: + raise ValueError("Constraint expression not supported.") + + @classmethod + def to_oef_constraint_type(cls, constraint_type: ConstraintType) -> OEFConstraintType: + """From our constraint type to OEF constraint type.""" + value = constraint_type.value + if constraint_type.type == ConstraintTypes.EQUAL: + return Eq(value) + elif constraint_type.type == ConstraintTypes.NOT_EQUAL: + return NotEq(value) + elif constraint_type.type == ConstraintTypes.LESS_THAN: + return Lt(value) + elif constraint_type.type == ConstraintTypes.LESS_THAN_EQ: + return LtEq(value) + elif constraint_type.type == ConstraintTypes.GREATER_THAN: + return Gt(value) + elif constraint_type.type == ConstraintTypes.GREATER_THAN_EQ: + return GtEq(value) + elif constraint_type.type == ConstraintTypes.WITHIN: + return Range(value) + elif constraint_type.type == ConstraintTypes.IN: + return In(value) + elif constraint_type.type == ConstraintTypes.NOT_IN: + return NotIn(value) + else: + raise ValueError("Constraint type not recognized.") + + @classmethod + def from_oef_description(cls, oef_desc: OEFDescription) -> Description: + """From an OEF description to our description.""" + data_model = cls.from_oef_data_model(oef_desc.data_model) if oef_desc.data_model is not None else None + return Description(oef_desc.values, data_model=data_model) + + @classmethod + def from_oef_data_model(cls, oef_data_model: OEFDataModel) -> DataModel: + """From an OEF data model to our data model.""" + attributes = [cls.from_oef_attribute(oef_attribute) for oef_attribute in oef_data_model.attribute_schemas] + return DataModel(oef_data_model.name, attributes, oef_data_model.description) + + @classmethod + def from_oef_attribute(cls, oef_attribute: OEFAttribute) -> Attribute: + """From an OEF attribute to our attribute.""" + return Attribute(oef_attribute.name, oef_attribute.type, oef_attribute.required, oef_attribute.description) + + @classmethod + def from_oef_query(cls, oef_query: OEFQuery) -> Query: + """From our query to OrOEF query.""" + data_model = cls.from_oef_data_model(oef_query.model) if oef_query.model is not None else None + constraints = [cls.from_oef_constraint_expr(c) for c in oef_query.constraints] + return Query(constraints, data_model) + + @classmethod + def from_oef_constraint_expr(cls, oef_constraint_expr: OEFConstraintExpr) -> ConstraintExpr: + """From our query to OEF query.""" + if isinstance(oef_constraint_expr, OEFAnd): + return And([cls.from_oef_constraint_expr(c) for c in oef_constraint_expr.constraints]) + elif isinstance(oef_constraint_expr, OEFOr): + return Or([cls.from_oef_constraint_expr(c) for c in oef_constraint_expr.constraints]) + elif isinstance(oef_constraint_expr, OEFNot): + return Not(cls.from_oef_constraint_expr(oef_constraint_expr.constraint)) + elif isinstance(oef_constraint_expr, OEFConstraint): + constraint_type = cls.from_oef_constraint_type(oef_constraint_expr.constraint) + return Constraint(oef_constraint_expr.attribute_name, constraint_type) + else: + raise ValueError("OEF Constraint not supported.") + + @classmethod + def from_oef_constraint_type(cls, constraint_type: OEFConstraintType) -> ConstraintType: + """From OEF constraint type to our constraint type.""" + if isinstance(constraint_type, Eq): + return ConstraintType(ConstraintTypes.EQUAL, constraint_type.value) + elif isinstance(constraint_type, NotEq): + return ConstraintType(ConstraintTypes.NOT_EQUAL, constraint_type.value) + elif isinstance(constraint_type, Lt): + return ConstraintType(ConstraintTypes.LESS_THAN, constraint_type.value) + elif isinstance(constraint_type, LtEq): + return ConstraintType(ConstraintTypes.LESS_THAN_EQ, constraint_type.value) + elif isinstance(constraint_type, Gt): + return ConstraintType(ConstraintTypes.GREATER_THAN, constraint_type.value) + elif isinstance(constraint_type, GtEq): + return ConstraintType(ConstraintTypes.GREATER_THAN_EQ, constraint_type.value) + elif isinstance(constraint_type, Range): + return ConstraintType(ConstraintTypes.WITHIN, constraint_type.values) + elif isinstance(constraint_type, In): + return ConstraintType(ConstraintTypes.IN, constraint_type.values) + elif isinstance(constraint_type, NotIn): + return ConstraintType(ConstraintTypes.NOT_IN, constraint_type.values) + else: + raise ValueError("Constraint type not recognized.") + + +class MailStats(object): + """The MailStats class tracks statistics on messages processed by MailBox.""" + + def __init__(self) -> None: + """ + Instantiate mail stats. + + :return: None + """ + self._search_count = 0 + self._search_start_time = {} # type: Dict[int, datetime.datetime] + self._search_timedelta = {} # type: Dict[int, float] + self._search_result_counts = {} # type: Dict[int, int] + + @property + def search_count(self) -> int: + """Get the search count.""" + return self._search_count + + def search_start(self, search_id: int) -> None: + """ + Add a search id and start time. + + :param search_id: the search id + + :return: None + """ + assert search_id not in self._search_start_time + self._search_count += 1 + self._search_start_time[search_id] = datetime.datetime.now() + + def search_end(self, search_id: int, nb_search_results: int) -> None: + """ + Add end time for a search id. + + :param search_id: the search id + :param nb_search_results: the number of agents returned in the search result + + :return: None + """ + assert search_id in self._search_start_time + assert search_id not in self._search_timedelta + self._search_timedelta[search_id] = (datetime.datetime.now() - self._search_start_time[search_id]).total_seconds() * 1000 + self._search_result_counts[search_id] = nb_search_results + + +class OEFChannel(OEFAgent, Channel): + """The OEFChannel connects the OEF Agent with the connection.""" + + def __init__(self, public_key: str, oef_addr: str, oef_port: int, core: AsyncioCore, in_queue: Queue): + """ + Initialize. + + :param public_key: the public key of the agent. + :param oef_addr: the OEF IP address. + :param oef_port: the OEF port. + :param in_queue: the in queue. + """ + super().__init__(public_key, oef_addr=oef_addr, oef_port=oef_port, core=core) + self.in_queue = in_queue + self.mail_stats = MailStats() + + def on_message(self, msg_id: int, dialogue_id: int, origin: str, content: bytes) -> None: + """ + On message event handler. + + :param msg_id: the message id. + :param dialogue_id: the dialogue id. + :param origin: the public key of the sender. + :param content: the bytes content. + :return: None + """ + # We are not using the 'origin' parameter because 'content' contains a serialized instance of 'Envelope', + # hence it already contains the address of the sender. + envelope = Envelope.decode(content) + self.in_queue.put(envelope) + + def on_cfp(self, msg_id: int, dialogue_id: int, origin: str, target: int, query: CFP_TYPES) -> None: + """ + On cfp event handler. + + :param msg_id: the message id. + :param dialogue_id: the dialogue id. + :param origin: the public key of the sender. + :param target: the message target. + :param query: the query. + :return: None + """ + try: + query = pickle.loads(query) + except Exception: + pass + msg = FIPAMessage(message_id=msg_id, + dialogue_id=dialogue_id, + target=target, + performative=FIPAMessage.Performative.CFP, + query=query if query != b"" else None) + msg_bytes = FIPASerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_propose(self, msg_id: int, dialogue_id: int, origin: str, target: int, b_proposals: PROPOSE_TYPES) -> None: + """ + On propose event handler. + + :param msg_id: the message id. + :param dialogue_id: the dialogue id. + :param origin: the public key of the sender. + :param target: the message target. + :param b_proposals: the proposals. + :return: None + """ + if type(b_proposals) == bytes: + proposals = pickle.loads(b_proposals) # type: List[Description] + else: + raise ValueError("No support for non-bytes proposals.") + + msg = FIPAMessage(message_id=msg_id, + dialogue_id=dialogue_id, + target=target, + performative=FIPAMessage.Performative.PROPOSE, + proposal=proposals) + msg_bytes = FIPASerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_accept(self, msg_id: int, dialogue_id: int, origin: str, target: int) -> None: + """ + On accept event handler. + + :param msg_id: the message id. + :param dialogue_id: the dialogue id. + :param origin: the public key of the sender. + :param target: the message target. + :return: None + """ + performative = FIPAMessage.Performative.MATCH_ACCEPT if msg_id == 4 and target == 3 else FIPAMessage.Performative.ACCEPT + msg = FIPAMessage(message_id=msg_id, + dialogue_id=dialogue_id, + target=target, + performative=performative) + msg_bytes = FIPASerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_decline(self, msg_id: int, dialogue_id: int, origin: str, target: int) -> None: + """ + On decline event handler. + + :param msg_id: the message id. + :param dialogue_id: the dialogue id. + :param origin: the public key of the sender. + :param target: the message target. + :return: None + """ + msg = FIPAMessage(message_id=msg_id, + dialogue_id=dialogue_id, + target=target, + performative=FIPAMessage.Performative.DECLINE) + msg_bytes = FIPASerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_search_result(self, search_id: int, agents: List[str]) -> None: + """ + On accept event handler. + + :param search_id: the search id. + :param agents: the list of agents. + :return: None + """ + self.mail_stats.search_end(search_id, len(agents)) + msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_RESULT, id=search_id, agents=agents) + msg_bytes = OEFSerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_oef_error(self, answer_id: int, operation: oef.messages.OEFErrorOperation) -> None: + """ + On oef error event handler. + + :param answer_id: the answer id. + :param operation: the error operation. + :return: None + """ + try: + operation = OEFMessage.OEFErrorOperation(operation) + except ValueError: + operation = OEFMessage.OEFErrorOperation.OTHER + + msg = OEFMessage(oef_type=OEFMessage.Type.OEF_ERROR, id=answer_id, operation=operation) + msg_bytes = OEFSerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def on_dialogue_error(self, answer_id: int, dialogue_id: int, origin: str) -> None: + """ + On dialogue error event handler. + + :param answer_id: the answer id. + :param dialogue_id: the dialogue id. + :param origin: the message sender. + :return: None + """ + msg = OEFMessage(oef_type=OEFMessage.Type.DIALOGUE_ERROR, + id=answer_id, + dialogue_id=dialogue_id, + origin=origin) + msg_bytes = OEFSerializer().encode(msg) + envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + self.in_queue.put(envelope) + + def send(self, envelope: Envelope) -> None: + """ + Send message handler. + + :param envelope: the message. + :return: None + """ + if envelope.protocol_id == "default": + self.send_default_message(envelope) + elif envelope.protocol_id == "fipa": + self.send_fipa_message(envelope) + elif envelope.protocol_id == "oef": + self.send_oef_message(envelope) + elif envelope.protocol_id == "tac": + self.send_default_message(envelope) + else: + logger.error("This envelope cannot be sent: protocol_id={}".format(envelope.protocol_id)) + raise ValueError("Cannot send message.") + + def send_default_message(self, envelope: Envelope): + """Send a 'default' message.""" + self.send_message(STUB_MESSSAGE_ID, STUB_DIALOGUE_ID, envelope.to, envelope.encode()) + + def send_fipa_message(self, envelope: Envelope) -> None: + """ + Send fipa message handler. + + :param envelope: the message. + :return: None + """ + fipa_message = FIPASerializer().decode(envelope.message) + id = fipa_message.get("message_id") + dialogue_id = fipa_message.get("dialogue_id") + destination = envelope.to + target = fipa_message.get("target") + performative = FIPAMessage.Performative(fipa_message.get("performative")) + if performative == FIPAMessage.Performative.CFP: + query = fipa_message.get("query") + query = b"" if query is None else query + if type(query) == Query: + query = pickle.dumps(query) + self.send_cfp(id, dialogue_id, destination, target, query) + elif performative == FIPAMessage.Performative.PROPOSE: + proposal = cast(List[Description], fipa_message.get("proposal")) + proposal_b = pickle.dumps(proposal) # type: bytes + self.send_propose(id, dialogue_id, destination, target, proposal_b) + elif performative == FIPAMessage.Performative.ACCEPT: + self.send_accept(id, dialogue_id, destination, target) + elif performative == FIPAMessage.Performative.MATCH_ACCEPT: + self.send_accept(id, dialogue_id, destination, target) + elif performative == FIPAMessage.Performative.DECLINE: + self.send_decline(id, dialogue_id, destination, target) + else: + raise ValueError("OEF FIPA message not recognized.") + + def send_oef_message(self, envelope: Envelope) -> None: + """ + Send oef message handler. + + :param envelope: the message. + :return: None + """ + oef_message = OEFSerializer().decode(envelope.message) + oef_type = OEFMessage.Type(oef_message.get("type")) + oef_msg_id = cast(int, oef_message.get("id")) + if oef_type == OEFMessage.Type.REGISTER_SERVICE: + service_description = cast(Description, oef_message.get("service_description")) + service_id = cast(int, oef_message.get("service_id")) + oef_service_description = OEFObjectTranslator.to_oef_description(service_description) + self.register_service(oef_msg_id, oef_service_description, service_id) + elif oef_type == OEFMessage.Type.UNREGISTER_SERVICE: + service_description = cast(Description, oef_message.get("service_description")) + service_id = cast(int, oef_message.get("service_id")) + oef_service_description = OEFObjectTranslator.to_oef_description(service_description) + self.unregister_service(oef_msg_id, oef_service_description, service_id) + elif oef_type == OEFMessage.Type.SEARCH_AGENTS: + query = cast(Query, oef_message.get("query")) + oef_query = OEFObjectTranslator.to_oef_query(query) + self.search_agents(oef_msg_id, oef_query) + elif oef_type == OEFMessage.Type.SEARCH_SERVICES: + query = cast(Query, oef_message.get("query")) + oef_query = OEFObjectTranslator.to_oef_query(query) + self.mail_stats.search_start(oef_msg_id) + self.search_services(oef_msg_id, oef_query) + else: + raise ValueError("OEF request not recognized.") + + +class OEFConnection(Connection): + """The OEFConnection connects the to the mailbox.""" + + def __init__(self, public_key: str, oef_addr: str, oef_port: int = 10000): + """ + Initialize. + + :param public_key: the public key of the agent. + :param oef_addr: the OEF IP address. + :param oef_port: the OEF port. + """ + super().__init__() + core = AsyncioCore(logger=logger) + self._core = core # type: AsyncioCore + self.channel = OEFChannel(public_key, oef_addr, oef_port, core=core, in_queue=self.in_queue) + + self._stopped = True + self._connected = False + self.out_thread = None # type: Optional[Thread] + + @property + def is_established(self) -> bool: + """Get the connection status.""" + return self._connected + + def _fetch(self) -> None: + """ + Fetch the messages from the outqueue and send them. + + :return: None + """ + while self._connected: + try: + msg = self.out_queue.get(block=True, timeout=1.0) + self.send(msg) + except Empty: + pass + + def connect(self) -> None: + """ + Connect to the channel. + + :return: None + :raises ConnectionError if the connection to the OEF fails. + """ + if self._stopped and not self._connected: + self._stopped = False + self._core.run_threaded() + try: + if not self.channel.connect(): + raise ConnectionError("Cannot connect to OEFChannel.") + self._connected = True + self.out_thread = Thread(target=self._fetch) + self.out_thread.start() + except ConnectionError as e: + self._core.stop() + raise e + + def disconnect(self) -> None: + """ + Disconnect from the channel. + + :return: None + """ + assert self.out_thread is not None, "Call connect before disconnect." + if not self._stopped and self._connected: + self._connected = False + self.out_thread.join() + self.out_thread = None + self.channel.disconnect() + self._core.stop() + self._stopped = True + + def send(self, envelope: Envelope): + """ + Send messages. + + :return: None + """ + if self._connected: + self.channel.send(envelope) + + @classmethod + def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection': + """ + Get the OEF connection from the connection configuration. + + :param public_key: the public key of the agent. + :param connection_configuration: the connection configuration object. + :return: the connection object + """ + oef_addr = cast(str, connection_configuration.config.get("addr")) + oef_port = cast(int, connection_configuration.config.get("port")) + return OEFConnection(public_key, oef_addr, oef_port) + + +class OEFMailBox(MailBox): + """The OEF mail box.""" + + def __init__(self, public_key: str, oef_addr: str, oef_port: int = 10000): + """ + Initialize. + + :param public_key: the public key of the agent. + :param oef_addr: the OEF IP address. + :param oef_port: the OEF port. + """ + connection = OEFConnection(public_key, oef_addr, oef_port) + super().__init__(connection) + + @property + def mail_stats(self) -> MailStats: + """Get the mail stats object.""" + return self._connection.channel.mail_stats # type: ignore diff --git a/my_agent/connections/oef/connection.yaml b/my_agent/connections/oef/connection.yaml new file mode 100644 index 0000000000..5a8cca4134 --- /dev/null +++ b/my_agent/connections/oef/connection.yaml @@ -0,0 +1,14 @@ +name: oef +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +class_name: OEFConnection +supported_protocols: ["oef"] +config: + addr: ${OEF_ADDR:127.0.0.1} + port: ${OEF_PORT:10000} +dependencies: + - colorlog + - oef +description: "oef connection description [Fill in]" \ No newline at end of file diff --git a/my_agent/default_private_key.pem b/my_agent/default_private_key.pem new file mode 100644 index 0000000000..e1375b9eac --- /dev/null +++ b/my_agent/default_private_key.pem @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDAlt8nlN6M8hWdV1HbV5i1kvvMLRlFUmfxJCstfWnjOYoF9kGIWUc/g +KH2l58/Hi1+gBwYFK4EEACKhZANiAATAbHHGWNnKQdoaqkuLtLO/7wZgTjwpY3H3 +kw2HCZBfXsN8DjTU7YXOnij1XBDAW+mpeDSqBbS4TspTeGISbyl9BOHUV2dlr6Ay +ZF1bQEhzcoS7KBBBtmo22rcb72H8CFE= +-----END EC PRIVATE KEY----- diff --git a/my_agent/eth_private_key.txt b/my_agent/eth_private_key.txt new file mode 100644 index 0000000000..2e2dbde073 --- /dev/null +++ b/my_agent/eth_private_key.txt @@ -0,0 +1 @@ +0xf510ead7999e37b4902e98a594384682fd1389fe48942f22dab4390ecd78cff1 \ No newline at end of file diff --git a/my_agent/fet_private_key.txt b/my_agent/fet_private_key.txt new file mode 100644 index 0000000000..db66b233f2 --- /dev/null +++ b/my_agent/fet_private_key.txt @@ -0,0 +1 @@ +5bc3f3175f8cf708ba7d809cb039b6cb31de2a8cb7791851f2bc9732dd509503 \ No newline at end of file diff --git a/my_agent/protocols/__init__.py b/my_agent/protocols/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/my_agent/protocols/gym/__init__.py b/my_agent/protocols/gym/__init__.py new file mode 100644 index 0000000000..a1766ab9cd --- /dev/null +++ b/my_agent/protocols/gym/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the support resources for the Gym protocol.""" diff --git a/my_agent/protocols/gym/message.py b/my_agent/protocols/gym/message.py new file mode 100644 index 0000000000..616b3e8226 --- /dev/null +++ b/my_agent/protocols/gym/message.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the FIPA message definition.""" +from enum import Enum +from typing import Optional, Union + +from aea.protocols.base import Message + + +class GymMessage(Message): + """The Gym message class.""" + + protocol_id = "gym" + + class Performative(Enum): + """Gym performatives.""" + + ACT = 'act' + PERCEPT = 'percept' + RESET = 'reset' + CLOSE = 'close' + + def __str__(self): + """Get string representation.""" + return self.value + + def __init__(self, performative: Optional[Union[str, Performative]] = None, **kwargs): + """ + Initialize. + + :param type: the type. + """ + super().__init__(performative=GymMessage.Performative(performative), **kwargs) + assert self.check_consistency(), "GymMessage initialization inconsistent." + + def check_consistency(self) -> bool: + """Check that the data is consistent.""" + try: + assert self.is_set("performative") + performative = GymMessage.Performative(self.get("performative")) + if performative == GymMessage.Performative.ACT: + assert self.is_set("action") + assert self.is_set("step_id") + elif performative == GymMessage.Performative.PERCEPT: + assert self.is_set("observation") + assert self.is_set("reward") + assert self.is_set("done") + assert self.is_set("info") + assert self.is_set("step_id") + elif performative == GymMessage.Performative.RESET or performative == GymMessage.Performative.CLOSE: + pass + else: + raise ValueError("Performative not recognized.") + + except (AssertionError, ValueError, KeyError): + return False + + return True diff --git a/my_agent/protocols/gym/protocol.yaml b/my_agent/protocols/gym/protocol.yaml new file mode 100644 index 0000000000..7b10d5429b --- /dev/null +++ b/my_agent/protocols/gym/protocol.yaml @@ -0,0 +1,6 @@ +name: gym +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +description: "The gym protocol implements the messages an agent needs to engage with a gym connection." \ No newline at end of file diff --git a/my_agent/protocols/gym/serialization.py b/my_agent/protocols/gym/serialization.py new file mode 100644 index 0000000000..6fb14c1354 --- /dev/null +++ b/my_agent/protocols/gym/serialization.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Serialization for the FIPA protocol.""" +import base64 +import copy +import json +import pickle +from typing import Any, TYPE_CHECKING + +from aea.protocols.base import Message +from aea.protocols.base import Serializer + +if TYPE_CHECKING: + from packages.protocols.gym.message import GymMessage +else: + from gym_protocol.message import GymMessage + + +class GymSerializer(Serializer): + """Serialization for the Gym protocol.""" + + def encode(self, msg: Message) -> bytes: + """ + Decode the message. + + :param msg: the message object + :return: the bytes + """ + performative = GymMessage.Performative(msg.get("performative")) + new_body = copy.copy(msg.body) + new_body["performative"] = performative.value + + if performative == GymMessage.Performative.ACT: + action = msg.body["action"] # type: Any + action_bytes = base64.b64encode(pickle.dumps(action)).decode("utf-8") + new_body["action"] = action_bytes + new_body["step_id"] = msg.body["step_id"] + elif performative == GymMessage.Performative.PERCEPT: + # observation, reward and info are gym implementation specific, done is boolean + observation = msg.body["observation"] # type: Any + observation_bytes = base64.b64encode(pickle.dumps(observation)).decode("utf-8") + new_body["observation"] = observation_bytes + reward = msg.body["reward"] # type: Any + reward_bytes = base64.b64encode(pickle.dumps(reward)).decode("utf-8") + new_body["reward"] = reward_bytes + info = msg.body["info"] # type: Any + info_bytes = base64.b64encode(pickle.dumps(info)).decode("utf-8") + new_body["info"] = info_bytes + new_body["step_id"] = msg.body["step_id"] + + gym_message_bytes = json.dumps(new_body).encode("utf-8") + return gym_message_bytes + + def decode(self, obj: bytes) -> Message: + """ + Decode the message. + + :param obj: the bytes object + :return: the message + """ + json_msg = json.loads(obj.decode("utf-8")) + performative = GymMessage.Performative(json_msg["performative"]) + new_body = copy.copy(json_msg) + new_body["type"] = performative + + if performative == GymMessage.Performative.ACT: + action_bytes = base64.b64decode(json_msg["action"]) + action = pickle.loads(action_bytes) + new_body["action"] = action + new_body["step_id"] = json_msg["step_id"] + elif performative == GymMessage.Performative.PERCEPT: + # observation, reward and info are gym implementation specific, done is boolean + observation_bytes = base64.b64decode(json_msg["observation"]) + observation = pickle.loads(observation_bytes) + new_body["observation"] = observation + reward_bytes = base64.b64decode(json_msg["reward"]) + reward = pickle.loads(reward_bytes) + new_body["reward"] = reward + info_bytes = base64.b64decode(json_msg["info"]) + info = pickle.loads(info_bytes) + new_body["info"] = info + new_body["step_id"] = json_msg["step_id"] + + gym_message = GymMessage(performative=performative, body=new_body) + return gym_message diff --git a/my_agent/protocols/oef/__init__.py b/my_agent/protocols/oef/__init__.py new file mode 100644 index 0000000000..8b7bf970cf --- /dev/null +++ b/my_agent/protocols/oef/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the support resources for the OEF protocol.""" diff --git a/my_agent/protocols/oef/message.py b/my_agent/protocols/oef/message.py new file mode 100644 index 0000000000..ab44e971d4 --- /dev/null +++ b/my_agent/protocols/oef/message.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the default message definition.""" +from enum import Enum +from typing import Optional, List, cast + +from aea.protocols.base import Message +from aea.protocols.oef.models import Description, Query + + +class OEFMessage(Message): + """The OEF message class.""" + + protocol_id = "oef" + + class Type(Enum): + """OEF Message types.""" + + REGISTER_SERVICE = "register_service" + UNREGISTER_SERVICE = "unregister_service" + SEARCH_SERVICES = "search_services" + SEARCH_AGENTS = "search_agents" + OEF_ERROR = "oef_error" + DIALOGUE_ERROR = "dialogue_error" + SEARCH_RESULT = "search_result" + + def __str__(self): + """Get string representation.""" + return self.value + + class OEFErrorOperation(Enum): + """Operation code for the OEF. It is returned in the OEF Error messages.""" + + REGISTER_SERVICE = 0 + UNREGISTER_SERVICE = 1 + SEARCH_SERVICES = 2 + SEARCH_SERVICES_WIDE = 3 + SEARCH_AGENTS = 4 + SEND_MESSAGE = 5 + + OTHER = 10000 + + def __str__(self): + """Get string representation.""" + return str(self.value) + + def __init__(self, oef_type: Optional[Type] = None, + **kwargs): + """ + Initialize. + + :param oef_type: the type of OEF message. + """ + super().__init__(type=oef_type, **kwargs) + assert self.check_consistency(), "OEFMessage initialization inconsistent." + + def check_consistency(self) -> bool: + """Check that the data is consistent.""" + try: + assert self.is_set("type") + oef_type = OEFMessage.Type(self.get("type")) + if oef_type == OEFMessage.Type.REGISTER_SERVICE: + assert self.is_set("id") + assert self.is_set("service_description") + assert self.is_set("service_id") + service_description = self.get("service_description") + service_id = self.get("service_id") + assert isinstance(service_description, Description) + assert isinstance(service_id, str) + elif oef_type == OEFMessage.Type.UNREGISTER_SERVICE: + assert self.is_set("id") + assert self.is_set("service_description") + assert self.is_set("service_id") + service_description = self.get("service_description") + service_id = self.get("service_id") + assert isinstance(service_description, Description) + assert isinstance(service_id, str) + elif oef_type == OEFMessage.Type.SEARCH_SERVICES: + assert self.is_set("id") + assert self.is_set("query") + query = self.get("query") + assert isinstance(query, Query) + elif oef_type == OEFMessage.Type.SEARCH_AGENTS: + assert self.is_set("id") + assert self.is_set("query") + query = self.get("query") + assert isinstance(query, Query) + elif oef_type == OEFMessage.Type.SEARCH_RESULT: + assert self.is_set("id") + assert self.is_set("agents") + agents = cast(List[str], self.get("agents")) + assert type(agents) == list and all(type(a) == str for a in agents) + elif oef_type == OEFMessage.Type.OEF_ERROR: + assert self.is_set("id") + assert self.is_set("operation") + operation = self.get("operation") + assert operation in set(OEFMessage.OEFErrorOperation) + elif oef_type == OEFMessage.Type.DIALOGUE_ERROR: + assert self.is_set("id") + assert self.is_set("dialogue_id") + assert self.is_set("origin") + else: + raise ValueError("Type not recognized.") + except (AssertionError, ValueError): + return False + + return True diff --git a/my_agent/protocols/oef/models.py b/my_agent/protocols/oef/models.py new file mode 100644 index 0000000000..d3811d4e69 --- /dev/null +++ b/my_agent/protocols/oef/models.py @@ -0,0 +1,450 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Useful classes for the OEF protocol.""" +from abc import ABC, abstractmethod +from copy import deepcopy +from enum import Enum +from typing import Dict, Type, Union, Optional, List, Any + + +ATTRIBUTE_TYPES = Union[float, str, bool, int] + + +class JSONSerializable(ABC): + """Interface for JSON-serializable objects.""" + + @abstractmethod + def to_json(self) -> Dict: + """ + Return the JSON representation of the object. + + :return: the JSON object. + """ + + @classmethod + @abstractmethod + def from_json(cls, d: Dict) -> Any: + """ + Parse the JSON representation of the object. + + :param d: the JSON object. + :return: the equivalent Python object. + """ + + +class Attribute: + """Implements an attribute for an OEF data model.""" + + def __init__(self, name: str, + type: Type[ATTRIBUTE_TYPES], + is_required: bool, + description: str = ""): + """ + Initialize an attribute. + + :param name: the name of the attribute. + :param type: the type of the attribute. + :param is_required: whether the attribute is required by the data model. + :param description: an (optional) human-readable description for the attribute. + """ + self.name: str = name + self.type: Type[ATTRIBUTE_TYPES] = type + self.is_required: bool = is_required + self.description: str = description + + def __eq__(self, other): + """Compare with another object.""" + return isinstance(other, Attribute) \ + and self.name == other.name \ + and self.type == other.type \ + and self.is_required == other.is_required + + +class DataModel: + """Implements an OEF data model.""" + + def __init__(self, name: str, attributes: List[Attribute], description: str = ""): + """ + Initialize a data model. + + :param name: the name of the data model. + :param attributes: the attributes of the data model. + """ + self.name: str = name + self.attributes: List[Attribute] = sorted(attributes, key=lambda x: x.name) + self.attributes_by_name = {a.name: a for a in attributes} + self.description = description + + def __eq__(self, other) -> bool: + """Compare with another object.""" + return isinstance(other, DataModel) \ + and self.name == other.name \ + and self.attributes == other.attributes + + +class Description: + """Implements an OEF description.""" + + def __init__(self, values: Dict, data_model: Optional[DataModel] = None): + """ + Initialize the description object. + + :param values: the values in the description. + """ + _values = deepcopy(values) + self.values = _values + self.data_model = data_model + + def __eq__(self, other) -> bool: + """Compare with another object.""" + return isinstance(other, Description) \ + and self.values == other.values \ + and self.data_model == other.data_model + + def __iter__(self): + """Create an iterator.""" + return self + + +class ConstraintTypes(Enum): + """Types of constraint.""" + + EQUAL = "==" + NOT_EQUAL = "!=" + LESS_THAN = "<" + LESS_THAN_EQ = "<=" + GREATER_THAN = ">" + GREATER_THAN_EQ = ">=" + WITHIN = "within" + IN = "in" + NOT_IN = "not_in" + + def __str__(self): + """Get the string representation.""" + return self.value + + +class ConstraintType: + """ + Type of constraint. + + Used with the Constraint class, this class allows to specify constraint over attributes. + + Examples: + Equal to three + >>> equal_3 = ConstraintType(ConstraintTypes.EQUAL, 3) + + You can also specify a type of constraint by using its string representation, e.g.: + >>> equal_3 = ConstraintType("==", 3) + >>> not_equal_london = ConstraintType("!=", "London") + >>> less_than_pi = ConstraintType("<", 3.14) + >>> within_range = ConstraintType("within", (-10.0, 10.0)) + >>> in_a_set = ConstraintType("in", [1, 2, 3]) + >>> not_in_a_set = ConstraintType("not_in", {"C", "Java", "Python"}) + + """ + + def __init__(self, type: Union[ConstraintTypes, str], value: Any, **kwargs): + """ + Initialize a constraint type. + + :param type: the type of the constraint. + | Either an instance of the ConstraintTypes enum, + | or a string representation associated with the type. + :param value: the value that defines the constraint. + :raises ValueError: if the type of the constraint is not + """ + self.type = ConstraintTypes(type) + self.value = value + + def _check_validity(self): + """ + Check the validity of the input provided. + + :return: None + :raises ValueError: if the value is not valid wrt the constraint type. + """ + try: + if self.type == ConstraintTypes.EQUAL: + assert isinstance(self.value, (int, float, str, bool)) + elif self.type == ConstraintTypes.NOT_EQUAL: + assert isinstance(self.value, (int, float, str, bool)) + elif self.type == ConstraintTypes.LESS_THAN: + assert isinstance(self.value, (int, float, str)) + elif self.type == ConstraintTypes.LESS_THAN_EQ: + assert isinstance(self.value, (int, float, str)) + elif self.type == ConstraintTypes.GREATER_THAN: + assert isinstance(self.value, (int, float, str)) + elif self.type == ConstraintTypes.GREATER_THAN_EQ: + assert isinstance(self.value, (int, float, str)) + elif self.type == ConstraintTypes.WITHIN: + assert isinstance(self.value, (list, tuple)) + assert len(self.value) == 2 + assert isinstance(self.value[0], type(self.value[1])) + assert isinstance(self.value[1], type(self.value[0])) + elif self.type == ConstraintTypes.IN: + assert isinstance(self.value, (list, tuple, set)) + if len(self.value) > 0: + _type = type(next(iter(self.value))) + assert all(isinstance(obj, _type) for obj in self.value) + elif self.type == ConstraintTypes.NOT_IN: + assert isinstance(self.value, (list, tuple, set)) + if len(self.value) > 0: + _type = type(next(iter(self.value))) + assert all(isinstance(obj, _type) for obj in self.value) + except AssertionError: + raise ValueError("Value '{}' not compatible with constraint type '{}'" + .format(self.value, str(self.type))) + + def check(self, value: ATTRIBUTE_TYPES) -> bool: + """ + Check if an attribute value satisfies the constraint. + + The implementation depends on the constraint type. + + :param value: the value to check. + :return: True if the value satisfy the constraint, False otherwise. + :raises ValueError: if the constraint type is not recognized. + """ + if self.type == ConstraintTypes.EQUAL: + return self.value == value + elif self.type == ConstraintTypes.NOT_EQUAL: + return self.value != value + elif self.type == ConstraintTypes.LESS_THAN: + return self.value < value + elif self.type == ConstraintTypes.LESS_THAN_EQ: + return self.value <= value + elif self.type == ConstraintTypes.GREATER_THAN: + return self.value > value + elif self.type == ConstraintTypes.GREATER_THAN_EQ: + return self.value >= value + elif self.type == ConstraintTypes.WITHIN: + low = self.value[0] + high = self.value[1] + return low <= value <= high + elif self.type == ConstraintTypes.IN: + return value in self.value + elif self.type == ConstraintTypes.NOT_IN: + return value not in self.value + else: + raise ValueError("Constraint type not recognized.") + + def __eq__(self, other): + """Check equality with another object.""" + return isinstance(other, ConstraintType) and self.value == other.value and self.type == other.type + + +class ConstraintExpr(ABC): + """Implementation of the constraint language to query the OEF node.""" + + @abstractmethod + def check(self, description: Description) -> bool: + """ + Check if a description satisfies the constraint expression. + + :param description: the description to check. + :return: True if the description satisfy the constraint expression, False otherwise. + """ + + +class And(ConstraintExpr): + """Implementation of the 'And' constraint expression.""" + + def __init__(self, constraints: List[ConstraintExpr]): + """ + Initialize an 'And' expression. + + :param constraints: the list of constraints expression (in conjunction). + """ + self.constraints = constraints + + def check(self, description: Description) -> bool: + """ + Check if a value satisfies the 'And' constraint expression. + + :param description: the description to check. + :return: True if the description satisfy the constraint expression, False otherwise. + """ + return all(expr.check(description) for expr in self.constraints) + + def __eq__(self, other): + """Compare with another object.""" + return isinstance(other, And) and self.constraints == other.constraints + + +class Or(ConstraintExpr): + """Implementation of the 'Or' constraint expression.""" + + def __init__(self, constraints: List[ConstraintExpr]): + """ + Initialize an 'Or' expression. + + :param constraints: the list of constraints expressions (in disjunction). + """ + self.constraints = constraints + + def check(self, description: Description) -> bool: + """ + Check if a value satisfies the 'Or' constraint expression. + + :param description: the description to check. + :return: True if the description satisfy the constraint expression, False otherwise. + """ + return any(expr.check(description) for expr in self.constraints) + + def __eq__(self, other): + """Compare with another object.""" + return isinstance(other, Or) and self.constraints == other.constraints + + +class Not(ConstraintExpr): + """Implementation of the 'Not' constraint expression.""" + + def __init__(self, constraint: ConstraintExpr): + """ + Initialize a 'Not' expression. + + :param constraint: the constraint expression to negate. + """ + self.constraint = constraint + + def check(self, description: Description) -> bool: + """ + Check if a value satisfies the 'Not; constraint expression. + + :param description: the description to check. + :return: True if the description satisfy the constraint expression, False otherwise. + """ + return not self.constraint.check(description) + + def __eq__(self, other): + """Compare with another object.""" + return isinstance(other, Not) and self.constraint == other.constraint + + +class Constraint(ConstraintExpr): + """The atomic component of a constraint expression.""" + + def __init__(self, attribute_name: str, constraint_type: ConstraintType): + """ + Initialize a constraint. + + :param attribute_name: the name of the attribute to be constrained. + :param constraint_type: the constraint type. + """ + self.attribute_name = attribute_name + self.constraint_type = constraint_type + + def check(self, description: Description) -> bool: + """ + Check if a description satisfies the constraint. The implementation depends on the type of the constraint. + + :param description: the description to check. + :return: True if the description satisfies the constraint, False otherwise. + + Examples: + >>> attr_author = Attribute("author" , str, True, "The author of the book.") + >>> attr_year = Attribute("year", int, True, "The year of publication of the book.") + >>> attr_genre = Attribute("genre", str, True, "The genre of the book.") + >>> c1 = Constraint("author", ConstraintType("==", "Stephen King")) + >>> c2 = Constraint("year", ConstraintType(">", 1990)) + >>> c3 = Constraint("genre", ConstraintType("in", {"horror", "science_fiction"})) + >>> book_1 = Description({"author": "Stephen King", "year": 1991, "genre": "horror"}) + >>> book_2 = Description({"author": "George Orwell", "year": 1948, "genre": "horror"}) + + The "author" attribute instantiation satisfies the constraint, so the result is True. + + >>> c1.check(book_1) + True + + Here, the "author" does not satisfy the constraints. Hence, the result is False. + + >>> c1.check(book_2) + False + + In this case, there is a missing field specified by the query, that is "year" + So the result is False, even in the case it is not required by the schema: + + >>> c2.check(Description({"author": "Stephen King"})) + False + + If the type of some attribute of the description is not correct, the result is False. + In this case, the field "year" has a string instead of an integer: + + >>> c2.check(Description({"author": "Stephen King", "year": "1991"})) + False + + >>> c3.check(Description({"author": "Stephen King", "genre": False})) + False + + """ + # if the name of the attribute is not present, return false. + name = self.attribute_name + if name not in description.values: + return False + + # if the type of the value is different from the type of the attribute, return false. + value = description.values[name] + if type(self.constraint_type.value) in {list, tuple, set} \ + and not isinstance(value, type(next(iter(self.constraint_type.value)))): + return False + if not isinstance(value, type(self.constraint_type.value)): + return False + + # dispatch the check to the right implementation for the concrete constraint type. + return self.constraint_type.check(value) + + def __eq__(self, other): + """Compare with another object.""" + return isinstance(other, Constraint) \ + and self.attribute_name == other.attribute_name \ + and self.constraint_type == other.constraint_type + + +class Query: + """This class lets you build a query for the OEF.""" + + def __init__(self, constraints: List[ConstraintExpr], model: Optional[DataModel] = None) -> None: + """ + Initialize a query. + + :param constraints: a list of constraint expressions. + :param model: the data model that the query refers to. + """ + self.constraints = constraints + self.model = model + + def check(self, description: Description) -> bool: + """ + Check if a description satisfies the constraints of the query. + + The constraints are interpreted as conjunction. + + :param description: the description to check. + :return: True if the description satisfies all the constraints, False otherwise. + """ + return all(c.check(description) for c in self.constraints) + + def __eq__(self, other): + """Compare with another object.""" + return isinstance(other, Query) \ + and self.constraints == other.constraints \ + and self.model == other.model diff --git a/my_agent/protocols/oef/protocol.yaml b/my_agent/protocols/oef/protocol.yaml new file mode 100644 index 0000000000..918c9a101e --- /dev/null +++ b/my_agent/protocols/oef/protocol.yaml @@ -0,0 +1,9 @@ +name: 'oef' +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +dependencies: + - colorlog + - oef +description: "oef protocol description [Fill in]" \ No newline at end of file diff --git a/my_agent/protocols/oef/serialization.py b/my_agent/protocols/oef/serialization.py new file mode 100644 index 0000000000..c2af02977f --- /dev/null +++ b/my_agent/protocols/oef/serialization.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Serialization for the FIPA protocol.""" +import base64 +import copy +import json +import pickle + +from aea.protocols.base import Message +from aea.protocols.base import Serializer +from aea.protocols.oef.message import OEFMessage +from aea.protocols.oef.models import Description, Query + +"""default 'to' field for OEF envelopes.""" +DEFAULT_OEF = "oef" + + +class OEFSerializer(Serializer): + """Serialization for the OEF protocol.""" + + def encode(self, msg: Message) -> bytes: + """ + Decode the message. + + :param msg: the message object + :return: the bytes + """ + oef_type = OEFMessage.Type(msg.get("type")) + new_body = copy.copy(msg.body) + new_body["type"] = oef_type.value + + if oef_type in {OEFMessage.Type.REGISTER_SERVICE, OEFMessage.Type.UNREGISTER_SERVICE}: + service_description = msg.body["service_description"] # type: Description + service_description_bytes = base64.b64encode(pickle.dumps(service_description)).decode("utf-8") + new_body["service_description"] = service_description_bytes + elif oef_type in {OEFMessage.Type.SEARCH_SERVICES, OEFMessage.Type.SEARCH_AGENTS}: + query = msg.body["query"] # type: Query + query_bytes = base64.b64encode(pickle.dumps(query)).decode("utf-8") + new_body["query"] = query_bytes + elif oef_type in {OEFMessage.Type.SEARCH_RESULT}: + # we need this cast because the "agents" field might contains + # the Protobuf type "RepeatedScalarContainer", which is not JSON serializable. + new_body["agents"] = list(msg.body["agents"]) + elif oef_type in {OEFMessage.Type.OEF_ERROR}: + operation = msg.body["operation"] + new_body["operation"] = str(operation) + + oef_message_bytes = json.dumps(new_body).encode("utf-8") + return oef_message_bytes + + def decode(self, obj: bytes) -> Message: + """ + Decode the message. + + :param obj: the bytes object + :return: the message + """ + json_msg = json.loads(obj.decode("utf-8")) + oef_type = OEFMessage.Type(json_msg["type"]) + new_body = copy.copy(json_msg) + new_body["type"] = oef_type + + if oef_type in {OEFMessage.Type.REGISTER_SERVICE, OEFMessage.Type.UNREGISTER_SERVICE}: + service_description_bytes = base64.b64decode(json_msg["service_description"]) + service_description = pickle.loads(service_description_bytes) + new_body["service_description"] = service_description + elif oef_type in {OEFMessage.Type.SEARCH_SERVICES, OEFMessage.Type.SEARCH_AGENTS}: + query_bytes = base64.b64decode(json_msg["query"]) + query = pickle.loads(query_bytes) + new_body["query"] = query + elif oef_type in {OEFMessage.Type.SEARCH_RESULT}: + new_body["agents"] = list(json_msg["agents"]) + elif oef_type in {OEFMessage.Type.OEF_ERROR}: + operation = json_msg["operation"] + new_body["operation"] = OEFMessage.OEFErrorOperation(int(operation)) + + oef_message = OEFMessage(oef_type=oef_type, body=new_body) + return oef_message diff --git a/my_agent/protocols/tac/__init__.py b/my_agent/protocols/tac/__init__.py new file mode 100644 index 0000000000..430d160a4f --- /dev/null +++ b/my_agent/protocols/tac/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the support resources for the TAC protocol.""" diff --git a/my_agent/protocols/tac/message.py b/my_agent/protocols/tac/message.py new file mode 100644 index 0000000000..07a100d90e --- /dev/null +++ b/my_agent/protocols/tac/message.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the default message definition.""" +from enum import Enum +from typing import Dict, Optional, cast + +from aea.protocols.base import Message + + +class TACMessage(Message): + """The TAC message class.""" + + protocol_id = "tac" + + class Type(Enum): + """TAC Message types.""" + + REGISTER = "register" + UNREGISTER = "unregister" + TRANSACTION = "transaction" + GET_STATE_UPDATE = "get_state_update" + CANCELLED = "cancelled" + GAME_DATA = "game_data" + TRANSACTION_CONFIRMATION = "transaction_confirmation" + STATE_UPDATE = "state_update" + TAC_ERROR = "tac_error" + + def __str__(self): + """Get string representation.""" + return self.value + + class ErrorCode(Enum): + """This class defines the error codes.""" + + GENERIC_ERROR = 0 + REQUEST_NOT_VALID = 1 + AGENT_PBK_ALREADY_REGISTERED = 2 + AGENT_NAME_ALREADY_REGISTERED = 3 + AGENT_NOT_REGISTERED = 4 + TRANSACTION_NOT_VALID = 5 + TRANSACTION_NOT_MATCHING = 6 + AGENT_NAME_NOT_IN_WHITELIST = 7 + COMPETITION_NOT_RUNNING = 8 + DIALOGUE_INCONSISTENT = 9 + + _from_ec_to_msg = { + ErrorCode.GENERIC_ERROR: "Unexpected error.", + ErrorCode.REQUEST_NOT_VALID: "Request not recognized", + ErrorCode.AGENT_PBK_ALREADY_REGISTERED: "Agent pbk already registered.", + ErrorCode.AGENT_NAME_ALREADY_REGISTERED: "Agent name already registered.", + ErrorCode.AGENT_NOT_REGISTERED: "Agent not registered.", + ErrorCode.TRANSACTION_NOT_VALID: "Error in checking transaction", + ErrorCode.TRANSACTION_NOT_MATCHING: "The transaction request does not match with a previous transaction request with the same id.", + ErrorCode.AGENT_NAME_NOT_IN_WHITELIST: "Agent name not in whitelist.", + ErrorCode.COMPETITION_NOT_RUNNING: "The competition is not running yet.", + ErrorCode.DIALOGUE_INCONSISTENT: "The message is inconsistent with the dialogue." + } # type: Dict[ErrorCode, str] + + def __init__(self, tac_type: Optional[Type] = None, + **kwargs): + """ + Initialize. + + :param tac_type: the type of TAC message. + """ + super().__init__(type=tac_type, **kwargs) + assert self.check_consistency(), "TACMessage initialization inconsistent." + + def check_consistency(self) -> bool: + """Check that the data is consistent.""" + try: + assert self.is_set("type") + tac_type = TACMessage.Type(self.get("type")) + if tac_type == TACMessage.Type.REGISTER: + assert self.is_set("agent_name") + elif tac_type == TACMessage.Type.UNREGISTER: + pass + elif tac_type == TACMessage.Type.TRANSACTION: + assert self.is_set("transaction_id") + assert self.is_set("is_sender_buyer") + assert self.is_set("counterparty") + assert self.is_set("amount") + amount = cast(float, self.get("amount")) + assert amount >= 0.0 + assert self.is_set("quantities_by_good_pbk") + quantities_by_good_pbk = cast(Dict[str, int], self.get("quantities_by_good_pbk")) + assert len(quantities_by_good_pbk.keys()) == len(set(quantities_by_good_pbk.keys())) + assert all(quantity >= 0 for quantity in quantities_by_good_pbk.values()) + elif tac_type == TACMessage.Type.GET_STATE_UPDATE: + pass + elif tac_type == TACMessage.Type.CANCELLED: + pass + elif tac_type == TACMessage.Type.GAME_DATA: + assert self.is_set("money") + assert self.is_set("endowment") + assert self.is_set("utility_params") + assert self.is_set("nb_agents") + assert self.is_set("nb_goods") + assert self.is_set("tx_fee") + assert self.is_set("agent_pbk_to_name") + assert self.is_set("good_pbk_to_name") + elif tac_type == TACMessage.Type.TRANSACTION_CONFIRMATION: + assert self.is_set("transaction_id") + elif tac_type == TACMessage.Type.STATE_UPDATE: + assert self.is_set("initial_state") + assert self.is_set("transactions") + elif tac_type == TACMessage.Type.TAC_ERROR: + assert self.is_set("error_code") + error_code = self.get("error_code") + assert error_code in set(self.ErrorCode) + else: + raise ValueError("Type not recognized.") + except (AssertionError, ValueError): + return False + + return True diff --git a/my_agent/protocols/tac/protocol.yaml b/my_agent/protocols/tac/protocol.yaml new file mode 100644 index 0000000000..435e33b383 --- /dev/null +++ b/my_agent/protocols/tac/protocol.yaml @@ -0,0 +1,6 @@ +name: tac +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +description: "The tac protocol implements the messages an AEA needs to participate in the TAC." \ No newline at end of file diff --git a/my_agent/protocols/tac/serialization.py b/my_agent/protocols/tac/serialization.py new file mode 100644 index 0000000000..e4e085d5b0 --- /dev/null +++ b/my_agent/protocols/tac/serialization.py @@ -0,0 +1,234 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Serialization for the TAC protocol.""" + +from typing import Any, Dict, List, cast, TYPE_CHECKING + +from aea.protocols.base import Message +from aea.protocols.base import Serializer + +if TYPE_CHECKING: + from packages.protocols.tac import tac_pb2 + from packages.protocols.tac.message import TACMessage +else: + import tac_protocol.tac_pb2 as tac_pb2 + from tac_protocol.message import TACMessage + + +def _from_dict_to_pairs(d): + """Convert a flat dictionary into a list of StrStrPair or StrIntPair.""" + result = [] + items = sorted(d.items(), key=lambda pair: pair[0]) + for key, value in items: + if type(value) == int: + pair = tac_pb2.StrIntPair() + elif type(value) == str: + pair = tac_pb2.StrStrPair() + else: + raise ValueError("Either 'int' or 'str', not {}".format(type(value))) + pair.first = key + pair.second = value + result.append(pair) + return result + + +def _from_pairs_to_dict(pairs): + """Convert a list of StrStrPair or StrIntPair into a flat dictionary.""" + result = {} + for pair in pairs: + key = pair.first + value = pair.second + result[key] = value + return result + + +class TACSerializer(Serializer): + """Serialization for the TAC protocol.""" + + def encode(self, msg: Message) -> bytes: + """ + Decode the message. + + :param msg: the message object + :return: the bytes + """ + tac_type = TACMessage.Type(msg.get("type")) + tac_container = tac_pb2.TACMessage() + + if tac_type == TACMessage.Type.REGISTER: + agent_name = msg.get("agent_name") + tac_msg = tac_pb2.TACAgent.Register() # type: ignore + tac_msg.agent_name = agent_name + tac_container.register.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.UNREGISTER: + tac_msg = tac_pb2.TACAgent.Unregister() # type: ignore + tac_container.unregister.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.TRANSACTION: + tac_msg = tac_pb2.TACAgent.Transaction() # type: ignore + tac_msg.transaction_id = msg.get("transaction_id") + tac_msg.is_sender_buyer = msg.get("is_sender_buyer") + tac_msg.counterparty = msg.get("counterparty") + tac_msg.amount = msg.get("amount") + tac_msg.quantities.extend(_from_dict_to_pairs(msg.get("quantities_by_good_pbk"))) + tac_container.transaction.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.GET_STATE_UPDATE: + tac_msg = tac_pb2.TACAgent.GetStateUpdate() # type: ignore + tac_container.get_state_update.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.CANCELLED: + tac_msg = tac_pb2.TACController.Cancelled() # type: ignore + tac_container.cancelled.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.GAME_DATA: + tac_msg = tac_pb2.TACController.GameData() # type: ignore + tac_msg.money = msg.get("money") + tac_msg.endowment.extend(msg.get("endowment")) + tac_msg.utility_params.extend(msg.get("utility_params")) + tac_msg.nb_agents = msg.get("nb_agents") + tac_msg.nb_goods = msg.get("nb_goods") + tac_msg.tx_fee = msg.get("tx_fee") + tac_msg.agent_pbk_to_name.extend(_from_dict_to_pairs(msg.get("agent_pbk_to_name"))) + tac_msg.good_pbk_to_name.extend(_from_dict_to_pairs(msg.get("good_pbk_to_name"))) + tac_container.game_data.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.TRANSACTION_CONFIRMATION: + tac_msg = tac_pb2.TACController.TransactionConfirmation() # type: ignore + tac_msg.transaction_id = msg.get("transaction_id") + tac_container.transaction_confirmation.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.STATE_UPDATE: + tac_msg = tac_pb2.TACController.StateUpdate() # type: ignore + game_data_json = msg.get("initial_state") + game_data = tac_pb2.TACController.GameData() # type: ignore + game_data.money = game_data_json["money"] # type: ignore + game_data.endowment.extend(game_data_json["endowment"]) # type: ignore + game_data.utility_params.extend(game_data_json["utility_params"]) # type: ignore + game_data.nb_agents = game_data_json["nb_agents"] # type: ignore + game_data.nb_goods = game_data_json["nb_goods"] # type: ignore + game_data.tx_fee = game_data_json["tx_fee"] # type: ignore + game_data.agent_pbk_to_name.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["agent_pbk_to_name"]))) # type: ignore + game_data.good_pbk_to_name.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["good_pbk_to_name"]))) # type: ignore + + tac_msg.initial_state.CopyFrom(game_data) + + transactions = [] + msg_transactions = cast(List[Any], msg.get("transactions")) + for t in msg_transactions: + tx = tac_pb2.TACAgent.Transaction() # type: ignore + tx.transaction_id = t.get("transaction_id") + tx.is_sender_buyer = t.get("is_sender_buyer") + tx.counterparty = t.get("counterparty") + tx.amount = t.get("amount") + tx.quantities.extend(_from_dict_to_pairs(t.get("quantities_by_good_pbk"))) + transactions.append(tx) + tac_msg.txs.extend(transactions) + tac_container.state_update.CopyFrom(tac_msg) + elif tac_type == TACMessage.Type.TAC_ERROR: + tac_msg = tac_pb2.TACController.Error() # type: ignore + tac_msg.error_code = TACMessage.ErrorCode(msg.get("error_code")).value + if msg.is_set("error_msg"): + tac_msg.error_msg = msg.get("error_msg") + if msg.is_set("details"): + tac_msg.details.update(msg.get("details")) + + tac_container.error.CopyFrom(tac_msg) + else: + raise ValueError("Type not recognized: {}.".format(tac_type)) + + tac_message_bytes = tac_container.SerializeToString() + return tac_message_bytes + + def decode(self, obj: bytes) -> Message: + """ + Decode the message. + + :param obj: the bytes object + :return: the message + """ + tac_container = tac_pb2.TACMessage() + tac_container.ParseFromString(obj) + + new_body = {} # type: Dict[str, Any] + tac_type = tac_container.WhichOneof("content") + + if tac_type == "register": + new_body["type"] = TACMessage.Type.REGISTER + new_body["agent_name"] = tac_container.register.agent_name + elif tac_type == "unregister": + new_body["type"] = TACMessage.Type.UNREGISTER + elif tac_type == "transaction": + new_body["type"] = TACMessage.Type.TRANSACTION + new_body["transaction_id"] = tac_container.transaction.transaction_id + new_body["is_sender_buyer"] = tac_container.transaction.is_sender_buyer + new_body["counterparty"] = tac_container.transaction.counterparty + new_body["amount"] = tac_container.transaction.amount + new_body["quantities_by_good_pbk"] = _from_pairs_to_dict(tac_container.transaction.quantities) + elif tac_type == "get_state_update": + new_body["type"] = TACMessage.Type.GET_STATE_UPDATE + elif tac_type == "cancelled": + new_body["type"] = TACMessage.Type.CANCELLED + elif tac_type == "game_data": + new_body["type"] = TACMessage.Type.GAME_DATA + new_body["money"] = tac_container.game_data.money + new_body["endowment"] = list(tac_container.game_data.endowment) + new_body["utility_params"] = list(tac_container.game_data.utility_params) + new_body["nb_agents"] = tac_container.game_data.nb_agents + new_body["nb_goods"] = tac_container.game_data.nb_goods + new_body["tx_fee"] = tac_container.game_data.tx_fee + new_body["agent_pbk_to_name"] = _from_pairs_to_dict(tac_container.game_data.agent_pbk_to_name) + new_body["good_pbk_to_name"] = _from_pairs_to_dict(tac_container.game_data.good_pbk_to_name) + elif tac_type == "transaction_confirmation": + new_body["type"] = TACMessage.Type.TRANSACTION_CONFIRMATION + new_body["transaction_id"] = tac_container.transaction_confirmation.transaction_id + elif tac_type == "state_update": + new_body["type"] = TACMessage.Type.STATE_UPDATE + game_data = dict( + money=tac_container.state_update.initial_state.money, + endowment=tac_container.state_update.initial_state.endowment, + utility_params=tac_container.state_update.initial_state.utility_params, + nb_agents=tac_container.state_update.initial_state.nb_agents, + nb_goods=tac_container.state_update.initial_state.nb_goods, + tx_fee=tac_container.state_update.initial_state.tx_fee, + agent_pbk_to_name=_from_pairs_to_dict(tac_container.state_update.initial_state.agent_pbk_to_name), + good_pbk_to_name=_from_pairs_to_dict(tac_container.state_update.initial_state.good_pbk_to_name), + ) + new_body["initial_state"] = game_data + transactions = [] + for t in tac_container.state_update.txs: + tx_json = dict( + transaction_id=t.transaction_id, + is_sender_buyer=t.is_sender_buyer, + counterparty=t.counterparty, + amount=t.amount, + quantities_by_good_pbk=_from_pairs_to_dict(t.quantities), + ) + transactions.append(tx_json) + new_body["transactions"] = transactions + elif tac_type == "error": + new_body["type"] = TACMessage.Type.TAC_ERROR + new_body["error_code"] = TACMessage.ErrorCode(tac_container.error.error_code) + if tac_container.error.error_msg: + new_body["error_msg"] = tac_container.error.error_msg + if tac_container.error.details: + new_body["details"] = dict(tac_container.error.details) + else: + raise ValueError("Type not recognized.") + + tac_type = TACMessage.Type(new_body["type"]) + new_body["type"] = tac_type + tac_message = TACMessage(tac_type=tac_type, body=new_body) + return tac_message diff --git a/my_agent/protocols/tac/tac.proto b/my_agent/protocols/tac/tac.proto new file mode 100644 index 0000000000..d6a714d425 --- /dev/null +++ b/my_agent/protocols/tac/tac.proto @@ -0,0 +1,102 @@ +syntax = "proto3"; + +package fetch.oef.pb; + +import "google/protobuf/struct.proto"; + +message StrIntPair { + string first = 1; + int32 second = 2; +} + +message StrStrPair { + string first = 1; + string second = 2; +} + +message TACController { + + message Registered { + } + message Unregistered { + } + message Cancelled { + } + + message GameData { + double money = 1; + repeated int32 endowment = 2; + repeated double utility_params = 3; + int32 nb_agents = 4; + int32 nb_goods = 5; + double tx_fee = 6; + repeated StrStrPair agent_pbk_to_name = 7; + repeated StrStrPair good_pbk_to_name = 8; + } + + message TransactionConfirmation { + string transaction_id = 1; + } + + message StateUpdate { + GameData initial_state = 1; + repeated TACAgent.Transaction txs = 2; + } + + message Error { + enum ErrorCode { + GENERIC_ERROR = 0; + REQUEST_NOT_VALID = 1; + AGENT_PBK_ALREADY_REGISTERED = 2; + AGENT_NAME_ALREADY_REGISTERED = 3; + AGENT_NOT_REGISTERED = 4; + TRANSACTION_NOT_VALID = 5; + TRANSACTION_NOT_MATCHING = 6; + AGENT_NAME_NOT_IN_WHITELIST = 7; + COMPETITION_NOT_RUNNING = 8; + DIALOGUE_INCONSISTENT = 9; + } + + ErrorCode error_code = 1; + string error_msg = 2; + google.protobuf.Struct details = 3; + } + +} + +message TACAgent { + + message Register { + string agent_name = 1; + } + message Unregister { + } + + message Transaction { + string transaction_id = 1; + bool is_sender_buyer = 2; // is the sender of this message a buyer? + string counterparty = 3; + double amount = 4; + repeated StrIntPair quantities = 5; + } + + message GetStateUpdate { + } + +} + +message TACMessage { + oneof content{ + TACAgent.Register register = 1; + TACAgent.Unregister unregister = 2; + TACAgent.Transaction transaction = 3; + TACAgent.GetStateUpdate get_state_update = 4; + TACController.Registered registered = 5; + TACController.Unregistered unregistered = 6; + TACController.Cancelled cancelled = 7; + TACController.GameData game_data = 8; + TACController.TransactionConfirmation transaction_confirmation = 9; + TACController.StateUpdate state_update = 10; + TACController.Error error = 11; + } +} diff --git a/my_agent/protocols/tac/tac_pb2.py b/my_agent/protocols/tac/tac_pb2.py new file mode 100644 index 0000000000..5eb63f23f1 --- /dev/null +++ b/my_agent/protocols/tac/tac_pb2.py @@ -0,0 +1,899 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: tac.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +from google.protobuf import descriptor_pb2 +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='tac.proto', + package='fetch.oef.pb', + syntax='proto3', + serialized_pb=_b('\n\ttac.proto\x12\x0c\x66\x65tch.oef.pb\x1a\x1cgoogle/protobuf/struct.proto\"+\n\nStrIntPair\x12\r\n\x05\x66irst\x18\x01 \x01(\t\x12\x0e\n\x06second\x18\x02 \x01(\x05\"+\n\nStrStrPair\x12\r\n\x05\x66irst\x18\x01 \x01(\t\x12\x0e\n\x06second\x18\x02 \x01(\t\"\x80\x07\n\rTACController\x1a\x0c\n\nRegistered\x1a\x0e\n\x0cUnregistered\x1a\x0b\n\tCancelled\x1a\xe2\x01\n\x08GameData\x12\r\n\x05money\x18\x01 \x01(\x01\x12\x11\n\tendowment\x18\x02 \x03(\x05\x12\x16\n\x0eutility_params\x18\x03 \x03(\x01\x12\x11\n\tnb_agents\x18\x04 \x01(\x05\x12\x10\n\x08nb_goods\x18\x05 \x01(\x05\x12\x0e\n\x06tx_fee\x18\x06 \x01(\x01\x12\x33\n\x11\x61gent_pbk_to_name\x18\x07 \x03(\x0b\x32\x18.fetch.oef.pb.StrStrPair\x12\x32\n\x10good_pbk_to_name\x18\x08 \x03(\x0b\x32\x18.fetch.oef.pb.StrStrPair\x1a\x31\n\x17TransactionConfirmation\x12\x16\n\x0etransaction_id\x18\x01 \x01(\t\x1a{\n\x0bStateUpdate\x12;\n\rinitial_state\x18\x01 \x01(\x0b\x32$.fetch.oef.pb.TACController.GameData\x12/\n\x03txs\x18\x02 \x03(\x0b\x32\".fetch.oef.pb.TACAgent.Transaction\x1a\xae\x03\n\x05\x45rror\x12?\n\nerror_code\x18\x01 \x01(\x0e\x32+.fetch.oef.pb.TACController.Error.ErrorCode\x12\x11\n\terror_msg\x18\x02 \x01(\t\x12(\n\x07\x64\x65tails\x18\x03 \x01(\x0b\x32\x17.google.protobuf.Struct\"\xa6\x02\n\tErrorCode\x12\x11\n\rGENERIC_ERROR\x10\x00\x12\x15\n\x11REQUEST_NOT_VALID\x10\x01\x12 \n\x1c\x41GENT_PBK_ALREADY_REGISTERED\x10\x02\x12!\n\x1d\x41GENT_NAME_ALREADY_REGISTERED\x10\x03\x12\x18\n\x14\x41GENT_NOT_REGISTERED\x10\x04\x12\x19\n\x15TRANSACTION_NOT_VALID\x10\x05\x12\x1c\n\x18TRANSACTION_NOT_MATCHING\x10\x06\x12\x1f\n\x1b\x41GENT_NAME_NOT_IN_WHITELIST\x10\x07\x12\x1b\n\x17\x43OMPETITION_NOT_RUNNING\x10\x08\x12\x19\n\x15\x44IALOGUE_INCONSISTENT\x10\t\"\xdf\x01\n\x08TACAgent\x1a\x1e\n\x08Register\x12\x12\n\nagent_name\x18\x01 \x01(\t\x1a\x0c\n\nUnregister\x1a\x92\x01\n\x0bTransaction\x12\x16\n\x0etransaction_id\x18\x01 \x01(\t\x12\x17\n\x0fis_sender_buyer\x18\x02 \x01(\x08\x12\x14\n\x0c\x63ounterparty\x18\x03 \x01(\t\x12\x0e\n\x06\x61mount\x18\x04 \x01(\x01\x12,\n\nquantities\x18\x05 \x03(\x0b\x32\x18.fetch.oef.pb.StrIntPair\x1a\x10\n\x0eGetStateUpdate\"\xc8\x05\n\nTACMessage\x12\x33\n\x08register\x18\x01 \x01(\x0b\x32\x1f.fetch.oef.pb.TACAgent.RegisterH\x00\x12\x37\n\nunregister\x18\x02 \x01(\x0b\x32!.fetch.oef.pb.TACAgent.UnregisterH\x00\x12\x39\n\x0btransaction\x18\x03 \x01(\x0b\x32\".fetch.oef.pb.TACAgent.TransactionH\x00\x12\x41\n\x10get_state_update\x18\x04 \x01(\x0b\x32%.fetch.oef.pb.TACAgent.GetStateUpdateH\x00\x12<\n\nregistered\x18\x05 \x01(\x0b\x32&.fetch.oef.pb.TACController.RegisteredH\x00\x12@\n\x0cunregistered\x18\x06 \x01(\x0b\x32(.fetch.oef.pb.TACController.UnregisteredH\x00\x12:\n\tcancelled\x18\x07 \x01(\x0b\x32%.fetch.oef.pb.TACController.CancelledH\x00\x12\x39\n\tgame_data\x18\x08 \x01(\x0b\x32$.fetch.oef.pb.TACController.GameDataH\x00\x12W\n\x18transaction_confirmation\x18\t \x01(\x0b\x32\x33.fetch.oef.pb.TACController.TransactionConfirmationH\x00\x12?\n\x0cstate_update\x18\n \x01(\x0b\x32\'.fetch.oef.pb.TACController.StateUpdateH\x00\x12\x32\n\x05\x65rror\x18\x0b \x01(\x0b\x32!.fetch.oef.pb.TACController.ErrorH\x00\x42\t\n\x07\x63ontentb\x06proto3') + , + dependencies=[google_dot_protobuf_dot_struct__pb2.DESCRIPTOR,]) +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + + + +_TACCONTROLLER_ERROR_ERRORCODE = _descriptor.EnumDescriptor( + name='ErrorCode', + full_name='fetch.oef.pb.TACController.Error.ErrorCode', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='GENERIC_ERROR', index=0, number=0, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='REQUEST_NOT_VALID', index=1, number=1, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='AGENT_PBK_ALREADY_REGISTERED', index=2, number=2, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='AGENT_NAME_ALREADY_REGISTERED', index=3, number=3, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='AGENT_NOT_REGISTERED', index=4, number=4, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='TRANSACTION_NOT_VALID', index=5, number=5, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='TRANSACTION_NOT_MATCHING', index=6, number=6, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='AGENT_NAME_NOT_IN_WHITELIST', index=7, number=7, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='COMPETITION_NOT_RUNNING', index=8, number=8, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='DIALOGUE_INCONSISTENT', index=9, number=9, + options=None, + type=None), + ], + containing_type=None, + options=None, + serialized_start=750, + serialized_end=1044, +) +_sym_db.RegisterEnumDescriptor(_TACCONTROLLER_ERROR_ERRORCODE) + + +_STRINTPAIR = _descriptor.Descriptor( + name='StrIntPair', + full_name='fetch.oef.pb.StrIntPair', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='first', full_name='fetch.oef.pb.StrIntPair.first', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='second', full_name='fetch.oef.pb.StrIntPair.second', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=57, + serialized_end=100, +) + + +_STRSTRPAIR = _descriptor.Descriptor( + name='StrStrPair', + full_name='fetch.oef.pb.StrStrPair', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='first', full_name='fetch.oef.pb.StrStrPair.first', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='second', full_name='fetch.oef.pb.StrStrPair.second', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=102, + serialized_end=145, +) + + +_TACCONTROLLER_REGISTERED = _descriptor.Descriptor( + name='Registered', + full_name='fetch.oef.pb.TACController.Registered', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=165, + serialized_end=177, +) + +_TACCONTROLLER_UNREGISTERED = _descriptor.Descriptor( + name='Unregistered', + full_name='fetch.oef.pb.TACController.Unregistered', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=179, + serialized_end=193, +) + +_TACCONTROLLER_CANCELLED = _descriptor.Descriptor( + name='Cancelled', + full_name='fetch.oef.pb.TACController.Cancelled', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=195, + serialized_end=206, +) + +_TACCONTROLLER_GAMEDATA = _descriptor.Descriptor( + name='GameData', + full_name='fetch.oef.pb.TACController.GameData', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='money', full_name='fetch.oef.pb.TACController.GameData.money', index=0, + number=1, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='endowment', full_name='fetch.oef.pb.TACController.GameData.endowment', index=1, + number=2, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='utility_params', full_name='fetch.oef.pb.TACController.GameData.utility_params', index=2, + number=3, type=1, cpp_type=5, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='nb_agents', full_name='fetch.oef.pb.TACController.GameData.nb_agents', index=3, + number=4, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='nb_goods', full_name='fetch.oef.pb.TACController.GameData.nb_goods', index=4, + number=5, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='tx_fee', full_name='fetch.oef.pb.TACController.GameData.tx_fee', index=5, + number=6, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='agent_pbk_to_name', full_name='fetch.oef.pb.TACController.GameData.agent_pbk_to_name', index=6, + number=7, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='good_pbk_to_name', full_name='fetch.oef.pb.TACController.GameData.good_pbk_to_name', index=7, + number=8, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=209, + serialized_end=435, +) + +_TACCONTROLLER_TRANSACTIONCONFIRMATION = _descriptor.Descriptor( + name='TransactionConfirmation', + full_name='fetch.oef.pb.TACController.TransactionConfirmation', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='transaction_id', full_name='fetch.oef.pb.TACController.TransactionConfirmation.transaction_id', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=437, + serialized_end=486, +) + +_TACCONTROLLER_STATEUPDATE = _descriptor.Descriptor( + name='StateUpdate', + full_name='fetch.oef.pb.TACController.StateUpdate', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='initial_state', full_name='fetch.oef.pb.TACController.StateUpdate.initial_state', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='txs', full_name='fetch.oef.pb.TACController.StateUpdate.txs', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=488, + serialized_end=611, +) + +_TACCONTROLLER_ERROR = _descriptor.Descriptor( + name='Error', + full_name='fetch.oef.pb.TACController.Error', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='error_code', full_name='fetch.oef.pb.TACController.Error.error_code', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='error_msg', full_name='fetch.oef.pb.TACController.Error.error_msg', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='details', full_name='fetch.oef.pb.TACController.Error.details', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _TACCONTROLLER_ERROR_ERRORCODE, + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=614, + serialized_end=1044, +) + +_TACCONTROLLER = _descriptor.Descriptor( + name='TACController', + full_name='fetch.oef.pb.TACController', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[_TACCONTROLLER_REGISTERED, _TACCONTROLLER_UNREGISTERED, _TACCONTROLLER_CANCELLED, _TACCONTROLLER_GAMEDATA, _TACCONTROLLER_TRANSACTIONCONFIRMATION, _TACCONTROLLER_STATEUPDATE, _TACCONTROLLER_ERROR, ], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=148, + serialized_end=1044, +) + + +_TACAGENT_REGISTER = _descriptor.Descriptor( + name='Register', + full_name='fetch.oef.pb.TACAgent.Register', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='agent_name', full_name='fetch.oef.pb.TACAgent.Register.agent_name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1059, + serialized_end=1089, +) + +_TACAGENT_UNREGISTER = _descriptor.Descriptor( + name='Unregister', + full_name='fetch.oef.pb.TACAgent.Unregister', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1091, + serialized_end=1103, +) + +_TACAGENT_TRANSACTION = _descriptor.Descriptor( + name='Transaction', + full_name='fetch.oef.pb.TACAgent.Transaction', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='transaction_id', full_name='fetch.oef.pb.TACAgent.Transaction.transaction_id', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='is_sender_buyer', full_name='fetch.oef.pb.TACAgent.Transaction.is_sender_buyer', index=1, + number=2, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='counterparty', full_name='fetch.oef.pb.TACAgent.Transaction.counterparty', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='amount', full_name='fetch.oef.pb.TACAgent.Transaction.amount', index=3, + number=4, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='quantities', full_name='fetch.oef.pb.TACAgent.Transaction.quantities', index=4, + number=5, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1106, + serialized_end=1252, +) + +_TACAGENT_GETSTATEUPDATE = _descriptor.Descriptor( + name='GetStateUpdate', + full_name='fetch.oef.pb.TACAgent.GetStateUpdate', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1254, + serialized_end=1270, +) + +_TACAGENT = _descriptor.Descriptor( + name='TACAgent', + full_name='fetch.oef.pb.TACAgent', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[_TACAGENT_REGISTER, _TACAGENT_UNREGISTER, _TACAGENT_TRANSACTION, _TACAGENT_GETSTATEUPDATE, ], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1047, + serialized_end=1270, +) + + +_TACMESSAGE = _descriptor.Descriptor( + name='TACMessage', + full_name='fetch.oef.pb.TACMessage', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='register', full_name='fetch.oef.pb.TACMessage.register', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='unregister', full_name='fetch.oef.pb.TACMessage.unregister', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='transaction', full_name='fetch.oef.pb.TACMessage.transaction', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='get_state_update', full_name='fetch.oef.pb.TACMessage.get_state_update', index=3, + number=4, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='registered', full_name='fetch.oef.pb.TACMessage.registered', index=4, + number=5, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='unregistered', full_name='fetch.oef.pb.TACMessage.unregistered', index=5, + number=6, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='cancelled', full_name='fetch.oef.pb.TACMessage.cancelled', index=6, + number=7, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='game_data', full_name='fetch.oef.pb.TACMessage.game_data', index=7, + number=8, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='transaction_confirmation', full_name='fetch.oef.pb.TACMessage.transaction_confirmation', index=8, + number=9, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='state_update', full_name='fetch.oef.pb.TACMessage.state_update', index=9, + number=10, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='error', full_name='fetch.oef.pb.TACMessage.error', index=10, + number=11, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='content', full_name='fetch.oef.pb.TACMessage.content', + index=0, containing_type=None, fields=[]), + ], + serialized_start=1273, + serialized_end=1985, +) + +_TACCONTROLLER_REGISTERED.containing_type = _TACCONTROLLER +_TACCONTROLLER_UNREGISTERED.containing_type = _TACCONTROLLER +_TACCONTROLLER_CANCELLED.containing_type = _TACCONTROLLER +_TACCONTROLLER_GAMEDATA.fields_by_name['agent_pbk_to_name'].message_type = _STRSTRPAIR +_TACCONTROLLER_GAMEDATA.fields_by_name['good_pbk_to_name'].message_type = _STRSTRPAIR +_TACCONTROLLER_GAMEDATA.containing_type = _TACCONTROLLER +_TACCONTROLLER_TRANSACTIONCONFIRMATION.containing_type = _TACCONTROLLER +_TACCONTROLLER_STATEUPDATE.fields_by_name['initial_state'].message_type = _TACCONTROLLER_GAMEDATA +_TACCONTROLLER_STATEUPDATE.fields_by_name['txs'].message_type = _TACAGENT_TRANSACTION +_TACCONTROLLER_STATEUPDATE.containing_type = _TACCONTROLLER +_TACCONTROLLER_ERROR.fields_by_name['error_code'].enum_type = _TACCONTROLLER_ERROR_ERRORCODE +_TACCONTROLLER_ERROR.fields_by_name['details'].message_type = google_dot_protobuf_dot_struct__pb2._STRUCT +_TACCONTROLLER_ERROR.containing_type = _TACCONTROLLER +_TACCONTROLLER_ERROR_ERRORCODE.containing_type = _TACCONTROLLER_ERROR +_TACAGENT_REGISTER.containing_type = _TACAGENT +_TACAGENT_UNREGISTER.containing_type = _TACAGENT +_TACAGENT_TRANSACTION.fields_by_name['quantities'].message_type = _STRINTPAIR +_TACAGENT_TRANSACTION.containing_type = _TACAGENT +_TACAGENT_GETSTATEUPDATE.containing_type = _TACAGENT +_TACMESSAGE.fields_by_name['register'].message_type = _TACAGENT_REGISTER +_TACMESSAGE.fields_by_name['unregister'].message_type = _TACAGENT_UNREGISTER +_TACMESSAGE.fields_by_name['transaction'].message_type = _TACAGENT_TRANSACTION +_TACMESSAGE.fields_by_name['get_state_update'].message_type = _TACAGENT_GETSTATEUPDATE +_TACMESSAGE.fields_by_name['registered'].message_type = _TACCONTROLLER_REGISTERED +_TACMESSAGE.fields_by_name['unregistered'].message_type = _TACCONTROLLER_UNREGISTERED +_TACMESSAGE.fields_by_name['cancelled'].message_type = _TACCONTROLLER_CANCELLED +_TACMESSAGE.fields_by_name['game_data'].message_type = _TACCONTROLLER_GAMEDATA +_TACMESSAGE.fields_by_name['transaction_confirmation'].message_type = _TACCONTROLLER_TRANSACTIONCONFIRMATION +_TACMESSAGE.fields_by_name['state_update'].message_type = _TACCONTROLLER_STATEUPDATE +_TACMESSAGE.fields_by_name['error'].message_type = _TACCONTROLLER_ERROR +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['register']) +_TACMESSAGE.fields_by_name['register'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['unregister']) +_TACMESSAGE.fields_by_name['unregister'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['transaction']) +_TACMESSAGE.fields_by_name['transaction'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['get_state_update']) +_TACMESSAGE.fields_by_name['get_state_update'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['registered']) +_TACMESSAGE.fields_by_name['registered'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['unregistered']) +_TACMESSAGE.fields_by_name['unregistered'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['cancelled']) +_TACMESSAGE.fields_by_name['cancelled'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['game_data']) +_TACMESSAGE.fields_by_name['game_data'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['transaction_confirmation']) +_TACMESSAGE.fields_by_name['transaction_confirmation'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['state_update']) +_TACMESSAGE.fields_by_name['state_update'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +_TACMESSAGE.oneofs_by_name['content'].fields.append( + _TACMESSAGE.fields_by_name['error']) +_TACMESSAGE.fields_by_name['error'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] +DESCRIPTOR.message_types_by_name['StrIntPair'] = _STRINTPAIR +DESCRIPTOR.message_types_by_name['StrStrPair'] = _STRSTRPAIR +DESCRIPTOR.message_types_by_name['TACController'] = _TACCONTROLLER +DESCRIPTOR.message_types_by_name['TACAgent'] = _TACAGENT +DESCRIPTOR.message_types_by_name['TACMessage'] = _TACMESSAGE + +StrIntPair = _reflection.GeneratedProtocolMessageType('StrIntPair', (_message.Message,), dict( + DESCRIPTOR = _STRINTPAIR, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.StrIntPair) + )) +_sym_db.RegisterMessage(StrIntPair) + +StrStrPair = _reflection.GeneratedProtocolMessageType('StrStrPair', (_message.Message,), dict( + DESCRIPTOR = _STRSTRPAIR, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.StrStrPair) + )) +_sym_db.RegisterMessage(StrStrPair) + +TACController = _reflection.GeneratedProtocolMessageType('TACController', (_message.Message,), dict( + + Registered = _reflection.GeneratedProtocolMessageType('Registered', (_message.Message,), dict( + DESCRIPTOR = _TACCONTROLLER_REGISTERED, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Registered) + )) + , + + Unregistered = _reflection.GeneratedProtocolMessageType('Unregistered', (_message.Message,), dict( + DESCRIPTOR = _TACCONTROLLER_UNREGISTERED, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Unregistered) + )) + , + + Cancelled = _reflection.GeneratedProtocolMessageType('Cancelled', (_message.Message,), dict( + DESCRIPTOR = _TACCONTROLLER_CANCELLED, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Cancelled) + )) + , + + GameData = _reflection.GeneratedProtocolMessageType('GameData', (_message.Message,), dict( + DESCRIPTOR = _TACCONTROLLER_GAMEDATA, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.GameData) + )) + , + + TransactionConfirmation = _reflection.GeneratedProtocolMessageType('TransactionConfirmation', (_message.Message,), dict( + DESCRIPTOR = _TACCONTROLLER_TRANSACTIONCONFIRMATION, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.TransactionConfirmation) + )) + , + + StateUpdate = _reflection.GeneratedProtocolMessageType('StateUpdate', (_message.Message,), dict( + DESCRIPTOR = _TACCONTROLLER_STATEUPDATE, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.StateUpdate) + )) + , + + Error = _reflection.GeneratedProtocolMessageType('Error', (_message.Message,), dict( + DESCRIPTOR = _TACCONTROLLER_ERROR, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Error) + )) + , + DESCRIPTOR = _TACCONTROLLER, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController) + )) +_sym_db.RegisterMessage(TACController) +_sym_db.RegisterMessage(TACController.Registered) +_sym_db.RegisterMessage(TACController.Unregistered) +_sym_db.RegisterMessage(TACController.Cancelled) +_sym_db.RegisterMessage(TACController.GameData) +_sym_db.RegisterMessage(TACController.TransactionConfirmation) +_sym_db.RegisterMessage(TACController.StateUpdate) +_sym_db.RegisterMessage(TACController.Error) + +TACAgent = _reflection.GeneratedProtocolMessageType('TACAgent', (_message.Message,), dict( + + Register = _reflection.GeneratedProtocolMessageType('Register', (_message.Message,), dict( + DESCRIPTOR = _TACAGENT_REGISTER, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.Register) + )) + , + + Unregister = _reflection.GeneratedProtocolMessageType('Unregister', (_message.Message,), dict( + DESCRIPTOR = _TACAGENT_UNREGISTER, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.Unregister) + )) + , + + Transaction = _reflection.GeneratedProtocolMessageType('Transaction', (_message.Message,), dict( + DESCRIPTOR = _TACAGENT_TRANSACTION, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.Transaction) + )) + , + + GetStateUpdate = _reflection.GeneratedProtocolMessageType('GetStateUpdate', (_message.Message,), dict( + DESCRIPTOR = _TACAGENT_GETSTATEUPDATE, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.GetStateUpdate) + )) + , + DESCRIPTOR = _TACAGENT, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent) + )) +_sym_db.RegisterMessage(TACAgent) +_sym_db.RegisterMessage(TACAgent.Register) +_sym_db.RegisterMessage(TACAgent.Unregister) +_sym_db.RegisterMessage(TACAgent.Transaction) +_sym_db.RegisterMessage(TACAgent.GetStateUpdate) + +TACMessage = _reflection.GeneratedProtocolMessageType('TACMessage', (_message.Message,), dict( + DESCRIPTOR = _TACMESSAGE, + __module__ = 'tac_pb2' + # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACMessage) + )) +_sym_db.RegisterMessage(TACMessage) + + +# @@protoc_insertion_point(module_scope) diff --git a/my_agent/skills/__init__.py b/my_agent/skills/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/my_agent/skills/error/__init__.py b/my_agent/skills/error/__init__.py new file mode 100644 index 0000000000..96c80ac32c --- /dev/null +++ b/my_agent/skills/error/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the implementation of the error skill.""" diff --git a/my_agent/skills/error/behaviours.py b/my_agent/skills/error/behaviours.py new file mode 100644 index 0000000000..556ee98ca7 --- /dev/null +++ b/my_agent/skills/error/behaviours.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains the error behaviours.""" + +from aea.skills.base import Behaviour + + +class ErrorBehaviour(Behaviour): + """This class implements the error behaviour.""" + + def setup(self) -> None: + """ + Implement the setup. + + :return: None + """ + pass + + def act(self) -> None: + """ + Implement the act. + + :return: None + """ + pass + + def teardown(self) -> None: + """ + Implement the task teardown. + + :return: None + """ + pass diff --git a/my_agent/skills/error/handlers.py b/my_agent/skills/error/handlers.py new file mode 100644 index 0000000000..098a61eced --- /dev/null +++ b/my_agent/skills/error/handlers.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains the implementation of the handler for the 'default' protocol.""" +import base64 +import logging +from typing import Optional + +from aea.configurations.base import ProtocolId +from aea.mail.base import Envelope +from aea.protocols.base import Message, Protocol +from aea.protocols.default.message import DefaultMessage +from aea.protocols.default.serialization import DefaultSerializer +from aea.skills.base import Handler + +logger = logging.getLogger(__name__) + + +class ErrorHandler(Handler): + """This class implements the error handler.""" + + SUPPORTED_PROTOCOL = 'default' # type: Optional[ProtocolId] + + def setup(self) -> None: + """ + Implement the setup. + + :return: None + """ + pass + + def handle(self, message: Message, sender: str) -> None: + """ + Implement the reaction to an envelope. + + :param message: the message + :param sender: the sender + """ + pass + + def teardown(self) -> None: + """ + Implement the handler teardown. + + :return: None + """ + pass + + def send_unsupported_protocol(self, envelope: Envelope) -> None: + """ + Handle the received envelope in case the protocol is not supported. + + :param envelope: the envelope + :return: None + """ + logger.warning("Unsupported protocol: {}".format(envelope.protocol_id)) + reply = DefaultMessage(type=DefaultMessage.Type.ERROR, + error_code=DefaultMessage.ErrorCode.UNSUPPORTED_PROTOCOL.value, + error_msg="Unsupported protocol.", + error_data={"protocol_id": envelope.protocol_id}) + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, + message=DefaultSerializer().encode(reply)) + + def send_decoding_error(self, envelope: Envelope) -> None: + """ + Handle a decoding error. + + :param envelope: the envelope + :return: None + """ + logger.warning("Decoding error: {}.".format(envelope)) + encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") + reply = DefaultMessage(type=DefaultMessage.Type.ERROR, + error_code=DefaultMessage.ErrorCode.DECODING_ERROR.value, + error_msg="Decoding error.", + error_data={"envelope": encoded_envelope}) + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, + message=DefaultSerializer().encode(reply)) + + def send_invalid_message(self, envelope: Envelope) -> None: + """ + Handle an message that is invalid wrt a protocol. + + :param envelope: the envelope + :return: None + """ + logger.warning("Invalid message wrt protocol: {}.".format(envelope.protocol_id)) + encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") + reply = DefaultMessage(type=DefaultMessage.Type.ERROR, + error_code=DefaultMessage.ErrorCode.INVALID_MESSAGE.value, + error_msg="Invalid message.", + error_data={"envelope": encoded_envelope}) + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, + message=DefaultSerializer().encode(reply)) + + def send_unsupported_skill(self, envelope: Envelope, protocol: Protocol) -> None: + """ + Handle the received envelope in case the skill is not supported. + + :param envelope: the envelope + :param protocol: the protocol + :return: None + """ + logger.warning("Cannot handle envelope: no handler registered for the protocol '{}'.".format(protocol.id)) + encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") + reply = DefaultMessage(type=DefaultMessage.Type.ERROR, + error_code=DefaultMessage.ErrorCode.UNSUPPORTED_SKILL.value, + error_msg="Unsupported skill.", + error_data={"envelope": encoded_envelope}) + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, + message=DefaultSerializer().encode(reply)) diff --git a/my_agent/skills/error/skill.yaml b/my_agent/skills/error/skill.yaml new file mode 100644 index 0000000000..e6f13e68b2 --- /dev/null +++ b/my_agent/skills/error/skill.yaml @@ -0,0 +1,15 @@ +name: error +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +behaviours: [] +handlers: + - handler: + class_name: ErrorHandler + args: + foo: bar +tasks: [] +shared_classes: [] +protocols: ['default'] +description: "Error skill description [Fill in]" diff --git a/my_agent/skills/error/tasks.py b/my_agent/skills/error/tasks.py new file mode 100644 index 0000000000..8922217537 --- /dev/null +++ b/my_agent/skills/error/tasks.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains the implementation of the error tasks.""" + +from aea.skills.base import Task + + +class ErrorTask(Task): + """This class implements the error task.""" + + def setup(self) -> None: + """ + Implement the setup. + + :return: None + """ + pass + + def execute(self) -> None: + """ + Implement the task execution. + + :param envelope: the envelope + :return: None + """ + pass + + def teardown(self) -> None: + """ + Implement the task teardown. + + :return: None + """ + pass diff --git a/my_agent/skills/my_search/__init__.py b/my_agent/skills/my_search/__init__.py new file mode 100644 index 0000000000..81d567366d --- /dev/null +++ b/my_agent/skills/my_search/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the implementation of the default skill.""" diff --git a/my_agent/skills/my_search/behaviours.py b/my_agent/skills/my_search/behaviours.py new file mode 100644 index 0000000000..408ed69412 --- /dev/null +++ b/my_agent/skills/my_search/behaviours.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +import logging +import time + +from aea.protocols.oef.message import OEFMessage +from aea.protocols.oef.models import Query, Constraint, ConstraintType +from aea.protocols.oef.serialization import DEFAULT_OEF, OEFSerializer +from aea.skills.base import Behaviour + +logger = logging.getLogger("aea.my_search_skill") + + +class MySearchBehaviour(Behaviour): + """This class provides a simple search behaviour.""" + + def __init__(self, **kwargs): + """Initialize the search behaviour.""" + super().__init__(**kwargs) + self.sent_search_count = 0 + + def setup(self) -> None: + """ + Implement the setup. + + :return: None + """ + logger.info("[{}]: setting up MySearchBehaviour".format(self.context.agent_name)) + + def act(self) -> None: + """ + Implement the act. + + :return: None + """ + time.sleep(1) # to slow down the agent + self.sent_search_count += 1 + + self.sent_search_count += 1 + search_constraints = [Constraint("country", ConstraintType("==", "UK"))] + + search_query_w_empty_model = Query(search_constraints, model=None) + + search_request = OEFMessage( + oef_type=OEFMessage.Type.SEARCH_SERVICES, + id=self.sent_search_count, + query=search_query_w_empty_model) + + logger.info("[{}]: sending search request to OEF, search_count={}".format( + self.context.agent_name, + self.sent_search_count)) + + self.context.outbox.put_message( + to=DEFAULT_OEF, + sender=self.context.agent_address, + protocol_id=OEFMessage.protocol_id, + message=OEFSerializer().encode(search_request)) + + def teardown(self) -> None: + """ + Implement the task teardown. + + :return: None + """ + logger.info("[{}]: tearing down MySearchBehaviour".format(self.context.agent_name)) \ No newline at end of file diff --git a/my_agent/skills/my_search/handlers.py b/my_agent/skills/my_search/handlers.py new file mode 100644 index 0000000000..4bac768e8f --- /dev/null +++ b/my_agent/skills/my_search/handlers.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains a scaffold of a handler.""" +import logging + +from aea.protocols.oef.message import OEFMessage +from aea.protocols.oef.serialization import OEFSerializer +from aea.skills.base import Handler + +logger = logging.getLogger("aea.my_search_skill") + + +class MySearchHandler(Handler): + """This class provides a simple search handler.""" + + SUPPORTED_PROTOCOL = OEFMessage.protocol_id + + def __init__(self, **kwargs): + """Initialize the handler.""" + super().__init__(**kwargs) + self.received_search_count = 0 + + def setup(self) -> None: + """Set up the handler.""" + logger.info("[{}]: setting up MySearchHandler".format(self.context.agent_name)) + + def handle(self, message: OEFMessage, sender: str) -> None: + """ + Handle the message. + + :param message: the message. + :param sender: the sender. + :return: None + """ + return + msg_type = OEFMessage.Type(message.get("type")) + + if msg_type is OEFMessage.Type.SEARCH_RESULT: + self.received_search_count += 1 + nb_agents_found = len(message.get("agents")) + logger.info("[{}]: found number of agents={}, received search count={}".format(self.context.agent_name, nb_agents_found, self.received_search_count)) + + def teardown(self) -> None: + """ + Teardown the handler. + + :return: None + """ + logger.info("[{}]: tearing down MySearchHandler".format(self.context.agent_name)) diff --git a/my_agent/skills/my_search/my_shared_class.py b/my_agent/skills/my_search/my_shared_class.py new file mode 100644 index 0000000000..bc9cacb2c7 --- /dev/null +++ b/my_agent/skills/my_search/my_shared_class.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains a scaffold of a shared class.""" + +from aea.skills.base import SharedClass + + +class MySharedClass(SharedClass): + """This class scaffolds a shared class.""" diff --git a/my_agent/skills/my_search/skill.yaml b/my_agent/skills/my_search/skill.yaml new file mode 100644 index 0000000000..8956027792 --- /dev/null +++ b/my_agent/skills/my_search/skill.yaml @@ -0,0 +1,21 @@ +name: my_search +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +description: 'A simple search skill utilising the OEF.' +behaviours: + - behaviour: + class_name: MySearchBehaviour + args: {} +handlers: + - handler: + class_name: MySearchHandler + args: {} +tasks: + - task: + class_name: MySearchTask + args: {} +shared_classes: [] +protocols: ["oef"] +dependencies: [] \ No newline at end of file diff --git a/my_agent/skills/my_search/tasks.py b/my_agent/skills/my_search/tasks.py new file mode 100644 index 0000000000..f0707f085f --- /dev/null +++ b/my_agent/skills/my_search/tasks.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +import logging + +from aea.skills.base import Task + +logger = logging.getLogger("aea.my_search_skill") + + +class MySearchTask(Task): + """This class scaffolds a task.""" + + def setup(self) -> None: + """ + Implement the setup. + + :return: None + """ + logger.info("[{}]: setting up MySearchTask".format(self.context.agent_name)) + + def execute(self) -> None: + """ + Implement the task execution. + + :param envelope: the envelope + :return: None + """ + my_search_behaviour = self.context.behaviours[0] + my_search_handler = self.context.handlers[0] + logger.info("[{}]: number of search requests sent={} vs. number of search responses received={}".format( + self.context.agent_name, + my_search_behaviour.sent_search_count, + my_search_handler.received_search_count) + ) + + def teardown(self) -> None: + """ + Implement the task teardown. + + :return: None + """ + logger.info("[{}]: tearing down MySearchTask".format(self.context.agent_name)) \ No newline at end of file From 263b706280b36d8079a298de0ee9101091d94878 Mon Sep 17 00:00:00 2001 From: Diarmid Campbell Date: Thu, 17 Oct 2019 16:55:30 +0100 Subject: [PATCH 64/71] removed spurois agents --- Test/aea-config.yaml | 21 - Test/connections/__init__.py | 0 Test/connections/oef/__init__.py | 21 - Test/connections/oef/connection.py | 604 ------------- Test/connections/oef/connection.yaml | 18 - Test/protocols/__init__.py | 0 Test/protocols/default/__init__.py | 21 - Test/protocols/default/message.py | 59 -- Test/protocols/default/protocol.yaml | 6 - Test/protocols/default/serialization.py | 71 -- Test/protocols/gym/__init__.py | 21 - Test/protocols/gym/message.py | 76 -- Test/protocols/gym/protocol.yaml | 6 - Test/protocols/gym/serialization.py | 103 --- Test/protocols/tac/__init__.py | 21 - Test/protocols/tac/message.py | 134 --- Test/protocols/tac/protocol.yaml | 6 - Test/protocols/tac/serialization.py | 234 ----- Test/protocols/tac/tac.proto | 102 --- Test/protocols/tac/tac_pb2.py | 899 ------------------- Test/skills/__init__.py | 0 Test/skills/error/__init__.py | 20 - Test/skills/error/behaviours.py | 50 -- Test/skills/error/handlers.py | 131 --- Test/skills/error/skill.yaml | 15 - Test/skills/error/tasks.py | 51 -- asdsd/aea-config.yaml | 21 - asdsd/connections/__init__.py | 0 asdsd/connections/oef/__init__.py | 21 - asdsd/connections/oef/connection.py | 604 ------------- asdsd/connections/oef/connection.yaml | 18 - asdsd/protocols/__init__.py | 0 asdsd/protocols/default/__init__.py | 21 - asdsd/protocols/default/message.py | 59 -- asdsd/protocols/default/protocol.yaml | 6 - asdsd/protocols/default/serialization.py | 71 -- asdsd/protocols/gym/__init__.py | 21 - asdsd/protocols/gym/message.py | 76 -- asdsd/protocols/gym/protocol.yaml | 6 - asdsd/protocols/gym/serialization.py | 103 --- asdsd/protocols/tac/__init__.py | 21 - asdsd/protocols/tac/message.py | 134 --- asdsd/protocols/tac/protocol.yaml | 6 - asdsd/protocols/tac/serialization.py | 234 ----- asdsd/protocols/tac/tac.proto | 102 --- asdsd/protocols/tac/tac_pb2.py | 899 ------------------- asdsd/skills/__init__.py | 0 asdsd/skills/error/__init__.py | 20 - asdsd/skills/error/behaviours.py | 50 -- asdsd/skills/error/handlers.py | 131 --- asdsd/skills/error/skill.yaml | 15 - asdsd/skills/error/tasks.py | 51 -- my_agent/aea-config.yaml | 31 - my_agent/connections/__init__.py | 0 my_agent/connections/oef/__init__.py | 21 - my_agent/connections/oef/connection.py | 604 ------------- my_agent/connections/oef/connection.yaml | 14 - my_agent/default_private_key.pem | 6 - my_agent/eth_private_key.txt | 1 - my_agent/fet_private_key.txt | 1 - my_agent/protocols/__init__.py | 0 my_agent/protocols/gym/__init__.py | 21 - my_agent/protocols/gym/message.py | 76 -- my_agent/protocols/gym/protocol.yaml | 6 - my_agent/protocols/gym/serialization.py | 103 --- my_agent/protocols/oef/__init__.py | 21 - my_agent/protocols/oef/message.py | 125 --- my_agent/protocols/oef/models.py | 450 ---------- my_agent/protocols/oef/protocol.yaml | 9 - my_agent/protocols/oef/serialization.py | 96 -- my_agent/protocols/tac/__init__.py | 21 - my_agent/protocols/tac/message.py | 134 --- my_agent/protocols/tac/protocol.yaml | 6 - my_agent/protocols/tac/serialization.py | 234 ----- my_agent/protocols/tac/tac.proto | 102 --- my_agent/protocols/tac/tac_pb2.py | 899 ------------------- my_agent/skills/__init__.py | 0 my_agent/skills/error/__init__.py | 20 - my_agent/skills/error/behaviours.py | 50 -- my_agent/skills/error/handlers.py | 131 --- my_agent/skills/error/skill.yaml | 15 - my_agent/skills/error/tasks.py | 51 -- my_agent/skills/my_search/__init__.py | 20 - my_agent/skills/my_search/behaviours.py | 82 -- my_agent/skills/my_search/handlers.py | 66 -- my_agent/skills/my_search/my_shared_class.py | 26 - my_agent/skills/my_search/skill.yaml | 21 - my_agent/skills/my_search/tasks.py | 59 -- 88 files changed, 8902 deletions(-) delete mode 100644 Test/aea-config.yaml delete mode 100644 Test/connections/__init__.py delete mode 100644 Test/connections/oef/__init__.py delete mode 100644 Test/connections/oef/connection.py delete mode 100644 Test/connections/oef/connection.yaml delete mode 100644 Test/protocols/__init__.py delete mode 100644 Test/protocols/default/__init__.py delete mode 100644 Test/protocols/default/message.py delete mode 100644 Test/protocols/default/protocol.yaml delete mode 100644 Test/protocols/default/serialization.py delete mode 100644 Test/protocols/gym/__init__.py delete mode 100644 Test/protocols/gym/message.py delete mode 100644 Test/protocols/gym/protocol.yaml delete mode 100644 Test/protocols/gym/serialization.py delete mode 100644 Test/protocols/tac/__init__.py delete mode 100644 Test/protocols/tac/message.py delete mode 100644 Test/protocols/tac/protocol.yaml delete mode 100644 Test/protocols/tac/serialization.py delete mode 100644 Test/protocols/tac/tac.proto delete mode 100644 Test/protocols/tac/tac_pb2.py delete mode 100644 Test/skills/__init__.py delete mode 100644 Test/skills/error/__init__.py delete mode 100644 Test/skills/error/behaviours.py delete mode 100644 Test/skills/error/handlers.py delete mode 100644 Test/skills/error/skill.yaml delete mode 100644 Test/skills/error/tasks.py delete mode 100644 asdsd/aea-config.yaml delete mode 100644 asdsd/connections/__init__.py delete mode 100644 asdsd/connections/oef/__init__.py delete mode 100644 asdsd/connections/oef/connection.py delete mode 100644 asdsd/connections/oef/connection.yaml delete mode 100644 asdsd/protocols/__init__.py delete mode 100644 asdsd/protocols/default/__init__.py delete mode 100644 asdsd/protocols/default/message.py delete mode 100644 asdsd/protocols/default/protocol.yaml delete mode 100644 asdsd/protocols/default/serialization.py delete mode 100644 asdsd/protocols/gym/__init__.py delete mode 100644 asdsd/protocols/gym/message.py delete mode 100644 asdsd/protocols/gym/protocol.yaml delete mode 100644 asdsd/protocols/gym/serialization.py delete mode 100644 asdsd/protocols/tac/__init__.py delete mode 100644 asdsd/protocols/tac/message.py delete mode 100644 asdsd/protocols/tac/protocol.yaml delete mode 100644 asdsd/protocols/tac/serialization.py delete mode 100644 asdsd/protocols/tac/tac.proto delete mode 100644 asdsd/protocols/tac/tac_pb2.py delete mode 100644 asdsd/skills/__init__.py delete mode 100644 asdsd/skills/error/__init__.py delete mode 100644 asdsd/skills/error/behaviours.py delete mode 100644 asdsd/skills/error/handlers.py delete mode 100644 asdsd/skills/error/skill.yaml delete mode 100644 asdsd/skills/error/tasks.py delete mode 100644 my_agent/aea-config.yaml delete mode 100644 my_agent/connections/__init__.py delete mode 100644 my_agent/connections/oef/__init__.py delete mode 100644 my_agent/connections/oef/connection.py delete mode 100644 my_agent/connections/oef/connection.yaml delete mode 100644 my_agent/default_private_key.pem delete mode 100644 my_agent/eth_private_key.txt delete mode 100644 my_agent/fet_private_key.txt delete mode 100644 my_agent/protocols/__init__.py delete mode 100644 my_agent/protocols/gym/__init__.py delete mode 100644 my_agent/protocols/gym/message.py delete mode 100644 my_agent/protocols/gym/protocol.yaml delete mode 100644 my_agent/protocols/gym/serialization.py delete mode 100644 my_agent/protocols/oef/__init__.py delete mode 100644 my_agent/protocols/oef/message.py delete mode 100644 my_agent/protocols/oef/models.py delete mode 100644 my_agent/protocols/oef/protocol.yaml delete mode 100644 my_agent/protocols/oef/serialization.py delete mode 100644 my_agent/protocols/tac/__init__.py delete mode 100644 my_agent/protocols/tac/message.py delete mode 100644 my_agent/protocols/tac/protocol.yaml delete mode 100644 my_agent/protocols/tac/serialization.py delete mode 100644 my_agent/protocols/tac/tac.proto delete mode 100644 my_agent/protocols/tac/tac_pb2.py delete mode 100644 my_agent/skills/__init__.py delete mode 100644 my_agent/skills/error/__init__.py delete mode 100644 my_agent/skills/error/behaviours.py delete mode 100644 my_agent/skills/error/handlers.py delete mode 100644 my_agent/skills/error/skill.yaml delete mode 100644 my_agent/skills/error/tasks.py delete mode 100644 my_agent/skills/my_search/__init__.py delete mode 100644 my_agent/skills/my_search/behaviours.py delete mode 100644 my_agent/skills/my_search/handlers.py delete mode 100644 my_agent/skills/my_search/my_shared_class.py delete mode 100644 my_agent/skills/my_search/skill.yaml delete mode 100644 my_agent/skills/my_search/tasks.py diff --git a/Test/aea-config.yaml b/Test/aea-config.yaml deleted file mode 100644 index 902c8f8c45..0000000000 --- a/Test/aea-config.yaml +++ /dev/null @@ -1,21 +0,0 @@ -aea_version: 0.1.7 -agent_name: Test -authors: '' -connections: -- oef -default_connection: oef -description: '' -license: '' -logging_config: - disable_existing_loggers: false - version: 1 -private_key_paths: [] -protocols: -- default -- gym -- tac -registry_path: ../packages -skills: -- error -url: '' -version: v1 diff --git a/Test/connections/__init__.py b/Test/connections/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/Test/connections/oef/__init__.py b/Test/connections/oef/__init__.py deleted file mode 100644 index 21ee4d83df..0000000000 --- a/Test/connections/oef/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Implementation of the OEF connection.""" diff --git a/Test/connections/oef/connection.py b/Test/connections/oef/connection.py deleted file mode 100644 index 7f489b867f..0000000000 --- a/Test/connections/oef/connection.py +++ /dev/null @@ -1,604 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Extension to the OEF Python SDK.""" -import datetime -import logging -import pickle -from queue import Empty, Queue -from threading import Thread -from typing import List, Dict, Optional, cast - -import oef -from oef.agents import OEFAgent -from oef.core import AsyncioCore -from oef.messages import CFP_TYPES, PROPOSE_TYPES -from oef.query import ( - Query as OEFQuery, - ConstraintExpr as OEFConstraintExpr, - And as OEFAnd, - Or as OEFOr, - Not as OEFNot, - Constraint as OEFConstraint, - ConstraintType as OEFConstraintType, Eq, NotEq, Lt, LtEq, Gt, GtEq, Range, In, NotIn) -from oef.schema import Description as OEFDescription, DataModel as OEFDataModel, AttributeSchema as OEFAttribute - -from aea.configurations.base import ConnectionConfig -from aea.connections.base import Channel, Connection -from aea.mail.base import MailBox, Envelope -from aea.protocols.fipa.message import FIPAMessage -from aea.protocols.fipa.serialization import FIPASerializer -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.models import Description, Attribute, DataModel, Query, ConstraintExpr, And, Or, Not, Constraint, \ - ConstraintType, ConstraintTypes -from aea.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF - -logger = logging.getLogger(__name__) - - -STUB_MESSSAGE_ID = 0 -STUB_DIALOGUE_ID = 0 - - -class OEFObjectTranslator: - """Translate our OEF object to object of OEF SDK classes.""" - - @classmethod - def to_oef_description(cls, desc: Description) -> OEFDescription: - """From our description to OEF description.""" - oef_data_model = cls.to_oef_data_model(desc.data_model) if desc.data_model is not None else None - return OEFDescription(desc.values, oef_data_model) - - @classmethod - def to_oef_data_model(cls, data_model: DataModel) -> OEFDataModel: - """From our data model to OEF data model.""" - oef_attributes = [cls.to_oef_attribute(attribute) for attribute in data_model.attributes] - return OEFDataModel(data_model.name, oef_attributes, data_model.description) - - @classmethod - def to_oef_attribute(cls, attribute: Attribute) -> OEFAttribute: - """From our attribute to OEF attribute.""" - return OEFAttribute(attribute.name, attribute.type, attribute.is_required, attribute.description) - - @classmethod - def to_oef_query(cls, query: Query) -> OEFQuery: - """From our query to OEF query.""" - oef_data_model = cls.to_oef_data_model(query.model) if query.model is not None else None - constraints = [cls.to_oef_constraint_expr(c) for c in query.constraints] - return OEFQuery(constraints, oef_data_model) - - @classmethod - def to_oef_constraint_expr(cls, constraint_expr: ConstraintExpr) -> OEFConstraintExpr: - """From our constraint expression to the OEF constraint expression.""" - if isinstance(constraint_expr, And): - return OEFAnd([cls.to_oef_constraint_expr(c) for c in constraint_expr.constraints]) - elif isinstance(constraint_expr, Or): - return OEFOr([cls.to_oef_constraint_expr(c) for c in constraint_expr.constraints]) - elif isinstance(constraint_expr, Not): - return OEFNot(cls.to_oef_constraint_expr(constraint_expr.constraint)) - elif isinstance(constraint_expr, Constraint): - oef_constraint_type = cls.to_oef_constraint_type(constraint_expr.constraint_type) - return OEFConstraint(constraint_expr.attribute_name, oef_constraint_type) - else: - raise ValueError("Constraint expression not supported.") - - @classmethod - def to_oef_constraint_type(cls, constraint_type: ConstraintType) -> OEFConstraintType: - """From our constraint type to OEF constraint type.""" - value = constraint_type.value - if constraint_type.type == ConstraintTypes.EQUAL: - return Eq(value) - elif constraint_type.type == ConstraintTypes.NOT_EQUAL: - return NotEq(value) - elif constraint_type.type == ConstraintTypes.LESS_THAN: - return Lt(value) - elif constraint_type.type == ConstraintTypes.LESS_THAN_EQ: - return LtEq(value) - elif constraint_type.type == ConstraintTypes.GREATER_THAN: - return Gt(value) - elif constraint_type.type == ConstraintTypes.GREATER_THAN_EQ: - return GtEq(value) - elif constraint_type.type == ConstraintTypes.WITHIN: - return Range(value) - elif constraint_type.type == ConstraintTypes.IN: - return In(value) - elif constraint_type.type == ConstraintTypes.NOT_IN: - return NotIn(value) - else: - raise ValueError("Constraint type not recognized.") - - @classmethod - def from_oef_description(cls, oef_desc: OEFDescription) -> Description: - """From an OEF description to our description.""" - data_model = cls.from_oef_data_model(oef_desc.data_model) if oef_desc.data_model is not None else None - return Description(oef_desc.values, data_model=data_model) - - @classmethod - def from_oef_data_model(cls, oef_data_model: OEFDataModel) -> DataModel: - """From an OEF data model to our data model.""" - attributes = [cls.from_oef_attribute(oef_attribute) for oef_attribute in oef_data_model.attribute_schemas] - return DataModel(oef_data_model.name, attributes, oef_data_model.description) - - @classmethod - def from_oef_attribute(cls, oef_attribute: OEFAttribute) -> Attribute: - """From an OEF attribute to our attribute.""" - return Attribute(oef_attribute.name, oef_attribute.type, oef_attribute.required, oef_attribute.description) - - @classmethod - def from_oef_query(cls, oef_query: OEFQuery) -> Query: - """From our query to OrOEF query.""" - data_model = cls.from_oef_data_model(oef_query.model) if oef_query.model is not None else None - constraints = [cls.from_oef_constraint_expr(c) for c in oef_query.constraints] - return Query(constraints, data_model) - - @classmethod - def from_oef_constraint_expr(cls, oef_constraint_expr: OEFConstraintExpr) -> ConstraintExpr: - """From our query to OEF query.""" - if isinstance(oef_constraint_expr, OEFAnd): - return And([cls.from_oef_constraint_expr(c) for c in oef_constraint_expr.constraints]) - elif isinstance(oef_constraint_expr, OEFOr): - return Or([cls.from_oef_constraint_expr(c) for c in oef_constraint_expr.constraints]) - elif isinstance(oef_constraint_expr, OEFNot): - return Not(cls.from_oef_constraint_expr(oef_constraint_expr.constraint)) - elif isinstance(oef_constraint_expr, OEFConstraint): - constraint_type = cls.from_oef_constraint_type(oef_constraint_expr.constraint) - return Constraint(oef_constraint_expr.attribute_name, constraint_type) - else: - raise ValueError("OEF Constraint not supported.") - - @classmethod - def from_oef_constraint_type(cls, constraint_type: OEFConstraintType) -> ConstraintType: - """From OEF constraint type to our constraint type.""" - if isinstance(constraint_type, Eq): - return ConstraintType(ConstraintTypes.EQUAL, constraint_type.value) - elif isinstance(constraint_type, NotEq): - return ConstraintType(ConstraintTypes.NOT_EQUAL, constraint_type.value) - elif isinstance(constraint_type, Lt): - return ConstraintType(ConstraintTypes.LESS_THAN, constraint_type.value) - elif isinstance(constraint_type, LtEq): - return ConstraintType(ConstraintTypes.LESS_THAN_EQ, constraint_type.value) - elif isinstance(constraint_type, Gt): - return ConstraintType(ConstraintTypes.GREATER_THAN, constraint_type.value) - elif isinstance(constraint_type, GtEq): - return ConstraintType(ConstraintTypes.GREATER_THAN_EQ, constraint_type.value) - elif isinstance(constraint_type, Range): - return ConstraintType(ConstraintTypes.WITHIN, constraint_type.values) - elif isinstance(constraint_type, In): - return ConstraintType(ConstraintTypes.IN, constraint_type.values) - elif isinstance(constraint_type, NotIn): - return ConstraintType(ConstraintTypes.NOT_IN, constraint_type.values) - else: - raise ValueError("Constraint type not recognized.") - - -class MailStats(object): - """The MailStats class tracks statistics on messages processed by MailBox.""" - - def __init__(self) -> None: - """ - Instantiate mail stats. - - :return: None - """ - self._search_count = 0 - self._search_start_time = {} # type: Dict[int, datetime.datetime] - self._search_timedelta = {} # type: Dict[int, float] - self._search_result_counts = {} # type: Dict[int, int] - - @property - def search_count(self) -> int: - """Get the search count.""" - return self._search_count - - def search_start(self, search_id: int) -> None: - """ - Add a search id and start time. - - :param search_id: the search id - - :return: None - """ - assert search_id not in self._search_start_time - self._search_count += 1 - self._search_start_time[search_id] = datetime.datetime.now() - - def search_end(self, search_id: int, nb_search_results: int) -> None: - """ - Add end time for a search id. - - :param search_id: the search id - :param nb_search_results: the number of agents returned in the search result - - :return: None - """ - assert search_id in self._search_start_time - assert search_id not in self._search_timedelta - self._search_timedelta[search_id] = (datetime.datetime.now() - self._search_start_time[search_id]).total_seconds() * 1000 - self._search_result_counts[search_id] = nb_search_results - - -class OEFChannel(OEFAgent, Channel): - """The OEFChannel connects the OEF Agent with the connection.""" - - def __init__(self, public_key: str, oef_addr: str, oef_port: int, core: AsyncioCore, in_queue: Queue): - """ - Initialize. - - :param public_key: the public key of the agent. - :param oef_addr: the OEF IP address. - :param oef_port: the OEF port. - :param in_queue: the in queue. - """ - super().__init__(public_key, oef_addr=oef_addr, oef_port=oef_port, core=core) - self.in_queue = in_queue - self.mail_stats = MailStats() - - def on_message(self, msg_id: int, dialogue_id: int, origin: str, content: bytes) -> None: - """ - On message event handler. - - :param msg_id: the message id. - :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. - :param content: the bytes content. - :return: None - """ - # We are not using the 'origin' parameter because 'content' contains a serialized instance of 'Envelope', - # hence it already contains the address of the sender. - envelope = Envelope.decode(content) - self.in_queue.put(envelope) - - def on_cfp(self, msg_id: int, dialogue_id: int, origin: str, target: int, query: CFP_TYPES) -> None: - """ - On cfp event handler. - - :param msg_id: the message id. - :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. - :param target: the message target. - :param query: the query. - :return: None - """ - try: - query = pickle.loads(query) - except Exception: - pass - msg = FIPAMessage(message_id=msg_id, - dialogue_id=dialogue_id, - target=target, - performative=FIPAMessage.Performative.CFP, - query=query if query != b"" else None) - msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_propose(self, msg_id: int, dialogue_id: int, origin: str, target: int, b_proposals: PROPOSE_TYPES) -> None: - """ - On propose event handler. - - :param msg_id: the message id. - :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. - :param target: the message target. - :param b_proposals: the proposals. - :return: None - """ - if type(b_proposals) == bytes: - proposals = pickle.loads(b_proposals) # type: List[Description] - else: - raise ValueError("No support for non-bytes proposals.") - - msg = FIPAMessage(message_id=msg_id, - dialogue_id=dialogue_id, - target=target, - performative=FIPAMessage.Performative.PROPOSE, - proposal=proposals) - msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_accept(self, msg_id: int, dialogue_id: int, origin: str, target: int) -> None: - """ - On accept event handler. - - :param msg_id: the message id. - :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. - :param target: the message target. - :return: None - """ - performative = FIPAMessage.Performative.MATCH_ACCEPT if msg_id == 4 and target == 3 else FIPAMessage.Performative.ACCEPT - msg = FIPAMessage(message_id=msg_id, - dialogue_id=dialogue_id, - target=target, - performative=performative) - msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_decline(self, msg_id: int, dialogue_id: int, origin: str, target: int) -> None: - """ - On decline event handler. - - :param msg_id: the message id. - :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. - :param target: the message target. - :return: None - """ - msg = FIPAMessage(message_id=msg_id, - dialogue_id=dialogue_id, - target=target, - performative=FIPAMessage.Performative.DECLINE) - msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_search_result(self, search_id: int, agents: List[str]) -> None: - """ - On accept event handler. - - :param search_id: the search id. - :param agents: the list of agents. - :return: None - """ - self.mail_stats.search_end(search_id, len(agents)) - msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_RESULT, id=search_id, agents=agents) - msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_oef_error(self, answer_id: int, operation: oef.messages.OEFErrorOperation) -> None: - """ - On oef error event handler. - - :param answer_id: the answer id. - :param operation: the error operation. - :return: None - """ - try: - operation = OEFMessage.OEFErrorOperation(operation) - except ValueError: - operation = OEFMessage.OEFErrorOperation.OTHER - - msg = OEFMessage(oef_type=OEFMessage.Type.OEF_ERROR, id=answer_id, operation=operation) - msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_dialogue_error(self, answer_id: int, dialogue_id: int, origin: str) -> None: - """ - On dialogue error event handler. - - :param answer_id: the answer id. - :param dialogue_id: the dialogue id. - :param origin: the message sender. - :return: None - """ - msg = OEFMessage(oef_type=OEFMessage.Type.DIALOGUE_ERROR, - id=answer_id, - dialogue_id=dialogue_id, - origin=origin) - msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def send(self, envelope: Envelope) -> None: - """ - Send message handler. - - :param envelope: the message. - :return: None - """ - if envelope.protocol_id == "default": - self.send_default_message(envelope) - elif envelope.protocol_id == "fipa": - self.send_fipa_message(envelope) - elif envelope.protocol_id == "oef": - self.send_oef_message(envelope) - elif envelope.protocol_id == "tac": - self.send_default_message(envelope) - else: - logger.error("This envelope cannot be sent: protocol_id={}".format(envelope.protocol_id)) - raise ValueError("Cannot send message.") - - def send_default_message(self, envelope: Envelope): - """Send a 'default' message.""" - self.send_message(STUB_MESSSAGE_ID, STUB_DIALOGUE_ID, envelope.to, envelope.encode()) - - def send_fipa_message(self, envelope: Envelope) -> None: - """ - Send fipa message handler. - - :param envelope: the message. - :return: None - """ - fipa_message = FIPASerializer().decode(envelope.message) - id = fipa_message.get("message_id") - dialogue_id = fipa_message.get("dialogue_id") - destination = envelope.to - target = fipa_message.get("target") - performative = FIPAMessage.Performative(fipa_message.get("performative")) - if performative == FIPAMessage.Performative.CFP: - query = fipa_message.get("query") - query = b"" if query is None else query - if type(query) == Query: - query = pickle.dumps(query) - self.send_cfp(id, dialogue_id, destination, target, query) - elif performative == FIPAMessage.Performative.PROPOSE: - proposal = cast(List[Description], fipa_message.get("proposal")) - proposal_b = pickle.dumps(proposal) # type: bytes - self.send_propose(id, dialogue_id, destination, target, proposal_b) - elif performative == FIPAMessage.Performative.ACCEPT: - self.send_accept(id, dialogue_id, destination, target) - elif performative == FIPAMessage.Performative.MATCH_ACCEPT: - self.send_accept(id, dialogue_id, destination, target) - elif performative == FIPAMessage.Performative.DECLINE: - self.send_decline(id, dialogue_id, destination, target) - else: - raise ValueError("OEF FIPA message not recognized.") - - def send_oef_message(self, envelope: Envelope) -> None: - """ - Send oef message handler. - - :param envelope: the message. - :return: None - """ - oef_message = OEFSerializer().decode(envelope.message) - oef_type = OEFMessage.Type(oef_message.get("type")) - oef_msg_id = cast(int, oef_message.get("id")) - if oef_type == OEFMessage.Type.REGISTER_SERVICE: - service_description = cast(Description, oef_message.get("service_description")) - service_id = cast(int, oef_message.get("service_id")) - oef_service_description = OEFObjectTranslator.to_oef_description(service_description) - self.register_service(oef_msg_id, oef_service_description, service_id) - elif oef_type == OEFMessage.Type.UNREGISTER_SERVICE: - service_description = cast(Description, oef_message.get("service_description")) - service_id = cast(int, oef_message.get("service_id")) - oef_service_description = OEFObjectTranslator.to_oef_description(service_description) - self.unregister_service(oef_msg_id, oef_service_description, service_id) - elif oef_type == OEFMessage.Type.SEARCH_AGENTS: - query = cast(Query, oef_message.get("query")) - oef_query = OEFObjectTranslator.to_oef_query(query) - self.search_agents(oef_msg_id, oef_query) - elif oef_type == OEFMessage.Type.SEARCH_SERVICES: - query = cast(Query, oef_message.get("query")) - oef_query = OEFObjectTranslator.to_oef_query(query) - self.mail_stats.search_start(oef_msg_id) - self.search_services(oef_msg_id, oef_query) - else: - raise ValueError("OEF request not recognized.") - - -class OEFConnection(Connection): - """The OEFConnection connects the to the mailbox.""" - - def __init__(self, public_key: str, oef_addr: str, oef_port: int = 10000): - """ - Initialize. - - :param public_key: the public key of the agent. - :param oef_addr: the OEF IP address. - :param oef_port: the OEF port. - """ - super().__init__() - core = AsyncioCore(logger=logger) - self._core = core # type: AsyncioCore - self.channel = OEFChannel(public_key, oef_addr, oef_port, core=core, in_queue=self.in_queue) - - self._stopped = True - self._connected = False - self.out_thread = None # type: Optional[Thread] - - @property - def is_established(self) -> bool: - """Get the connection status.""" - return self._connected - - def _fetch(self) -> None: - """ - Fetch the messages from the outqueue and send them. - - :return: None - """ - while self._connected: - try: - msg = self.out_queue.get(block=True, timeout=1.0) - self.send(msg) - except Empty: - pass - - def connect(self) -> None: - """ - Connect to the channel. - - :return: None - :raises ConnectionError if the connection to the OEF fails. - """ - if self._stopped and not self._connected: - self._stopped = False - self._core.run_threaded() - try: - if not self.channel.connect(): - raise ConnectionError("Cannot connect to OEFChannel.") - self._connected = True - self.out_thread = Thread(target=self._fetch) - self.out_thread.start() - except ConnectionError as e: - self._core.stop() - raise e - - def disconnect(self) -> None: - """ - Disconnect from the channel. - - :return: None - """ - assert self.out_thread is not None, "Call connect before disconnect." - if not self._stopped and self._connected: - self._connected = False - self.out_thread.join() - self.out_thread = None - self.channel.disconnect() - self._core.stop() - self._stopped = True - - def send(self, envelope: Envelope): - """ - Send messages. - - :return: None - """ - if self._connected: - self.channel.send(envelope) - - @classmethod - def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection': - """ - Get the OEF connection from the connection configuration. - - :param public_key: the public key of the agent. - :param connection_configuration: the connection configuration object. - :return: the connection object - """ - oef_addr = cast(str, connection_configuration.config.get("addr")) - oef_port = cast(int, connection_configuration.config.get("port")) - return OEFConnection(public_key, oef_addr, oef_port) - - -class OEFMailBox(MailBox): - """The OEF mail box.""" - - def __init__(self, public_key: str, oef_addr: str, oef_port: int = 10000): - """ - Initialize. - - :param public_key: the public key of the agent. - :param oef_addr: the OEF IP address. - :param oef_port: the OEF port. - """ - connection = OEFConnection(public_key, oef_addr, oef_port) - super().__init__(connection) - - @property - def mail_stats(self) -> MailStats: - """Get the mail stats object.""" - return self._connection.channel.mail_stats # type: ignore diff --git a/Test/connections/oef/connection.yaml b/Test/connections/oef/connection.yaml deleted file mode 100644 index 6a69ed33d1..0000000000 --- a/Test/connections/oef/connection.yaml +++ /dev/null @@ -1,18 +0,0 @@ -name: oef -authors: Fetch.AI Limited -version: 0.1.0 -license: Apache 2.0 -url: "" -description: "The oef connection provides a wrapper around the OEF sdk." -class_name: OEFConnection -supported_protocols: - - default - - oef - - fipa - - tac -config: - addr: ${OEF_ADDR:127.0.0.1} - port: ${OEF_PORT:10000} -dependencies: - - colorlog - - oef diff --git a/Test/protocols/__init__.py b/Test/protocols/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/Test/protocols/default/__init__.py b/Test/protocols/default/__init__.py deleted file mode 100644 index 52e51b51e3..0000000000 --- a/Test/protocols/default/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the support resources for the default protocol.""" diff --git a/Test/protocols/default/message.py b/Test/protocols/default/message.py deleted file mode 100644 index 475714a2f0..0000000000 --- a/Test/protocols/default/message.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the default message definition.""" -from enum import Enum -from typing import Optional - -from aea.protocols.base import Message - - -class DefaultMessage(Message): - """The Default message class.""" - - protocol_id = "default" - - class Type(Enum): - """Default message types.""" - - BYTES = "bytes" - ERROR = "error" - - def __str__(self): - """Get the string representation.""" - return self.value - - class ErrorCode(Enum): - """The error codes.""" - - UNSUPPORTED_PROTOCOL = -10001 - DECODING_ERROR = -10002 - INVALID_MESSAGE = -10003 - UNSUPPORTED_SKILL = -10004 - - def __init__(self, type: Optional[Type] = None, - **kwargs): - """ - Initialize. - - :param type: the type. - """ - super().__init__(type=type, **kwargs) - assert self.check_consistency(), "DefaultMessage initialization inconsistent." diff --git a/Test/protocols/default/protocol.yaml b/Test/protocols/default/protocol.yaml deleted file mode 100644 index 66d97cfc83..0000000000 --- a/Test/protocols/default/protocol.yaml +++ /dev/null @@ -1,6 +0,0 @@ -name: 'default' -authors: Fetch.AI Limited -version: 0.1.0 -license: Apache 2.0 -url: "" -description: "The default protocol allows for any bytes message." \ No newline at end of file diff --git a/Test/protocols/default/serialization.py b/Test/protocols/default/serialization.py deleted file mode 100644 index 080b8f386b..0000000000 --- a/Test/protocols/default/serialization.py +++ /dev/null @@ -1,71 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Serialization module for the default protocol.""" -import base64 -import json -from typing import cast - -from aea.protocols.base import Message -from aea.protocols.base import Serializer -from aea.protocols.default.message import DefaultMessage - - -class DefaultSerializer(Serializer): - """Serialization for the 'default' protocol.""" - - def encode(self, msg: Message) -> bytes: - """Encode a 'default' message into bytes.""" - body = {} # Dict[str, Any] - - msg_type = DefaultMessage.Type(msg.get("type")) - body["type"] = str(msg_type.value) - - if msg_type == DefaultMessage.Type.BYTES: - content = cast(bytes, msg.get("content")) - body["content"] = base64.b64encode(content).decode("utf-8") - elif msg_type == DefaultMessage.Type.ERROR: - body["error_code"] = cast(str, msg.get("error_code")) - body["error_msg"] = cast(str, msg.get("error_msg")) - body["error_data"] = cast(str, msg.get("error_data")) - else: - raise ValueError("Type not recognized.") - - bytes_msg = json.dumps(body).encode("utf-8") - return bytes_msg - - def decode(self, obj: bytes) -> Message: - """Decode bytes into a 'default' message.""" - json_body = json.loads(obj.decode("utf-8")) - body = {} - - msg_type = DefaultMessage.Type(json_body["type"]) - body["type"] = msg_type - if msg_type == DefaultMessage.Type.BYTES: - content = base64.b64decode(json_body["content"].encode("utf-8")) - body["content"] = content # type: ignore - elif msg_type == DefaultMessage.Type.ERROR: - body["error_code"] = json_body["error_code"] - body["error_msg"] = json_body["error_msg"] - body["error_data"] = json_body["error_data"] - else: - raise ValueError("Type not recognized.") - - return DefaultMessage(type=msg_type, body=body) diff --git a/Test/protocols/gym/__init__.py b/Test/protocols/gym/__init__.py deleted file mode 100644 index a1766ab9cd..0000000000 --- a/Test/protocols/gym/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the support resources for the Gym protocol.""" diff --git a/Test/protocols/gym/message.py b/Test/protocols/gym/message.py deleted file mode 100644 index 616b3e8226..0000000000 --- a/Test/protocols/gym/message.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the FIPA message definition.""" -from enum import Enum -from typing import Optional, Union - -from aea.protocols.base import Message - - -class GymMessage(Message): - """The Gym message class.""" - - protocol_id = "gym" - - class Performative(Enum): - """Gym performatives.""" - - ACT = 'act' - PERCEPT = 'percept' - RESET = 'reset' - CLOSE = 'close' - - def __str__(self): - """Get string representation.""" - return self.value - - def __init__(self, performative: Optional[Union[str, Performative]] = None, **kwargs): - """ - Initialize. - - :param type: the type. - """ - super().__init__(performative=GymMessage.Performative(performative), **kwargs) - assert self.check_consistency(), "GymMessage initialization inconsistent." - - def check_consistency(self) -> bool: - """Check that the data is consistent.""" - try: - assert self.is_set("performative") - performative = GymMessage.Performative(self.get("performative")) - if performative == GymMessage.Performative.ACT: - assert self.is_set("action") - assert self.is_set("step_id") - elif performative == GymMessage.Performative.PERCEPT: - assert self.is_set("observation") - assert self.is_set("reward") - assert self.is_set("done") - assert self.is_set("info") - assert self.is_set("step_id") - elif performative == GymMessage.Performative.RESET or performative == GymMessage.Performative.CLOSE: - pass - else: - raise ValueError("Performative not recognized.") - - except (AssertionError, ValueError, KeyError): - return False - - return True diff --git a/Test/protocols/gym/protocol.yaml b/Test/protocols/gym/protocol.yaml deleted file mode 100644 index 7b10d5429b..0000000000 --- a/Test/protocols/gym/protocol.yaml +++ /dev/null @@ -1,6 +0,0 @@ -name: gym -authors: Fetch.AI Limited -version: 0.1.0 -license: Apache 2.0 -url: "" -description: "The gym protocol implements the messages an agent needs to engage with a gym connection." \ No newline at end of file diff --git a/Test/protocols/gym/serialization.py b/Test/protocols/gym/serialization.py deleted file mode 100644 index 6fb14c1354..0000000000 --- a/Test/protocols/gym/serialization.py +++ /dev/null @@ -1,103 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Serialization for the FIPA protocol.""" -import base64 -import copy -import json -import pickle -from typing import Any, TYPE_CHECKING - -from aea.protocols.base import Message -from aea.protocols.base import Serializer - -if TYPE_CHECKING: - from packages.protocols.gym.message import GymMessage -else: - from gym_protocol.message import GymMessage - - -class GymSerializer(Serializer): - """Serialization for the Gym protocol.""" - - def encode(self, msg: Message) -> bytes: - """ - Decode the message. - - :param msg: the message object - :return: the bytes - """ - performative = GymMessage.Performative(msg.get("performative")) - new_body = copy.copy(msg.body) - new_body["performative"] = performative.value - - if performative == GymMessage.Performative.ACT: - action = msg.body["action"] # type: Any - action_bytes = base64.b64encode(pickle.dumps(action)).decode("utf-8") - new_body["action"] = action_bytes - new_body["step_id"] = msg.body["step_id"] - elif performative == GymMessage.Performative.PERCEPT: - # observation, reward and info are gym implementation specific, done is boolean - observation = msg.body["observation"] # type: Any - observation_bytes = base64.b64encode(pickle.dumps(observation)).decode("utf-8") - new_body["observation"] = observation_bytes - reward = msg.body["reward"] # type: Any - reward_bytes = base64.b64encode(pickle.dumps(reward)).decode("utf-8") - new_body["reward"] = reward_bytes - info = msg.body["info"] # type: Any - info_bytes = base64.b64encode(pickle.dumps(info)).decode("utf-8") - new_body["info"] = info_bytes - new_body["step_id"] = msg.body["step_id"] - - gym_message_bytes = json.dumps(new_body).encode("utf-8") - return gym_message_bytes - - def decode(self, obj: bytes) -> Message: - """ - Decode the message. - - :param obj: the bytes object - :return: the message - """ - json_msg = json.loads(obj.decode("utf-8")) - performative = GymMessage.Performative(json_msg["performative"]) - new_body = copy.copy(json_msg) - new_body["type"] = performative - - if performative == GymMessage.Performative.ACT: - action_bytes = base64.b64decode(json_msg["action"]) - action = pickle.loads(action_bytes) - new_body["action"] = action - new_body["step_id"] = json_msg["step_id"] - elif performative == GymMessage.Performative.PERCEPT: - # observation, reward and info are gym implementation specific, done is boolean - observation_bytes = base64.b64decode(json_msg["observation"]) - observation = pickle.loads(observation_bytes) - new_body["observation"] = observation - reward_bytes = base64.b64decode(json_msg["reward"]) - reward = pickle.loads(reward_bytes) - new_body["reward"] = reward - info_bytes = base64.b64decode(json_msg["info"]) - info = pickle.loads(info_bytes) - new_body["info"] = info - new_body["step_id"] = json_msg["step_id"] - - gym_message = GymMessage(performative=performative, body=new_body) - return gym_message diff --git a/Test/protocols/tac/__init__.py b/Test/protocols/tac/__init__.py deleted file mode 100644 index 430d160a4f..0000000000 --- a/Test/protocols/tac/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the support resources for the TAC protocol.""" diff --git a/Test/protocols/tac/message.py b/Test/protocols/tac/message.py deleted file mode 100644 index 07a100d90e..0000000000 --- a/Test/protocols/tac/message.py +++ /dev/null @@ -1,134 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the default message definition.""" -from enum import Enum -from typing import Dict, Optional, cast - -from aea.protocols.base import Message - - -class TACMessage(Message): - """The TAC message class.""" - - protocol_id = "tac" - - class Type(Enum): - """TAC Message types.""" - - REGISTER = "register" - UNREGISTER = "unregister" - TRANSACTION = "transaction" - GET_STATE_UPDATE = "get_state_update" - CANCELLED = "cancelled" - GAME_DATA = "game_data" - TRANSACTION_CONFIRMATION = "transaction_confirmation" - STATE_UPDATE = "state_update" - TAC_ERROR = "tac_error" - - def __str__(self): - """Get string representation.""" - return self.value - - class ErrorCode(Enum): - """This class defines the error codes.""" - - GENERIC_ERROR = 0 - REQUEST_NOT_VALID = 1 - AGENT_PBK_ALREADY_REGISTERED = 2 - AGENT_NAME_ALREADY_REGISTERED = 3 - AGENT_NOT_REGISTERED = 4 - TRANSACTION_NOT_VALID = 5 - TRANSACTION_NOT_MATCHING = 6 - AGENT_NAME_NOT_IN_WHITELIST = 7 - COMPETITION_NOT_RUNNING = 8 - DIALOGUE_INCONSISTENT = 9 - - _from_ec_to_msg = { - ErrorCode.GENERIC_ERROR: "Unexpected error.", - ErrorCode.REQUEST_NOT_VALID: "Request not recognized", - ErrorCode.AGENT_PBK_ALREADY_REGISTERED: "Agent pbk already registered.", - ErrorCode.AGENT_NAME_ALREADY_REGISTERED: "Agent name already registered.", - ErrorCode.AGENT_NOT_REGISTERED: "Agent not registered.", - ErrorCode.TRANSACTION_NOT_VALID: "Error in checking transaction", - ErrorCode.TRANSACTION_NOT_MATCHING: "The transaction request does not match with a previous transaction request with the same id.", - ErrorCode.AGENT_NAME_NOT_IN_WHITELIST: "Agent name not in whitelist.", - ErrorCode.COMPETITION_NOT_RUNNING: "The competition is not running yet.", - ErrorCode.DIALOGUE_INCONSISTENT: "The message is inconsistent with the dialogue." - } # type: Dict[ErrorCode, str] - - def __init__(self, tac_type: Optional[Type] = None, - **kwargs): - """ - Initialize. - - :param tac_type: the type of TAC message. - """ - super().__init__(type=tac_type, **kwargs) - assert self.check_consistency(), "TACMessage initialization inconsistent." - - def check_consistency(self) -> bool: - """Check that the data is consistent.""" - try: - assert self.is_set("type") - tac_type = TACMessage.Type(self.get("type")) - if tac_type == TACMessage.Type.REGISTER: - assert self.is_set("agent_name") - elif tac_type == TACMessage.Type.UNREGISTER: - pass - elif tac_type == TACMessage.Type.TRANSACTION: - assert self.is_set("transaction_id") - assert self.is_set("is_sender_buyer") - assert self.is_set("counterparty") - assert self.is_set("amount") - amount = cast(float, self.get("amount")) - assert amount >= 0.0 - assert self.is_set("quantities_by_good_pbk") - quantities_by_good_pbk = cast(Dict[str, int], self.get("quantities_by_good_pbk")) - assert len(quantities_by_good_pbk.keys()) == len(set(quantities_by_good_pbk.keys())) - assert all(quantity >= 0 for quantity in quantities_by_good_pbk.values()) - elif tac_type == TACMessage.Type.GET_STATE_UPDATE: - pass - elif tac_type == TACMessage.Type.CANCELLED: - pass - elif tac_type == TACMessage.Type.GAME_DATA: - assert self.is_set("money") - assert self.is_set("endowment") - assert self.is_set("utility_params") - assert self.is_set("nb_agents") - assert self.is_set("nb_goods") - assert self.is_set("tx_fee") - assert self.is_set("agent_pbk_to_name") - assert self.is_set("good_pbk_to_name") - elif tac_type == TACMessage.Type.TRANSACTION_CONFIRMATION: - assert self.is_set("transaction_id") - elif tac_type == TACMessage.Type.STATE_UPDATE: - assert self.is_set("initial_state") - assert self.is_set("transactions") - elif tac_type == TACMessage.Type.TAC_ERROR: - assert self.is_set("error_code") - error_code = self.get("error_code") - assert error_code in set(self.ErrorCode) - else: - raise ValueError("Type not recognized.") - except (AssertionError, ValueError): - return False - - return True diff --git a/Test/protocols/tac/protocol.yaml b/Test/protocols/tac/protocol.yaml deleted file mode 100644 index 435e33b383..0000000000 --- a/Test/protocols/tac/protocol.yaml +++ /dev/null @@ -1,6 +0,0 @@ -name: tac -authors: Fetch.AI Limited -version: 0.1.0 -license: Apache 2.0 -url: "" -description: "The tac protocol implements the messages an AEA needs to participate in the TAC." \ No newline at end of file diff --git a/Test/protocols/tac/serialization.py b/Test/protocols/tac/serialization.py deleted file mode 100644 index e4e085d5b0..0000000000 --- a/Test/protocols/tac/serialization.py +++ /dev/null @@ -1,234 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Serialization for the TAC protocol.""" - -from typing import Any, Dict, List, cast, TYPE_CHECKING - -from aea.protocols.base import Message -from aea.protocols.base import Serializer - -if TYPE_CHECKING: - from packages.protocols.tac import tac_pb2 - from packages.protocols.tac.message import TACMessage -else: - import tac_protocol.tac_pb2 as tac_pb2 - from tac_protocol.message import TACMessage - - -def _from_dict_to_pairs(d): - """Convert a flat dictionary into a list of StrStrPair or StrIntPair.""" - result = [] - items = sorted(d.items(), key=lambda pair: pair[0]) - for key, value in items: - if type(value) == int: - pair = tac_pb2.StrIntPair() - elif type(value) == str: - pair = tac_pb2.StrStrPair() - else: - raise ValueError("Either 'int' or 'str', not {}".format(type(value))) - pair.first = key - pair.second = value - result.append(pair) - return result - - -def _from_pairs_to_dict(pairs): - """Convert a list of StrStrPair or StrIntPair into a flat dictionary.""" - result = {} - for pair in pairs: - key = pair.first - value = pair.second - result[key] = value - return result - - -class TACSerializer(Serializer): - """Serialization for the TAC protocol.""" - - def encode(self, msg: Message) -> bytes: - """ - Decode the message. - - :param msg: the message object - :return: the bytes - """ - tac_type = TACMessage.Type(msg.get("type")) - tac_container = tac_pb2.TACMessage() - - if tac_type == TACMessage.Type.REGISTER: - agent_name = msg.get("agent_name") - tac_msg = tac_pb2.TACAgent.Register() # type: ignore - tac_msg.agent_name = agent_name - tac_container.register.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.UNREGISTER: - tac_msg = tac_pb2.TACAgent.Unregister() # type: ignore - tac_container.unregister.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.TRANSACTION: - tac_msg = tac_pb2.TACAgent.Transaction() # type: ignore - tac_msg.transaction_id = msg.get("transaction_id") - tac_msg.is_sender_buyer = msg.get("is_sender_buyer") - tac_msg.counterparty = msg.get("counterparty") - tac_msg.amount = msg.get("amount") - tac_msg.quantities.extend(_from_dict_to_pairs(msg.get("quantities_by_good_pbk"))) - tac_container.transaction.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.GET_STATE_UPDATE: - tac_msg = tac_pb2.TACAgent.GetStateUpdate() # type: ignore - tac_container.get_state_update.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.CANCELLED: - tac_msg = tac_pb2.TACController.Cancelled() # type: ignore - tac_container.cancelled.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.GAME_DATA: - tac_msg = tac_pb2.TACController.GameData() # type: ignore - tac_msg.money = msg.get("money") - tac_msg.endowment.extend(msg.get("endowment")) - tac_msg.utility_params.extend(msg.get("utility_params")) - tac_msg.nb_agents = msg.get("nb_agents") - tac_msg.nb_goods = msg.get("nb_goods") - tac_msg.tx_fee = msg.get("tx_fee") - tac_msg.agent_pbk_to_name.extend(_from_dict_to_pairs(msg.get("agent_pbk_to_name"))) - tac_msg.good_pbk_to_name.extend(_from_dict_to_pairs(msg.get("good_pbk_to_name"))) - tac_container.game_data.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.TRANSACTION_CONFIRMATION: - tac_msg = tac_pb2.TACController.TransactionConfirmation() # type: ignore - tac_msg.transaction_id = msg.get("transaction_id") - tac_container.transaction_confirmation.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.STATE_UPDATE: - tac_msg = tac_pb2.TACController.StateUpdate() # type: ignore - game_data_json = msg.get("initial_state") - game_data = tac_pb2.TACController.GameData() # type: ignore - game_data.money = game_data_json["money"] # type: ignore - game_data.endowment.extend(game_data_json["endowment"]) # type: ignore - game_data.utility_params.extend(game_data_json["utility_params"]) # type: ignore - game_data.nb_agents = game_data_json["nb_agents"] # type: ignore - game_data.nb_goods = game_data_json["nb_goods"] # type: ignore - game_data.tx_fee = game_data_json["tx_fee"] # type: ignore - game_data.agent_pbk_to_name.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["agent_pbk_to_name"]))) # type: ignore - game_data.good_pbk_to_name.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["good_pbk_to_name"]))) # type: ignore - - tac_msg.initial_state.CopyFrom(game_data) - - transactions = [] - msg_transactions = cast(List[Any], msg.get("transactions")) - for t in msg_transactions: - tx = tac_pb2.TACAgent.Transaction() # type: ignore - tx.transaction_id = t.get("transaction_id") - tx.is_sender_buyer = t.get("is_sender_buyer") - tx.counterparty = t.get("counterparty") - tx.amount = t.get("amount") - tx.quantities.extend(_from_dict_to_pairs(t.get("quantities_by_good_pbk"))) - transactions.append(tx) - tac_msg.txs.extend(transactions) - tac_container.state_update.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.TAC_ERROR: - tac_msg = tac_pb2.TACController.Error() # type: ignore - tac_msg.error_code = TACMessage.ErrorCode(msg.get("error_code")).value - if msg.is_set("error_msg"): - tac_msg.error_msg = msg.get("error_msg") - if msg.is_set("details"): - tac_msg.details.update(msg.get("details")) - - tac_container.error.CopyFrom(tac_msg) - else: - raise ValueError("Type not recognized: {}.".format(tac_type)) - - tac_message_bytes = tac_container.SerializeToString() - return tac_message_bytes - - def decode(self, obj: bytes) -> Message: - """ - Decode the message. - - :param obj: the bytes object - :return: the message - """ - tac_container = tac_pb2.TACMessage() - tac_container.ParseFromString(obj) - - new_body = {} # type: Dict[str, Any] - tac_type = tac_container.WhichOneof("content") - - if tac_type == "register": - new_body["type"] = TACMessage.Type.REGISTER - new_body["agent_name"] = tac_container.register.agent_name - elif tac_type == "unregister": - new_body["type"] = TACMessage.Type.UNREGISTER - elif tac_type == "transaction": - new_body["type"] = TACMessage.Type.TRANSACTION - new_body["transaction_id"] = tac_container.transaction.transaction_id - new_body["is_sender_buyer"] = tac_container.transaction.is_sender_buyer - new_body["counterparty"] = tac_container.transaction.counterparty - new_body["amount"] = tac_container.transaction.amount - new_body["quantities_by_good_pbk"] = _from_pairs_to_dict(tac_container.transaction.quantities) - elif tac_type == "get_state_update": - new_body["type"] = TACMessage.Type.GET_STATE_UPDATE - elif tac_type == "cancelled": - new_body["type"] = TACMessage.Type.CANCELLED - elif tac_type == "game_data": - new_body["type"] = TACMessage.Type.GAME_DATA - new_body["money"] = tac_container.game_data.money - new_body["endowment"] = list(tac_container.game_data.endowment) - new_body["utility_params"] = list(tac_container.game_data.utility_params) - new_body["nb_agents"] = tac_container.game_data.nb_agents - new_body["nb_goods"] = tac_container.game_data.nb_goods - new_body["tx_fee"] = tac_container.game_data.tx_fee - new_body["agent_pbk_to_name"] = _from_pairs_to_dict(tac_container.game_data.agent_pbk_to_name) - new_body["good_pbk_to_name"] = _from_pairs_to_dict(tac_container.game_data.good_pbk_to_name) - elif tac_type == "transaction_confirmation": - new_body["type"] = TACMessage.Type.TRANSACTION_CONFIRMATION - new_body["transaction_id"] = tac_container.transaction_confirmation.transaction_id - elif tac_type == "state_update": - new_body["type"] = TACMessage.Type.STATE_UPDATE - game_data = dict( - money=tac_container.state_update.initial_state.money, - endowment=tac_container.state_update.initial_state.endowment, - utility_params=tac_container.state_update.initial_state.utility_params, - nb_agents=tac_container.state_update.initial_state.nb_agents, - nb_goods=tac_container.state_update.initial_state.nb_goods, - tx_fee=tac_container.state_update.initial_state.tx_fee, - agent_pbk_to_name=_from_pairs_to_dict(tac_container.state_update.initial_state.agent_pbk_to_name), - good_pbk_to_name=_from_pairs_to_dict(tac_container.state_update.initial_state.good_pbk_to_name), - ) - new_body["initial_state"] = game_data - transactions = [] - for t in tac_container.state_update.txs: - tx_json = dict( - transaction_id=t.transaction_id, - is_sender_buyer=t.is_sender_buyer, - counterparty=t.counterparty, - amount=t.amount, - quantities_by_good_pbk=_from_pairs_to_dict(t.quantities), - ) - transactions.append(tx_json) - new_body["transactions"] = transactions - elif tac_type == "error": - new_body["type"] = TACMessage.Type.TAC_ERROR - new_body["error_code"] = TACMessage.ErrorCode(tac_container.error.error_code) - if tac_container.error.error_msg: - new_body["error_msg"] = tac_container.error.error_msg - if tac_container.error.details: - new_body["details"] = dict(tac_container.error.details) - else: - raise ValueError("Type not recognized.") - - tac_type = TACMessage.Type(new_body["type"]) - new_body["type"] = tac_type - tac_message = TACMessage(tac_type=tac_type, body=new_body) - return tac_message diff --git a/Test/protocols/tac/tac.proto b/Test/protocols/tac/tac.proto deleted file mode 100644 index d6a714d425..0000000000 --- a/Test/protocols/tac/tac.proto +++ /dev/null @@ -1,102 +0,0 @@ -syntax = "proto3"; - -package fetch.oef.pb; - -import "google/protobuf/struct.proto"; - -message StrIntPair { - string first = 1; - int32 second = 2; -} - -message StrStrPair { - string first = 1; - string second = 2; -} - -message TACController { - - message Registered { - } - message Unregistered { - } - message Cancelled { - } - - message GameData { - double money = 1; - repeated int32 endowment = 2; - repeated double utility_params = 3; - int32 nb_agents = 4; - int32 nb_goods = 5; - double tx_fee = 6; - repeated StrStrPair agent_pbk_to_name = 7; - repeated StrStrPair good_pbk_to_name = 8; - } - - message TransactionConfirmation { - string transaction_id = 1; - } - - message StateUpdate { - GameData initial_state = 1; - repeated TACAgent.Transaction txs = 2; - } - - message Error { - enum ErrorCode { - GENERIC_ERROR = 0; - REQUEST_NOT_VALID = 1; - AGENT_PBK_ALREADY_REGISTERED = 2; - AGENT_NAME_ALREADY_REGISTERED = 3; - AGENT_NOT_REGISTERED = 4; - TRANSACTION_NOT_VALID = 5; - TRANSACTION_NOT_MATCHING = 6; - AGENT_NAME_NOT_IN_WHITELIST = 7; - COMPETITION_NOT_RUNNING = 8; - DIALOGUE_INCONSISTENT = 9; - } - - ErrorCode error_code = 1; - string error_msg = 2; - google.protobuf.Struct details = 3; - } - -} - -message TACAgent { - - message Register { - string agent_name = 1; - } - message Unregister { - } - - message Transaction { - string transaction_id = 1; - bool is_sender_buyer = 2; // is the sender of this message a buyer? - string counterparty = 3; - double amount = 4; - repeated StrIntPair quantities = 5; - } - - message GetStateUpdate { - } - -} - -message TACMessage { - oneof content{ - TACAgent.Register register = 1; - TACAgent.Unregister unregister = 2; - TACAgent.Transaction transaction = 3; - TACAgent.GetStateUpdate get_state_update = 4; - TACController.Registered registered = 5; - TACController.Unregistered unregistered = 6; - TACController.Cancelled cancelled = 7; - TACController.GameData game_data = 8; - TACController.TransactionConfirmation transaction_confirmation = 9; - TACController.StateUpdate state_update = 10; - TACController.Error error = 11; - } -} diff --git a/Test/protocols/tac/tac_pb2.py b/Test/protocols/tac/tac_pb2.py deleted file mode 100644 index 5eb63f23f1..0000000000 --- a/Test/protocols/tac/tac_pb2.py +++ /dev/null @@ -1,899 +0,0 @@ -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: tac.proto - -import sys -_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection -from google.protobuf import symbol_database as _symbol_database -from google.protobuf import descriptor_pb2 -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='tac.proto', - package='fetch.oef.pb', - syntax='proto3', - serialized_pb=_b('\n\ttac.proto\x12\x0c\x66\x65tch.oef.pb\x1a\x1cgoogle/protobuf/struct.proto\"+\n\nStrIntPair\x12\r\n\x05\x66irst\x18\x01 \x01(\t\x12\x0e\n\x06second\x18\x02 \x01(\x05\"+\n\nStrStrPair\x12\r\n\x05\x66irst\x18\x01 \x01(\t\x12\x0e\n\x06second\x18\x02 \x01(\t\"\x80\x07\n\rTACController\x1a\x0c\n\nRegistered\x1a\x0e\n\x0cUnregistered\x1a\x0b\n\tCancelled\x1a\xe2\x01\n\x08GameData\x12\r\n\x05money\x18\x01 \x01(\x01\x12\x11\n\tendowment\x18\x02 \x03(\x05\x12\x16\n\x0eutility_params\x18\x03 \x03(\x01\x12\x11\n\tnb_agents\x18\x04 \x01(\x05\x12\x10\n\x08nb_goods\x18\x05 \x01(\x05\x12\x0e\n\x06tx_fee\x18\x06 \x01(\x01\x12\x33\n\x11\x61gent_pbk_to_name\x18\x07 \x03(\x0b\x32\x18.fetch.oef.pb.StrStrPair\x12\x32\n\x10good_pbk_to_name\x18\x08 \x03(\x0b\x32\x18.fetch.oef.pb.StrStrPair\x1a\x31\n\x17TransactionConfirmation\x12\x16\n\x0etransaction_id\x18\x01 \x01(\t\x1a{\n\x0bStateUpdate\x12;\n\rinitial_state\x18\x01 \x01(\x0b\x32$.fetch.oef.pb.TACController.GameData\x12/\n\x03txs\x18\x02 \x03(\x0b\x32\".fetch.oef.pb.TACAgent.Transaction\x1a\xae\x03\n\x05\x45rror\x12?\n\nerror_code\x18\x01 \x01(\x0e\x32+.fetch.oef.pb.TACController.Error.ErrorCode\x12\x11\n\terror_msg\x18\x02 \x01(\t\x12(\n\x07\x64\x65tails\x18\x03 \x01(\x0b\x32\x17.google.protobuf.Struct\"\xa6\x02\n\tErrorCode\x12\x11\n\rGENERIC_ERROR\x10\x00\x12\x15\n\x11REQUEST_NOT_VALID\x10\x01\x12 \n\x1c\x41GENT_PBK_ALREADY_REGISTERED\x10\x02\x12!\n\x1d\x41GENT_NAME_ALREADY_REGISTERED\x10\x03\x12\x18\n\x14\x41GENT_NOT_REGISTERED\x10\x04\x12\x19\n\x15TRANSACTION_NOT_VALID\x10\x05\x12\x1c\n\x18TRANSACTION_NOT_MATCHING\x10\x06\x12\x1f\n\x1b\x41GENT_NAME_NOT_IN_WHITELIST\x10\x07\x12\x1b\n\x17\x43OMPETITION_NOT_RUNNING\x10\x08\x12\x19\n\x15\x44IALOGUE_INCONSISTENT\x10\t\"\xdf\x01\n\x08TACAgent\x1a\x1e\n\x08Register\x12\x12\n\nagent_name\x18\x01 \x01(\t\x1a\x0c\n\nUnregister\x1a\x92\x01\n\x0bTransaction\x12\x16\n\x0etransaction_id\x18\x01 \x01(\t\x12\x17\n\x0fis_sender_buyer\x18\x02 \x01(\x08\x12\x14\n\x0c\x63ounterparty\x18\x03 \x01(\t\x12\x0e\n\x06\x61mount\x18\x04 \x01(\x01\x12,\n\nquantities\x18\x05 \x03(\x0b\x32\x18.fetch.oef.pb.StrIntPair\x1a\x10\n\x0eGetStateUpdate\"\xc8\x05\n\nTACMessage\x12\x33\n\x08register\x18\x01 \x01(\x0b\x32\x1f.fetch.oef.pb.TACAgent.RegisterH\x00\x12\x37\n\nunregister\x18\x02 \x01(\x0b\x32!.fetch.oef.pb.TACAgent.UnregisterH\x00\x12\x39\n\x0btransaction\x18\x03 \x01(\x0b\x32\".fetch.oef.pb.TACAgent.TransactionH\x00\x12\x41\n\x10get_state_update\x18\x04 \x01(\x0b\x32%.fetch.oef.pb.TACAgent.GetStateUpdateH\x00\x12<\n\nregistered\x18\x05 \x01(\x0b\x32&.fetch.oef.pb.TACController.RegisteredH\x00\x12@\n\x0cunregistered\x18\x06 \x01(\x0b\x32(.fetch.oef.pb.TACController.UnregisteredH\x00\x12:\n\tcancelled\x18\x07 \x01(\x0b\x32%.fetch.oef.pb.TACController.CancelledH\x00\x12\x39\n\tgame_data\x18\x08 \x01(\x0b\x32$.fetch.oef.pb.TACController.GameDataH\x00\x12W\n\x18transaction_confirmation\x18\t \x01(\x0b\x32\x33.fetch.oef.pb.TACController.TransactionConfirmationH\x00\x12?\n\x0cstate_update\x18\n \x01(\x0b\x32\'.fetch.oef.pb.TACController.StateUpdateH\x00\x12\x32\n\x05\x65rror\x18\x0b \x01(\x0b\x32!.fetch.oef.pb.TACController.ErrorH\x00\x42\t\n\x07\x63ontentb\x06proto3') - , - dependencies=[google_dot_protobuf_dot_struct__pb2.DESCRIPTOR,]) -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - - - -_TACCONTROLLER_ERROR_ERRORCODE = _descriptor.EnumDescriptor( - name='ErrorCode', - full_name='fetch.oef.pb.TACController.Error.ErrorCode', - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name='GENERIC_ERROR', index=0, number=0, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='REQUEST_NOT_VALID', index=1, number=1, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='AGENT_PBK_ALREADY_REGISTERED', index=2, number=2, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='AGENT_NAME_ALREADY_REGISTERED', index=3, number=3, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='AGENT_NOT_REGISTERED', index=4, number=4, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='TRANSACTION_NOT_VALID', index=5, number=5, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='TRANSACTION_NOT_MATCHING', index=6, number=6, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='AGENT_NAME_NOT_IN_WHITELIST', index=7, number=7, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='COMPETITION_NOT_RUNNING', index=8, number=8, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='DIALOGUE_INCONSISTENT', index=9, number=9, - options=None, - type=None), - ], - containing_type=None, - options=None, - serialized_start=750, - serialized_end=1044, -) -_sym_db.RegisterEnumDescriptor(_TACCONTROLLER_ERROR_ERRORCODE) - - -_STRINTPAIR = _descriptor.Descriptor( - name='StrIntPair', - full_name='fetch.oef.pb.StrIntPair', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='first', full_name='fetch.oef.pb.StrIntPair.first', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='second', full_name='fetch.oef.pb.StrIntPair.second', index=1, - number=2, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=57, - serialized_end=100, -) - - -_STRSTRPAIR = _descriptor.Descriptor( - name='StrStrPair', - full_name='fetch.oef.pb.StrStrPair', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='first', full_name='fetch.oef.pb.StrStrPair.first', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='second', full_name='fetch.oef.pb.StrStrPair.second', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=102, - serialized_end=145, -) - - -_TACCONTROLLER_REGISTERED = _descriptor.Descriptor( - name='Registered', - full_name='fetch.oef.pb.TACController.Registered', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=165, - serialized_end=177, -) - -_TACCONTROLLER_UNREGISTERED = _descriptor.Descriptor( - name='Unregistered', - full_name='fetch.oef.pb.TACController.Unregistered', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=179, - serialized_end=193, -) - -_TACCONTROLLER_CANCELLED = _descriptor.Descriptor( - name='Cancelled', - full_name='fetch.oef.pb.TACController.Cancelled', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=195, - serialized_end=206, -) - -_TACCONTROLLER_GAMEDATA = _descriptor.Descriptor( - name='GameData', - full_name='fetch.oef.pb.TACController.GameData', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='money', full_name='fetch.oef.pb.TACController.GameData.money', index=0, - number=1, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='endowment', full_name='fetch.oef.pb.TACController.GameData.endowment', index=1, - number=2, type=5, cpp_type=1, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='utility_params', full_name='fetch.oef.pb.TACController.GameData.utility_params', index=2, - number=3, type=1, cpp_type=5, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='nb_agents', full_name='fetch.oef.pb.TACController.GameData.nb_agents', index=3, - number=4, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='nb_goods', full_name='fetch.oef.pb.TACController.GameData.nb_goods', index=4, - number=5, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='tx_fee', full_name='fetch.oef.pb.TACController.GameData.tx_fee', index=5, - number=6, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='agent_pbk_to_name', full_name='fetch.oef.pb.TACController.GameData.agent_pbk_to_name', index=6, - number=7, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='good_pbk_to_name', full_name='fetch.oef.pb.TACController.GameData.good_pbk_to_name', index=7, - number=8, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=209, - serialized_end=435, -) - -_TACCONTROLLER_TRANSACTIONCONFIRMATION = _descriptor.Descriptor( - name='TransactionConfirmation', - full_name='fetch.oef.pb.TACController.TransactionConfirmation', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='transaction_id', full_name='fetch.oef.pb.TACController.TransactionConfirmation.transaction_id', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=437, - serialized_end=486, -) - -_TACCONTROLLER_STATEUPDATE = _descriptor.Descriptor( - name='StateUpdate', - full_name='fetch.oef.pb.TACController.StateUpdate', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='initial_state', full_name='fetch.oef.pb.TACController.StateUpdate.initial_state', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='txs', full_name='fetch.oef.pb.TACController.StateUpdate.txs', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=488, - serialized_end=611, -) - -_TACCONTROLLER_ERROR = _descriptor.Descriptor( - name='Error', - full_name='fetch.oef.pb.TACController.Error', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='error_code', full_name='fetch.oef.pb.TACController.Error.error_code', index=0, - number=1, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='error_msg', full_name='fetch.oef.pb.TACController.Error.error_msg', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='details', full_name='fetch.oef.pb.TACController.Error.details', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _TACCONTROLLER_ERROR_ERRORCODE, - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=614, - serialized_end=1044, -) - -_TACCONTROLLER = _descriptor.Descriptor( - name='TACController', - full_name='fetch.oef.pb.TACController', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[_TACCONTROLLER_REGISTERED, _TACCONTROLLER_UNREGISTERED, _TACCONTROLLER_CANCELLED, _TACCONTROLLER_GAMEDATA, _TACCONTROLLER_TRANSACTIONCONFIRMATION, _TACCONTROLLER_STATEUPDATE, _TACCONTROLLER_ERROR, ], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=148, - serialized_end=1044, -) - - -_TACAGENT_REGISTER = _descriptor.Descriptor( - name='Register', - full_name='fetch.oef.pb.TACAgent.Register', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='agent_name', full_name='fetch.oef.pb.TACAgent.Register.agent_name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1059, - serialized_end=1089, -) - -_TACAGENT_UNREGISTER = _descriptor.Descriptor( - name='Unregister', - full_name='fetch.oef.pb.TACAgent.Unregister', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1091, - serialized_end=1103, -) - -_TACAGENT_TRANSACTION = _descriptor.Descriptor( - name='Transaction', - full_name='fetch.oef.pb.TACAgent.Transaction', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='transaction_id', full_name='fetch.oef.pb.TACAgent.Transaction.transaction_id', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='is_sender_buyer', full_name='fetch.oef.pb.TACAgent.Transaction.is_sender_buyer', index=1, - number=2, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='counterparty', full_name='fetch.oef.pb.TACAgent.Transaction.counterparty', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='amount', full_name='fetch.oef.pb.TACAgent.Transaction.amount', index=3, - number=4, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='quantities', full_name='fetch.oef.pb.TACAgent.Transaction.quantities', index=4, - number=5, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1106, - serialized_end=1252, -) - -_TACAGENT_GETSTATEUPDATE = _descriptor.Descriptor( - name='GetStateUpdate', - full_name='fetch.oef.pb.TACAgent.GetStateUpdate', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1254, - serialized_end=1270, -) - -_TACAGENT = _descriptor.Descriptor( - name='TACAgent', - full_name='fetch.oef.pb.TACAgent', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[_TACAGENT_REGISTER, _TACAGENT_UNREGISTER, _TACAGENT_TRANSACTION, _TACAGENT_GETSTATEUPDATE, ], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1047, - serialized_end=1270, -) - - -_TACMESSAGE = _descriptor.Descriptor( - name='TACMessage', - full_name='fetch.oef.pb.TACMessage', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='register', full_name='fetch.oef.pb.TACMessage.register', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='unregister', full_name='fetch.oef.pb.TACMessage.unregister', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='transaction', full_name='fetch.oef.pb.TACMessage.transaction', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='get_state_update', full_name='fetch.oef.pb.TACMessage.get_state_update', index=3, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='registered', full_name='fetch.oef.pb.TACMessage.registered', index=4, - number=5, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='unregistered', full_name='fetch.oef.pb.TACMessage.unregistered', index=5, - number=6, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='cancelled', full_name='fetch.oef.pb.TACMessage.cancelled', index=6, - number=7, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='game_data', full_name='fetch.oef.pb.TACMessage.game_data', index=7, - number=8, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='transaction_confirmation', full_name='fetch.oef.pb.TACMessage.transaction_confirmation', index=8, - number=9, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='state_update', full_name='fetch.oef.pb.TACMessage.state_update', index=9, - number=10, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='error', full_name='fetch.oef.pb.TACMessage.error', index=10, - number=11, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='content', full_name='fetch.oef.pb.TACMessage.content', - index=0, containing_type=None, fields=[]), - ], - serialized_start=1273, - serialized_end=1985, -) - -_TACCONTROLLER_REGISTERED.containing_type = _TACCONTROLLER -_TACCONTROLLER_UNREGISTERED.containing_type = _TACCONTROLLER -_TACCONTROLLER_CANCELLED.containing_type = _TACCONTROLLER -_TACCONTROLLER_GAMEDATA.fields_by_name['agent_pbk_to_name'].message_type = _STRSTRPAIR -_TACCONTROLLER_GAMEDATA.fields_by_name['good_pbk_to_name'].message_type = _STRSTRPAIR -_TACCONTROLLER_GAMEDATA.containing_type = _TACCONTROLLER -_TACCONTROLLER_TRANSACTIONCONFIRMATION.containing_type = _TACCONTROLLER -_TACCONTROLLER_STATEUPDATE.fields_by_name['initial_state'].message_type = _TACCONTROLLER_GAMEDATA -_TACCONTROLLER_STATEUPDATE.fields_by_name['txs'].message_type = _TACAGENT_TRANSACTION -_TACCONTROLLER_STATEUPDATE.containing_type = _TACCONTROLLER -_TACCONTROLLER_ERROR.fields_by_name['error_code'].enum_type = _TACCONTROLLER_ERROR_ERRORCODE -_TACCONTROLLER_ERROR.fields_by_name['details'].message_type = google_dot_protobuf_dot_struct__pb2._STRUCT -_TACCONTROLLER_ERROR.containing_type = _TACCONTROLLER -_TACCONTROLLER_ERROR_ERRORCODE.containing_type = _TACCONTROLLER_ERROR -_TACAGENT_REGISTER.containing_type = _TACAGENT -_TACAGENT_UNREGISTER.containing_type = _TACAGENT -_TACAGENT_TRANSACTION.fields_by_name['quantities'].message_type = _STRINTPAIR -_TACAGENT_TRANSACTION.containing_type = _TACAGENT -_TACAGENT_GETSTATEUPDATE.containing_type = _TACAGENT -_TACMESSAGE.fields_by_name['register'].message_type = _TACAGENT_REGISTER -_TACMESSAGE.fields_by_name['unregister'].message_type = _TACAGENT_UNREGISTER -_TACMESSAGE.fields_by_name['transaction'].message_type = _TACAGENT_TRANSACTION -_TACMESSAGE.fields_by_name['get_state_update'].message_type = _TACAGENT_GETSTATEUPDATE -_TACMESSAGE.fields_by_name['registered'].message_type = _TACCONTROLLER_REGISTERED -_TACMESSAGE.fields_by_name['unregistered'].message_type = _TACCONTROLLER_UNREGISTERED -_TACMESSAGE.fields_by_name['cancelled'].message_type = _TACCONTROLLER_CANCELLED -_TACMESSAGE.fields_by_name['game_data'].message_type = _TACCONTROLLER_GAMEDATA -_TACMESSAGE.fields_by_name['transaction_confirmation'].message_type = _TACCONTROLLER_TRANSACTIONCONFIRMATION -_TACMESSAGE.fields_by_name['state_update'].message_type = _TACCONTROLLER_STATEUPDATE -_TACMESSAGE.fields_by_name['error'].message_type = _TACCONTROLLER_ERROR -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['register']) -_TACMESSAGE.fields_by_name['register'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['unregister']) -_TACMESSAGE.fields_by_name['unregister'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['transaction']) -_TACMESSAGE.fields_by_name['transaction'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['get_state_update']) -_TACMESSAGE.fields_by_name['get_state_update'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['registered']) -_TACMESSAGE.fields_by_name['registered'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['unregistered']) -_TACMESSAGE.fields_by_name['unregistered'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['cancelled']) -_TACMESSAGE.fields_by_name['cancelled'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['game_data']) -_TACMESSAGE.fields_by_name['game_data'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['transaction_confirmation']) -_TACMESSAGE.fields_by_name['transaction_confirmation'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['state_update']) -_TACMESSAGE.fields_by_name['state_update'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['error']) -_TACMESSAGE.fields_by_name['error'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -DESCRIPTOR.message_types_by_name['StrIntPair'] = _STRINTPAIR -DESCRIPTOR.message_types_by_name['StrStrPair'] = _STRSTRPAIR -DESCRIPTOR.message_types_by_name['TACController'] = _TACCONTROLLER -DESCRIPTOR.message_types_by_name['TACAgent'] = _TACAGENT -DESCRIPTOR.message_types_by_name['TACMessage'] = _TACMESSAGE - -StrIntPair = _reflection.GeneratedProtocolMessageType('StrIntPair', (_message.Message,), dict( - DESCRIPTOR = _STRINTPAIR, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.StrIntPair) - )) -_sym_db.RegisterMessage(StrIntPair) - -StrStrPair = _reflection.GeneratedProtocolMessageType('StrStrPair', (_message.Message,), dict( - DESCRIPTOR = _STRSTRPAIR, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.StrStrPair) - )) -_sym_db.RegisterMessage(StrStrPair) - -TACController = _reflection.GeneratedProtocolMessageType('TACController', (_message.Message,), dict( - - Registered = _reflection.GeneratedProtocolMessageType('Registered', (_message.Message,), dict( - DESCRIPTOR = _TACCONTROLLER_REGISTERED, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Registered) - )) - , - - Unregistered = _reflection.GeneratedProtocolMessageType('Unregistered', (_message.Message,), dict( - DESCRIPTOR = _TACCONTROLLER_UNREGISTERED, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Unregistered) - )) - , - - Cancelled = _reflection.GeneratedProtocolMessageType('Cancelled', (_message.Message,), dict( - DESCRIPTOR = _TACCONTROLLER_CANCELLED, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Cancelled) - )) - , - - GameData = _reflection.GeneratedProtocolMessageType('GameData', (_message.Message,), dict( - DESCRIPTOR = _TACCONTROLLER_GAMEDATA, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.GameData) - )) - , - - TransactionConfirmation = _reflection.GeneratedProtocolMessageType('TransactionConfirmation', (_message.Message,), dict( - DESCRIPTOR = _TACCONTROLLER_TRANSACTIONCONFIRMATION, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.TransactionConfirmation) - )) - , - - StateUpdate = _reflection.GeneratedProtocolMessageType('StateUpdate', (_message.Message,), dict( - DESCRIPTOR = _TACCONTROLLER_STATEUPDATE, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.StateUpdate) - )) - , - - Error = _reflection.GeneratedProtocolMessageType('Error', (_message.Message,), dict( - DESCRIPTOR = _TACCONTROLLER_ERROR, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Error) - )) - , - DESCRIPTOR = _TACCONTROLLER, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController) - )) -_sym_db.RegisterMessage(TACController) -_sym_db.RegisterMessage(TACController.Registered) -_sym_db.RegisterMessage(TACController.Unregistered) -_sym_db.RegisterMessage(TACController.Cancelled) -_sym_db.RegisterMessage(TACController.GameData) -_sym_db.RegisterMessage(TACController.TransactionConfirmation) -_sym_db.RegisterMessage(TACController.StateUpdate) -_sym_db.RegisterMessage(TACController.Error) - -TACAgent = _reflection.GeneratedProtocolMessageType('TACAgent', (_message.Message,), dict( - - Register = _reflection.GeneratedProtocolMessageType('Register', (_message.Message,), dict( - DESCRIPTOR = _TACAGENT_REGISTER, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.Register) - )) - , - - Unregister = _reflection.GeneratedProtocolMessageType('Unregister', (_message.Message,), dict( - DESCRIPTOR = _TACAGENT_UNREGISTER, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.Unregister) - )) - , - - Transaction = _reflection.GeneratedProtocolMessageType('Transaction', (_message.Message,), dict( - DESCRIPTOR = _TACAGENT_TRANSACTION, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.Transaction) - )) - , - - GetStateUpdate = _reflection.GeneratedProtocolMessageType('GetStateUpdate', (_message.Message,), dict( - DESCRIPTOR = _TACAGENT_GETSTATEUPDATE, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.GetStateUpdate) - )) - , - DESCRIPTOR = _TACAGENT, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent) - )) -_sym_db.RegisterMessage(TACAgent) -_sym_db.RegisterMessage(TACAgent.Register) -_sym_db.RegisterMessage(TACAgent.Unregister) -_sym_db.RegisterMessage(TACAgent.Transaction) -_sym_db.RegisterMessage(TACAgent.GetStateUpdate) - -TACMessage = _reflection.GeneratedProtocolMessageType('TACMessage', (_message.Message,), dict( - DESCRIPTOR = _TACMESSAGE, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACMessage) - )) -_sym_db.RegisterMessage(TACMessage) - - -# @@protoc_insertion_point(module_scope) diff --git a/Test/skills/__init__.py b/Test/skills/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/Test/skills/error/__init__.py b/Test/skills/error/__init__.py deleted file mode 100644 index 96c80ac32c..0000000000 --- a/Test/skills/error/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the implementation of the error skill.""" diff --git a/Test/skills/error/behaviours.py b/Test/skills/error/behaviours.py deleted file mode 100644 index 556ee98ca7..0000000000 --- a/Test/skills/error/behaviours.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains the error behaviours.""" - -from aea.skills.base import Behaviour - - -class ErrorBehaviour(Behaviour): - """This class implements the error behaviour.""" - - def setup(self) -> None: - """ - Implement the setup. - - :return: None - """ - pass - - def act(self) -> None: - """ - Implement the act. - - :return: None - """ - pass - - def teardown(self) -> None: - """ - Implement the task teardown. - - :return: None - """ - pass diff --git a/Test/skills/error/handlers.py b/Test/skills/error/handlers.py deleted file mode 100644 index 098a61eced..0000000000 --- a/Test/skills/error/handlers.py +++ /dev/null @@ -1,131 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains the implementation of the handler for the 'default' protocol.""" -import base64 -import logging -from typing import Optional - -from aea.configurations.base import ProtocolId -from aea.mail.base import Envelope -from aea.protocols.base import Message, Protocol -from aea.protocols.default.message import DefaultMessage -from aea.protocols.default.serialization import DefaultSerializer -from aea.skills.base import Handler - -logger = logging.getLogger(__name__) - - -class ErrorHandler(Handler): - """This class implements the error handler.""" - - SUPPORTED_PROTOCOL = 'default' # type: Optional[ProtocolId] - - def setup(self) -> None: - """ - Implement the setup. - - :return: None - """ - pass - - def handle(self, message: Message, sender: str) -> None: - """ - Implement the reaction to an envelope. - - :param message: the message - :param sender: the sender - """ - pass - - def teardown(self) -> None: - """ - Implement the handler teardown. - - :return: None - """ - pass - - def send_unsupported_protocol(self, envelope: Envelope) -> None: - """ - Handle the received envelope in case the protocol is not supported. - - :param envelope: the envelope - :return: None - """ - logger.warning("Unsupported protocol: {}".format(envelope.protocol_id)) - reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.UNSUPPORTED_PROTOCOL.value, - error_msg="Unsupported protocol.", - error_data={"protocol_id": envelope.protocol_id}) - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, - protocol_id=DefaultMessage.protocol_id, - message=DefaultSerializer().encode(reply)) - - def send_decoding_error(self, envelope: Envelope) -> None: - """ - Handle a decoding error. - - :param envelope: the envelope - :return: None - """ - logger.warning("Decoding error: {}.".format(envelope)) - encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") - reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.DECODING_ERROR.value, - error_msg="Decoding error.", - error_data={"envelope": encoded_envelope}) - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, - protocol_id=DefaultMessage.protocol_id, - message=DefaultSerializer().encode(reply)) - - def send_invalid_message(self, envelope: Envelope) -> None: - """ - Handle an message that is invalid wrt a protocol. - - :param envelope: the envelope - :return: None - """ - logger.warning("Invalid message wrt protocol: {}.".format(envelope.protocol_id)) - encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") - reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.INVALID_MESSAGE.value, - error_msg="Invalid message.", - error_data={"envelope": encoded_envelope}) - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, - protocol_id=DefaultMessage.protocol_id, - message=DefaultSerializer().encode(reply)) - - def send_unsupported_skill(self, envelope: Envelope, protocol: Protocol) -> None: - """ - Handle the received envelope in case the skill is not supported. - - :param envelope: the envelope - :param protocol: the protocol - :return: None - """ - logger.warning("Cannot handle envelope: no handler registered for the protocol '{}'.".format(protocol.id)) - encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") - reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.UNSUPPORTED_SKILL.value, - error_msg="Unsupported skill.", - error_data={"envelope": encoded_envelope}) - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, - protocol_id=DefaultMessage.protocol_id, - message=DefaultSerializer().encode(reply)) diff --git a/Test/skills/error/skill.yaml b/Test/skills/error/skill.yaml deleted file mode 100644 index 33806a3345..0000000000 --- a/Test/skills/error/skill.yaml +++ /dev/null @@ -1,15 +0,0 @@ -name: error -authors: Fetch.AI Limited -version: 0.1.0 -license: Apache 2.0 -url: "" -description: "The error skill implements basic error handling required by all AEAs." -behaviours: [] -handlers: - - handler: - class_name: ErrorHandler - args: - foo: bar -tasks: [] -shared_classes: [] -protocols: ['default'] diff --git a/Test/skills/error/tasks.py b/Test/skills/error/tasks.py deleted file mode 100644 index 8922217537..0000000000 --- a/Test/skills/error/tasks.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains the implementation of the error tasks.""" - -from aea.skills.base import Task - - -class ErrorTask(Task): - """This class implements the error task.""" - - def setup(self) -> None: - """ - Implement the setup. - - :return: None - """ - pass - - def execute(self) -> None: - """ - Implement the task execution. - - :param envelope: the envelope - :return: None - """ - pass - - def teardown(self) -> None: - """ - Implement the task teardown. - - :return: None - """ - pass diff --git a/asdsd/aea-config.yaml b/asdsd/aea-config.yaml deleted file mode 100644 index a446facf18..0000000000 --- a/asdsd/aea-config.yaml +++ /dev/null @@ -1,21 +0,0 @@ -aea_version: 0.1.7 -agent_name: asdsd -authors: '' -connections: -- oef -default_connection: oef -description: '' -license: '' -logging_config: - disable_existing_loggers: false - version: 1 -private_key_paths: [] -protocols: -- default -- gym -- tac -registry_path: ../packages -skills: -- error -url: '' -version: v1 diff --git a/asdsd/connections/__init__.py b/asdsd/connections/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/asdsd/connections/oef/__init__.py b/asdsd/connections/oef/__init__.py deleted file mode 100644 index 21ee4d83df..0000000000 --- a/asdsd/connections/oef/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Implementation of the OEF connection.""" diff --git a/asdsd/connections/oef/connection.py b/asdsd/connections/oef/connection.py deleted file mode 100644 index 7f489b867f..0000000000 --- a/asdsd/connections/oef/connection.py +++ /dev/null @@ -1,604 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Extension to the OEF Python SDK.""" -import datetime -import logging -import pickle -from queue import Empty, Queue -from threading import Thread -from typing import List, Dict, Optional, cast - -import oef -from oef.agents import OEFAgent -from oef.core import AsyncioCore -from oef.messages import CFP_TYPES, PROPOSE_TYPES -from oef.query import ( - Query as OEFQuery, - ConstraintExpr as OEFConstraintExpr, - And as OEFAnd, - Or as OEFOr, - Not as OEFNot, - Constraint as OEFConstraint, - ConstraintType as OEFConstraintType, Eq, NotEq, Lt, LtEq, Gt, GtEq, Range, In, NotIn) -from oef.schema import Description as OEFDescription, DataModel as OEFDataModel, AttributeSchema as OEFAttribute - -from aea.configurations.base import ConnectionConfig -from aea.connections.base import Channel, Connection -from aea.mail.base import MailBox, Envelope -from aea.protocols.fipa.message import FIPAMessage -from aea.protocols.fipa.serialization import FIPASerializer -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.models import Description, Attribute, DataModel, Query, ConstraintExpr, And, Or, Not, Constraint, \ - ConstraintType, ConstraintTypes -from aea.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF - -logger = logging.getLogger(__name__) - - -STUB_MESSSAGE_ID = 0 -STUB_DIALOGUE_ID = 0 - - -class OEFObjectTranslator: - """Translate our OEF object to object of OEF SDK classes.""" - - @classmethod - def to_oef_description(cls, desc: Description) -> OEFDescription: - """From our description to OEF description.""" - oef_data_model = cls.to_oef_data_model(desc.data_model) if desc.data_model is not None else None - return OEFDescription(desc.values, oef_data_model) - - @classmethod - def to_oef_data_model(cls, data_model: DataModel) -> OEFDataModel: - """From our data model to OEF data model.""" - oef_attributes = [cls.to_oef_attribute(attribute) for attribute in data_model.attributes] - return OEFDataModel(data_model.name, oef_attributes, data_model.description) - - @classmethod - def to_oef_attribute(cls, attribute: Attribute) -> OEFAttribute: - """From our attribute to OEF attribute.""" - return OEFAttribute(attribute.name, attribute.type, attribute.is_required, attribute.description) - - @classmethod - def to_oef_query(cls, query: Query) -> OEFQuery: - """From our query to OEF query.""" - oef_data_model = cls.to_oef_data_model(query.model) if query.model is not None else None - constraints = [cls.to_oef_constraint_expr(c) for c in query.constraints] - return OEFQuery(constraints, oef_data_model) - - @classmethod - def to_oef_constraint_expr(cls, constraint_expr: ConstraintExpr) -> OEFConstraintExpr: - """From our constraint expression to the OEF constraint expression.""" - if isinstance(constraint_expr, And): - return OEFAnd([cls.to_oef_constraint_expr(c) for c in constraint_expr.constraints]) - elif isinstance(constraint_expr, Or): - return OEFOr([cls.to_oef_constraint_expr(c) for c in constraint_expr.constraints]) - elif isinstance(constraint_expr, Not): - return OEFNot(cls.to_oef_constraint_expr(constraint_expr.constraint)) - elif isinstance(constraint_expr, Constraint): - oef_constraint_type = cls.to_oef_constraint_type(constraint_expr.constraint_type) - return OEFConstraint(constraint_expr.attribute_name, oef_constraint_type) - else: - raise ValueError("Constraint expression not supported.") - - @classmethod - def to_oef_constraint_type(cls, constraint_type: ConstraintType) -> OEFConstraintType: - """From our constraint type to OEF constraint type.""" - value = constraint_type.value - if constraint_type.type == ConstraintTypes.EQUAL: - return Eq(value) - elif constraint_type.type == ConstraintTypes.NOT_EQUAL: - return NotEq(value) - elif constraint_type.type == ConstraintTypes.LESS_THAN: - return Lt(value) - elif constraint_type.type == ConstraintTypes.LESS_THAN_EQ: - return LtEq(value) - elif constraint_type.type == ConstraintTypes.GREATER_THAN: - return Gt(value) - elif constraint_type.type == ConstraintTypes.GREATER_THAN_EQ: - return GtEq(value) - elif constraint_type.type == ConstraintTypes.WITHIN: - return Range(value) - elif constraint_type.type == ConstraintTypes.IN: - return In(value) - elif constraint_type.type == ConstraintTypes.NOT_IN: - return NotIn(value) - else: - raise ValueError("Constraint type not recognized.") - - @classmethod - def from_oef_description(cls, oef_desc: OEFDescription) -> Description: - """From an OEF description to our description.""" - data_model = cls.from_oef_data_model(oef_desc.data_model) if oef_desc.data_model is not None else None - return Description(oef_desc.values, data_model=data_model) - - @classmethod - def from_oef_data_model(cls, oef_data_model: OEFDataModel) -> DataModel: - """From an OEF data model to our data model.""" - attributes = [cls.from_oef_attribute(oef_attribute) for oef_attribute in oef_data_model.attribute_schemas] - return DataModel(oef_data_model.name, attributes, oef_data_model.description) - - @classmethod - def from_oef_attribute(cls, oef_attribute: OEFAttribute) -> Attribute: - """From an OEF attribute to our attribute.""" - return Attribute(oef_attribute.name, oef_attribute.type, oef_attribute.required, oef_attribute.description) - - @classmethod - def from_oef_query(cls, oef_query: OEFQuery) -> Query: - """From our query to OrOEF query.""" - data_model = cls.from_oef_data_model(oef_query.model) if oef_query.model is not None else None - constraints = [cls.from_oef_constraint_expr(c) for c in oef_query.constraints] - return Query(constraints, data_model) - - @classmethod - def from_oef_constraint_expr(cls, oef_constraint_expr: OEFConstraintExpr) -> ConstraintExpr: - """From our query to OEF query.""" - if isinstance(oef_constraint_expr, OEFAnd): - return And([cls.from_oef_constraint_expr(c) for c in oef_constraint_expr.constraints]) - elif isinstance(oef_constraint_expr, OEFOr): - return Or([cls.from_oef_constraint_expr(c) for c in oef_constraint_expr.constraints]) - elif isinstance(oef_constraint_expr, OEFNot): - return Not(cls.from_oef_constraint_expr(oef_constraint_expr.constraint)) - elif isinstance(oef_constraint_expr, OEFConstraint): - constraint_type = cls.from_oef_constraint_type(oef_constraint_expr.constraint) - return Constraint(oef_constraint_expr.attribute_name, constraint_type) - else: - raise ValueError("OEF Constraint not supported.") - - @classmethod - def from_oef_constraint_type(cls, constraint_type: OEFConstraintType) -> ConstraintType: - """From OEF constraint type to our constraint type.""" - if isinstance(constraint_type, Eq): - return ConstraintType(ConstraintTypes.EQUAL, constraint_type.value) - elif isinstance(constraint_type, NotEq): - return ConstraintType(ConstraintTypes.NOT_EQUAL, constraint_type.value) - elif isinstance(constraint_type, Lt): - return ConstraintType(ConstraintTypes.LESS_THAN, constraint_type.value) - elif isinstance(constraint_type, LtEq): - return ConstraintType(ConstraintTypes.LESS_THAN_EQ, constraint_type.value) - elif isinstance(constraint_type, Gt): - return ConstraintType(ConstraintTypes.GREATER_THAN, constraint_type.value) - elif isinstance(constraint_type, GtEq): - return ConstraintType(ConstraintTypes.GREATER_THAN_EQ, constraint_type.value) - elif isinstance(constraint_type, Range): - return ConstraintType(ConstraintTypes.WITHIN, constraint_type.values) - elif isinstance(constraint_type, In): - return ConstraintType(ConstraintTypes.IN, constraint_type.values) - elif isinstance(constraint_type, NotIn): - return ConstraintType(ConstraintTypes.NOT_IN, constraint_type.values) - else: - raise ValueError("Constraint type not recognized.") - - -class MailStats(object): - """The MailStats class tracks statistics on messages processed by MailBox.""" - - def __init__(self) -> None: - """ - Instantiate mail stats. - - :return: None - """ - self._search_count = 0 - self._search_start_time = {} # type: Dict[int, datetime.datetime] - self._search_timedelta = {} # type: Dict[int, float] - self._search_result_counts = {} # type: Dict[int, int] - - @property - def search_count(self) -> int: - """Get the search count.""" - return self._search_count - - def search_start(self, search_id: int) -> None: - """ - Add a search id and start time. - - :param search_id: the search id - - :return: None - """ - assert search_id not in self._search_start_time - self._search_count += 1 - self._search_start_time[search_id] = datetime.datetime.now() - - def search_end(self, search_id: int, nb_search_results: int) -> None: - """ - Add end time for a search id. - - :param search_id: the search id - :param nb_search_results: the number of agents returned in the search result - - :return: None - """ - assert search_id in self._search_start_time - assert search_id not in self._search_timedelta - self._search_timedelta[search_id] = (datetime.datetime.now() - self._search_start_time[search_id]).total_seconds() * 1000 - self._search_result_counts[search_id] = nb_search_results - - -class OEFChannel(OEFAgent, Channel): - """The OEFChannel connects the OEF Agent with the connection.""" - - def __init__(self, public_key: str, oef_addr: str, oef_port: int, core: AsyncioCore, in_queue: Queue): - """ - Initialize. - - :param public_key: the public key of the agent. - :param oef_addr: the OEF IP address. - :param oef_port: the OEF port. - :param in_queue: the in queue. - """ - super().__init__(public_key, oef_addr=oef_addr, oef_port=oef_port, core=core) - self.in_queue = in_queue - self.mail_stats = MailStats() - - def on_message(self, msg_id: int, dialogue_id: int, origin: str, content: bytes) -> None: - """ - On message event handler. - - :param msg_id: the message id. - :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. - :param content: the bytes content. - :return: None - """ - # We are not using the 'origin' parameter because 'content' contains a serialized instance of 'Envelope', - # hence it already contains the address of the sender. - envelope = Envelope.decode(content) - self.in_queue.put(envelope) - - def on_cfp(self, msg_id: int, dialogue_id: int, origin: str, target: int, query: CFP_TYPES) -> None: - """ - On cfp event handler. - - :param msg_id: the message id. - :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. - :param target: the message target. - :param query: the query. - :return: None - """ - try: - query = pickle.loads(query) - except Exception: - pass - msg = FIPAMessage(message_id=msg_id, - dialogue_id=dialogue_id, - target=target, - performative=FIPAMessage.Performative.CFP, - query=query if query != b"" else None) - msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_propose(self, msg_id: int, dialogue_id: int, origin: str, target: int, b_proposals: PROPOSE_TYPES) -> None: - """ - On propose event handler. - - :param msg_id: the message id. - :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. - :param target: the message target. - :param b_proposals: the proposals. - :return: None - """ - if type(b_proposals) == bytes: - proposals = pickle.loads(b_proposals) # type: List[Description] - else: - raise ValueError("No support for non-bytes proposals.") - - msg = FIPAMessage(message_id=msg_id, - dialogue_id=dialogue_id, - target=target, - performative=FIPAMessage.Performative.PROPOSE, - proposal=proposals) - msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_accept(self, msg_id: int, dialogue_id: int, origin: str, target: int) -> None: - """ - On accept event handler. - - :param msg_id: the message id. - :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. - :param target: the message target. - :return: None - """ - performative = FIPAMessage.Performative.MATCH_ACCEPT if msg_id == 4 and target == 3 else FIPAMessage.Performative.ACCEPT - msg = FIPAMessage(message_id=msg_id, - dialogue_id=dialogue_id, - target=target, - performative=performative) - msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_decline(self, msg_id: int, dialogue_id: int, origin: str, target: int) -> None: - """ - On decline event handler. - - :param msg_id: the message id. - :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. - :param target: the message target. - :return: None - """ - msg = FIPAMessage(message_id=msg_id, - dialogue_id=dialogue_id, - target=target, - performative=FIPAMessage.Performative.DECLINE) - msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_search_result(self, search_id: int, agents: List[str]) -> None: - """ - On accept event handler. - - :param search_id: the search id. - :param agents: the list of agents. - :return: None - """ - self.mail_stats.search_end(search_id, len(agents)) - msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_RESULT, id=search_id, agents=agents) - msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_oef_error(self, answer_id: int, operation: oef.messages.OEFErrorOperation) -> None: - """ - On oef error event handler. - - :param answer_id: the answer id. - :param operation: the error operation. - :return: None - """ - try: - operation = OEFMessage.OEFErrorOperation(operation) - except ValueError: - operation = OEFMessage.OEFErrorOperation.OTHER - - msg = OEFMessage(oef_type=OEFMessage.Type.OEF_ERROR, id=answer_id, operation=operation) - msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_dialogue_error(self, answer_id: int, dialogue_id: int, origin: str) -> None: - """ - On dialogue error event handler. - - :param answer_id: the answer id. - :param dialogue_id: the dialogue id. - :param origin: the message sender. - :return: None - """ - msg = OEFMessage(oef_type=OEFMessage.Type.DIALOGUE_ERROR, - id=answer_id, - dialogue_id=dialogue_id, - origin=origin) - msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def send(self, envelope: Envelope) -> None: - """ - Send message handler. - - :param envelope: the message. - :return: None - """ - if envelope.protocol_id == "default": - self.send_default_message(envelope) - elif envelope.protocol_id == "fipa": - self.send_fipa_message(envelope) - elif envelope.protocol_id == "oef": - self.send_oef_message(envelope) - elif envelope.protocol_id == "tac": - self.send_default_message(envelope) - else: - logger.error("This envelope cannot be sent: protocol_id={}".format(envelope.protocol_id)) - raise ValueError("Cannot send message.") - - def send_default_message(self, envelope: Envelope): - """Send a 'default' message.""" - self.send_message(STUB_MESSSAGE_ID, STUB_DIALOGUE_ID, envelope.to, envelope.encode()) - - def send_fipa_message(self, envelope: Envelope) -> None: - """ - Send fipa message handler. - - :param envelope: the message. - :return: None - """ - fipa_message = FIPASerializer().decode(envelope.message) - id = fipa_message.get("message_id") - dialogue_id = fipa_message.get("dialogue_id") - destination = envelope.to - target = fipa_message.get("target") - performative = FIPAMessage.Performative(fipa_message.get("performative")) - if performative == FIPAMessage.Performative.CFP: - query = fipa_message.get("query") - query = b"" if query is None else query - if type(query) == Query: - query = pickle.dumps(query) - self.send_cfp(id, dialogue_id, destination, target, query) - elif performative == FIPAMessage.Performative.PROPOSE: - proposal = cast(List[Description], fipa_message.get("proposal")) - proposal_b = pickle.dumps(proposal) # type: bytes - self.send_propose(id, dialogue_id, destination, target, proposal_b) - elif performative == FIPAMessage.Performative.ACCEPT: - self.send_accept(id, dialogue_id, destination, target) - elif performative == FIPAMessage.Performative.MATCH_ACCEPT: - self.send_accept(id, dialogue_id, destination, target) - elif performative == FIPAMessage.Performative.DECLINE: - self.send_decline(id, dialogue_id, destination, target) - else: - raise ValueError("OEF FIPA message not recognized.") - - def send_oef_message(self, envelope: Envelope) -> None: - """ - Send oef message handler. - - :param envelope: the message. - :return: None - """ - oef_message = OEFSerializer().decode(envelope.message) - oef_type = OEFMessage.Type(oef_message.get("type")) - oef_msg_id = cast(int, oef_message.get("id")) - if oef_type == OEFMessage.Type.REGISTER_SERVICE: - service_description = cast(Description, oef_message.get("service_description")) - service_id = cast(int, oef_message.get("service_id")) - oef_service_description = OEFObjectTranslator.to_oef_description(service_description) - self.register_service(oef_msg_id, oef_service_description, service_id) - elif oef_type == OEFMessage.Type.UNREGISTER_SERVICE: - service_description = cast(Description, oef_message.get("service_description")) - service_id = cast(int, oef_message.get("service_id")) - oef_service_description = OEFObjectTranslator.to_oef_description(service_description) - self.unregister_service(oef_msg_id, oef_service_description, service_id) - elif oef_type == OEFMessage.Type.SEARCH_AGENTS: - query = cast(Query, oef_message.get("query")) - oef_query = OEFObjectTranslator.to_oef_query(query) - self.search_agents(oef_msg_id, oef_query) - elif oef_type == OEFMessage.Type.SEARCH_SERVICES: - query = cast(Query, oef_message.get("query")) - oef_query = OEFObjectTranslator.to_oef_query(query) - self.mail_stats.search_start(oef_msg_id) - self.search_services(oef_msg_id, oef_query) - else: - raise ValueError("OEF request not recognized.") - - -class OEFConnection(Connection): - """The OEFConnection connects the to the mailbox.""" - - def __init__(self, public_key: str, oef_addr: str, oef_port: int = 10000): - """ - Initialize. - - :param public_key: the public key of the agent. - :param oef_addr: the OEF IP address. - :param oef_port: the OEF port. - """ - super().__init__() - core = AsyncioCore(logger=logger) - self._core = core # type: AsyncioCore - self.channel = OEFChannel(public_key, oef_addr, oef_port, core=core, in_queue=self.in_queue) - - self._stopped = True - self._connected = False - self.out_thread = None # type: Optional[Thread] - - @property - def is_established(self) -> bool: - """Get the connection status.""" - return self._connected - - def _fetch(self) -> None: - """ - Fetch the messages from the outqueue and send them. - - :return: None - """ - while self._connected: - try: - msg = self.out_queue.get(block=True, timeout=1.0) - self.send(msg) - except Empty: - pass - - def connect(self) -> None: - """ - Connect to the channel. - - :return: None - :raises ConnectionError if the connection to the OEF fails. - """ - if self._stopped and not self._connected: - self._stopped = False - self._core.run_threaded() - try: - if not self.channel.connect(): - raise ConnectionError("Cannot connect to OEFChannel.") - self._connected = True - self.out_thread = Thread(target=self._fetch) - self.out_thread.start() - except ConnectionError as e: - self._core.stop() - raise e - - def disconnect(self) -> None: - """ - Disconnect from the channel. - - :return: None - """ - assert self.out_thread is not None, "Call connect before disconnect." - if not self._stopped and self._connected: - self._connected = False - self.out_thread.join() - self.out_thread = None - self.channel.disconnect() - self._core.stop() - self._stopped = True - - def send(self, envelope: Envelope): - """ - Send messages. - - :return: None - """ - if self._connected: - self.channel.send(envelope) - - @classmethod - def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection': - """ - Get the OEF connection from the connection configuration. - - :param public_key: the public key of the agent. - :param connection_configuration: the connection configuration object. - :return: the connection object - """ - oef_addr = cast(str, connection_configuration.config.get("addr")) - oef_port = cast(int, connection_configuration.config.get("port")) - return OEFConnection(public_key, oef_addr, oef_port) - - -class OEFMailBox(MailBox): - """The OEF mail box.""" - - def __init__(self, public_key: str, oef_addr: str, oef_port: int = 10000): - """ - Initialize. - - :param public_key: the public key of the agent. - :param oef_addr: the OEF IP address. - :param oef_port: the OEF port. - """ - connection = OEFConnection(public_key, oef_addr, oef_port) - super().__init__(connection) - - @property - def mail_stats(self) -> MailStats: - """Get the mail stats object.""" - return self._connection.channel.mail_stats # type: ignore diff --git a/asdsd/connections/oef/connection.yaml b/asdsd/connections/oef/connection.yaml deleted file mode 100644 index 6a69ed33d1..0000000000 --- a/asdsd/connections/oef/connection.yaml +++ /dev/null @@ -1,18 +0,0 @@ -name: oef -authors: Fetch.AI Limited -version: 0.1.0 -license: Apache 2.0 -url: "" -description: "The oef connection provides a wrapper around the OEF sdk." -class_name: OEFConnection -supported_protocols: - - default - - oef - - fipa - - tac -config: - addr: ${OEF_ADDR:127.0.0.1} - port: ${OEF_PORT:10000} -dependencies: - - colorlog - - oef diff --git a/asdsd/protocols/__init__.py b/asdsd/protocols/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/asdsd/protocols/default/__init__.py b/asdsd/protocols/default/__init__.py deleted file mode 100644 index 52e51b51e3..0000000000 --- a/asdsd/protocols/default/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the support resources for the default protocol.""" diff --git a/asdsd/protocols/default/message.py b/asdsd/protocols/default/message.py deleted file mode 100644 index 475714a2f0..0000000000 --- a/asdsd/protocols/default/message.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the default message definition.""" -from enum import Enum -from typing import Optional - -from aea.protocols.base import Message - - -class DefaultMessage(Message): - """The Default message class.""" - - protocol_id = "default" - - class Type(Enum): - """Default message types.""" - - BYTES = "bytes" - ERROR = "error" - - def __str__(self): - """Get the string representation.""" - return self.value - - class ErrorCode(Enum): - """The error codes.""" - - UNSUPPORTED_PROTOCOL = -10001 - DECODING_ERROR = -10002 - INVALID_MESSAGE = -10003 - UNSUPPORTED_SKILL = -10004 - - def __init__(self, type: Optional[Type] = None, - **kwargs): - """ - Initialize. - - :param type: the type. - """ - super().__init__(type=type, **kwargs) - assert self.check_consistency(), "DefaultMessage initialization inconsistent." diff --git a/asdsd/protocols/default/protocol.yaml b/asdsd/protocols/default/protocol.yaml deleted file mode 100644 index 66d97cfc83..0000000000 --- a/asdsd/protocols/default/protocol.yaml +++ /dev/null @@ -1,6 +0,0 @@ -name: 'default' -authors: Fetch.AI Limited -version: 0.1.0 -license: Apache 2.0 -url: "" -description: "The default protocol allows for any bytes message." \ No newline at end of file diff --git a/asdsd/protocols/default/serialization.py b/asdsd/protocols/default/serialization.py deleted file mode 100644 index 080b8f386b..0000000000 --- a/asdsd/protocols/default/serialization.py +++ /dev/null @@ -1,71 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Serialization module for the default protocol.""" -import base64 -import json -from typing import cast - -from aea.protocols.base import Message -from aea.protocols.base import Serializer -from aea.protocols.default.message import DefaultMessage - - -class DefaultSerializer(Serializer): - """Serialization for the 'default' protocol.""" - - def encode(self, msg: Message) -> bytes: - """Encode a 'default' message into bytes.""" - body = {} # Dict[str, Any] - - msg_type = DefaultMessage.Type(msg.get("type")) - body["type"] = str(msg_type.value) - - if msg_type == DefaultMessage.Type.BYTES: - content = cast(bytes, msg.get("content")) - body["content"] = base64.b64encode(content).decode("utf-8") - elif msg_type == DefaultMessage.Type.ERROR: - body["error_code"] = cast(str, msg.get("error_code")) - body["error_msg"] = cast(str, msg.get("error_msg")) - body["error_data"] = cast(str, msg.get("error_data")) - else: - raise ValueError("Type not recognized.") - - bytes_msg = json.dumps(body).encode("utf-8") - return bytes_msg - - def decode(self, obj: bytes) -> Message: - """Decode bytes into a 'default' message.""" - json_body = json.loads(obj.decode("utf-8")) - body = {} - - msg_type = DefaultMessage.Type(json_body["type"]) - body["type"] = msg_type - if msg_type == DefaultMessage.Type.BYTES: - content = base64.b64decode(json_body["content"].encode("utf-8")) - body["content"] = content # type: ignore - elif msg_type == DefaultMessage.Type.ERROR: - body["error_code"] = json_body["error_code"] - body["error_msg"] = json_body["error_msg"] - body["error_data"] = json_body["error_data"] - else: - raise ValueError("Type not recognized.") - - return DefaultMessage(type=msg_type, body=body) diff --git a/asdsd/protocols/gym/__init__.py b/asdsd/protocols/gym/__init__.py deleted file mode 100644 index a1766ab9cd..0000000000 --- a/asdsd/protocols/gym/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the support resources for the Gym protocol.""" diff --git a/asdsd/protocols/gym/message.py b/asdsd/protocols/gym/message.py deleted file mode 100644 index 616b3e8226..0000000000 --- a/asdsd/protocols/gym/message.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the FIPA message definition.""" -from enum import Enum -from typing import Optional, Union - -from aea.protocols.base import Message - - -class GymMessage(Message): - """The Gym message class.""" - - protocol_id = "gym" - - class Performative(Enum): - """Gym performatives.""" - - ACT = 'act' - PERCEPT = 'percept' - RESET = 'reset' - CLOSE = 'close' - - def __str__(self): - """Get string representation.""" - return self.value - - def __init__(self, performative: Optional[Union[str, Performative]] = None, **kwargs): - """ - Initialize. - - :param type: the type. - """ - super().__init__(performative=GymMessage.Performative(performative), **kwargs) - assert self.check_consistency(), "GymMessage initialization inconsistent." - - def check_consistency(self) -> bool: - """Check that the data is consistent.""" - try: - assert self.is_set("performative") - performative = GymMessage.Performative(self.get("performative")) - if performative == GymMessage.Performative.ACT: - assert self.is_set("action") - assert self.is_set("step_id") - elif performative == GymMessage.Performative.PERCEPT: - assert self.is_set("observation") - assert self.is_set("reward") - assert self.is_set("done") - assert self.is_set("info") - assert self.is_set("step_id") - elif performative == GymMessage.Performative.RESET or performative == GymMessage.Performative.CLOSE: - pass - else: - raise ValueError("Performative not recognized.") - - except (AssertionError, ValueError, KeyError): - return False - - return True diff --git a/asdsd/protocols/gym/protocol.yaml b/asdsd/protocols/gym/protocol.yaml deleted file mode 100644 index 7b10d5429b..0000000000 --- a/asdsd/protocols/gym/protocol.yaml +++ /dev/null @@ -1,6 +0,0 @@ -name: gym -authors: Fetch.AI Limited -version: 0.1.0 -license: Apache 2.0 -url: "" -description: "The gym protocol implements the messages an agent needs to engage with a gym connection." \ No newline at end of file diff --git a/asdsd/protocols/gym/serialization.py b/asdsd/protocols/gym/serialization.py deleted file mode 100644 index 6fb14c1354..0000000000 --- a/asdsd/protocols/gym/serialization.py +++ /dev/null @@ -1,103 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Serialization for the FIPA protocol.""" -import base64 -import copy -import json -import pickle -from typing import Any, TYPE_CHECKING - -from aea.protocols.base import Message -from aea.protocols.base import Serializer - -if TYPE_CHECKING: - from packages.protocols.gym.message import GymMessage -else: - from gym_protocol.message import GymMessage - - -class GymSerializer(Serializer): - """Serialization for the Gym protocol.""" - - def encode(self, msg: Message) -> bytes: - """ - Decode the message. - - :param msg: the message object - :return: the bytes - """ - performative = GymMessage.Performative(msg.get("performative")) - new_body = copy.copy(msg.body) - new_body["performative"] = performative.value - - if performative == GymMessage.Performative.ACT: - action = msg.body["action"] # type: Any - action_bytes = base64.b64encode(pickle.dumps(action)).decode("utf-8") - new_body["action"] = action_bytes - new_body["step_id"] = msg.body["step_id"] - elif performative == GymMessage.Performative.PERCEPT: - # observation, reward and info are gym implementation specific, done is boolean - observation = msg.body["observation"] # type: Any - observation_bytes = base64.b64encode(pickle.dumps(observation)).decode("utf-8") - new_body["observation"] = observation_bytes - reward = msg.body["reward"] # type: Any - reward_bytes = base64.b64encode(pickle.dumps(reward)).decode("utf-8") - new_body["reward"] = reward_bytes - info = msg.body["info"] # type: Any - info_bytes = base64.b64encode(pickle.dumps(info)).decode("utf-8") - new_body["info"] = info_bytes - new_body["step_id"] = msg.body["step_id"] - - gym_message_bytes = json.dumps(new_body).encode("utf-8") - return gym_message_bytes - - def decode(self, obj: bytes) -> Message: - """ - Decode the message. - - :param obj: the bytes object - :return: the message - """ - json_msg = json.loads(obj.decode("utf-8")) - performative = GymMessage.Performative(json_msg["performative"]) - new_body = copy.copy(json_msg) - new_body["type"] = performative - - if performative == GymMessage.Performative.ACT: - action_bytes = base64.b64decode(json_msg["action"]) - action = pickle.loads(action_bytes) - new_body["action"] = action - new_body["step_id"] = json_msg["step_id"] - elif performative == GymMessage.Performative.PERCEPT: - # observation, reward and info are gym implementation specific, done is boolean - observation_bytes = base64.b64decode(json_msg["observation"]) - observation = pickle.loads(observation_bytes) - new_body["observation"] = observation - reward_bytes = base64.b64decode(json_msg["reward"]) - reward = pickle.loads(reward_bytes) - new_body["reward"] = reward - info_bytes = base64.b64decode(json_msg["info"]) - info = pickle.loads(info_bytes) - new_body["info"] = info - new_body["step_id"] = json_msg["step_id"] - - gym_message = GymMessage(performative=performative, body=new_body) - return gym_message diff --git a/asdsd/protocols/tac/__init__.py b/asdsd/protocols/tac/__init__.py deleted file mode 100644 index 430d160a4f..0000000000 --- a/asdsd/protocols/tac/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the support resources for the TAC protocol.""" diff --git a/asdsd/protocols/tac/message.py b/asdsd/protocols/tac/message.py deleted file mode 100644 index 07a100d90e..0000000000 --- a/asdsd/protocols/tac/message.py +++ /dev/null @@ -1,134 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the default message definition.""" -from enum import Enum -from typing import Dict, Optional, cast - -from aea.protocols.base import Message - - -class TACMessage(Message): - """The TAC message class.""" - - protocol_id = "tac" - - class Type(Enum): - """TAC Message types.""" - - REGISTER = "register" - UNREGISTER = "unregister" - TRANSACTION = "transaction" - GET_STATE_UPDATE = "get_state_update" - CANCELLED = "cancelled" - GAME_DATA = "game_data" - TRANSACTION_CONFIRMATION = "transaction_confirmation" - STATE_UPDATE = "state_update" - TAC_ERROR = "tac_error" - - def __str__(self): - """Get string representation.""" - return self.value - - class ErrorCode(Enum): - """This class defines the error codes.""" - - GENERIC_ERROR = 0 - REQUEST_NOT_VALID = 1 - AGENT_PBK_ALREADY_REGISTERED = 2 - AGENT_NAME_ALREADY_REGISTERED = 3 - AGENT_NOT_REGISTERED = 4 - TRANSACTION_NOT_VALID = 5 - TRANSACTION_NOT_MATCHING = 6 - AGENT_NAME_NOT_IN_WHITELIST = 7 - COMPETITION_NOT_RUNNING = 8 - DIALOGUE_INCONSISTENT = 9 - - _from_ec_to_msg = { - ErrorCode.GENERIC_ERROR: "Unexpected error.", - ErrorCode.REQUEST_NOT_VALID: "Request not recognized", - ErrorCode.AGENT_PBK_ALREADY_REGISTERED: "Agent pbk already registered.", - ErrorCode.AGENT_NAME_ALREADY_REGISTERED: "Agent name already registered.", - ErrorCode.AGENT_NOT_REGISTERED: "Agent not registered.", - ErrorCode.TRANSACTION_NOT_VALID: "Error in checking transaction", - ErrorCode.TRANSACTION_NOT_MATCHING: "The transaction request does not match with a previous transaction request with the same id.", - ErrorCode.AGENT_NAME_NOT_IN_WHITELIST: "Agent name not in whitelist.", - ErrorCode.COMPETITION_NOT_RUNNING: "The competition is not running yet.", - ErrorCode.DIALOGUE_INCONSISTENT: "The message is inconsistent with the dialogue." - } # type: Dict[ErrorCode, str] - - def __init__(self, tac_type: Optional[Type] = None, - **kwargs): - """ - Initialize. - - :param tac_type: the type of TAC message. - """ - super().__init__(type=tac_type, **kwargs) - assert self.check_consistency(), "TACMessage initialization inconsistent." - - def check_consistency(self) -> bool: - """Check that the data is consistent.""" - try: - assert self.is_set("type") - tac_type = TACMessage.Type(self.get("type")) - if tac_type == TACMessage.Type.REGISTER: - assert self.is_set("agent_name") - elif tac_type == TACMessage.Type.UNREGISTER: - pass - elif tac_type == TACMessage.Type.TRANSACTION: - assert self.is_set("transaction_id") - assert self.is_set("is_sender_buyer") - assert self.is_set("counterparty") - assert self.is_set("amount") - amount = cast(float, self.get("amount")) - assert amount >= 0.0 - assert self.is_set("quantities_by_good_pbk") - quantities_by_good_pbk = cast(Dict[str, int], self.get("quantities_by_good_pbk")) - assert len(quantities_by_good_pbk.keys()) == len(set(quantities_by_good_pbk.keys())) - assert all(quantity >= 0 for quantity in quantities_by_good_pbk.values()) - elif tac_type == TACMessage.Type.GET_STATE_UPDATE: - pass - elif tac_type == TACMessage.Type.CANCELLED: - pass - elif tac_type == TACMessage.Type.GAME_DATA: - assert self.is_set("money") - assert self.is_set("endowment") - assert self.is_set("utility_params") - assert self.is_set("nb_agents") - assert self.is_set("nb_goods") - assert self.is_set("tx_fee") - assert self.is_set("agent_pbk_to_name") - assert self.is_set("good_pbk_to_name") - elif tac_type == TACMessage.Type.TRANSACTION_CONFIRMATION: - assert self.is_set("transaction_id") - elif tac_type == TACMessage.Type.STATE_UPDATE: - assert self.is_set("initial_state") - assert self.is_set("transactions") - elif tac_type == TACMessage.Type.TAC_ERROR: - assert self.is_set("error_code") - error_code = self.get("error_code") - assert error_code in set(self.ErrorCode) - else: - raise ValueError("Type not recognized.") - except (AssertionError, ValueError): - return False - - return True diff --git a/asdsd/protocols/tac/protocol.yaml b/asdsd/protocols/tac/protocol.yaml deleted file mode 100644 index 435e33b383..0000000000 --- a/asdsd/protocols/tac/protocol.yaml +++ /dev/null @@ -1,6 +0,0 @@ -name: tac -authors: Fetch.AI Limited -version: 0.1.0 -license: Apache 2.0 -url: "" -description: "The tac protocol implements the messages an AEA needs to participate in the TAC." \ No newline at end of file diff --git a/asdsd/protocols/tac/serialization.py b/asdsd/protocols/tac/serialization.py deleted file mode 100644 index e4e085d5b0..0000000000 --- a/asdsd/protocols/tac/serialization.py +++ /dev/null @@ -1,234 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Serialization for the TAC protocol.""" - -from typing import Any, Dict, List, cast, TYPE_CHECKING - -from aea.protocols.base import Message -from aea.protocols.base import Serializer - -if TYPE_CHECKING: - from packages.protocols.tac import tac_pb2 - from packages.protocols.tac.message import TACMessage -else: - import tac_protocol.tac_pb2 as tac_pb2 - from tac_protocol.message import TACMessage - - -def _from_dict_to_pairs(d): - """Convert a flat dictionary into a list of StrStrPair or StrIntPair.""" - result = [] - items = sorted(d.items(), key=lambda pair: pair[0]) - for key, value in items: - if type(value) == int: - pair = tac_pb2.StrIntPair() - elif type(value) == str: - pair = tac_pb2.StrStrPair() - else: - raise ValueError("Either 'int' or 'str', not {}".format(type(value))) - pair.first = key - pair.second = value - result.append(pair) - return result - - -def _from_pairs_to_dict(pairs): - """Convert a list of StrStrPair or StrIntPair into a flat dictionary.""" - result = {} - for pair in pairs: - key = pair.first - value = pair.second - result[key] = value - return result - - -class TACSerializer(Serializer): - """Serialization for the TAC protocol.""" - - def encode(self, msg: Message) -> bytes: - """ - Decode the message. - - :param msg: the message object - :return: the bytes - """ - tac_type = TACMessage.Type(msg.get("type")) - tac_container = tac_pb2.TACMessage() - - if tac_type == TACMessage.Type.REGISTER: - agent_name = msg.get("agent_name") - tac_msg = tac_pb2.TACAgent.Register() # type: ignore - tac_msg.agent_name = agent_name - tac_container.register.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.UNREGISTER: - tac_msg = tac_pb2.TACAgent.Unregister() # type: ignore - tac_container.unregister.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.TRANSACTION: - tac_msg = tac_pb2.TACAgent.Transaction() # type: ignore - tac_msg.transaction_id = msg.get("transaction_id") - tac_msg.is_sender_buyer = msg.get("is_sender_buyer") - tac_msg.counterparty = msg.get("counterparty") - tac_msg.amount = msg.get("amount") - tac_msg.quantities.extend(_from_dict_to_pairs(msg.get("quantities_by_good_pbk"))) - tac_container.transaction.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.GET_STATE_UPDATE: - tac_msg = tac_pb2.TACAgent.GetStateUpdate() # type: ignore - tac_container.get_state_update.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.CANCELLED: - tac_msg = tac_pb2.TACController.Cancelled() # type: ignore - tac_container.cancelled.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.GAME_DATA: - tac_msg = tac_pb2.TACController.GameData() # type: ignore - tac_msg.money = msg.get("money") - tac_msg.endowment.extend(msg.get("endowment")) - tac_msg.utility_params.extend(msg.get("utility_params")) - tac_msg.nb_agents = msg.get("nb_agents") - tac_msg.nb_goods = msg.get("nb_goods") - tac_msg.tx_fee = msg.get("tx_fee") - tac_msg.agent_pbk_to_name.extend(_from_dict_to_pairs(msg.get("agent_pbk_to_name"))) - tac_msg.good_pbk_to_name.extend(_from_dict_to_pairs(msg.get("good_pbk_to_name"))) - tac_container.game_data.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.TRANSACTION_CONFIRMATION: - tac_msg = tac_pb2.TACController.TransactionConfirmation() # type: ignore - tac_msg.transaction_id = msg.get("transaction_id") - tac_container.transaction_confirmation.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.STATE_UPDATE: - tac_msg = tac_pb2.TACController.StateUpdate() # type: ignore - game_data_json = msg.get("initial_state") - game_data = tac_pb2.TACController.GameData() # type: ignore - game_data.money = game_data_json["money"] # type: ignore - game_data.endowment.extend(game_data_json["endowment"]) # type: ignore - game_data.utility_params.extend(game_data_json["utility_params"]) # type: ignore - game_data.nb_agents = game_data_json["nb_agents"] # type: ignore - game_data.nb_goods = game_data_json["nb_goods"] # type: ignore - game_data.tx_fee = game_data_json["tx_fee"] # type: ignore - game_data.agent_pbk_to_name.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["agent_pbk_to_name"]))) # type: ignore - game_data.good_pbk_to_name.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["good_pbk_to_name"]))) # type: ignore - - tac_msg.initial_state.CopyFrom(game_data) - - transactions = [] - msg_transactions = cast(List[Any], msg.get("transactions")) - for t in msg_transactions: - tx = tac_pb2.TACAgent.Transaction() # type: ignore - tx.transaction_id = t.get("transaction_id") - tx.is_sender_buyer = t.get("is_sender_buyer") - tx.counterparty = t.get("counterparty") - tx.amount = t.get("amount") - tx.quantities.extend(_from_dict_to_pairs(t.get("quantities_by_good_pbk"))) - transactions.append(tx) - tac_msg.txs.extend(transactions) - tac_container.state_update.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.TAC_ERROR: - tac_msg = tac_pb2.TACController.Error() # type: ignore - tac_msg.error_code = TACMessage.ErrorCode(msg.get("error_code")).value - if msg.is_set("error_msg"): - tac_msg.error_msg = msg.get("error_msg") - if msg.is_set("details"): - tac_msg.details.update(msg.get("details")) - - tac_container.error.CopyFrom(tac_msg) - else: - raise ValueError("Type not recognized: {}.".format(tac_type)) - - tac_message_bytes = tac_container.SerializeToString() - return tac_message_bytes - - def decode(self, obj: bytes) -> Message: - """ - Decode the message. - - :param obj: the bytes object - :return: the message - """ - tac_container = tac_pb2.TACMessage() - tac_container.ParseFromString(obj) - - new_body = {} # type: Dict[str, Any] - tac_type = tac_container.WhichOneof("content") - - if tac_type == "register": - new_body["type"] = TACMessage.Type.REGISTER - new_body["agent_name"] = tac_container.register.agent_name - elif tac_type == "unregister": - new_body["type"] = TACMessage.Type.UNREGISTER - elif tac_type == "transaction": - new_body["type"] = TACMessage.Type.TRANSACTION - new_body["transaction_id"] = tac_container.transaction.transaction_id - new_body["is_sender_buyer"] = tac_container.transaction.is_sender_buyer - new_body["counterparty"] = tac_container.transaction.counterparty - new_body["amount"] = tac_container.transaction.amount - new_body["quantities_by_good_pbk"] = _from_pairs_to_dict(tac_container.transaction.quantities) - elif tac_type == "get_state_update": - new_body["type"] = TACMessage.Type.GET_STATE_UPDATE - elif tac_type == "cancelled": - new_body["type"] = TACMessage.Type.CANCELLED - elif tac_type == "game_data": - new_body["type"] = TACMessage.Type.GAME_DATA - new_body["money"] = tac_container.game_data.money - new_body["endowment"] = list(tac_container.game_data.endowment) - new_body["utility_params"] = list(tac_container.game_data.utility_params) - new_body["nb_agents"] = tac_container.game_data.nb_agents - new_body["nb_goods"] = tac_container.game_data.nb_goods - new_body["tx_fee"] = tac_container.game_data.tx_fee - new_body["agent_pbk_to_name"] = _from_pairs_to_dict(tac_container.game_data.agent_pbk_to_name) - new_body["good_pbk_to_name"] = _from_pairs_to_dict(tac_container.game_data.good_pbk_to_name) - elif tac_type == "transaction_confirmation": - new_body["type"] = TACMessage.Type.TRANSACTION_CONFIRMATION - new_body["transaction_id"] = tac_container.transaction_confirmation.transaction_id - elif tac_type == "state_update": - new_body["type"] = TACMessage.Type.STATE_UPDATE - game_data = dict( - money=tac_container.state_update.initial_state.money, - endowment=tac_container.state_update.initial_state.endowment, - utility_params=tac_container.state_update.initial_state.utility_params, - nb_agents=tac_container.state_update.initial_state.nb_agents, - nb_goods=tac_container.state_update.initial_state.nb_goods, - tx_fee=tac_container.state_update.initial_state.tx_fee, - agent_pbk_to_name=_from_pairs_to_dict(tac_container.state_update.initial_state.agent_pbk_to_name), - good_pbk_to_name=_from_pairs_to_dict(tac_container.state_update.initial_state.good_pbk_to_name), - ) - new_body["initial_state"] = game_data - transactions = [] - for t in tac_container.state_update.txs: - tx_json = dict( - transaction_id=t.transaction_id, - is_sender_buyer=t.is_sender_buyer, - counterparty=t.counterparty, - amount=t.amount, - quantities_by_good_pbk=_from_pairs_to_dict(t.quantities), - ) - transactions.append(tx_json) - new_body["transactions"] = transactions - elif tac_type == "error": - new_body["type"] = TACMessage.Type.TAC_ERROR - new_body["error_code"] = TACMessage.ErrorCode(tac_container.error.error_code) - if tac_container.error.error_msg: - new_body["error_msg"] = tac_container.error.error_msg - if tac_container.error.details: - new_body["details"] = dict(tac_container.error.details) - else: - raise ValueError("Type not recognized.") - - tac_type = TACMessage.Type(new_body["type"]) - new_body["type"] = tac_type - tac_message = TACMessage(tac_type=tac_type, body=new_body) - return tac_message diff --git a/asdsd/protocols/tac/tac.proto b/asdsd/protocols/tac/tac.proto deleted file mode 100644 index d6a714d425..0000000000 --- a/asdsd/protocols/tac/tac.proto +++ /dev/null @@ -1,102 +0,0 @@ -syntax = "proto3"; - -package fetch.oef.pb; - -import "google/protobuf/struct.proto"; - -message StrIntPair { - string first = 1; - int32 second = 2; -} - -message StrStrPair { - string first = 1; - string second = 2; -} - -message TACController { - - message Registered { - } - message Unregistered { - } - message Cancelled { - } - - message GameData { - double money = 1; - repeated int32 endowment = 2; - repeated double utility_params = 3; - int32 nb_agents = 4; - int32 nb_goods = 5; - double tx_fee = 6; - repeated StrStrPair agent_pbk_to_name = 7; - repeated StrStrPair good_pbk_to_name = 8; - } - - message TransactionConfirmation { - string transaction_id = 1; - } - - message StateUpdate { - GameData initial_state = 1; - repeated TACAgent.Transaction txs = 2; - } - - message Error { - enum ErrorCode { - GENERIC_ERROR = 0; - REQUEST_NOT_VALID = 1; - AGENT_PBK_ALREADY_REGISTERED = 2; - AGENT_NAME_ALREADY_REGISTERED = 3; - AGENT_NOT_REGISTERED = 4; - TRANSACTION_NOT_VALID = 5; - TRANSACTION_NOT_MATCHING = 6; - AGENT_NAME_NOT_IN_WHITELIST = 7; - COMPETITION_NOT_RUNNING = 8; - DIALOGUE_INCONSISTENT = 9; - } - - ErrorCode error_code = 1; - string error_msg = 2; - google.protobuf.Struct details = 3; - } - -} - -message TACAgent { - - message Register { - string agent_name = 1; - } - message Unregister { - } - - message Transaction { - string transaction_id = 1; - bool is_sender_buyer = 2; // is the sender of this message a buyer? - string counterparty = 3; - double amount = 4; - repeated StrIntPair quantities = 5; - } - - message GetStateUpdate { - } - -} - -message TACMessage { - oneof content{ - TACAgent.Register register = 1; - TACAgent.Unregister unregister = 2; - TACAgent.Transaction transaction = 3; - TACAgent.GetStateUpdate get_state_update = 4; - TACController.Registered registered = 5; - TACController.Unregistered unregistered = 6; - TACController.Cancelled cancelled = 7; - TACController.GameData game_data = 8; - TACController.TransactionConfirmation transaction_confirmation = 9; - TACController.StateUpdate state_update = 10; - TACController.Error error = 11; - } -} diff --git a/asdsd/protocols/tac/tac_pb2.py b/asdsd/protocols/tac/tac_pb2.py deleted file mode 100644 index 5eb63f23f1..0000000000 --- a/asdsd/protocols/tac/tac_pb2.py +++ /dev/null @@ -1,899 +0,0 @@ -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: tac.proto - -import sys -_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection -from google.protobuf import symbol_database as _symbol_database -from google.protobuf import descriptor_pb2 -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='tac.proto', - package='fetch.oef.pb', - syntax='proto3', - serialized_pb=_b('\n\ttac.proto\x12\x0c\x66\x65tch.oef.pb\x1a\x1cgoogle/protobuf/struct.proto\"+\n\nStrIntPair\x12\r\n\x05\x66irst\x18\x01 \x01(\t\x12\x0e\n\x06second\x18\x02 \x01(\x05\"+\n\nStrStrPair\x12\r\n\x05\x66irst\x18\x01 \x01(\t\x12\x0e\n\x06second\x18\x02 \x01(\t\"\x80\x07\n\rTACController\x1a\x0c\n\nRegistered\x1a\x0e\n\x0cUnregistered\x1a\x0b\n\tCancelled\x1a\xe2\x01\n\x08GameData\x12\r\n\x05money\x18\x01 \x01(\x01\x12\x11\n\tendowment\x18\x02 \x03(\x05\x12\x16\n\x0eutility_params\x18\x03 \x03(\x01\x12\x11\n\tnb_agents\x18\x04 \x01(\x05\x12\x10\n\x08nb_goods\x18\x05 \x01(\x05\x12\x0e\n\x06tx_fee\x18\x06 \x01(\x01\x12\x33\n\x11\x61gent_pbk_to_name\x18\x07 \x03(\x0b\x32\x18.fetch.oef.pb.StrStrPair\x12\x32\n\x10good_pbk_to_name\x18\x08 \x03(\x0b\x32\x18.fetch.oef.pb.StrStrPair\x1a\x31\n\x17TransactionConfirmation\x12\x16\n\x0etransaction_id\x18\x01 \x01(\t\x1a{\n\x0bStateUpdate\x12;\n\rinitial_state\x18\x01 \x01(\x0b\x32$.fetch.oef.pb.TACController.GameData\x12/\n\x03txs\x18\x02 \x03(\x0b\x32\".fetch.oef.pb.TACAgent.Transaction\x1a\xae\x03\n\x05\x45rror\x12?\n\nerror_code\x18\x01 \x01(\x0e\x32+.fetch.oef.pb.TACController.Error.ErrorCode\x12\x11\n\terror_msg\x18\x02 \x01(\t\x12(\n\x07\x64\x65tails\x18\x03 \x01(\x0b\x32\x17.google.protobuf.Struct\"\xa6\x02\n\tErrorCode\x12\x11\n\rGENERIC_ERROR\x10\x00\x12\x15\n\x11REQUEST_NOT_VALID\x10\x01\x12 \n\x1c\x41GENT_PBK_ALREADY_REGISTERED\x10\x02\x12!\n\x1d\x41GENT_NAME_ALREADY_REGISTERED\x10\x03\x12\x18\n\x14\x41GENT_NOT_REGISTERED\x10\x04\x12\x19\n\x15TRANSACTION_NOT_VALID\x10\x05\x12\x1c\n\x18TRANSACTION_NOT_MATCHING\x10\x06\x12\x1f\n\x1b\x41GENT_NAME_NOT_IN_WHITELIST\x10\x07\x12\x1b\n\x17\x43OMPETITION_NOT_RUNNING\x10\x08\x12\x19\n\x15\x44IALOGUE_INCONSISTENT\x10\t\"\xdf\x01\n\x08TACAgent\x1a\x1e\n\x08Register\x12\x12\n\nagent_name\x18\x01 \x01(\t\x1a\x0c\n\nUnregister\x1a\x92\x01\n\x0bTransaction\x12\x16\n\x0etransaction_id\x18\x01 \x01(\t\x12\x17\n\x0fis_sender_buyer\x18\x02 \x01(\x08\x12\x14\n\x0c\x63ounterparty\x18\x03 \x01(\t\x12\x0e\n\x06\x61mount\x18\x04 \x01(\x01\x12,\n\nquantities\x18\x05 \x03(\x0b\x32\x18.fetch.oef.pb.StrIntPair\x1a\x10\n\x0eGetStateUpdate\"\xc8\x05\n\nTACMessage\x12\x33\n\x08register\x18\x01 \x01(\x0b\x32\x1f.fetch.oef.pb.TACAgent.RegisterH\x00\x12\x37\n\nunregister\x18\x02 \x01(\x0b\x32!.fetch.oef.pb.TACAgent.UnregisterH\x00\x12\x39\n\x0btransaction\x18\x03 \x01(\x0b\x32\".fetch.oef.pb.TACAgent.TransactionH\x00\x12\x41\n\x10get_state_update\x18\x04 \x01(\x0b\x32%.fetch.oef.pb.TACAgent.GetStateUpdateH\x00\x12<\n\nregistered\x18\x05 \x01(\x0b\x32&.fetch.oef.pb.TACController.RegisteredH\x00\x12@\n\x0cunregistered\x18\x06 \x01(\x0b\x32(.fetch.oef.pb.TACController.UnregisteredH\x00\x12:\n\tcancelled\x18\x07 \x01(\x0b\x32%.fetch.oef.pb.TACController.CancelledH\x00\x12\x39\n\tgame_data\x18\x08 \x01(\x0b\x32$.fetch.oef.pb.TACController.GameDataH\x00\x12W\n\x18transaction_confirmation\x18\t \x01(\x0b\x32\x33.fetch.oef.pb.TACController.TransactionConfirmationH\x00\x12?\n\x0cstate_update\x18\n \x01(\x0b\x32\'.fetch.oef.pb.TACController.StateUpdateH\x00\x12\x32\n\x05\x65rror\x18\x0b \x01(\x0b\x32!.fetch.oef.pb.TACController.ErrorH\x00\x42\t\n\x07\x63ontentb\x06proto3') - , - dependencies=[google_dot_protobuf_dot_struct__pb2.DESCRIPTOR,]) -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - - - -_TACCONTROLLER_ERROR_ERRORCODE = _descriptor.EnumDescriptor( - name='ErrorCode', - full_name='fetch.oef.pb.TACController.Error.ErrorCode', - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name='GENERIC_ERROR', index=0, number=0, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='REQUEST_NOT_VALID', index=1, number=1, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='AGENT_PBK_ALREADY_REGISTERED', index=2, number=2, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='AGENT_NAME_ALREADY_REGISTERED', index=3, number=3, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='AGENT_NOT_REGISTERED', index=4, number=4, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='TRANSACTION_NOT_VALID', index=5, number=5, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='TRANSACTION_NOT_MATCHING', index=6, number=6, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='AGENT_NAME_NOT_IN_WHITELIST', index=7, number=7, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='COMPETITION_NOT_RUNNING', index=8, number=8, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='DIALOGUE_INCONSISTENT', index=9, number=9, - options=None, - type=None), - ], - containing_type=None, - options=None, - serialized_start=750, - serialized_end=1044, -) -_sym_db.RegisterEnumDescriptor(_TACCONTROLLER_ERROR_ERRORCODE) - - -_STRINTPAIR = _descriptor.Descriptor( - name='StrIntPair', - full_name='fetch.oef.pb.StrIntPair', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='first', full_name='fetch.oef.pb.StrIntPair.first', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='second', full_name='fetch.oef.pb.StrIntPair.second', index=1, - number=2, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=57, - serialized_end=100, -) - - -_STRSTRPAIR = _descriptor.Descriptor( - name='StrStrPair', - full_name='fetch.oef.pb.StrStrPair', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='first', full_name='fetch.oef.pb.StrStrPair.first', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='second', full_name='fetch.oef.pb.StrStrPair.second', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=102, - serialized_end=145, -) - - -_TACCONTROLLER_REGISTERED = _descriptor.Descriptor( - name='Registered', - full_name='fetch.oef.pb.TACController.Registered', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=165, - serialized_end=177, -) - -_TACCONTROLLER_UNREGISTERED = _descriptor.Descriptor( - name='Unregistered', - full_name='fetch.oef.pb.TACController.Unregistered', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=179, - serialized_end=193, -) - -_TACCONTROLLER_CANCELLED = _descriptor.Descriptor( - name='Cancelled', - full_name='fetch.oef.pb.TACController.Cancelled', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=195, - serialized_end=206, -) - -_TACCONTROLLER_GAMEDATA = _descriptor.Descriptor( - name='GameData', - full_name='fetch.oef.pb.TACController.GameData', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='money', full_name='fetch.oef.pb.TACController.GameData.money', index=0, - number=1, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='endowment', full_name='fetch.oef.pb.TACController.GameData.endowment', index=1, - number=2, type=5, cpp_type=1, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='utility_params', full_name='fetch.oef.pb.TACController.GameData.utility_params', index=2, - number=3, type=1, cpp_type=5, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='nb_agents', full_name='fetch.oef.pb.TACController.GameData.nb_agents', index=3, - number=4, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='nb_goods', full_name='fetch.oef.pb.TACController.GameData.nb_goods', index=4, - number=5, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='tx_fee', full_name='fetch.oef.pb.TACController.GameData.tx_fee', index=5, - number=6, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='agent_pbk_to_name', full_name='fetch.oef.pb.TACController.GameData.agent_pbk_to_name', index=6, - number=7, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='good_pbk_to_name', full_name='fetch.oef.pb.TACController.GameData.good_pbk_to_name', index=7, - number=8, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=209, - serialized_end=435, -) - -_TACCONTROLLER_TRANSACTIONCONFIRMATION = _descriptor.Descriptor( - name='TransactionConfirmation', - full_name='fetch.oef.pb.TACController.TransactionConfirmation', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='transaction_id', full_name='fetch.oef.pb.TACController.TransactionConfirmation.transaction_id', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=437, - serialized_end=486, -) - -_TACCONTROLLER_STATEUPDATE = _descriptor.Descriptor( - name='StateUpdate', - full_name='fetch.oef.pb.TACController.StateUpdate', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='initial_state', full_name='fetch.oef.pb.TACController.StateUpdate.initial_state', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='txs', full_name='fetch.oef.pb.TACController.StateUpdate.txs', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=488, - serialized_end=611, -) - -_TACCONTROLLER_ERROR = _descriptor.Descriptor( - name='Error', - full_name='fetch.oef.pb.TACController.Error', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='error_code', full_name='fetch.oef.pb.TACController.Error.error_code', index=0, - number=1, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='error_msg', full_name='fetch.oef.pb.TACController.Error.error_msg', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='details', full_name='fetch.oef.pb.TACController.Error.details', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _TACCONTROLLER_ERROR_ERRORCODE, - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=614, - serialized_end=1044, -) - -_TACCONTROLLER = _descriptor.Descriptor( - name='TACController', - full_name='fetch.oef.pb.TACController', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[_TACCONTROLLER_REGISTERED, _TACCONTROLLER_UNREGISTERED, _TACCONTROLLER_CANCELLED, _TACCONTROLLER_GAMEDATA, _TACCONTROLLER_TRANSACTIONCONFIRMATION, _TACCONTROLLER_STATEUPDATE, _TACCONTROLLER_ERROR, ], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=148, - serialized_end=1044, -) - - -_TACAGENT_REGISTER = _descriptor.Descriptor( - name='Register', - full_name='fetch.oef.pb.TACAgent.Register', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='agent_name', full_name='fetch.oef.pb.TACAgent.Register.agent_name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1059, - serialized_end=1089, -) - -_TACAGENT_UNREGISTER = _descriptor.Descriptor( - name='Unregister', - full_name='fetch.oef.pb.TACAgent.Unregister', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1091, - serialized_end=1103, -) - -_TACAGENT_TRANSACTION = _descriptor.Descriptor( - name='Transaction', - full_name='fetch.oef.pb.TACAgent.Transaction', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='transaction_id', full_name='fetch.oef.pb.TACAgent.Transaction.transaction_id', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='is_sender_buyer', full_name='fetch.oef.pb.TACAgent.Transaction.is_sender_buyer', index=1, - number=2, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='counterparty', full_name='fetch.oef.pb.TACAgent.Transaction.counterparty', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='amount', full_name='fetch.oef.pb.TACAgent.Transaction.amount', index=3, - number=4, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='quantities', full_name='fetch.oef.pb.TACAgent.Transaction.quantities', index=4, - number=5, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1106, - serialized_end=1252, -) - -_TACAGENT_GETSTATEUPDATE = _descriptor.Descriptor( - name='GetStateUpdate', - full_name='fetch.oef.pb.TACAgent.GetStateUpdate', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1254, - serialized_end=1270, -) - -_TACAGENT = _descriptor.Descriptor( - name='TACAgent', - full_name='fetch.oef.pb.TACAgent', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[_TACAGENT_REGISTER, _TACAGENT_UNREGISTER, _TACAGENT_TRANSACTION, _TACAGENT_GETSTATEUPDATE, ], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1047, - serialized_end=1270, -) - - -_TACMESSAGE = _descriptor.Descriptor( - name='TACMessage', - full_name='fetch.oef.pb.TACMessage', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='register', full_name='fetch.oef.pb.TACMessage.register', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='unregister', full_name='fetch.oef.pb.TACMessage.unregister', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='transaction', full_name='fetch.oef.pb.TACMessage.transaction', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='get_state_update', full_name='fetch.oef.pb.TACMessage.get_state_update', index=3, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='registered', full_name='fetch.oef.pb.TACMessage.registered', index=4, - number=5, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='unregistered', full_name='fetch.oef.pb.TACMessage.unregistered', index=5, - number=6, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='cancelled', full_name='fetch.oef.pb.TACMessage.cancelled', index=6, - number=7, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='game_data', full_name='fetch.oef.pb.TACMessage.game_data', index=7, - number=8, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='transaction_confirmation', full_name='fetch.oef.pb.TACMessage.transaction_confirmation', index=8, - number=9, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='state_update', full_name='fetch.oef.pb.TACMessage.state_update', index=9, - number=10, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='error', full_name='fetch.oef.pb.TACMessage.error', index=10, - number=11, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='content', full_name='fetch.oef.pb.TACMessage.content', - index=0, containing_type=None, fields=[]), - ], - serialized_start=1273, - serialized_end=1985, -) - -_TACCONTROLLER_REGISTERED.containing_type = _TACCONTROLLER -_TACCONTROLLER_UNREGISTERED.containing_type = _TACCONTROLLER -_TACCONTROLLER_CANCELLED.containing_type = _TACCONTROLLER -_TACCONTROLLER_GAMEDATA.fields_by_name['agent_pbk_to_name'].message_type = _STRSTRPAIR -_TACCONTROLLER_GAMEDATA.fields_by_name['good_pbk_to_name'].message_type = _STRSTRPAIR -_TACCONTROLLER_GAMEDATA.containing_type = _TACCONTROLLER -_TACCONTROLLER_TRANSACTIONCONFIRMATION.containing_type = _TACCONTROLLER -_TACCONTROLLER_STATEUPDATE.fields_by_name['initial_state'].message_type = _TACCONTROLLER_GAMEDATA -_TACCONTROLLER_STATEUPDATE.fields_by_name['txs'].message_type = _TACAGENT_TRANSACTION -_TACCONTROLLER_STATEUPDATE.containing_type = _TACCONTROLLER -_TACCONTROLLER_ERROR.fields_by_name['error_code'].enum_type = _TACCONTROLLER_ERROR_ERRORCODE -_TACCONTROLLER_ERROR.fields_by_name['details'].message_type = google_dot_protobuf_dot_struct__pb2._STRUCT -_TACCONTROLLER_ERROR.containing_type = _TACCONTROLLER -_TACCONTROLLER_ERROR_ERRORCODE.containing_type = _TACCONTROLLER_ERROR -_TACAGENT_REGISTER.containing_type = _TACAGENT -_TACAGENT_UNREGISTER.containing_type = _TACAGENT -_TACAGENT_TRANSACTION.fields_by_name['quantities'].message_type = _STRINTPAIR -_TACAGENT_TRANSACTION.containing_type = _TACAGENT -_TACAGENT_GETSTATEUPDATE.containing_type = _TACAGENT -_TACMESSAGE.fields_by_name['register'].message_type = _TACAGENT_REGISTER -_TACMESSAGE.fields_by_name['unregister'].message_type = _TACAGENT_UNREGISTER -_TACMESSAGE.fields_by_name['transaction'].message_type = _TACAGENT_TRANSACTION -_TACMESSAGE.fields_by_name['get_state_update'].message_type = _TACAGENT_GETSTATEUPDATE -_TACMESSAGE.fields_by_name['registered'].message_type = _TACCONTROLLER_REGISTERED -_TACMESSAGE.fields_by_name['unregistered'].message_type = _TACCONTROLLER_UNREGISTERED -_TACMESSAGE.fields_by_name['cancelled'].message_type = _TACCONTROLLER_CANCELLED -_TACMESSAGE.fields_by_name['game_data'].message_type = _TACCONTROLLER_GAMEDATA -_TACMESSAGE.fields_by_name['transaction_confirmation'].message_type = _TACCONTROLLER_TRANSACTIONCONFIRMATION -_TACMESSAGE.fields_by_name['state_update'].message_type = _TACCONTROLLER_STATEUPDATE -_TACMESSAGE.fields_by_name['error'].message_type = _TACCONTROLLER_ERROR -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['register']) -_TACMESSAGE.fields_by_name['register'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['unregister']) -_TACMESSAGE.fields_by_name['unregister'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['transaction']) -_TACMESSAGE.fields_by_name['transaction'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['get_state_update']) -_TACMESSAGE.fields_by_name['get_state_update'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['registered']) -_TACMESSAGE.fields_by_name['registered'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['unregistered']) -_TACMESSAGE.fields_by_name['unregistered'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['cancelled']) -_TACMESSAGE.fields_by_name['cancelled'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['game_data']) -_TACMESSAGE.fields_by_name['game_data'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['transaction_confirmation']) -_TACMESSAGE.fields_by_name['transaction_confirmation'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['state_update']) -_TACMESSAGE.fields_by_name['state_update'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['error']) -_TACMESSAGE.fields_by_name['error'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -DESCRIPTOR.message_types_by_name['StrIntPair'] = _STRINTPAIR -DESCRIPTOR.message_types_by_name['StrStrPair'] = _STRSTRPAIR -DESCRIPTOR.message_types_by_name['TACController'] = _TACCONTROLLER -DESCRIPTOR.message_types_by_name['TACAgent'] = _TACAGENT -DESCRIPTOR.message_types_by_name['TACMessage'] = _TACMESSAGE - -StrIntPair = _reflection.GeneratedProtocolMessageType('StrIntPair', (_message.Message,), dict( - DESCRIPTOR = _STRINTPAIR, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.StrIntPair) - )) -_sym_db.RegisterMessage(StrIntPair) - -StrStrPair = _reflection.GeneratedProtocolMessageType('StrStrPair', (_message.Message,), dict( - DESCRIPTOR = _STRSTRPAIR, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.StrStrPair) - )) -_sym_db.RegisterMessage(StrStrPair) - -TACController = _reflection.GeneratedProtocolMessageType('TACController', (_message.Message,), dict( - - Registered = _reflection.GeneratedProtocolMessageType('Registered', (_message.Message,), dict( - DESCRIPTOR = _TACCONTROLLER_REGISTERED, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Registered) - )) - , - - Unregistered = _reflection.GeneratedProtocolMessageType('Unregistered', (_message.Message,), dict( - DESCRIPTOR = _TACCONTROLLER_UNREGISTERED, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Unregistered) - )) - , - - Cancelled = _reflection.GeneratedProtocolMessageType('Cancelled', (_message.Message,), dict( - DESCRIPTOR = _TACCONTROLLER_CANCELLED, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Cancelled) - )) - , - - GameData = _reflection.GeneratedProtocolMessageType('GameData', (_message.Message,), dict( - DESCRIPTOR = _TACCONTROLLER_GAMEDATA, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.GameData) - )) - , - - TransactionConfirmation = _reflection.GeneratedProtocolMessageType('TransactionConfirmation', (_message.Message,), dict( - DESCRIPTOR = _TACCONTROLLER_TRANSACTIONCONFIRMATION, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.TransactionConfirmation) - )) - , - - StateUpdate = _reflection.GeneratedProtocolMessageType('StateUpdate', (_message.Message,), dict( - DESCRIPTOR = _TACCONTROLLER_STATEUPDATE, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.StateUpdate) - )) - , - - Error = _reflection.GeneratedProtocolMessageType('Error', (_message.Message,), dict( - DESCRIPTOR = _TACCONTROLLER_ERROR, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Error) - )) - , - DESCRIPTOR = _TACCONTROLLER, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController) - )) -_sym_db.RegisterMessage(TACController) -_sym_db.RegisterMessage(TACController.Registered) -_sym_db.RegisterMessage(TACController.Unregistered) -_sym_db.RegisterMessage(TACController.Cancelled) -_sym_db.RegisterMessage(TACController.GameData) -_sym_db.RegisterMessage(TACController.TransactionConfirmation) -_sym_db.RegisterMessage(TACController.StateUpdate) -_sym_db.RegisterMessage(TACController.Error) - -TACAgent = _reflection.GeneratedProtocolMessageType('TACAgent', (_message.Message,), dict( - - Register = _reflection.GeneratedProtocolMessageType('Register', (_message.Message,), dict( - DESCRIPTOR = _TACAGENT_REGISTER, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.Register) - )) - , - - Unregister = _reflection.GeneratedProtocolMessageType('Unregister', (_message.Message,), dict( - DESCRIPTOR = _TACAGENT_UNREGISTER, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.Unregister) - )) - , - - Transaction = _reflection.GeneratedProtocolMessageType('Transaction', (_message.Message,), dict( - DESCRIPTOR = _TACAGENT_TRANSACTION, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.Transaction) - )) - , - - GetStateUpdate = _reflection.GeneratedProtocolMessageType('GetStateUpdate', (_message.Message,), dict( - DESCRIPTOR = _TACAGENT_GETSTATEUPDATE, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.GetStateUpdate) - )) - , - DESCRIPTOR = _TACAGENT, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent) - )) -_sym_db.RegisterMessage(TACAgent) -_sym_db.RegisterMessage(TACAgent.Register) -_sym_db.RegisterMessage(TACAgent.Unregister) -_sym_db.RegisterMessage(TACAgent.Transaction) -_sym_db.RegisterMessage(TACAgent.GetStateUpdate) - -TACMessage = _reflection.GeneratedProtocolMessageType('TACMessage', (_message.Message,), dict( - DESCRIPTOR = _TACMESSAGE, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACMessage) - )) -_sym_db.RegisterMessage(TACMessage) - - -# @@protoc_insertion_point(module_scope) diff --git a/asdsd/skills/__init__.py b/asdsd/skills/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/asdsd/skills/error/__init__.py b/asdsd/skills/error/__init__.py deleted file mode 100644 index 96c80ac32c..0000000000 --- a/asdsd/skills/error/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the implementation of the error skill.""" diff --git a/asdsd/skills/error/behaviours.py b/asdsd/skills/error/behaviours.py deleted file mode 100644 index 556ee98ca7..0000000000 --- a/asdsd/skills/error/behaviours.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains the error behaviours.""" - -from aea.skills.base import Behaviour - - -class ErrorBehaviour(Behaviour): - """This class implements the error behaviour.""" - - def setup(self) -> None: - """ - Implement the setup. - - :return: None - """ - pass - - def act(self) -> None: - """ - Implement the act. - - :return: None - """ - pass - - def teardown(self) -> None: - """ - Implement the task teardown. - - :return: None - """ - pass diff --git a/asdsd/skills/error/handlers.py b/asdsd/skills/error/handlers.py deleted file mode 100644 index 098a61eced..0000000000 --- a/asdsd/skills/error/handlers.py +++ /dev/null @@ -1,131 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains the implementation of the handler for the 'default' protocol.""" -import base64 -import logging -from typing import Optional - -from aea.configurations.base import ProtocolId -from aea.mail.base import Envelope -from aea.protocols.base import Message, Protocol -from aea.protocols.default.message import DefaultMessage -from aea.protocols.default.serialization import DefaultSerializer -from aea.skills.base import Handler - -logger = logging.getLogger(__name__) - - -class ErrorHandler(Handler): - """This class implements the error handler.""" - - SUPPORTED_PROTOCOL = 'default' # type: Optional[ProtocolId] - - def setup(self) -> None: - """ - Implement the setup. - - :return: None - """ - pass - - def handle(self, message: Message, sender: str) -> None: - """ - Implement the reaction to an envelope. - - :param message: the message - :param sender: the sender - """ - pass - - def teardown(self) -> None: - """ - Implement the handler teardown. - - :return: None - """ - pass - - def send_unsupported_protocol(self, envelope: Envelope) -> None: - """ - Handle the received envelope in case the protocol is not supported. - - :param envelope: the envelope - :return: None - """ - logger.warning("Unsupported protocol: {}".format(envelope.protocol_id)) - reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.UNSUPPORTED_PROTOCOL.value, - error_msg="Unsupported protocol.", - error_data={"protocol_id": envelope.protocol_id}) - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, - protocol_id=DefaultMessage.protocol_id, - message=DefaultSerializer().encode(reply)) - - def send_decoding_error(self, envelope: Envelope) -> None: - """ - Handle a decoding error. - - :param envelope: the envelope - :return: None - """ - logger.warning("Decoding error: {}.".format(envelope)) - encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") - reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.DECODING_ERROR.value, - error_msg="Decoding error.", - error_data={"envelope": encoded_envelope}) - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, - protocol_id=DefaultMessage.protocol_id, - message=DefaultSerializer().encode(reply)) - - def send_invalid_message(self, envelope: Envelope) -> None: - """ - Handle an message that is invalid wrt a protocol. - - :param envelope: the envelope - :return: None - """ - logger.warning("Invalid message wrt protocol: {}.".format(envelope.protocol_id)) - encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") - reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.INVALID_MESSAGE.value, - error_msg="Invalid message.", - error_data={"envelope": encoded_envelope}) - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, - protocol_id=DefaultMessage.protocol_id, - message=DefaultSerializer().encode(reply)) - - def send_unsupported_skill(self, envelope: Envelope, protocol: Protocol) -> None: - """ - Handle the received envelope in case the skill is not supported. - - :param envelope: the envelope - :param protocol: the protocol - :return: None - """ - logger.warning("Cannot handle envelope: no handler registered for the protocol '{}'.".format(protocol.id)) - encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") - reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.UNSUPPORTED_SKILL.value, - error_msg="Unsupported skill.", - error_data={"envelope": encoded_envelope}) - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, - protocol_id=DefaultMessage.protocol_id, - message=DefaultSerializer().encode(reply)) diff --git a/asdsd/skills/error/skill.yaml b/asdsd/skills/error/skill.yaml deleted file mode 100644 index 33806a3345..0000000000 --- a/asdsd/skills/error/skill.yaml +++ /dev/null @@ -1,15 +0,0 @@ -name: error -authors: Fetch.AI Limited -version: 0.1.0 -license: Apache 2.0 -url: "" -description: "The error skill implements basic error handling required by all AEAs." -behaviours: [] -handlers: - - handler: - class_name: ErrorHandler - args: - foo: bar -tasks: [] -shared_classes: [] -protocols: ['default'] diff --git a/asdsd/skills/error/tasks.py b/asdsd/skills/error/tasks.py deleted file mode 100644 index 8922217537..0000000000 --- a/asdsd/skills/error/tasks.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains the implementation of the error tasks.""" - -from aea.skills.base import Task - - -class ErrorTask(Task): - """This class implements the error task.""" - - def setup(self) -> None: - """ - Implement the setup. - - :return: None - """ - pass - - def execute(self) -> None: - """ - Implement the task execution. - - :param envelope: the envelope - :return: None - """ - pass - - def teardown(self) -> None: - """ - Implement the task teardown. - - :return: None - """ - pass diff --git a/my_agent/aea-config.yaml b/my_agent/aea-config.yaml deleted file mode 100644 index 6f5d588acc..0000000000 --- a/my_agent/aea-config.yaml +++ /dev/null @@ -1,31 +0,0 @@ -aea_version: 0.1.7 -agent_name: my_agent -authors: '' -connections: -- oef -default_connection: oef -description: '' -license: '' -logging_config: - disable_existing_loggers: false - version: 1 -private_key_paths: -- private_key_path: - ledger: default - path: default_private_key.pem -- private_key_path: - ledger: fetchai - path: fet_private_key.txt -- private_key_path: - ledger: ethereum - path: eth_private_key.txt -protocols: -- gym -- oef -- tac -registry_path: ../packages -skills: -- error -- my_search -url: '' -version: v1 diff --git a/my_agent/connections/__init__.py b/my_agent/connections/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/my_agent/connections/oef/__init__.py b/my_agent/connections/oef/__init__.py deleted file mode 100644 index 21ee4d83df..0000000000 --- a/my_agent/connections/oef/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Implementation of the OEF connection.""" diff --git a/my_agent/connections/oef/connection.py b/my_agent/connections/oef/connection.py deleted file mode 100644 index 7f489b867f..0000000000 --- a/my_agent/connections/oef/connection.py +++ /dev/null @@ -1,604 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Extension to the OEF Python SDK.""" -import datetime -import logging -import pickle -from queue import Empty, Queue -from threading import Thread -from typing import List, Dict, Optional, cast - -import oef -from oef.agents import OEFAgent -from oef.core import AsyncioCore -from oef.messages import CFP_TYPES, PROPOSE_TYPES -from oef.query import ( - Query as OEFQuery, - ConstraintExpr as OEFConstraintExpr, - And as OEFAnd, - Or as OEFOr, - Not as OEFNot, - Constraint as OEFConstraint, - ConstraintType as OEFConstraintType, Eq, NotEq, Lt, LtEq, Gt, GtEq, Range, In, NotIn) -from oef.schema import Description as OEFDescription, DataModel as OEFDataModel, AttributeSchema as OEFAttribute - -from aea.configurations.base import ConnectionConfig -from aea.connections.base import Channel, Connection -from aea.mail.base import MailBox, Envelope -from aea.protocols.fipa.message import FIPAMessage -from aea.protocols.fipa.serialization import FIPASerializer -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.models import Description, Attribute, DataModel, Query, ConstraintExpr, And, Or, Not, Constraint, \ - ConstraintType, ConstraintTypes -from aea.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF - -logger = logging.getLogger(__name__) - - -STUB_MESSSAGE_ID = 0 -STUB_DIALOGUE_ID = 0 - - -class OEFObjectTranslator: - """Translate our OEF object to object of OEF SDK classes.""" - - @classmethod - def to_oef_description(cls, desc: Description) -> OEFDescription: - """From our description to OEF description.""" - oef_data_model = cls.to_oef_data_model(desc.data_model) if desc.data_model is not None else None - return OEFDescription(desc.values, oef_data_model) - - @classmethod - def to_oef_data_model(cls, data_model: DataModel) -> OEFDataModel: - """From our data model to OEF data model.""" - oef_attributes = [cls.to_oef_attribute(attribute) for attribute in data_model.attributes] - return OEFDataModel(data_model.name, oef_attributes, data_model.description) - - @classmethod - def to_oef_attribute(cls, attribute: Attribute) -> OEFAttribute: - """From our attribute to OEF attribute.""" - return OEFAttribute(attribute.name, attribute.type, attribute.is_required, attribute.description) - - @classmethod - def to_oef_query(cls, query: Query) -> OEFQuery: - """From our query to OEF query.""" - oef_data_model = cls.to_oef_data_model(query.model) if query.model is not None else None - constraints = [cls.to_oef_constraint_expr(c) for c in query.constraints] - return OEFQuery(constraints, oef_data_model) - - @classmethod - def to_oef_constraint_expr(cls, constraint_expr: ConstraintExpr) -> OEFConstraintExpr: - """From our constraint expression to the OEF constraint expression.""" - if isinstance(constraint_expr, And): - return OEFAnd([cls.to_oef_constraint_expr(c) for c in constraint_expr.constraints]) - elif isinstance(constraint_expr, Or): - return OEFOr([cls.to_oef_constraint_expr(c) for c in constraint_expr.constraints]) - elif isinstance(constraint_expr, Not): - return OEFNot(cls.to_oef_constraint_expr(constraint_expr.constraint)) - elif isinstance(constraint_expr, Constraint): - oef_constraint_type = cls.to_oef_constraint_type(constraint_expr.constraint_type) - return OEFConstraint(constraint_expr.attribute_name, oef_constraint_type) - else: - raise ValueError("Constraint expression not supported.") - - @classmethod - def to_oef_constraint_type(cls, constraint_type: ConstraintType) -> OEFConstraintType: - """From our constraint type to OEF constraint type.""" - value = constraint_type.value - if constraint_type.type == ConstraintTypes.EQUAL: - return Eq(value) - elif constraint_type.type == ConstraintTypes.NOT_EQUAL: - return NotEq(value) - elif constraint_type.type == ConstraintTypes.LESS_THAN: - return Lt(value) - elif constraint_type.type == ConstraintTypes.LESS_THAN_EQ: - return LtEq(value) - elif constraint_type.type == ConstraintTypes.GREATER_THAN: - return Gt(value) - elif constraint_type.type == ConstraintTypes.GREATER_THAN_EQ: - return GtEq(value) - elif constraint_type.type == ConstraintTypes.WITHIN: - return Range(value) - elif constraint_type.type == ConstraintTypes.IN: - return In(value) - elif constraint_type.type == ConstraintTypes.NOT_IN: - return NotIn(value) - else: - raise ValueError("Constraint type not recognized.") - - @classmethod - def from_oef_description(cls, oef_desc: OEFDescription) -> Description: - """From an OEF description to our description.""" - data_model = cls.from_oef_data_model(oef_desc.data_model) if oef_desc.data_model is not None else None - return Description(oef_desc.values, data_model=data_model) - - @classmethod - def from_oef_data_model(cls, oef_data_model: OEFDataModel) -> DataModel: - """From an OEF data model to our data model.""" - attributes = [cls.from_oef_attribute(oef_attribute) for oef_attribute in oef_data_model.attribute_schemas] - return DataModel(oef_data_model.name, attributes, oef_data_model.description) - - @classmethod - def from_oef_attribute(cls, oef_attribute: OEFAttribute) -> Attribute: - """From an OEF attribute to our attribute.""" - return Attribute(oef_attribute.name, oef_attribute.type, oef_attribute.required, oef_attribute.description) - - @classmethod - def from_oef_query(cls, oef_query: OEFQuery) -> Query: - """From our query to OrOEF query.""" - data_model = cls.from_oef_data_model(oef_query.model) if oef_query.model is not None else None - constraints = [cls.from_oef_constraint_expr(c) for c in oef_query.constraints] - return Query(constraints, data_model) - - @classmethod - def from_oef_constraint_expr(cls, oef_constraint_expr: OEFConstraintExpr) -> ConstraintExpr: - """From our query to OEF query.""" - if isinstance(oef_constraint_expr, OEFAnd): - return And([cls.from_oef_constraint_expr(c) for c in oef_constraint_expr.constraints]) - elif isinstance(oef_constraint_expr, OEFOr): - return Or([cls.from_oef_constraint_expr(c) for c in oef_constraint_expr.constraints]) - elif isinstance(oef_constraint_expr, OEFNot): - return Not(cls.from_oef_constraint_expr(oef_constraint_expr.constraint)) - elif isinstance(oef_constraint_expr, OEFConstraint): - constraint_type = cls.from_oef_constraint_type(oef_constraint_expr.constraint) - return Constraint(oef_constraint_expr.attribute_name, constraint_type) - else: - raise ValueError("OEF Constraint not supported.") - - @classmethod - def from_oef_constraint_type(cls, constraint_type: OEFConstraintType) -> ConstraintType: - """From OEF constraint type to our constraint type.""" - if isinstance(constraint_type, Eq): - return ConstraintType(ConstraintTypes.EQUAL, constraint_type.value) - elif isinstance(constraint_type, NotEq): - return ConstraintType(ConstraintTypes.NOT_EQUAL, constraint_type.value) - elif isinstance(constraint_type, Lt): - return ConstraintType(ConstraintTypes.LESS_THAN, constraint_type.value) - elif isinstance(constraint_type, LtEq): - return ConstraintType(ConstraintTypes.LESS_THAN_EQ, constraint_type.value) - elif isinstance(constraint_type, Gt): - return ConstraintType(ConstraintTypes.GREATER_THAN, constraint_type.value) - elif isinstance(constraint_type, GtEq): - return ConstraintType(ConstraintTypes.GREATER_THAN_EQ, constraint_type.value) - elif isinstance(constraint_type, Range): - return ConstraintType(ConstraintTypes.WITHIN, constraint_type.values) - elif isinstance(constraint_type, In): - return ConstraintType(ConstraintTypes.IN, constraint_type.values) - elif isinstance(constraint_type, NotIn): - return ConstraintType(ConstraintTypes.NOT_IN, constraint_type.values) - else: - raise ValueError("Constraint type not recognized.") - - -class MailStats(object): - """The MailStats class tracks statistics on messages processed by MailBox.""" - - def __init__(self) -> None: - """ - Instantiate mail stats. - - :return: None - """ - self._search_count = 0 - self._search_start_time = {} # type: Dict[int, datetime.datetime] - self._search_timedelta = {} # type: Dict[int, float] - self._search_result_counts = {} # type: Dict[int, int] - - @property - def search_count(self) -> int: - """Get the search count.""" - return self._search_count - - def search_start(self, search_id: int) -> None: - """ - Add a search id and start time. - - :param search_id: the search id - - :return: None - """ - assert search_id not in self._search_start_time - self._search_count += 1 - self._search_start_time[search_id] = datetime.datetime.now() - - def search_end(self, search_id: int, nb_search_results: int) -> None: - """ - Add end time for a search id. - - :param search_id: the search id - :param nb_search_results: the number of agents returned in the search result - - :return: None - """ - assert search_id in self._search_start_time - assert search_id not in self._search_timedelta - self._search_timedelta[search_id] = (datetime.datetime.now() - self._search_start_time[search_id]).total_seconds() * 1000 - self._search_result_counts[search_id] = nb_search_results - - -class OEFChannel(OEFAgent, Channel): - """The OEFChannel connects the OEF Agent with the connection.""" - - def __init__(self, public_key: str, oef_addr: str, oef_port: int, core: AsyncioCore, in_queue: Queue): - """ - Initialize. - - :param public_key: the public key of the agent. - :param oef_addr: the OEF IP address. - :param oef_port: the OEF port. - :param in_queue: the in queue. - """ - super().__init__(public_key, oef_addr=oef_addr, oef_port=oef_port, core=core) - self.in_queue = in_queue - self.mail_stats = MailStats() - - def on_message(self, msg_id: int, dialogue_id: int, origin: str, content: bytes) -> None: - """ - On message event handler. - - :param msg_id: the message id. - :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. - :param content: the bytes content. - :return: None - """ - # We are not using the 'origin' parameter because 'content' contains a serialized instance of 'Envelope', - # hence it already contains the address of the sender. - envelope = Envelope.decode(content) - self.in_queue.put(envelope) - - def on_cfp(self, msg_id: int, dialogue_id: int, origin: str, target: int, query: CFP_TYPES) -> None: - """ - On cfp event handler. - - :param msg_id: the message id. - :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. - :param target: the message target. - :param query: the query. - :return: None - """ - try: - query = pickle.loads(query) - except Exception: - pass - msg = FIPAMessage(message_id=msg_id, - dialogue_id=dialogue_id, - target=target, - performative=FIPAMessage.Performative.CFP, - query=query if query != b"" else None) - msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_propose(self, msg_id: int, dialogue_id: int, origin: str, target: int, b_proposals: PROPOSE_TYPES) -> None: - """ - On propose event handler. - - :param msg_id: the message id. - :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. - :param target: the message target. - :param b_proposals: the proposals. - :return: None - """ - if type(b_proposals) == bytes: - proposals = pickle.loads(b_proposals) # type: List[Description] - else: - raise ValueError("No support for non-bytes proposals.") - - msg = FIPAMessage(message_id=msg_id, - dialogue_id=dialogue_id, - target=target, - performative=FIPAMessage.Performative.PROPOSE, - proposal=proposals) - msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_accept(self, msg_id: int, dialogue_id: int, origin: str, target: int) -> None: - """ - On accept event handler. - - :param msg_id: the message id. - :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. - :param target: the message target. - :return: None - """ - performative = FIPAMessage.Performative.MATCH_ACCEPT if msg_id == 4 and target == 3 else FIPAMessage.Performative.ACCEPT - msg = FIPAMessage(message_id=msg_id, - dialogue_id=dialogue_id, - target=target, - performative=performative) - msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_decline(self, msg_id: int, dialogue_id: int, origin: str, target: int) -> None: - """ - On decline event handler. - - :param msg_id: the message id. - :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. - :param target: the message target. - :return: None - """ - msg = FIPAMessage(message_id=msg_id, - dialogue_id=dialogue_id, - target=target, - performative=FIPAMessage.Performative.DECLINE) - msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_search_result(self, search_id: int, agents: List[str]) -> None: - """ - On accept event handler. - - :param search_id: the search id. - :param agents: the list of agents. - :return: None - """ - self.mail_stats.search_end(search_id, len(agents)) - msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_RESULT, id=search_id, agents=agents) - msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_oef_error(self, answer_id: int, operation: oef.messages.OEFErrorOperation) -> None: - """ - On oef error event handler. - - :param answer_id: the answer id. - :param operation: the error operation. - :return: None - """ - try: - operation = OEFMessage.OEFErrorOperation(operation) - except ValueError: - operation = OEFMessage.OEFErrorOperation.OTHER - - msg = OEFMessage(oef_type=OEFMessage.Type.OEF_ERROR, id=answer_id, operation=operation) - msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def on_dialogue_error(self, answer_id: int, dialogue_id: int, origin: str) -> None: - """ - On dialogue error event handler. - - :param answer_id: the answer id. - :param dialogue_id: the dialogue id. - :param origin: the message sender. - :return: None - """ - msg = OEFMessage(oef_type=OEFMessage.Type.DIALOGUE_ERROR, - id=answer_id, - dialogue_id=dialogue_id, - origin=origin) - msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) - self.in_queue.put(envelope) - - def send(self, envelope: Envelope) -> None: - """ - Send message handler. - - :param envelope: the message. - :return: None - """ - if envelope.protocol_id == "default": - self.send_default_message(envelope) - elif envelope.protocol_id == "fipa": - self.send_fipa_message(envelope) - elif envelope.protocol_id == "oef": - self.send_oef_message(envelope) - elif envelope.protocol_id == "tac": - self.send_default_message(envelope) - else: - logger.error("This envelope cannot be sent: protocol_id={}".format(envelope.protocol_id)) - raise ValueError("Cannot send message.") - - def send_default_message(self, envelope: Envelope): - """Send a 'default' message.""" - self.send_message(STUB_MESSSAGE_ID, STUB_DIALOGUE_ID, envelope.to, envelope.encode()) - - def send_fipa_message(self, envelope: Envelope) -> None: - """ - Send fipa message handler. - - :param envelope: the message. - :return: None - """ - fipa_message = FIPASerializer().decode(envelope.message) - id = fipa_message.get("message_id") - dialogue_id = fipa_message.get("dialogue_id") - destination = envelope.to - target = fipa_message.get("target") - performative = FIPAMessage.Performative(fipa_message.get("performative")) - if performative == FIPAMessage.Performative.CFP: - query = fipa_message.get("query") - query = b"" if query is None else query - if type(query) == Query: - query = pickle.dumps(query) - self.send_cfp(id, dialogue_id, destination, target, query) - elif performative == FIPAMessage.Performative.PROPOSE: - proposal = cast(List[Description], fipa_message.get("proposal")) - proposal_b = pickle.dumps(proposal) # type: bytes - self.send_propose(id, dialogue_id, destination, target, proposal_b) - elif performative == FIPAMessage.Performative.ACCEPT: - self.send_accept(id, dialogue_id, destination, target) - elif performative == FIPAMessage.Performative.MATCH_ACCEPT: - self.send_accept(id, dialogue_id, destination, target) - elif performative == FIPAMessage.Performative.DECLINE: - self.send_decline(id, dialogue_id, destination, target) - else: - raise ValueError("OEF FIPA message not recognized.") - - def send_oef_message(self, envelope: Envelope) -> None: - """ - Send oef message handler. - - :param envelope: the message. - :return: None - """ - oef_message = OEFSerializer().decode(envelope.message) - oef_type = OEFMessage.Type(oef_message.get("type")) - oef_msg_id = cast(int, oef_message.get("id")) - if oef_type == OEFMessage.Type.REGISTER_SERVICE: - service_description = cast(Description, oef_message.get("service_description")) - service_id = cast(int, oef_message.get("service_id")) - oef_service_description = OEFObjectTranslator.to_oef_description(service_description) - self.register_service(oef_msg_id, oef_service_description, service_id) - elif oef_type == OEFMessage.Type.UNREGISTER_SERVICE: - service_description = cast(Description, oef_message.get("service_description")) - service_id = cast(int, oef_message.get("service_id")) - oef_service_description = OEFObjectTranslator.to_oef_description(service_description) - self.unregister_service(oef_msg_id, oef_service_description, service_id) - elif oef_type == OEFMessage.Type.SEARCH_AGENTS: - query = cast(Query, oef_message.get("query")) - oef_query = OEFObjectTranslator.to_oef_query(query) - self.search_agents(oef_msg_id, oef_query) - elif oef_type == OEFMessage.Type.SEARCH_SERVICES: - query = cast(Query, oef_message.get("query")) - oef_query = OEFObjectTranslator.to_oef_query(query) - self.mail_stats.search_start(oef_msg_id) - self.search_services(oef_msg_id, oef_query) - else: - raise ValueError("OEF request not recognized.") - - -class OEFConnection(Connection): - """The OEFConnection connects the to the mailbox.""" - - def __init__(self, public_key: str, oef_addr: str, oef_port: int = 10000): - """ - Initialize. - - :param public_key: the public key of the agent. - :param oef_addr: the OEF IP address. - :param oef_port: the OEF port. - """ - super().__init__() - core = AsyncioCore(logger=logger) - self._core = core # type: AsyncioCore - self.channel = OEFChannel(public_key, oef_addr, oef_port, core=core, in_queue=self.in_queue) - - self._stopped = True - self._connected = False - self.out_thread = None # type: Optional[Thread] - - @property - def is_established(self) -> bool: - """Get the connection status.""" - return self._connected - - def _fetch(self) -> None: - """ - Fetch the messages from the outqueue and send them. - - :return: None - """ - while self._connected: - try: - msg = self.out_queue.get(block=True, timeout=1.0) - self.send(msg) - except Empty: - pass - - def connect(self) -> None: - """ - Connect to the channel. - - :return: None - :raises ConnectionError if the connection to the OEF fails. - """ - if self._stopped and not self._connected: - self._stopped = False - self._core.run_threaded() - try: - if not self.channel.connect(): - raise ConnectionError("Cannot connect to OEFChannel.") - self._connected = True - self.out_thread = Thread(target=self._fetch) - self.out_thread.start() - except ConnectionError as e: - self._core.stop() - raise e - - def disconnect(self) -> None: - """ - Disconnect from the channel. - - :return: None - """ - assert self.out_thread is not None, "Call connect before disconnect." - if not self._stopped and self._connected: - self._connected = False - self.out_thread.join() - self.out_thread = None - self.channel.disconnect() - self._core.stop() - self._stopped = True - - def send(self, envelope: Envelope): - """ - Send messages. - - :return: None - """ - if self._connected: - self.channel.send(envelope) - - @classmethod - def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection': - """ - Get the OEF connection from the connection configuration. - - :param public_key: the public key of the agent. - :param connection_configuration: the connection configuration object. - :return: the connection object - """ - oef_addr = cast(str, connection_configuration.config.get("addr")) - oef_port = cast(int, connection_configuration.config.get("port")) - return OEFConnection(public_key, oef_addr, oef_port) - - -class OEFMailBox(MailBox): - """The OEF mail box.""" - - def __init__(self, public_key: str, oef_addr: str, oef_port: int = 10000): - """ - Initialize. - - :param public_key: the public key of the agent. - :param oef_addr: the OEF IP address. - :param oef_port: the OEF port. - """ - connection = OEFConnection(public_key, oef_addr, oef_port) - super().__init__(connection) - - @property - def mail_stats(self) -> MailStats: - """Get the mail stats object.""" - return self._connection.channel.mail_stats # type: ignore diff --git a/my_agent/connections/oef/connection.yaml b/my_agent/connections/oef/connection.yaml deleted file mode 100644 index 5a8cca4134..0000000000 --- a/my_agent/connections/oef/connection.yaml +++ /dev/null @@ -1,14 +0,0 @@ -name: oef -authors: Fetch.AI Limited -version: 0.1.0 -license: Apache 2.0 -url: "" -class_name: OEFConnection -supported_protocols: ["oef"] -config: - addr: ${OEF_ADDR:127.0.0.1} - port: ${OEF_PORT:10000} -dependencies: - - colorlog - - oef -description: "oef connection description [Fill in]" \ No newline at end of file diff --git a/my_agent/default_private_key.pem b/my_agent/default_private_key.pem deleted file mode 100644 index e1375b9eac..0000000000 --- a/my_agent/default_private_key.pem +++ /dev/null @@ -1,6 +0,0 @@ ------BEGIN EC PRIVATE KEY----- -MIGkAgEBBDAlt8nlN6M8hWdV1HbV5i1kvvMLRlFUmfxJCstfWnjOYoF9kGIWUc/g -KH2l58/Hi1+gBwYFK4EEACKhZANiAATAbHHGWNnKQdoaqkuLtLO/7wZgTjwpY3H3 -kw2HCZBfXsN8DjTU7YXOnij1XBDAW+mpeDSqBbS4TspTeGISbyl9BOHUV2dlr6Ay -ZF1bQEhzcoS7KBBBtmo22rcb72H8CFE= ------END EC PRIVATE KEY----- diff --git a/my_agent/eth_private_key.txt b/my_agent/eth_private_key.txt deleted file mode 100644 index 2e2dbde073..0000000000 --- a/my_agent/eth_private_key.txt +++ /dev/null @@ -1 +0,0 @@ -0xf510ead7999e37b4902e98a594384682fd1389fe48942f22dab4390ecd78cff1 \ No newline at end of file diff --git a/my_agent/fet_private_key.txt b/my_agent/fet_private_key.txt deleted file mode 100644 index db66b233f2..0000000000 --- a/my_agent/fet_private_key.txt +++ /dev/null @@ -1 +0,0 @@ -5bc3f3175f8cf708ba7d809cb039b6cb31de2a8cb7791851f2bc9732dd509503 \ No newline at end of file diff --git a/my_agent/protocols/__init__.py b/my_agent/protocols/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/my_agent/protocols/gym/__init__.py b/my_agent/protocols/gym/__init__.py deleted file mode 100644 index a1766ab9cd..0000000000 --- a/my_agent/protocols/gym/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the support resources for the Gym protocol.""" diff --git a/my_agent/protocols/gym/message.py b/my_agent/protocols/gym/message.py deleted file mode 100644 index 616b3e8226..0000000000 --- a/my_agent/protocols/gym/message.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the FIPA message definition.""" -from enum import Enum -from typing import Optional, Union - -from aea.protocols.base import Message - - -class GymMessage(Message): - """The Gym message class.""" - - protocol_id = "gym" - - class Performative(Enum): - """Gym performatives.""" - - ACT = 'act' - PERCEPT = 'percept' - RESET = 'reset' - CLOSE = 'close' - - def __str__(self): - """Get string representation.""" - return self.value - - def __init__(self, performative: Optional[Union[str, Performative]] = None, **kwargs): - """ - Initialize. - - :param type: the type. - """ - super().__init__(performative=GymMessage.Performative(performative), **kwargs) - assert self.check_consistency(), "GymMessage initialization inconsistent." - - def check_consistency(self) -> bool: - """Check that the data is consistent.""" - try: - assert self.is_set("performative") - performative = GymMessage.Performative(self.get("performative")) - if performative == GymMessage.Performative.ACT: - assert self.is_set("action") - assert self.is_set("step_id") - elif performative == GymMessage.Performative.PERCEPT: - assert self.is_set("observation") - assert self.is_set("reward") - assert self.is_set("done") - assert self.is_set("info") - assert self.is_set("step_id") - elif performative == GymMessage.Performative.RESET or performative == GymMessage.Performative.CLOSE: - pass - else: - raise ValueError("Performative not recognized.") - - except (AssertionError, ValueError, KeyError): - return False - - return True diff --git a/my_agent/protocols/gym/protocol.yaml b/my_agent/protocols/gym/protocol.yaml deleted file mode 100644 index 7b10d5429b..0000000000 --- a/my_agent/protocols/gym/protocol.yaml +++ /dev/null @@ -1,6 +0,0 @@ -name: gym -authors: Fetch.AI Limited -version: 0.1.0 -license: Apache 2.0 -url: "" -description: "The gym protocol implements the messages an agent needs to engage with a gym connection." \ No newline at end of file diff --git a/my_agent/protocols/gym/serialization.py b/my_agent/protocols/gym/serialization.py deleted file mode 100644 index 6fb14c1354..0000000000 --- a/my_agent/protocols/gym/serialization.py +++ /dev/null @@ -1,103 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Serialization for the FIPA protocol.""" -import base64 -import copy -import json -import pickle -from typing import Any, TYPE_CHECKING - -from aea.protocols.base import Message -from aea.protocols.base import Serializer - -if TYPE_CHECKING: - from packages.protocols.gym.message import GymMessage -else: - from gym_protocol.message import GymMessage - - -class GymSerializer(Serializer): - """Serialization for the Gym protocol.""" - - def encode(self, msg: Message) -> bytes: - """ - Decode the message. - - :param msg: the message object - :return: the bytes - """ - performative = GymMessage.Performative(msg.get("performative")) - new_body = copy.copy(msg.body) - new_body["performative"] = performative.value - - if performative == GymMessage.Performative.ACT: - action = msg.body["action"] # type: Any - action_bytes = base64.b64encode(pickle.dumps(action)).decode("utf-8") - new_body["action"] = action_bytes - new_body["step_id"] = msg.body["step_id"] - elif performative == GymMessage.Performative.PERCEPT: - # observation, reward and info are gym implementation specific, done is boolean - observation = msg.body["observation"] # type: Any - observation_bytes = base64.b64encode(pickle.dumps(observation)).decode("utf-8") - new_body["observation"] = observation_bytes - reward = msg.body["reward"] # type: Any - reward_bytes = base64.b64encode(pickle.dumps(reward)).decode("utf-8") - new_body["reward"] = reward_bytes - info = msg.body["info"] # type: Any - info_bytes = base64.b64encode(pickle.dumps(info)).decode("utf-8") - new_body["info"] = info_bytes - new_body["step_id"] = msg.body["step_id"] - - gym_message_bytes = json.dumps(new_body).encode("utf-8") - return gym_message_bytes - - def decode(self, obj: bytes) -> Message: - """ - Decode the message. - - :param obj: the bytes object - :return: the message - """ - json_msg = json.loads(obj.decode("utf-8")) - performative = GymMessage.Performative(json_msg["performative"]) - new_body = copy.copy(json_msg) - new_body["type"] = performative - - if performative == GymMessage.Performative.ACT: - action_bytes = base64.b64decode(json_msg["action"]) - action = pickle.loads(action_bytes) - new_body["action"] = action - new_body["step_id"] = json_msg["step_id"] - elif performative == GymMessage.Performative.PERCEPT: - # observation, reward and info are gym implementation specific, done is boolean - observation_bytes = base64.b64decode(json_msg["observation"]) - observation = pickle.loads(observation_bytes) - new_body["observation"] = observation - reward_bytes = base64.b64decode(json_msg["reward"]) - reward = pickle.loads(reward_bytes) - new_body["reward"] = reward - info_bytes = base64.b64decode(json_msg["info"]) - info = pickle.loads(info_bytes) - new_body["info"] = info - new_body["step_id"] = json_msg["step_id"] - - gym_message = GymMessage(performative=performative, body=new_body) - return gym_message diff --git a/my_agent/protocols/oef/__init__.py b/my_agent/protocols/oef/__init__.py deleted file mode 100644 index 8b7bf970cf..0000000000 --- a/my_agent/protocols/oef/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the support resources for the OEF protocol.""" diff --git a/my_agent/protocols/oef/message.py b/my_agent/protocols/oef/message.py deleted file mode 100644 index ab44e971d4..0000000000 --- a/my_agent/protocols/oef/message.py +++ /dev/null @@ -1,125 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the default message definition.""" -from enum import Enum -from typing import Optional, List, cast - -from aea.protocols.base import Message -from aea.protocols.oef.models import Description, Query - - -class OEFMessage(Message): - """The OEF message class.""" - - protocol_id = "oef" - - class Type(Enum): - """OEF Message types.""" - - REGISTER_SERVICE = "register_service" - UNREGISTER_SERVICE = "unregister_service" - SEARCH_SERVICES = "search_services" - SEARCH_AGENTS = "search_agents" - OEF_ERROR = "oef_error" - DIALOGUE_ERROR = "dialogue_error" - SEARCH_RESULT = "search_result" - - def __str__(self): - """Get string representation.""" - return self.value - - class OEFErrorOperation(Enum): - """Operation code for the OEF. It is returned in the OEF Error messages.""" - - REGISTER_SERVICE = 0 - UNREGISTER_SERVICE = 1 - SEARCH_SERVICES = 2 - SEARCH_SERVICES_WIDE = 3 - SEARCH_AGENTS = 4 - SEND_MESSAGE = 5 - - OTHER = 10000 - - def __str__(self): - """Get string representation.""" - return str(self.value) - - def __init__(self, oef_type: Optional[Type] = None, - **kwargs): - """ - Initialize. - - :param oef_type: the type of OEF message. - """ - super().__init__(type=oef_type, **kwargs) - assert self.check_consistency(), "OEFMessage initialization inconsistent." - - def check_consistency(self) -> bool: - """Check that the data is consistent.""" - try: - assert self.is_set("type") - oef_type = OEFMessage.Type(self.get("type")) - if oef_type == OEFMessage.Type.REGISTER_SERVICE: - assert self.is_set("id") - assert self.is_set("service_description") - assert self.is_set("service_id") - service_description = self.get("service_description") - service_id = self.get("service_id") - assert isinstance(service_description, Description) - assert isinstance(service_id, str) - elif oef_type == OEFMessage.Type.UNREGISTER_SERVICE: - assert self.is_set("id") - assert self.is_set("service_description") - assert self.is_set("service_id") - service_description = self.get("service_description") - service_id = self.get("service_id") - assert isinstance(service_description, Description) - assert isinstance(service_id, str) - elif oef_type == OEFMessage.Type.SEARCH_SERVICES: - assert self.is_set("id") - assert self.is_set("query") - query = self.get("query") - assert isinstance(query, Query) - elif oef_type == OEFMessage.Type.SEARCH_AGENTS: - assert self.is_set("id") - assert self.is_set("query") - query = self.get("query") - assert isinstance(query, Query) - elif oef_type == OEFMessage.Type.SEARCH_RESULT: - assert self.is_set("id") - assert self.is_set("agents") - agents = cast(List[str], self.get("agents")) - assert type(agents) == list and all(type(a) == str for a in agents) - elif oef_type == OEFMessage.Type.OEF_ERROR: - assert self.is_set("id") - assert self.is_set("operation") - operation = self.get("operation") - assert operation in set(OEFMessage.OEFErrorOperation) - elif oef_type == OEFMessage.Type.DIALOGUE_ERROR: - assert self.is_set("id") - assert self.is_set("dialogue_id") - assert self.is_set("origin") - else: - raise ValueError("Type not recognized.") - except (AssertionError, ValueError): - return False - - return True diff --git a/my_agent/protocols/oef/models.py b/my_agent/protocols/oef/models.py deleted file mode 100644 index d3811d4e69..0000000000 --- a/my_agent/protocols/oef/models.py +++ /dev/null @@ -1,450 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Useful classes for the OEF protocol.""" -from abc import ABC, abstractmethod -from copy import deepcopy -from enum import Enum -from typing import Dict, Type, Union, Optional, List, Any - - -ATTRIBUTE_TYPES = Union[float, str, bool, int] - - -class JSONSerializable(ABC): - """Interface for JSON-serializable objects.""" - - @abstractmethod - def to_json(self) -> Dict: - """ - Return the JSON representation of the object. - - :return: the JSON object. - """ - - @classmethod - @abstractmethod - def from_json(cls, d: Dict) -> Any: - """ - Parse the JSON representation of the object. - - :param d: the JSON object. - :return: the equivalent Python object. - """ - - -class Attribute: - """Implements an attribute for an OEF data model.""" - - def __init__(self, name: str, - type: Type[ATTRIBUTE_TYPES], - is_required: bool, - description: str = ""): - """ - Initialize an attribute. - - :param name: the name of the attribute. - :param type: the type of the attribute. - :param is_required: whether the attribute is required by the data model. - :param description: an (optional) human-readable description for the attribute. - """ - self.name: str = name - self.type: Type[ATTRIBUTE_TYPES] = type - self.is_required: bool = is_required - self.description: str = description - - def __eq__(self, other): - """Compare with another object.""" - return isinstance(other, Attribute) \ - and self.name == other.name \ - and self.type == other.type \ - and self.is_required == other.is_required - - -class DataModel: - """Implements an OEF data model.""" - - def __init__(self, name: str, attributes: List[Attribute], description: str = ""): - """ - Initialize a data model. - - :param name: the name of the data model. - :param attributes: the attributes of the data model. - """ - self.name: str = name - self.attributes: List[Attribute] = sorted(attributes, key=lambda x: x.name) - self.attributes_by_name = {a.name: a for a in attributes} - self.description = description - - def __eq__(self, other) -> bool: - """Compare with another object.""" - return isinstance(other, DataModel) \ - and self.name == other.name \ - and self.attributes == other.attributes - - -class Description: - """Implements an OEF description.""" - - def __init__(self, values: Dict, data_model: Optional[DataModel] = None): - """ - Initialize the description object. - - :param values: the values in the description. - """ - _values = deepcopy(values) - self.values = _values - self.data_model = data_model - - def __eq__(self, other) -> bool: - """Compare with another object.""" - return isinstance(other, Description) \ - and self.values == other.values \ - and self.data_model == other.data_model - - def __iter__(self): - """Create an iterator.""" - return self - - -class ConstraintTypes(Enum): - """Types of constraint.""" - - EQUAL = "==" - NOT_EQUAL = "!=" - LESS_THAN = "<" - LESS_THAN_EQ = "<=" - GREATER_THAN = ">" - GREATER_THAN_EQ = ">=" - WITHIN = "within" - IN = "in" - NOT_IN = "not_in" - - def __str__(self): - """Get the string representation.""" - return self.value - - -class ConstraintType: - """ - Type of constraint. - - Used with the Constraint class, this class allows to specify constraint over attributes. - - Examples: - Equal to three - >>> equal_3 = ConstraintType(ConstraintTypes.EQUAL, 3) - - You can also specify a type of constraint by using its string representation, e.g.: - >>> equal_3 = ConstraintType("==", 3) - >>> not_equal_london = ConstraintType("!=", "London") - >>> less_than_pi = ConstraintType("<", 3.14) - >>> within_range = ConstraintType("within", (-10.0, 10.0)) - >>> in_a_set = ConstraintType("in", [1, 2, 3]) - >>> not_in_a_set = ConstraintType("not_in", {"C", "Java", "Python"}) - - """ - - def __init__(self, type: Union[ConstraintTypes, str], value: Any, **kwargs): - """ - Initialize a constraint type. - - :param type: the type of the constraint. - | Either an instance of the ConstraintTypes enum, - | or a string representation associated with the type. - :param value: the value that defines the constraint. - :raises ValueError: if the type of the constraint is not - """ - self.type = ConstraintTypes(type) - self.value = value - - def _check_validity(self): - """ - Check the validity of the input provided. - - :return: None - :raises ValueError: if the value is not valid wrt the constraint type. - """ - try: - if self.type == ConstraintTypes.EQUAL: - assert isinstance(self.value, (int, float, str, bool)) - elif self.type == ConstraintTypes.NOT_EQUAL: - assert isinstance(self.value, (int, float, str, bool)) - elif self.type == ConstraintTypes.LESS_THAN: - assert isinstance(self.value, (int, float, str)) - elif self.type == ConstraintTypes.LESS_THAN_EQ: - assert isinstance(self.value, (int, float, str)) - elif self.type == ConstraintTypes.GREATER_THAN: - assert isinstance(self.value, (int, float, str)) - elif self.type == ConstraintTypes.GREATER_THAN_EQ: - assert isinstance(self.value, (int, float, str)) - elif self.type == ConstraintTypes.WITHIN: - assert isinstance(self.value, (list, tuple)) - assert len(self.value) == 2 - assert isinstance(self.value[0], type(self.value[1])) - assert isinstance(self.value[1], type(self.value[0])) - elif self.type == ConstraintTypes.IN: - assert isinstance(self.value, (list, tuple, set)) - if len(self.value) > 0: - _type = type(next(iter(self.value))) - assert all(isinstance(obj, _type) for obj in self.value) - elif self.type == ConstraintTypes.NOT_IN: - assert isinstance(self.value, (list, tuple, set)) - if len(self.value) > 0: - _type = type(next(iter(self.value))) - assert all(isinstance(obj, _type) for obj in self.value) - except AssertionError: - raise ValueError("Value '{}' not compatible with constraint type '{}'" - .format(self.value, str(self.type))) - - def check(self, value: ATTRIBUTE_TYPES) -> bool: - """ - Check if an attribute value satisfies the constraint. - - The implementation depends on the constraint type. - - :param value: the value to check. - :return: True if the value satisfy the constraint, False otherwise. - :raises ValueError: if the constraint type is not recognized. - """ - if self.type == ConstraintTypes.EQUAL: - return self.value == value - elif self.type == ConstraintTypes.NOT_EQUAL: - return self.value != value - elif self.type == ConstraintTypes.LESS_THAN: - return self.value < value - elif self.type == ConstraintTypes.LESS_THAN_EQ: - return self.value <= value - elif self.type == ConstraintTypes.GREATER_THAN: - return self.value > value - elif self.type == ConstraintTypes.GREATER_THAN_EQ: - return self.value >= value - elif self.type == ConstraintTypes.WITHIN: - low = self.value[0] - high = self.value[1] - return low <= value <= high - elif self.type == ConstraintTypes.IN: - return value in self.value - elif self.type == ConstraintTypes.NOT_IN: - return value not in self.value - else: - raise ValueError("Constraint type not recognized.") - - def __eq__(self, other): - """Check equality with another object.""" - return isinstance(other, ConstraintType) and self.value == other.value and self.type == other.type - - -class ConstraintExpr(ABC): - """Implementation of the constraint language to query the OEF node.""" - - @abstractmethod - def check(self, description: Description) -> bool: - """ - Check if a description satisfies the constraint expression. - - :param description: the description to check. - :return: True if the description satisfy the constraint expression, False otherwise. - """ - - -class And(ConstraintExpr): - """Implementation of the 'And' constraint expression.""" - - def __init__(self, constraints: List[ConstraintExpr]): - """ - Initialize an 'And' expression. - - :param constraints: the list of constraints expression (in conjunction). - """ - self.constraints = constraints - - def check(self, description: Description) -> bool: - """ - Check if a value satisfies the 'And' constraint expression. - - :param description: the description to check. - :return: True if the description satisfy the constraint expression, False otherwise. - """ - return all(expr.check(description) for expr in self.constraints) - - def __eq__(self, other): - """Compare with another object.""" - return isinstance(other, And) and self.constraints == other.constraints - - -class Or(ConstraintExpr): - """Implementation of the 'Or' constraint expression.""" - - def __init__(self, constraints: List[ConstraintExpr]): - """ - Initialize an 'Or' expression. - - :param constraints: the list of constraints expressions (in disjunction). - """ - self.constraints = constraints - - def check(self, description: Description) -> bool: - """ - Check if a value satisfies the 'Or' constraint expression. - - :param description: the description to check. - :return: True if the description satisfy the constraint expression, False otherwise. - """ - return any(expr.check(description) for expr in self.constraints) - - def __eq__(self, other): - """Compare with another object.""" - return isinstance(other, Or) and self.constraints == other.constraints - - -class Not(ConstraintExpr): - """Implementation of the 'Not' constraint expression.""" - - def __init__(self, constraint: ConstraintExpr): - """ - Initialize a 'Not' expression. - - :param constraint: the constraint expression to negate. - """ - self.constraint = constraint - - def check(self, description: Description) -> bool: - """ - Check if a value satisfies the 'Not; constraint expression. - - :param description: the description to check. - :return: True if the description satisfy the constraint expression, False otherwise. - """ - return not self.constraint.check(description) - - def __eq__(self, other): - """Compare with another object.""" - return isinstance(other, Not) and self.constraint == other.constraint - - -class Constraint(ConstraintExpr): - """The atomic component of a constraint expression.""" - - def __init__(self, attribute_name: str, constraint_type: ConstraintType): - """ - Initialize a constraint. - - :param attribute_name: the name of the attribute to be constrained. - :param constraint_type: the constraint type. - """ - self.attribute_name = attribute_name - self.constraint_type = constraint_type - - def check(self, description: Description) -> bool: - """ - Check if a description satisfies the constraint. The implementation depends on the type of the constraint. - - :param description: the description to check. - :return: True if the description satisfies the constraint, False otherwise. - - Examples: - >>> attr_author = Attribute("author" , str, True, "The author of the book.") - >>> attr_year = Attribute("year", int, True, "The year of publication of the book.") - >>> attr_genre = Attribute("genre", str, True, "The genre of the book.") - >>> c1 = Constraint("author", ConstraintType("==", "Stephen King")) - >>> c2 = Constraint("year", ConstraintType(">", 1990)) - >>> c3 = Constraint("genre", ConstraintType("in", {"horror", "science_fiction"})) - >>> book_1 = Description({"author": "Stephen King", "year": 1991, "genre": "horror"}) - >>> book_2 = Description({"author": "George Orwell", "year": 1948, "genre": "horror"}) - - The "author" attribute instantiation satisfies the constraint, so the result is True. - - >>> c1.check(book_1) - True - - Here, the "author" does not satisfy the constraints. Hence, the result is False. - - >>> c1.check(book_2) - False - - In this case, there is a missing field specified by the query, that is "year" - So the result is False, even in the case it is not required by the schema: - - >>> c2.check(Description({"author": "Stephen King"})) - False - - If the type of some attribute of the description is not correct, the result is False. - In this case, the field "year" has a string instead of an integer: - - >>> c2.check(Description({"author": "Stephen King", "year": "1991"})) - False - - >>> c3.check(Description({"author": "Stephen King", "genre": False})) - False - - """ - # if the name of the attribute is not present, return false. - name = self.attribute_name - if name not in description.values: - return False - - # if the type of the value is different from the type of the attribute, return false. - value = description.values[name] - if type(self.constraint_type.value) in {list, tuple, set} \ - and not isinstance(value, type(next(iter(self.constraint_type.value)))): - return False - if not isinstance(value, type(self.constraint_type.value)): - return False - - # dispatch the check to the right implementation for the concrete constraint type. - return self.constraint_type.check(value) - - def __eq__(self, other): - """Compare with another object.""" - return isinstance(other, Constraint) \ - and self.attribute_name == other.attribute_name \ - and self.constraint_type == other.constraint_type - - -class Query: - """This class lets you build a query for the OEF.""" - - def __init__(self, constraints: List[ConstraintExpr], model: Optional[DataModel] = None) -> None: - """ - Initialize a query. - - :param constraints: a list of constraint expressions. - :param model: the data model that the query refers to. - """ - self.constraints = constraints - self.model = model - - def check(self, description: Description) -> bool: - """ - Check if a description satisfies the constraints of the query. - - The constraints are interpreted as conjunction. - - :param description: the description to check. - :return: True if the description satisfies all the constraints, False otherwise. - """ - return all(c.check(description) for c in self.constraints) - - def __eq__(self, other): - """Compare with another object.""" - return isinstance(other, Query) \ - and self.constraints == other.constraints \ - and self.model == other.model diff --git a/my_agent/protocols/oef/protocol.yaml b/my_agent/protocols/oef/protocol.yaml deleted file mode 100644 index 918c9a101e..0000000000 --- a/my_agent/protocols/oef/protocol.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: 'oef' -authors: Fetch.AI Limited -version: 0.1.0 -license: Apache 2.0 -url: "" -dependencies: - - colorlog - - oef -description: "oef protocol description [Fill in]" \ No newline at end of file diff --git a/my_agent/protocols/oef/serialization.py b/my_agent/protocols/oef/serialization.py deleted file mode 100644 index c2af02977f..0000000000 --- a/my_agent/protocols/oef/serialization.py +++ /dev/null @@ -1,96 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Serialization for the FIPA protocol.""" -import base64 -import copy -import json -import pickle - -from aea.protocols.base import Message -from aea.protocols.base import Serializer -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.models import Description, Query - -"""default 'to' field for OEF envelopes.""" -DEFAULT_OEF = "oef" - - -class OEFSerializer(Serializer): - """Serialization for the OEF protocol.""" - - def encode(self, msg: Message) -> bytes: - """ - Decode the message. - - :param msg: the message object - :return: the bytes - """ - oef_type = OEFMessage.Type(msg.get("type")) - new_body = copy.copy(msg.body) - new_body["type"] = oef_type.value - - if oef_type in {OEFMessage.Type.REGISTER_SERVICE, OEFMessage.Type.UNREGISTER_SERVICE}: - service_description = msg.body["service_description"] # type: Description - service_description_bytes = base64.b64encode(pickle.dumps(service_description)).decode("utf-8") - new_body["service_description"] = service_description_bytes - elif oef_type in {OEFMessage.Type.SEARCH_SERVICES, OEFMessage.Type.SEARCH_AGENTS}: - query = msg.body["query"] # type: Query - query_bytes = base64.b64encode(pickle.dumps(query)).decode("utf-8") - new_body["query"] = query_bytes - elif oef_type in {OEFMessage.Type.SEARCH_RESULT}: - # we need this cast because the "agents" field might contains - # the Protobuf type "RepeatedScalarContainer", which is not JSON serializable. - new_body["agents"] = list(msg.body["agents"]) - elif oef_type in {OEFMessage.Type.OEF_ERROR}: - operation = msg.body["operation"] - new_body["operation"] = str(operation) - - oef_message_bytes = json.dumps(new_body).encode("utf-8") - return oef_message_bytes - - def decode(self, obj: bytes) -> Message: - """ - Decode the message. - - :param obj: the bytes object - :return: the message - """ - json_msg = json.loads(obj.decode("utf-8")) - oef_type = OEFMessage.Type(json_msg["type"]) - new_body = copy.copy(json_msg) - new_body["type"] = oef_type - - if oef_type in {OEFMessage.Type.REGISTER_SERVICE, OEFMessage.Type.UNREGISTER_SERVICE}: - service_description_bytes = base64.b64decode(json_msg["service_description"]) - service_description = pickle.loads(service_description_bytes) - new_body["service_description"] = service_description - elif oef_type in {OEFMessage.Type.SEARCH_SERVICES, OEFMessage.Type.SEARCH_AGENTS}: - query_bytes = base64.b64decode(json_msg["query"]) - query = pickle.loads(query_bytes) - new_body["query"] = query - elif oef_type in {OEFMessage.Type.SEARCH_RESULT}: - new_body["agents"] = list(json_msg["agents"]) - elif oef_type in {OEFMessage.Type.OEF_ERROR}: - operation = json_msg["operation"] - new_body["operation"] = OEFMessage.OEFErrorOperation(int(operation)) - - oef_message = OEFMessage(oef_type=oef_type, body=new_body) - return oef_message diff --git a/my_agent/protocols/tac/__init__.py b/my_agent/protocols/tac/__init__.py deleted file mode 100644 index 430d160a4f..0000000000 --- a/my_agent/protocols/tac/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the support resources for the TAC protocol.""" diff --git a/my_agent/protocols/tac/message.py b/my_agent/protocols/tac/message.py deleted file mode 100644 index 07a100d90e..0000000000 --- a/my_agent/protocols/tac/message.py +++ /dev/null @@ -1,134 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the default message definition.""" -from enum import Enum -from typing import Dict, Optional, cast - -from aea.protocols.base import Message - - -class TACMessage(Message): - """The TAC message class.""" - - protocol_id = "tac" - - class Type(Enum): - """TAC Message types.""" - - REGISTER = "register" - UNREGISTER = "unregister" - TRANSACTION = "transaction" - GET_STATE_UPDATE = "get_state_update" - CANCELLED = "cancelled" - GAME_DATA = "game_data" - TRANSACTION_CONFIRMATION = "transaction_confirmation" - STATE_UPDATE = "state_update" - TAC_ERROR = "tac_error" - - def __str__(self): - """Get string representation.""" - return self.value - - class ErrorCode(Enum): - """This class defines the error codes.""" - - GENERIC_ERROR = 0 - REQUEST_NOT_VALID = 1 - AGENT_PBK_ALREADY_REGISTERED = 2 - AGENT_NAME_ALREADY_REGISTERED = 3 - AGENT_NOT_REGISTERED = 4 - TRANSACTION_NOT_VALID = 5 - TRANSACTION_NOT_MATCHING = 6 - AGENT_NAME_NOT_IN_WHITELIST = 7 - COMPETITION_NOT_RUNNING = 8 - DIALOGUE_INCONSISTENT = 9 - - _from_ec_to_msg = { - ErrorCode.GENERIC_ERROR: "Unexpected error.", - ErrorCode.REQUEST_NOT_VALID: "Request not recognized", - ErrorCode.AGENT_PBK_ALREADY_REGISTERED: "Agent pbk already registered.", - ErrorCode.AGENT_NAME_ALREADY_REGISTERED: "Agent name already registered.", - ErrorCode.AGENT_NOT_REGISTERED: "Agent not registered.", - ErrorCode.TRANSACTION_NOT_VALID: "Error in checking transaction", - ErrorCode.TRANSACTION_NOT_MATCHING: "The transaction request does not match with a previous transaction request with the same id.", - ErrorCode.AGENT_NAME_NOT_IN_WHITELIST: "Agent name not in whitelist.", - ErrorCode.COMPETITION_NOT_RUNNING: "The competition is not running yet.", - ErrorCode.DIALOGUE_INCONSISTENT: "The message is inconsistent with the dialogue." - } # type: Dict[ErrorCode, str] - - def __init__(self, tac_type: Optional[Type] = None, - **kwargs): - """ - Initialize. - - :param tac_type: the type of TAC message. - """ - super().__init__(type=tac_type, **kwargs) - assert self.check_consistency(), "TACMessage initialization inconsistent." - - def check_consistency(self) -> bool: - """Check that the data is consistent.""" - try: - assert self.is_set("type") - tac_type = TACMessage.Type(self.get("type")) - if tac_type == TACMessage.Type.REGISTER: - assert self.is_set("agent_name") - elif tac_type == TACMessage.Type.UNREGISTER: - pass - elif tac_type == TACMessage.Type.TRANSACTION: - assert self.is_set("transaction_id") - assert self.is_set("is_sender_buyer") - assert self.is_set("counterparty") - assert self.is_set("amount") - amount = cast(float, self.get("amount")) - assert amount >= 0.0 - assert self.is_set("quantities_by_good_pbk") - quantities_by_good_pbk = cast(Dict[str, int], self.get("quantities_by_good_pbk")) - assert len(quantities_by_good_pbk.keys()) == len(set(quantities_by_good_pbk.keys())) - assert all(quantity >= 0 for quantity in quantities_by_good_pbk.values()) - elif tac_type == TACMessage.Type.GET_STATE_UPDATE: - pass - elif tac_type == TACMessage.Type.CANCELLED: - pass - elif tac_type == TACMessage.Type.GAME_DATA: - assert self.is_set("money") - assert self.is_set("endowment") - assert self.is_set("utility_params") - assert self.is_set("nb_agents") - assert self.is_set("nb_goods") - assert self.is_set("tx_fee") - assert self.is_set("agent_pbk_to_name") - assert self.is_set("good_pbk_to_name") - elif tac_type == TACMessage.Type.TRANSACTION_CONFIRMATION: - assert self.is_set("transaction_id") - elif tac_type == TACMessage.Type.STATE_UPDATE: - assert self.is_set("initial_state") - assert self.is_set("transactions") - elif tac_type == TACMessage.Type.TAC_ERROR: - assert self.is_set("error_code") - error_code = self.get("error_code") - assert error_code in set(self.ErrorCode) - else: - raise ValueError("Type not recognized.") - except (AssertionError, ValueError): - return False - - return True diff --git a/my_agent/protocols/tac/protocol.yaml b/my_agent/protocols/tac/protocol.yaml deleted file mode 100644 index 435e33b383..0000000000 --- a/my_agent/protocols/tac/protocol.yaml +++ /dev/null @@ -1,6 +0,0 @@ -name: tac -authors: Fetch.AI Limited -version: 0.1.0 -license: Apache 2.0 -url: "" -description: "The tac protocol implements the messages an AEA needs to participate in the TAC." \ No newline at end of file diff --git a/my_agent/protocols/tac/serialization.py b/my_agent/protocols/tac/serialization.py deleted file mode 100644 index e4e085d5b0..0000000000 --- a/my_agent/protocols/tac/serialization.py +++ /dev/null @@ -1,234 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Serialization for the TAC protocol.""" - -from typing import Any, Dict, List, cast, TYPE_CHECKING - -from aea.protocols.base import Message -from aea.protocols.base import Serializer - -if TYPE_CHECKING: - from packages.protocols.tac import tac_pb2 - from packages.protocols.tac.message import TACMessage -else: - import tac_protocol.tac_pb2 as tac_pb2 - from tac_protocol.message import TACMessage - - -def _from_dict_to_pairs(d): - """Convert a flat dictionary into a list of StrStrPair or StrIntPair.""" - result = [] - items = sorted(d.items(), key=lambda pair: pair[0]) - for key, value in items: - if type(value) == int: - pair = tac_pb2.StrIntPair() - elif type(value) == str: - pair = tac_pb2.StrStrPair() - else: - raise ValueError("Either 'int' or 'str', not {}".format(type(value))) - pair.first = key - pair.second = value - result.append(pair) - return result - - -def _from_pairs_to_dict(pairs): - """Convert a list of StrStrPair or StrIntPair into a flat dictionary.""" - result = {} - for pair in pairs: - key = pair.first - value = pair.second - result[key] = value - return result - - -class TACSerializer(Serializer): - """Serialization for the TAC protocol.""" - - def encode(self, msg: Message) -> bytes: - """ - Decode the message. - - :param msg: the message object - :return: the bytes - """ - tac_type = TACMessage.Type(msg.get("type")) - tac_container = tac_pb2.TACMessage() - - if tac_type == TACMessage.Type.REGISTER: - agent_name = msg.get("agent_name") - tac_msg = tac_pb2.TACAgent.Register() # type: ignore - tac_msg.agent_name = agent_name - tac_container.register.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.UNREGISTER: - tac_msg = tac_pb2.TACAgent.Unregister() # type: ignore - tac_container.unregister.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.TRANSACTION: - tac_msg = tac_pb2.TACAgent.Transaction() # type: ignore - tac_msg.transaction_id = msg.get("transaction_id") - tac_msg.is_sender_buyer = msg.get("is_sender_buyer") - tac_msg.counterparty = msg.get("counterparty") - tac_msg.amount = msg.get("amount") - tac_msg.quantities.extend(_from_dict_to_pairs(msg.get("quantities_by_good_pbk"))) - tac_container.transaction.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.GET_STATE_UPDATE: - tac_msg = tac_pb2.TACAgent.GetStateUpdate() # type: ignore - tac_container.get_state_update.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.CANCELLED: - tac_msg = tac_pb2.TACController.Cancelled() # type: ignore - tac_container.cancelled.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.GAME_DATA: - tac_msg = tac_pb2.TACController.GameData() # type: ignore - tac_msg.money = msg.get("money") - tac_msg.endowment.extend(msg.get("endowment")) - tac_msg.utility_params.extend(msg.get("utility_params")) - tac_msg.nb_agents = msg.get("nb_agents") - tac_msg.nb_goods = msg.get("nb_goods") - tac_msg.tx_fee = msg.get("tx_fee") - tac_msg.agent_pbk_to_name.extend(_from_dict_to_pairs(msg.get("agent_pbk_to_name"))) - tac_msg.good_pbk_to_name.extend(_from_dict_to_pairs(msg.get("good_pbk_to_name"))) - tac_container.game_data.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.TRANSACTION_CONFIRMATION: - tac_msg = tac_pb2.TACController.TransactionConfirmation() # type: ignore - tac_msg.transaction_id = msg.get("transaction_id") - tac_container.transaction_confirmation.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.STATE_UPDATE: - tac_msg = tac_pb2.TACController.StateUpdate() # type: ignore - game_data_json = msg.get("initial_state") - game_data = tac_pb2.TACController.GameData() # type: ignore - game_data.money = game_data_json["money"] # type: ignore - game_data.endowment.extend(game_data_json["endowment"]) # type: ignore - game_data.utility_params.extend(game_data_json["utility_params"]) # type: ignore - game_data.nb_agents = game_data_json["nb_agents"] # type: ignore - game_data.nb_goods = game_data_json["nb_goods"] # type: ignore - game_data.tx_fee = game_data_json["tx_fee"] # type: ignore - game_data.agent_pbk_to_name.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["agent_pbk_to_name"]))) # type: ignore - game_data.good_pbk_to_name.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["good_pbk_to_name"]))) # type: ignore - - tac_msg.initial_state.CopyFrom(game_data) - - transactions = [] - msg_transactions = cast(List[Any], msg.get("transactions")) - for t in msg_transactions: - tx = tac_pb2.TACAgent.Transaction() # type: ignore - tx.transaction_id = t.get("transaction_id") - tx.is_sender_buyer = t.get("is_sender_buyer") - tx.counterparty = t.get("counterparty") - tx.amount = t.get("amount") - tx.quantities.extend(_from_dict_to_pairs(t.get("quantities_by_good_pbk"))) - transactions.append(tx) - tac_msg.txs.extend(transactions) - tac_container.state_update.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.TAC_ERROR: - tac_msg = tac_pb2.TACController.Error() # type: ignore - tac_msg.error_code = TACMessage.ErrorCode(msg.get("error_code")).value - if msg.is_set("error_msg"): - tac_msg.error_msg = msg.get("error_msg") - if msg.is_set("details"): - tac_msg.details.update(msg.get("details")) - - tac_container.error.CopyFrom(tac_msg) - else: - raise ValueError("Type not recognized: {}.".format(tac_type)) - - tac_message_bytes = tac_container.SerializeToString() - return tac_message_bytes - - def decode(self, obj: bytes) -> Message: - """ - Decode the message. - - :param obj: the bytes object - :return: the message - """ - tac_container = tac_pb2.TACMessage() - tac_container.ParseFromString(obj) - - new_body = {} # type: Dict[str, Any] - tac_type = tac_container.WhichOneof("content") - - if tac_type == "register": - new_body["type"] = TACMessage.Type.REGISTER - new_body["agent_name"] = tac_container.register.agent_name - elif tac_type == "unregister": - new_body["type"] = TACMessage.Type.UNREGISTER - elif tac_type == "transaction": - new_body["type"] = TACMessage.Type.TRANSACTION - new_body["transaction_id"] = tac_container.transaction.transaction_id - new_body["is_sender_buyer"] = tac_container.transaction.is_sender_buyer - new_body["counterparty"] = tac_container.transaction.counterparty - new_body["amount"] = tac_container.transaction.amount - new_body["quantities_by_good_pbk"] = _from_pairs_to_dict(tac_container.transaction.quantities) - elif tac_type == "get_state_update": - new_body["type"] = TACMessage.Type.GET_STATE_UPDATE - elif tac_type == "cancelled": - new_body["type"] = TACMessage.Type.CANCELLED - elif tac_type == "game_data": - new_body["type"] = TACMessage.Type.GAME_DATA - new_body["money"] = tac_container.game_data.money - new_body["endowment"] = list(tac_container.game_data.endowment) - new_body["utility_params"] = list(tac_container.game_data.utility_params) - new_body["nb_agents"] = tac_container.game_data.nb_agents - new_body["nb_goods"] = tac_container.game_data.nb_goods - new_body["tx_fee"] = tac_container.game_data.tx_fee - new_body["agent_pbk_to_name"] = _from_pairs_to_dict(tac_container.game_data.agent_pbk_to_name) - new_body["good_pbk_to_name"] = _from_pairs_to_dict(tac_container.game_data.good_pbk_to_name) - elif tac_type == "transaction_confirmation": - new_body["type"] = TACMessage.Type.TRANSACTION_CONFIRMATION - new_body["transaction_id"] = tac_container.transaction_confirmation.transaction_id - elif tac_type == "state_update": - new_body["type"] = TACMessage.Type.STATE_UPDATE - game_data = dict( - money=tac_container.state_update.initial_state.money, - endowment=tac_container.state_update.initial_state.endowment, - utility_params=tac_container.state_update.initial_state.utility_params, - nb_agents=tac_container.state_update.initial_state.nb_agents, - nb_goods=tac_container.state_update.initial_state.nb_goods, - tx_fee=tac_container.state_update.initial_state.tx_fee, - agent_pbk_to_name=_from_pairs_to_dict(tac_container.state_update.initial_state.agent_pbk_to_name), - good_pbk_to_name=_from_pairs_to_dict(tac_container.state_update.initial_state.good_pbk_to_name), - ) - new_body["initial_state"] = game_data - transactions = [] - for t in tac_container.state_update.txs: - tx_json = dict( - transaction_id=t.transaction_id, - is_sender_buyer=t.is_sender_buyer, - counterparty=t.counterparty, - amount=t.amount, - quantities_by_good_pbk=_from_pairs_to_dict(t.quantities), - ) - transactions.append(tx_json) - new_body["transactions"] = transactions - elif tac_type == "error": - new_body["type"] = TACMessage.Type.TAC_ERROR - new_body["error_code"] = TACMessage.ErrorCode(tac_container.error.error_code) - if tac_container.error.error_msg: - new_body["error_msg"] = tac_container.error.error_msg - if tac_container.error.details: - new_body["details"] = dict(tac_container.error.details) - else: - raise ValueError("Type not recognized.") - - tac_type = TACMessage.Type(new_body["type"]) - new_body["type"] = tac_type - tac_message = TACMessage(tac_type=tac_type, body=new_body) - return tac_message diff --git a/my_agent/protocols/tac/tac.proto b/my_agent/protocols/tac/tac.proto deleted file mode 100644 index d6a714d425..0000000000 --- a/my_agent/protocols/tac/tac.proto +++ /dev/null @@ -1,102 +0,0 @@ -syntax = "proto3"; - -package fetch.oef.pb; - -import "google/protobuf/struct.proto"; - -message StrIntPair { - string first = 1; - int32 second = 2; -} - -message StrStrPair { - string first = 1; - string second = 2; -} - -message TACController { - - message Registered { - } - message Unregistered { - } - message Cancelled { - } - - message GameData { - double money = 1; - repeated int32 endowment = 2; - repeated double utility_params = 3; - int32 nb_agents = 4; - int32 nb_goods = 5; - double tx_fee = 6; - repeated StrStrPair agent_pbk_to_name = 7; - repeated StrStrPair good_pbk_to_name = 8; - } - - message TransactionConfirmation { - string transaction_id = 1; - } - - message StateUpdate { - GameData initial_state = 1; - repeated TACAgent.Transaction txs = 2; - } - - message Error { - enum ErrorCode { - GENERIC_ERROR = 0; - REQUEST_NOT_VALID = 1; - AGENT_PBK_ALREADY_REGISTERED = 2; - AGENT_NAME_ALREADY_REGISTERED = 3; - AGENT_NOT_REGISTERED = 4; - TRANSACTION_NOT_VALID = 5; - TRANSACTION_NOT_MATCHING = 6; - AGENT_NAME_NOT_IN_WHITELIST = 7; - COMPETITION_NOT_RUNNING = 8; - DIALOGUE_INCONSISTENT = 9; - } - - ErrorCode error_code = 1; - string error_msg = 2; - google.protobuf.Struct details = 3; - } - -} - -message TACAgent { - - message Register { - string agent_name = 1; - } - message Unregister { - } - - message Transaction { - string transaction_id = 1; - bool is_sender_buyer = 2; // is the sender of this message a buyer? - string counterparty = 3; - double amount = 4; - repeated StrIntPair quantities = 5; - } - - message GetStateUpdate { - } - -} - -message TACMessage { - oneof content{ - TACAgent.Register register = 1; - TACAgent.Unregister unregister = 2; - TACAgent.Transaction transaction = 3; - TACAgent.GetStateUpdate get_state_update = 4; - TACController.Registered registered = 5; - TACController.Unregistered unregistered = 6; - TACController.Cancelled cancelled = 7; - TACController.GameData game_data = 8; - TACController.TransactionConfirmation transaction_confirmation = 9; - TACController.StateUpdate state_update = 10; - TACController.Error error = 11; - } -} diff --git a/my_agent/protocols/tac/tac_pb2.py b/my_agent/protocols/tac/tac_pb2.py deleted file mode 100644 index 5eb63f23f1..0000000000 --- a/my_agent/protocols/tac/tac_pb2.py +++ /dev/null @@ -1,899 +0,0 @@ -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: tac.proto - -import sys -_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection -from google.protobuf import symbol_database as _symbol_database -from google.protobuf import descriptor_pb2 -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='tac.proto', - package='fetch.oef.pb', - syntax='proto3', - serialized_pb=_b('\n\ttac.proto\x12\x0c\x66\x65tch.oef.pb\x1a\x1cgoogle/protobuf/struct.proto\"+\n\nStrIntPair\x12\r\n\x05\x66irst\x18\x01 \x01(\t\x12\x0e\n\x06second\x18\x02 \x01(\x05\"+\n\nStrStrPair\x12\r\n\x05\x66irst\x18\x01 \x01(\t\x12\x0e\n\x06second\x18\x02 \x01(\t\"\x80\x07\n\rTACController\x1a\x0c\n\nRegistered\x1a\x0e\n\x0cUnregistered\x1a\x0b\n\tCancelled\x1a\xe2\x01\n\x08GameData\x12\r\n\x05money\x18\x01 \x01(\x01\x12\x11\n\tendowment\x18\x02 \x03(\x05\x12\x16\n\x0eutility_params\x18\x03 \x03(\x01\x12\x11\n\tnb_agents\x18\x04 \x01(\x05\x12\x10\n\x08nb_goods\x18\x05 \x01(\x05\x12\x0e\n\x06tx_fee\x18\x06 \x01(\x01\x12\x33\n\x11\x61gent_pbk_to_name\x18\x07 \x03(\x0b\x32\x18.fetch.oef.pb.StrStrPair\x12\x32\n\x10good_pbk_to_name\x18\x08 \x03(\x0b\x32\x18.fetch.oef.pb.StrStrPair\x1a\x31\n\x17TransactionConfirmation\x12\x16\n\x0etransaction_id\x18\x01 \x01(\t\x1a{\n\x0bStateUpdate\x12;\n\rinitial_state\x18\x01 \x01(\x0b\x32$.fetch.oef.pb.TACController.GameData\x12/\n\x03txs\x18\x02 \x03(\x0b\x32\".fetch.oef.pb.TACAgent.Transaction\x1a\xae\x03\n\x05\x45rror\x12?\n\nerror_code\x18\x01 \x01(\x0e\x32+.fetch.oef.pb.TACController.Error.ErrorCode\x12\x11\n\terror_msg\x18\x02 \x01(\t\x12(\n\x07\x64\x65tails\x18\x03 \x01(\x0b\x32\x17.google.protobuf.Struct\"\xa6\x02\n\tErrorCode\x12\x11\n\rGENERIC_ERROR\x10\x00\x12\x15\n\x11REQUEST_NOT_VALID\x10\x01\x12 \n\x1c\x41GENT_PBK_ALREADY_REGISTERED\x10\x02\x12!\n\x1d\x41GENT_NAME_ALREADY_REGISTERED\x10\x03\x12\x18\n\x14\x41GENT_NOT_REGISTERED\x10\x04\x12\x19\n\x15TRANSACTION_NOT_VALID\x10\x05\x12\x1c\n\x18TRANSACTION_NOT_MATCHING\x10\x06\x12\x1f\n\x1b\x41GENT_NAME_NOT_IN_WHITELIST\x10\x07\x12\x1b\n\x17\x43OMPETITION_NOT_RUNNING\x10\x08\x12\x19\n\x15\x44IALOGUE_INCONSISTENT\x10\t\"\xdf\x01\n\x08TACAgent\x1a\x1e\n\x08Register\x12\x12\n\nagent_name\x18\x01 \x01(\t\x1a\x0c\n\nUnregister\x1a\x92\x01\n\x0bTransaction\x12\x16\n\x0etransaction_id\x18\x01 \x01(\t\x12\x17\n\x0fis_sender_buyer\x18\x02 \x01(\x08\x12\x14\n\x0c\x63ounterparty\x18\x03 \x01(\t\x12\x0e\n\x06\x61mount\x18\x04 \x01(\x01\x12,\n\nquantities\x18\x05 \x03(\x0b\x32\x18.fetch.oef.pb.StrIntPair\x1a\x10\n\x0eGetStateUpdate\"\xc8\x05\n\nTACMessage\x12\x33\n\x08register\x18\x01 \x01(\x0b\x32\x1f.fetch.oef.pb.TACAgent.RegisterH\x00\x12\x37\n\nunregister\x18\x02 \x01(\x0b\x32!.fetch.oef.pb.TACAgent.UnregisterH\x00\x12\x39\n\x0btransaction\x18\x03 \x01(\x0b\x32\".fetch.oef.pb.TACAgent.TransactionH\x00\x12\x41\n\x10get_state_update\x18\x04 \x01(\x0b\x32%.fetch.oef.pb.TACAgent.GetStateUpdateH\x00\x12<\n\nregistered\x18\x05 \x01(\x0b\x32&.fetch.oef.pb.TACController.RegisteredH\x00\x12@\n\x0cunregistered\x18\x06 \x01(\x0b\x32(.fetch.oef.pb.TACController.UnregisteredH\x00\x12:\n\tcancelled\x18\x07 \x01(\x0b\x32%.fetch.oef.pb.TACController.CancelledH\x00\x12\x39\n\tgame_data\x18\x08 \x01(\x0b\x32$.fetch.oef.pb.TACController.GameDataH\x00\x12W\n\x18transaction_confirmation\x18\t \x01(\x0b\x32\x33.fetch.oef.pb.TACController.TransactionConfirmationH\x00\x12?\n\x0cstate_update\x18\n \x01(\x0b\x32\'.fetch.oef.pb.TACController.StateUpdateH\x00\x12\x32\n\x05\x65rror\x18\x0b \x01(\x0b\x32!.fetch.oef.pb.TACController.ErrorH\x00\x42\t\n\x07\x63ontentb\x06proto3') - , - dependencies=[google_dot_protobuf_dot_struct__pb2.DESCRIPTOR,]) -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - - - -_TACCONTROLLER_ERROR_ERRORCODE = _descriptor.EnumDescriptor( - name='ErrorCode', - full_name='fetch.oef.pb.TACController.Error.ErrorCode', - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name='GENERIC_ERROR', index=0, number=0, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='REQUEST_NOT_VALID', index=1, number=1, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='AGENT_PBK_ALREADY_REGISTERED', index=2, number=2, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='AGENT_NAME_ALREADY_REGISTERED', index=3, number=3, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='AGENT_NOT_REGISTERED', index=4, number=4, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='TRANSACTION_NOT_VALID', index=5, number=5, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='TRANSACTION_NOT_MATCHING', index=6, number=6, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='AGENT_NAME_NOT_IN_WHITELIST', index=7, number=7, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='COMPETITION_NOT_RUNNING', index=8, number=8, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='DIALOGUE_INCONSISTENT', index=9, number=9, - options=None, - type=None), - ], - containing_type=None, - options=None, - serialized_start=750, - serialized_end=1044, -) -_sym_db.RegisterEnumDescriptor(_TACCONTROLLER_ERROR_ERRORCODE) - - -_STRINTPAIR = _descriptor.Descriptor( - name='StrIntPair', - full_name='fetch.oef.pb.StrIntPair', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='first', full_name='fetch.oef.pb.StrIntPair.first', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='second', full_name='fetch.oef.pb.StrIntPair.second', index=1, - number=2, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=57, - serialized_end=100, -) - - -_STRSTRPAIR = _descriptor.Descriptor( - name='StrStrPair', - full_name='fetch.oef.pb.StrStrPair', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='first', full_name='fetch.oef.pb.StrStrPair.first', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='second', full_name='fetch.oef.pb.StrStrPair.second', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=102, - serialized_end=145, -) - - -_TACCONTROLLER_REGISTERED = _descriptor.Descriptor( - name='Registered', - full_name='fetch.oef.pb.TACController.Registered', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=165, - serialized_end=177, -) - -_TACCONTROLLER_UNREGISTERED = _descriptor.Descriptor( - name='Unregistered', - full_name='fetch.oef.pb.TACController.Unregistered', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=179, - serialized_end=193, -) - -_TACCONTROLLER_CANCELLED = _descriptor.Descriptor( - name='Cancelled', - full_name='fetch.oef.pb.TACController.Cancelled', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=195, - serialized_end=206, -) - -_TACCONTROLLER_GAMEDATA = _descriptor.Descriptor( - name='GameData', - full_name='fetch.oef.pb.TACController.GameData', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='money', full_name='fetch.oef.pb.TACController.GameData.money', index=0, - number=1, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='endowment', full_name='fetch.oef.pb.TACController.GameData.endowment', index=1, - number=2, type=5, cpp_type=1, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='utility_params', full_name='fetch.oef.pb.TACController.GameData.utility_params', index=2, - number=3, type=1, cpp_type=5, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='nb_agents', full_name='fetch.oef.pb.TACController.GameData.nb_agents', index=3, - number=4, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='nb_goods', full_name='fetch.oef.pb.TACController.GameData.nb_goods', index=4, - number=5, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='tx_fee', full_name='fetch.oef.pb.TACController.GameData.tx_fee', index=5, - number=6, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='agent_pbk_to_name', full_name='fetch.oef.pb.TACController.GameData.agent_pbk_to_name', index=6, - number=7, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='good_pbk_to_name', full_name='fetch.oef.pb.TACController.GameData.good_pbk_to_name', index=7, - number=8, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=209, - serialized_end=435, -) - -_TACCONTROLLER_TRANSACTIONCONFIRMATION = _descriptor.Descriptor( - name='TransactionConfirmation', - full_name='fetch.oef.pb.TACController.TransactionConfirmation', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='transaction_id', full_name='fetch.oef.pb.TACController.TransactionConfirmation.transaction_id', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=437, - serialized_end=486, -) - -_TACCONTROLLER_STATEUPDATE = _descriptor.Descriptor( - name='StateUpdate', - full_name='fetch.oef.pb.TACController.StateUpdate', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='initial_state', full_name='fetch.oef.pb.TACController.StateUpdate.initial_state', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='txs', full_name='fetch.oef.pb.TACController.StateUpdate.txs', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=488, - serialized_end=611, -) - -_TACCONTROLLER_ERROR = _descriptor.Descriptor( - name='Error', - full_name='fetch.oef.pb.TACController.Error', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='error_code', full_name='fetch.oef.pb.TACController.Error.error_code', index=0, - number=1, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='error_msg', full_name='fetch.oef.pb.TACController.Error.error_msg', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='details', full_name='fetch.oef.pb.TACController.Error.details', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _TACCONTROLLER_ERROR_ERRORCODE, - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=614, - serialized_end=1044, -) - -_TACCONTROLLER = _descriptor.Descriptor( - name='TACController', - full_name='fetch.oef.pb.TACController', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[_TACCONTROLLER_REGISTERED, _TACCONTROLLER_UNREGISTERED, _TACCONTROLLER_CANCELLED, _TACCONTROLLER_GAMEDATA, _TACCONTROLLER_TRANSACTIONCONFIRMATION, _TACCONTROLLER_STATEUPDATE, _TACCONTROLLER_ERROR, ], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=148, - serialized_end=1044, -) - - -_TACAGENT_REGISTER = _descriptor.Descriptor( - name='Register', - full_name='fetch.oef.pb.TACAgent.Register', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='agent_name', full_name='fetch.oef.pb.TACAgent.Register.agent_name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1059, - serialized_end=1089, -) - -_TACAGENT_UNREGISTER = _descriptor.Descriptor( - name='Unregister', - full_name='fetch.oef.pb.TACAgent.Unregister', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1091, - serialized_end=1103, -) - -_TACAGENT_TRANSACTION = _descriptor.Descriptor( - name='Transaction', - full_name='fetch.oef.pb.TACAgent.Transaction', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='transaction_id', full_name='fetch.oef.pb.TACAgent.Transaction.transaction_id', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='is_sender_buyer', full_name='fetch.oef.pb.TACAgent.Transaction.is_sender_buyer', index=1, - number=2, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='counterparty', full_name='fetch.oef.pb.TACAgent.Transaction.counterparty', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='amount', full_name='fetch.oef.pb.TACAgent.Transaction.amount', index=3, - number=4, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='quantities', full_name='fetch.oef.pb.TACAgent.Transaction.quantities', index=4, - number=5, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1106, - serialized_end=1252, -) - -_TACAGENT_GETSTATEUPDATE = _descriptor.Descriptor( - name='GetStateUpdate', - full_name='fetch.oef.pb.TACAgent.GetStateUpdate', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1254, - serialized_end=1270, -) - -_TACAGENT = _descriptor.Descriptor( - name='TACAgent', - full_name='fetch.oef.pb.TACAgent', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[_TACAGENT_REGISTER, _TACAGENT_UNREGISTER, _TACAGENT_TRANSACTION, _TACAGENT_GETSTATEUPDATE, ], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1047, - serialized_end=1270, -) - - -_TACMESSAGE = _descriptor.Descriptor( - name='TACMessage', - full_name='fetch.oef.pb.TACMessage', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='register', full_name='fetch.oef.pb.TACMessage.register', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='unregister', full_name='fetch.oef.pb.TACMessage.unregister', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='transaction', full_name='fetch.oef.pb.TACMessage.transaction', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='get_state_update', full_name='fetch.oef.pb.TACMessage.get_state_update', index=3, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='registered', full_name='fetch.oef.pb.TACMessage.registered', index=4, - number=5, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='unregistered', full_name='fetch.oef.pb.TACMessage.unregistered', index=5, - number=6, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='cancelled', full_name='fetch.oef.pb.TACMessage.cancelled', index=6, - number=7, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='game_data', full_name='fetch.oef.pb.TACMessage.game_data', index=7, - number=8, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='transaction_confirmation', full_name='fetch.oef.pb.TACMessage.transaction_confirmation', index=8, - number=9, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='state_update', full_name='fetch.oef.pb.TACMessage.state_update', index=9, - number=10, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='error', full_name='fetch.oef.pb.TACMessage.error', index=10, - number=11, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='content', full_name='fetch.oef.pb.TACMessage.content', - index=0, containing_type=None, fields=[]), - ], - serialized_start=1273, - serialized_end=1985, -) - -_TACCONTROLLER_REGISTERED.containing_type = _TACCONTROLLER -_TACCONTROLLER_UNREGISTERED.containing_type = _TACCONTROLLER -_TACCONTROLLER_CANCELLED.containing_type = _TACCONTROLLER -_TACCONTROLLER_GAMEDATA.fields_by_name['agent_pbk_to_name'].message_type = _STRSTRPAIR -_TACCONTROLLER_GAMEDATA.fields_by_name['good_pbk_to_name'].message_type = _STRSTRPAIR -_TACCONTROLLER_GAMEDATA.containing_type = _TACCONTROLLER -_TACCONTROLLER_TRANSACTIONCONFIRMATION.containing_type = _TACCONTROLLER -_TACCONTROLLER_STATEUPDATE.fields_by_name['initial_state'].message_type = _TACCONTROLLER_GAMEDATA -_TACCONTROLLER_STATEUPDATE.fields_by_name['txs'].message_type = _TACAGENT_TRANSACTION -_TACCONTROLLER_STATEUPDATE.containing_type = _TACCONTROLLER -_TACCONTROLLER_ERROR.fields_by_name['error_code'].enum_type = _TACCONTROLLER_ERROR_ERRORCODE -_TACCONTROLLER_ERROR.fields_by_name['details'].message_type = google_dot_protobuf_dot_struct__pb2._STRUCT -_TACCONTROLLER_ERROR.containing_type = _TACCONTROLLER -_TACCONTROLLER_ERROR_ERRORCODE.containing_type = _TACCONTROLLER_ERROR -_TACAGENT_REGISTER.containing_type = _TACAGENT -_TACAGENT_UNREGISTER.containing_type = _TACAGENT -_TACAGENT_TRANSACTION.fields_by_name['quantities'].message_type = _STRINTPAIR -_TACAGENT_TRANSACTION.containing_type = _TACAGENT -_TACAGENT_GETSTATEUPDATE.containing_type = _TACAGENT -_TACMESSAGE.fields_by_name['register'].message_type = _TACAGENT_REGISTER -_TACMESSAGE.fields_by_name['unregister'].message_type = _TACAGENT_UNREGISTER -_TACMESSAGE.fields_by_name['transaction'].message_type = _TACAGENT_TRANSACTION -_TACMESSAGE.fields_by_name['get_state_update'].message_type = _TACAGENT_GETSTATEUPDATE -_TACMESSAGE.fields_by_name['registered'].message_type = _TACCONTROLLER_REGISTERED -_TACMESSAGE.fields_by_name['unregistered'].message_type = _TACCONTROLLER_UNREGISTERED -_TACMESSAGE.fields_by_name['cancelled'].message_type = _TACCONTROLLER_CANCELLED -_TACMESSAGE.fields_by_name['game_data'].message_type = _TACCONTROLLER_GAMEDATA -_TACMESSAGE.fields_by_name['transaction_confirmation'].message_type = _TACCONTROLLER_TRANSACTIONCONFIRMATION -_TACMESSAGE.fields_by_name['state_update'].message_type = _TACCONTROLLER_STATEUPDATE -_TACMESSAGE.fields_by_name['error'].message_type = _TACCONTROLLER_ERROR -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['register']) -_TACMESSAGE.fields_by_name['register'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['unregister']) -_TACMESSAGE.fields_by_name['unregister'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['transaction']) -_TACMESSAGE.fields_by_name['transaction'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['get_state_update']) -_TACMESSAGE.fields_by_name['get_state_update'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['registered']) -_TACMESSAGE.fields_by_name['registered'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['unregistered']) -_TACMESSAGE.fields_by_name['unregistered'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['cancelled']) -_TACMESSAGE.fields_by_name['cancelled'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['game_data']) -_TACMESSAGE.fields_by_name['game_data'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['transaction_confirmation']) -_TACMESSAGE.fields_by_name['transaction_confirmation'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['state_update']) -_TACMESSAGE.fields_by_name['state_update'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -_TACMESSAGE.oneofs_by_name['content'].fields.append( - _TACMESSAGE.fields_by_name['error']) -_TACMESSAGE.fields_by_name['error'].containing_oneof = _TACMESSAGE.oneofs_by_name['content'] -DESCRIPTOR.message_types_by_name['StrIntPair'] = _STRINTPAIR -DESCRIPTOR.message_types_by_name['StrStrPair'] = _STRSTRPAIR -DESCRIPTOR.message_types_by_name['TACController'] = _TACCONTROLLER -DESCRIPTOR.message_types_by_name['TACAgent'] = _TACAGENT -DESCRIPTOR.message_types_by_name['TACMessage'] = _TACMESSAGE - -StrIntPair = _reflection.GeneratedProtocolMessageType('StrIntPair', (_message.Message,), dict( - DESCRIPTOR = _STRINTPAIR, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.StrIntPair) - )) -_sym_db.RegisterMessage(StrIntPair) - -StrStrPair = _reflection.GeneratedProtocolMessageType('StrStrPair', (_message.Message,), dict( - DESCRIPTOR = _STRSTRPAIR, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.StrStrPair) - )) -_sym_db.RegisterMessage(StrStrPair) - -TACController = _reflection.GeneratedProtocolMessageType('TACController', (_message.Message,), dict( - - Registered = _reflection.GeneratedProtocolMessageType('Registered', (_message.Message,), dict( - DESCRIPTOR = _TACCONTROLLER_REGISTERED, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Registered) - )) - , - - Unregistered = _reflection.GeneratedProtocolMessageType('Unregistered', (_message.Message,), dict( - DESCRIPTOR = _TACCONTROLLER_UNREGISTERED, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Unregistered) - )) - , - - Cancelled = _reflection.GeneratedProtocolMessageType('Cancelled', (_message.Message,), dict( - DESCRIPTOR = _TACCONTROLLER_CANCELLED, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Cancelled) - )) - , - - GameData = _reflection.GeneratedProtocolMessageType('GameData', (_message.Message,), dict( - DESCRIPTOR = _TACCONTROLLER_GAMEDATA, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.GameData) - )) - , - - TransactionConfirmation = _reflection.GeneratedProtocolMessageType('TransactionConfirmation', (_message.Message,), dict( - DESCRIPTOR = _TACCONTROLLER_TRANSACTIONCONFIRMATION, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.TransactionConfirmation) - )) - , - - StateUpdate = _reflection.GeneratedProtocolMessageType('StateUpdate', (_message.Message,), dict( - DESCRIPTOR = _TACCONTROLLER_STATEUPDATE, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.StateUpdate) - )) - , - - Error = _reflection.GeneratedProtocolMessageType('Error', (_message.Message,), dict( - DESCRIPTOR = _TACCONTROLLER_ERROR, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController.Error) - )) - , - DESCRIPTOR = _TACCONTROLLER, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACController) - )) -_sym_db.RegisterMessage(TACController) -_sym_db.RegisterMessage(TACController.Registered) -_sym_db.RegisterMessage(TACController.Unregistered) -_sym_db.RegisterMessage(TACController.Cancelled) -_sym_db.RegisterMessage(TACController.GameData) -_sym_db.RegisterMessage(TACController.TransactionConfirmation) -_sym_db.RegisterMessage(TACController.StateUpdate) -_sym_db.RegisterMessage(TACController.Error) - -TACAgent = _reflection.GeneratedProtocolMessageType('TACAgent', (_message.Message,), dict( - - Register = _reflection.GeneratedProtocolMessageType('Register', (_message.Message,), dict( - DESCRIPTOR = _TACAGENT_REGISTER, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.Register) - )) - , - - Unregister = _reflection.GeneratedProtocolMessageType('Unregister', (_message.Message,), dict( - DESCRIPTOR = _TACAGENT_UNREGISTER, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.Unregister) - )) - , - - Transaction = _reflection.GeneratedProtocolMessageType('Transaction', (_message.Message,), dict( - DESCRIPTOR = _TACAGENT_TRANSACTION, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.Transaction) - )) - , - - GetStateUpdate = _reflection.GeneratedProtocolMessageType('GetStateUpdate', (_message.Message,), dict( - DESCRIPTOR = _TACAGENT_GETSTATEUPDATE, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent.GetStateUpdate) - )) - , - DESCRIPTOR = _TACAGENT, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACAgent) - )) -_sym_db.RegisterMessage(TACAgent) -_sym_db.RegisterMessage(TACAgent.Register) -_sym_db.RegisterMessage(TACAgent.Unregister) -_sym_db.RegisterMessage(TACAgent.Transaction) -_sym_db.RegisterMessage(TACAgent.GetStateUpdate) - -TACMessage = _reflection.GeneratedProtocolMessageType('TACMessage', (_message.Message,), dict( - DESCRIPTOR = _TACMESSAGE, - __module__ = 'tac_pb2' - # @@protoc_insertion_point(class_scope:fetch.oef.pb.TACMessage) - )) -_sym_db.RegisterMessage(TACMessage) - - -# @@protoc_insertion_point(module_scope) diff --git a/my_agent/skills/__init__.py b/my_agent/skills/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/my_agent/skills/error/__init__.py b/my_agent/skills/error/__init__.py deleted file mode 100644 index 96c80ac32c..0000000000 --- a/my_agent/skills/error/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the implementation of the error skill.""" diff --git a/my_agent/skills/error/behaviours.py b/my_agent/skills/error/behaviours.py deleted file mode 100644 index 556ee98ca7..0000000000 --- a/my_agent/skills/error/behaviours.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains the error behaviours.""" - -from aea.skills.base import Behaviour - - -class ErrorBehaviour(Behaviour): - """This class implements the error behaviour.""" - - def setup(self) -> None: - """ - Implement the setup. - - :return: None - """ - pass - - def act(self) -> None: - """ - Implement the act. - - :return: None - """ - pass - - def teardown(self) -> None: - """ - Implement the task teardown. - - :return: None - """ - pass diff --git a/my_agent/skills/error/handlers.py b/my_agent/skills/error/handlers.py deleted file mode 100644 index 098a61eced..0000000000 --- a/my_agent/skills/error/handlers.py +++ /dev/null @@ -1,131 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains the implementation of the handler for the 'default' protocol.""" -import base64 -import logging -from typing import Optional - -from aea.configurations.base import ProtocolId -from aea.mail.base import Envelope -from aea.protocols.base import Message, Protocol -from aea.protocols.default.message import DefaultMessage -from aea.protocols.default.serialization import DefaultSerializer -from aea.skills.base import Handler - -logger = logging.getLogger(__name__) - - -class ErrorHandler(Handler): - """This class implements the error handler.""" - - SUPPORTED_PROTOCOL = 'default' # type: Optional[ProtocolId] - - def setup(self) -> None: - """ - Implement the setup. - - :return: None - """ - pass - - def handle(self, message: Message, sender: str) -> None: - """ - Implement the reaction to an envelope. - - :param message: the message - :param sender: the sender - """ - pass - - def teardown(self) -> None: - """ - Implement the handler teardown. - - :return: None - """ - pass - - def send_unsupported_protocol(self, envelope: Envelope) -> None: - """ - Handle the received envelope in case the protocol is not supported. - - :param envelope: the envelope - :return: None - """ - logger.warning("Unsupported protocol: {}".format(envelope.protocol_id)) - reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.UNSUPPORTED_PROTOCOL.value, - error_msg="Unsupported protocol.", - error_data={"protocol_id": envelope.protocol_id}) - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, - protocol_id=DefaultMessage.protocol_id, - message=DefaultSerializer().encode(reply)) - - def send_decoding_error(self, envelope: Envelope) -> None: - """ - Handle a decoding error. - - :param envelope: the envelope - :return: None - """ - logger.warning("Decoding error: {}.".format(envelope)) - encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") - reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.DECODING_ERROR.value, - error_msg="Decoding error.", - error_data={"envelope": encoded_envelope}) - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, - protocol_id=DefaultMessage.protocol_id, - message=DefaultSerializer().encode(reply)) - - def send_invalid_message(self, envelope: Envelope) -> None: - """ - Handle an message that is invalid wrt a protocol. - - :param envelope: the envelope - :return: None - """ - logger.warning("Invalid message wrt protocol: {}.".format(envelope.protocol_id)) - encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") - reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.INVALID_MESSAGE.value, - error_msg="Invalid message.", - error_data={"envelope": encoded_envelope}) - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, - protocol_id=DefaultMessage.protocol_id, - message=DefaultSerializer().encode(reply)) - - def send_unsupported_skill(self, envelope: Envelope, protocol: Protocol) -> None: - """ - Handle the received envelope in case the skill is not supported. - - :param envelope: the envelope - :param protocol: the protocol - :return: None - """ - logger.warning("Cannot handle envelope: no handler registered for the protocol '{}'.".format(protocol.id)) - encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") - reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.UNSUPPORTED_SKILL.value, - error_msg="Unsupported skill.", - error_data={"envelope": encoded_envelope}) - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, - protocol_id=DefaultMessage.protocol_id, - message=DefaultSerializer().encode(reply)) diff --git a/my_agent/skills/error/skill.yaml b/my_agent/skills/error/skill.yaml deleted file mode 100644 index e6f13e68b2..0000000000 --- a/my_agent/skills/error/skill.yaml +++ /dev/null @@ -1,15 +0,0 @@ -name: error -authors: Fetch.AI Limited -version: 0.1.0 -license: Apache 2.0 -url: "" -behaviours: [] -handlers: - - handler: - class_name: ErrorHandler - args: - foo: bar -tasks: [] -shared_classes: [] -protocols: ['default'] -description: "Error skill description [Fill in]" diff --git a/my_agent/skills/error/tasks.py b/my_agent/skills/error/tasks.py deleted file mode 100644 index 8922217537..0000000000 --- a/my_agent/skills/error/tasks.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains the implementation of the error tasks.""" - -from aea.skills.base import Task - - -class ErrorTask(Task): - """This class implements the error task.""" - - def setup(self) -> None: - """ - Implement the setup. - - :return: None - """ - pass - - def execute(self) -> None: - """ - Implement the task execution. - - :param envelope: the envelope - :return: None - """ - pass - - def teardown(self) -> None: - """ - Implement the task teardown. - - :return: None - """ - pass diff --git a/my_agent/skills/my_search/__init__.py b/my_agent/skills/my_search/__init__.py deleted file mode 100644 index 81d567366d..0000000000 --- a/my_agent/skills/my_search/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the implementation of the default skill.""" diff --git a/my_agent/skills/my_search/behaviours.py b/my_agent/skills/my_search/behaviours.py deleted file mode 100644 index 408ed69412..0000000000 --- a/my_agent/skills/my_search/behaviours.py +++ /dev/null @@ -1,82 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -import logging -import time - -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.models import Query, Constraint, ConstraintType -from aea.protocols.oef.serialization import DEFAULT_OEF, OEFSerializer -from aea.skills.base import Behaviour - -logger = logging.getLogger("aea.my_search_skill") - - -class MySearchBehaviour(Behaviour): - """This class provides a simple search behaviour.""" - - def __init__(self, **kwargs): - """Initialize the search behaviour.""" - super().__init__(**kwargs) - self.sent_search_count = 0 - - def setup(self) -> None: - """ - Implement the setup. - - :return: None - """ - logger.info("[{}]: setting up MySearchBehaviour".format(self.context.agent_name)) - - def act(self) -> None: - """ - Implement the act. - - :return: None - """ - time.sleep(1) # to slow down the agent - self.sent_search_count += 1 - - self.sent_search_count += 1 - search_constraints = [Constraint("country", ConstraintType("==", "UK"))] - - search_query_w_empty_model = Query(search_constraints, model=None) - - search_request = OEFMessage( - oef_type=OEFMessage.Type.SEARCH_SERVICES, - id=self.sent_search_count, - query=search_query_w_empty_model) - - logger.info("[{}]: sending search request to OEF, search_count={}".format( - self.context.agent_name, - self.sent_search_count)) - - self.context.outbox.put_message( - to=DEFAULT_OEF, - sender=self.context.agent_address, - protocol_id=OEFMessage.protocol_id, - message=OEFSerializer().encode(search_request)) - - def teardown(self) -> None: - """ - Implement the task teardown. - - :return: None - """ - logger.info("[{}]: tearing down MySearchBehaviour".format(self.context.agent_name)) \ No newline at end of file diff --git a/my_agent/skills/my_search/handlers.py b/my_agent/skills/my_search/handlers.py deleted file mode 100644 index 4bac768e8f..0000000000 --- a/my_agent/skills/my_search/handlers.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains a scaffold of a handler.""" -import logging - -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.serialization import OEFSerializer -from aea.skills.base import Handler - -logger = logging.getLogger("aea.my_search_skill") - - -class MySearchHandler(Handler): - """This class provides a simple search handler.""" - - SUPPORTED_PROTOCOL = OEFMessage.protocol_id - - def __init__(self, **kwargs): - """Initialize the handler.""" - super().__init__(**kwargs) - self.received_search_count = 0 - - def setup(self) -> None: - """Set up the handler.""" - logger.info("[{}]: setting up MySearchHandler".format(self.context.agent_name)) - - def handle(self, message: OEFMessage, sender: str) -> None: - """ - Handle the message. - - :param message: the message. - :param sender: the sender. - :return: None - """ - return - msg_type = OEFMessage.Type(message.get("type")) - - if msg_type is OEFMessage.Type.SEARCH_RESULT: - self.received_search_count += 1 - nb_agents_found = len(message.get("agents")) - logger.info("[{}]: found number of agents={}, received search count={}".format(self.context.agent_name, nb_agents_found, self.received_search_count)) - - def teardown(self) -> None: - """ - Teardown the handler. - - :return: None - """ - logger.info("[{}]: tearing down MySearchHandler".format(self.context.agent_name)) diff --git a/my_agent/skills/my_search/my_shared_class.py b/my_agent/skills/my_search/my_shared_class.py deleted file mode 100644 index bc9cacb2c7..0000000000 --- a/my_agent/skills/my_search/my_shared_class.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains a scaffold of a shared class.""" - -from aea.skills.base import SharedClass - - -class MySharedClass(SharedClass): - """This class scaffolds a shared class.""" diff --git a/my_agent/skills/my_search/skill.yaml b/my_agent/skills/my_search/skill.yaml deleted file mode 100644 index 8956027792..0000000000 --- a/my_agent/skills/my_search/skill.yaml +++ /dev/null @@ -1,21 +0,0 @@ -name: my_search -authors: Fetch.AI Limited -version: 0.1.0 -license: Apache 2.0 -url: "" -description: 'A simple search skill utilising the OEF.' -behaviours: - - behaviour: - class_name: MySearchBehaviour - args: {} -handlers: - - handler: - class_name: MySearchHandler - args: {} -tasks: - - task: - class_name: MySearchTask - args: {} -shared_classes: [] -protocols: ["oef"] -dependencies: [] \ No newline at end of file diff --git a/my_agent/skills/my_search/tasks.py b/my_agent/skills/my_search/tasks.py deleted file mode 100644 index f0707f085f..0000000000 --- a/my_agent/skills/my_search/tasks.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -import logging - -from aea.skills.base import Task - -logger = logging.getLogger("aea.my_search_skill") - - -class MySearchTask(Task): - """This class scaffolds a task.""" - - def setup(self) -> None: - """ - Implement the setup. - - :return: None - """ - logger.info("[{}]: setting up MySearchTask".format(self.context.agent_name)) - - def execute(self) -> None: - """ - Implement the task execution. - - :param envelope: the envelope - :return: None - """ - my_search_behaviour = self.context.behaviours[0] - my_search_handler = self.context.handlers[0] - logger.info("[{}]: number of search requests sent={} vs. number of search responses received={}".format( - self.context.agent_name, - my_search_behaviour.sent_search_count, - my_search_handler.received_search_count) - ) - - def teardown(self) -> None: - """ - Implement the task teardown. - - :return: None - """ - logger.info("[{}]: tearing down MySearchTask".format(self.context.agent_name)) \ No newline at end of file From 0cce0b58ba6e681635dedfd05c957d17aa43a9a4 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Thu, 17 Oct 2019 20:08:19 +0200 Subject: [PATCH 65/71] add test on 'aea run' in the case when a protocol directory is not complete. --- aea/cli/common.py | 11 ++++-- tests/test_cli/test_commands/test_run.py | 45 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/aea/cli/common.py b/aea/cli/common.py index 4e22119921..50dbaa5598 100644 --- a/aea/cli/common.py +++ b/aea/cli/common.py @@ -120,9 +120,14 @@ def _try_to_load_protocols(ctx: Context): logger.error("Protocol configuration file for protocol {} not found.".format(protocol_name)) exit(-1) - protocol_spec = importlib.util.spec_from_file_location(protocol_name, os.path.join("protocols", protocol_name, "__init__.py")) - protocol_module = importlib.util.module_from_spec(protocol_spec) - sys.modules[protocol_spec.name + "_protocol"] = protocol_module + try: + protocol_spec = importlib.util.spec_from_file_location(protocol_name, os.path.join("protocols", protocol_name, "__init__.py")) + protocol_module = importlib.util.module_from_spec(protocol_spec) + protocol_spec.loader.exec_module(protocol_module) + sys.modules[protocol_spec.name + "_protocol"] = protocol_module + except Exception: + logger.error("A problem occurred while processing protocol {}.".format(protocol_name)) + exit(-1) def _load_env_file(env_file: str): diff --git a/tests/test_cli/test_commands/test_run.py b/tests/test_cli/test_commands/test_run.py index fcc1f5d15c..44620b72bf 100644 --- a/tests/test_cli/test_commands/test_run.py +++ b/tests/test_cli/test_commands/test_run.py @@ -531,3 +531,48 @@ def teardown_class(cls): shutil.rmtree(cls.t) except (OSError, IOError): pass + + +class TestRunFailsWhenProtocolNotComplete: + """Test that the command 'aea run' fails when a protocol directory is not complete.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.connection_name = "local" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(Path(cls.t, cls.agent_name)) + + Path(cls.t, cls.agent_name, "protocols", "default", "__init__.py").unlink() + + try: + cli.main([*CLI_LOG_OPTION, "run", "--connection", cls.connection_name]) + except SystemExit as e: + cls.exit_code = e.code + + def test_exit_code_equal_to_minus_one(self): + """Assert that the exit code is equal to -1 (i.e. failure).""" + assert self.exit_code == -1 + + def test_log_error_message(self): + """Test that the log error message is fixed.""" + s = "A problem occurred while processing protocol {}.".format("default") + self.mocked_logger_error.assert_called_once_with(s) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass From 27d5f34eb6d8f525f79b7e6d6d15543e9efe85b6 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Thu, 17 Oct 2019 20:10:05 +0200 Subject: [PATCH 66/71] use proper variable names in cli/list.py --- aea/cli/list.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/aea/cli/list.py b/aea/cli/list.py index 42175db694..5c749edf62 100644 --- a/aea/cli/list.py +++ b/aea/cli/list.py @@ -35,21 +35,21 @@ def list(ctx: Context): @pass_ctx def connections(ctx: Context): """List all the installed connections.""" - for c in sorted(ctx.agent_config.connections): - print(c) + for connection_id in sorted(ctx.agent_config.connections): + print(connection_id) @list.command() @pass_ctx def protocols(ctx: Context): """List all the installed protocols.""" - for c in sorted(ctx.agent_config.protocols): - print(c) + for protocol_id in sorted(ctx.agent_config.protocols): + print(protocol_id) @list.command() @pass_ctx def skills(ctx: Context): """List all the installed skills.""" - for c in sorted(ctx.agent_config.skills): - print(c) + for skill_id in sorted(ctx.agent_config.skills): + print(skill_id) From 1d01e9d8767651b13953917515b7188138397968 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Thu, 17 Oct 2019 20:12:38 +0200 Subject: [PATCH 67/71] print meaningful error messages for 'aea scaffold' when resource name already exists. --- aea/cli/scaffold.py | 6 +++--- .../test_cli/test_commands/test_scaffold/test_connection.py | 2 +- .../test_cli/test_commands/test_scaffold/test_protocols.py | 2 +- tests/test_cli/test_commands/test_scaffold/test_skills.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/aea/cli/scaffold.py b/aea/cli/scaffold.py index a1e065dae8..eb484ba507 100644 --- a/aea/cli/scaffold.py +++ b/aea/cli/scaffold.py @@ -68,7 +68,7 @@ def connection(ctx: Context, connection_name: str) -> None: ctx.agent_loader.dump(ctx.agent_config, open(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w")) except FileExistsError: - logger.error("Directory already exist. Aborting...") + logger.error("A connection with this name already exists. Please choose a different name and try again.") exit(-1) except ValidationError: logger.error("Error when validating the skill configuration file.") @@ -110,7 +110,7 @@ def protocol(ctx: Context, protocol_name: str): ctx.agent_loader.dump(ctx.agent_config, open(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w")) except FileExistsError: - logger.error("Directory already exist. Aborting...") + logger.error("A protocol with this name already exists. Please choose a different name and try again.") exit(-1) except ValidationError: logger.error("Error when validating the skill configuration file.") @@ -152,7 +152,7 @@ def skill(ctx: Context, skill_name: str): ctx.agent_loader.dump(ctx.agent_config, open(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w")) except FileExistsError: - logger.error("Directory already exist. Aborting...") + logger.error("A skill with this name already exists. Please choose a different name and try again.") exit(-1) except ValidationError: logger.error("Error when validating the skill configuration file.") diff --git a/tests/test_cli/test_commands/test_scaffold/test_connection.py b/tests/test_cli/test_commands/test_scaffold/test_connection.py index 59ba7f67b1..22803feaa7 100644 --- a/tests/test_cli/test_commands/test_scaffold/test_connection.py +++ b/tests/test_cli/test_commands/test_scaffold/test_connection.py @@ -121,7 +121,7 @@ def test_error_message_connection_already_existing(self): The expected message is: 'A connection with name '{connection_name}' already exists. Aborting...' """ - s = "Directory already exist. Aborting..." + s = "A connection with this name already exists. Please choose a different name and try again." self.mocked_logger_error.assert_called_once_with(s) def test_resource_directory_exists(self): diff --git a/tests/test_cli/test_commands/test_scaffold/test_protocols.py b/tests/test_cli/test_commands/test_scaffold/test_protocols.py index af7d79e0d7..d8d8dc0c5a 100644 --- a/tests/test_cli/test_commands/test_scaffold/test_protocols.py +++ b/tests/test_cli/test_commands/test_scaffold/test_protocols.py @@ -127,7 +127,7 @@ def test_error_message_protocol_already_existing(self): The expected message is: 'A protocol with name '{protocol_name}' already exists. Aborting...' """ - s = "Directory already exist. Aborting..." + s = "A protocol with this name already exists. Please choose a different name and try again." self.mocked_logger_error.assert_called_once_with(s) def test_resource_directory_exists(self): diff --git a/tests/test_cli/test_commands/test_scaffold/test_skills.py b/tests/test_cli/test_commands/test_scaffold/test_skills.py index 50f9eafd1e..ff1d32f5a2 100644 --- a/tests/test_cli/test_commands/test_scaffold/test_skills.py +++ b/tests/test_cli/test_commands/test_scaffold/test_skills.py @@ -139,7 +139,7 @@ def test_error_message_skill_already_existing(self): The expected message is: 'A skill with name '{skill_name}' already exists. Aborting...' """ - s = "Directory already exist. Aborting..." + s = "A skill with this name already exists. Please choose a different name and try again." self.mocked_logger_error.assert_called_once_with(s) def test_resource_directory_exists(self): From 45c48929c6625fcac6676b3cbdac0354d66bbd90 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Thu, 17 Oct 2019 22:01:03 +0200 Subject: [PATCH 68/71] remove liveness from AgentContext --- aea/aea.py | 3 +- aea/cli/common.py | 2 +- aea/context/base.py | 11 +- aea/skills/base.py | 9 +- tests/data/stopping_skill/__init__.py | 20 -- tests/data/stopping_skill/behaviours.py | 20 -- tests/data/stopping_skill/handlers.py | 20 -- tests/data/stopping_skill/skill.yaml | 17 -- tests/data/stopping_skill/tasks.py | 51 ----- tests/test_cli/test_commands/test_run.py | 265 ++++++++++++----------- 10 files changed, 141 insertions(+), 277 deletions(-) delete mode 100644 tests/data/stopping_skill/__init__.py delete mode 100644 tests/data/stopping_skill/behaviours.py delete mode 100644 tests/data/stopping_skill/handlers.py delete mode 100644 tests/data/stopping_skill/skill.yaml delete mode 100644 tests/data/stopping_skill/tasks.py diff --git a/aea/aea.py b/aea/aea.py index 77bf248d84..2a49d5ec0f 100644 --- a/aea/aea.py +++ b/aea/aea.py @@ -71,8 +71,7 @@ def __init__(self, name: str, self.decision_maker.message_queue, self.decision_maker.ownership_state, self.decision_maker.preferences, - self.decision_maker.is_ready_to_pursuit_goals, - self.liveness) + self.decision_maker.is_ready_to_pursuit_goals) self._resources = None # type: Optional[Resources] @property diff --git a/aea/cli/common.py b/aea/cli/common.py index 50dbaa5598..65d43835f5 100644 --- a/aea/cli/common.py +++ b/aea/cli/common.py @@ -123,7 +123,7 @@ def _try_to_load_protocols(ctx: Context): try: protocol_spec = importlib.util.spec_from_file_location(protocol_name, os.path.join("protocols", protocol_name, "__init__.py")) protocol_module = importlib.util.module_from_spec(protocol_spec) - protocol_spec.loader.exec_module(protocol_module) + protocol_spec.loader.exec_module(protocol_module) # type: ignore sys.modules[protocol_spec.name + "_protocol"] = protocol_module except Exception: logger.error("A problem occurred while processing protocol {}.".format(protocol_name)) diff --git a/aea/context/base.py b/aea/context/base.py index 51cf83542a..7ea75091d0 100644 --- a/aea/context/base.py +++ b/aea/context/base.py @@ -22,7 +22,6 @@ from queue import Queue from typing import Dict -from aea.agent import Liveness from aea.decision_maker.base import OwnershipState, Preferences from aea.mail.base import OutBox @@ -36,8 +35,7 @@ def __init__(self, agent_name: str, decision_maker_message_queue: Queue, ownership_state: OwnershipState, preferences: Preferences, - is_ready_to_pursuit_goals: bool, - liveness: Liveness): + is_ready_to_pursuit_goals: bool): """ Initialize an agent context. @@ -49,7 +47,6 @@ def __init__(self, agent_name: str, :param ownership_state: the ownership state of the agent :param preferences: the preferences of the agent :param is_ready_to_pursuit_goals: whether the agent is ready to pursuit its goals - :param liveness: the liveness object. """ self._agent_name = agent_name self._public_keys = public_keys @@ -58,7 +55,6 @@ def __init__(self, agent_name: str, self._ownership_state = ownership_state self._preferences = preferences self._is_ready_to_pursuit_goals = is_ready_to_pursuit_goals - self._liveness = liveness @property def agent_name(self) -> str: @@ -99,8 +95,3 @@ def preferences(self) -> Preferences: def is_ready_to_pursuit_goals(self) -> bool: """Get the goal pursuit readiness.""" return self._is_ready_to_pursuit_goals - - @property - def liveness(self) -> Liveness: - """Get the liveness object.""" - return self._liveness diff --git a/aea/skills/base.py b/aea/skills/base.py index f1d2e1b0e1..2703220785 100644 --- a/aea/skills/base.py +++ b/aea/skills/base.py @@ -28,8 +28,8 @@ from queue import Queue from typing import Optional, List, Dict, Any, cast -from aea.agent import Liveness -from aea.configurations.base import BehaviourConfig, HandlerConfig, TaskConfig, SharedClassConfig, SkillConfig, ProtocolId, DEFAULT_SKILL_CONFIG_FILE +from aea.configurations.base import BehaviourConfig, HandlerConfig, TaskConfig, SharedClassConfig, SkillConfig, \ + ProtocolId, DEFAULT_SKILL_CONFIG_FILE from aea.configurations.loader import ConfigLoader from aea.context.base import AgentContext from aea.decision_maker.base import OwnershipState, Preferences @@ -91,11 +91,6 @@ def agent_is_ready_to_pursuit_goals(self) -> bool: """Get the goal pursuit readiness.""" return self._agent_context.is_ready_to_pursuit_goals - @property - def liveness(self) -> Liveness: - """Get the liveness object.""" - return self._agent_context.liveness - @property def handlers(self) -> Optional[List['Handler']]: """Get handlers of the skill.""" diff --git a/tests/data/stopping_skill/__init__.py b/tests/data/stopping_skill/__init__.py deleted file mode 100644 index 47f4b85aa7..0000000000 --- a/tests/data/stopping_skill/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains a dummy 'stopping' skill for an AEA.""" diff --git a/tests/data/stopping_skill/behaviours.py b/tests/data/stopping_skill/behaviours.py deleted file mode 100644 index b7f424a017..0000000000 --- a/tests/data/stopping_skill/behaviours.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the behaviours for the 'stop' skill.""" diff --git a/tests/data/stopping_skill/handlers.py b/tests/data/stopping_skill/handlers.py deleted file mode 100644 index 4b1dbadb2e..0000000000 --- a/tests/data/stopping_skill/handlers.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the handler for the 'stop' skill.""" diff --git a/tests/data/stopping_skill/skill.yaml b/tests/data/stopping_skill/skill.yaml deleted file mode 100644 index e605d2468b..0000000000 --- a/tests/data/stopping_skill/skill.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: stopping -authors: Fetch.AI Limited -version: 0.1.0 -license: Apache 2.0 -url: "" -behaviours: [] -handlers: [] -tasks: - - task: - class_name: StopTask - args: - enabled: true - timeout: 3 -shared_classes: [] -protocols: [] -dependencies: [] -description: "Stop the agent as soon as possible." \ No newline at end of file diff --git a/tests/data/stopping_skill/tasks.py b/tests/data/stopping_skill/tasks.py deleted file mode 100644 index b823c0e044..0000000000 --- a/tests/data/stopping_skill/tasks.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the tasks for the 'stop' skill.""" -import datetime - -from aea.skills.base import Task - - -class StopTask(Task): - """Dummy task.""" - - def __init__(self, **kwargs): - """Initialize the task.""" - super().__init__(**kwargs) - self.kwargs = kwargs - self.timeout = kwargs.get("timeout", 3.0) - self.enabled = kwargs.get("enabled", False) - - def setup(self) -> None: - """ - Implement the setup. - - :return: None - """ - self.start_time = datetime.datetime.now() - self.end_time = self.start_time + datetime.timedelta(0, self.timeout) - - def execute(self) -> None: - """Execute the task.""" - if self.enabled and datetime.datetime.now() > self.end_time: - self.context.liveness._is_stopped = True - - def teardown(self) -> None: - """Teardown the task.""" diff --git a/tests/test_cli/test_commands/test_run.py b/tests/test_cli/test_commands/test_run.py index 44620b72bf..0c3f413999 100644 --- a/tests/test_cli/test_commands/test_run.py +++ b/tests/test_cli/test_commands/test_run.py @@ -20,10 +20,15 @@ """This test module contains the tests for the `aea run` sub-command.""" import os import shutil +import signal +import subprocess +import sys import tempfile +import time import unittest.mock from pathlib import Path +import pytest import yaml from click.testing import CliRunner @@ -33,148 +38,150 @@ from ...conftest import CLI_LOG_OPTION, CUR_PATH -class TestRun: +def test_run(pytestconfig): """Test that the command 'aea run' works as expected.""" + if pytestconfig.getoption("ci"): + pytest.skip("Skipping the test since it doesn't work in CI.") - @classmethod - def setup_class(cls): - """Set the test up.""" - cls.runner = CliRunner() - cls.agent_name = "myagent" - cls.cwd = os.getcwd() - cls.t = tempfile.mkdtemp() - os.chdir(cls.t) - result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) - assert result.exit_code == 0 - - os.chdir(Path(cls.t, cls.agent_name)) - - result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "local"]) - assert result.exit_code == 0 - - shutil.copytree(Path(CUR_PATH, "data", "stopping_skill"), Path(cls.t, cls.agent_name, "skills", "stopping")) - config_path = Path(cls.t, cls.agent_name, DEFAULT_AEA_CONFIG_FILE) - config = yaml.safe_load(open(config_path)) - config.setdefault("skills", []).append("stopping") - yaml.safe_dump(config, open(config_path, "w")) - - try: - cli.main([*CLI_LOG_OPTION, "run", "--connection", "local"]) - except SystemExit as e: - cls.exit_code = e.code - - def test_exit_code_equal_to_zero(self): - """Assert that the exit code is equal to zero (i.e. success).""" - assert self.exit_code == 0 - - @classmethod - def teardown_class(cls): - """Teardowm the test.""" - os.chdir(cls.cwd) - try: - shutil.rmtree(cls.t) - except (OSError, IOError): - pass + runner = CliRunner() + agent_name = "myagent" + cwd = os.getcwd() + t = tempfile.mkdtemp() + os.chdir(t) + result = runner.invoke(cli, [*CLI_LOG_OPTION, "create", agent_name]) + assert result.exit_code == 0 + os.chdir(Path(t, agent_name)) -class TestRunWithInstallDeps: - """Test that the command 'aea run --install-deps' does not crash.""" - - @classmethod - def setup_class(cls): - """Set the test up.""" - cls.runner = CliRunner() - cls.agent_name = "myagent" - cls.connection_name = "stub" - cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') - cls.mocked_logger_error = cls.patch.__enter__() - cls.cwd = os.getcwd() - cls.t = tempfile.mkdtemp() - os.chdir(cls.t) - result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) - assert result.exit_code == 0 + result = runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "local"]) + assert result.exit_code == 0 - os.chdir(Path(cls.t, cls.agent_name)) + process = subprocess.Popen([ + sys.executable, + '-m', + 'aea.cli', + "run", + "--connection", + "local" + ], + stdout=subprocess.PIPE, + env=os.environ.copy()) - result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "local"]) - assert result.exit_code == 0 + time.sleep(10.0) + process.send_signal(signal.SIGINT) + process.wait(timeout=20) - shutil.copytree(Path(CUR_PATH, "data", "stopping_skill"), Path(cls.t, cls.agent_name, "skills", "stopping")) - config_path = Path(cls.t, cls.agent_name, DEFAULT_AEA_CONFIG_FILE) - config = yaml.safe_load(open(config_path)) - config.setdefault("skills", []).append("stopping") - yaml.safe_dump(config, open(config_path, "w")) + assert process.returncode == 0 - try: - cli.main([*CLI_LOG_OPTION, "run", "--install-deps", "--connection", "local"]) - except SystemExit as e: - cls.exit_code = e.code + os.chdir(cwd) - def test_exit_code_equal_to_zero(self): - """Assert that the exit code is equal to zero (i.e. success).""" - assert self.exit_code == 0 + poll = process.poll() + if poll is None: + process.terminate() + process.wait(2) - @classmethod - def teardown_class(cls): - """Teardowm the test.""" - cls.patch.__exit__() - os.chdir(cls.cwd) - try: - shutil.rmtree(cls.t) - except (OSError, IOError): - pass + try: + shutil.rmtree(t) + except (OSError, IOError): + pass -class TestRunWithInstallDepsAndRequirementFile: +def test_run_with_install_deps(pytestconfig): + """Test that the command 'aea run --install-deps' does not crash.""" + if pytestconfig.getoption("ci"): + pytest.skip("Skipping the test since it doesn't work in CI.") + runner = CliRunner() + agent_name = "myagent" + cwd = os.getcwd() + t = tempfile.mkdtemp() + os.chdir(t) + result = runner.invoke(cli, [*CLI_LOG_OPTION, "create", agent_name]) + assert result.exit_code == 0 + + os.chdir(Path(t, agent_name)) + + result = runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "local"]) + assert result.exit_code == 0 + + process = subprocess.Popen([ + sys.executable, + '-m', + 'aea.cli', + "run", + "--install-deps", + "--connection", + "local" + ], + stdout=subprocess.PIPE, + env=os.environ.copy()) + + time.sleep(10.0) + process.send_signal(signal.SIGINT) + process.communicate(timeout=20) + + assert process.returncode == 0 + + poll = process.poll() + if poll is None: + process.terminate() + process.wait(2) + + os.chdir(cwd) + try: + shutil.rmtree(t) + except (OSError, IOError): + pass + + +def test_run_with_install_deps_and_requirement_file(pytestconfig): """Test that the command 'aea run --install-deps' with requirement file does not crash.""" - - @classmethod - def setup_class(cls): - """Set the test up.""" - cls.runner = CliRunner() - cls.agent_name = "myagent" - cls.connection_name = "stub" - cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') - cls.mocked_logger_error = cls.patch.__enter__() - cls.cwd = os.getcwd() - cls.t = tempfile.mkdtemp() - os.chdir(cls.t) - result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name]) - assert result.exit_code == 0 - - os.chdir(Path(cls.t, cls.agent_name)) - - result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "local"]) - assert result.exit_code == 0 - - result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "freeze"]) - assert result.exit_code == 0 - Path(cls.t, cls.agent_name, "requirements.txt").write_text(result.output) - - shutil.copytree(Path(CUR_PATH, "data", "stopping_skill"), Path(cls.t, cls.agent_name, "skills", "stopping")) - config_path = Path(cls.t, cls.agent_name, DEFAULT_AEA_CONFIG_FILE) - config = yaml.safe_load(open(config_path)) - config.setdefault("skills", []).append("stopping") - yaml.safe_dump(config, open(config_path, "w")) - - try: - cli.main([*CLI_LOG_OPTION, "run", "--install-deps", "--connection", "local"]) - except SystemExit as e: - cls.exit_code = e.code - - def test_exit_code_equal_to_zero(self): - """Assert that the exit code is equal to zero (i.e. success).""" - assert self.exit_code == 0 - - @classmethod - def teardown_class(cls): - """Teardowm the test.""" - cls.patch.__exit__() - os.chdir(cls.cwd) - try: - shutil.rmtree(cls.t) - except (OSError, IOError): - pass + if pytestconfig.getoption("ci"): + pytest.skip("Skipping the test since it doesn't work in CI.") + runner = CliRunner() + agent_name = "myagent" + cwd = os.getcwd() + t = tempfile.mkdtemp() + os.chdir(t) + result = runner.invoke(cli, [*CLI_LOG_OPTION, "create", agent_name]) + assert result.exit_code == 0 + + os.chdir(Path(t, agent_name)) + + result = runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "local"]) + assert result.exit_code == 0 + + result = runner.invoke(cli, [*CLI_LOG_OPTION, "freeze"]) + assert result.exit_code == 0 + Path(t, agent_name, "requirements.txt").write_text(result.output) + + process = subprocess.Popen([ + sys.executable, + '-m', + 'aea.cli', + "run", + "--install-deps", + "--connection", + "local" + ], + stdout=subprocess.PIPE, + env=os.environ.copy()) + + time.sleep(10.0) + process.send_signal(signal.SIGINT) + process.wait(timeout=20) + + assert process.returncode == 0 + + poll = process.poll() + if poll is None: + process.terminate() + process.wait(10) + + os.chdir(cwd) + try: + shutil.rmtree(t) + except (OSError, IOError): + pass class TestRunFailsWhenExceptionOccursInSkill: From b944b5a08d5f3e7192cfa9e217dcd186c3595a8a Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Fri, 18 Oct 2019 08:13:12 +0100 Subject: [PATCH 69/71] Fix some failing tests --- tests/test_crypto/test_ethereum_base.py | 3 ++- tests/test_crypto/test_fetchai_base.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_crypto/test_ethereum_base.py b/tests/test_crypto/test_ethereum_base.py index fe094aeddf..a6dbec9a5a 100644 --- a/tests/test_crypto/test_ethereum_base.py +++ b/tests/test_crypto/test_ethereum_base.py @@ -18,11 +18,12 @@ # ------------------------------------------------------------------------------ """This module contains the tests of the ethereum module.""" +import os from aea.crypto.ethereum_base import EthCrypto from ..conftest import ROOT_DIR -PRIVATE_KEY_PATH = ROOT_DIR + "/tests/data/eth_private_key.txt" +PRIVATE_KEY_PATH = os.path.join(ROOT_DIR, "/tests/data/eth_private_key.txt") def test_creation(): diff --git a/tests/test_crypto/test_fetchai_base.py b/tests/test_crypto/test_fetchai_base.py index ccba2b5b5b..0a1a913abe 100644 --- a/tests/test_crypto/test_fetchai_base.py +++ b/tests/test_crypto/test_fetchai_base.py @@ -19,11 +19,12 @@ # ------------------------------------------------------------------------------ """This module contains the tests of the ethereum module.""" +import os from aea.crypto.fetchai_base import FetchCrypto from ..conftest import ROOT_DIR -PRIVATE_KEY_PATH = ROOT_DIR + "/tests/data/fet_private_key.txt" +PRIVATE_KEY_PATH = os.path.join(ROOT_DIR, "/tests/data/fet_private_key.txt") def test_initialisation(): From f387e4fa840fabef4beb9707b9709722e6eb3e72 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Fri, 18 Oct 2019 08:34:07 +0100 Subject: [PATCH 70/71] Prepares develop for release v0.1.8 --- HISTORY.rst | 8 ++++++++ aea/__version__.py | 2 +- deploy-image/docker-env.sh | 2 +- develop-image/docker-env.sh | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 1c8c6b8a22..bf8c31ef21 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -61,3 +61,11 @@ Release History - Adds ledger entities (fetchai and ethereum); creates wallet for ledger entities - Adds more documentation and fixes old one - Multiple additional minor fixes and changes + +0.1.8 (2019-10-18) +------------------- + +- Multiple bug fixes and improvements to gui of cli +- Adds full test coverage on cli +- Improves docs +- Multiple additional minor fixes and changes diff --git a/aea/__version__.py b/aea/__version__.py index 2a3e46ef56..6d4c90140f 100644 --- a/aea/__version__.py +++ b/aea/__version__.py @@ -23,7 +23,7 @@ __title__ = 'aea' __description__ = 'Autonomous Economic Agent framework' __url__ = 'https://github.com/fetchai/agents-aea.git' -__version__ = '0.1.7' +__version__ = '0.1.8' __author__ = 'Fetch.AI Limited' __license__ = 'Apache 2.0' __copyright__ = '2019 Fetch.AI Limited' diff --git a/deploy-image/docker-env.sh b/deploy-image/docker-env.sh index e121a3aa4f..5050b3df09 100755 --- a/deploy-image/docker-env.sh +++ b/deploy-image/docker-env.sh @@ -1,5 +1,5 @@ #!/bin/bash -DOCKER_IMAGE_TAG=aea-deploy:0.1.7 +DOCKER_IMAGE_TAG=aea-deploy:0.1.8 DOCKER_BUILD_CONTEXT_DIR=.. DOCKERFILE=./Dockerfile diff --git a/develop-image/docker-env.sh b/develop-image/docker-env.sh index 0a7e215e0f..d60a4e7601 100755 --- a/develop-image/docker-env.sh +++ b/develop-image/docker-env.sh @@ -1,5 +1,5 @@ #!/bin/bash -DOCKER_IMAGE_TAG=aea-develop:0.1.7 +DOCKER_IMAGE_TAG=aea-develop:0.1.8 DOCKER_BUILD_CONTEXT_DIR=.. DOCKERFILE=./Dockerfile From 245134a45baa1b69ced7583a93dccc7b61db0d93 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Fri, 18 Oct 2019 09:01:21 +0100 Subject: [PATCH 71/71] Update main readme and remove outdated readme in subdir --- README.md | 25 ++++++++++++--------- aea/cli/README.md | 56 ----------------------------------------------- 2 files changed, 15 insertions(+), 66 deletions(-) delete mode 100644 aea/cli/README.md diff --git a/README.md b/README.md index 2d7b771318..a1b56153b1 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,21 @@ A framework for autonomous economic agent (AEA) development ## Get started -First, install the package from [pypi](https://pypi.org/project/aea/): +- Create and launch a virtual environment with Python 3.7: + + pipenv --python 3.7 && pipenv shell + +- Install the package from [pypi](https://pypi.org/project/aea/): -` -pip install aea -` -Then, build your agent as described in the [AEA CLI readme](../master/aea/cli/README.md) or in the [examples](../master/examples). + pip install aea[all] -## Install from Source -## Cloning +- Then, build your agent as described in the [docs](https://fetchai.github.io/agents-aea/). + +## Alternatively: Install from Source + +### Cloning This repository contains submodules. Clone with recursive strategy: @@ -39,7 +43,7 @@ Or, you can have more control on the installed dependencies by leveraging the se pip install .[cli] -## Contribute +### Contribute The following dependency is only relevant if you intend to contribute to the repository: - the project uses [Google Protocol Buffers](https://developers.google.com/protocol-buffers/) compiler for message serialization. A guide on how to install it is found [here](https://fetchai.github.io/oef-sdk-python/user/install.html#protobuf-compiler). @@ -76,5 +80,6 @@ The following steps are only relevant if you intend to contribute to the reposit - Docs: - * `mkdocs serve` - Start the live-reloading docs server. - * `mkdocs build --clean` - Build the documentation site. + * `mkdocs serve` - Start the live-reloading docs server on localhost. + +To amend the docs, create a new documentation file in `docs/` and add a reference to it in `mkdocs.yml`. diff --git a/aea/cli/README.md b/aea/cli/README.md deleted file mode 100644 index 3e854d1432..0000000000 --- a/aea/cli/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# `aea` command-line tool - -The `aea` command-line tool is an extra feature of the `aea` package, that provides a useful tool to manage AEA agents. - -## Installation - -To use `aea`, install by including the `[cli]` extra dependencies when installing the package: -``` -pip install aea[cli] -``` - -## Quick start - -This quick start explains how to create and launch an agent with the cli. - -- in any directory, open a terminal and execute: - - aea create my_first_agent - - a directory named `my_first_agent` will be created. - -- enter into the agent's directory: - - cd my_first_agent - -- add a protocol to the agent, e.g.: - - aea add protocol oef - - This command will create the `my_first_agent/protocols` folder, with the `oef` protocol package inside. - You can find the supported protocols in `aea/protocols`. - -- add a skill to the agent, e.g.: - - aea add skill echo_skill ../examples/echo_skill - - This command will create the `my_first_agent/skills` folder, with the `echo_skill` skill package inside. - -- start an oef from a separate terminal: - - python scripts/oef/launch.py -c ./scripts/oef/launch_config.json - -- Run the agent. Assuming an OEF node is running at `127.0.0.1:10000` - - aea run - -- For debugging run with: - - aea -v DEBUG run - -Press CTRL+C to stop the execution. - -- Delete the agent: - - cd .. - aea delete my_first_agent