diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 925ce42f97..d24e452db8 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -53,6 +53,8 @@ jobs: run: | tox -e black-check tox -e flake8 + - name: Unused code check + run: tox -e vulture - name: Static type check run: tox -e mypy - name: Golang code style check @@ -97,7 +99,7 @@ jobs: pip install tox # install IPFS sudo apt-get install -y wget - wget -O ./go-ipfs.tar.gz https://dist.ipfs.io/go-ipfs/v0.4.23/go-ipfs_v0.4.23_linux-amd64.tar.gz + wget -O ./go-ipfs.tar.gz https://dist.ipfs.io/go-ipfs/v0.6.0/go-ipfs_v0.6.0_linux-amd64.tar.gz tar xvfz go-ipfs.tar.gz sudo mv go-ipfs/ipfs /usr/local/bin/ipfs ipfs init @@ -140,7 +142,7 @@ jobs: sudo apt-get install -y protobuf-compiler - name: Sync AEA loop integration tests run: | - tox -e py3.8 -- --aea-loop sync -m 'sync' + tox -e py3.8 -- -m 'sync' # --aea-loop sync - name: Async integration tests run: tox -e py3.8 -- -m 'integration and not unstable and not ledger' @@ -241,7 +243,7 @@ jobs: sudo apt-get autoclean pip install tox sudo apt-get install -y protobuf-compiler - - name: Unit tests + - name: Unit tests with sync agent loop run: | tox -e py3.8 -- --aea-loop sync -m 'not integration and not unstable' @@ -277,7 +279,7 @@ jobs: - integration_checks - integration_checks_ledger - platform_checks - - platform_checks_sync_aea_loop +# - platform_checks_sync_aea_loop runs-on: ubuntu-latest timeout-minutes: 60 steps: diff --git a/.pylintrc b/.pylintrc index 3465e8ea7d..f289c246a0 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,68 +1,34 @@ [MASTER] -ignore-patterns=serialization.py,message.py,__main__.py,.*_pb2.py,launch.py,transaction.py +ignore-patterns=serialization.py,message.py,__main__.py,.*_pb2.py [MESSAGES CONTROL] -disable=C0103,C0201,C0330,C0301,C0302,W1202,W1203,W0511,W0107,W0105,W0621,W0235,W0613,W0221,R0902,R0913,R0914,R1720,R1705,R0801,R0904,R0903,R0911,R0912,R0901,R1704,R0916,R1702,R0915,R1710,R1703,R0401 +disable=C0103,C0201,C0301,C0302,C0330,W0105,W0107,W1202,W1203,R0902,R0913,R0914,R0801,R0904,R0903,R0911,R0912,R0901,R0916,R1702,R0915 -ENABLED: -# W0703: broad-except -# W0212: protected-access -# W0706: try-except-raise -# W0108: unnecessary-lambda -# W0622: redefined-builtin -# W0163: unused-argument -# W0201: attribute-defined-outside-init -# W0222: signature-differs -# W0223: abstract-method -# W0611: unused-import -# W0612: unused-variable -# W1505: deprecated-method -# W0106: expression-not-assigned -# R0201: no-self-use -# R0205: useless-object-inheritance -# R1723: no-else-break -# R1721: unnecessary-comprehension -# R1718: consider-using-set-comprehension -# R1716: chained-comparison -# R1714: consider-using-in -# R0123: literal-comparison -# R1711: useless-return -# R1722: consider-using-sys-exit - -## Resolve these: -# R0401: cyclic-import -# W0221: arguments-differ +## Eventually resolve these: # R0902: too-many-instance-attributes # R0913: too-many-arguments # R0914: too-many-locals -# R1720: no-else-raise -# R1705: no-else-return # R0904: too-many-public-methods # R0903: too-few-public-methods # R0911: too-many-return-statements # R0912: too-many-branches # R0901: too-many-ancestors -# R1704: redefined-argument-from-local # R0916: too-many-boolean-expressions # R1702: too-many-nested-blocks # R0915: too-many-statements -# R1710: inconsistent-return-statements -# R1703: simplifiable-if-statement +# decide on a logging policy: +# W1202: logging-format-interpolation +# W1203: logging-fstring-interpolation ## Keep the following: # C0103: invalid-name # C0201: consider-iterating-dictionary -# C0330: Wrong haning indentation -# http://pylint-messages.wikidot.com/messages:c0301 > Line too long (%s/%s) -# http://pylint-messages.wikidot.com/messages:c0302 > Too many lines in module (%s) -# W1202: logging-format-interpolation -# W1203: logging-fstring-interpolation -# W0511: fixme -# W0107: unnecessary-pass -# W0105: pointless-string-statement -# W0621: redefined-outer-name -# W0235: useless-super-delegation -# R0801: similar lines +# C0301: http://pylint-messages.wikidot.com/messages:c0301 > Line too long (%s/%s) +# C0302: http://pylint-messages.wikidot.com/messages:c0302 > Too many lines in module (%s) +# C0330: Wrong hanging indentation +# W0105: pointless-string-statement, # kept as no harm +# W0107: unnecessary-pass, # kept as no harm +# R0801: similar lines, # too granular [IMPORTS] ignored-modules=aiohttp,defusedxml,gym,fetch,matplotlib,memory_profiler,numpy,oef,openapi_core,psutil,tensorflow,temper,skimage,vyper,web3 diff --git a/AUTHORS.md b/AUTHORS.md index 7f0cd1e391..e6ed8ddf47 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -12,3 +12,4 @@ This is the official list of Fetch.AI authors for copyright purposes. * Yuri Turchenkov [solarw](https://github.com/solarw) * Lokman Rahmani [lrahmani](https://github.com/lrahmani) * Jiří Vestfál [MissingNO57](https://github.com/MissingNO57) +* Ed Fitzgerald [ejfitzgerald](https://github.com/ejfitzgerald) diff --git a/HISTORY.md b/HISTORY.md index f1faeaac4d..56c46c7045 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,32 @@ # Release History +## 0.6.0 (2020-09-01) + +- Makes FetchAICrypto default again +- Bumps web3 dependencies +- Introduces support for arbitrary protocol handling by DM +- Removes custom fields in signing protocol +- Refactors and updates dialogue and dialogues models +- Moves dialogue module to protocols module +- Introduces MultiplexerStatus to collect aggregate connection status +- Moves Address types from mail to common +- Updates FetchAICrypto to work with Agentland +- Fixes circular dependencies in helpers and configurations +- Unifies contract loading with loading mechanism of other packages +- Adds get-multiaddress command to CLI +- Updates helpers scripts +- Introduces MultiInbox to unify internal message handling +- Adds additional linters (eradicate, more pylint options) +- Improves error reporting in libp2p connection +- Replaces all assert statements with proper exceptions +- Adds skill id to envelope context for improved routing +- Refactors IPC pipes +- Refactors core dependencies +- Adds support for multi-page agent configurations +- Adds type field to all package configurations +- Multiple docs updates including additional explanations of contracts usage +- Multiple additional tests and test stability fixes + ## 0.5.4 (2020-08-13) - Adds support for Windows in p2p connections diff --git a/Makefile b/Makefile index 02d06c05f1..059e991009 100644 --- a/Makefile +++ b/Makefile @@ -42,6 +42,7 @@ clean-test: lint: black aea benchmark examples packages scripts tests flake8 aea benchmark examples packages scripts tests + vulture aea scripts/whitelist.py --exclude "*_pb2.py" .PHONY: pylint pylint: @@ -49,7 +50,8 @@ pylint: .PHONY: security security: - bandit -s B101 -r aea benchmark examples packages scripts tests + bandit -r aea benchmark examples packages + bandit -s B101 -r tests scripts safety check -i 37524 -i 38038 -i 37776 -i 38039 .PHONY: static diff --git a/Pipfile b/Pipfile index ee8e85647e..d54d72e4e5 100644 --- a/Pipfile +++ b/Pipfile @@ -16,13 +16,13 @@ bs4 = "==0.0.1" colorlog = "==4.1.0" defusedxml = "==0.6.0" docker = "==4.2.0" -fetch-p2p-api = {index = "https://test.pypi.org/simple/",version = "==0.0.2"} flake8 = "==3.7.9" flake8-bugbear = "==20.1.4" flake8-docstrings = "==1.5.0" +flake8-eradicate = "==0.4.0" flake8-import-order = "==0.18.1" gym = "==0.15.6" -ipfshttpclient = "==0.4.12" +ipfshttpclient = "==0.6.1" liccheck = "==0.4.3" markdown = ">=3.2.1" matplotlib = "==3.2.1" @@ -52,6 +52,7 @@ requests = "==2.22.0" safety = "==1.8.5" sqlalchemy = "==1.3.17" tox = "==3.15.1" +vulture = "==2.1" vyper = "==0.1.0b12" [packages] diff --git a/README.md b/README.md index d03618ff09..2e21a4006b 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ A framework for autonomous economic agent (AEA) development pip install "aea[all]" -3. Then, build your agent as described in the [docs](https://fetchai.github.io/agents-aea/). +3. Then, build your agent as described in the [docs](https://docs.fetch.ai/aea/).

diff --git a/aea/__version__.py b/aea/__version__.py index 3891260ff4..b16a79a5d0 100644 --- a/aea/__version__.py +++ b/aea/__version__.py @@ -22,7 +22,7 @@ __title__ = "aea" __description__ = "Autonomous Economic Agent framework" __url__ = "https://github.com/fetchai/agents-aea.git" -__version__ = "0.5.4" +__version__ = "0.6.0" __author__ = "Fetch.AI Limited" __license__ = "Apache-2.0" __copyright__ = "2019 Fetch.AI Limited" diff --git a/aea/abstract_agent.py b/aea/abstract_agent.py new file mode 100644 index 0000000000..cb3e617dc1 --- /dev/null +++ b/aea/abstract_agent.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 interface definition of the abstract agent.""" +import datetime +from abc import ABC, abstractmethod, abstractproperty +from typing import Any, Callable, Dict, List, Optional, Tuple + +from aea.connections.base import Connection +from aea.mail.base import Envelope + + +class AbstractAgent(ABC): + """This class provides an abstract base interface for an agent.""" + + @abstractproperty + def name(self) -> str: + """Get agent's name.""" + + @abstractmethod + def start(self) -> None: + """ + Start the agent. + + :return: None + """ + + @abstractmethod + def stop(self) -> None: + """ + Stop the agent. + + :return: None + """ + + @abstractmethod + def setup(self) -> None: + """ + Set up the agent. + + :return: None + """ + + @abstractmethod + def act(self) -> None: + """ + Perform actions on period. + + :return: None + """ + + @abstractmethod + def handle_envelope(self, envelope: Envelope) -> None: + """ + Handle an envelope. + + :param envelope: the envelope to handle. + :return: None + """ + + @abstractmethod + def get_periodic_tasks( + self, + ) -> Dict[Callable, Tuple[float, Optional[datetime.datetime]]]: + """ + Get all periodic tasks for agent. + + :return: dict of callable with period specified + """ + + @abstractmethod + def get_message_handlers(self) -> List[Tuple[Callable[[Any], None], Callable]]: + """ + Get handlers with message getters. + + :return: List of tuples of callables: handler and coroutine to get a message + """ + + @abstractmethod + def get_multiplexer_setup_options(self) -> Optional[Dict]: + """ + Get options to pass to Multiplexer.setup. + + :return: dict of kwargs + """ + + @abstractproperty + def connections(self) -> List[Connection]: + """Return list of connections.""" + + @abstractmethod + def exception_handler( + self, exception: Exception, function: Callable + ) -> Optional[bool]: + """ + Handle exception raised during agent main loop execution. + + :param exception: exception raised + :param function: a callable exception raised in. + + :return: skip exception if True, otherwise re-raise it + """ + + @abstractmethod + def teardown(self) -> None: + """ + Tear down the agent. + + :return: None + """ diff --git a/aea/aea.py b/aea/aea.py index fdb96bcd82..181a234a39 100644 --- a/aea/aea.py +++ b/aea/aea.py @@ -16,24 +16,38 @@ # limitations under the License. # # ------------------------------------------------------------------------------ + """This module contains the implementation of an autonomous economic agent (AEA).""" +import datetime import logging from asyncio import AbstractEventLoop -from typing import Any, Callable, Collection, Dict, List, Optional, Sequence, Type, cast +from multiprocessing.pool import AsyncResult +from typing import ( + Any, + Callable, + Collection, + Dict, + List, + Optional, + Sequence, + Tuple, + Type, + cast, +) from aea.agent import Agent from aea.agent_loop import AsyncAgentLoop, BaseAgentLoop, SyncAgentLoop from aea.configurations.base import PublicId from aea.configurations.constants import DEFAULT_SKILL +from aea.connections.base import Connection from aea.context.base import AgentContext from aea.crypto.wallet import Wallet -from aea.decision_maker.base import DecisionMaker, DecisionMakerHandler +from aea.decision_maker.base import DecisionMakerHandler from aea.decision_maker.default import ( DecisionMakerHandler as DefaultDecisionMakerHandler, ) from aea.exceptions import AEAException from aea.helpers.exception_policy import ExceptionPolicyEnum -from aea.helpers.exec_timeout import ExecTimeoutThreadGuard, TimeoutException from aea.helpers.logging import AgentLoggerAdapter, WithLogger from aea.identity.base import Identity from aea.mail.base import Envelope @@ -41,17 +55,16 @@ from aea.protocols.default.message import DefaultMessage from aea.registries.filter import Filter from aea.registries.resources import Resources -from aea.skills.base import Behaviour, Handler, SkillComponent +from aea.skills.base import Behaviour, Handler from aea.skills.error.handlers import ErrorHandler -from aea.skills.tasks import TaskManager class AEA(Agent, WithLogger): """This class implements an autonomous economic agent.""" RUN_LOOPS: Dict[str, Type[BaseAgentLoop]] = { - "sync": SyncAgentLoop, "async": AsyncAgentLoop, + "sync": SyncAgentLoop, } DEFAULT_RUN_LOOP: str = "async" @@ -61,7 +74,7 @@ def __init__( wallet: Wallet, resources: Resources, loop: Optional[AbstractEventLoop] = None, - timeout: float = 0.05, + period: float = 0.05, execution_timeout: float = 0, max_reactions: int = 20, decision_maker_handler_class: Type[ @@ -83,7 +96,7 @@ def __init__( :param wallet: the wallet of the agent. :param resources: the resources (protocols and skills) of the agent. :param loop: the event loop to run the connections. - :param timeout: the time in (fractions of) seconds to time out an agent between act and react + :param period: period to call agent's act :param exeution_timeout: amount of time to limit single act/handle to execute. :param max_reactions: the processing rate of envelopes per tick (i.e. single loop). :param decision_maker_handler_class: the class implementing the decision maker handler to be used. @@ -102,7 +115,7 @@ def __init__( identity=identity, connections=[], loop=loop, - timeout=timeout, + period=period, loop_mode=loop_mode, runtime_mode=runtime_mode, ) @@ -112,20 +125,18 @@ def __init__( WithLogger.__init__(self, logger=cast(logging.Logger, aea_logger)) self.max_reactions = max_reactions - self._task_manager = TaskManager() decision_maker_handler = decision_maker_handler_class( identity=identity, wallet=wallet ) - self._decision_maker = DecisionMaker( - decision_maker_handler=decision_maker_handler - ) + self.runtime.set_decision_maker(decision_maker_handler) + self._context = AgentContext( self.identity, - self.multiplexer.connection_status, + self.runtime.multiplexer.connection_status, self.outbox, - self.decision_maker.message_in_queue, + self.runtime.decision_maker.message_in_queue, decision_maker_handler.context, - self.task_manager, + self.runtime.task_manager, default_connection, default_routing if default_routing is not None else {}, search_service_address, @@ -134,17 +145,14 @@ def __init__( self._execution_timeout = execution_timeout self._connection_ids = connection_ids self._resources = resources - self._filter = Filter(self.resources, self.decision_maker.message_out_queue) + self._filter = Filter( + self.resources, self.runtime.decision_maker.message_out_queue + ) self._skills_exception_policy = skill_exception_policy self._setup_loggers() - @property - def decision_maker(self) -> DecisionMaker: - """Get decision maker.""" - return self._decision_maker - @property def context(self) -> AgentContext: """Get (agent) context.""" @@ -160,24 +168,6 @@ def resources(self, resources: "Resources") -> None: """Set resources.""" self._resources = resources - @property - def task_manager(self) -> TaskManager: - """Get the task manager.""" - return self._task_manager - - def setup_multiplexer(self) -> None: - """Set up the multiplexer.""" - connections = self.resources.get_all_connections() - if self._connection_ids is not None: - connections = [ - c for c in connections if c.connection_id in self._connection_ids - ] - self.multiplexer.setup( - connections, - default_routing=self.context.default_routing, - default_connection=self.context.default_connection, - ) - @property def filter(self) -> Filter: """Get the filter.""" @@ -195,16 +185,11 @@ def setup(self) -> None: Performs the following: - loads the resources (unless in programmatic mode) - - starts the task manager - - starts the decision maker - calls setup() on the resources :return: None """ - self.task_manager.start() - self.decision_maker.start() self.resources.setup() - ExecTimeoutThreadGuard.start() def act(self) -> None: """ @@ -214,79 +199,62 @@ def act(self) -> None: :return: None """ - for behaviour in self.active_behaviours: - self._behaviour_act(behaviour) - - def react(self) -> None: - """ - React to incoming envelopes. - - Gets up to max_reactions number of envelopes from the inbox and - handles each envelope, which entailes: + self.filter.handle_new_handlers_and_behaviours() - - fetching the protocol referenced by the envelope, and - - returning an envelope to sender if the protocol is unsupported, using the error handler, or - - returning an envelope to sender if there is a decoding error, using the error handler, or - - returning an envelope to sender if no active handler is available for the specified protocol, using the error handler, or - - handling the message recovered from the envelope with all active handlers for the specified protocol. - - :return: None - """ - counter = 0 - while not self.inbox.empty() and counter < self.max_reactions: - counter += 1 - self._react_one() + @property + def active_connections(self) -> List[Connection]: + """Return list of active connections.""" + connections = self.resources.get_all_connections() + if self._connection_ids is not None: + connections = [ + c for c in connections if c.connection_id in self._connection_ids + ] + return connections - def _react_one(self) -> None: + def get_multiplexer_setup_options(self) -> Optional[Dict]: """ - Get and process one envelop from inbox. + Get options to pass to Multiplexer.setup. - :return: None + :return: dict of kwargs """ - envelope = self.inbox.get_nowait() # type: Optional[Envelope] - if envelope is not None: - self._handle(envelope) + return dict( + connections=self.active_connections, + default_routing=self.context.default_routing, + default_connection=self.context.default_connection, + ) def _get_error_handler(self) -> Optional[Handler]: - """Get error hadnler.""" + """Get error handler.""" return self.resources.get_handler(DefaultMessage.protocol_id, DEFAULT_SKILL) - def _handle(self, envelope: Envelope) -> None: - """ - Handle an envelope. - - :param envelope: the envelope to handle. - :return: None - """ - self.logger.debug("Handling envelope: {}".format(envelope)) + def _get_msg_and_handlers_for_envelope( + self, envelope: Envelope + ) -> Tuple[Optional[Message], List[Handler]]: protocol = self.resources.get_protocol(envelope.protocol_id) - # TODO specify error handler in config and make this work for different skill/protocol versions. error_handler = self._get_error_handler() if error_handler is None: self.logger.warning("ErrorHandler not initialized. Stopping AEA!") self.stop() - return + return None, [] error_handler = cast(ErrorHandler, error_handler) if protocol is None: error_handler.send_unsupported_protocol(envelope) - return + return None, [] - try: - if isinstance(envelope.message, Message): - msg = envelope.message - else: + if isinstance(envelope.message, Message): + msg = envelope.message + else: + try: msg = protocol.serializer.decode(envelope.message) - msg.counterparty = envelope.sender - msg.sender = envelope.sender - # msg.to = envelope.to - msg.is_incoming = True - except Exception as e: # pylint: disable=broad-except # thats ok, because we send the decoding error back - self.logger.warning("Decoding error. Exception: {}".format(str(e))) - error_handler.send_decoding_error(envelope) - return + msg.sender = envelope.sender + msg.to = envelope.to + except Exception as e: # pylint: disable=broad-except # thats ok, because we send the decoding error back + self.logger.warning("Decoding error. Exception: {}".format(str(e))) + error_handler.send_decoding_error(envelope) + return None, [] handlers = self.filter.get_active_handlers( protocol.public_id, envelope.skill_id @@ -294,86 +262,114 @@ def _handle(self, envelope: Envelope) -> None: if len(handlers) == 0: error_handler.send_unsupported_skill(envelope) - return + return None, [] - for handler in handlers: - self._handle_message_with_handler(msg, handler) + return msg, handlers - def _handle_message_with_handler(self, message: Message, handler: Handler) -> None: + def handle_envelope(self, envelope: Envelope) -> None: """ - Handle one message with one predefined handler. + Handle an envelope. + + - fetching the protocol referenced by the envelope, and + - returning an envelope to sender if the protocol is unsupported, using the error handler, or + - returning an envelope to sender if there is a decoding error, using the error handler, or + - returning an envelope to sender if no active handler is available for the specified protocol, using the error handler, or + - handling the message recovered from the envelope with all active handlers for the specified protocol. - :param message: message to be handled. - :param handler: handler suitable for this message protocol. + :param envelope: the envelope to handle. + :return: None """ - self._execution_control(handler.handle, handler, [message]) + self.logger.debug("Handling envelope: {}".format(envelope)) + msg, handlers = self._get_msg_and_handlers_for_envelope(envelope) - def _behaviour_act(self, behaviour: Behaviour) -> None: + if msg is None: + return + + for handler in handlers: + handler.handle(msg) + + def _setup_loggers(self): + """Set up logger with agent name.""" + for element in [ + self.runtime.main_loop, + self.runtime.multiplexer, + self.runtime.task_manager, + self.resources.component_registry, + self.resources.behaviour_registry, + self.resources.handler_registry, + self.resources.model_registry, + ]: + element.logger = AgentLoggerAdapter( + element.logger, agent_name=self._identity.name + ) + + def get_periodic_tasks( + self, + ) -> Dict[Callable, Tuple[float, Optional[datetime.datetime]]]: """ - Call behaviour's act. + Get all periodic tasks for agent. - :param behaviour: behaviour already defined - :return: None + :return: dict of callable with period specified """ - self._execution_control(behaviour.act_wrapper, behaviour) + tasks = super().get_periodic_tasks() + tasks.update(self._get_behaviours_tasks()) + return tasks - def _execution_control( + def _get_behaviours_tasks( self, - fn: Callable, - component: SkillComponent, - args: Optional[Sequence] = None, - kwargs: Optional[Dict] = None, - ) -> Any: + ) -> Dict[Callable, Tuple[float, Optional[datetime.datetime]]]: """ - Execute skill function in exception handling environment. + Get all periodic tasks for AEA behaviours. - Logs error, stop agent or propagate excepion depends on policy defined. + :return: dict of callable with period specified + """ + tasks = {} - :param fn: function to call - :param component: skill component function belongs to - :param args: optional sequence of arguments to pass to function on call - :param kwargs: optional dict of keyword arguments to pass to function on call + for behaviour in self.active_behaviours: + tasks[behaviour.act_wrapper] = (behaviour.tick_interval, behaviour.start_at) + + return tasks - :return: same as function + def get_message_handlers(self) -> List[Tuple[Callable[[Any], None], Callable]]: """ - # docstyle: ignore - def log_exception(e, fn, component): - self.logger.exception(f"<{e}> raised during `{fn}` call of `{component}`") - - try: - with ExecTimeoutThreadGuard(self._execution_timeout): - return fn(*(args or []), **(kwargs or {})) - except TimeoutException: - self.logger.warning( - "`{}` of `{}` was terminated as its execution exceeded the timeout of {} seconds. Please refactor your code!".format( - fn, component, self._execution_timeout - ) - ) - except Exception as e: # pylint: disable=broad-except - if self._skills_exception_policy == ExceptionPolicyEnum.propagate: - raise - elif self._skills_exception_policy == ExceptionPolicyEnum.just_log: - log_exception(e, fn, component) - elif self._skills_exception_policy == ExceptionPolicyEnum.stop_and_exit: - log_exception(e, fn, component) - self.stop() - raise AEAException( - f"AEA was terminated cause exception `{e}` in skills {component} {fn}! Please check logs." - ) - else: - raise AEAException( - f"Unsupported exception policy: {self._skills_exception_policy}" - ) - - def update(self) -> None: + Get handlers with message getters. + + :return: List of tuples of callables: handler and coroutine to get a message + """ + return super(AEA, self).get_message_handlers() + [ + (self.filter.handle_internal_message, self.filter.get_internal_message,), + ] + + def exception_handler(self, exception: Exception, function: Callable) -> bool: """ - Update the current state of the agent. + Handle exception raised during agent main loop execution. - Handles the internal messages from the skills to the decision maker. + :param exception: exception raised + :param function: a callable exception raised in. - :return None + :return: bool, propagate exception if True otherwise skip it. """ - self.filter.handle_internal_messages() # pragma: nocover + # docstyle: ignore # noqa: E800 + def log_exception(e, fn): + self.logger.exception(f"<{e}> raised during `{fn}`") + + if self._skills_exception_policy == ExceptionPolicyEnum.propagate: + return True + + if self._skills_exception_policy == ExceptionPolicyEnum.stop_and_exit: + log_exception(exception, function) + self.stop() + raise AEAException( + f"AEA was terminated cause exception `{exception}` in skills {function}! Please check logs." + ) + + if self._skills_exception_policy == ExceptionPolicyEnum.just_log: + log_exception(exception, function) + return False + + raise AEAException( + f"Unsupported exception policy: {self._skills_exception_policy}" + ) def teardown(self) -> None: """ @@ -381,29 +377,31 @@ def teardown(self) -> None: Performs the following: - - stops the decision maker - - stops the task manager - tears down the resources. :return: None """ self.logger.debug("[{}]: Calling teardown method...".format(self.name)) - self.decision_maker.stop() - self.task_manager.stop() self.resources.teardown() - ExecTimeoutThreadGuard.stop() - def _setup_loggers(self): - """Set up logger with agent name.""" - for element in [ - self.main_loop, - self.multiplexer, - self.task_manager, - self.resources.component_registry, - self.resources.behaviour_registry, - self.resources.handler_registry, - self.resources.model_registry, - ]: - element.logger = AgentLoggerAdapter( - element.logger, agent_name=self._identity.name - ) + def get_task_result(self, task_id: int) -> AsyncResult: + """ + Get the result from a task. + + :return: async result for task_id + """ + return self.runtime.task_manager.get_task_result(task_id) + + def enqueue_task( + self, func: Callable, args: Sequence = (), kwds: Optional[Dict[str, Any]] = None + ) -> int: + """ + Enqueue a task with the task manager. + + :param func: the callable instance to be enqueued + :param args: the positional arguments to be passed to the function. + :param kwds: the keyword arguments to be passed to the function. + :return the task id to get the the result. + :raises ValueError: if the task manager is not running. + """ + return self.runtime.task_manager.enqueue_task(func, args, kwds) diff --git a/aea/aea_builder.py b/aea/aea_builder.py index a7fa324122..01e3cf3ca1 100644 --- a/aea/aea_builder.py +++ b/aea/aea_builder.py @@ -47,7 +47,7 @@ from aea import AEA_DIR from aea.aea import AEA -from aea.components.base import Component +from aea.components.base import Component, load_aea_package from aea.components.loader import load_component_from_config from aea.configurations.base import ( AgentConfig, @@ -69,8 +69,8 @@ DEFAULT_PROTOCOL, DEFAULT_SKILL, ) -from aea.configurations.loader import ConfigLoader -from aea.contracts import contract_registry +from aea.configurations.loader import ConfigLoader, load_component_configuration +from aea.configurations.pypi import is_satisfiable, merge_dependencies from aea.crypto.helpers import verify_or_create_private_keys from aea.crypto.wallet import Wallet from aea.decision_maker.base import DecisionMakerHandler @@ -78,11 +78,9 @@ DecisionMakerHandler as DefaultDecisionMakerHandler, ) from aea.exceptions import AEAException -from aea.helpers.base import load_aea_package, load_module +from aea.helpers.base import load_module from aea.helpers.exception_policy import ExceptionPolicyEnum from aea.helpers.logging import AgentLoggerAdapter -from aea.helpers.pypi import is_satisfiable -from aea.helpers.pypi import merge_dependencies from aea.identity.base import Identity from aea.registries.resources import Resources @@ -282,7 +280,7 @@ class AEABuilder: """ - DEFAULT_AGENT_LOOP_TIMEOUT = 0.05 + DEFAULT_AGENT_ACT_PERIOD = 0.05 # seconds DEFAULT_EXECUTION_TIMEOUT = 0 DEFAULT_MAX_REACTIONS = 20 DEFAULT_DECISION_MAKER_HANDLER_CLASS: Type[ @@ -326,25 +324,28 @@ def _reset(self, is_full_reset: bool = False) -> None: :param is_full_reset: whether it is a full reset or not. :return: None. """ - self._name = None # type: Optional[str] - self._private_key_paths = {} # type: Dict[str, Optional[str]] - self._connection_private_key_paths = {} # type: Dict[str, Optional[str]] + self._name: Optional[str] = None + self._private_key_paths: Dict[str, Optional[str]] = {} + self._connection_private_key_paths: Dict[str, Optional[str]] = {} if not is_full_reset: self._remove_components_from_dependency_manager() - self._component_instances = { + self._component_instances: Dict[ + ComponentType, Dict[ComponentConfiguration, Component] + ] = { ComponentType.CONNECTION: {}, ComponentType.CONTRACT: {}, ComponentType.PROTOCOL: {}, ComponentType.SKILL: {}, - } # type: Dict[ComponentType, Dict[ComponentConfiguration, Component]] + } + self._custom_component_configurations: Dict[ComponentId, Dict] = {} self._to_reset: bool = False self._build_called: bool = False if not is_full_reset: return self._default_ledger = DEFAULT_LEDGER self._default_connection: PublicId = DEFAULT_CONNECTION - self._context_namespace = {} # type: Dict[str, Any] - self._timeout: Optional[float] = None + self._context_namespace: Dict[str, Any] = {} + self._period: Optional[float] = None self._execution_timeout: Optional[float] = None self._max_reactions: Optional[int] = None self._decision_maker_handler_class: Optional[Type[DecisionMakerHandler]] = None @@ -366,15 +367,15 @@ def _remove_components_from_dependency_manager(self) -> None: component_config.component_id ) - def set_timeout(self, timeout: Optional[float]) -> "AEABuilder": + def set_period(self, period: Optional[float]) -> "AEABuilder": """ - Set agent loop idle timeout in seconds. + Set agent act period. - :param timeout: timeout in seconds + :param period: period in seconds :return: self """ - self._timeout = timeout + self._period = period return self def set_execution_timeout(self, execution_timeout: Optional[float]) -> "AEABuilder": @@ -627,7 +628,7 @@ def add_component( :return: the AEABuilder """ directory = Path(directory) - configuration = ComponentConfiguration.load( + configuration = load_component_configuration( component_type, directory, skip_consistency_check ) self._check_can_add(configuration) @@ -765,10 +766,11 @@ def _build_identity_from_wallet(self, wallet: Wallet) -> Identity: :param wallet: the wallet :return: the identity """ - assert self._name is not None, "You must set the name of the agent." + if self._name is None: # pragma: nocover + raise ValueError("You must set the name of the agent.") if not wallet.addresses: - raise ValueError("wallet has no addresses") + raise ValueError("Wallet has no addresses.") if len(wallet.addresses) > 1: identity = Identity( @@ -869,7 +871,7 @@ def build(self, connection_ids: Optional[Collection[PublicId]] = None,) -> AEA: wallet, resources, loop=None, - timeout=self._get_agent_loop_timeout(), + period=self._get_agent_act_period(), execution_timeout=self._get_execution_timeout(), is_debug=False, max_reactions=self._get_max_reactions(), @@ -887,20 +889,15 @@ def build(self, connection_ids: Optional[Collection[PublicId]] = None,) -> AEA: ComponentType.SKILL, resources, identity.name, agent_context=aea.context ) self._build_called = True - self._populate_contract_registry() return aea - def _get_agent_loop_timeout(self) -> float: + def _get_agent_act_period(self) -> float: """ - Return agent loop idle timeout. + Return agent act period. - :return: timeout in seconds if set else default value. + :return: period in seconds if set else default value. """ - return ( - self._timeout - if self._timeout is not None - else self.DEFAULT_AGENT_LOOP_TIMEOUT - ) + return self._period or self.DEFAULT_AGENT_ACT_PERIOD def _get_execution_timeout(self) -> float: """ @@ -1131,7 +1128,7 @@ def set_from_configuration( self.set_default_connection( PublicId.from_str(agent_configuration.default_connection) ) - self.set_timeout(agent_configuration.timeout) + self.set_period(agent_configuration.period) self.set_execution_timeout(agent_configuration.execution_timeout) self.set_max_reactions(agent_configuration.max_reactions) if agent_configuration.decision_maker_handler != {}: @@ -1216,6 +1213,9 @@ def set_from_configuration( component_path, skip_consistency_check=skip_consistency_check, ) + self._custom_component_configurations = ( + agent_configuration.component_configurations + ) def _find_import_order( self, @@ -1244,7 +1244,7 @@ def _find_import_order( ) configuration = cast( SkillConfig, - ComponentConfiguration.load( + load_component_configuration( skill_id.component_type, component_path, skip_consistency_check ), ) @@ -1268,9 +1268,7 @@ def _find_import_order( while len(queue) > 0: current = queue.pop() order.append(current) - for node in supports[ - current - ]: # pragma: nocover # TODO: extract method and test properly + for node in supports[current]: # pragma: nocover depends_on[node].discard(current) if len(depends_on[node]) == 0: queue.append(node) @@ -1305,9 +1303,6 @@ def from_aea_project( ) builder = AEABuilder(with_default_packages=False) - # TODO isolate environment - # load_env_file(str(aea_config_path / ".env")) - # load agent configuration file configuration_file = aea_project_path / DEFAULT_AEA_CONFIG_FILE @@ -1350,6 +1345,11 @@ def _load_and_add_components( ) else: configuration = deepcopy(configuration) + configuration.update( + self._custom_component_configurations.get( + configuration.component_id, {} + ) + ) _logger = make_logger(configuration, agent_name) component = load_component_from_config( configuration, logger=_logger, **kwargs @@ -1357,37 +1357,6 @@ def _load_and_add_components( resources.add_component(component) - def _populate_contract_registry(self): - """Populate contract registry.""" - for configuration in self._package_dependency_manager.get_components_by_type( - ComponentType.CONTRACT - ).values(): - configuration = cast(ContractConfig, configuration) - if str(configuration.public_id) in contract_registry.specs: - logger.warning( - f"Skipping registration of contract {configuration.public_id} since already registered." - ) - continue - logger.debug( # pragma: nocover - f"Registering contract {configuration.public_id}" - ) - try: # pragma: nocover - contract_registry.register( - id_=str(configuration.public_id), - entry_point=f"{configuration.prefix_import_path}.contract:{configuration.class_name}", - class_kwargs={ - "contract_interface": configuration.contract_interfaces - }, - contract_config=configuration, # TODO: resolve configuration being applied globally - ) - except AEAException as e: # pragma: nocover - if "Cannot re-register id:" in str(e): - logger.warning( - "Already registered: {}".format(configuration.class_name) - ) - else: - raise e - def _check_we_can_build(self): if self._build_called and self._to_reset: raise ValueError( diff --git a/aea/agent.py b/aea/agent.py index 31e1270a4b..b62acaadf6 100644 --- a/aea/agent.py +++ b/aea/agent.py @@ -17,30 +17,25 @@ # # ------------------------------------------------------------------------------ """This module contains the implementation of a generic agent.""" - +import datetime import logging -from abc import ABC, abstractmethod from asyncio import AbstractEventLoop -from typing import Dict, List, Optional, Type +from typing import Any, Callable, Dict, List, Optional, Tuple, Type -from aea.agent_loop import BaseAgentLoop, SyncAgentLoop +from aea.abstract_agent import AbstractAgent from aea.connections.base import Connection from aea.identity.base import Identity -from aea.multiplexer import InBox, Multiplexer, OutBox +from aea.mail.base import Envelope +from aea.multiplexer import InBox, OutBox from aea.runtime import AsyncRuntime, BaseRuntime, RuntimeStates, ThreadedRuntime logger = logging.getLogger(__name__) -class Agent(ABC): +class Agent(AbstractAgent): """This class provides an abstract base class for a generic agent.""" - RUN_LOOPS: Dict[str, Type[BaseAgentLoop]] = { - "sync": SyncAgentLoop, - } - DEFAULT_RUN_LOOP: str = "sync" - RUNTIMES: Dict[str, Type[BaseRuntime]] = { "async": AsyncRuntime, "threaded": ThreadedRuntime, @@ -52,7 +47,7 @@ def __init__( identity: Identity, connections: List[Connection], loop: Optional[AbstractEventLoop] = None, - timeout: float = 1.0, + period: float = 1.0, loop_mode: Optional[str] = None, runtime_mode: Optional[str] = None, ) -> None: @@ -62,48 +57,45 @@ def __init__( :param identity: the identity of the agent. :param connections: the list of connections of the agent. :param loop: the event loop to run the connections. - :param timeout: the time in (fractions of) seconds to time out an agent between act and react + :param period: period to call agent's act :param loop_mode: loop_mode to choose agent run loop. :param runtime_mode: runtime mode to up agent. :return: None """ - self._identity = identity self._connections = connections - - self._multiplexer = Multiplexer(self._connections, loop=loop) - self._inbox = InBox(self._multiplexer) - self._outbox = OutBox(self._multiplexer, identity.address) - self._timeout = timeout - + self._identity = identity + self._period = period self._tick = 0 - - self._loop_mode = loop_mode or self.DEFAULT_RUN_LOOP - loop_cls = self._get_main_loop_class() - self._main_loop: BaseAgentLoop = loop_cls(self) - self._runtime_mode = runtime_mode or self.DEFAULT_RUNTIME runtime_cls = self._get_runtime_class() - self._runtime: BaseRuntime = runtime_cls(agent=self, loop=loop) + self._runtime: BaseRuntime = runtime_cls( + agent=self, loop_mode=loop_mode, loop=loop + ) + + self._inbox = InBox(self.runtime.multiplexer) + self._outbox = OutBox(self.runtime.multiplexer) + + @property + def connections(self) -> List[Connection]: + """Return list of connections.""" + return self._connections @property - def is_running(self): + def active_connections(self) -> List[Connection]: + """Return list of active connections.""" + return self._connections + + @property + def is_running(self) -> bool: """Get running state of the runtime and agent.""" return self.runtime.is_running @property - def is_stopped(self): + def is_stopped(self) -> bool: """Get running state of the runtime and agent.""" return self.runtime.is_stopped - def _get_main_loop_class(self) -> Type[BaseAgentLoop]: - """Get main loop class based on loop mode.""" - if self._loop_mode not in self.RUN_LOOPS: - raise ValueError( - f"Loop `{self._loop_mode} is not supported. valid are: `{list(self.RUN_LOOPS.keys())}`" - ) - return self.RUN_LOOPS[self._loop_mode] - def _get_runtime_class(self) -> Type[BaseRuntime]: """Get runtime class based on runtime mode.""" if self._runtime_mode not in self.RUNTIMES: @@ -112,16 +104,19 @@ def _get_runtime_class(self) -> Type[BaseRuntime]: ) return self.RUNTIMES[self._runtime_mode] + def get_multiplexer_setup_options(self) -> Optional[Dict]: + """ + Get options to pass to Multiplexer.setup. + + :return: dict of kwargs + """ + return {"connections": self.active_connections} + @property def identity(self) -> Identity: """Get the identity.""" return self._identity - @property - def multiplexer(self) -> Multiplexer: - """Get the multiplexer.""" - return self._multiplexer - @property def inbox(self) -> InBox: # pragma: nocover """ @@ -156,30 +151,25 @@ def tick(self) -> int: # pragma: nocover """ return self._tick - @property - def timeout(self) -> float: - """Get the time in (fractions of) seconds to time out an agent between act and react.""" - return self._timeout + def handle_envelope(self, envelope: Envelope) -> None: # pragma: nocover + """ + Handle an envelope. - @property - def loop_mode(self) -> str: - """Get the agent loop mode.""" - return self._loop_mode + :param envelope: the envelope to handle. + :return: None + """ + raise NotImplementedError @property - def main_loop(self) -> BaseAgentLoop: - """Get the main agent loop.""" - return self._main_loop + def period(self) -> float: + """Get a period to call act.""" + return self._period @property def runtime(self) -> BaseRuntime: """Get the runtime.""" return self._runtime - def setup_multiplexer(self) -> None: - """Set up the multiplexer.""" - pass - def start(self) -> None: """ Start the agent. @@ -203,19 +193,6 @@ def start(self) -> None: """ self.runtime.start() - def start_setup(self) -> None: - """ - Set up Agent on start. - - - connect Multiplexer - - call agent.setup - - set liveness to started - - :return: None - """ - logger.debug("[{}]: Calling setup method...".format(self.name)) - self.setup() - def stop(self) -> None: """ Stop the agent. @@ -230,51 +207,45 @@ def stop(self) -> None: """ self.runtime.stop() - @abstractmethod - def setup(self) -> None: - """ - Set up the agent. - - :return: None - """ - - @abstractmethod - def act(self) -> None: - """ - Perform actions. - - :return: None + @property + def state(self) -> RuntimeStates: """ + Get state of the agent's runtime. - @abstractmethod - def react(self) -> None: + :return: RuntimeStates """ - React to events. + return self._runtime.state - :return: None + def get_periodic_tasks( + self, + ) -> Dict[Callable, Tuple[float, Optional[datetime.datetime]]]: """ + Get all periodic tasks for agent. - @abstractmethod - def update(self) -> None: + :return: dict of callable with period specified """ - Update the internals of the agent which are not exposed to the skills. + return {self.act: (self.period, None)} - :return None + def get_message_handlers(self) -> List[Tuple[Callable[[Any], None], Callable]]: """ + Get handlers with message getters. - @abstractmethod - def teardown(self) -> None: + :return: List of tuples of callables: handler and coroutine to get a message """ - Tear down the agent. + return [(self.handle_envelope, self.inbox.async_get)] - :return: None + def exception_handler( + self, exception: Exception, function: Callable + ) -> bool: # pragma: nocover """ + Handle exception raised during agent main loop execution. - @property - def state(self) -> RuntimeStates: - """ - Get state of the agent's runtime. + :param exception: exception raised + :param function: a callable exception raised in. - :return: RuntimeStates + :return: bool, propagate exception if True otherwise skip it. """ - return self._runtime.state + logger.exception( + f"Exception {repr(exception)} raised during {repr(function)} call." + ) + return True diff --git a/aea/agent_loop.py b/aea/agent_loop.py index e60a612360..59fff7c62d 100644 --- a/aea/agent_loop.py +++ b/aea/agent_loop.py @@ -18,6 +18,7 @@ # ------------------------------------------------------------------------------ """This module contains the implementation of an agent loop using asyncio.""" import asyncio +import datetime import logging from abc import ABC, abstractmethod from asyncio import CancelledError @@ -25,37 +26,28 @@ from asyncio.tasks import Task from enum import Enum from functools import partial -from typing import ( - Callable, - Dict, - List, - Optional, - TYPE_CHECKING, -) +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple +from aea.abstract_agent import AbstractAgent from aea.exceptions import AEAException from aea.helpers.async_utils import ( AsyncState, + HandlerItemGetter, PeriodicCaller, ensure_loop, ) +from aea.helpers.exec_timeout import ExecTimeoutThreadGuard, TimeoutException from aea.helpers.logging import WithLogger -from aea.multiplexer import InBox -from aea.skills.base import Behaviour logger = logging.getLogger(__name__) -if TYPE_CHECKING: - from aea.aea import AEA # pragma: no cover - from aea.agent import Agent # pragma: no cover - class BaseAgentLoop(WithLogger, ABC): """Base abstract agent loop class.""" def __init__( - self, agent: "Agent", loop: Optional[AbstractEventLoop] = None + self, agent: AbstractAgent, loop: Optional[AbstractEventLoop] = None ) -> None: """Init loop. @@ -63,24 +55,39 @@ def __init__( :params loop: optional asyncio event loop. if not specified a new loop will be created. """ WithLogger.__init__(self, logger) - self._agent: "Agent" = agent + self._agent: AbstractAgent = agent self.set_loop(ensure_loop(loop)) self._tasks: List[asyncio.Task] = [] self._state: AsyncState = AsyncState() self._exceptions: List[Exception] = [] + @property + def agent(self) -> AbstractAgent: # pragma: nocover + """Get agent.""" + return self._agent + def set_loop(self, loop: AbstractEventLoop) -> None: """Set event loop and all event loopp related objects.""" self._loop: AbstractEventLoop = loop def start(self) -> None: """Start agent loop synchronously in own asyncio loop.""" + self.setup() self._loop.run_until_complete(self.run_loop()) + def setup(self) -> None: # pylint: disable=no-self-use + """Set up loop before started.""" + # start and stop methods are classmethods cause one instance shared across muiltiple threads + ExecTimeoutThreadGuard.start() + + def teardown(self): # pylint: disable=no-self-use + """Tear down loop on stop.""" + # start and stop methods are classmethods cause one instance shared across muiltiple threads + ExecTimeoutThreadGuard.stop() + async def run_loop(self) -> None: """Run agent loop.""" self.logger.debug("agent loop started") - self._state.set(AgentLoopStates.started) self._set_tasks() try: await self._gather_tasks() @@ -106,6 +113,7 @@ async def wait_run_loop_stopped(self) -> None: def stop(self) -> None: """Stop agent loop.""" + self.teardown() self._state.set(AgentLoopStates.stopping) logger.debug("agent loop stopping!") if self._loop.is_running(): @@ -122,7 +130,7 @@ def _stop_tasks(self) -> None: """Cancel all tasks.""" for task in self._tasks: if task.done(): - continue + continue #  pragma: nocover task.cancel() @property @@ -150,7 +158,7 @@ class AsyncAgentLoop(BaseAgentLoop): NEW_BEHAVIOURS_PROCESS_SLEEP = 1 # check new behaviours registered every second. - def __init__(self, agent: "AEA", loop: AbstractEventLoop = None): + def __init__(self, agent: AbstractAgent, loop: AbstractEventLoop = None): """ Init agent loop. @@ -158,85 +166,130 @@ def __init__(self, agent: "AEA", loop: AbstractEventLoop = None): :param loop: asyncio loop to use. optional """ super().__init__(agent=agent, loop=loop) - self._agent: "AEA" = self._agent + self._agent: AbstractAgent = self._agent - self._behaviours_registry: Dict[Behaviour, PeriodicCaller] = {} + self._periodic_tasks: Dict[Callable, PeriodicCaller] = {} - def _behaviour_exception_callback(self, fn: Callable, exc: Exception) -> None: + def _periodic_task_exception_callback( + self, task_callable: Callable, exc: Exception + ) -> None: """ - Call on behaviour's act exception. + Call on periodic task exception. - :param fn: behaviour's act + :param task_callable: function to be called :param: exc: Exception raised :return: None """ self.logger.exception( - f"Loop: Exception: `{exc}` occured during `{fn}` processing" + f"Loop: Exception: `{exc}` occured during `{task_callable}` processing" ) self._exceptions.append(exc) - self._state.set(AgentLoopStates.error) - def _register_behaviour(self, behaviour: Behaviour) -> None: + def _execution_control( + self, + fn: Callable, + args: Optional[Sequence] = None, + kwargs: Optional[Dict] = None, + ) -> Any: """ - Register behaviour to run periodically. + Execute skill function in exception handling environment. - :param behaviour: Behaviour object + Logs error, stop agent or propagate exception depends on policy defined. + + :param fn: function to call + :param args: optional sequence of arguments to pass to function on call + :param kwargs: optional dict of keyword arguments to pass to function on call + + :return: same as function + """ + execution_timeout = getattr(self.agent, "_execution_timeout", 0) + + try: + with ExecTimeoutThreadGuard(execution_timeout): + return fn(*(args or []), **(kwargs or {})) + except TimeoutException: #  pragma: nocover + self.logger.warning( + "`{}` was terminated as its execution exceeded the timeout of {} seconds. Please refactor your code!".format( + fn, execution_timeout + ) + ) + except Exception as e: # pylint: disable=broad-except + try: + if self.agent.exception_handler(e, fn) is True: + self._state.set(AgentLoopStates.error) + raise + except Exception as e: + self._state.set(AgentLoopStates.error) + self._exceptions.append(e) + raise + + def _register_periodic_task( + self, + task_callable: Callable, + period: float, + start_at: Optional[datetime.datetime], + ) -> None: + """ + Register function to run periodically. + + :param task_callable: function to be called + :param pediod: float: in seconds + :param start_at: optional datetime, when to run task for the first time, otherwise call it right now :return: None """ - if behaviour in self._behaviours_registry: # pragma: nocover + if task_callable in self._periodic_tasks: # pragma: nocover # already registered return periodic_caller = PeriodicCaller( - partial( - self._agent._execution_control, # pylint: disable=protected-access # TODO: refactoring! - behaviour.act_wrapper, - behaviour, - ), - behaviour.tick_interval, - behaviour.start_at, - self._behaviour_exception_callback, - self._loop, + partial(self._execution_control, task_callable), + period=period, + start_at=start_at, + exception_callback=self._periodic_task_exception_callback, + loop=self._loop, ) - self._behaviours_registry[behaviour] = periodic_caller + self._periodic_tasks[task_callable] = periodic_caller periodic_caller.start() - self.logger.debug(f"Behaviour {behaviour} registered.") + self.logger.debug(f"Periodic task {task_callable} registered.") - def _register_all_behaviours(self) -> None: - """Register all AEA behaviours to run periodically.""" - for behaviour in self._agent.active_behaviours: - self._register_behaviour(behaviour) + def _register_periodic_tasks(self) -> None: + """Register all AEA related periodic tasks.""" + for ( + task_callable, + (period, start_at), + ) in self._agent.get_periodic_tasks().items(): + self._register_periodic_task(task_callable, period, start_at) - def _unregister_behaviour(self, behaviour: Behaviour) -> None: + def _unregister_periodic_task(self, task_callable: Callable) -> None: """ - Unregister periodic execution of the behaviour. + Unregister periodic execution of the task. - :param behaviour: Behaviour to schedule periodic execution. + :param task_callable: function to be called periodically. :return: None """ - periodic_caller = self._behaviours_registry.pop(behaviour, None) + periodic_caller = self._periodic_tasks.pop(task_callable, None) if periodic_caller is None: # pragma: nocover return periodic_caller.stop() def _stop_all_behaviours(self) -> None: """Unregister periodic execution of all registered behaviours.""" - for behaviour in list(self._behaviours_registry.keys()): - self._unregister_behaviour(behaviour) + for task_callable in list(self._periodic_tasks.keys()): + self._unregister_periodic_task(task_callable) async def _task_wait_for_error(self) -> None: """Wait for error and raise first.""" await self._state.wait(AgentLoopStates.error) raise self._exceptions[0] - def _stop_tasks(self): + def _stop_tasks(self) -> None: """Cancel all tasks and stop behaviours registered.""" BaseAgentLoop._stop_tasks(self) self._stop_all_behaviours() - def _set_tasks(self): + def _set_tasks(self) -> None: """Set run loop tasks.""" self._tasks = self._create_tasks() self.logger.debug("tasks created!") @@ -248,67 +301,29 @@ def _create_tasks(self) -> List[Task]: :return: list of asyncio Tasks """ tasks = [ - self._task_process_inbox(), - self._task_process_internal_messages(), - self._task_process_new_skill_components(), + self._process_messages(HandlerItemGetter(self._message_handlers())), + self._task_register_periodic_tasks(), self._task_wait_for_error(), ] return list(map(self._loop.create_task, tasks)) # type: ignore # some issue with map and create_task - async def _task_process_inbox(self) -> None: - """Process incoming messages.""" - inbox: InBox = self._agent.inbox - self.logger.info("Start processing messages...") - while self.is_running: - await inbox.async_wait() - self._agent.react() + def _message_handlers(self) -> List[Tuple[Callable[[Any], None], Callable]]: + """Get all agent's message handlers.""" + return self._agent.get_message_handlers() - async def _task_process_internal_messages(self) -> None: - """Process decision maker's internal messages.""" - queue = self._agent.decision_maker.message_out_queue + async def _process_messages(self, getter: HandlerItemGetter) -> None: + """Process message from ItemGetter.""" + self.logger.info("Start processing messages...") + self._state.set(AgentLoopStates.started) while self.is_running: - msg = await queue.async_get() - # TODO: better interaction with agent's internal messages - self._agent.filter._process_internal_message( # pylint: disable=protected-access # TODO: refactoring! - msg - ) + handler, item = await getter.get() + self._execution_control(handler, [item]) - async def _task_process_new_skill_components(self) -> None: + async def _task_register_periodic_tasks(self) -> None: """Process new behaviours added to skills in runtime.""" while self.is_running: - # TODO: better handling internal messages for skills internal updates - self._agent.filter._handle_new_behaviours() # pylint: disable=protected-access # TODO: refactoring! - self._agent.filter._handle_new_handlers() # pylint: disable=protected-access # TODO: refactoring! - self._register_all_behaviours() # re register, cause new may appear + self._register_periodic_tasks() # re register, cause new may appear await asyncio.sleep(self.NEW_BEHAVIOURS_PROCESS_SLEEP) -class SyncAgentLoop(BaseAgentLoop): - """Synchronous agent loop.""" - - def __init__(self, agent: "Agent", loop: AbstractEventLoop = None): - """ - Init agent loop. - - :param agent: AEA instance - :param loop: asyncio loop to use. optional - """ - super().__init__(agent=agent, loop=loop) - self._agent: "AEA" = self._agent - asyncio.set_event_loop(self._loop) - - async def _agent_loop(self) -> None: - """Run loop inside coroutine but call synchronous callbacks from agent.""" - while self.is_running: - self._spin_main_loop() - await asyncio.sleep(self._agent.timeout) - - def _spin_main_loop(self) -> None: - """Run one spin of agent loop: act, react, update.""" - self._agent.act() - self._agent.react() - self._agent.update() - - def _set_tasks(self) -> None: - """Set run loop tasks.""" - self._tasks = [self._loop.create_task(self._agent_loop())] +SyncAgentLoop = AsyncAgentLoop # temporary solution! diff --git a/aea/cli/config.py b/aea/cli/config.py index b4ba80a08b..06c18ae08b 100644 --- a/aea/cli/config.py +++ b/aea/cli/config.py @@ -37,7 +37,7 @@ @click.group() @click.pass_context @check_aea_project -def config(click_context): +def config(click_context): # pylint: disable=unused-argument """Read or modify a configuration.""" @@ -112,7 +112,7 @@ def _set_config(ctx: Context, json_path: List[str], value: str, type_str: str) - configuration_obj = config_loader.configuration_class.from_json( configuration_object ) - config_loader.validator.validate(instance=configuration_obj.json) + config_loader.validate(configuration_obj.json) config_loader.dump(configuration_obj, open(configuration_file_path, "w")) except Exception: raise click.ClickException("Attribute or value not valid.") diff --git a/aea/cli/core.py b/aea/cli/core.py index 9113fc9916..9bdb9f40db 100644 --- a/aea/cli/core.py +++ b/aea/cli/core.py @@ -37,6 +37,7 @@ from aea.cli.generate_key import generate_key from aea.cli.generate_wealth import generate_wealth from aea.cli.get_address import get_address +from aea.cli.get_multiaddress import get_multiaddress from aea.cli.get_wealth import get_wealth from aea.cli.init import init from aea.cli.install import install @@ -85,7 +86,9 @@ def cli(click_context, skip_consistency_check: bool) -> None: @click.option("-p", "--port", default=8080) @click.option("--local", is_flag=True, help="For using local folder.") @click.pass_context -def gui(click_context, port, local): # pragma: no cover +def gui( # pylint: disable=unused-argument + click_context, port, local +): # pragma: no cover """Run the CLI GUI.""" _init_gui() import aea.cli_gui # pylint: disable=import-outside-toplevel,redefined-outer-name @@ -101,8 +104,8 @@ def _init_gui() -> None: :return: None :raisees: ClickException if author is not set up. """ - config = get_or_create_cli_config() - author = config.get(AUTHOR_KEY, None) + config_ = get_or_create_cli_config() + author = config_.get(AUTHOR_KEY, None) if author is None: raise click.ClickException( "Author is not set up. Please run 'aea init' and then restart." @@ -123,6 +126,7 @@ def _init_gui() -> None: cli.add_command(generate_wealth) cli.add_command(generate) cli.add_command(get_address) +cli.add_command(get_multiaddress) cli.add_command(get_wealth) cli.add_command(init) cli.add_command(install) diff --git a/aea/cli/eject.py b/aea/cli/eject.py index f031b632ae..0cb1ca2d55 100644 --- a/aea/cli/eject.py +++ b/aea/cli/eject.py @@ -39,7 +39,7 @@ @click.group() @click.pass_context @check_aea_project -def eject(click_context: click.core.Context): +def eject(click_context: click.core.Context): # pylint: disable=unused-argument """Eject an installed item.""" diff --git a/aea/cli/fingerprint.py b/aea/cli/fingerprint.py index 3e9507bb92..c52bebbafe 100644 --- a/aea/cli/fingerprint.py +++ b/aea/cli/fingerprint.py @@ -40,7 +40,7 @@ @click.group() @click.pass_context -def fingerprint(click_context): +def fingerprint(click_context): # pylint: disable=unused-argument """Fingerprint a resource.""" diff --git a/aea/cli/generate.py b/aea/cli/generate.py index 2cfeeaeb35..2eef2171b5 100644 --- a/aea/cli/generate.py +++ b/aea/cli/generate.py @@ -40,7 +40,7 @@ @click.group() @click.pass_context @check_aea_project -def generate(click_context): +def generate(click_context): # pylint: disable=unused-argument """Generate a resource for the agent.""" diff --git a/aea/cli/generate_key.py b/aea/cli/generate_key.py index 1781c0fd5b..8173a276ec 100644 --- a/aea/cli/generate_key.py +++ b/aea/cli/generate_key.py @@ -24,7 +24,7 @@ import click -from aea.crypto.helpers import IDENTIFIER_TO_KEY_FILES, create_private_key +from aea.crypto.helpers import PRIVATE_KEY_PATH_SCHEMA, create_private_key from aea.crypto.registries import crypto_registry @@ -57,14 +57,13 @@ def _generate_private_key(type_: str, file: Optional[str] = None) -> None: """ if type_ == "all" and file is not None: raise click.ClickException("Type all cannot be used in combination with file.") - elif type_ == "all": - types = list(IDENTIFIER_TO_KEY_FILES.keys()) - else: - types = [type_] - for type_ in types: - private_key_file = IDENTIFIER_TO_KEY_FILES[type_] if file is None else file + types = list(crypto_registry.supported_ids) if type_ == "all" else [type_] + for type__ in types: + private_key_file = ( + PRIVATE_KEY_PATH_SCHEMA.format(type__) if file is None else file + ) if _can_write(private_key_file): - create_private_key(type_, private_key_file) + create_private_key(type__, private_key_file) def _can_write(path) -> bool: @@ -74,5 +73,4 @@ def _can_write(path) -> bool: default=False, ) return value - else: - return True + return True diff --git a/aea/cli/generate_wealth.py b/aea/cli/generate_wealth.py index fc96215077..c8d346a4a0 100644 --- a/aea/cli/generate_wealth.py +++ b/aea/cli/generate_wealth.py @@ -19,26 +19,18 @@ """Implementation of the 'aea generate_wealth' subcommand.""" -import time from typing import Dict, Optional, cast import click from aea.cli.utils.context import Context from aea.cli.utils.decorators import check_aea_project -from aea.cli.utils.package_utils import ( - try_get_balance, - verify_or_create_private_keys_ctx, -) -from aea.configurations.base import AgentConfig +from aea.cli.utils.package_utils import verify_or_create_private_keys_ctx from aea.crypto.helpers import try_generate_testnet_wealth from aea.crypto.registries import faucet_apis_registry, make_faucet_api_cls from aea.crypto.wallet import Wallet -FUNDS_RELEASE_TIMEOUT = 30 - - @click.command() @click.argument( "type_", @@ -84,30 +76,7 @@ def _try_generate_wealth( address, testnet ) ) - try_generate_testnet_wealth(type_, address) - if sync: - _wait_funds_release(ctx.agent_config, wallet, type_) + try_generate_testnet_wealth(type_, address, sync) - except (AssertionError, ValueError) as e: # pragma: no cover + except ValueError as e: # pragma: no cover raise click.ClickException(str(e)) - - -def _wait_funds_release(agent_config: AgentConfig, wallet: Wallet, type_: str) -> None: - """ - Wait for the funds to be released. - - :param agent_config: the agent config - :param wallet: the wallet - :param type_: the network type - """ - start_balance = try_get_balance(agent_config, wallet, type_) - end_time = time.time() + FUNDS_RELEASE_TIMEOUT - has_hit_timeout = True - while time.time() < end_time: - current_balance = try_get_balance(agent_config, wallet, type_) - if start_balance != current_balance: - has_hit_timeout = False - break # pragma: no cover - time.sleep(1) - if has_hit_timeout: - raise ValueError("Timeout hit. Syncing did not finish.") diff --git a/aea/cli/get_multiaddress.py b/aea/cli/get_multiaddress.py new file mode 100644 index 0000000000..81d63ad2f3 --- /dev/null +++ b/aea/cli/get_multiaddress.py @@ -0,0 +1,243 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2020 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 'aea get_multiaddress' subcommand.""" +import re +import typing +from pathlib import Path +from typing import Optional, Tuple, cast + +import click +from click import ClickException + +from aea.cli.utils.click_utils import PublicIdParameter +from aea.cli.utils.config import load_item_config +from aea.cli.utils.context import Context +from aea.cli.utils.decorators import check_aea_project +from aea.cli.utils.package_utils import get_package_path_unified +from aea.configurations.base import ( + ConnectionConfig, + PublicId, +) +from aea.crypto.base import Crypto +from aea.crypto.registries import crypto_registry +from aea.exceptions import enforce +from aea.helpers.multiaddr.base import MultiAddr + + +URI_REGEX = re.compile(r"(?:https?://)?(?P[^:/ ]+):(?P[0-9]*)") + + +@click.command() +@click.argument( + "ledger_id", + metavar="TYPE", + type=click.Choice(list(crypto_registry.supported_ids)), + required=True, +) +@click.option("-c", "--connection", is_flag=True) +@click.option( + "-i", "--connection-id", type=PublicIdParameter(), required=False, default=None, +) +@click.option( + "-h", "--host-field", type=str, required=False, default=None, +) +@click.option( + "-p", "--port-field", type=str, required=False, default=None, +) +@click.option( + "-u", "--uri-field", type=str, required=False, default="public_uri", +) +@click.pass_context +@check_aea_project +def get_multiaddress( + click_context, + ledger_id: str, + connection: bool, + connection_id: Optional[PublicId], + host_field: str, + port_field: str, + uri_field: str, +): + """Get the multiaddress associated with a private key or connection.""" + address = _try_get_multiaddress( + click_context, + ledger_id, + connection, + connection_id, + host_field, + port_field, + uri_field, + ) + click.echo(address) + + +def _try_get_multiaddress( + click_context, + ledger_id: str, + is_connection: bool, + connection_id: Optional[PublicId], + host_field: str, + port_field: str, + uri_field: str, +): + """ + Try to get the multi-address. + + :param click_context: click context object. + :param ledger_id: the ledger id. + :param is_connection: whether the key to load is from the wallet or from connections. + :param connection_id: the connection id. + :param host_field: if connection_id specified, the config field to retrieve the host + :param port_field: if connection_id specified, the config field to retrieve the port + + :return: address. + """ + ctx = cast(Context, click_context.obj) + # connection_id not None implies is_connection + is_connection = connection_id is not None or is_connection + + private_key_paths = ( + ctx.agent_config.private_key_paths + if not is_connection + else ctx.agent_config.connection_private_key_paths + ) + private_key_path = private_key_paths.read(ledger_id) + + if private_key_path is None: + raise ClickException( + f"Cannot find '{ledger_id}'. Please check {'private_key_path' if not is_connection else 'connection_private_key_paths'}." + ) + + path_to_key = Path(private_key_path) + crypto = crypto_registry.make(ledger_id, private_key_path=path_to_key) + + if connection_id is None: + return _try_get_peerid(crypto) + return _try_get_connection_multiaddress( + click_context, + crypto, + cast(PublicId, connection_id), + host_field, + port_field, + uri_field, + ) + + +def _try_get_peerid(crypto: Crypto) -> str: + """Try to get the peer id.""" + try: + peer_id = MultiAddr("", 0, crypto.public_key).peer_id + return peer_id + except Exception as e: + raise ClickException(str(e)) + + +def _read_host_and_port_from_config( + connection_config: ConnectionConfig, + uri_field: str, + host_field: Optional[str], + port_field: Optional[str], +) -> Tuple[str, int]: + """ + Read host and port from config connection. + + :param host_field: the host field. + :param port_field: the port field. + :param uri_field: the uri field. + :return: the host and the port. + """ + host_is_none = host_field is None + port_is_none = port_field is None + one_is_none = (not host_is_none and port_is_none) or ( + host_is_none and not port_is_none + ) + if not host_is_none and not port_is_none: + if host_field not in connection_config.config: + raise ClickException( + f"Host field '{host_field}' not present in connection configuration {connection_config.public_id}" + ) + if port_field not in connection_config.config: + raise ClickException( + f"Port field '{port_field}' not present in connection configuration {connection_config.public_id}" + ) + host = connection_config.config[host_field] + port = int(connection_config.config[port_field]) + return host, port + if one_is_none: + raise ClickException( + "-h/--host-field and -p/--port-field must be specified together." + ) + if uri_field not in connection_config.config: + raise ClickException( + f"URI field '{uri_field}' not present in connection configuration {connection_config.public_id}" + ) + url_value = connection_config.config[uri_field] + try: + m = URI_REGEX.search(url_value) + enforce(m is not None, f"URI Doesn't match regex '{URI_REGEX}'") + m = cast(typing.Match, m) + host = m.group("host") + port = int(m.group("port")) + return host, port + except Exception as e: + raise ClickException( + f"Cannot extract host and port from {uri_field}: '{url_value}'. Reason: {str(e)}" + ) + + +def _try_get_connection_multiaddress( + click_context, + crypto: Crypto, + connection_id: PublicId, + host_field: Optional[str], + port_field: Optional[str], + uri_field: str, +) -> str: + """ + Try to get the connection multiaddress. + + The host and the port options have the precedence over the uri option. + + :param click_context: the click context object. + :param crypto: the crypto. + :param connection_id: the connection id. + :param host_field: the host field. + :param port_field: the port field. + :param uri_field: the uri field. + :return: the multiaddress. + """ + ctx = cast(Context, click_context.obj) + if connection_id not in ctx.agent_config.connections: + raise ValueError(f"Cannot find connection with the public id {connection_id}.") + + package_path = Path(get_package_path_unified(ctx, "connection", connection_id)) + connection_config = cast( + ConnectionConfig, load_item_config("connection", package_path) + ) + + host, port = _read_host_and_port_from_config( + connection_config, uri_field, host_field, port_field + ) + + try: + multiaddr = MultiAddr(host, port, crypto.public_key) + return multiaddr.format() + except Exception as e: + raise ClickException(f"An error occurred while creating the multiaddress: {e}") diff --git a/aea/cli/init.py b/aea/cli/init.py index 89f1af2422..693bb6761f 100644 --- a/aea/cli/init.py +++ b/aea/cli/init.py @@ -38,7 +38,9 @@ @click.option("--reset", is_flag=True, help="To reset the initialization.") @click.option("--local", is_flag=True, help="For init AEA locally.") @pass_ctx -def init(ctx: Context, author: str, reset: bool, local: bool): +def init( # pylint: disable=unused-argument + ctx: Context, author: str, reset: bool, local: bool +): """Initialize your AEA configurations.""" do_init(author, reset, not local) diff --git a/aea/cli/install.py b/aea/cli/install.py index a4023cb1a5..88ce64dfd5 100644 --- a/aea/cli/install.py +++ b/aea/cli/install.py @@ -30,7 +30,7 @@ from aea.cli.utils.decorators import check_aea_project from aea.cli.utils.loggers import logger from aea.configurations.base import Dependency -from aea.exceptions import AEAException +from aea.exceptions import AEAException, enforce @click.command() @@ -92,7 +92,7 @@ def _install_dependency(dependency_name: str, dependency: Dependency): if return_code == 1: # try a second time return_code = _run_install_subprocess(command) - assert return_code == 0, "Return code != 0." + enforce(return_code == 0, "Return code != 0.") except Exception as e: raise AEAException( "An error occurred while installing {}, {}: {}".format( @@ -136,7 +136,7 @@ def _install_from_requirement(file: str, install_timeout: float = 300) -> None: returncode = _run_install_subprocess( [sys.executable, "-m", "pip", "install", "-r", file], install_timeout ) - assert returncode == 0, "Return code != 0." + enforce(returncode == 0, "Return code != 0.") except Exception: raise AEAException( "An error occurred while installing requirement file {}. Stopping...".format( diff --git a/aea/cli/interact.py b/aea/cli/interact.py index d62ab576ef..19d1976f4d 100644 --- a/aea/cli/interact.py +++ b/aea/cli/interact.py @@ -27,6 +27,7 @@ from aea.cli.utils.decorators import check_aea_project from aea.cli.utils.exceptions import InterruptInputException +from aea.common import Address from aea.configurations.base import ( ConnectionConfig, DEFAULT_AEA_CONFIG_FILE, @@ -39,16 +40,17 @@ StubConnection, ) from aea.identity.base import Identity -from aea.mail.base import Envelope +from aea.mail.base import Envelope, Message from aea.multiplexer import InBox, Multiplexer, OutBox -from aea.protocols.default.dialogues import DefaultDialogues +from aea.protocols.default.dialogues import DefaultDialogue, DefaultDialogues from aea.protocols.default.message import DefaultMessage +from aea.protocols.dialogue.base import Dialogue as BaseDialogue @click.command() @click.pass_context @check_aea_project -def interact(click_context: click.core.Context): +def interact(click_context: click.core.Context): # pylint: disable=unused-argument """Interact with a running AEA via the stub connection.""" click.echo("Starting AEA interaction channel...") _run_interaction_channel() @@ -71,13 +73,25 @@ def _run_interaction_channel(): ) multiplexer = Multiplexer([stub_connection]) inbox = InBox(multiplexer) - outbox = OutBox(multiplexer, default_address=identity_stub.address) - dialogues = DefaultDialogues(identity_stub.name) + outbox = OutBox(multiplexer) + + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> BaseDialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message + + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return DefaultDialogue.Role.AGENT + + dialogues = DefaultDialogues(identity_stub.name, role_from_first_message) try: multiplexer.connect() while True: # pragma: no cover - _process_envelopes(agent_name, identity_stub, inbox, outbox, dialogues) + _process_envelopes(agent_name, inbox, outbox, dialogues) except KeyboardInterrupt: click.echo("Interaction interrupted!") @@ -88,37 +102,37 @@ def _run_interaction_channel(): def _process_envelopes( - agent_name: str, - identity_stub: Identity, - inbox: InBox, - outbox: OutBox, - dialogues: DefaultDialogues, + agent_name: str, inbox: InBox, outbox: OutBox, dialogues: DefaultDialogues, ) -> None: """ Process envelopes. :param agent_name: name of an agent. - :param identity_stub: stub identity. :param inbox: an inbox object. :param outbox: an outbox object. :param dialogues: the dialogues object. :return: None. """ - envelope = _try_construct_envelope(agent_name, identity_stub.name, dialogues) + envelope = _try_construct_envelope(agent_name, dialogues) if envelope is None: - if not inbox.empty(): - envelope = inbox.get_nowait() - assert envelope is not None, "Could not recover envelope from inbox." - click.echo(_construct_message("received", envelope)) - else: - click.echo("Received no new envelope!") + _check_for_incoming_envelope(inbox) else: outbox.put(envelope) click.echo(_construct_message("sending", envelope)) -def _construct_message(action_name, envelope): +def _check_for_incoming_envelope(inbox: InBox): + if not inbox.empty(): + envelope = inbox.get_nowait() + if envelope is None: + raise ValueError("Could not recover envelope from inbox.") + click.echo(_construct_message("received", envelope)) + else: + click.echo("Received no new envelope!") + + +def _construct_message(action_name: str, envelope: Envelope): action_name = action_name.title() msg = ( DefaultMessage.serializer.decode(envelope.message) @@ -135,7 +149,7 @@ def _construct_message(action_name, envelope): def _try_construct_envelope( - agent_name: str, sender: str, dialogues: DefaultDialogues + agent_name: str, dialogues: DefaultDialogues ) -> Optional[Envelope]: """Try construct an envelope from user input.""" envelope = None # type: Optional[Envelope] @@ -143,7 +157,7 @@ def _try_construct_envelope( performative_str = "bytes" performative = DefaultMessage.Performative(performative_str) click.echo( - "Provide message of protocol fetchai/default:0.4.0 for performative {}:".format( + "Provide message of protocol fetchai/default:0.5.0 for performative {}:".format( performative_str ) ) @@ -158,20 +172,11 @@ def _try_construct_envelope( message = message_decoded.encode("utf-8") # type: Union[str, bytes] else: message = message_escaped # pragma: no cover - dialogue_reference = dialogues.new_self_initiated_dialogue_reference() - msg = DefaultMessage( - performative=performative, - dialogue_reference=dialogue_reference, - content=message, + msg, _ = dialogues.create( + counterparty=agent_name, performative=performative, content=message, ) - msg.counterparty = agent_name - msg.sender = sender - assert dialogues.update(msg) is not None envelope = Envelope( - to=msg.counterparty, - sender=msg.sender, - protocol_id=msg.protocol_id, - message=msg, + to=msg.to, sender=msg.sender, protocol_id=msg.protocol_id, message=msg, ) except InterruptInputException: click.echo("Interrupting input, checking inbox ...") diff --git a/aea/cli/launch.py b/aea/cli/launch.py index b60c03aa78..a35458d39b 100644 --- a/aea/cli/launch.py +++ b/aea/cli/launch.py @@ -62,7 +62,7 @@ def _launch_agents( failed = _launch_threads(agents_directories) else: failed = _launch_subprocesses(click_context, agents_directories) - except BaseException: # pragma: no cover + except BaseException: # pragma: no cover # pylint: disable=broad-except logger.exception("Exception in launch agents.") failed = -1 finally: diff --git a/aea/cli/list.py b/aea/cli/list.py index 923b858940..a827e8c821 100644 --- a/aea/cli/list.py +++ b/aea/cli/list.py @@ -40,7 +40,7 @@ @click.group(name="list") @click.pass_context @check_aea_project -def list_command(click_context): +def list_command(click_context): # pylint: disable=unused-argument """List the installed resources.""" diff --git a/aea/cli/registry/add.py b/aea/cli/registry/add.py index 271b7e826f..6b3b3aae32 100644 --- a/aea/cli/registry/add.py +++ b/aea/cli/registry/add.py @@ -59,7 +59,7 @@ def fetch_package(obj_type: str, public_id: PublicId, cwd: str, dest: str) -> Pa filepath = download_file(file_url, cwd) # next code line is needed because the items are stored in tarball packages as folders - dest = os.path.split(dest)[0] # TODO: replace this hotfix with a proper solution + dest = os.path.split(dest)[0] logger.debug( "Extracting {obj_type} {public_id}...".format( public_id=public_id, obj_type=obj_type diff --git a/aea/cli/registry/registration.py b/aea/cli/registry/registration.py index e2da0dfcaf..2f6207c0e6 100644 --- a/aea/cli/registry/registration.py +++ b/aea/cli/registry/registration.py @@ -61,5 +61,4 @@ def register( raise ClickException( "Errors occured during registration.\n" + "\n".join(errors) ) - else: - return resp_json["key"] + return resp_json["key"] diff --git a/aea/cli/registry/utils.py b/aea/cli/registry/utils.py index 70b181e633..323fdef364 100644 --- a/aea/cli/registry/utils.py +++ b/aea/cli/registry/utils.py @@ -72,8 +72,7 @@ def request_api( "Unable to read authentication config. " 'Please sign in with "aea login" command.' ) - else: - headers.update({"Authorization": "Token {}".format(token)}) + headers.update({"Authorization": "Token {}".format(token)}) request_kwargs = dict( method=method, @@ -116,8 +115,7 @@ def request_api( ) if return_code: return resp_json, resp.status_code - else: - return resp_json + return resp_json def download_file(url: str, cwd: str) -> str: diff --git a/aea/cli/remove.py b/aea/cli/remove.py index 8ae5acd86d..7e8243d7d7 100644 --- a/aea/cli/remove.py +++ b/aea/cli/remove.py @@ -35,7 +35,7 @@ @click.group() @click.pass_context @check_aea_project -def remove(click_context): +def remove(click_context): # pylint: disable=unused-argument """Remove a resource from the agent.""" @@ -128,17 +128,16 @@ def remove_item(ctx: Context, item_type: str, item_id: PublicId) -> None: raise click.ClickException( "{} {} not found. Aborting.".format(item_type.title(), item_name) ) - elif ( + if ( item_folder.exists() and not ctx.agent_config.author == item_id.author ): # pragma: no cover raise click.ClickException( "{} {} author is different from {} agent author. " "Please fix the author field.".format(item_name, item_type, agent_name) ) - else: - logger.debug( - "Removing local {} {}.".format(item_type, item_name) - ) # pragma: no cover + logger.debug( + "Removing local {} {}.".format(item_type, item_name) + ) # pragma: no cover try: shutil.rmtree(item_folder) diff --git a/aea/cli/run.py b/aea/cli/run.py index 28dae32151..1d7e9bb160 100644 --- a/aea/cli/run.py +++ b/aea/cli/run.py @@ -91,7 +91,9 @@ def run_aea( aea = _build_aea(connection_ids, skip_consistency_check) click.echo(AEA_LOGO + "v" + __version__ + "\n") - click.echo("Starting AEA '{}' in '{}' mode...".format(aea.name, aea.loop_mode)) + click.echo( + "Starting AEA '{}' in '{}' mode...".format(aea.name, aea.runtime.loop_mode) + ) try: aea.start() except KeyboardInterrupt: # pragma: no cover @@ -130,6 +132,4 @@ def _build_aea( except AEAPackageLoadingError as e: # pragma: nocover raise click.ClickException("Package loading error: {}".format(str(e))) except Exception as e: - # TODO use an ad-hoc exception class for predictable errors - # all the other exceptions should be logged with ClickException raise click.ClickException(str(e)) diff --git a/aea/cli/scaffold.py b/aea/cli/scaffold.py index e8690a6f4d..48fdab355b 100644 --- a/aea/cli/scaffold.py +++ b/aea/cli/scaffold.py @@ -44,7 +44,7 @@ @click.group() @click.pass_context @check_aea_project -def scaffold(click_context): +def scaffold(click_context): # pylint: disable=unused-argument """Scaffold a resource for the agent.""" @@ -146,18 +146,19 @@ def scaffold_item(ctx: Context, item_type: str, item_name: str) -> None: "Registering the {} into {}".format(item_type, DEFAULT_AEA_CONFIG_FILE) ) existing_ids.add(PublicId(author_name, item_name, DEFAULT_VERSION)) - ctx.agent_loader.dump( - ctx.agent_config, open(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w") - ) + with open(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w") as fp: + ctx.agent_loader.dump(ctx.agent_config, fp) # ensure the name in the yaml and the name of the folder are the same config_filepath = Path( ctx.cwd, item_type_plural, item_name, default_config_filename ) - config = loader.load(config_filepath.open()) + with config_filepath.open() as fp: + config = loader.load(fp) config.name = item_name config.author = author_name - loader.dump(config, open(config_filepath, "w")) + with config_filepath.open("w") as fp: + loader.dump(config, fp) except ValidationError: raise click.ClickException( @@ -169,7 +170,7 @@ def scaffold_item(ctx: Context, item_type: str, item_name: str) -> None: def _scaffold_dm_handler(ctx: Context): """Add a scaffolded decision maker handler to the project and configuration.""" - existing_dm_handler = getattr(ctx.agent_config, "decision_maker_handler") + existing_dm_handler = ctx.agent_config.decision_maker_handler # check if we already have a decision maker in the project if existing_dm_handler != {}: diff --git a/aea/cli/search.py b/aea/cli/search.py index 9246e9e88d..6c37f58d4c 100644 --- a/aea/cli/search.py +++ b/aea/cli/search.py @@ -120,9 +120,6 @@ def setup_search_ctx(ctx: Context, local: bool) -> None: # otherwise, use the default path (i.e. 'packages/' in the current directory.) try: try_to_load_agent_config(ctx, is_exit_on_except=False) - # path = Path(DEFAULT_AEA_CONFIG_FILE) - # fp = open(str(path), mode="r", encoding="utf-8") - # agent_config = ctx.agent_loader.load(fp) registry_directory = ctx.agent_config.registry_path except Exception: # pylint: disable=broad-except registry_directory = os.path.join(ctx.cwd, DEFAULT_REGISTRY_PATH) @@ -215,10 +212,7 @@ def search_items(ctx: Context, item_type: str, query: str) -> List: item_type_plural = item_type + "s" if ctx.config.get("is_local"): return _search_items_locally(ctx, item_type_plural) - else: - return request_api( - "GET", "/{}".format(item_type_plural), params={"search": query} - ) + return request_api("GET", "/{}".format(item_type_plural), params={"search": query}) def _output_search_results(item_type: str, results: List[Dict]) -> None: diff --git a/aea/cli/utils/click_utils.py b/aea/cli/utils/click_utils.py index 265851b229..5af58b86b7 100644 --- a/aea/cli/utils/click_utils.py +++ b/aea/cli/utils/click_utils.py @@ -68,13 +68,13 @@ def arg_strip(s): class PublicIdParameter(click.ParamType): """Define a public id parameter for Click applications.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs): # pylint: disable=useless-super-delegation """ Initialize the Public Id parameter. Just forwards arguments to parent constructor. """ - super().__init__(*args, **kwargs) # pylint: disable=useless-super-delegation + super().__init__(*args, **kwargs) def get_metavar(self, param): """Return the metavar default for this param if it provides one.""" diff --git a/aea/cli/utils/decorators.py b/aea/cli/utils/decorators.py index 05c227f790..20a41f367d 100644 --- a/aea/cli/utils/decorators.py +++ b/aea/cli/utils/decorators.py @@ -39,7 +39,7 @@ _get_default_configuration_file_name_from_type, ) from aea.configurations.loader import ConfigLoaders -from aea.exceptions import AEAException +from aea.exceptions import AEAException, enforce pass_ctx = click.make_pass_decorator(Context) @@ -77,14 +77,15 @@ def _validate_config_consistency(ctx: Context): ) is_vendor = True # we fail if none of the two alternative works. - assert package_directory.exists(), "Package directory does not exist!" + enforce(package_directory.exists(), "Package directory does not exist!") loader = ConfigLoaders.from_package_type(item_type) config_file_name = _get_default_configuration_file_name_from_type(item_type) configuration_file_path = package_directory / config_file_name - assert ( - configuration_file_path.exists() - ), "Configuration file path does not exist!" + enforce( + configuration_file_path.exists(), + "Configuration file path does not exist!", + ) except Exception: raise ValueError("Cannot find {}: '{}'".format(item_type.value, public_id)) @@ -155,13 +156,12 @@ def _cast_ctx(context: Union[Context, click.core.Context]) -> Context: """ if isinstance(context, Context): return context - elif isinstance(context, click.core.Context): + if isinstance(context, click.core.Context): return cast(Context, context.obj) - else: # pragma: no cover - raise AEAException( - "clean_after decorator should be used only on methods with Context " - "or click.core.Context object as a first argument." - ) + raise AEAException( # pragma: no cover + "clean_after decorator should be used only on methods with Context " + "or click.core.Context object as a first argument." + ) def clean_after(func: Callable) -> Callable: diff --git a/aea/cli/utils/formatting.py b/aea/cli/utils/formatting.py index e4f09e5f4c..9df1724790 100644 --- a/aea/cli/utils/formatting.py +++ b/aea/cli/utils/formatting.py @@ -23,6 +23,7 @@ from aea.configurations.base import AgentConfig from aea.configurations.loader import ConfigLoader +from aea.exceptions import enforce def format_items(items): @@ -52,7 +53,7 @@ def retrieve_details(name: str, loader: ConfigLoader, config_filepath: str) -> D """Return description of a protocol, skill, connection.""" config = loader.load(open(str(config_filepath))) item_name = config.agent_name if isinstance(config, AgentConfig) else config.name - assert item_name == name, "Item names do not match!" + enforce(item_name == name, "Item names do not match!") return { "public_id": str(config.public_id), "name": item_name, diff --git a/aea/cli/utils/generic.py b/aea/cli/utils/generic.py index 9daee43c26..8e4ba55a10 100644 --- a/aea/cli/utils/generic.py +++ b/aea/cli/utils/generic.py @@ -50,8 +50,7 @@ def get_parent_object(obj: Dict, dotted_path: List[str]): # if we are not at the last step and the attribute value is not a dictionary, fail. if isinstance(current_object, dict): return current_object - else: - raise ValueError("The target object is not a dictionary.") + raise ValueError("The target object is not a dictionary.") def load_yaml(filepath: str) -> Dict: diff --git a/aea/cli/utils/loggers.py b/aea/cli/utils/loggers.py index dc8fa65779..237315f396 100644 --- a/aea/cli/utils/loggers.py +++ b/aea/cli/utils/loggers.py @@ -74,7 +74,7 @@ def simple_verbosity_option( kwargs.setdefault("is_eager", True) def decorator(f): - def _set_level(ctx, param, value): + def _set_level(ctx, param, value): # pylint: disable=unused-argument level = logging.getLevelName(value) logger.setLevel(level) # save verbosity option so it can be diff --git a/aea/cli/utils/package_utils.py b/aea/cli/utils/package_utils.py index e199097ca9..32d0727958 100644 --- a/aea/cli/utils/package_utils.py +++ b/aea/cli/utils/package_utils.py @@ -168,8 +168,30 @@ def get_package_path( return os.path.join( ctx.cwd, "vendor", public_id.author, item_type_plural, public_id.name ) - else: - return os.path.join(ctx.cwd, item_type_plural, public_id.name) + return os.path.join(ctx.cwd, item_type_plural, public_id.name) + + +def get_package_path_unified(ctx: Context, item_type: str, public_id: PublicId) -> str: + """ + Get a path for a package, either vendor or not. + + That is: + - if the author in the public id is not the same of the AEA project author, + just look into vendor/ + - Otherwise, first look into local packages, then into vendor/. + + :param ctx: context. + :param item_type: item type. + :param public_id: item public ID. + + :return: vendorized estenation path for package. + """ + vendor_path = get_package_path(ctx, item_type, public_id, is_vendor=True) + if ctx.agent_config.author != public_id.author or not is_item_present( + ctx, item_type, public_id, is_vendor=False + ): + return vendor_path + return get_package_path(ctx, item_type, public_id, is_vendor=False) def copy_package_directory(src: Path, dst: str) -> Path: @@ -244,7 +266,9 @@ def find_item_locally(ctx, item_type, item_public_id) -> Path: return package_path -def find_item_in_distribution(ctx, item_type, item_public_id) -> Path: +def find_item_in_distribution( # pylint: disable=unused-argument + ctx, item_type, item_public_id +) -> Path: """ Find an item in the AEA directory. @@ -369,6 +393,28 @@ def register_item(ctx: Context, item_type: str, item_public_id: PublicId) -> Non ) +def is_item_present_unified(ctx: Context, item_type: str, item_public_id: PublicId): + """ + Check if item is present, either vendor or not. + + That is: + - if the author in the public id is not the same of the AEA project author, + just look into vendor/ + - Otherwise, first look into local packages, then into vendor/. + + :param ctx: context object. + :param item_type: type of an item. + :param item_public_id: PublicId of an item. + :return: True if the item is present, False otherwise. + """ + is_in_vendor = is_item_present(ctx, item_type, item_public_id, is_vendor=True) + if item_public_id.author != ctx.agent_config.author: + return is_in_vendor + return is_in_vendor or is_item_present( + ctx, item_type, item_public_id, is_vendor=False + ) + + def is_item_present( ctx: Context, item_type: str, item_public_id: PublicId, is_vendor: bool = True ) -> bool: @@ -393,7 +439,9 @@ def is_item_present( ).exists() -def try_get_balance(agent_config: AgentConfig, wallet: Wallet, type_: str) -> int: +def try_get_balance( # pylint: disable=unused-argument + agent_config: AgentConfig, wallet: Wallet, type_: str +) -> int: """ Try to get wallet balance. @@ -411,5 +459,5 @@ def try_get_balance(agent_config: AgentConfig, wallet: Wallet, type_: str) -> in if balance is None: # pragma: no cover raise ValueError("No balance returned!") return balance - except (AssertionError, ValueError) as e: # pragma: no cover + except ValueError as e: # pragma: no cover raise click.ClickException(str(e)) diff --git a/aea/cli_gui/__init__.py b/aea/cli_gui/__init__.py index ce23c16bd7..60db0067a7 100644 --- a/aea/cli_gui/__init__.py +++ b/aea/cli_gui/__init__.py @@ -107,7 +107,7 @@ def get_agents() -> List[Dict]: "public_id": tail, # it is not a public_id actually, just a folder name. # the reason it's called here so is the view that is used to represent items with public_ids # used also for agent displaying - # TODO: change it when we will have a separate view for an agent. + # change it when we will have a separate view for an agent. "description": "placeholder description", } ) @@ -199,10 +199,10 @@ def add_item(agent_id: str, item_type: str, item_id: str): def fetch_agent(agent_id: str): """Fetch an agent.""" ctx = Context(cwd=app_context.agents_dir) - fetch_agent = cli_fetch_agent_locally if app_context.local else cli_fetch_agent + fetch_agent_ = cli_fetch_agent_locally if app_context.local else cli_fetch_agent try: agent_public_id = PublicId.from_str(agent_id) - fetch_agent(ctx, agent_public_id) + fetch_agent_(ctx, agent_public_id) except ClickException as e: return ( {"detail": "Failed to fetch an agent {}. {}".format(agent_id, str(e))}, @@ -329,28 +329,24 @@ def start_agent(agent_id: str, connection_id: PublicId): {"detail": "Failed to run agent {}".format(agent_id)}, 400, ) # 400 Bad request - else: - app_context.agent_processes[agent_id] = agent_process - app_context.agent_tty[agent_id] = [] - app_context.agent_error[agent_id] = [] - - tty_read_thread = threading.Thread( - target=read_tty, - args=( - app_context.agent_processes[agent_id], - app_context.agent_tty[agent_id], - ), - ) - tty_read_thread.start() - - error_read_thread = threading.Thread( - target=read_error, - args=( - app_context.agent_processes[agent_id], - app_context.agent_error[agent_id], - ), - ) - error_read_thread.start() + app_context.agent_processes[agent_id] = agent_process + app_context.agent_tty[agent_id] = [] + app_context.agent_error[agent_id] = [] + + tty_read_thread = threading.Thread( + target=read_tty, + args=(app_context.agent_processes[agent_id], app_context.agent_tty[agent_id],), + ) + tty_read_thread.start() + + error_read_thread = threading.Thread( + target=read_error, + args=( + app_context.agent_processes[agent_id], + app_context.agent_error[agent_id], + ), + ) + error_read_thread.start() return agent_id, 201 # 200 (OK) diff --git a/aea/cli_gui/utils.py b/aea/cli_gui/utils.py index 4fb9143b97..d51f79a1c4 100644 --- a/aea/cli_gui/utils.py +++ b/aea/cli_gui/utils.py @@ -66,8 +66,7 @@ def is_agent_dir(dir_name: str) -> bool: """Return true if this directory contains an AEA project (an agent).""" if not os.path.isdir(dir_name): return False - else: - return os.path.isfile(os.path.join(dir_name, "aea-config.yaml")) + return os.path.isfile(os.path.join(dir_name, "aea-config.yaml")) def call_aea_async(param_list: List[str], dir_arg: str) -> subprocess.Popen: @@ -165,12 +164,12 @@ def get_process_status(process_id: subprocess.Popen) -> ProcessState: :param process_id: the process id """ - assert process_id is not None, "Process id cannot be None!" + if process_id is None: # pragma: nocover + raise ValueError("Process id cannot be None!") return_code = process_id.poll() if return_code is None: return ProcessState.RUNNING - elif return_code <= 0: + if return_code <= 0: return ProcessState.FINISHED - else: - return ProcessState.FAILED + return ProcessState.FAILED diff --git a/tests/test_packages/test_connections/test_p2p_client/__init__.py b/aea/common.py similarity index 84% rename from tests/test_packages/test_connections/test_p2p_client/__init__.py rename to aea/common.py index 3a13cf4126..907be61ca3 100644 --- a/tests/test_packages/test_connections/test_p2p_client/__init__.py +++ b/aea/common.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # ------------------------------------------------------------------------------ # -# Copyright 2018-2020 Fetch.AI Limited +# 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. @@ -16,5 +16,6 @@ # limitations under the License. # # ------------------------------------------------------------------------------ +"""This module contains the common types and interfaces used in the aea framework.""" -"""This module contains the tests of the p2p_client connection implementation.""" +Address = str diff --git a/aea/components/base.py b/aea/components/base.py index 0455e736f2..08e906fe27 100644 --- a/aea/components/base.py +++ b/aea/components/base.py @@ -18,11 +18,13 @@ # ------------------------------------------------------------------------------ """This module contains definitions of agent components.""" +import importlib.util import logging +import sys import types from abc import ABC from pathlib import Path -from typing import Dict, Optional +from typing import Optional from aea.configurations.base import ( ComponentConfiguration, @@ -30,6 +32,7 @@ ComponentType, PublicId, ) +from aea.exceptions import AEAEnforceError from aea.helpers.logging import WithLogger logger = logging.getLogger(__name__) @@ -42,7 +45,7 @@ def __init__( self, configuration: Optional[ComponentConfiguration] = None, is_vendor: bool = False, - **kwargs + **kwargs, ): """ Initialize a package. @@ -55,10 +58,6 @@ def __init__( self._directory = None # type: Optional[Path] self._is_vendor = is_vendor - # mapping from import path to module object - # the keys are dotted paths of Python modules. - self.importpath_to_module = {} # type: Dict[str, types.ModuleType] - @property def component_type(self) -> ComponentType: """Get the component type.""" @@ -87,19 +86,59 @@ def public_id(self) -> PublicId: @property def configuration(self) -> ComponentConfiguration: """Get the component configuration.""" - assert ( - self._configuration is not None - ), "The component is not associated with a configuration." + if self._configuration is None: # pragma: nocover + raise ValueError("The component is not associated with a configuration.") return self._configuration @property def directory(self) -> Path: """Get the directory. Raise error if it has not been set yet.""" - assert self._directory is not None, "Directory not set yet." + if self._directory is None: + raise ValueError("Directory not set yet.") return self._directory @directory.setter def directory(self, path: Path) -> None: """Set the directory. Raise error if already set.""" - assert self._directory is None, "Directory already set." + if self._directory is not None: # pragma: nocover + raise ValueError("Directory already set.") self._directory = path + + +def load_aea_package(configuration: ComponentConfiguration) -> None: + """ + Load the AEA package. + + It adds all the __init__.py modules into `sys.modules`. + + :param configuration: the configuration object. + :return: None + """ + dir_ = configuration.directory + if dir_ is None: # pragma: nocover + raise AEAEnforceError("configuration directory does not exists.") + + # patch sys.modules with dummy modules + prefix_root = "packages" + prefix_author = prefix_root + f".{configuration.author}" + prefix_pkg_type = prefix_author + f".{configuration.component_type.to_plural()}" + prefix_pkg = prefix_pkg_type + f".{configuration.name}" + sys.modules[prefix_root] = types.ModuleType(prefix_root) + sys.modules[prefix_author] = types.ModuleType(prefix_author) + sys.modules[prefix_pkg_type] = types.ModuleType(prefix_pkg_type) + + for subpackage_init_file in dir_.rglob("__init__.py"): + parent_dir = subpackage_init_file.parent + relative_parent_dir = parent_dir.relative_to(dir_) + if relative_parent_dir == Path("."): + # this handles the case when 'subpackage_init_file' + # is path/to/package/__init__.py + import_path = prefix_pkg + else: + import_path = prefix_pkg + "." + ".".join(relative_parent_dir.parts) + + spec = importlib.util.spec_from_file_location(import_path, subpackage_init_file) + module = importlib.util.module_from_spec(spec) + sys.modules[import_path] = module + logger.debug(f"loading {import_path}: {module}") + spec.loader.exec_module(module) # type: ignore diff --git a/aea/components/loader.py b/aea/components/loader.py index 24c0258255..f53e92f8a9 100644 --- a/aea/components/loader.py +++ b/aea/components/loader.py @@ -29,7 +29,7 @@ ) from aea.connections.base import Connection from aea.contracts.base import Contract -from aea.exceptions import AEAPackageLoadingError +from aea.exceptions import AEAPackageLoadingError, enforce from aea.protocols.base import Protocol from aea.skills.base import Skill @@ -107,7 +107,7 @@ def _handle_error_while_loading_component_module_not_found( def get_new_error_message_no_package_found() -> str: """Create a new error message in case the package is not found.""" - assert nb_parts <= 4, "More than 4 parts!" + enforce(nb_parts <= 4, "More than 4 parts!") author = parts[1] new_message = "No AEA package found with author name '{}'".format(author) @@ -127,7 +127,7 @@ def get_new_error_message_no_package_found() -> str: def get_new_error_message_with_package_found() -> str: """Create a new error message in case the package is found.""" - assert nb_parts >= 5, "Less than 5 parts!" + enforce(nb_parts >= 5, "Less than 5 parts!") author, pkg_name, pkg_type = parts[:3] the_rest = ".".join(parts[4:]) return "The package '{}/{}' of type '{}' exists, but cannot find module '{}'".format( diff --git a/aea/configurations/base.py b/aea/configurations/base.py index 84377d69cb..dbd1612f60 100644 --- a/aea/configurations/base.py +++ b/aea/configurations/base.py @@ -26,6 +26,7 @@ import re from abc import ABC, abstractmethod from collections import OrderedDict +from copy import deepcopy from enum import Enum from pathlib import Path from typing import ( @@ -50,7 +51,8 @@ import semver -import aea +from aea.__version__ import __version__ as __aea_version__ +from aea.exceptions import enforce from aea.helpers.ipfs.base import IPFSHashOnly T = TypeVar("T") @@ -76,7 +78,6 @@ "contract.yaml", ] -# TODO implement a proper class to represent this type. Dependency = dict """ A dependency is a dictionary with the following (optional) keys: @@ -128,6 +129,17 @@ def to_plural(self) -> str: """ return self.value + "s" + def configuration_class(self) -> Type["PackageConfiguration"]: + """Get the configuration class.""" + d: Dict[PackageType, Type["PackageConfiguration"]] = { + PackageType.AGENT: AgentConfig, + PackageType.PROTOCOL: ProtocolConfig, + PackageType.CONNECTION: ConnectionConfig, + PackageType.CONTRACT: ContractConfig, + PackageType.SKILL: SkillConfig, + } + return d[self] + def __str__(self): """Convert to string.""" return str(self.value) @@ -140,18 +152,17 @@ def _get_default_configuration_file_name_from_type( item_type = PackageType(item_type) if item_type == PackageType.AGENT: return DEFAULT_AEA_CONFIG_FILE - elif item_type == PackageType.PROTOCOL: + if item_type == PackageType.PROTOCOL: return DEFAULT_PROTOCOL_CONFIG_FILE - elif item_type == PackageType.CONNECTION: + if item_type == PackageType.CONNECTION: return DEFAULT_CONNECTION_CONFIG_FILE - elif item_type == PackageType.SKILL: + if item_type == PackageType.SKILL: return DEFAULT_SKILL_CONFIG_FILE - elif item_type == PackageType.CONTRACT: + if item_type == PackageType.CONTRACT: return DEFAULT_CONTRACT_CONFIG_FILE - else: - raise ValueError( - "Item type not valid: {}".format(str(item_type)) - ) # pragma: no cover + raise ValueError( # pragma: no cover + "Item type not valid: {}".format(str(item_type)) + ) class ComponentType(Enum): @@ -238,7 +249,7 @@ def ordered_json(self) -> OrderedDict: # parse all the known keys. This might ignore some keys in the dictionary. seen_keys = set() for key in self._key_order: - assert key not in result, "Key in results!" + enforce(key not in result, "Key in results!") value = data.get(key) if value is not None: result[key] = value @@ -269,8 +280,7 @@ def create(self, item_id: str, item: T) -> None: """ if item_id in self._items_by_id: raise ValueError("Item with name {} already present!".format(item_id)) - else: - self._items_by_id[item_id] = item + self._items_by_id[item_id] = item def read(self, item_id: str) -> Optional[T]: """ @@ -344,10 +354,9 @@ def __init__(self, author: str, name: str, version: PackageVersionLike): def _process_version(version_like: PackageVersionLike) -> Tuple[Any, Any]: if isinstance(version_like, str): return version_like, semver.VersionInfo.parse(version_like) - elif isinstance(version_like, semver.VersionInfo): + if isinstance(version_like, semver.VersionInfo): return str(version_like), version_like - else: - raise ValueError("Version type not valid.") + raise ValueError("Version type not valid.") @property def author(self) -> str: @@ -396,11 +405,10 @@ def from_str(cls, public_id_string: str) -> "PublicId": raise ValueError( "Input '{}' is not well formatted.".format(public_id_string) ) - else: - username, package_name, version = re.findall( - cls.PUBLIC_ID_REGEX, public_id_string - )[0][:3] - return PublicId(username, package_name, version) + username, package_name, version = re.findall( + cls.PUBLIC_ID_REGEX, public_id_string + )[0][:3] + return PublicId(username, package_name, version) @classmethod def from_uri_path(cls, public_id_uri_path: str) -> "PublicId": @@ -424,11 +432,10 @@ def from_uri_path(cls, public_id_uri_path: str) -> "PublicId": raise ValueError( "Input '{}' is not well formatted.".format(public_id_uri_path) ) - else: - username, package_name, version = re.findall( - cls.PUBLIC_ID_URI_REGEX, public_id_uri_path - )[0][:3] - return PublicId(username, package_name, version) + username, package_name, version = re.findall( + cls.PUBLIC_ID_URI_REGEX, public_id_uri_path + )[0][:3] + return PublicId(username, package_name, version) @property def to_uri_path(self) -> str: @@ -498,17 +505,27 @@ def __lt__(self, other): and self.name == other.name ): return self.version_info < other.version_info - else: - raise ValueError( - "The public IDs {} and {} cannot be compared. Their author or name attributes are different.".format( - self, other - ) + raise ValueError( + "The public IDs {} and {} cannot be compared. Their author or name attributes are different.".format( + self, other ) + ) class PackageId: """A package identifier.""" + PACKAGE_TYPE_REGEX = r"({}|{}|{}|{}|{})".format( + PackageType.AGENT, + PackageType.PROTOCOL, + PackageType.SKILL, + PackageType.CONNECTION, + PackageType.CONTRACT, + ) + PACKAGE_ID_URI_REGEX = r"{}/{}".format( + PACKAGE_TYPE_REGEX, PublicId.PUBLIC_ID_URI_REGEX[1:-1] + ) + def __init__(self, package_type: Union[PackageType, str], public_id: PublicId): """ Initialize the package id. @@ -549,6 +566,44 @@ def package_prefix(self) -> Tuple[PackageType, str, str]: """Get the package identifier without the version.""" return self.package_type, self.author, self.name + @classmethod + def from_uri_path(cls, package_id_uri_path: str) -> "PackageId": + """ + Initialize the public id from the string. + + >>> str(PackageId.from_uri_path("skill/author/package_name/0.1.0")) + '(skill, author/package_name:0.1.0)' + + A bad formatted input raises value error: + >>> PackageId.from_uri_path("very/bad/formatted:input") + Traceback (most recent call last): + ... + ValueError: Input 'very/bad/formatted:input' is not well formatted. + + :param public_id_uri_path: the public id in uri path string format. + :return: the public id object. + :raises ValueError: if the string in input is not well formatted. + """ + if not re.match(cls.PACKAGE_ID_URI_REGEX, package_id_uri_path): + raise ValueError( + "Input '{}' is not well formatted.".format(package_id_uri_path) + ) + package_type_str, username, package_name, version = re.findall( + cls.PACKAGE_ID_URI_REGEX, package_id_uri_path + )[0][:4] + package_type = PackageType(package_type_str) + public_id = PublicId(username, package_name, version) + return PackageId(package_type, public_id) + + @property + def to_uri_path(self) -> str: + """ + Turn the package id into a uri path string. + + :return: uri path string + """ + return f"{str(self.package_type)}/{self.author}/{self.name}/{self.version}" + def __hash__(self): """Get the hash.""" return hash((self.package_type, self.public_id)) @@ -636,6 +691,8 @@ class PackageConfiguration(Configuration, ABC): """ default_configuration_filename: str + package_type: PackageType + configurable_fields: Set[str] = set() def __init__( self, @@ -662,9 +719,8 @@ def __init__( :param fingerprint_ignore_patterns: a list of file patterns to ignore files to fingerprint. """ super().__init__() - assert ( - name is not None and author is not None - ), "Name and author must be set on the configuration!" + if name is None or author is None: # pragma: nocover + raise ValueError("Name and author must be set on the configuration!") self.name = name self.author = author self.version = version if version != "" else DEFAULT_VERSION @@ -675,7 +731,7 @@ def __init__( if fingerprint_ignore_patterns is not None else [] ) - self.aea_version = aea_version if aea_version != "" else aea.__version__ + self.aea_version = aea_version if aea_version != "" else __aea_version__ self._aea_version_specifiers = self._parse_aea_version_specifier(aea_version) self._directory = None # type: Optional[Path] @@ -688,7 +744,8 @@ def directory(self) -> Optional[Path]: @directory.setter def directory(self, directory: Path) -> None: """Set directory if not already set.""" - assert self._directory is None, "Directory already set" + if self._directory is not None: # pragma: nocover + raise ValueError("Directory already set") self._directory = directory @staticmethod @@ -719,6 +776,8 @@ def package_dependencies(self) -> Set[ComponentId]: class ComponentConfiguration(PackageConfiguration, ABC): """Class to represent an agent component configuration.""" + package_type: PackageType + def __init__( self, name: str, @@ -748,9 +807,9 @@ def pypi_dependencies(self) -> Dependencies: return self._pypi_dependencies @property - @abstractmethod def component_type(self) -> ComponentType: """Get the component type.""" + return ComponentType(self.package_type.value) @property def component_id(self) -> ComponentId: @@ -769,63 +828,6 @@ def is_abstract_component(self) -> bool: """Check whether the component is abstract.""" return False - @staticmethod - def load( - component_type: ComponentType, - directory: Path, - skip_consistency_check: bool = False, - ) -> "ComponentConfiguration": - """ - Load configuration and check that it is consistent against the directory. - - :param component_type: the component type. - :param directory: the root of the package - :param skip_consistency_check: if True, the consistency check are skipped. - :return: the configuration object. - """ - configuration_object = ComponentConfiguration._load_configuration_object( - component_type, directory - ) - if not skip_consistency_check: - configuration_object._check_configuration_consistency( # pylint: disable=protected-access - directory - ) - return configuration_object - - @staticmethod - def _load_configuration_object( - component_type: ComponentType, directory: Path - ) -> "ComponentConfiguration": - """ - Load the configuration object, without consistency checks. - - :param component_type: the component type. - :param directory: the directory of the configuration. - :return: the configuration object. - :raises FileNotFoundError: if the configuration file is not found. - """ - from aea.configurations.loader import ( # pylint: disable=import-outside-toplevel - ConfigLoader, - ) - - configuration_loader = ConfigLoader.from_configuration_type( - component_type.to_configuration_type() - ) - configuration_filename = ( - configuration_loader.configuration_class.default_configuration_filename - ) - configuration_filepath = directory / configuration_filename - try: - fp = open(configuration_filepath) - configuration_object = configuration_loader.load(fp) - except FileNotFoundError: - raise FileNotFoundError( - "{} configuration not found: {}".format( - component_type.value.capitalize(), configuration_filepath - ) - ) - return configuration_object - def _check_configuration_consistency(self, directory: Path): """Check that the configuration file is consistent against a directory.""" self.check_fingerprint(directory) @@ -853,11 +855,21 @@ def check_aea_version(self): """ _check_aea_version(self) + def update(self, data: Dict) -> None: + """ + Update configuration with other data. + + :param data: the data to replace. + :return: None + """ + class ConnectionConfig(ComponentConfiguration): """Handle connection configuration.""" default_configuration_filename = DEFAULT_CONNECTION_CONFIG_FILE + package_type = PackageType.CONNECTION + configurable_fields = {"config"} def __init__( self, @@ -879,24 +891,24 @@ def __init__( ): """Initialize a connection configuration object.""" if connection_id is None: - assert name != "", "Name or connection_id must be set." - assert author != "", "Author or connection_id must be set." - assert version != "", "Version or connection_id must be set." + enforce(name != "", "Name or connection_id must be set.") + enforce(author != "", "Author or connection_id must be set.") + enforce(version != "", "Version or connection_id must be set.") else: - assert name in ( - "", - connection_id.name, - ), "Non matching name in ConnectionConfig name and public id." + enforce( + name in ("", connection_id.name,), + "Non matching name in ConnectionConfig name and public id.", + ) name = connection_id.name - assert author in ( - "", - connection_id.author, - ), "Non matching author in ConnectionConfig author and public id." + enforce( + author in ("", connection_id.author,), + "Non matching author in ConnectionConfig author and public id.", + ) author = connection_id.author - assert version in ( - "", - connection_id.version, - ), "Non matching version in ConnectionConfig version and public id." + enforce( + version in ("", connection_id.version,), + "Non matching version in ConnectionConfig version and public id.", + ) version = connection_id.version super().__init__( name, @@ -918,12 +930,7 @@ def __init__( ) self.dependencies = dependencies if dependencies is not None else {} self.description = description - self.config = config - - @property - def component_type(self) -> ComponentType: - """Get the component type.""" - return ComponentType.CONNECTION + self.config = config if len(config) > 0 else {} @property def package_dependencies(self) -> Set[ComponentId]: @@ -941,6 +948,7 @@ def json(self) -> Dict: "name": self.name, "author": self.author, "version": self.version, + "type": self.component_type.value, "description": self.description, "license": self.license, "aea_version": self.aea_version, @@ -984,14 +992,24 @@ def from_json(cls, obj: Dict): excluded_protocols=cast(Set[PublicId], excluded_protocols), dependencies=cast(Dependencies, dependencies), description=cast(str, obj.get("description", "")), - **cast(dict, obj.get("config")), + **cast(dict, obj.get("config", {})), ) + def update(self, data: Dict) -> None: + """ + Update configuration with other data. + + :param data: the data to replace. + :return: None + """ + self.config = data.get("config", self.config) + class ProtocolConfig(ComponentConfiguration): """Handle protocol configuration.""" default_configuration_filename = DEFAULT_PROTOCOL_CONFIG_FILE + package_type = PackageType.PROTOCOL def __init__( self, @@ -1019,11 +1037,6 @@ def __init__( self.dependencies = dependencies if dependencies is not None else {} self.description = description - @property - def component_type(self) -> ComponentType: - """Get the component type.""" - return ComponentType.PROTOCOL - @property def json(self) -> Dict: """Return the JSON representation.""" @@ -1032,6 +1045,7 @@ def json(self) -> Dict: "name": self.name, "author": self.author, "version": self.version, + "type": self.component_type.value, "description": self.description, "license": self.license, "aea_version": self.aea_version, @@ -1090,6 +1104,8 @@ class SkillConfig(ComponentConfiguration): """Class to represent a skill configuration file.""" default_configuration_filename = DEFAULT_SKILL_CONFIG_FILE + package_type = PackageType.SKILL + configurable_fields = {"handlers", "behaviours", "models"} def __init__( self, @@ -1123,17 +1139,12 @@ def __init__( self.skills: List[PublicId] = (skills if skills is not None else []) self.dependencies = dependencies if dependencies is not None else {} self.description = description - self.handlers = CRUDCollection[SkillComponentConfiguration]() - self.behaviours = CRUDCollection[SkillComponentConfiguration]() - self.models = CRUDCollection[SkillComponentConfiguration]() + self.handlers: CRUDCollection[SkillComponentConfiguration] = CRUDCollection() + self.behaviours: CRUDCollection[SkillComponentConfiguration] = CRUDCollection() + self.models: CRUDCollection[SkillComponentConfiguration] = CRUDCollection() self.is_abstract = is_abstract - @property - def component_type(self) -> ComponentType: - """Get the component type.""" - return ComponentType.SKILL - @property def package_dependencies(self) -> Set[ComponentId]: """Get the skill dependencies.""" @@ -1166,6 +1177,7 @@ def json(self) -> Dict: "name": self.name, "author": self.author, "version": self.version, + "type": self.component_type.value, "description": self.description, "license": self.license, "aea_version": self.aea_version, @@ -1241,11 +1253,31 @@ def from_json(cls, obj: Dict): return skill_config + def update(self, data: Dict) -> None: + """ + Update configuration with other data. + + :param data: the data to replace. + :return: None + """ + for behaviour_id, behaviour_data in data.get("behaviours", {}).items(): + behaviour_config = SkillComponentConfiguration.from_json(behaviour_data) + self.behaviours.update(behaviour_id, behaviour_config) + + for handler_id, handler_data in data.get("handlers", {}).items(): + handler_config = SkillComponentConfiguration.from_json(handler_data) + self.handlers.update(handler_id, handler_config) + + for model_id, model_data in data.get("models", {}).items(): + model_config = SkillComponentConfiguration.from_json(model_data) + self.models.update(model_id, model_config) + class AgentConfig(PackageConfiguration): """Class to represent the agent configuration file.""" default_configuration_filename = DEFAULT_AEA_CONFIG_FILE + package_type = PackageType.AGENT def __init__( self, @@ -1259,7 +1291,7 @@ def __init__( registry_path: str = DEFAULT_REGISTRY_PATH, description: str = "", logging_config: Optional[Dict] = None, - timeout: Optional[float] = None, + period: Optional[float] = None, execution_timeout: Optional[float] = None, max_reactions: Optional[int] = None, decision_maker_handler: Optional[Dict] = None, @@ -1267,6 +1299,7 @@ def __init__( default_routing: Optional[Dict] = None, loop_mode: Optional[str] = None, runtime_mode: Optional[str] = None, + component_configurations: Optional[Dict[ComponentId, Dict]] = None, ): """Instantiate the agent configuration object.""" super().__init__( @@ -1296,7 +1329,7 @@ def __init__( self.logging_config["version"] = 1 self.logging_config["disable_existing_loggers"] = False - self.timeout: Optional[float] = timeout + self.period: Optional[float] = period self.execution_timeout: Optional[float] = execution_timeout self.max_reactions: Optional[int] = max_reactions self.skill_exception_policy: Optional[str] = skill_exception_policy @@ -1315,6 +1348,32 @@ def __init__( ) # type: Dict[PublicId, PublicId] self.loop_mode = loop_mode self.runtime_mode = runtime_mode + self._component_configurations: Dict[ComponentId, Dict] = {} + self.component_configurations = ( + component_configurations if component_configurations is not None else {} + ) + + @property + def component_configurations(self) -> Dict[ComponentId, Dict]: + """Get the custom component configurations.""" + return self._component_configurations + + @component_configurations.setter + def component_configurations(self, d: Dict[ComponentId, Dict]) -> None: + """Set the component configurations.""" + package_type_to_set = { + PackageType.PROTOCOL: self.protocols, + PackageType.CONNECTION: self.connections, + PackageType.CONTRACT: self.contracts, + PackageType.SKILL: self.skills, + } + for component_id, _ in d.items(): + enforce( + component_id.public_id + in package_type_to_set[component_id.package_type], + f"Component {component_id} not declared in the agent configuration.", + ) + self._component_configurations = d @property def package_dependencies(self) -> Set[ComponentId]: @@ -1355,7 +1414,8 @@ def connection_private_key_paths_dict(self) -> Dict[str, str]: @property def default_connection(self) -> str: """Get the default connection.""" - assert self._default_connection is not None, "Default connection not set yet." + if self._default_connection is None: # pragma: nocover + raise ValueError("Default connection not set yet.") return str(self._default_connection) @default_connection.setter @@ -1376,7 +1436,8 @@ def default_connection(self, connection_id: Optional[Union[str, PublicId]]): @property def default_ledger(self) -> str: """Get the default ledger.""" - assert self._default_ledger is not None, "Default ledger not set yet." + if self._default_ledger is None: # pragma: nocover + raise ValueError("Default ledger not set yet.") return self._default_ledger @default_ledger.setter @@ -1389,6 +1450,19 @@ def default_ledger(self, ledger_id: str): """ self._default_ledger = ledger_id + def component_configurations_json(self) -> List[OrderedDict]: + """Get the component configurations in JSON format.""" + return [ + OrderedDict( + name=component_id.name, + author=component_id.author, + version=component_id.version, + type=component_id.component_type.value, + **obj, + ) + for component_id, obj in self.component_configurations.items() + ] + @property def json(self) -> Dict: """Return the JSON representation.""" @@ -1411,6 +1485,7 @@ def json(self) -> Dict: "logging_config": self.logging_config, "private_key_paths": self.private_key_paths_dict, "registry_path": self.registry_path, + "component_configurations": self.component_configurations_json(), } ) # type: Dict[str, Any] @@ -1419,8 +1494,8 @@ def json(self) -> Dict: "connection_private_key_paths" ] = self.connection_private_key_paths_dict - if self.timeout is not None: - config["timeout"] = self.timeout + if self.period is not None: + config["period"] = self.period if self.execution_timeout is not None: config["execution_timeout"] = self.execution_timeout if self.max_reactions is not None: @@ -1457,7 +1532,7 @@ def from_json(cls, obj: Dict): Sequence[str], obj.get("fingerprint_ignore_patterns") ), logging_config=cast(Dict, obj.get("logging_config", {})), - timeout=cast(float, obj.get("timeout")), + period=cast(float, obj.get("period")), execution_timeout=cast(float, obj.get("execution_timeout")), max_reactions=cast(int, obj.get("max_reactions")), decision_maker_handler=cast(Dict, obj.get("decision_maker_handler", {})), @@ -1465,6 +1540,7 @@ def from_json(cls, obj: Dict): default_routing=cast(Dict, obj.get("default_routing", {})), loop_mode=cast(str, obj.get("loop_mode")), runtime_mode=cast(str, obj.get("runtime_mode")), + component_configurations=None, ) for crypto_id, path in obj.get("private_key_paths", {}).items(): @@ -1474,40 +1550,31 @@ def from_json(cls, obj: Dict): agent_config.connection_private_key_paths.create(crypto_id, path) # parse connection public ids - connections = set( - map( - lambda x: PublicId.from_str(x), # pylint: disable=unnecessary-lambda - obj.get("connections", []), - ) + agent_config.connections = set( + map(PublicId.from_str, obj.get("connections", []),) ) - agent_config.connections = cast(Set[PublicId], connections) # parse contracts public ids - contracts = set( - map( - lambda x: PublicId.from_str(x), # pylint: disable=unnecessary-lambda - obj.get("contracts", []), - ) - ) - agent_config.contracts = cast(Set[PublicId], contracts) + agent_config.contracts = set(map(PublicId.from_str, obj.get("contracts", []),)) # parse protocol public ids - protocols = set( - map( - lambda x: PublicId.from_str(x), # pylint: disable=unnecessary-lambda - obj.get("protocols", []), - ) - ) - agent_config.protocols = cast(Set[PublicId], protocols) + agent_config.protocols = set(map(PublicId.from_str, obj.get("protocols", []),)) # parse skills public ids - skills = set( - map( - lambda x: PublicId.from_str(x), # pylint: disable=unnecessary-lambda - obj.get("skills", []), + agent_config.skills = set(map(PublicId.from_str, obj.get("skills", []),)) + + component_configurations = {} + for config in obj.get("component_configurations", []): + tmp = deepcopy(config) + name = tmp.pop("name") + author = tmp.pop("author") + version = tmp.pop("version") + type_ = tmp.pop("type") + component_id = ComponentId( + ComponentType(type_), PublicId(author, name, version) ) - ) - agent_config.skills = cast(Set[PublicId], skills) + component_configurations[component_id] = tmp + agent_config.component_configurations = component_configurations # set default connection default_connection_name = obj.get("default_connection", None) @@ -1666,6 +1733,7 @@ class ContractConfig(ComponentConfiguration): """Handle contract configuration.""" default_configuration_filename = DEFAULT_CONTRACT_CONFIG_FILE + package_type = PackageType.CONTRACT def __init__( self, @@ -1699,11 +1767,6 @@ def __init__( ) self.class_name = class_name - @property - def component_type(self) -> ComponentType: - """Get the component type.""" - return ComponentType.CONTRACT - @property def contract_interfaces(self) -> Dict[str, str]: """Get the contract interfaces.""" @@ -1711,7 +1774,8 @@ def contract_interfaces(self) -> Dict[str, str]: def _get_contract_interfaces(self) -> Dict[str, str]: """Get the contract interfaces.""" - assert self.directory is not None, "Set directory before calling." + if self.directory is None: # pragma: nocover + raise ValueError("Set directory before calling.") contract_interfaces = {} # type: Dict[str, str] for identifier, path in self.contract_interface_paths.items(): full_path = Path(self.directory, path) @@ -1719,7 +1783,7 @@ def _get_contract_interfaces(self) -> Dict[str, str]: with open(full_path, "r") as interface_file_ethereum: contract_interface = json.load(interface_file_ethereum) contract_interfaces[identifier] = contract_interface - elif identifier == "cosmos": + elif identifier in ["cosmos", "fetchai"]: with open(full_path, "rb") as interface_file_cosmos: contract_interface = { "wasm_byte_code": str( @@ -1730,7 +1794,7 @@ def _get_contract_interfaces(self) -> Dict[str, str]: } contract_interfaces[identifier] = contract_interface else: - ValueError( # pragma: nocover + raise ValueError( # pragma: nocover "Identifier {} is not supported for contracts." ) return contract_interfaces @@ -1743,6 +1807,7 @@ def json(self) -> Dict: "name": self.name, "author": self.author, "version": self.version, + "type": self.component_type.value, "description": self.description, "license": self.license, "aea_version": self.aea_version, @@ -1800,7 +1865,7 @@ def _compute_fingerprint( for file in all_files: file_hash = hasher.get(str(file)) key = str(file.relative_to(package_directory)) - assert key not in fingerprints, "Key in fingerprints!" # nosec + enforce(key not in fingerprints, "Key in fingerprints!") # nosec # use '/' as path separator normalized_path = Path(key).as_posix() fingerprints[normalized_path] = file_hash @@ -1841,24 +1906,23 @@ def _compare_fingerprints( package_configuration.public_id, ) ) - else: - raise ValueError( - ( - "Fingerprints for package {} do not match:\nExpected: {}\nActual: {}\n" - "Please fingerprint the package before continuing: 'aea fingerprint {} {}'" - ).format( - package_directory, - pprint.pformat(expected_fingerprints), - pprint.pformat(actual_fingerprints), - str(item_type), - package_configuration.public_id, - ) + raise ValueError( + ( + "Fingerprints for package {} do not match:\nExpected: {}\nActual: {}\n" + "Please fingerprint the package before continuing: 'aea fingerprint {} {}'" + ).format( + package_directory, + pprint.pformat(expected_fingerprints), + pprint.pformat(actual_fingerprints), + str(item_type), + package_configuration.public_id, ) + ) def _check_aea_version(package_configuration: PackageConfiguration): """Check the package configuration version against the version of the framework.""" - current_aea_version = Version(aea.__version__) + current_aea_version = Version(__aea_version__) version_specifiers = package_configuration.aea_version_specifiers if current_aea_version not in version_specifiers: raise ValueError( diff --git a/aea/configurations/constants.py b/aea/configurations/constants.py index b08e98c80f..9370662185 100644 --- a/aea/configurations/constants.py +++ b/aea/configurations/constants.py @@ -22,16 +22,16 @@ from aea.configurations.base import DEFAULT_LICENSE as DL from aea.configurations.base import DEFAULT_REGISTRY_PATH as DRP from aea.configurations.base import PublicId -from aea.crypto.cosmos import CosmosCrypto -from aea.crypto.helpers import COSMOS_PRIVATE_KEY_FILE +from aea.crypto.fetchai import FetchAICrypto +from aea.crypto.helpers import PRIVATE_KEY_PATH_SCHEMA -DEFAULT_CONNECTION = PublicId.from_str("fetchai/stub:0.8.0") -DEFAULT_PROTOCOL = PublicId.from_str("fetchai/default:0.4.0") -DEFAULT_SKILL = PublicId.from_str("fetchai/error:0.4.0") -DEFAULT_LEDGER = CosmosCrypto.identifier -DEFAULT_PRIVATE_KEY_FILE = COSMOS_PRIVATE_KEY_FILE +DEFAULT_CONNECTION = PublicId.from_str("fetchai/stub:0.9.0") +DEFAULT_PROTOCOL = PublicId.from_str("fetchai/default:0.5.0") +DEFAULT_SKILL = PublicId.from_str("fetchai/error:0.5.0") +DEFAULT_LEDGER = FetchAICrypto.identifier +DEFAULT_PRIVATE_KEY_FILE = PRIVATE_KEY_PATH_SCHEMA.format(DEFAULT_LEDGER) DEFAULT_REGISTRY_PATH = DRP DEFAULT_LICENSE = DL -SIGNING_PROTOCOL = PublicId.from_str("fetchai/signing:0.2.0") -STATE_UPDATE_PROTOCOL = PublicId.from_str("fetchai/state_update:0.2.0") +SIGNING_PROTOCOL = PublicId.from_str("fetchai/signing:0.3.0") +STATE_UPDATE_PROTOCOL = PublicId.from_str("fetchai/state_update:0.3.0") LOCAL_PROTOCOLS = [DEFAULT_PROTOCOL, SIGNING_PROTOCOL, STATE_UPDATE_PROTOCOL] diff --git a/aea/configurations/loader.py b/aea/configurations/loader.py index 3630de6aab..492b8dd3da 100644 --- a/aea/configurations/loader.py +++ b/aea/configurations/loader.py @@ -23,8 +23,10 @@ import json import os import re +from copy import deepcopy from pathlib import Path -from typing import Dict, Generic, List, TextIO, Type, TypeVar, Union +from typing import Dict, Generic, List, TextIO, Tuple, Type, TypeVar, Union, cast + import jsonschema from jsonschema import Draft4Validator @@ -34,14 +36,19 @@ from aea.configurations.base import ( AgentConfig, + ComponentConfiguration, + ComponentId, + ComponentType, ConnectionConfig, ContractConfig, PackageType, ProtocolConfig, ProtocolSpecification, + PublicId, SkillConfig, ) -from aea.helpers.base import yaml_dump, yaml_load +from aea.exceptions import enforce +from aea.helpers.base import yaml_dump, yaml_dump_all, yaml_load, yaml_load_all _CUR_DIR = os.path.dirname(inspect.getfile(inspect.currentframe())) # type: ignore _SCHEMAS_DIR = os.path.join(_CUR_DIR, "schemas") @@ -145,22 +152,30 @@ def load_protocol_specification(self, file_pointer: TextIO) -> T: protocol_specification.dialogue_config = dialogue_configuration return protocol_specification + def validate(self, json_data: Dict) -> None: + """ + Validate a JSON object. + + :param json_data: the JSON data. + :return: None. + """ + if self.configuration_class.package_type == PackageType.AGENT: + json_data_copy = deepcopy(json_data) + json_data_copy.pop("component_configurations", None) + self._validator.validate(instance=json_data_copy) + else: + self._validator.validate(instance=json_data) + def load(self, file_pointer: TextIO) -> T: """ - Load an agent configuration file. + Load a configuration file. :param file_pointer: the file pointer to the configuration file :return: the configuration object. - :raises """ - configuration_file_json = yaml_load(file_pointer) - - self.validator.validate(instance=configuration_file_json) - - key_order = list(configuration_file_json.keys()) - configuration_obj = self.configuration_class.from_json(configuration_file_json) - configuration_obj._key_order = key_order # pylint: disable=protected-access - return configuration_obj + if self.configuration_class.package_type == PackageType.AGENT: + return cast(T, self._load_agent_config(file_pointer)) + return self._load_component_config(file_pointer) def dump(self, configuration: T, file_pointer: TextIO) -> None: """Dump a configuration. @@ -169,9 +184,10 @@ def dump(self, configuration: T, file_pointer: TextIO) -> None: :param file_pointer: the file pointer to the configuration file :return: None """ - result = configuration.ordered_json - self.validator.validate(instance=result) - yaml_dump(result, file_pointer) + if self.configuration_class.package_type == PackageType.AGENT: + self._dump_agent_config(cast(AgentConfig, configuration), file_pointer) + else: + self._dump_component_config(configuration, file_pointer) @classmethod def from_configuration_type( @@ -181,6 +197,204 @@ def from_configuration_type( configuration_type = PackageType(configuration_type) return ConfigLoaders.from_package_type(configuration_type) + def _validate(self, json_data: Dict) -> None: + """ + Validate a configuration file. + + :param json_data: the JSON object of the configuration file to validate. + :return: None + :raises ValidationError: if the file doesn't comply with the JSON schema. + | ValueError: if other consistency checks fail. + """ + # this might raise ValidationError. + self.validate(json_data) + + expected_type = self.configuration_class.package_type + if expected_type != PackageType.AGENT and "type" in json_data: + actual_type = PackageType(json_data["type"]) + if expected_type != actual_type: + raise ValueError( + f"The field type is not correct: expected {expected_type}, found {actual_type}." + ) + + def _load_component_config(self, file_pointer: TextIO) -> T: + """Load a component configuration.""" + configuration_file_json = yaml_load(file_pointer) + return self._load_from_json(configuration_file_json) + + def _load_from_json(self, configuration_file_json: Dict) -> T: + """Load component configuration from JSON object.""" + self._validate(configuration_file_json) + key_order = list(configuration_file_json.keys()) + configuration_obj = self.configuration_class.from_json(configuration_file_json) + configuration_obj._key_order = key_order # pylint: disable=protected-access + return configuration_obj + + def _load_agent_config(self, file_pointer: TextIO) -> AgentConfig: + """Load an agent configuration.""" + configuration_file_jsons = yaml_load_all(file_pointer) + + if len(configuration_file_jsons) == 0: + raise ValueError("Agent configuration file was empty.") + agent_config_json = configuration_file_jsons[0] + self._validate(agent_config_json) + key_order = list(agent_config_json.keys()) + agent_configuration_obj = cast( + AgentConfig, self.configuration_class.from_json(agent_config_json) + ) + agent_configuration_obj._key_order = ( # pylint: disable=protected-access + key_order + ) + + component_configurations: Dict[ComponentId, Dict] = {} + # load the other components. + for i, component_configuration_json in enumerate(configuration_file_jsons[1:]): + component_id, component_config = self._process_component_section( + i, component_configuration_json + ) + if component_id in component_configurations: + raise ValueError( + f"Configuration of component {component_id} occurs more than once." + ) + component_configurations[component_id] = component_config + + agent_configuration_obj.component_configurations = component_configurations + return agent_configuration_obj + + def _dump_agent_config( + self, configuration: AgentConfig, file_pointer: TextIO + ) -> None: + """Dump agent configuration.""" + agent_config_part = configuration.ordered_json + agent_config_part.pop("component_configurations") + self.validator.validate(instance=agent_config_part) + result = [agent_config_part] + configuration.component_configurations_json() + yaml_dump_all(result, file_pointer) + + def _dump_component_config(self, configuration: T, file_pointer: TextIO) -> None: + """Dump component configuration.""" + result = configuration.ordered_json + self.validator.validate(instance=result) + yaml_dump(result, file_pointer) + + def _process_component_section( + self, i: int, component_configuration_json: Dict + ) -> Tuple[ComponentId, Dict]: + """ + Process a component configuration in an agent configuration file. + + It breaks down in: + - extract the component id + - validate the component configuration + - check that there are only configurable fields + + :param i: the index of the component in the file. + :param component_configuration_json: the JSON object. + :return: the processed component configuration. + """ + component_id, result = self._split_component_id_and_config( + i, component_configuration_json + ) + self._validate_component_configuration(component_id, result) + self._check_only_configurable_fields(component_id, result) + return component_id, result + + @staticmethod + def _split_component_id_and_config( + i: int, component_configuration_json: Dict + ) -> Tuple[ComponentId, Dict]: + """ + Split component id and configuration. + + :param i: the position of the component configuration in the agent config file.. + :param component_configuration_json: the JSON object to process. + :return: the component id and the configuration object. + :raises ValueError: if the component id cannot be extracted. + """ + result = deepcopy(component_configuration_json) + # author, name, version, type are mandatory fields + missing_fields = {"author", "name", "version", "type"}.difference( + component_configuration_json.keys() + ) + if len(missing_fields) > 0: + raise ValueError( + f"There are missing fields in component id {i + 1}: {missing_fields}." + ) + component_name = result.pop("name") + component_author = result.pop("author") + component_version = result.pop("version") + component_type = ComponentType(result.pop("type")) + component_public_id = PublicId( + component_author, component_name, component_version + ) + component_id = ComponentId(component_type, component_public_id) + return component_id, result + + @staticmethod + def _validate_component_configuration( + component_id: ComponentId, configuration: Dict + ) -> None: + """ + Validate the component configuration of an agent configuration file. + + This check is to detect inconsistencies in the specified fields. + + :param component_id: the component id. + :param configuration: the configuration dictionary. + :return: None + :raises ValueError: if the configuration is not valid. + """ + # we need to populate the required fields to validate the configurations. + temporary_config = deepcopy(configuration) + # common to every package + temporary_config["name"] = component_id.name + temporary_config["author"] = component_id.author + temporary_config["version"] = component_id.version + temporary_config["license"] = "some_license" + temporary_config["aea_version"] = "0.1.0" + if component_id.component_type == ComponentType.PROTOCOL: + pass # no other required field + elif component_id.component_type == ComponentType.CONNECTION: + temporary_config["class_name"] = "SomeClassName" + temporary_config["protocols"] = [] + temporary_config.setdefault("config", {}) + elif component_id.component_type == ComponentType.CONTRACT: + temporary_config["class_name"] = "SomeClassName" + elif component_id.component_type == ComponentType.SKILL: + temporary_config["protocols"] = [] + temporary_config["contracts"] = [] + temporary_config["skills"] = [] + loader = ConfigLoaders.from_package_type(component_id.package_type) + try: + loader._load_from_json(temporary_config) # pylint: disable=protected-access + except jsonschema.ValidationError as e: + raise ValueError( + f"Configuration of component {component_id} is not valid." + ) from e + # all good! + + @staticmethod + def _check_only_configurable_fields( + component_id: ComponentId, configuration: Dict + ) -> None: + """ + Check that there are only configurable fields. + + :param component_id: the component id. + :param configuration: the configuration object. + :return: None + """ + configurable_fields = ( + component_id.package_type.configuration_class().configurable_fields + ) + non_configurable_fields = set(configuration.keys()).difference( + configurable_fields + ) + enforce( + len(non_configurable_fields) == 0, + f"Bad configuration for component {component_id}: {non_configurable_fields} are non-configurable fields.", + ) + class ConfigLoaders: """Configuration Loader class to load any package type.""" @@ -212,6 +426,57 @@ def from_package_type( return cls._from_configuration_type_to_loaders[configuration_type] +def load_component_configuration( + component_type: ComponentType, + directory: Path, + skip_consistency_check: bool = False, +) -> "ComponentConfiguration": + """ + Load configuration and check that it is consistent against the directory. + + :param component_type: the component type. + :param directory: the root of the package + :param skip_consistency_check: if True, the consistency check are skipped. + :return: the configuration object. + """ + configuration_object = _load_configuration_object(component_type, directory) + if not skip_consistency_check: + configuration_object._check_configuration_consistency( # pylint: disable=protected-access + directory + ) + return configuration_object + + +def _load_configuration_object( + component_type: ComponentType, directory: Path +) -> "ComponentConfiguration": + """ + Load the configuration object, without consistency checks. + + :param component_type: the component type. + :param directory: the directory of the configuration. + :return: the configuration object. + :raises FileNotFoundError: if the configuration file is not found. + """ + configuration_loader = ConfigLoader.from_configuration_type( + component_type.to_configuration_type() + ) + configuration_filename = ( + configuration_loader.configuration_class.default_configuration_filename + ) + configuration_filepath = directory / configuration_filename + try: + fp = open(configuration_filepath) + configuration_object = configuration_loader.load(fp) + except FileNotFoundError: + raise FileNotFoundError( + "{} configuration not found: {}".format( + component_type.value.capitalize(), configuration_filepath + ) + ) + return configuration_object + + def _config_loader(): envvar_matcher = re.compile(r"\${([^}^{]+)\}") @@ -232,6 +497,4 @@ def envvar_constructor(_loader, node): # pragma: no cover yaml.add_constructor("!envvar", envvar_constructor, SafeLoader) -# TODO: instead of this, create custom loader and use it -# by wrapping yaml.safe_load to use it _config_loader() diff --git a/aea/helpers/pypi.py b/aea/configurations/pypi.py similarity index 98% rename from aea/helpers/pypi.py rename to aea/configurations/pypi.py index 5db62d827e..5dd0bd0953 100644 --- a/aea/helpers/pypi.py +++ b/aea/configurations/pypi.py @@ -83,7 +83,7 @@ def is_satisfiable(specifier_set: SpecifierSet) -> bool: # split specifier "~=" in two specifiers: # - >= # - < - # TODO this is not the full story. we should check the version number + # this is not the full story. we should check the version number # up to the last zero, which might be the micro number. # e.g. see last examples of https://www.python.org/dev/peps/pep-0440/#compatible-release if specifier.operator == "~=": @@ -174,14 +174,13 @@ def _handle_range_constraints( version_greater_than = Version(greatest_greater_than.version) if version_less_than < version_greater_than: return False - elif version_greater_than == version_less_than: + if version_greater_than == version_less_than: # check if one of them has NOT the equality one_of_them_is_a_strict_comparison = ( greatest_greater_than.operator == ">" ) or (lowest_less_than.operator == "<") return not one_of_them_is_a_strict_comparison - else: - return True + return True def is_simple_dep(dep: Dependency) -> bool: diff --git a/aea/configurations/schemas/aea-config_schema.json b/aea/configurations/schemas/aea-config_schema.json index f3235df29e..6e3228a7d7 100644 --- a/aea/configurations/schemas/aea-config_schema.json +++ b/aea/configurations/schemas/aea-config_schema.json @@ -60,15 +60,6 @@ } } }, - "ledger_apis": { - "type": "object", - "uniqueItems": true, - "patternProperties": { - "^[^\\d\\W]\\w*\\Z": { - "$ref": "definitions.json#/definitions/ledger_api" - } - } - }, "default_ledger": { "$ref": "definitions.json#/definitions/ledger_id" }, @@ -109,8 +100,8 @@ "description": { "$ref": "definitions.json#/definitions/description" }, - "timeout": { - "$ref": "definitions.json#/definitions/timeout" + "period": { + "$ref": "definitions.json#/definitions/period" }, "execution_timeout": { "$ref": "definitions.json#/definitions/execution_timeout" diff --git a/aea/configurations/schemas/connection-config_schema.json b/aea/configurations/schemas/connection-config_schema.json index 4ba2cd4b85..50c9d9d5e2 100644 --- a/aea/configurations/schemas/connection-config_schema.json +++ b/aea/configurations/schemas/connection-config_schema.json @@ -23,6 +23,9 @@ "version": { "$ref": "definitions.json#/definitions/package_version" }, + "type": { + "$ref": "definitions.json#/definitions/component_type" + }, "license": { "$ref": "definitions.json#/definitions/license" }, diff --git a/aea/configurations/schemas/contract-config_schema.json b/aea/configurations/schemas/contract-config_schema.json index b9cda0e72b..0ad8333401 100644 --- a/aea/configurations/schemas/contract-config_schema.json +++ b/aea/configurations/schemas/contract-config_schema.json @@ -21,6 +21,9 @@ "version": { "$ref": "definitions.json#/definitions/package_version" }, + "type": { + "$ref": "definitions.json#/definitions/component_type" + }, "license": { "$ref": "definitions.json#/definitions/license" }, diff --git a/aea/configurations/schemas/definitions.json b/aea/configurations/schemas/definitions.json index 0f59cc8d85..ffe353b5f8 100644 --- a/aea/configurations/schemas/definitions.json +++ b/aea/configurations/schemas/definitions.json @@ -35,6 +35,10 @@ "type": "string", "pattern": "[a-zA-Z_][a-zA-Z0-9_]*" }, + "component_type": { + "type": "string", + "enum": ["protocol", "connection", "contract", "skill"] + }, "private_key_path": { "type": "string" }, @@ -113,9 +117,10 @@ "type": ["integer", "null"], "minimum": 1 }, - "timeout": { + "period": { "type": ["number", "null"], - "minimum": 0 + "minimum": 0, + "exclusiveMinimum": true }, "execution_timeout": { "type": ["number", "null"], @@ -127,7 +132,7 @@ }, "loop_mode": { "type": "string", - "enum": ["sync", "async"] + "enum": ["async", "sync"] }, "runtime_mode": { "type": "string", diff --git a/aea/configurations/schemas/protocol-config_schema.json b/aea/configurations/schemas/protocol-config_schema.json index cb6596b0d0..ba34921514 100644 --- a/aea/configurations/schemas/protocol-config_schema.json +++ b/aea/configurations/schemas/protocol-config_schema.json @@ -20,6 +20,9 @@ "version": { "$ref": "definitions.json#/definitions/package_version" }, + "type": { + "$ref": "definitions.json#/definitions/component_type" + }, "license": { "$ref": "definitions.json#/definitions/license" }, diff --git a/aea/configurations/schemas/skill-config_schema.json b/aea/configurations/schemas/skill-config_schema.json index 24b6a53bf5..bfb97e3d9d 100644 --- a/aea/configurations/schemas/skill-config_schema.json +++ b/aea/configurations/schemas/skill-config_schema.json @@ -23,6 +23,9 @@ "version": { "$ref": "definitions.json#/definitions/package_version" }, + "type": { + "$ref": "definitions.json#/definitions/component_type" + }, "license": { "$ref": "definitions.json#/definitions/license" }, @@ -87,13 +90,6 @@ } } }, - "ledgers": { - "type": "array", - "uniqueItems": true, - "items": { - "type": "string" - } - }, "dependencies": { "$ref": "definitions.json#/definitions/dependencies" }, diff --git a/aea/connections/base.py b/aea/connections/base.py index f04dd49021..983b2234b3 100644 --- a/aea/connections/base.py +++ b/aea/connections/base.py @@ -16,26 +16,29 @@ # limitations under the License. # # ------------------------------------------------------------------------------ + """The base connection package.""" +import asyncio import inspect import logging import re from abc import ABC, abstractmethod -from asyncio import AbstractEventLoop +from contextlib import contextmanager from enum import Enum from pathlib import Path -from typing import Optional, Set, TYPE_CHECKING, cast +from typing import Generator, Optional, Set, TYPE_CHECKING, cast -from aea.components.base import Component +from aea.components.base import Component, load_aea_package from aea.configurations.base import ( - ComponentConfiguration, ComponentType, ConnectionConfig, PublicId, ) +from aea.configurations.loader import load_component_configuration from aea.crypto.wallet import CryptoStore +from aea.exceptions import enforce from aea.helpers.async_utils import AsyncState -from aea.helpers.base import load_aea_package, load_module +from aea.helpers.base import load_module from aea.identity.base import Identity @@ -81,12 +84,12 @@ def __init__( :param restricted_to_protocols: the set of protocols ids of the only supported protocols for this connection. :param excluded_protocols: the set of protocols ids that we want to exclude for this connection. """ - assert configuration is not None, "The configuration must be provided." + enforce(configuration is not None, "The configuration must be provided.") super().__init__(configuration, **kwargs) - assert ( - super().public_id == self.connection_id - ), "Connection ids in configuration and class not matching." - self._loop: Optional[AbstractEventLoop] = None + enforce( + super().public_id == self.connection_id, + "Connection ids in configuration and class not matching.", + ) self._state = AsyncState(ConnectionStates.disconnected) self._identity = identity @@ -100,35 +103,40 @@ def __init__( ) @property - def loop(self) -> Optional[AbstractEventLoop]: + def loop(self) -> asyncio.AbstractEventLoop: """Get the event loop.""" - return self._loop - - @loop.setter - def loop(self, loop: AbstractEventLoop) -> None: - """ - Set the event loop. - - :param loop: the event loop. - :return: None - """ - assert ( - self._loop is None or not self._loop.is_running() - ), "Cannot set the loop while it is running." - self._loop = loop + enforce(asyncio.get_event_loop().is_running(), "Event loop is not running.") + return asyncio.get_event_loop() + + def _ensure_connected(self) -> None: # pragma: nocover + """Raise exception if connection is not connected.""" + if not self.is_connected: + raise ConnectionError("Connection is not connected! Connect first!") + + @contextmanager + def _connect_context(self) -> Generator: + """Set state connecting, disconnecteing, dicsconnected during connect method.""" + with self._state.transit( + initial=ConnectionStates.connecting, + success=ConnectionStates.connected, + fail=ConnectionStates.disconnected, + ): + yield @property def address(self) -> "Address": # pragma: nocover """Get the address.""" - assert ( - self._identity is not None - ), "You must provide the identity in order to retrieve the address." + if self._identity is None: + raise ValueError( + "You must provide the identity in order to retrieve the address." + ) return self._identity.address @property def crypto_store(self) -> CryptoStore: # pragma: nocover """Get the crypto store.""" - assert self._crypto_store is not None, "CryptoStore not available." + if self._crypto_store is None: + raise ValueError("CryptoStore not available.") return self._crypto_store @property @@ -144,7 +152,8 @@ def component_type(self) -> ComponentType: # pragma: nocover @property def configuration(self) -> ConnectionConfig: """Get the connection configuration.""" - assert self._configuration is not None, "Configuration not set." + if self._configuration is None: # pragma: nocover + raise ValueError("Configuration not set.") return cast(ConnectionConfig, super().configuration) @property @@ -152,16 +161,14 @@ def restricted_to_protocols(self) -> Set[PublicId]: # pragma: nocover """Get the ids of the protocols this connection is restricted to.""" if self._configuration is None: return self._restricted_to_protocols - else: - return self.configuration.restricted_to_protocols + return self.configuration.restricted_to_protocols @property def excluded_protocols(self) -> Set[PublicId]: # pragma: nocover """Get the ids of the excluded protocols for this connection.""" if self._configuration is None: return self._excluded_protocols - else: - return self.configuration.excluded_protocols + return self.configuration.excluded_protocols @property def state(self) -> ConnectionStates: @@ -207,7 +214,7 @@ def from_dir( """ configuration = cast( ConnectionConfig, - ComponentConfiguration.load(ComponentType.CONNECTION, Path(directory)), + load_component_configuration(ComponentType.CONNECTION, Path(directory)), ) configuration.directory = Path(directory) return Connection.from_config(configuration, identity, crypto_store, **kwargs) @@ -232,9 +239,10 @@ def from_config( directory = cast(Path, configuration.directory) load_aea_package(configuration) connection_module_path = directory / "connection.py" - assert ( - connection_module_path.exists() and connection_module_path.is_file() - ), "Connection module '{}' not found.".format(connection_module_path) + enforce( + connection_module_path.exists() and connection_module_path.is_file(), + "Connection module '{}' not found.".format(connection_module_path), + ) connection_module = load_module( "connection_module", directory / "connection.py" ) @@ -246,8 +254,9 @@ def from_config( name_to_class = dict(connection_classes) logger.debug("Processing connection {}".format(connection_class_name)) connection_class = name_to_class.get(connection_class_name, None) - assert connection_class is not None, "Connection class '{}' not found.".format( - connection_class_name + enforce( + connection_class is not None, + "Connection class '{}' not found.".format(connection_class_name), ) return connection_class( configuration=configuration, @@ -257,11 +266,16 @@ def from_config( ) @property - def is_connected(self) -> bool: + def is_connected(self) -> bool: # pragma: nocover """Return is connected state.""" return self.state == ConnectionStates.connected @property - def is_disconnected(self) -> bool: + def is_connecting(self) -> bool: # pragma: nocover + """Return is connecting state.""" + return self.state == ConnectionStates.connecting + + @property + def is_disconnected(self) -> bool: # pragma: nocover """Return is disconnected state.""" return self.state == ConnectionStates.disconnected diff --git a/aea/connections/scaffold/connection.yaml b/aea/connections/scaffold/connection.yaml index c1e655bae3..fc2af889a2 100644 --- a/aea/connections/scaffold/connection.yaml +++ b/aea/connections/scaffold/connection.yaml @@ -1,10 +1,11 @@ name: scaffold author: fetchai version: 0.1.0 +type: connection description: The scaffold connection provides a scaffold for a connection to be implemented by the developer. license: Apache-2.0 -aea_version: '>=0.5.0, <0.6.0' +aea_version: '>=0.6.0, <0.7.0' fingerprint: __init__.py: QmZvYZ5ECcWwqiNGh8qNTg735wu51HqaLxTSifUxkQ4KGj connection.py: QmT7MNg8gkmWMzthN3k77i6UVhwXBeC2bGiNrUmXQcjWit diff --git a/aea/connections/stub/connection.py b/aea/connections/stub/connection.py index 30bd5bfc71..a80267f0c9 100644 --- a/aea/connections/stub/connection.py +++ b/aea/connections/stub/connection.py @@ -44,7 +44,7 @@ DEFAULT_OUTPUT_FILE_NAME = "./output_file" SEPARATOR = b"," -PUBLIC_ID = PublicId.from_str("fetchai/stub:0.8.0") +PUBLIC_ID = PublicId.from_str("fetchai/stub:0.9.0") def _encode(e: Envelope, separator: bytes = SEPARATOR): @@ -215,8 +215,9 @@ async def _file_read_and_trunc(self, delay: float = 0.001) -> AsyncIterable[byte async def read_envelopes(self) -> None: """Read envelopes from inptut file, decode and put into in_queue.""" - assert self.in_queue is not None, "Input queue not initialized." - assert self._loop is not None, "Loop not initialized." + self._ensure_connected() + if self.in_queue is None: # pragma: nocover + raise ValueError("Input queue not initialized.") logger.debug("Read messages!") async for data in self._file_read_and_trunc(delay=self.read_delay): @@ -243,6 +244,7 @@ def _split_messages(cls, data: bytes) -> List[bytes]: async def receive(self, *args, **kwargs) -> Optional["Envelope"]: """Receive an envelope.""" + self._ensure_connected() if self.in_queue is None: # pragma: nocover logger.error("Input queue not initialized.") return None @@ -258,16 +260,9 @@ async def connect(self) -> None: if self.is_connected: return - self._state.set(ConnectionStates.connecting) - - try: - self._loop = asyncio.get_event_loop() + with self._connect_context(): self.in_queue = asyncio.Queue() - self._read_envelopes_task = self._loop.create_task(self.read_envelopes()) - self._state.set(ConnectionStates.connected) - except Exception: # pragma: no cover - self._state.set(ConnectionStates.disconnected) - raise + self._read_envelopes_task = self.loop.create_task(self.read_envelopes()) async def _stop_read_envelopes(self) -> None: """ @@ -299,7 +294,8 @@ async def disconnect(self) -> None: if self.is_disconnected: return - assert self.in_queue is not None, "Input queue not initialized." + if self.in_queue is None: # pragma: nocover + raise ValueError("Input queue not initialized.") self._state.set(ConnectionStates.disconnecting) await self._stop_read_envelopes() @@ -313,7 +309,7 @@ async def send(self, envelope: Envelope) -> None: :return: None """ - assert self.loop is not None, "Loop not initialized." + self._ensure_connected() await self.loop.run_in_executor( self._write_pool, write_envelope, envelope, self.output_file ) diff --git a/aea/connections/stub/connection.yaml b/aea/connections/stub/connection.yaml index fd160e4712..743ee3598a 100644 --- a/aea/connections/stub/connection.yaml +++ b/aea/connections/stub/connection.yaml @@ -1,14 +1,15 @@ name: stub author: fetchai -version: 0.8.0 +version: 0.9.0 +type: connection description: The stub connection implements a connection stub which reads/writes messages from/to file. license: Apache-2.0 -aea_version: '>=0.5.0, <0.6.0' +aea_version: '>=0.6.0, <0.7.0' fingerprint: __init__.py: QmWwepN9Fy9gHAp39vUGFSLdnB9JZjdyE3STnbowSUhJkC - connection.py: QmQgWadBUdhxrHh1qzHXJgC3Gezm4FsevRrkVf5np33WV6 - readme.md: QmagrvTRcqPsnDrZeggJCWMU5Ubxic3wsKLcoo67sd5sgM + connection.py: QmTSESVFvsDc9iq2QM3YdZju3yHqTA3wHbLkAzHXEFY17Z + readme.md: QmXSAtxSY7C2YkvUxeVnpqCJY9uJYZxZBmuUcE4zjFXcXz fingerprint_ignore_patterns: [] protocols: [] class_name: StubConnection diff --git a/aea/connections/stub/readme.md b/aea/connections/stub/readme.md index 844cc6fc18..839993ec33 100644 --- a/aea/connections/stub/readme.md +++ b/aea/connections/stub/readme.md @@ -2,6 +2,6 @@ A simple connection for communication with an AEA, using the file system as a point of data exchange. ## Usage -First, add the connection to your AEA project: `aea add connection fetchai/stub:0.8.0`. (If you have created your AEA project with `aea create` then the connection will already be available by default.) +First, add the connection to your AEA project: `aea add connection fetchai/stub:0.9.0`. (If you have created your AEA project with `aea create` then the connection will already be available by default.) Optionally, in the `connection.yaml` file under `config` set the `input_file` and `output_file` to the desired file path. The `stub` connection reads encoded envelopes from the `input_file` and writes encoded envelopes to the `output_file`. diff --git a/aea/context/base.py b/aea/context/base.py index f789a9d6c3..15caf597bb 100644 --- a/aea/context/base.py +++ b/aea/context/base.py @@ -23,10 +23,10 @@ from types import SimpleNamespace from typing import Any, Dict, Optional +from aea.common import Address from aea.configurations.base import PublicId from aea.identity.base import Identity -from aea.mail.base import Address -from aea.multiplexer import ConnectionStatus, OutBox +from aea.multiplexer import MultiplexerStatus, OutBox from aea.skills.tasks import TaskManager @@ -36,7 +36,7 @@ class AgentContext: def __init__( self, identity: Identity, - connection_status: ConnectionStatus, + connection_status: MultiplexerStatus, outbox: OutBox, decision_maker_message_queue: Queue, decision_maker_handler_context: SimpleNamespace, @@ -101,7 +101,7 @@ def address(self) -> Address: return self.identity.address @property - def connection_status(self) -> ConnectionStatus: + def connection_status(self) -> MultiplexerStatus: """Get connection status of the multiplexer.""" return self._connection_status diff --git a/aea/contracts/__init__.py b/aea/contracts/__init__.py index 2e59102e6f..2491ebefb3 100644 --- a/aea/contracts/__init__.py +++ b/aea/contracts/__init__.py @@ -18,7 +18,5 @@ # ------------------------------------------------------------------------------ """This module contains the contract modules.""" -from aea.contracts.base import Contract -from aea.crypto.registries import Registry -contract_registry: Registry[Contract] = Registry[Contract]() +from aea.contracts.base import Contract, contract_registry # noqa: F401 diff --git a/aea/contracts/base.py b/aea/contracts/base.py index 3f9eb20e8e..2d82b162fd 100644 --- a/aea/contracts/base.py +++ b/aea/contracts/base.py @@ -24,16 +24,20 @@ from pathlib import Path from typing import Any, Dict, Optional, cast -from aea.components.base import Component +from aea.components.base import Component, load_aea_package from aea.configurations.base import ( - ComponentConfiguration, ComponentType, ContractConfig, ContractId, ) +from aea.configurations.loader import load_component_configuration from aea.crypto.base import LedgerApi -from aea.helpers.base import load_aea_package, load_module +from aea.crypto.registries import Registry +from aea.exceptions import AEAException, enforce +from aea.helpers.base import load_module + +contract_registry: Registry["Contract"] = Registry["Contract"]() logger = logging.getLogger(__name__) @@ -58,7 +62,8 @@ def id(self) -> ContractId: @property def configuration(self) -> ContractConfig: """Get the configuration.""" - assert self._configuration is not None, "Configuration not set." + if self._configuration is None: # pragma: nocover + raise ValueError("Configuration not set.") return cast(ContractConfig, super().configuration) @classmethod @@ -88,7 +93,7 @@ def from_dir(cls, directory: str, **kwargs) -> "Contract": """ configuration = cast( ContractConfig, - ComponentConfiguration.load(ComponentType.CONTRACT, Path(directory)), + load_component_configuration(ComponentType.CONTRACT, Path(directory)), ) configuration.directory = Path(directory) return Contract.from_config(configuration, **kwargs) @@ -101,9 +106,8 @@ def from_config(cls, configuration: ContractConfig, **kwargs) -> "Contract": :param configuration: the contract configuration. :return: the contract object. """ - assert ( - configuration.directory is not None - ), "Configuration must be associated with a directory." + if configuration.directory is None: # pragma: nocover + raise ValueError("Configuration must be associated with a directory.") directory = configuration.directory load_aea_package(configuration) contract_module = load_module("contracts", directory / "contract.py") @@ -113,16 +117,16 @@ def from_config(cls, configuration: ContractConfig, **kwargs) -> "Contract": filter(lambda x: re.match(contract_class_name, x[0]), classes) ) name_to_class = dict(contract_classes) - logger.debug("Processing contract {}".format(contract_class_name)) + logger.debug(f"Processing contract {contract_class_name}") contract_class = name_to_class.get(contract_class_name, None) - assert contract_class_name is not None, "Contract class '{}' not found.".format( - contract_class_name + enforce( + contract_class is not None, + f"Contract class '{contract_class_name}' not found.", ) - # TODO: load interfaces here - # contract_interface = configuration.contract_interfaces - - return contract_class(configuration, **kwargs) + _try_to_register_contract(configuration) + contract = contract_registry.make(str(configuration.public_id), **kwargs) + return contract @classmethod def get_deploy_transaction( @@ -192,3 +196,25 @@ def get_state( :return: the tx """ raise NotImplementedError + + +def _try_to_register_contract(configuration: ContractConfig): + """Register a contract to the registry.""" + if str(configuration.public_id) in contract_registry.specs: # pragma: nocover + logger.warning( + f"Skipping registration of contract {configuration.public_id} since already registered." + ) + return + logger.debug(f"Registering contract {configuration.public_id}") # pragma: nocover + try: # pragma: nocover + contract_registry.register( + id_=str(configuration.public_id), + entry_point=f"{configuration.prefix_import_path}.contract:{configuration.class_name}", + class_kwargs={"contract_interface": configuration.contract_interfaces}, + contract_config=configuration, + ) + except AEAException as e: # pragma: nocover + if "Cannot re-register id:" in str(e): + logger.warning("Already registered: {}".format(configuration.class_name)) + else: + raise e diff --git a/aea/contracts/scaffold/contract.py b/aea/contracts/scaffold/contract.py index 7336d841df..a472786e28 100644 --- a/aea/contracts/scaffold/contract.py +++ b/aea/contracts/scaffold/contract.py @@ -19,12 +19,59 @@ """This module contains the scaffold contract definition.""" +from typing import Any, Dict + from aea.contracts.base import Contract +from aea.crypto.base import LedgerApi class MyScaffoldContract(Contract): - """ - The scaffold contract class for an ethereum based smart contract. + """The scaffold contract class for a smart contract.""" + + @classmethod + def get_raw_transaction( + cls, ledger_api: LedgerApi, contract_address: str, **kwargs + ) -> Dict[str, Any]: + """ + Handler method for the 'GET_RAW_TRANSACTION' requests. + + Implement this method in the sub class if you want + to handle the contract requests manually. + + :param ledger_api: the ledger apis. + :param contract_address: the contract address. + :return: the tx + """ + raise NotImplementedError + + @classmethod + def get_raw_message( + cls, ledger_api: LedgerApi, contract_address: str, **kwargs + ) -> Dict[str, Any]: + """ + Handler method for the 'GET_RAW_MESSAGE' requests. + + Implement this method in the sub class if you want + to handle the contract requests manually. + + :param ledger_api: the ledger apis. + :param contract_address: the contract address. + :return: the tx + """ + raise NotImplementedError + + @classmethod + def get_state( + cls, ledger_api: LedgerApi, contract_address: str, **kwargs + ) -> Dict[str, Any]: + """ + Handler method for the 'GET_STATE' requests. + + Implement this method in the sub class if you want + to handle the contract requests manually. - For non-ethereum based contracts import `from aea.contracts.base import Contract` and extend accordingly. - """ + :param ledger_api: the ledger apis. + :param contract_address: the contract address. + :return: the tx + """ + raise NotImplementedError diff --git a/aea/contracts/scaffold/contract.yaml b/aea/contracts/scaffold/contract.yaml index 1c3991adaa..75d77014fe 100644 --- a/aea/contracts/scaffold/contract.yaml +++ b/aea/contracts/scaffold/contract.yaml @@ -1,12 +1,13 @@ name: scaffold author: fetchai version: 0.1.0 +type: contract description: The scaffold contract scaffolds a contract to be implemented by the developer. license: Apache-2.0 -aea_version: '>=0.5.0, <0.6.0' +aea_version: '>=0.6.0, <0.7.0' fingerprint: __init__.py: QmPBwWhEg3wcH1q9612srZYAYdANVdWLDFWKs7TviZmVj6 - contract.py: QmY4h4X9iwgvjsHhRan8bwbA6EfReTeyNTfBVvZoVKEhmA + contract.py: QmZ2b8pRckXaBpeSdw1nnEqy3dT4Gro7aijpYU9QKFTJdk fingerprint_ignore_patterns: [] class_name: MyScaffoldContract contract_interface_paths: {} diff --git a/aea/crypto/base.py b/aea/crypto/base.py index 2b3b1e7225..2f70e8513b 100644 --- a/aea/crypto/base.py +++ b/aea/crypto/base.py @@ -22,7 +22,7 @@ from abc import ABC, abstractmethod from typing import Any, BinaryIO, Dict, Generic, Optional, Tuple, TypeVar -from aea.mail.base import Address +from aea.common import Address EntityClass = TypeVar("EntityClass") @@ -178,9 +178,9 @@ def generate_tx_nonce(seller: Address, client: Address) -> str: :return: return the hash in hex. """ - @staticmethod + @classmethod @abstractmethod - def get_address_from_public_key(public_key: str) -> str: + def get_address_from_public_key(cls, public_key: str) -> str: """ Get the address from the public key. @@ -188,10 +188,10 @@ def get_address_from_public_key(public_key: str) -> str: :return: str """ - @staticmethod + @classmethod @abstractmethod def recover_message( - message: bytes, signature: str, is_deprecated_mode: bool = False + cls, message: bytes, signature: str, is_deprecated_mode: bool = False ) -> Tuple[Address, ...]: """ Recover the addresses from the hash. diff --git a/aea/crypto/cosmos.py b/aea/crypto/cosmos.py index aa0b7903f0..0c49fed96a 100644 --- a/aea/crypto/cosmos.py +++ b/aea/crypto/cosmos.py @@ -27,8 +27,9 @@ import subprocess # nosec import tempfile import time +from collections import namedtuple from pathlib import Path -from typing import Any, BinaryIO, Dict, Optional, Tuple +from typing import Any, BinaryIO, Dict, List, Optional, Tuple from bech32 import bech32_encode, convertbits @@ -37,24 +38,142 @@ import requests +from aea.common import Address from aea.crypto.base import Crypto, FaucetApi, Helper, LedgerApi +from aea.exceptions import AEAEnforceError from aea.helpers.base import try_decorator -from aea.mail.base import Address logger = logging.getLogger(__name__) _COSMOS = "cosmos" -COSMOS_TESTNET_FAUCET_URL = "https://faucet-agent-land.prod.fetch-ai.com:443/claim" TESTNET_NAME = "testnet" -DEFAULT_ADDRESS = "https://rest-agent-land.prod.fetch-ai.com:443" -DEFAULT_CURRENCY_DENOM = "atestfet" -DEFAULT_CHAIN_ID = "agent-land" +DEFAULT_FAUCET_URL = "INVALID_URL" +DEFAULT_ADDRESS = "INVALID_URL" +DEFAULT_CURRENCY_DENOM = "INVALID_CURRENCY_DENOM" +DEFAULT_CHAIN_ID = "INVALID_CHAIN_ID" + + +class CosmosHelper(Helper): + """Helper class usable as Mixin for CosmosApi or as standalone class.""" + + address_prefix = _COSMOS + + @staticmethod + def is_transaction_settled(tx_receipt: Any) -> bool: + """ + Check whether a transaction is settled or not. + + :param tx_digest: the digest associated to the transaction. + :return: True if the transaction has been settled, False o/w. + """ + is_successful = False + if tx_receipt is not None: + is_successful = True + return is_successful + + @staticmethod + def is_transaction_valid( + tx: Any, seller: Address, client: Address, tx_nonce: str, amount: int, + ) -> bool: + """ + Check whether a transaction is valid or not. + + :param tx: the transaction. + :param seller: the address of the seller. + :param client: the address of the client. + :param tx_nonce: the transaction nonce. + :param amount: the amount we expect to get from the transaction. + :return: True if the random_message is equals to tx['input'] + """ + if tx is None: + return False # pragma: no cover + + try: + _tx = tx.get("tx").get("value").get("msg")[0] + recovered_amount = int(_tx.get("value").get("amount")[0].get("amount")) + sender = _tx.get("value").get("from_address") + recipient = _tx.get("value").get("to_address") + is_valid = ( + recovered_amount == amount and sender == client and recipient == seller + ) + except (KeyError, IndexError): # pragma: no cover + is_valid = False + return is_valid + + @staticmethod + def generate_tx_nonce(seller: Address, client: Address) -> str: + """ + Generate a unique hash to distinguish txs with the same terms. + + :param seller: the address of the seller. + :param client: the address of the client. + :return: return the hash in hex. + """ + time_stamp = int(time.time()) + aggregate_hash = hashlib.sha256( + b"".join([seller.encode(), client.encode(), time_stamp.to_bytes(32, "big")]) + ) + return aggregate_hash.hexdigest() + + @classmethod + def get_address_from_public_key(cls, public_key: str) -> str: + """ + Get the address from the public key. + + :param public_key: the public key + :return: str + """ + public_key_bytes = bytes.fromhex(public_key) + s = hashlib.new("sha256", public_key_bytes).digest() + r = hashlib.new("ripemd160", s).digest() + five_bit_r = convertbits(r, 8, 5) + if five_bit_r is None: # pragma: nocover + raise AEAEnforceError("Unsuccessful bech32.convertbits call") + address = bech32_encode(cls.address_prefix, five_bit_r) + return address + + @classmethod + def recover_message( + cls, message: bytes, signature: str, is_deprecated_mode: bool = False + ) -> Tuple[Address, ...]: + """ + Recover the addresses from the hash. + + :param message: the message we expect + :param signature: the transaction signature + :param is_deprecated_mode: if the deprecated signing was used + :return: the recovered addresses + """ + signature_b64 = base64.b64decode(signature) + verifying_keys = VerifyingKey.from_public_key_recovery( + signature_b64, message, SECP256k1, hashfunc=hashlib.sha256, + ) + public_keys = [ + verifying_key.to_string("compressed").hex() + for verifying_key in verifying_keys + ] + addresses = [ + cls.get_address_from_public_key(public_key) for public_key in public_keys + ] + return tuple(addresses) + + @staticmethod + def get_hash(message: bytes) -> str: + """ + Get the hash of a message. + + :param message: the message to be hashed. + :return: the hash of the message. + """ + digest = hashlib.sha256(message).hexdigest() + return digest class CosmosCrypto(Crypto[SigningKey]): """Class wrapping the Account Generation from Ethereum ledger.""" identifier = _COSMOS + helper = CosmosHelper def __init__(self, private_key_path: Optional[str] = None): """ @@ -64,7 +183,7 @@ def __init__(self, private_key_path: Optional[str] = None): """ super().__init__(private_key_path=private_key_path) self._public_key = self.entity.get_verifying_key().to_string("compressed").hex() - self._address = CosmosHelper.get_address_from_public_key(self.public_key) + self._address = self.helper.get_address_from_public_key(self.public_key) @property def private_key(self) -> str: @@ -107,7 +226,9 @@ def load_private_key_from_path(cls, file_name) -> SigningKey: signing_key = SigningKey.from_string(bytes.fromhex(data), curve=SECP256k1) return signing_key - def sign_message(self, message: bytes, is_deprecated_mode: bool = False) -> str: + def sign_message( # pylint: disable=unused-argument + self, message: bytes, is_deprecated_mode: bool = False + ) -> str: """ Sign a message in bytes string form. @@ -126,7 +247,7 @@ def format_default_transaction( transaction: Any, signature: str, base64_pbk: str ) -> Any: """ - Format default CosmosSDK transaction and add signature + Format default CosmosSDK transaction and add signature. :param transaction: the transaction to be formatted :param signature: the transaction signature @@ -160,7 +281,7 @@ def format_wasm_transaction( transaction: Any, signature: str, base64_pbk: str ) -> Any: """ - Format CosmWasm transaction and add signature + Format CosmWasm transaction and add signature. :param transaction: the transaction to be formatted :param signature: the transaction signature @@ -168,7 +289,6 @@ def format_wasm_transaction( :return: formatted transaction with signature """ - pushable_tx = { "type": "cosmos-sdk/StdTx", "value": { @@ -195,7 +315,6 @@ def sign_transaction(self, transaction: Any) -> Any: :param transaction: the transaction to be signed :return: signed transaction """ - transaction_str = json.dumps(transaction, separators=(",", ":"), sort_keys=True) transaction_bytes = transaction_str.encode("utf-8") signed_transaction = self.sign_message(transaction_bytes) @@ -210,10 +329,9 @@ def sign_transaction(self, transaction: Any) -> Any: return self.format_wasm_transaction( transaction, signed_transaction, base64_pbk ) - else: - return self.format_default_transaction( - transaction, signed_transaction, base64_pbk - ) + return self.format_default_transaction( + transaction, signed_transaction, base64_pbk + ) @classmethod def generate_private_key(cls) -> SigningKey: @@ -231,129 +349,14 @@ def dump(self, fp: BinaryIO) -> None: fp.write(self.private_key.encode("utf-8")) -class CosmosHelper(Helper): - """Helper class usable as Mixin for CosmosApi or as standalone class.""" - - @staticmethod - def is_transaction_settled(tx_receipt: Any) -> bool: - """ - Check whether a transaction is settled or not. - - :param tx_digest: the digest associated to the transaction. - :return: True if the transaction has been settled, False o/w. - """ - is_successful = False - if tx_receipt is not None: - # TODO: quick fix only, not sure this is reliable - is_successful = True - return is_successful - - @staticmethod - def is_transaction_valid( - tx: Any, seller: Address, client: Address, tx_nonce: str, amount: int, - ) -> bool: - """ - Check whether a transaction is valid or not. - - :param tx: the transaction. - :param seller: the address of the seller. - :param client: the address of the client. - :param tx_nonce: the transaction nonce. - :param amount: the amount we expect to get from the transaction. - :return: True if the random_message is equals to tx['input'] - """ - if tx is None: - return False # pragma: no cover - - try: - _tx = tx.get("tx").get("value").get("msg")[0] - recovered_amount = int(_tx.get("value").get("amount")[0].get("amount")) - sender = _tx.get("value").get("from_address") - recipient = _tx.get("value").get("to_address") - is_valid = ( - recovered_amount == amount and sender == client and recipient == seller - ) - except (KeyError, IndexError): # pragma: no cover - is_valid = False - return is_valid - - @staticmethod - def generate_tx_nonce(seller: Address, client: Address) -> str: - """ - Generate a unique hash to distinguish txs with the same terms. - - :param seller: the address of the seller. - :param client: the address of the client. - :return: return the hash in hex. - """ - time_stamp = int(time.time()) - aggregate_hash = hashlib.sha256( - b"".join([seller.encode(), client.encode(), time_stamp.to_bytes(32, "big")]) - ) - return aggregate_hash.hexdigest() - - @staticmethod - def get_address_from_public_key(public_key: str) -> str: - """ - Get the address from the public key. - - :param public_key: the public key - :return: str - """ - public_key_bytes = bytes.fromhex(public_key) - s = hashlib.new("sha256", public_key_bytes).digest() - r = hashlib.new("ripemd160", s).digest() - five_bit_r = convertbits(r, 8, 5) - assert five_bit_r is not None, "Unsuccessful bech32.convertbits call" - address = bech32_encode(_COSMOS, five_bit_r) - return address - - @staticmethod - def recover_message( - message: bytes, signature: str, is_deprecated_mode: bool = False - ) -> Tuple[Address, ...]: - """ - Recover the addresses from the hash. - - :param message: the message we expect - :param signature: the transaction signature - :param is_deprecated_mode: if the deprecated signing was used - :return: the recovered addresses - """ - signature_b64 = base64.b64decode(signature) - verifying_keys = VerifyingKey.from_public_key_recovery( - signature_b64, message, SECP256k1, hashfunc=hashlib.sha256, - ) - public_keys = [ - verifying_key.to_string("compressed").hex() - for verifying_key in verifying_keys - ] - addresses = [ - CosmosHelper.get_address_from_public_key(public_key) - for public_key in public_keys - ] - return tuple(addresses) - - @staticmethod - def get_hash(message: bytes) -> str: - """ - Get the hash of a message. - - :param message: the message to be hashed. - :return: the hash of the message. - """ - digest = hashlib.sha256(message).hexdigest() - return digest - - -class CosmosApi(LedgerApi, CosmosHelper): +class _CosmosApi(LedgerApi): """Class to interact with the Cosmos SDK via a HTTP APIs.""" identifier = _COSMOS def __init__(self, **kwargs): """ - Initialize the Ethereum ledger APIs. + Initialize the Cosmos ledger APIs. """ self._api = None self.network_address = kwargs.pop("address", DEFAULT_ADDRESS) @@ -387,7 +390,7 @@ def _try_get_balance(self, address: Address) -> Optional[int]: balance = int(result[0]["amount"]) return balance - def get_deploy_transaction( + def get_deploy_transaction( # pylint: disable=arguments-differ self, contract_interface: Dict[str, str], deployer_address: Address, @@ -820,45 +823,184 @@ def get_contract_instance( # Instance object not available for cosmwasm return None + @staticmethod + def _execute_shell_command(command: List[str]) -> List[Dict[str, str]]: + """ + Execute command using subprocess and get result as JSON dict. + + :param command: the shell command to be executed + :return: the stdout result converted to JSON dict + """ + stdout, _ = subprocess.Popen( # nosec + command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ).communicate() + + return json.loads(stdout.decode("ascii")) + + def get_last_code_id(self) -> int: + """ + Get ID of latest deployed .wasm bytecode. + + :return: code id of last deployed .wasm bytecode + """ + + command = ["wasmcli", "query", "wasm", "list-code"] + res = self._execute_shell_command(command) + + return int(res[-1]["id"]) + + def get_contract_address(self, code_id: int) -> str: + """ + Get contract address of latest initialised contract by its ID. + + :param code_id: id of deployed CosmWasm bytecode + :return: contract address of last initialised contract + """ + + command = ["wasmcli", "query", "wasm", "list-contract-by-code", str(code_id)] + res = self._execute_shell_command(command) + + return res[-1]["address"] + + +class CosmosApi(_CosmosApi, CosmosHelper): + """Class to interact with the Cosmos SDK via a HTTP APIs.""" + class CosmWasmCLIWrapper: """Wrapper of the CosmWasm CLI.""" +""" Equivalent to: + +@dataclass +class CosmosFaucetStatus: + tx_digest: Optional[str] + status: str + status_code: int +""" +CosmosFaucetStatus = namedtuple( + "CosmosFaucetStatus", ["tx_digest", "status", "status_code"] +) + + class CosmosFaucetApi(FaucetApi): """Cosmos testnet faucet API.""" + FAUCET_STATUS_PENDING = 1 # noqa: F841 + FAUCET_STATUS_PROCESSING = 2 # noqa: F841 + FAUCET_STATUS_COMPLETED = 20 # noqa: F841 + FAUCET_STATUS_FAILED = 21 # noqa: F841 + FAUCET_STATUS_TIMED_OUT = 22 # noqa: F841 + FAUCET_STATUS_RATE_LIMITED = 23 # noqa: F841 + FAUCET_STATUS_RATE_UNAVAILABLE = 99 # noqa: F841 + identifier = _COSMOS + testnet_faucet_url = DEFAULT_FAUCET_URL testnet_name = TESTNET_NAME + def __init__(self, poll_interval=None): + """Initialize CosmosFaucetApi.""" + self._poll_interval = float(poll_interval or 1) + def get_wealth(self, address: Address) -> None: """ Get wealth from the faucet for the provided address. :param address: the address. :return: None + :raises: RuntimeError of explicit faucet failures """ - self._try_get_wealth(address) + uid = self._try_create_faucet_claim(address) + if uid is None: # pragma: nocover + raise RuntimeError("Unable to create faucet claim") - @staticmethod + while True: + + # lookup status form the claim uid + status = self._try_check_faucet_claim(uid) + if status is None: # pragma: nocover + raise RuntimeError("Failed to check faucet claim status") + + # if the status is complete + if status.status_code == self.FAUCET_STATUS_COMPLETED: + break + + # if the status is failure + if status.status_code > self.FAUCET_STATUS_COMPLETED: # pragma: nocover + raise RuntimeError(f"Failed to get wealth for {address}") + + # if the status is incomplete + time.sleep(self._poll_interval) + + @classmethod @try_decorator( - "An error occured while attempting to generate wealth:\n{}", + "An error occured while attempting to request a faucet request:\n{}", logger_method=logger.error, ) - def _try_get_wealth(address: Address) -> None: + def _try_create_faucet_claim(cls, address: Address) -> Optional[str]: """ - Get wealth from the faucet for the provided address. + Create a token faucet claim request - :param address: the address. - :return: None + :param address: the address to request funds + :return: None on failure, otherwise the request uid """ response = requests.post( - url=COSMOS_TESTNET_FAUCET_URL, data={"Address": address} + url=cls._faucet_request_uri(), data={"Address": address} ) + + uid = None if response.status_code == 200: - tx_hash = response.text - logger.info("Wealth generated, tx_hash: {}".format(tx_hash)) + data = response.json() + uid = data["uid"] + + logger.info("Wealth claim generated, uid: {}".format(uid)) else: # pragma: no cover logger.warning( "Response: {}, Text: {}".format(response.status_code, response.text) ) + + return uid + + @classmethod + @try_decorator( + "An error occured while attempting to request a faucet request:\n{}", + logger_method=logger.error, + ) + def _try_check_faucet_claim(cls, uid: str) -> Optional[CosmosFaucetStatus]: + """ + Check the status of a faucet request + + :param uid: The request uid to be checked + :return: None on failure otherwise a CosmosFaucetStatus for the specified uid + """ + response = requests.get(cls._faucet_status_uri(uid)) + if response.status_code != 200: # pragma: nocover + logger.warning( + "Response: {}, Text: {}".format(response.status_code, response.text) + ) + return None + + # parse the response + data = response.json() + return CosmosFaucetStatus( + tx_digest=data.get("txDigest"), + status=data["status"], + status_code=data["statusCode"], + ) + + @classmethod + def _faucet_request_uri(cls) -> str: + """ + Generates the request URI derived from `cls.faucet_base_url` + """ + if cls.testnet_faucet_url is None: # pragma: nocover + raise ValueError("Testnet faucet url not set.") + return f"{cls.testnet_faucet_url}/claim/requests" + + @classmethod + def _faucet_status_uri(cls, uid: str) -> str: + """ + Generates the status URI derived from `cls.faucet_base_url` + """ + return f"{cls._faucet_request_uri()}/{uid}" diff --git a/aea/crypto/ethereum.py b/aea/crypto/ethereum.py index 3856f8db17..8ff54ef609 100644 --- a/aea/crypto/ethereum.py +++ b/aea/crypto/ethereum.py @@ -27,7 +27,7 @@ from typing import Any, BinaryIO, Dict, Optional, Tuple, Union, cast from eth_account import Account -from eth_account.datastructures import AttributeDict +from eth_account.datastructures import SignedTransaction from eth_account.messages import encode_defunct from eth_keys import keys @@ -35,11 +35,12 @@ import requests from web3 import HTTPProvider, Web3 -from web3.contract import Contract as EthereumContract +from web3.types import TxParams +from aea.common import Address from aea.crypto.base import Crypto, FaucetApi, Helper, LedgerApi +from aea.exceptions import enforce from aea.helpers.base import try_decorator -from aea.mail.base import Address logger = logging.getLogger(__name__) @@ -138,7 +139,7 @@ def sign_transaction(self, transaction: Any) -> Any: :return: signed transaction """ signed_transaction = self.entity.sign_transaction(transaction_dict=transaction) - # Note: self.entity.signTransaction(transaction_dict=transaction) == signed_transaction + # Note: self.entity.signTransaction(transaction_dict=transaction) == signed_transaction # noqa: E800 return signed_transaction @classmethod @@ -212,8 +213,8 @@ def generate_tx_nonce(seller: Address, client: Address) -> str: ) return aggregate_hash.hex() - @staticmethod - def get_address_from_public_key(public_key: str) -> str: + @classmethod + def get_address_from_public_key(cls, public_key: str) -> str: """ Get the address from the public key. @@ -225,9 +226,9 @@ def get_address_from_public_key(public_key: str) -> str: address = Web3.toChecksumAddress(raw_address) return address - @staticmethod + @classmethod def recover_message( - message: bytes, signature: str, is_deprecated_mode: bool = False + cls, message: bytes, signature: str, is_deprecated_mode: bool = False ) -> Tuple[Address, ...]: """ Recover the addresses from the hash. @@ -238,7 +239,7 @@ def recover_message( :return: the recovered addresses """ if is_deprecated_mode: - assert len(message) == 32, "Message must be hashed to exactly 32 bytes." + enforce(len(message) == 32, "Message must be hashed to exactly 32 bytes.") with warnings.catch_warnings(): warnings.simplefilter("ignore") address = Account.recoverHash( # pylint: disable=no-value-for-parameter @@ -378,7 +379,7 @@ def _try_send_signed_transaction(self, tx_signed: Any) -> Optional[str]: :param tx_signed: the signed transaction :return: tx_digest, if present """ - tx_signed = cast(AttributeDict, tx_signed) + tx_signed = cast(SignedTransaction, tx_signed) hex_value = self._api.eth.sendRawTransaction( # pylint: disable=no-member tx_signed.rawTransaction ) @@ -447,15 +448,15 @@ def get_contract_instance( abi=contract_interface["abi"], bytecode=contract_interface["bytecode"], ) else: + _contract_address = self.api.toChecksumAddress(contract_address) instance = self.api.eth.contract( - address=contract_address, + address=_contract_address, abi=contract_interface["abi"], bytecode=contract_interface["bytecode"], ) - instance = cast(EthereumContract, instance) return instance - def get_deploy_transaction( + def get_deploy_transaction( # pylint: disable=arguments-differ self, contract_interface: Dict[str, str], deployer_address: Address, @@ -473,14 +474,15 @@ def get_deploy_transaction( :returns tx: the transaction dictionary. """ # create the transaction dict - nonce = self.api.eth.getTransactionCount(deployer_address) + _deployer_address = self.api.toChecksumAddress(deployer_address) + nonce = self.api.eth.getTransactionCount(_deployer_address) instance = self.get_contract_instance(contract_interface) - data = instance.constructor().__dict__.get("data_in_transaction") + data = instance.constructor(**kwargs).buildTransaction().get("data", "0x") tx = { "from": deployer_address, # only 'from' address, don't insert 'to' address! "value": value, # transfer as part of deployment "gas": gas, - "gasPrice": self.api.eth.gasPrice, # TODO: refine + "gasPrice": self.api.eth.gasPrice, "nonce": nonce, "data": data, } @@ -496,7 +498,8 @@ def try_estimate_gas(self, tx: Dict[str, Any]) -> Dict[str, Any]: """ try: # try estimate the gas and update the transaction dict - gas_estimate = self.api.eth.estimateGas(transaction=tx) + _tx = cast(TxParams, tx) + gas_estimate = self.api.eth.estimateGas(transaction=_tx) logger.debug("gas estimate: {}".format(gas_estimate)) tx["gas"] = gas_estimate except Exception as e: # pylint: disable=broad-except # pragma: nocover diff --git a/aea/crypto/fetchai.py b/aea/crypto/fetchai.py index 5f89252377..2d54b2a2b9 100644 --- a/aea/crypto/fetchai.py +++ b/aea/crypto/fetchai.py @@ -18,433 +18,53 @@ # ------------------------------------------------------------------------------ """Fetchai module wrapping the public and private key cryptography and ledger api.""" -import base64 -import hashlib -import json -import logging -import time -import warnings -from pathlib import Path -from typing import Any, BinaryIO, Dict, Optional, Tuple, cast -from ecdsa import SECP256k1, VerifyingKey -from ecdsa.util import sigencode_string_canonize +from aea.crypto.cosmos import CosmosCrypto, CosmosFaucetApi, CosmosHelper, _CosmosApi -from fetchai.ledger.api import LedgerApi as FetchaiLedgerApi -from fetchai.ledger.api.token import TokenTxFactory -from fetchai.ledger.api.tx import TxContents -from fetchai.ledger.crypto import Address as FetchaiAddress -from fetchai.ledger.crypto import Entity, Identity -from fetchai.ledger.serialisation import sha256_hash, transaction - -import requests - -from aea.crypto.base import Crypto, FaucetApi, Helper, LedgerApi -from aea.helpers.base import try_decorator -from aea.mail.base import Address - -logger = logging.getLogger(__name__) _FETCHAI = "fetchai" -DEFAULT_NETWORK = "testnet" -SUCCESSFUL_TERMINAL_STATES = ("Executed", "Submitted") -FETCHAI_TESTNET_FAUCET_URL = "https://explore-testnet.fetch.ai/api/v1/send_tokens/" +_FETCH = "fetch" TESTNET_NAME = "testnet" +FETCHAI_TESTNET_FAUCET_URL = "https://faucet-agent-land.fetch.ai" +DEFAULT_ADDRESS = "https://rest-agent-land.fetch.ai" +DEFAULT_CURRENCY_DENOM = "atestfet" +DEFAULT_CHAIN_ID = "agent-land" -class FetchAICrypto(Crypto[Entity]): - """Class wrapping the Entity Generation from Fetch.AI ledger.""" - - identifier = _FETCHAI - - def __init__(self, private_key_path: Optional[str] = None): - """ - Instantiate a fetchai crypto object. - - :param private_key_path: the private key path of the agent - """ - super().__init__(private_key_path=private_key_path) - self._address = str(FetchaiAddress(Identity.from_hex(self.public_key))) - - @property - def private_key(self) -> str: - """ - Return a private key. - - :return: a private key string - """ - return self.entity.private_key_hex - - @property - def public_key(self) -> str: - """ - Return a public key in hex format. - - :return: a public key string in hex format - """ - return self.entity.public_key_hex - - @property - def address(self) -> str: - """ - Return the address for the key pair. - - :return: a display_address str - """ - return self._address - - @classmethod - def load_private_key_from_path(cls, file_name: str) -> Entity: - """ - Load a private key in hex format from a file. - - :param file_name: the path to the hex file. - - :return: the Entity. - """ - path = Path(file_name) - with path.open() as key: - data = key.read() - entity = Entity.from_hex(data) - return entity - - @classmethod - def generate_private_key(cls) -> Entity: - """Generate a key pair for fetchai network.""" - entity = Entity() - return entity - - def sign_message(self, message: bytes, is_deprecated_mode: bool = False) -> str: - """ - Sign a message in bytes string form. - - :param message: the message we want to send - :param is_deprecated_mode: if the deprecated signing is used - :return: signature of the message in string form - """ - signature_compact = self.entity.signing_key.sign_deterministic( - message, hashfunc=hashlib.sha256, sigencode=sigencode_string_canonize, - ) - signature_base64_str = base64.b64encode(signature_compact).decode("utf-8") - return signature_base64_str - - def sign_transaction(self, transaction: Any) -> Any: - """ - Sign a transaction in bytes string form. - - :param transaction: the transaction to be signed - :return: signed transaction - """ - identity = Identity.from_hex(self.public_key) - transaction.add_signer(identity) - transaction.sign(self.entity) - return transaction - - def dump(self, fp: BinaryIO) -> None: - """ - Serialize crypto object as binary stream to `fp` (a `.write()`-supporting file-like object). - - :param fp: the output file pointer. Must be set in binary mode (mode='wb') - :return: None - """ - fp.write(self.private_key.encode("utf-8")) - - -class FetchAIHelper(Helper): +class FetchAIHelper(CosmosHelper): """Helper class usable as Mixin for FetchAIApi or as standalone class.""" - @staticmethod - def is_transaction_settled(tx_receipt: Any) -> bool: - """ - Check whether a transaction is settled or not. - - :param tx_digest: the digest associated to the transaction. - :return: True if the transaction has been settled, False o/w. - """ - is_successful = False - if tx_receipt is not None: - is_successful = tx_receipt.status in SUCCESSFUL_TERMINAL_STATES - return is_successful - - @staticmethod - def is_transaction_valid( - tx: Any, seller: Address, client: Address, tx_nonce: str, amount: int, - ) -> bool: - """ - Check whether a transaction is valid or not. - - :param tx: the transaction. - :param seller: the address of the seller. - :param client: the address of the client. - :param tx_nonce: the transaction nonce. - :param amount: the amount we expect to get from the transaction. - :return: True if the random_message is equals to tx['input'] - """ - is_valid = False - if tx is not None: - seller_address = FetchaiAddress(seller) - is_valid = ( - str(tx.from_address) == client - and amount == tx.transfers[seller_address] - # and self.is_transaction_settled(tx_digest=tx_digest) - ) - return is_valid + address_prefix = _FETCH - @staticmethod - def generate_tx_nonce(seller: Address, client: Address) -> str: - """ - Generate a unique hash to distinguish txs with the same terms. - - :param seller: the address of the seller. - :param client: the address of the client. - :return: return the hash in hex. - """ - time_stamp = int(time.time()) - aggregate_hash = sha256_hash( - b"".join([seller.encode(), client.encode(), time_stamp.to_bytes(32, "big")]) - ) - return aggregate_hash.hex() - - @staticmethod - def get_address_from_public_key(public_key: str) -> Address: - """ - Get the address from the public key. - - :param public_key: the public key - :return: str - """ - identity = Identity.from_hex(public_key) - address = str(FetchaiAddress(identity)) - return address - @staticmethod - def recover_message( - message: bytes, signature: str, is_deprecated_mode: bool = False - ) -> Tuple[Address, ...]: - """ - Recover the addresses from the hash. - - :param message: the message we expect - :param signature: the transaction signature - :param is_deprecated_mode: if the deprecated signing was used - :return: the recovered addresses - """ - signature_b64 = base64.b64decode(signature) - verifying_keys = VerifyingKey.from_public_key_recovery( - signature_b64, message, SECP256k1, hashfunc=hashlib.sha256, - ) - public_keys = [ - verifying_key.to_string("compressed").hex() - for verifying_key in verifying_keys - ] - addresses = [ - FetchAIHelper.get_address_from_public_key(public_key) - for public_key in public_keys - ] - return tuple(addresses) - - @staticmethod - def get_hash(message: bytes) -> str: - """ - Get the hash of a message. +class FetchAICrypto(CosmosCrypto): + """Class wrapping the Entity Generation from Fetch.AI ledger.""" - :param message: the message to be hashed. - :return: the hash of the message. - """ - digest = sha256_hash(message) - return digest.hex() + identifier = _FETCHAI + helper = FetchAIHelper -class FetchAIApi(LedgerApi, FetchAIHelper): +class FetchAIApi(_CosmosApi, FetchAIHelper): """Class to interact with the Fetch ledger APIs.""" identifier = _FETCHAI def __init__(self, **kwargs): """ - Initialize the Fetch.AI ledger APIs. - - :param kwargs: key word arguments (expects either a pair of 'host' and 'port' or a 'network') - """ - if not ("host" in kwargs and "port" in kwargs): - network = kwargs.pop("network", DEFAULT_NETWORK) - kwargs["network"] = network - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - self._api = FetchaiLedgerApi(**kwargs) - - @property - def api(self) -> FetchaiLedgerApi: - """Get the underlying API object.""" - return self._api - - def get_balance(self, address: Address) -> Optional[int]: - """ - Get the balance of a given account. - - :param address: the address for which to retrieve the balance. - :return: the balance, if retrivable, otherwise None - """ - balance = self._try_get_balance(address) - return balance - - @try_decorator("Unable to retrieve balance: {}", logger_method="debug") - def _try_get_balance(self, address: Address) -> Optional[int]: - """Try get the balance.""" - return self._api.tokens.balance(FetchaiAddress(address)) - - def get_transfer_transaction( # pylint: disable=arguments-differ - self, - sender_address: Address, - destination_address: Address, - amount: int, - tx_fee: int, - tx_nonce: str, - **kwargs, - ) -> Optional[Any]: - """ - Submit a transfer transaction to the ledger. - - :param sender_address: the sender address of the payer. - :param destination_address: the destination address of the payee. - :param amount: the amount of wealth to be transferred. - :param tx_fee: the transaction fee. - :param tx_nonce: verifies the authenticity of the tx - :return: the transfer transaction - """ - tx = TokenTxFactory.transfer( - FetchaiAddress(sender_address), - FetchaiAddress(destination_address), - amount, - tx_fee, - [], # we don't add signer here as we would need the public key for this - ) - self._api.set_validity_period(tx) - return tx - - def send_signed_transaction(self, tx_signed: Any) -> Optional[str]: - """ - Send a signed transaction and wait for confirmation. - - :param tx_signed: the signed transaction - """ - encoded_tx = transaction.encode_transaction(tx_signed) - endpoint = "transfer" if tx_signed.transfers is not None else "create" - return self.api.tokens._post_tx_json( # pylint: disable=protected-access - encoded_tx, endpoint - ) - - def get_transaction_receipt(self, tx_digest: str) -> Optional[Any]: + Initialize the Fetch.ai ledger APIs. """ - Get the transaction receipt for a transaction digest (non-blocking). + if "address" not in kwargs: + kwargs["address"] = DEFAULT_ADDRESS # pragma: nocover + if "denom" not in kwargs: + kwargs["denom"] = DEFAULT_CURRENCY_DENOM + if "chain_id" not in kwargs: + kwargs["chain_id"] = DEFAULT_CHAIN_ID + super().__init__(**kwargs) - :param tx_digest: the digest associated to the transaction. - :return: the tx receipt, if present - """ - tx_receipt = self._try_get_transaction_receipt(tx_digest) - return tx_receipt - - @try_decorator( - "Error when attempting getting tx receipt: {}", logger_method="debug" - ) - def _try_get_transaction_receipt(self, tx_digest: str) -> Optional[Any]: - """ - Get the transaction receipt (non-blocking). - :param tx_digest: the transaction digest. - :return: the transaction receipt, if found - """ - return self._api.tx.status(tx_digest) - - def get_transaction(self, tx_digest: str) -> Optional[Any]: - """ - Get the transaction for a transaction digest. - - :param tx_digest: the digest associated to the transaction. - :return: the tx, if present - """ - tx = self._try_get_transaction(tx_digest) - return tx - - @try_decorator("Error when attempting getting tx: {}", logger_method="debug") - def _try_get_transaction(self, tx_digest: str) -> Optional[TxContents]: - """ - Try get the transaction (non-blocking). - - :param tx_digest: the transaction digest. - :return: the tx, if found - """ - return cast(TxContents, self._api.tx.contents(tx_digest)) - - def get_contract_instance( - self, contract_interface: Dict[str, str], contract_address: Optional[str] = None - ) -> Any: - """ - Get the instance of a contract. - - :param contract_interface: the contract interface. - :param contract_address: the contract address. - :return: the contract instance - """ - raise NotImplementedError - - def get_deploy_transaction( - self, contract_interface: Dict[str, str], deployer_address: Address, **kwargs, - ) -> Dict[str, Any]: - """ - Get the transaction to deploy the smart contract. - - :param contract_interface: the contract interface. - :param deployer_address: The address that will deploy the contract. - :returns tx: the transaction dictionary. - """ - raise NotImplementedError - - -class FetchAIFaucetApi(FaucetApi): +class FetchAIFaucetApi(CosmosFaucetApi): """Fetchai testnet faucet API.""" identifier = _FETCHAI testnet_name = TESTNET_NAME - - def get_wealth(self, address: Address) -> None: - """ - Get wealth from the faucet for the provided address. - - :param address: the address. - :return: None - """ - self._try_get_wealth(address) - - @staticmethod - @try_decorator( - "An error occured while attempting to generate wealth:\n{}", - logger_method="error", - ) - def _try_get_wealth(address: Address) -> None: - """ - Get wealth from the faucet for the provided address. - - :param address: the address. - :return: None - """ - payload = json.dumps({"address": address}) - response = requests.post(FETCHAI_TESTNET_FAUCET_URL, data=payload) - if response.status_code // 100 == 5: - logger.error("Response: {}".format(response.status_code)) - else: - response_dict = json.loads(response.text) - if response_dict.get("error_message") is not None: - logger.warning( - "Response: {}\nMessage: {}".format( - response.status_code, response_dict.get("error_message") - ) - ) - else: - logger.info( - "Response: {}\nMessage: {}\nDigest: {}".format( - response.status_code, - response_dict.get("message"), - response_dict.get("digest"), - ) - ) # pragma: no cover + testnet_faucet_url = FETCHAI_TESTNET_FAUCET_URL diff --git a/aea/crypto/helpers.py b/aea/crypto/helpers.py index ad6e1bf970..53d64a31a9 100644 --- a/aea/crypto/helpers.py +++ b/aea/crypto/helpers.py @@ -23,21 +23,11 @@ import sys from pathlib import Path -from aea.configurations.base import AgentConfig, DEFAULT_AEA_CONFIG_FILE -from aea.configurations.loader import ConfigLoader -from aea.crypto.cosmos import CosmosCrypto -from aea.crypto.ethereum import EthereumCrypto -from aea.crypto.fetchai import FetchAICrypto +from aea.configurations.base import AgentConfig, DEFAULT_AEA_CONFIG_FILE, PackageType +from aea.configurations.loader import ConfigLoaders from aea.crypto.registries import crypto_registry, make_crypto, make_faucet_api -COSMOS_PRIVATE_KEY_FILE = "cosmos_private_key.txt" -FETCHAI_PRIVATE_KEY_FILE = "fet_private_key.txt" -ETHEREUM_PRIVATE_KEY_FILE = "eth_private_key.txt" -IDENTIFIER_TO_KEY_FILES = { - CosmosCrypto.identifier: COSMOS_PRIVATE_KEY_FILE, - EthereumCrypto.identifier: ETHEREUM_PRIVATE_KEY_FILE, - FetchAICrypto.identifier: FETCHAI_PRIVATE_KEY_FILE, -} +PRIVATE_KEY_PATH_SCHEMA = "{}_private_key.txt" logger = logging.getLogger(__name__) @@ -48,20 +38,27 @@ def verify_or_create_private_keys( """ Verify or create private keys. - :param ctx: Context + :param aea_project_path: path to an AEA project. + :param exit_on_error: whether we should exit the program on error. + :return: the agent configuration. """ path_to_aea_config = aea_project_path / DEFAULT_AEA_CONFIG_FILE - agent_loader = ConfigLoader("aea-config_schema.json", AgentConfig) + agent_loader = ConfigLoaders.from_package_type(PackageType.AGENT) fp = path_to_aea_config.open(mode="r", encoding="utf-8") aea_conf = agent_loader.load(fp) - for identifier, _value in aea_conf.private_key_paths.read_all(): + for identifier, _ in aea_conf.private_key_paths.read_all(): if identifier not in crypto_registry.supported_ids: # pragma: nocover - ValueError("Unsupported identifier in private key paths.") + raise ValueError( + "Unsupported identifier `{}` in private key paths. Supported identifiers: {}.".format( + identifier, sorted(crypto_registry.supported_ids) + ) + ) - for identifier, private_key_path in IDENTIFIER_TO_KEY_FILES.items(): + for identifier in crypto_registry.supported_ids: config_private_key_path = aea_conf.private_key_paths.read(identifier) if config_private_key_path is None: + private_key_path = PRIVATE_KEY_PATH_SCHEMA.format(identifier) if identifier == aea_conf.default_ledger: # pragma: nocover create_private_key( identifier, @@ -72,13 +69,13 @@ def verify_or_create_private_keys( try: try_validate_private_key_path( identifier, - str(aea_project_path / private_key_path), + str(aea_project_path / config_private_key_path), exit_on_error=exit_on_error, ) except FileNotFoundError: # pragma: no cover raise ValueError( "File {} for private key {} not found.".format( - repr(private_key_path), identifier, + repr(config_private_key_path), identifier, ) ) @@ -128,12 +125,15 @@ def create_private_key(ledger_id: str, private_key_file: str) -> None: crypto.dump(open(private_key_file, "wb")) -def try_generate_testnet_wealth(identifier: str, address: str) -> None: +def try_generate_testnet_wealth( + identifier: str, address: str, _sync: bool = True +) -> None: """ Try generate wealth on a testnet. :param identifier: the identifier of the ledger :param address: the address to check for + :param _sync: whether to wait to sync or not; currently unused :return: None """ faucet_api = make_faucet_api(identifier) diff --git a/aea/crypto/ledger_apis.py b/aea/crypto/ledger_apis.py index 29aa10cf2f..bdf82496f4 100644 --- a/aea/crypto/ledger_apis.py +++ b/aea/crypto/ledger_apis.py @@ -21,19 +21,21 @@ import logging from typing import Any, Dict, Optional, Tuple, Union +from aea.common import Address from aea.configurations.constants import DEFAULT_LEDGER from aea.crypto.base import LedgerApi from aea.crypto.cosmos import CosmosApi from aea.crypto.cosmos import DEFAULT_ADDRESS as COSMOS_DEFAULT_ADDRESS from aea.crypto.ethereum import DEFAULT_ADDRESS as ETHEREUM_DEFAULT_ADDRESS from aea.crypto.ethereum import DEFAULT_CHAIN_ID, EthereumApi -from aea.crypto.fetchai import DEFAULT_NETWORK, FetchAIApi +from aea.crypto.fetchai import DEFAULT_ADDRESS as FETCHAI_DEFAULT_ADDRESS +from aea.crypto.fetchai import FetchAIApi from aea.crypto.registries import ( ledger_apis_registry, make_ledger_api, make_ledger_api_cls, ) -from aea.mail.base import Address +from aea.exceptions import enforce DEFAULT_LEDGER_CONFIGS = { CosmosApi.identifier: {"address": COSMOS_DEFAULT_ADDRESS}, @@ -41,7 +43,7 @@ "address": ETHEREUM_DEFAULT_ADDRESS, "chain_id": DEFAULT_CHAIN_ID, }, - FetchAIApi.identifier: {"network": DEFAULT_NETWORK}, + FetchAIApi.identifier: {"address": FETCHAI_DEFAULT_ADDRESS}, } # type: Dict[str, Dict[str, Union[str, int]]] logger = logging.getLogger(__name__) @@ -60,9 +62,10 @@ def has_ledger(identifier: str) -> bool: @classmethod def get_api(cls, identifier: str) -> LedgerApi: """Get the ledger API.""" - assert ( - identifier in ledger_apis_registry.supported_ids - ), "Not a registered ledger api identifier." + enforce( + identifier in ledger_apis_registry.supported_ids, + "Not a registered ledger api identifier.", + ) api = make_ledger_api(identifier, **cls.ledger_api_configs[identifier]) return api @@ -75,9 +78,10 @@ def get_balance(cls, identifier: str, address: str) -> Optional[int]: :param address: the address to check for :return: the token balance """ - assert ( - identifier in ledger_apis_registry.supported_ids - ), "Not a registered ledger api identifier." + enforce( + identifier in ledger_apis_registry.supported_ids, + "Not a registered ledger api identifier.", + ) api = make_ledger_api(identifier, **cls.ledger_api_configs[identifier]) balance = api.get_balance(address) return balance @@ -105,9 +109,10 @@ def get_transfer_transaction( :return: tx """ - assert ( - identifier in ledger_apis_registry.supported_ids - ), "Not a registered ledger api identifier." + enforce( + identifier in ledger_apis_registry.supported_ids, + "Not a registered ledger api identifier.", + ) api = make_ledger_api(identifier, **cls.ledger_api_configs[identifier]) tx = api.get_transfer_transaction( sender_address, destination_address, amount, tx_fee, tx_nonce, **kwargs, @@ -123,9 +128,10 @@ def send_signed_transaction(cls, identifier: str, tx_signed: Any) -> Optional[st :param tx_signed: the signed transaction :return: the tx_digest, if present """ - assert ( - identifier in ledger_apis_registry.supported_ids - ), "Not a registered ledger api identifier." + enforce( + identifier in ledger_apis_registry.supported_ids, + "Not a registered ledger api identifier.", + ) api = make_ledger_api(identifier, **cls.ledger_api_configs[identifier]) tx_digest = api.send_signed_transaction(tx_signed) return tx_digest @@ -139,9 +145,10 @@ def get_transaction_receipt(cls, identifier: str, tx_digest: str) -> Optional[An :param tx_digest: the digest associated to the transaction. :return: the tx receipt, if present """ - assert ( - identifier in ledger_apis_registry.supported_ids - ), "Not a registered ledger api identifier." + enforce( + identifier in ledger_apis_registry.supported_ids, + "Not a registered ledger api identifier.", + ) api = make_ledger_api(identifier, **cls.ledger_api_configs[identifier]) tx_receipt = api.get_transaction_receipt(tx_digest) return tx_receipt @@ -155,9 +162,10 @@ def get_transaction(cls, identifier: str, tx_digest: str) -> Optional[Any]: :param tx_digest: the digest associated to the transaction. :return: the tx, if present """ - assert ( - identifier in ledger_apis_registry.supported_ids - ), "Not a registered ledger api identifier." + enforce( + identifier in ledger_apis_registry.supported_ids, + "Not a registered ledger api identifier.", + ) api = make_ledger_api(identifier, **cls.ledger_api_configs[identifier]) tx = api.get_transaction(tx_digest) return tx @@ -171,9 +179,10 @@ def is_transaction_settled(identifier: str, tx_receipt: Any) -> bool: :param tx_receipt: the transaction digest :return: True if correctly settled, False otherwise """ - assert ( - identifier in ledger_apis_registry.supported_ids - ), "Not a registered ledger api identifier." + enforce( + identifier in ledger_apis_registry.supported_ids, + "Not a registered ledger api identifier.", + ) api_class = make_ledger_api_cls(identifier) is_settled = api_class.is_transaction_settled(tx_receipt) return is_settled @@ -198,9 +207,10 @@ def is_transaction_valid( :param amount: the amount we expect to get from the transaction. :return: True if is valid , False otherwise """ - assert ( - identifier in ledger_apis_registry.supported_ids - ), "Not a registered ledger api identifier." + enforce( + identifier in ledger_apis_registry.supported_ids, + "Not a registered ledger api identifier.", + ) api_class = make_ledger_api_cls(identifier) is_valid = api_class.is_transaction_valid(tx, seller, client, tx_nonce, amount) return is_valid @@ -215,9 +225,10 @@ def generate_tx_nonce(identifier: str, seller: Address, client: Address) -> str: :param client: the address of the client. :return: return the hash in hex. """ - assert ( - identifier in ledger_apis_registry.supported_ids - ), "Not a registered ledger api identifier." + enforce( + identifier in ledger_apis_registry.supported_ids, + "Not a registered ledger api identifier.", + ) api_class = make_ledger_api_cls(identifier) tx_nonce = api_class.generate_tx_nonce(seller=seller, client=client) return tx_nonce @@ -238,9 +249,10 @@ def recover_message( :param is_deprecated_mode: if the deprecated signing was used :return: the recovered addresses """ - assert ( - identifier in ledger_apis_registry.supported_ids - ), "Not a registered ledger api identifier." + enforce( + identifier in ledger_apis_registry.supported_ids, + "Not a registered ledger api identifier.", + ) api_class = make_ledger_api_cls(identifier) addresses = api_class.recover_message( message=message, signature=signature, is_deprecated_mode=is_deprecated_mode diff --git a/aea/crypto/registries/base.py b/aea/crypto/registries/base.py index f1bab9d9f6..854a1cb26e 100644 --- a/aea/crypto/registries/base.py +++ b/aea/crypto/registries/base.py @@ -45,10 +45,6 @@ class ItemId(RegexConstrainedString): REGEX = re.compile(r"^({})$".format(ITEM_ID_REGEX)) - def __init__(self, seq): - """Initialize the item id.""" - super().__init__(seq) - @property def name(self): """Get the id name.""" diff --git a/aea/crypto/wallet.py b/aea/crypto/wallet.py index c4971d0e59..29a19f1be2 100644 --- a/aea/crypto/wallet.py +++ b/aea/crypto/wallet.py @@ -91,8 +91,6 @@ class Wallet: """ - # TODO do some check after loading the keys - # to see whether we have duplicate cryptos? def __init__( self, private_key_paths: Dict[str, Optional[str]], diff --git a/aea/decision_maker/base.py b/aea/decision_maker/base.py index 3cc826502f..4f6a93169d 100644 --- a/aea/decision_maker/base.py +++ b/aea/decision_maker/base.py @@ -18,7 +18,6 @@ # ------------------------------------------------------------------------------ """This module contains the decision maker class.""" -import copy import hashlib import logging import threading @@ -159,7 +158,7 @@ def __init__(self, access_code: str): super().__init__() self._access_code_hash = _hash(access_code) - def put( + def put( # pylint: disable=arguments-differ self, internal_message: Optional[Message], block=True, timeout=None ) -> None: """ @@ -181,7 +180,9 @@ def put( raise ValueError("Only messages are allowed!") super().put(internal_message, block=True, timeout=None) - def put_nowait(self, internal_message: Optional[Message]) -> None: + def put_nowait( # pylint: disable=arguments-differ + self, internal_message: Optional[Message] + ) -> None: """ Put an internal message on the queue. @@ -377,10 +378,4 @@ def handle(self, message: Message) -> None: :param message: the internal message :return: None """ - # TODO: remove next three lines - copy_message = copy.copy(message) - copy_message.counterparty = message.sender - copy_message.sender = message.sender - # copy_message.to = message.to - copy_message.is_incoming = True - self.decision_maker_handler.handle(copy_message) + self.decision_maker_handler.handle(message) diff --git a/aea/decision_maker/default.py b/aea/decision_maker/default.py index be8755422c..e033b21e70 100644 --- a/aea/decision_maker/default.py +++ b/aea/decision_maker/default.py @@ -22,21 +22,21 @@ import copy import logging from enum import Enum -from typing import Dict, List, Optional, cast +from typing import Any, Dict, List, Optional, cast from aea.crypto.wallet import Wallet from aea.decision_maker.base import DecisionMakerHandler as BaseDecisionMakerHandler from aea.decision_maker.base import OwnershipState as BaseOwnershipState from aea.decision_maker.base import Preferences as BasePreferences -from aea.helpers.dialogue.base import Dialogue as BaseDialogue -from aea.helpers.dialogue.base import DialogueLabel as BaseDialogueLabel +from aea.exceptions import enforce from aea.helpers.preference_representations.base import ( linear_utility, logarithmic_utility, ) from aea.helpers.transaction.base import SignedMessage, SignedTransaction, Terms from aea.identity.base import Identity -from aea.protocols.base import Message +from aea.protocols.base import Address, Message +from aea.protocols.dialogue.base import Dialogue as BaseDialogue from aea.protocols.signing.dialogues import SigningDialogue from aea.protocols.signing.dialogues import SigningDialogues as BaseSigningDialogues from aea.protocols.signing.message import SigningMessage @@ -51,7 +51,6 @@ UtilityParams = Dict[str, float] # a map from identifier to quantity ExchangeParams = Dict[str, float] # a map from identifier to quantity -SENDER_TX_SHARE = 0.5 QUANTITY_SHIFT = 100 logger = logging.getLogger(__name__) @@ -67,32 +66,24 @@ def __init__(self, **kwargs) -> None: :param agent_address: the address of the agent for whom dialogues are maintained :return: None """ - BaseSigningDialogues.__init__(self, "decision_maker") - @staticmethod - def role_from_first_message(message: Message) -> BaseDialogue.Role: - """Infer the role of the agent from an incoming/outgoing first message + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> BaseDialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message - :param message: an incoming/outgoing first message - :return: The role of the agent - """ - return SigningDialogue.Role.DECISION_MAKER - - def create_dialogue( - self, dialogue_label: BaseDialogueLabel, role: BaseDialogue.Role, - ) -> SigningDialogue: - """ - Create an instance of fipa dialogue. - - :param dialogue_label: the identifier of the dialogue - :param role: the role of the agent this dialogue is maintained for + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return SigningDialogue.Role.DECISION_MAKER - :return: the created dialogue - """ - dialogue = SigningDialogue( - dialogue_label=dialogue_label, agent_address="decision_maker", role=role + BaseSigningDialogues.__init__( + self, + self_address="decision_maker", + role_from_first_message=role_from_first_message, + **kwargs, ) - return dialogue class StateUpdateDialogues(BaseStateUpdateDialogues): @@ -105,32 +96,24 @@ def __init__(self, **kwargs) -> None: :param agent_address: the address of the agent for whom dialogues are maintained :return: None """ - BaseStateUpdateDialogues.__init__(self, "decision_maker") - @staticmethod - def role_from_first_message(message: Message) -> BaseDialogue.Role: - """Infer the role of the agent from an incoming/outgoing first message + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> BaseDialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message - :param message: an incoming/outgoing first message - :return: The role of the agent - """ - return StateUpdateDialogue.Role.DECISION_MAKER + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return StateUpdateDialogue.Role.DECISION_MAKER - def create_dialogue( - self, dialogue_label: BaseDialogueLabel, role: BaseDialogue.Role, - ) -> StateUpdateDialogue: - """ - Create an instance of fipa dialogue. - - :param dialogue_label: the identifier of the dialogue - :param role: the role of the agent this dialogue is maintained for - - :return: the created dialogue - """ - dialogue = StateUpdateDialogue( - dialogue_label=dialogue_label, agent_address="decision_maker", role=role + BaseStateUpdateDialogues.__init__( + self, + self_address="decision_maker", + role_from_first_message=role_from_first_message, + **kwargs, ) - return dialogue class GoalPursuitReadiness: @@ -180,7 +163,7 @@ def __init__(self): self._amount_by_currency_id = None # type: Optional[CurrencyHoldings] self._quantities_by_good_id = None # type: Optional[GoodHoldings] - def set( + def set( # pylint: disable=arguments-differ self, amount_by_currency_id: CurrencyHoldings = None, quantities_by_good_id: GoodHoldings = None, @@ -192,17 +175,19 @@ def set( :param amount_by_currency_id: the currency endowment of the agent in this state. :param quantities_by_good_id: the good endowment of the agent in this state. """ - assert ( - amount_by_currency_id is not None and quantities_by_good_id is not None - ), "Must provide values." - assert ( - not self.is_initialized - ), "Cannot apply state update, current state is already initialized!" + if amount_by_currency_id is None: # pragma: nocover + raise ValueError("Must provide amount_by_currency_id.") + if quantities_by_good_id is None: # pragma: nocover + raise ValueError("Must provide quantities_by_good_id.") + enforce( + not self.is_initialized, + "Cannot apply state update, current state is already initialized!", + ) self._amount_by_currency_id = copy.copy(amount_by_currency_id) self._quantities_by_good_id = copy.copy(quantities_by_good_id) - def apply_delta( + def apply_delta( # pylint: disable=arguments-differ self, delta_amount_by_currency_id: Dict[str, int] = None, delta_quantities_by_good_id: Dict[str, int] = None, @@ -217,26 +202,32 @@ def apply_delta( :param delta_quantities_by_good_id: the delta in the quantities by good :return: None """ - assert ( - delta_amount_by_currency_id is not None - and delta_quantities_by_good_id is not None - ), "Must provide values." - assert ( - self._amount_by_currency_id is not None - and self._quantities_by_good_id is not None - ), "Cannot apply state update, current state is not initialized!" - assert all( - [ - key in self._amount_by_currency_id - for key in delta_amount_by_currency_id.keys() - ] - ), "Invalid keys present in delta_amount_by_currency_id." - assert all( - [ - key in self._quantities_by_good_id - for key in delta_quantities_by_good_id.keys() - ] - ), "Invalid keys present in delta_quantities_by_good_id." + if delta_amount_by_currency_id is None: # pragma: nocover + raise ValueError("Must provide delta_amount_by_currency_id.") + if delta_quantities_by_good_id is None: # pragma: nocover + raise ValueError("Must provide delta_quantities_by_good_id.") + if self._amount_by_currency_id is None or self._quantities_by_good_id is None: + raise ValueError( # pragma: nocover + "Cannot apply state update, current state is not initialized!" + ) + enforce( + all( + [ + key in self._amount_by_currency_id + for key in delta_amount_by_currency_id.keys() + ] + ), + "Invalid keys present in delta_amount_by_currency_id.", + ) + enforce( + all( + [ + key in self._quantities_by_good_id + for key in delta_quantities_by_good_id.keys() + ] + ), + "Invalid keys present in delta_quantities_by_good_id.", + ) for currency_id, amount_delta in delta_amount_by_currency_id.items(): self._amount_by_currency_id[currency_id] += amount_delta @@ -255,13 +246,15 @@ def is_initialized(self) -> bool: @property def amount_by_currency_id(self) -> CurrencyHoldings: """Get currency holdings in this state.""" - assert self._amount_by_currency_id is not None, "CurrencyHoldings not set!" + if self._amount_by_currency_id is None: + raise ValueError("amount_by_currency_id is not set!") return copy.copy(self._amount_by_currency_id) @property def quantities_by_good_id(self) -> GoodHoldings: """Get good holdings in this state.""" - assert self._quantities_by_good_id is not None, "GoodHoldings not set!" + if self._quantities_by_good_id is None: + raise ValueError("quantities_by_good_id is not set!") return copy.copy(self._quantities_by_good_id) def is_affordable_transaction(self, terms: Terms) -> bool: @@ -322,10 +315,10 @@ def update(self, terms: Terms) -> None: :param terms: the transaction terms :return: None """ - assert ( - self._amount_by_currency_id is not None - and self._quantities_by_good_id is not None - ), "Cannot apply state update, current state is not initialized!" + if self._amount_by_currency_id is None or self._quantities_by_good_id is None: + raise ValueError( # pragma: nocover + "Cannot apply state update, current state is not initialized!" + ) for currency_id, amount_delta in terms.amount_by_currency_id.items(): self._amount_by_currency_id[currency_id] += amount_delta @@ -363,7 +356,7 @@ def __init__(self): self._utility_params_by_good_id = None # type: Optional[UtilityParams] self._quantity_shift = QUANTITY_SHIFT - def set( + def set( # pylint: disable=arguments-differ self, exchange_params_by_currency_id: ExchangeParams = None, utility_params_by_good_id: UtilityParams = None, @@ -375,13 +368,14 @@ def set( :param exchange_params_by_currency_id: the exchange params. :param utility_params_by_good_id: the utility params for every asset. """ - assert ( - exchange_params_by_currency_id is not None - and utility_params_by_good_id is not None - ), "Must provide values." - assert ( - not self.is_initialized - ), "Cannot apply preferences update, preferences already initialized!" + if exchange_params_by_currency_id is None: # pragma: nocover + raise ValueError("Must provide exchange_params_by_currency_id.") + if utility_params_by_good_id is None: # pragma: nocover + raise ValueError("Must provide utility_params_by_good_id.") + enforce( + not self.is_initialized, + "Cannot apply preferences update, preferences already initialized!", + ) self._exchange_params_by_currency_id = copy.copy(exchange_params_by_currency_id) self._utility_params_by_good_id = copy.copy(utility_params_by_good_id) @@ -400,15 +394,15 @@ def is_initialized(self) -> bool: @property def exchange_params_by_currency_id(self) -> ExchangeParams: """Get exchange parameter for each currency.""" - assert ( - self._exchange_params_by_currency_id is not None - ), "ExchangeParams not set!" + if self._exchange_params_by_currency_id is None: + raise ValueError("ExchangeParams not set!") return self._exchange_params_by_currency_id @property def utility_params_by_good_id(self) -> UtilityParams: """Get utility parameter for each good.""" - assert self._utility_params_by_good_id is not None, "UtilityParams not set!" + if self._utility_params_by_good_id is None: + raise ValueError("UtilityParams not set!") return self._utility_params_by_good_id def logarithmic_utility(self, quantities_by_good_id: GoodHoldings) -> float: @@ -418,7 +412,7 @@ def logarithmic_utility(self, quantities_by_good_id: GoodHoldings) -> float: :param quantities_by_good_id: the good holdings (dictionary) with the identifier (key) and quantity (value) for each good :return: utility value """ - assert self.is_initialized, "Preferences params not set!" + enforce(self.is_initialized, "Preferences params not set!") result = logarithmic_utility( self.utility_params_by_good_id, quantities_by_good_id, self._quantity_shift ) @@ -431,7 +425,7 @@ def linear_utility(self, amount_by_currency_id: CurrencyHoldings) -> float: :param amount_by_currency_id: the currency holdings (dictionary) with the identifier (key) and quantity (value) for each currency :return: utility value """ - assert self.is_initialized, "Preferences params not set!" + enforce(self.is_initialized, "Preferences params not set!") result = linear_utility( self.exchange_params_by_currency_id, amount_by_currency_id ) @@ -449,13 +443,13 @@ def utility( :param amount_by_currency_id: the currency holdings :return: the utility value. """ - assert self.is_initialized, "Preferences params not set!" + enforce(self.is_initialized, "Preferences params not set!") goods_score = self.logarithmic_utility(quantities_by_good_id) currency_score = self.linear_utility(amount_by_currency_id) score = goods_score + currency_score return score - def marginal_utility( + def marginal_utility( # pylint: disable=arguments-differ self, ownership_state: BaseOwnershipState, delta_quantities_by_good_id: Optional[GoodHoldings] = None, @@ -470,7 +464,7 @@ def marginal_utility( :param delta_amount_by_currency_id: the change in money holdings :return: the marginal utility score """ - assert self.is_initialized, "Preferences params not set!" + enforce(self.is_initialized, "Preferences params not set!") ownership_state = cast(OwnershipState, ownership_state) current_goods_score = self.logarithmic_utility( ownership_state.quantities_by_good_id @@ -510,7 +504,7 @@ def utility_diff_from_transaction( :param terms: the transaction terms. :return: the score. """ - assert self.is_initialized, "Preferences params not set!" + enforce(self.is_initialized, "Preferences params not set!") ownership_state = cast(OwnershipState, ownership_state) current_score = self.utility( quantities_by_good_id=ownership_state.quantities_by_good_id, @@ -642,15 +636,10 @@ def _handle_message_signing( :param signing_dialogue: the signing dialogue :return: None """ - signing_msg_response = SigningMessage( - performative=SigningMessage.Performative.ERROR, - dialogue_reference=signing_dialogue.dialogue_label.dialogue_reference, - target=signing_msg.message_id, - message_id=signing_msg.message_id + 1, - skill_callback_ids=signing_msg.skill_callback_ids, - skill_callback_info=signing_msg.skill_callback_info, - error_code=SigningMessage.ErrorCode.UNSUCCESSFUL_MESSAGE_SIGNING, - ) + performative = SigningMessage.Performative.ERROR + kwargs = { + "error_code": SigningMessage.ErrorCode.UNSUCCESSFUL_MESSAGE_SIGNING, + } # type: Dict[str, Any] if self._is_acceptable_for_signing(signing_msg): signed_message = self.wallet.sign_message( signing_msg.raw_message.ledger_id, @@ -658,21 +647,16 @@ def _handle_message_signing( signing_msg.raw_message.is_deprecated_mode, ) if signed_message is not None: - signing_msg_response = SigningMessage( - performative=SigningMessage.Performative.SIGNED_MESSAGE, - dialogue_reference=signing_dialogue.dialogue_label.dialogue_reference, - target=signing_msg.message_id, - message_id=signing_msg.message_id + 1, - skill_callback_ids=signing_msg.skill_callback_ids, - skill_callback_info=signing_msg.skill_callback_info, - signed_message=SignedMessage( - signing_msg.raw_message.ledger_id, - signed_message, - signing_msg.raw_message.is_deprecated_mode, - ), + performative = SigningMessage.Performative.SIGNED_MESSAGE + kwargs.pop("error_code") + kwargs["signed_message"] = SignedMessage( + signing_msg.raw_message.ledger_id, + signed_message, + signing_msg.raw_message.is_deprecated_mode, ) - signing_msg_response.counterparty = signing_msg.counterparty - signing_dialogue.update(signing_msg_response) + signing_msg_response = signing_dialogue.reply( + performative=performative, target_message=signing_msg, **kwargs, + ) self.message_out_queue.put(signing_msg_response) def _handle_transaction_signing( @@ -685,33 +669,23 @@ def _handle_transaction_signing( :param signing_dialogue: the signing dialogue :return: None """ - signing_msg_response = SigningMessage( - performative=SigningMessage.Performative.ERROR, - dialogue_reference=signing_dialogue.dialogue_label.dialogue_reference, - target=signing_msg.message_id, - message_id=signing_msg.message_id + 1, - skill_callback_ids=signing_msg.skill_callback_ids, - skill_callback_info=signing_msg.skill_callback_info, - error_code=SigningMessage.ErrorCode.UNSUCCESSFUL_TRANSACTION_SIGNING, - ) + performative = SigningMessage.Performative.ERROR + kwargs = { + "error_code": SigningMessage.ErrorCode.UNSUCCESSFUL_TRANSACTION_SIGNING, + } # type: Dict[str, Any] if self._is_acceptable_for_signing(signing_msg): signed_tx = self.wallet.sign_transaction( signing_msg.raw_transaction.ledger_id, signing_msg.raw_transaction.body ) if signed_tx is not None: - signing_msg_response = SigningMessage( - performative=SigningMessage.Performative.SIGNED_TRANSACTION, - dialogue_reference=signing_dialogue.dialogue_label.dialogue_reference, - target=signing_msg.message_id, - message_id=signing_msg.message_id + 1, - skill_callback_ids=signing_msg.skill_callback_ids, - skill_callback_info=signing_msg.skill_callback_info, - signed_transaction=SignedTransaction( - signing_msg.raw_transaction.ledger_id, signed_tx - ), + performative = SigningMessage.Performative.SIGNED_TRANSACTION + kwargs.pop("error_code") + kwargs["signed_transaction"] = SignedTransaction( + signing_msg.raw_transaction.ledger_id, signed_tx ) - signing_msg_response.counterparty = signing_msg.counterparty - signing_dialogue.update(signing_msg_response) + signing_msg_response = signing_dialogue.reply( + performative=performative, target_message=signing_msg, **kwargs, + ) self.message_out_queue.put(signing_msg_response) def _is_acceptable_for_signing(self, signing_msg: SigningMessage) -> bool: diff --git a/aea/exceptions.py b/aea/exceptions.py index 2a83f77b93..5b68e82559 100644 --- a/aea/exceptions.py +++ b/aea/exceptions.py @@ -19,6 +19,8 @@ """Exceptions for the AEA package.""" +from typing import Type + class AEAException(Exception): """User-defined exception for the AEA framework.""" @@ -26,3 +28,23 @@ class AEAException(Exception): class AEAPackageLoadingError(AEAException): """Class for exceptions that are raised for loading errors of AEA packages.""" + + +class AEAEnforceError(AEAException): + """Class for enforcement errors.""" + + +def enforce( + is_valid_condition: bool, + exception_text: str, + exception_class: Type[Exception] = AEAEnforceError, +) -> None: + """ + Evaluate a condition and raise an exception with the provided text if it is not satisfied. + + :param is_valid_condition: the valid condition + :param exception_text: the exception to be raised + :param exception_class: the class of exception + """ + if not is_valid_condition: + raise exception_class(exception_text) diff --git a/aea/helpers/async_friendly_queue.py b/aea/helpers/async_friendly_queue.py index c3012e7cfb..143543bc6a 100644 --- a/aea/helpers/async_friendly_queue.py +++ b/aea/helpers/async_friendly_queue.py @@ -85,4 +85,5 @@ async def async_get(self) -> Any: await self.async_wait() with suppress(queue.Empty): - return self.get_nowait() + item = self.get_nowait() + return item diff --git a/aea/helpers/async_utils.py b/aea/helpers/async_utils.py index ef7a14a310..af987c0d62 100644 --- a/aea/helpers/async_utils.py +++ b/aea/helpers/async_utils.py @@ -18,6 +18,7 @@ # ------------------------------------------------------------------------------ """This module contains the misc utils for async code.""" import asyncio +import collections import datetime import logging import subprocess # nosec @@ -25,14 +26,16 @@ from asyncio import CancelledError from asyncio.events import AbstractEventLoop, TimerHandle from asyncio.futures import Future -from asyncio.tasks import Task +from asyncio.tasks import FIRST_COMPLETED, Task from collections.abc import Iterable +from contextlib import contextmanager, suppress from threading import Thread from typing import ( Any, Awaitable, Callable, Container, + Generator, List, Optional, Sequence, @@ -62,6 +65,9 @@ def ensure_list(value: Any) -> List: return [value] +not_set = object() + + class AsyncState: """Awaitable state.""" @@ -159,6 +165,30 @@ async def wait(self, state_or_states: Union[Any, Sequence[Any]]) -> Tuple[Any, A finally: self._remove_watcher(watcher) + @contextmanager + def transit( + self, initial: Any = not_set, success: Any = not_set, fail: Any = not_set + ) -> Generator: + """ + Change state context according to success or not. + + :param initial: set state on context enter, not_set by default + :param success: set state on context block done, not_set by default + :param fail: set state on context block raises exception, not_set by default + + :return: None + """ + try: + if initial is not not_set: + self.set(initial) + yield + if success is not not_set: + self.set(success) + except BaseException: + if fail is not not_set: + self.set(fail) + raise + class PeriodicCaller: """ @@ -226,7 +256,7 @@ def stop(self) -> None: self._timerhandle = None -def ensure_loop(loop: AbstractEventLoop = None) -> AbstractEventLoop: +def ensure_loop(loop: Optional[AbstractEventLoop] = None) -> AbstractEventLoop: """ Use loop provided or create new if not provided or closed. @@ -237,9 +267,11 @@ def ensure_loop(loop: AbstractEventLoop = None) -> AbstractEventLoop: """ try: loop = loop or asyncio.new_event_loop() - assert not loop.is_closed() - assert not loop.is_running() - except (RuntimeError, AssertionError): + if loop.is_closed(): + raise ValueError("Event loop closed.") # pragma: nocover + if loop.is_running(): + raise ValueError("Event loop running.") + except (RuntimeError, ValueError): loop = asyncio.new_event_loop() return loop @@ -303,7 +335,8 @@ def __init__(self, loop=None) -> None: :param loop: optional event loop. is it's running loop, threaded runner will use it. """ self._loop = loop or asyncio.new_event_loop() - assert not self._loop.is_closed() + if self._loop.is_closed(): + raise ValueError("Event loop closed.") # pragma: nocover super().__init__(daemon=True) def start(self) -> None: @@ -364,6 +397,7 @@ class AwaitableProc: """ def __init__(self, *args, **kwargs): + """Initialise awaitable proc.""" self.args = args self.kwargs = kwargs self.proc = None @@ -389,3 +423,70 @@ async def start(self): def _in_thread(self): self.proc.wait() self.loop.call_soon_threadsafe(self.future.set_result, self.proc.returncode) + + +class ItemGetter: + """Virtual queue like object to get items from getters function.""" + + def __init__(self, getters: List[Callable]) -> None: + """ + Init ItemGetter. + + :param getters: List of couroutines to be awaited. + """ + if not getters: # pragma: nocover + raise ValueError("getters list can not be empty!") + self._getters = getters + self._items: collections.deque = collections.deque() + + async def get(self) -> Any: + """Get item.""" + if not self._items: + await self._wait() + return self._items.pop() + + async def _wait(self) -> None: + """Populate cache queue with items.""" + loop = asyncio.get_event_loop() + try: + tasks = [loop.create_task(getter()) for getter in self._getters] + done, _ = await asyncio.wait(tasks, return_when=FIRST_COMPLETED) + for task in done: + self._items.append(await task) + finally: + for task in tasks: + if task.done(): + continue + task.cancel() + with suppress(CancelledError): + await task + + +class HandlerItemGetter(ItemGetter): + """ItemGetter with handler passed.""" + + @staticmethod + def _make_getter(handler: Callable[[Any], None], getter: Callable) -> Callable: + """ + Create getter for handler and item getter function. + + :param handler: callable with one position argument. + :param getter: a couroutine to await for item + + :return: callable to return handler and item from getter. + """ + # for pydocstyle + async def _getter(): + return handler, await getter() + + return _getter + + def __init__(self, getters: List[Tuple[Callable[[Any], None], Callable]]): + """ + Init HandlerItemGetter. + + :param getters: List of tuples of handler and couroutine to be awaiteed for an item. + """ + super(HandlerItemGetter, self).__init__( + [self._make_getter(handler, getter) for handler, getter in getters] + ) diff --git a/aea/helpers/base.py b/aea/helpers/base.py index faa187afd8..98ccdedca9 100644 --- a/aea/helpers/base.py +++ b/aea/helpers/base.py @@ -34,26 +34,20 @@ from collections import OrderedDict, UserString from functools import wraps from pathlib import Path -from typing import Any, Callable, Dict, TextIO, Union +from typing import Any, Callable, Dict, List, TextIO, Union from dotenv import load_dotenv import yaml -from aea.configurations.base import ComponentConfiguration - logger = logging.getLogger(__name__) -def yaml_load(stream: TextIO) -> Dict[str, str]: - """ - Load a yaml from a file pointer in an ordered way. - - :param stream: the file pointer - :return: the yaml - """ +def _ordered_loading(fun: Callable): # for pydocstyle - def ordered_load(stream: TextIO, object_pairs_hook=OrderedDict): + def ordered_load(stream: TextIO): + object_pairs_hook = OrderedDict + class OrderedLoader(yaml.SafeLoader): """A wrapper for safe yaml loader.""" @@ -66,18 +60,12 @@ def construct_mapping(loader, node): OrderedLoader.add_constructor( yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, construct_mapping ) - return yaml.load(stream, OrderedLoader) # nosec - - return ordered_load(stream) + return fun(stream, Loader=OrderedLoader) # nosec + return ordered_load -def yaml_dump(data, stream: TextIO) -> None: - """ - Dump data to a yaml file in an ordered way. - :param data: the data to be dumped - :param stream: the file pointer - """ +def _ordered_dumping(fun: Callable): # for pydocstyle def ordered_dump(data, stream=None, **kwds): class OrderedDumper(yaml.SafeDumper): @@ -91,9 +79,49 @@ def _dict_representer(dumper, data): ) OrderedDumper.add_representer(OrderedDict, _dict_representer) - return yaml.dump(data, stream, OrderedDumper, **kwds) # nosec + return fun(data, stream, Dumper=OrderedDumper, **kwds) # nosec + + return ordered_dump - ordered_dump(data, stream) + +@_ordered_loading +def yaml_load(*args, **kwargs) -> Dict[str, Any]: + """ + Load a yaml from a file pointer in an ordered way. + + :return: the yaml + """ + return yaml.load(*args, **kwargs) # nosec + + +@_ordered_loading +def yaml_load_all(*args, **kwargs) -> List[Dict[str, Any]]: + """ + Load a multi-paged yaml from a file pointer in an ordered way. + + :return: the yaml + """ + return list(yaml.load_all(*args, **kwargs)) # nosec + + +@_ordered_dumping +def yaml_dump(*args, **kwargs) -> None: + """ + Dump multi-paged yaml data to a yaml file in an ordered way. + + :return None + """ + yaml.dump(*args, **kwargs) # nosec + + +@_ordered_dumping +def yaml_dump_all(*args, **kwargs) -> None: + """ + Dump multi-paged yaml data to a yaml file in an ordered way. + + :return None + """ + yaml.dump_all(*args, **kwargs) # nosec def _get_module(spec): @@ -139,44 +167,6 @@ def locate(path: str) -> Any: return object_ -def load_aea_package(configuration: ComponentConfiguration) -> None: - """ - Load the AEA package. - - It adds all the __init__.py modules into `sys.modules`. - - :param configuration: the configuration object. - :return: None - """ - dir_ = configuration.directory - assert dir_ is not None - - # patch sys.modules with dummy modules - prefix_root = "packages" - prefix_author = prefix_root + f".{configuration.author}" - prefix_pkg_type = prefix_author + f".{configuration.component_type.to_plural()}" - prefix_pkg = prefix_pkg_type + f".{configuration.name}" - sys.modules[prefix_root] = types.ModuleType(prefix_root) - sys.modules[prefix_author] = types.ModuleType(prefix_author) - sys.modules[prefix_pkg_type] = types.ModuleType(prefix_pkg_type) - - for subpackage_init_file in dir_.rglob("__init__.py"): - parent_dir = subpackage_init_file.parent - relative_parent_dir = parent_dir.relative_to(dir_) - if relative_parent_dir == Path("."): - # this handles the case when 'subpackage_init_file' - # is path/to/package/__init__.py - import_path = prefix_pkg - else: - import_path = prefix_pkg + "." + ".".join(relative_parent_dir.parts) - - spec = importlib.util.spec_from_file_location(import_path, subpackage_init_file) - module = importlib.util.module_from_spec(spec) - sys.modules[import_path] = module - logger.debug(f"loading {import_path}: {module}") - spec.loader.exec_module(module) # type: ignore - - def load_module(dotted_path: str, filepath: Path) -> types.ModuleType: """ Load a module. @@ -316,9 +306,9 @@ def get_logger_method(fn: Callable, logger_method: Union[str, Callable]) -> Call if callable(logger_method): # pragma: nocover return logger_method - logger = fn.__globals__.get("logger", logging.getLogger(fn.__globals__["__name__"])) # type: ignore + logger_ = fn.__globals__.get("logger", logging.getLogger(fn.__globals__["__name__"])) # type: ignore - return getattr(logger, logger_method) + return getattr(logger_, logger_method) def try_decorator(error_message: str, default_return=None, logger_method="error"): @@ -331,6 +321,7 @@ def try_decorator(error_message: str, default_return=None, logger_method="error" :param default_return: value to return on exception, by default None :param logger_method: name of the logger method or callable to print logs """ + # for pydocstyle def decorator(fn): @wraps(fn) @@ -365,6 +356,7 @@ def retry_decorator( :param delay: num of seconds to sleep between retries. default 0 :param logger_method: name of the logger method or callable to print logs """ + # for pydocstyle def decorator(fn): @wraps(fn) diff --git a/aea/helpers/exec_timeout.py b/aea/helpers/exec_timeout.py index c70d172e86..d291a1f88b 100644 --- a/aea/helpers/exec_timeout.py +++ b/aea/helpers/exec_timeout.py @@ -234,7 +234,7 @@ def stop(cls, force: bool = False) -> None: @classmethod def _supervisor_event_loop(cls) -> None: """Start supervisor thread to execute asyncio task controlling execution time.""" - # pydocstyle: noqa # cause black reformats with pydocstyle confilct + # pydocstyle: noqa # cause black reformats with pydocstyle conflict # noqa: E800 async def wait_stopped() -> None: await cls._stopped_future # type: ignore diff --git a/packages/fetchai/connections/p2p_client/__init__.py b/aea/helpers/multiaddr/__init__.py similarity index 94% rename from packages/fetchai/connections/p2p_client/__init__.py rename to aea/helpers/multiaddr/__init__.py index fed866f1ba..02dadfea86 100644 --- a/packages/fetchai/connections/p2p_client/__init__.py +++ b/aea/helpers/multiaddr/__init__.py @@ -17,4 +17,4 @@ # # ------------------------------------------------------------------------------ -"""Peer to Peer connection and channel.""" +"""This module contains multiaddress class.""" diff --git a/aea/helpers/multiaddr/base.py b/aea/helpers/multiaddr/base.py new file mode 100644 index 0000000000..c7275427fe --- /dev/null +++ b/aea/helpers/multiaddr/base.py @@ -0,0 +1,144 @@ +# -*- 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 multiaddress class.""" + +from binascii import unhexlify + +import base58 + +from ecdsa import VerifyingKey, curves, keys + +import multihash # type: ignore + +from aea.helpers.multiaddr.crypto_pb2 import KeyType, PublicKey + +# NOTE: +# - Reference: https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md#keys +# - Implementation inspired from https://github.com/libp2p/py-libp2p +# - On inlining see: https://github.com/libp2p/specs/issues/138 +# - Enabling inlining to be interoperable w/ the Go implementation + +ENABLE_INLINING = True +MAX_INLINE_KEY_LENGTH = 42 +IDENTITY_MULTIHASH_CODE = 0x00 + +KEY_SIZE = 32 +ZERO = b"\x00" + +if ENABLE_INLINING: + + class IdentityHash: + """ Neutral hashing implementation for inline multihashing """ + + _digest: bytes + + def __init__(self) -> None: + """ Initialize IdentityHash object """ + self._digest = bytearray() + + def update(self, input_data: bytes) -> None: + """ Update data to hash """ + self._digest += input_data + + def digest(self) -> bytes: + """ Hash of input data """ + return self._digest + + multihash.FuncReg.register( + IDENTITY_MULTIHASH_CODE, "identity", hash_new=IdentityHash + ) + + +def _pad_scalar(scalar): + return (ZERO * (KEY_SIZE - len(scalar))) + scalar + + +def _pad_hex(hexed): + """ Pad odd-length hex strings """ + return hexed if not len(hexed) & 1 else "0" + hexed + + +def _hex_to_bytes(hexed): + return _pad_scalar(unhexlify(_pad_hex(hexed))) + + +class MultiAddr: + """ + Protocol Labs' Multiaddress representation of a network address + """ + + def __init__(self, host: str, port: int, public_key: str): + """ + Initialize a multiaddress + + :param host: ip host of the address + :param host: port number of the address + :param host: hex encoded public key. Must conform to Bitcoin EC encoding standard for Secp256k1 + """ + + self._host = host + self._port = port + + try: + VerifyingKey._from_compressed(_hex_to_bytes(public_key), curves.SECP256k1) + except keys.MalformedPointError as e: # pragma: no cover + raise Exception("Malformed public key:{}".format(str(e))) + + self._public_key = public_key + self._peerid = self.compute_peerid(self._public_key) + + @staticmethod + def compute_peerid(public_key: str) -> str: + """ + Compute the peer id from a public key. + + In particular, compute the base58 representation of + libp2p PeerID from Bitcoin EC encoded Secp256k1 public key. + + :param public_key: the public key. + :return: the peer id. + """ + key_protobuf = PublicKey( + key_type=KeyType.Secp256k1, data=_hex_to_bytes(public_key) # type: ignore + ) + key_serialized = key_protobuf.SerializeToString() + algo = multihash.Func.sha2_256 + if ENABLE_INLINING and len(key_serialized) <= MAX_INLINE_KEY_LENGTH: + algo = IDENTITY_MULTIHASH_CODE + key_mh = multihash.digest(key_serialized, algo) + return base58.b58encode(key_mh.encode()).decode() + + @property + def public_key(self) -> str: + """Get the public key.""" + return self._public_key + + @property + def peer_id(self) -> str: + """Get the peer id.""" + return self._peerid + + def format(self) -> str: + """ Canonical representation of a multiaddress """ + return f"/dns4/{self._host}/tcp/{self._port}/p2p/{self._peerid}" + + def __str__(self) -> str: + """Default string representation of a mutliaddress.""" + return self.format() diff --git a/aea/helpers/multiaddr/crypto.proto b/aea/helpers/multiaddr/crypto.proto new file mode 100644 index 0000000000..a12f7b6aa5 --- /dev/null +++ b/aea/helpers/multiaddr/crypto.proto @@ -0,0 +1,20 @@ +syntax = "proto2"; + +package crypto.pb; + +enum KeyType { + RSA = 0; + Ed25519 = 1; + Secp256k1 = 2; + ECDSA = 3; +} + +message PublicKey { + required KeyType key_type = 1; + required bytes data = 2; +} + +message PrivateKey { + required KeyType key_type = 1; + required bytes data = 2; +} \ No newline at end of file diff --git a/aea/helpers/multiaddr/crypto_pb2.py b/aea/helpers/multiaddr/crypto_pb2.py new file mode 100644 index 0000000000..9094243b4c --- /dev/null +++ b/aea/helpers/multiaddr/crypto_pb2.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: libp2p/crypto/pb/crypto.proto + +import sys + +_b = sys.version_info[0] < 3 and (lambda x: x) or (lambda x: x.encode("latin1")) +from google.protobuf.internal import enum_type_wrapper +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="libp2p/crypto/pb/crypto.proto", + package="crypto.pb", + syntax="proto2", + serialized_options=None, + serialized_pb=_b( + '\n\x1dlibp2p/crypto/pb/crypto.proto\x12\tcrypto.pb"?\n\tPublicKey\x12$\n\x08key_type\x18\x01 \x02(\x0e\x32\x12.crypto.pb.KeyType\x12\x0c\n\x04\x64\x61ta\x18\x02 \x02(\x0c"@\n\nPrivateKey\x12$\n\x08key_type\x18\x01 \x02(\x0e\x32\x12.crypto.pb.KeyType\x12\x0c\n\x04\x64\x61ta\x18\x02 \x02(\x0c*9\n\x07KeyType\x12\x07\n\x03RSA\x10\x00\x12\x0b\n\x07\x45\x64\x32\x35\x35\x31\x39\x10\x01\x12\r\n\tSecp256k1\x10\x02\x12\t\n\x05\x45\x43\x44SA\x10\x03' + ), +) + +_KEYTYPE = _descriptor.EnumDescriptor( + name="KeyType", + full_name="crypto.pb.KeyType", + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name="RSA", index=0, number=0, serialized_options=None, type=None + ), + _descriptor.EnumValueDescriptor( + name="Ed25519", index=1, number=1, serialized_options=None, type=None + ), + _descriptor.EnumValueDescriptor( + name="Secp256k1", index=2, number=2, serialized_options=None, type=None + ), + _descriptor.EnumValueDescriptor( + name="ECDSA", index=3, number=3, serialized_options=None, type=None + ), + ], + containing_type=None, + serialized_options=None, + serialized_start=175, + serialized_end=232, +) +_sym_db.RegisterEnumDescriptor(_KEYTYPE) + +KeyType = enum_type_wrapper.EnumTypeWrapper(_KEYTYPE) +RSA = 0 +Ed25519 = 1 +Secp256k1 = 2 +ECDSA = 3 + + +_PUBLICKEY = _descriptor.Descriptor( + name="PublicKey", + full_name="crypto.pb.PublicKey", + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name="key_type", + full_name="crypto.pb.PublicKey.key_type", + index=0, + number=1, + type=14, + cpp_type=8, + label=2, + 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="data", + full_name="crypto.pb.PublicKey.data", + index=1, + number=2, + type=12, + cpp_type=9, + label=2, + 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="proto2", + extension_ranges=[], + oneofs=[], + serialized_start=44, + serialized_end=107, +) + + +_PRIVATEKEY = _descriptor.Descriptor( + name="PrivateKey", + full_name="crypto.pb.PrivateKey", + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name="key_type", + full_name="crypto.pb.PrivateKey.key_type", + index=0, + number=1, + type=14, + cpp_type=8, + label=2, + 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="data", + full_name="crypto.pb.PrivateKey.data", + index=1, + number=2, + type=12, + cpp_type=9, + label=2, + 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="proto2", + extension_ranges=[], + oneofs=[], + serialized_start=109, + serialized_end=173, +) + +_PUBLICKEY.fields_by_name["key_type"].enum_type = _KEYTYPE +_PRIVATEKEY.fields_by_name["key_type"].enum_type = _KEYTYPE +DESCRIPTOR.message_types_by_name["PublicKey"] = _PUBLICKEY +DESCRIPTOR.message_types_by_name["PrivateKey"] = _PRIVATEKEY +DESCRIPTOR.enum_types_by_name["KeyType"] = _KEYTYPE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +PublicKey = _reflection.GeneratedProtocolMessageType( + "PublicKey", + (_message.Message,), + { + "DESCRIPTOR": _PUBLICKEY, + "__module__": "libp2p.crypto.pb.crypto_pb2" + # @@protoc_insertion_point(class_scope:crypto.pb.PublicKey) + }, +) +_sym_db.RegisterMessage(PublicKey) + +PrivateKey = _reflection.GeneratedProtocolMessageType( + "PrivateKey", + (_message.Message,), + { + "DESCRIPTOR": _PRIVATEKEY, + "__module__": "libp2p.crypto.pb.crypto_pb2" + # @@protoc_insertion_point(class_scope:crypto.pb.PrivateKey) + }, +) +_sym_db.RegisterMessage(PrivateKey) + + +# @@protoc_insertion_point(module_scope) diff --git a/aea/helpers/multiple_executor.py b/aea/helpers/multiple_executor.py index 7c460b2c29..a4d0772c12 100644 --- a/aea/helpers/multiple_executor.py +++ b/aea/helpers/multiple_executor.py @@ -240,7 +240,7 @@ async def _handle_exception( logger.info(f"Exception raised during {task.id} running.") if self._task_fail_policy == ExecutorExceptionPolicies.propagate: raise exc - elif self._task_fail_policy == ExecutorExceptionPolicies.log_only: + if self._task_fail_policy == ExecutorExceptionPolicies.log_only: pass elif self._task_fail_policy == ExecutorExceptionPolicies.stop_all: logger.info( diff --git a/aea/helpers/pipe.py b/aea/helpers/pipe.py index 4f1b606e37..2e79f0a27f 100644 --- a/aea/helpers/pipe.py +++ b/aea/helpers/pipe.py @@ -28,23 +28,30 @@ import tempfile from abc import ABC, abstractmethod from asyncio import AbstractEventLoop +from shutil import rmtree from typing import IO, Optional +from aea.exceptions import enforce + _default_logger = logging.getLogger(__name__) PIPE_CONN_TIMEOUT = 10.0 PIPE_CONN_ATTEMPTS = 10 +TCP_SOCKET_PIPE_CLIENT_CONN_ATTEMPTS = 5 + -class LocalPortablePipe(ABC): +class IPCChannelClient(ABC): """ - Multi-platform interprocess communication channel + Multi-platform interprocess communication channel for the client side """ @abstractmethod async def connect(self, timeout=PIPE_CONN_TIMEOUT) -> bool: """ - Setup the communication channel with the other process + Connect to communication channel + + :param timeout: timeout for other end to connect """ @abstractmethod @@ -52,6 +59,8 @@ async def write(self, data: bytes) -> None: """ Write `data` bytes to the other end of the channel Will first write the size than the actual data + + :param data: bytes to write """ @abstractmethod @@ -59,6 +68,8 @@ async def read(self) -> Optional[bytes]: """ Read bytes from the other end of the channel Will first read the size than the actual data + + :return: read bytes """ @abstractmethod @@ -67,85 +78,217 @@ async def close(self) -> None: Close the communication channel """ + +class IPCChannel(IPCChannelClient): + """ + Multi-platform interprocess communication channel + """ + @property @abstractmethod def in_path(self) -> str: """ - Returns the rendezvous point for incoming communication + Rendezvous point for incoming communication """ @property @abstractmethod def out_path(self) -> str: """ - Returns the rendezvous point for outgoing communication + Rendezvous point for outgoing communication """ -class TCPSocketPipe(LocalPortablePipe): +class PosixNamedPipeProtocol: """ - Interprocess communication implementation using tcp sockets + Posix named pipes async wrapper communication protocol """ def __init__( self, + in_path: str, + out_path: str, logger: logging.Logger = _default_logger, loop: Optional[AbstractEventLoop] = None, ): + """ + Initialize a new posix named pipe + + :param in_path: rendezvous point for incoming data + :param out_path: rendezvous point for outgoing daa + """ + self.logger = logger - self._timeout = 0 - self._server = None # type: Optional[asyncio.AbstractServer] - self._connected = None # type: Optional[asyncio.Event] - self._reader = None # type: Optional[asyncio.StreamReader] - self._writer = None # type: Optional[asyncio.StreamWriter] - self._loop = loop if loop is not None else asyncio.get_event_loop() + self._loop = loop + self._in_path = in_path + self._out_path = out_path + self._in = -1 + self._out = -1 - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.bind(("", 0)) - s.listen(1) - self._port = s.getsockname()[1] - s.close() + self._stream_reader = None # type: Optional[asyncio.StreamReader] + self._reader_protocol = None # type: Optional[asyncio.StreamReaderProtocol] + self._fileobj = None # type: Optional[IO[str]] - async def connect(self, timeout=PIPE_CONN_TIMEOUT) -> bool: - self._connected = asyncio.Event() - self._timeout = timeout if timeout > 0 else 0 - self._server = await asyncio.start_server( - self._handle_connection, host="127.0.0.1", port=self._port + self._connection_attempts = PIPE_CONN_ATTEMPTS + self._connection_timeout = PIPE_CONN_TIMEOUT + + async def connect(self, timeout: float = PIPE_CONN_TIMEOUT) -> bool: + """ + Connect to the other end of the pipe + + :param timeout: timeout before failing + :return: connection success + """ + + if self._loop is None: + self._loop = asyncio.get_event_loop() + + self._connection_timeout = timeout / PIPE_CONN_ATTEMPTS if timeout > 0 else 0 + if self._connection_attempts <= 1: # pragma: no cover + return False + self._connection_attempts -= 1 + + self.logger.debug( + "Attempt opening pipes {}, {}...".format(self._in_path, self._out_path) ) - assert self._server.sockets is not None - self._port = self._server.sockets[0].getsockname()[1] - self.logger.debug("socket pipe setup {}".format(self._port)) + + self._in = os.open(self._in_path, os.O_RDONLY | os.O_NONBLOCK | os.O_SYNC) try: - await asyncio.wait_for(self._connected.wait(), self._timeout) - except asyncio.TimeoutError: # pragma: no cover - return False + self._out = os.open(self._out_path, os.O_WRONLY | os.O_NONBLOCK) + except OSError as e: + if e.errno == errno.ENXIO: + self.logger.debug("Sleeping for {}...".format(self._connection_timeout)) + await asyncio.sleep(self._connection_timeout) + return await self.connect(timeout) + raise e # pragma: no cover - self._server.close() - await self._server.wait_closed() + # setup reader + enforce( + self._in != -1 and self._out != -1 and self._loop is not None, + "Incomplete initialization.", + ) + self._stream_reader = asyncio.StreamReader(loop=self._loop) + self._reader_protocol = asyncio.StreamReaderProtocol( + self._stream_reader, loop=self._loop + ) + self._fileobj = os.fdopen(self._in, "r") + await self._loop.connect_read_pipe( + lambda: self.__reader_protocol, self._fileobj + ) return True - async def _handle_connection( - self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter + @property + def __reader_protocol(self) -> asyncio.StreamReaderProtocol: + """Get reader protocol.""" + if self._reader_protocol is None: + raise ValueError("reader protocol not set!") # pragma: nocover + return self._reader_protocol + + async def write(self, data: bytes) -> None: + """ + Write to pipe. + + :param data: bytes to write to pipe + """ + self.logger.debug("writing {}...".format(len(data))) + size = struct.pack("!I", len(data)) + os.write(self._out, size + data) + await asyncio.sleep(0.0) + + async def read(self) -> Optional[bytes]: + """ + Read from pipe. + + :return: read bytes + """ + if self._stream_reader is None: # pragma: nocover + raise ValueError("StreamReader not set, call connect first!") + try: + self.logger.debug("waiting for messages (in={})...".format(self._in_path)) + buf = await self._stream_reader.readexactly(4) + if not buf: # pragma: no cover + return None + size = struct.unpack("!I", buf)[0] + if size <= 0: # pragma: no cover + return None + data = await self._stream_reader.readexactly(size) + if not data: # pragma: no cover + return None + return data + except asyncio.IncompleteReadError as e: # pragma: no cover + self.logger.info( + "Connection disconnected while reading from pipe ({}/{})".format( + len(e.partial), e.expected + ) + ) + return None + except asyncio.CancelledError: # pragma: no cover + return None + + async def close(self) -> None: + """ Disconnect pipe """ + self.logger.debug("closing pipe (in={})...".format(self._in_path)) + if self._fileobj is None: + raise ValueError("Pipe not connected") # pragma: nocover + try: + # TOFIX(LR) Hack for MacOSX + size = struct.pack("!I", 0) + os.write(self._out, size) + + os.close(self._out) + self._fileobj.close() + except OSError: # pragma: no cover + pass + await asyncio.sleep(0) + + +class TCPSocketProtocol: + """ + TCP socket communication protocol + """ + + def __init__( + self, + reader: asyncio.StreamReader, + writer: asyncio.StreamWriter, + logger: logging.Logger = _default_logger, + loop: Optional[AbstractEventLoop] = None, ): - assert self._connected is not None - self._connected.set() + """ + Initialize the tcp socket protocol + + :param reader: established asyncio reader + :param writer: established asyncio writer + """ + + self.logger = logger + self.loop = loop if loop is not None else asyncio.get_event_loop() self._reader = reader self._writer = writer async def write(self, data: bytes) -> None: - assert self._writer is not None + """ + Write to socket. + + :param data: bytes to write + """ + if self._writer is None: + raise ValueError("writer not set!") # pragma: nocover + self.logger.debug("writing {}...".format(len(data))) size = struct.pack("!I", len(data)) - self._writer.write(size) - self._writer.write(data) + self._writer.write(size + data) await self._writer.drain() async def read(self) -> Optional[bytes]: - self.logger.debug("Reading pipes...") - assert self._reader is not None + """ + Read from socket. + + :return: read bytes + """ try: - self.logger.debug("Waiting for messages...") + self.logger.debug("waiting for messages...") buf = await self._reader.readexactly(4) if not buf: # pragma: no cover return None @@ -156,30 +299,123 @@ async def read(self) -> Optional[bytes]: return data except asyncio.IncompleteReadError as e: # pragma: no cover self.logger.info( - "Connection disconnected while reading from node ({}/{})".format( + "Connection disconnected while reading from pipe ({}/{})".format( len(e.partial), e.expected ) ) return None + except asyncio.CancelledError: # pragma: no cover + return None async def close(self) -> None: - assert self._writer is not None, "Pipe not connected" + """ Disconnect socket """ self._writer.write_eof() await self._writer.drain() self._writer.close() + +class TCPSocketChannel(IPCChannel): + """ + Interprocess communication channel implementation using tcp sockets + """ + + def __init__( + self, + logger: logging.Logger = _default_logger, + loop: Optional[AbstractEventLoop] = None, + ): + """ Initialize tcp socket interprocess communication channel""" + self.logger = logger + self._loop = loop + self._server = None # type: Optional[asyncio.AbstractServer] + self._connected = None # type: Optional[asyncio.Event] + self._sock = None # type: Optional[TCPSocketProtocol] + + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind(("", 0)) + s.listen(1) + self._port = s.getsockname()[1] + s.close() + + async def connect(self, timeout: float = PIPE_CONN_TIMEOUT) -> bool: + """ + Setup communication channel and wait for other end to connect + + :param timeout: timeout for the connection to be established + """ + + if self._loop is None: + self._loop = asyncio.get_event_loop() + + self._connected = asyncio.Event() + self._server = await asyncio.start_server( + self._handle_connection, host="127.0.0.1", port=self._port + ) + if self._server.sockets is None: + raise ValueError("Server sockets is None!") # pragma: nocover + self._port = self._server.sockets[0].getsockname()[1] + self.logger.debug("socket pipe rdv point: {}".format(self._port)) + + try: + await asyncio.wait_for(self._connected.wait(), timeout) + except asyncio.TimeoutError: # pragma: no cover + return False + + self._server.close() + await self._server.wait_closed() + + return True + + async def _handle_connection( + self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter + ): + if self._connected is None: + raise ValueError("Connected is None!") # pragma: nocover + self._connected.set() + self._sock = TCPSocketProtocol( + reader, writer, logger=self.logger, loop=self._loop + ) + + async def write(self, data: bytes) -> None: + """ + Write to channel. + + :param data: bytes to write + """ + if self._sock is None: + raise ValueError("Socket pipe not connected.") # pragma: nocover + await self._sock.write(data) + + async def read(self) -> Optional[bytes]: + """ + Read from channel. + + :param data: read bytes + """ + if self._sock is None: + raise ValueError("Socket pipe not connected.") # pragma: nocover + return await self._sock.read() + + async def close(self) -> None: + """ Disconnect from channel and clean it up """ + if self._sock is None: + raise ValueError("Socket pipe not connected.") # pragma: nocover + await self._sock.close() + @property def in_path(self) -> str: + """ Rendezvous point for incoming communication """ return str(self._port) @property def out_path(self) -> str: + """ Rendezvous point for outgoing communication """ return str(self._port) -class PosixNamedPipe(LocalPortablePipe): +class PosixNamedPipeChannel(IPCChannel): """ - Interprocess communication implementation using Posix named pipes + Interprocess communication channel implementation using Posix named pipes """ def __init__( @@ -187,13 +423,13 @@ def __init__( logger: logging.Logger = _default_logger, loop: Optional[AbstractEventLoop] = None, ): + """ Initialize posix named pipe interprocess communication channel """ self.logger = logger - tmp_dir = tempfile.mkdtemp() - self._in_path = "{}/process_to_aea".format(tmp_dir) - self._out_path = "{}/aea_to_process".format(tmp_dir) - self._in = -1 - self._out = -1 - self._loop = loop if loop is not None else asyncio.get_event_loop() + self._loop = loop + + self._pipe_dir = tempfile.mkdtemp() + self._in_path = "{}/process_to_aea".format(self._pipe_dir) + self._out_path = "{}/aea_to_process".format(self._pipe_dir) # setup fifos self.logger.debug( @@ -206,119 +442,246 @@ def __init__( os.mkfifo(self._in_path) os.mkfifo(self._out_path) - self._stream_reader = None # type: Optional[asyncio.StreamReader] - self._log_file_desc = None # type: Optional[IO[str]] - self._reader_protocol = None # type: Optional[asyncio.StreamReaderProtocol] - self._fileobj = None # type: Optional[IO[str]] - - self._connection_attempts = PIPE_CONN_ATTEMPTS - self._connection_timeout = -1 - - async def connect(self, timeout=PIPE_CONN_TIMEOUT) -> bool: - self._connection_timeout = timeout / PIPE_CONN_ATTEMPTS if timeout > 0 else 0 - if self._connection_attempts <= 1: # pragma: no cover - return False - self._connection_attempts -= 1 - - self.logger.debug( - "Attempt opening pipes {}, {}...".format(self._in_path, self._out_path) + self._pipe = PosixNamedPipeProtocol( + self._in_path, self._out_path, logger=logger, loop=loop ) - self._in = os.open(self._in_path, os.O_RDONLY | os.O_NONBLOCK) - - try: - self._out = os.open(self._out_path, os.O_WRONLY | os.O_NONBLOCK) - except OSError as e: - if e.errno == errno.ENXIO: - self.logger.debug("Sleeping for {}...".format(self._connection_timeout)) - await asyncio.sleep(self._connection_timeout) - return await self.connect(timeout) - else: - raise e # pragma: no cover + async def connect(self, timeout: float = PIPE_CONN_TIMEOUT) -> bool: + """ + Setup communication channel and wait for other end to connect - # setup reader - assert ( - self._in != -1 and self._out != -1 and self._loop is not None - ), "Incomplete initialization." - self._stream_reader = asyncio.StreamReader(loop=self._loop) - self._reader_protocol = asyncio.StreamReaderProtocol( - self._stream_reader, loop=self._loop - ) - self._fileobj = os.fdopen(self._in, "r") - await self._loop.connect_read_pipe( - lambda: self.__reader_protocol, self._fileobj - ) + :param timeout: timeout for connection to be established + """ - return True + if self._loop is None: + self._loop = asyncio.get_event_loop() - @property - def __reader_protocol(self) -> asyncio.StreamReaderProtocol: - """Get reader protocol.""" - assert self._reader_protocol is not None, "reader protocol not set!" - return self._reader_protocol + return await self._pipe.connect(timeout) async def write(self, data: bytes) -> None: """ - Write to the writer stream. + Write to the channel. - :param data: data to write to stream + :param data: data to write to channel """ - self.logger.debug("writing {}...".format(str(data))) - size = struct.pack("!I", len(data)) - os.write(self._out, size) - os.write(self._out, data) + await self._pipe.write(data) async def read(self) -> Optional[bytes]: """ - Read from the reader stream. + Read from the channel. - :return: bytes + :return: read bytes """ - self.logger.debug("reading {}...".format("")) - assert ( - self._stream_reader is not None - ), "StreamReader not set, call connect first!" - try: - self.logger.debug("Waiting for messages...") - buf = await self._stream_reader.readexactly(4) - if not buf: # pragma: no cover - return None - size = struct.unpack("!I", buf)[0] - data = await self._stream_reader.readexactly(size) - if not data: # pragma: no cover - return None - return data - except asyncio.IncompleteReadError as e: # pragma: no cover - self.logger.info( - "Connection disconnected while reading from pipe ({}/{})".format( - len(e.partial), e.expected - ) - ) - return None + return await self._pipe.read() async def close(self) -> None: - assert self._fileobj is not None, "Pipe not connected" - self._fileobj.close() - os.close(self._out) - await asyncio.sleep(0) + """ + Close the channel and clean it up + """ + await self._pipe.close() + rmtree(self._pipe_dir) @property def in_path(self) -> str: + """ Rendezvous point for incoming communication """ return self._in_path @property def out_path(self) -> str: + """ Rendezvous point for outgoing communication """ return self._out_path -def make_pipe(logger: logging.Logger = _default_logger) -> LocalPortablePipe: +class TCPSocketChannelClient(IPCChannelClient): """ - Build a portable bidirectional Interprocess Communication Channel + Interprocess communication channel client using tcp sockets """ + def __init__( # pylint: disable=unused-argument + self, + in_path: str, + out_path: str, + logger: logging.Logger = _default_logger, + loop: Optional[AbstractEventLoop] = None, + ): + """ + Initialize a tcp socket communication channel client + + :param in_path: rendezvous point for incoming data + :param out_path: rendezvous point for outgoing data + """ + self.logger = logger + self._loop = loop + + self._port = int(in_path) + self._sock = None # type: Optional[TCPSocketProtocol] + + self._attempts = TCP_SOCKET_PIPE_CLIENT_CONN_ATTEMPTS + self._timeout = PIPE_CONN_TIMEOUT / self._attempts + + async def connect(self, timeout: float = PIPE_CONN_TIMEOUT) -> bool: + """ + Connect to the other end of the communication channel + + :param timeout: timeout for connection to be established + """ + + if self._loop is None: + self._loop = asyncio.get_event_loop() + + self._timeout = timeout / TCP_SOCKET_PIPE_CLIENT_CONN_ATTEMPTS + + self.logger.debug( + "Attempting to connect to {}:{}.....".format("127.0.0.1", self._port) + ) + + connected = False + while self._attempts > 0: + self._attempts -= 1 + try: + reader, writer = await asyncio.open_connection( + "127.0.0.1", + self._port, # pylint: disable=protected-access + loop=self._loop, + ) + self._sock = TCPSocketProtocol( + reader, writer, logger=self.logger, loop=self._loop + ) + connected = True + break + except ConnectionRefusedError: + await asyncio.sleep(self._timeout) + except Exception: # pylint: disable=broad-except # pragma: nocover + return False + + return connected + + async def write(self, data: bytes) -> None: + """ + Write data to channel + + :param data: bytes to write + """ + if self._sock is None: + raise ValueError("Socket pipe not connected.") # pragma: nocover + await self._sock.write(data) + + async def read(self) -> Optional[bytes]: + """ + Read data from channel + + :return: read bytes + """ + if self._sock is None: + raise ValueError("Socket pipe not connected.") # pragma: nocover + return await self._sock.read() + + async def close(self) -> None: + """ Disconnect from communication channel """ + if self._sock is None: + raise ValueError("Socket pipe not connected.") # pragma: nocover + await self._sock.close() + + +class PosixNamedPipeChannelClient(IPCChannelClient): + """ + Interprocess communication channel client using Posix named pipes + """ + + def __init__( + self, + in_path: str, + out_path: str, + logger: logging.Logger = _default_logger, + loop: Optional[AbstractEventLoop] = None, + ): + """ + Initialize a posix named pipe communication channel client + + :param in_path: rendezvous point for incoming data + :param out_path: rendezvous point for outgoing data + """ + + self.logger = logger + self._loop = loop + + self._in_path = in_path + self._out_path = out_path + self._pipe = None # type: Optional[PosixNamedPipeProtocol] + + async def connect(self, timeout: float = PIPE_CONN_TIMEOUT) -> bool: + """ + Connect to the other end of the communication channel + + :param timeout: timeout for connection to be established + """ + + if self._loop is None: + self._loop = asyncio.get_event_loop() + + self._pipe = PosixNamedPipeProtocol( + self._in_path, self._out_path, logger=self.logger, loop=self._loop + ) + return await self._pipe.connect() + + async def write(self, data: bytes) -> None: + """ + Write data to channel + + :param data: bytes to write + """ + if self._pipe is None: + raise ValueError("Pipe not connected.") # pragma: nocover + await self._pipe.write(data) + + async def read(self) -> Optional[bytes]: + """ + Read data from channel + + :return: read bytes + """ + if self._pipe is None: + raise ValueError("Pipe not connected.") # pragma: nocover + return await self._pipe.read() + + async def close(self) -> None: + """ Disconnect from communication channel """ + if self._pipe is None: + raise ValueError("Pipe not connected.") # pragma: nocover + return await self._pipe.close() + + +def make_ipc_channel( + logger: logging.Logger = _default_logger, loop: Optional[AbstractEventLoop] = None +) -> IPCChannel: + """ + Build a portable bidirectional InterProcess Communication channel + """ + if os.name == "posix": + return PosixNamedPipeChannel(logger=logger, loop=loop) + if os.name == "nt": # pragma: nocover + return TCPSocketChannel(logger=logger, loop=loop) + raise Exception( # pragma: nocover + "make ipc channel is not supported on platform {}".format(os.name) + ) + + +def make_ipc_channel_client( + in_path: str, + out_path: str, + logger: logging.Logger = _default_logger, + loop: Optional[AbstractEventLoop] = None, +) -> IPCChannelClient: + """ + Build a portable bidirectional InterProcess Communication client channel + + :param in_path: rendezvous point for incoming communication + :param out_path: rendezvous point for outgoing outgoing + """ if os.name == "posix": - return PosixNamedPipe(logger=logger) - elif os.name == "nt": # pragma: nocover - return TCPSocketPipe(logger=logger) - else: # pragma: nocover - raise Exception("make pipe is not supported on platform {}".format(os.name)) + return PosixNamedPipeChannelClient(in_path, out_path, logger=logger, loop=loop) + if os.name == "nt": # pragma: nocover + return TCPSocketChannelClient(in_path, out_path, logger=logger, loop=loop) + raise Exception( # pragma: nocover + "make ip channel client is not supported on platform {}".format(os.name) + ) diff --git a/aea/helpers/preference_representations/base.py b/aea/helpers/preference_representations/base.py index b99c5b193f..357df7af28 100644 --- a/aea/helpers/preference_representations/base.py +++ b/aea/helpers/preference_representations/base.py @@ -22,6 +22,8 @@ import math from typing import Dict +from aea.exceptions import enforce + def logarithmic_utility( utility_params_by_good_id: Dict[str, float], @@ -36,9 +38,10 @@ def logarithmic_utility( :param quantity_shift: a non-negative factor to shift the quantities in the utility function (to ensure the natural logarithm can be used on the entire range of quantities) :return: utility value """ - assert ( - quantity_shift >= 0 - ), "The quantity_shift argument must be a non-negative integer." + enforce( + quantity_shift >= 0, + "The quantity_shift argument must be a non-negative integer.", + ) goodwise_utility = [ utility_params_by_good_id[good_id] * math.log(quantity + quantity_shift) if quantity + quantity_shift > 0 diff --git a/aea/helpers/search/generic.py b/aea/helpers/search/generic.py index d42a843cb5..5af99adc88 100644 --- a/aea/helpers/search/generic.py +++ b/aea/helpers/search/generic.py @@ -21,6 +21,7 @@ from typing import Any, Dict, List +from aea.exceptions import enforce from aea.helpers.search.models import Attribute, DataModel, Location SUPPORTED_TYPES = {"str": str, "int": int, "float": float, "bool": bool} @@ -33,13 +34,15 @@ def __init__(self, data_model_name: str, data_model_attributes: Dict[str, Any]): """Initialise the dataModel.""" self.attributes = [] # type: List[Attribute] for values in data_model_attributes.values(): - assert ( - values["type"] in SUPPORTED_TYPES.keys() - ), "Type is not supported. Use str, int, float or bool" - assert isinstance(values["name"], str), "Name must be a string!" - assert isinstance( - values["is_required"], bool - ), "Wrong type for is_required. Must be bool!" + enforce( + values["type"] in SUPPORTED_TYPES.keys(), + "Type is not supported. Use str, int, float or bool", + ) + enforce(isinstance(values["name"], str), "Name must be a string!") + enforce( + isinstance(values["is_required"], bool), + "Wrong type for is_required. Must be bool!", + ) self.attributes.append( Attribute( name=values["name"], # type: ignore diff --git a/aea/helpers/search/models.py b/aea/helpers/search/models.py index de16a1628e..02dbe0b686 100644 --- a/aea/helpers/search/models.py +++ b/aea/helpers/search/models.py @@ -27,6 +27,8 @@ from math import asin, cos, radians, sin, sqrt from typing import Any, Dict, List, Mapping, Optional, Tuple, Type, Union, cast +from aea.exceptions import enforce + logger = logging.getLogger(__name__) @@ -61,8 +63,7 @@ def __eq__(self, other): """Compare equality of two locations.""" if not isinstance(other, Location): return False # pragma: nocover - else: - return self.latitude == other.latitude and self.longitude == other.longitude + return self.latitude == other.latitude and self.longitude == other.longitude """ @@ -258,7 +259,7 @@ def _check_consistency(self): attribute.name, attribute.type ) ) - elif not type(value) in ALLOWED_ATTRIBUTE_TYPES: + if not type(value) in ALLOWED_ATTRIBUTE_TYPES: # value type matches data model, but it is not an allowed type raise AttributeInconsistencyException( "Attribute {} has unallowed type: {}. Allowed types: {}".format( @@ -349,7 +350,7 @@ def __init__(self, type_: Union[ConstraintTypes, str], value: Any): """ self.type = ConstraintTypes(type_) self.value = value - assert self.check_validity(), "ConstraintType initialization inconsistent." + enforce(self.check_validity(), "ConstraintType initialization inconsistent.") def check_validity(self): """ @@ -360,41 +361,90 @@ def check_validity(self): """ try: if self.type == ConstraintTypes.EQUAL: - assert isinstance(self.value, (int, float, str, bool)) + enforce( + isinstance(self.value, (int, float, str, bool)), + f"Expected one of type in (int, float, str, bool), got {self.value}", + ) elif self.type == ConstraintTypes.NOT_EQUAL: - assert isinstance(self.value, (int, float, str, bool)) + enforce( + isinstance(self.value, (int, float, str, bool)), + f"Expected one of type in (int, float, str, bool), got {self.value}", + ) elif self.type == ConstraintTypes.LESS_THAN: - assert isinstance(self.value, (int, float, str)) + enforce( + isinstance(self.value, (int, float, str)), + f"Expected one of type in (int, float, str), got {self.value}", + ) elif self.type == ConstraintTypes.LESS_THAN_EQ: - assert isinstance(self.value, (int, float, str)) + enforce( + isinstance(self.value, (int, float, str)), + f"Expected one of type in (int, float, str), got {self.value}", + ) elif self.type == ConstraintTypes.GREATER_THAN: - assert isinstance(self.value, (int, float, str)) + enforce( + isinstance(self.value, (int, float, str)), + f"Expected one of type in (int, float, str), got {self.value}", + ) elif self.type == ConstraintTypes.GREATER_THAN_EQ: - assert isinstance(self.value, (int, float, str)) + enforce( + isinstance(self.value, (int, float, str)), + f"Expected one of type in (int, float, str), got {self.value}", + ) 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])) + enforce( + isinstance(self.value, (list, tuple)), + f"Expected one of type in (list, tuple), got {self.value}", + ) + enforce( + len(self.value) == 2, f"Expected length=2, got {len(self.value)}" + ) + enforce( + isinstance(self.value[0], type(self.value[1])), "Invalid types." + ) + enforce( + isinstance(self.value[1], type(self.value[0])), "Invalid types." + ) elif self.type == ConstraintTypes.IN: - assert isinstance(self.value, (list, tuple, set)) + enforce( + isinstance(self.value, (list, tuple, set)), + f"Expected one of type in (list, tuple, set), got {self.value}", + ) if len(self.value) > 0: _type = type(next(iter(self.value))) - assert all(isinstance(obj, _type) for obj in self.value) + enforce( + all(isinstance(obj, _type) for obj in self.value), + "Invalid types.", + ) elif self.type == ConstraintTypes.NOT_IN: - assert isinstance(self.value, (list, tuple, set)) + enforce( + isinstance(self.value, (list, tuple, set)), + f"Expected one of type in (list, tuple, set), got {self.value}", + ) if len(self.value) > 0: _type = type(next(iter(self.value))) - assert all(isinstance(obj, _type) for obj in self.value) + enforce( + all(isinstance(obj, _type) for obj in self.value), + "Invalid types.", + ) elif self.type == ConstraintTypes.DISTANCE: - assert isinstance(self.value, (list, tuple)) - assert len(self.value) == 2 - assert isinstance(self.value[0], Location) - assert isinstance(self.value[1], float) + enforce( + isinstance(self.value, (list, tuple)), + f"Expected one of type in (list, tuple), got {self.value}", + ) + enforce( + len(self.value) == 2, f"Expected length=2, got {len(self.value)}" + ) + enforce( + isinstance(self.value[0], Location), + "Invalid type, expected Location.", + ) + enforce( + isinstance(self.value[1], float), "Invalid type, expected Location." + ) else: # pragma: nocover raise ValueError("Type not recognized.") - except (AssertionError, ValueError): - return False + except ValueError: + return False # pragma: nocover return True @@ -455,31 +505,31 @@ def check(self, value: ATTRIBUTE_TYPES) -> bool: """ if self.type == ConstraintTypes.EQUAL: return self.value == value - elif self.type == ConstraintTypes.NOT_EQUAL: + if self.type == ConstraintTypes.NOT_EQUAL: return self.value != value - elif self.type == ConstraintTypes.LESS_THAN: + if self.type == ConstraintTypes.LESS_THAN: return self.value < value - elif self.type == ConstraintTypes.LESS_THAN_EQ: + if self.type == ConstraintTypes.LESS_THAN_EQ: return self.value <= value - elif self.type == ConstraintTypes.GREATER_THAN: + if self.type == ConstraintTypes.GREATER_THAN: return self.value > value - elif self.type == ConstraintTypes.GREATER_THAN_EQ: + if self.type == ConstraintTypes.GREATER_THAN_EQ: return self.value >= value - elif self.type == ConstraintTypes.WITHIN: + if self.type == ConstraintTypes.WITHIN: low = self.value[0] high = self.value[1] return low <= value <= high - elif self.type == ConstraintTypes.IN: + if self.type == ConstraintTypes.IN: return value in self.value - elif self.type == ConstraintTypes.NOT_IN: + if self.type == ConstraintTypes.NOT_IN: return value not in self.value - elif self.type == ConstraintTypes.DISTANCE: - assert isinstance(value, Location), "Value must be of type Location." + if self.type == ConstraintTypes.DISTANCE: + if not isinstance(value, Location): # pragma: nocover + raise ValueError("Value must be of type Location.") location = cast(Location, self.value[0]) distance = self.value[1] return location.distance(value) <= distance - else: # pragma: nocover - raise ValueError("Constraint type not recognized.") + raise ValueError("Constraint type not recognized.") # pragma: nocover def __eq__(self, other): """Check equality with another object.""" @@ -560,7 +610,7 @@ def check_validity(self): :return ``None`` :raises ValueError: if the object does not satisfy some requirements. """ - if len(self.constraints) < 2: # pragma: nocover # TODO: do we need this check? + if len(self.constraints) < 2: # pragma: nocover raise ValueError( "Invalid input value for type '{}': number of " "subexpression must be at least 2.".format(type(self).__name__) @@ -609,7 +659,7 @@ def check_validity(self): :return ``None`` :raises ValueError: if the object does not satisfy some requirements. """ - if len(self.constraints) < 2: # pragma: nocover # TODO: do we need this check? + if len(self.constraints) < 2: # pragma: nocover raise ValueError( "Invalid input value for type '{}': number of " "subexpression must be at least 2.".format(type(self).__name__) diff --git a/aea/helpers/transaction/base.py b/aea/helpers/transaction/base.py index 86a5c391a9..d90e5b9679 100644 --- a/aea/helpers/transaction/base.py +++ b/aea/helpers/transaction/base.py @@ -25,6 +25,7 @@ from typing import Any, Dict, List, Optional, Tuple from aea.crypto.ledger_apis import LedgerApis +from aea.exceptions import enforce Address = str @@ -42,8 +43,8 @@ def __init__( def _check_consistency(self) -> None: """Check consistency of the object.""" - assert isinstance(self._ledger_id, str), "ledger_id must be str" - assert self._body is not None, "body must not be None" + enforce(isinstance(self._ledger_id, str), "ledger_id must be str") + enforce(self._body is not None, "body must not be None") @property def ledger_id(self) -> str: @@ -113,11 +114,12 @@ def __init__( def _check_consistency(self) -> None: """Check consistency of the object.""" - assert isinstance(self._ledger_id, str), "ledger_id must be str" - assert self._body is not None, "body must not be None" - assert isinstance( - self._is_deprecated_mode, bool - ), "is_deprecated_mode must be bool" + enforce(isinstance(self._ledger_id, str), "ledger_id must be str") + enforce(self._body is not None, "body must not be None") + enforce( + isinstance(self._is_deprecated_mode, bool), + "is_deprecated_mode must be bool", + ) @property def ledger_id(self) -> str: @@ -188,8 +190,8 @@ def __init__( def _check_consistency(self) -> None: """Check consistency of the object.""" - assert isinstance(self._ledger_id, str), "ledger_id must be str" - assert self._body is not None, "body must not be None" + enforce(isinstance(self._ledger_id, str), "ledger_id must be str") + enforce(self._body is not None, "body must not be None") @property def ledger_id(self) -> str: @@ -260,11 +262,12 @@ def __init__( def _check_consistency(self) -> None: """Check consistency of the object.""" - assert isinstance(self._ledger_id, str), "ledger_id must be str" - assert isinstance(self._body, str), "body must be string" - assert isinstance( - self._is_deprecated_mode, bool - ), "is_deprecated_mode must be bool" + enforce(isinstance(self._ledger_id, str), "ledger_id must be str") + enforce(isinstance(self._body, str), "body must be string") + enforce( + isinstance(self._is_deprecated_mode, bool), + "is_deprecated_mode must be bool", + ) @property def ledger_id(self) -> str: @@ -337,8 +340,8 @@ def __init__(self, ledger_id: str, body: bytes): def _check_consistency(self) -> None: """Check consistency of the object.""" - assert isinstance(self._ledger_id, str), "ledger_id must be str" - assert self._body is not None, "body must not be None" + enforce(isinstance(self._ledger_id, str), "ledger_id must be str") + enforce(self._body is not None, "body must not be None") @property def ledger_id(self) -> str: @@ -463,42 +466,59 @@ def __init__( def _check_consistency(self) -> None: """Check consistency of the object.""" - assert isinstance(self._ledger_id, str), "ledger_id must be str" - assert isinstance(self._sender_address, str), "sender_address must be str" - assert isinstance( - self._counterparty_address, str - ), "counterparty_address must be str" - assert isinstance(self._amount_by_currency_id, dict) and all( - [ - isinstance(key, str) and isinstance(value, int) - for key, value in self._amount_by_currency_id.items() - ] - ), "amount_by_currency_id must be a dictionary with str keys and int values." - assert isinstance(self._quantities_by_good_id, dict) and all( - [ - isinstance(key, str) and isinstance(value, int) - for key, value in self._quantities_by_good_id.items() - ] - ), "quantities_by_good_id must be a dictionary with str keys and int values." - assert isinstance( - self._is_sender_payable_tx_fee, bool - ), "is_sender_payable_tx_fee must be bool" - assert isinstance(self._nonce, str), "nonce must be str" - assert self._fee_by_currency_id is None or ( - isinstance(self._fee_by_currency_id, dict) + enforce(isinstance(self._ledger_id, str), "ledger_id must be str") + enforce(isinstance(self._sender_address, str), "sender_address must be str") + enforce( + isinstance(self._counterparty_address, str), + "counterparty_address must be str", + ) + enforce( + isinstance(self._amount_by_currency_id, dict) and all( [ - isinstance(key, str) and isinstance(value, int) and value >= 0 - for key, value in self._fee_by_currency_id.items() + isinstance(key, str) and isinstance(value, int) + for key, value in self._amount_by_currency_id.items() ] - ) - ), "fee must be None or Dict[str, int] with positive fees only." - assert all( - [ - key in self._amount_by_currency_id - for key in self._fee_by_currency_id.keys() - ] - ), "Fee dictionary has keys which are not present in amount dictionary." + ), + "amount_by_currency_id must be a dictionary with str keys and int values.", + ) + enforce( + isinstance(self._quantities_by_good_id, dict) + and all( + [ + isinstance(key, str) and isinstance(value, int) + for key, value in self._quantities_by_good_id.items() + ] + ), + "quantities_by_good_id must be a dictionary with str keys and int values.", + ) + enforce( + isinstance(self._is_sender_payable_tx_fee, bool), + "is_sender_payable_tx_fee must be bool", + ) + enforce(isinstance(self._nonce, str), "nonce must be str") + enforce( + self._fee_by_currency_id is None + or ( + isinstance(self._fee_by_currency_id, dict) + and all( + [ + isinstance(key, str) and isinstance(value, int) and value >= 0 + for key, value in self._fee_by_currency_id.items() + ] + ) + ), + "fee must be None or Dict[str, int] with positive fees only.", + ) + enforce( + all( + [ + key in self._amount_by_currency_id + for key in self._fee_by_currency_id.keys() + ] + ), + "Fee dictionary has keys which are not present in amount dictionary.", + ) if self._is_strict: is_pos_amounts = all( [amount >= 0 for amount in self._amount_by_currency_id.values()] @@ -512,9 +532,11 @@ def _check_consistency(self) -> None: is_neg_quantities = all( [quantity <= 0 for quantity in self._quantities_by_good_id.values()] ) - assert (is_pos_amounts and is_neg_quantities) or ( - is_neg_amounts and is_pos_quantities - ), "quantities and amounts do not constitute valid terms. All quantities must be of same sign. All amounts must be of same sign. Quantities and amounts must be of different sign." + enforce( + (is_pos_amounts and is_neg_quantities) + or (is_neg_amounts and is_pos_quantities), + "quantities and amounts do not constitute valid terms. All quantities must be of same sign. All amounts must be of same sign. Quantities and amounts must be of different sign.", + ) @property def id(self) -> str: @@ -549,7 +571,9 @@ def counterparty_address(self) -> Address: @counterparty_address.setter def counterparty_address(self, counterparty_address: Address) -> None: """Set the counterparty address.""" - assert isinstance(counterparty_address, str), "counterparty_address must be str" + enforce( + isinstance(counterparty_address, str), "counterparty_address must be str" + ) self._counterparty_address = counterparty_address @property @@ -577,16 +601,17 @@ def is_empty_currency(self) -> bool: @property def currency_id(self) -> str: """Get the amount the sender must pay.""" - assert self.is_single_currency, "More than one currency id, cannot get id." + enforce(self.is_single_currency, "More than one currency id, cannot get id.") value = next(iter(self._amount_by_currency_id.keys())) return value @property def sender_payable_amount(self) -> int: """Get the amount the sender must pay.""" - assert ( - self.is_single_currency or self.is_empty_currency - ), "More than one currency id, cannot get amount." + enforce( + self.is_single_currency or self.is_empty_currency, + "More than one currency id, cannot get amount.", + ) value = ( next(iter(self._amount_by_currency_id.values())) if not self.is_empty_currency @@ -598,9 +623,10 @@ def sender_payable_amount(self) -> int: @property def sender_payable_amount_incl_fee(self) -> int: """Get the amount the sender must pay inclusive fee.""" - assert ( - self.is_single_currency or self.is_empty_currency - ), "More than one currency id, cannot get amount." + enforce( + self.is_single_currency or self.is_empty_currency, + "More than one currency id, cannot get amount.", + ) payable = self.sender_payable_amount if self.is_sender_payable_tx_fee and len(self._fee_by_currency_id) == 1: payable += next(iter(self._fee_by_currency_id.values())) @@ -609,9 +635,10 @@ def sender_payable_amount_incl_fee(self) -> int: @property def counterparty_payable_amount(self) -> int: """Get the amount the counterparty must pay.""" - assert ( - self.is_single_currency or self.is_empty_currency - ), "More than one currency id, cannot get amount." + enforce( + self.is_single_currency or self.is_empty_currency, + "More than one currency id, cannot get amount.", + ) value = ( next(iter(self._amount_by_currency_id.values())) if not self.is_empty_currency @@ -623,9 +650,10 @@ def counterparty_payable_amount(self) -> int: @property def counterparty_payable_amount_incl_fee(self) -> int: """Get the amount the counterparty must pay.""" - assert ( - self.is_single_currency or self.is_empty_currency - ), "More than one currency id, cannot get amount." + enforce( + self.is_single_currency or self.is_empty_currency, + "More than one currency id, cannot get amount.", + ) payable = self.counterparty_payable_amount if not self.is_sender_payable_tx_fee and len(self._fee_by_currency_id) == 1: payable += next(iter(self._fee_by_currency_id.values())) @@ -664,10 +692,11 @@ def has_fee(self) -> bool: @property def fee(self) -> int: """Get the fee.""" - assert self.has_fee, "fee_by_currency_id not set." - assert ( - len(self.fee_by_currency_id) == 1 - ), "More than one currency id, cannot get fee." + enforce(self.has_fee, "fee_by_currency_id not set.") + enforce( + len(self.fee_by_currency_id) == 1, + "More than one currency id, cannot get fee.", + ) return next(iter(self._fee_by_currency_id.values())) @property @@ -838,8 +867,8 @@ def __init__(self, ledger_id: str, body: Any): def _check_consistency(self) -> None: """Check consistency of the object.""" - assert isinstance(self._ledger_id, str), "ledger_id must be str" - assert self._body is not None, "body must not be None" + enforce(isinstance(self._ledger_id, str), "ledger_id must be str") + enforce(self._body is not None, "body must not be None") @property def ledger_id(self) -> str: @@ -910,9 +939,9 @@ def __init__(self, ledger_id: str, receipt: Any, transaction: Any): def _check_consistency(self) -> None: """Check consistency of the object.""" - assert isinstance(self._ledger_id, str), "ledger_id must be str" - assert self._receipt is not None, "receipt must not be None" - assert self._transaction is not None, "transaction must not be None" + enforce(isinstance(self._ledger_id, str), "ledger_id must be str") + enforce(self._receipt is not None, "receipt must not be None") + enforce(self._transaction is not None, "transaction must not be None") @property def ledger_id(self) -> str: diff --git a/aea/identity/base.py b/aea/identity/base.py index 2de6985fb1..888a3b4375 100644 --- a/aea/identity/base.py +++ b/aea/identity/base.py @@ -21,8 +21,8 @@ from typing import Dict, Optional +from aea.common import Address from aea.configurations.constants import DEFAULT_LEDGER -from aea.mail.base import Address DEFAULT_ADDRESS_KEY = DEFAULT_LEDGER @@ -53,20 +53,22 @@ def __init__( :param default_address_key: the key for the default address. """ self._name = name - assert default_address_key is not None, "Provide a key for the default address." - assert (address is None) != ( - addresses is None - ), "Either provide a single address or a dictionary of addresses, not both." + if default_address_key is None: + raise ValueError( + "Provide a key for the default address." + ) # pragma: nocover + if (address is None) == (addresses is None): + raise ValueError( + "Either provide a single address or a dictionary of addresses, not both." + ) if address is None: - assert (addresses is not None) and len( - addresses - ) > 0, "Provide at least one pair of addresses." + if addresses is None or len(addresses) == 0: # pragma: nocover + raise ValueError("Provide at least one pair of addresses.") address = addresses[default_address_key] self._address = address if addresses is None: addresses = {default_address_key: address} self._addresses = addresses - self._default_address_key = default_address_key @property def name(self) -> str: diff --git a/aea/launcher.py b/aea/launcher.py index 0f7901fdde..8512bd7485 100644 --- a/aea/launcher.py +++ b/aea/launcher.py @@ -67,11 +67,11 @@ def _set_logger( default_logging_config, # pylint: disable=import-outside-toplevel ) - logger = logging.getLogger("aea") - logger = default_logging_config(logger) + logger_ = logging.getLogger("aea") + logger_ = default_logging_config(logger_) if log_level is not None: level = logging.getLevelName(log_level) - logger.setLevel(level) + logger_.setLevel(level) def _run_agent( @@ -252,5 +252,4 @@ def _make_tasks(self) -> Sequence[AbstractExecutorTask]: AEADirMultiprocessTask(agent_dir, log_level=self._log_level) for agent_dir in self._agent_dirs ] - else: - return [AEADirTask(agent_dir) for agent_dir in self._agent_dirs] + return [AEADirTask(agent_dir) for agent_dir in self._agent_dirs] diff --git a/aea/mail/base.py b/aea/mail/base.py index 9ec1977584..7e41685ec5 100644 --- a/aea/mail/base.py +++ b/aea/mail/base.py @@ -20,19 +20,18 @@ import logging from abc import ABC, abstractmethod -from typing import Optional, Union +from typing import Optional, Tuple, Union from urllib.parse import urlparse -from aea.configurations.base import ProtocolId, PublicId, SkillId +from aea.common import Address +from aea.configurations.base import PackageId, ProtocolId, PublicId +from aea.exceptions import enforce from aea.mail import base_pb2 from aea.protocols.base import Message logger = logging.getLogger(__name__) -Address = str - - class AEAConnectionError(Exception): """Exception class for connection errors.""" @@ -141,33 +140,81 @@ class EnvelopeContext: """Extra information for the handling of an envelope.""" def __init__( - self, connection_id: Optional[PublicId] = None, uri: Optional[URI] = None + self, + connection_id: Optional[PublicId] = None, + skill_id: Optional[PublicId] = None, + uri: Optional[URI] = None, ): """ Initialize the envelope context. :param connection_id: the connection id used for routing the outgoing envelope in the multiplexer. + :param skill_id: the skill id used for routing the incoming envelope in the AEA. :param uri: the URI sent with the envelope. """ - self.connection_id = connection_id + skill_id_from_uri, connection_id_from_uri = ( + self._get_public_ids_from_uri(uri) if uri is not None else (None, None) + ) + if connection_id_from_uri and connection_id: + raise ValueError("Cannot define connection_id explicitly and in URI.") + self._connection_id = connection_id or connection_id_from_uri + if skill_id_from_uri and skill_id: + raise ValueError("Cannot define skill_id explicitly and in URI.") + self._skill_id = skill_id or skill_id_from_uri self.uri = uri + @property + def connection_id(self) -> Optional[PublicId]: + """Get the connection id.""" + return self._connection_id + + @property + def skill_id(self) -> Optional[PublicId]: + """Get the skill id.""" + return self._skill_id + @property def uri_raw(self) -> str: """Get uri in string format.""" return str(self.uri) if self.uri is not None else "" + @staticmethod + def _get_public_ids_from_uri( + uri: URI, + ) -> Tuple[Optional[PublicId], Optional[PublicId]]: + """ + Try get skill and connection id from uri. + + :param uri: the uri + :return: (skill_id if present in uri, connection if present in uri) + """ + skill_id = None + connection_id = None + try: + package_id = PackageId.from_uri_path(uri.path) + package_type = str(package_id.package_type) + if package_type == "skill": + skill_id = package_id.public_id + elif package_type == "connection": + connection_id = package_id.public_id + else: + raise ValueError( + f"Invalid package type {package_type} in uri for envelope context." + ) + except ValueError as e: + logger.debug(f"URI - {uri.path} - not a valid package_id id. Error: {e}") + return (skill_id, connection_id) + def __str__(self): """Get the string representation.""" - return "EnvelopeContext(connection_id={connection_id}, uri_raw={uri_raw})".format( - connection_id=str(self.connection_id), uri_raw=str(self.uri), - ) + return f"EnvelopeContext(connection_id={self.connection_id}, skill_id={self.skill_id}, uri_raw={self.uri_raw})" def __eq__(self, other): """Compare with another object.""" return ( isinstance(other, EnvelopeContext) and self.connection_id == other.connection_id + and self.skill_id == other.skill_id and self.uri == other.uri ) @@ -274,6 +321,8 @@ def __init__( :param message: the protocol-specific message. :param context: the optional envelope context. """ + if isinstance(message, Message): + message = self._check_consistency(message, to, sender) self._to = to self._sender = sender self._protocol_id = protocol_id @@ -333,21 +382,47 @@ def context(self) -> EnvelopeContext: return self._context @property - def skill_id(self) -> Optional[SkillId]: + def skill_id(self) -> Optional[PublicId]: """ Get the skill id from an envelope context, if set. :return: skill id """ skill_id = None # Optional[PublicId] - if self.context is not None and self.context.uri is not None: - uri_path = self.context.uri.path - try: - skill_id = PublicId.from_uri_path(uri_path) - except ValueError: - logger.debug("URI - {} - not a valid skill id.".format(uri_path)) + if self.context is not None: + skill_id = self.context.skill_id return skill_id + @property + def connection_id(self) -> Optional[PublicId]: + """ + Get the connection id from an envelope context, if set. + + :return: connection id + """ + connection_id = None # Optional[PublicId] + if self.context is not None: + connection_id = self.context.connection_id + return connection_id + + @staticmethod + def _check_consistency(message: Message, to: str, sender: str) -> Message: + """Check consistency of sender and to.""" + if message.has_to: + enforce( + message.to == to, "To specified on message does not match envelope." + ) + else: + message.to = to + if message.has_sender: + enforce( + message.sender == sender, + "Sender specified on message does not match envelope.", + ) + else: + message.sender = sender + return message + def __eq__(self, other): """Compare with another object.""" return ( diff --git a/aea/multiplexer.py b/aea/multiplexer.py index 55fe80ce64..c3098018fe 100644 --- a/aea/multiplexer.py +++ b/aea/multiplexer.py @@ -26,12 +26,12 @@ from aea.configurations.base import PublicId from aea.connections.base import Connection, ConnectionStates +from aea.exceptions import enforce from aea.helpers.async_friendly_queue import AsyncFriendlyQueue -from aea.helpers.async_utils import ThreadedAsyncRunner, cancel_and_wait +from aea.helpers.async_utils import AsyncState, ThreadedAsyncRunner, cancel_and_wait from aea.helpers.logging import WithLogger from aea.mail.base import ( AEAConnectionError, - Address, Empty, Envelope, EnvelopeContext, @@ -40,17 +40,34 @@ from aea.protocols.base import Message -# TODO refactoring: this should be an enum -# but beware of backward-compatibility. - - -class ConnectionStatus: +class MultiplexerStatus(AsyncState): """The connection status class.""" def __init__(self): """Initialize the connection status.""" - self.is_connected = False # type: bool - self.is_connecting = False # type: bool + super().__init__( + initial_state=ConnectionStates.disconnected, states_enum=ConnectionStates + ) + + @property + def is_connected(self) -> bool: # pragma: nocover + """Return is connected.""" + return self.get() == ConnectionStates.connected + + @property + def is_connecting(self) -> bool: # pragma: nocover + """Return is connecting.""" + return self.get() == ConnectionStates.connecting + + @property + def is_disconnected(self) -> bool: # pragma: nocover + """Return is disconnected.""" + return self.get() == ConnectionStates.disconnected + + @property + def is_disconnecting(self) -> bool: # pragma: nocover + """Return is disconnected.""" + return self.get() == ConnectionStates.disconnecting class AsyncMultiplexer(WithLogger): @@ -78,7 +95,7 @@ def __init__( self._default_connection: Optional[Connection] = None self._initialize_connections_if_any(connections, default_connection_index) - self._connection_status = ConnectionStatus() + self._connection_status = MultiplexerStatus() self._in_queue = AsyncFriendlyQueue() # type: AsyncFriendlyQueue self._out_queue = None # type: Optional[asyncio.Queue] @@ -108,9 +125,10 @@ def _initialize_connections_if_any( self, connections: Optional[Sequence[Connection]], default_connection_index: int ): if connections is not None and len(connections) > 0: - assert ( - 0 <= default_connection_index <= len(connections) - 1 - ), "Default connection index out of range." + enforce( + (0 <= default_connection_index <= len(connections) - 1), + "Default connection index out of range.", + ) for idx, connection in enumerate(connections): self.add_connection(connection, idx == default_connection_index) @@ -137,13 +155,15 @@ def _connection_consistency_checks(self): Do some consistency checks on the multiplexer connections. :return: None - :raise AssertionError: if an inconsistency is found. + :raise AEAEnforceError: if an inconsistency is found. """ - assert len(self.connections) > 0, "List of connections cannot be empty." + enforce(len(self.connections) > 0, "List of connections cannot be empty.") - assert len(set(c.connection_id for c in self.connections)) == len( - self.connections - ), "Connection names must be unique." + enforce( + len(set(c.connection_id for c in self.connections)) + == len(self.connections), + "Connection names must be unique.", + ) def _set_default_connection_if_none(self): """Set the default connection if it is none.""" @@ -158,9 +178,8 @@ def in_queue(self) -> AsyncFriendlyQueue: @property def out_queue(self) -> asyncio.Queue: """Get the out queue.""" - assert ( - self._out_queue is not None - ), "Accessing out queue before loop is started." + if self._out_queue is None: # pragma: nocover + raise ValueError("Accessing out queue before loop is started.") return self._out_queue @property @@ -171,7 +190,7 @@ def connections(self) -> Tuple[Connection, ...]: @property def is_connected(self) -> bool: """Check whether the multiplexer is processing envelopes.""" - return all(c.is_connected for c in self._connections) + return self.connection_status.is_connected @property def default_routing(self) -> Dict[PublicId, PublicId]: @@ -184,7 +203,7 @@ def default_routing(self, default_routing: Dict[PublicId, PublicId]): self._default_routing = default_routing @property - def connection_status(self) -> ConnectionStatus: + def connection_status(self) -> MultiplexerStatus: """Get the connection status.""" return self._connection_status @@ -199,15 +218,19 @@ async def connect(self) -> None: self.logger.debug("Multiplexer already connected.") return try: + self.connection_status.set(ConnectionStates.connecting) await self._connect_all() - assert self.is_connected, "At least one connection failed to connect!" - self._connection_status.is_connected = True + + if all(c.is_connected for c in self._connections): + self.connection_status.set(ConnectionStates.connected) + else: + raise AEAConnectionError("Failed to connect the multiplexer.") + self._recv_loop_task = self._loop.create_task(self._receiving_loop()) self._send_loop_task = self._loop.create_task(self._send_loop()) self.logger.debug("Multiplexer connected and running.") except (CancelledError, Exception): self.logger.exception("Exception on connect:") - self._connection_status.is_connected = False await self._stop() raise AEAConnectionError("Failed to connect the multiplexer.") @@ -215,14 +238,14 @@ async def disconnect(self) -> None: """Disconnect the multiplexer.""" self.logger.debug("Multiplexer disconnecting...") async with self._lock: - if not self.connection_status.is_connected: + if self.connection_status.is_disconnected: self.logger.debug("Multiplexer already disconnected.") await asyncio.wait_for(self._stop(), timeout=60) return try: + self.connection_status.set(ConnectionStates.disconnecting) await asyncio.wait_for(self._disconnect_all(), timeout=60) await asyncio.wait_for(self._stop(), timeout=60) - self._connection_status.is_connected = False self.logger.debug("Multiplexer disconnected.") except (CancelledError, Exception): self.logger.exception("Exception on disconnect:") @@ -245,12 +268,13 @@ async def _stop(self) -> None: await cancel_and_wait(self._send_loop_task) self._send_loop_task = None - for connection in [ - c - for c in self.connections - if c.state in (ConnectionStates.connecting, ConnectionStates.connected) - ]: - await connection.disconnect() + if all([c.is_disconnected for c in self.connections]): + self.connection_status.set(ConnectionStates.disconnected) + else: + raise AEAConnectionError( + "Failed to disconnect multiplexer, some connections are not disconnected!" + ) + self.logger.debug("Multiplexer stopped.") async def _connect_all(self) -> None: @@ -286,7 +310,6 @@ async def _connect_one(self, connection_id: PublicId) -> None: "Connection {} already established.".format(connection.connection_id) ) else: - connection.loop = self._loop await connection.connect() self.logger.debug( "Connection {} has been set up successfully.".format( @@ -365,7 +388,6 @@ async def _receiving_loop(self) -> None: while self.connection_status.is_connected and len(task_to_connection) > 0: try: - # self.self.logger.debug("Waiting for incoming envelopes...") done, _pending = await asyncio.wait( task_to_connection.keys(), return_when=asyncio.FIRST_COMPLETED ) @@ -420,6 +442,7 @@ async def _send(self, envelope: Envelope) -> None: "No connection registered with id: {}.".format(connection_id) ) + # third, if no other option route by default connection if connection_id is None: self.logger.debug( "Using default connection: {}".format(self.default_connection) @@ -582,7 +605,7 @@ def put(self, envelope: Envelope) -> None: # type: ignore # cause overrides co def setup( self, connections: Collection[Connection], - default_routing: Dict[PublicId, PublicId], + default_routing: Optional[Dict[PublicId, PublicId]] = None, default_connection: Optional[PublicId] = None, ) -> None: """ @@ -593,7 +616,7 @@ def setup( :param default_connection: the default connection. :return: None. """ - self.default_routing = default_routing + self.default_routing = default_routing or {} self._connections = [] for c in connections: self.add_connection(c, c.public_id == default_connection) @@ -689,16 +712,14 @@ async def async_wait(self) -> None: class OutBox: """A queue from where you can only enqueue envelopes.""" - def __init__(self, multiplexer: Multiplexer, default_address: Address): + def __init__(self, multiplexer: Multiplexer): """ Initialize the outbox. :param multiplexer: the multiplexer - :param default_address: the default address of the agent """ super().__init__() self._multiplexer = multiplexer - self._default_address = default_address def empty(self) -> bool: """ @@ -716,7 +737,7 @@ def put(self, envelope: Envelope) -> None: :return: None """ self._multiplexer.logger.debug( - "Put an envelope in the queue: to='{}' sender='{}' protocol_id='{}' message='{!r}' context='{}'...".format( + "Put an envelope in the queue: to='{}' sender='{}' protocol_id='{}' message='{!r}' context='{}'.".format( envelope.to, envelope.sender, envelope.protocol_id, @@ -729,38 +750,33 @@ def put(self, envelope: Envelope) -> None: "Only Message type allowed in envelope message field when putting into outbox." ) message = cast(Message, envelope.message) - if not message.has_counterparty: # pragma: nocover - raise ValueError("Provided message has message.counterparty not set.") + if not message.has_to: # pragma: nocover + raise ValueError("Provided message has message.to not set.") if not message.has_sender: # pragma: nocover raise ValueError("Provided message has message.sender not set.") self._multiplexer.put(envelope) def put_message( - self, - message: Message, - context: Optional[EnvelopeContext] = None, - sender: Optional[str] = None, + self, message: Message, context: Optional[EnvelopeContext] = None, ) -> None: """ Put a message in the outbox. This constructs an envelope with the input arguments. - "sender" is a deprecated kwarg and will be removed in the next version - :param message: the message :param context: the envelope context :return: None """ if not isinstance(message, Message): raise ValueError("Provided message not of type Message.") - if not message.has_counterparty: - raise ValueError("Provided message has message.counterparty not set.") - if not message.has_sender and sender is None: + if not message.has_to: + raise ValueError("Provided message has message.to not set.") + if not message.has_sender: raise ValueError("Provided message has message.sender not set.") envelope = Envelope( - to=message.counterparty, - sender=sender or message.sender, # TODO: remove "sender" + to=message.to, + sender=message.sender, protocol_id=message.protocol_id, message=message, context=context, diff --git a/aea/protocols/base.py b/aea/protocols/base.py index dcd602293a..f79089ac4e 100644 --- a/aea/protocols/base.py +++ b/aea/protocols/base.py @@ -20,7 +20,6 @@ """This module contains the base message and serialization definition.""" import importlib import inspect -import json import logging import re from abc import ABC, abstractmethod @@ -31,14 +30,14 @@ from google.protobuf.struct_pb2 import Struct -from aea.components.base import Component +from aea.components.base import Component, load_aea_package from aea.configurations.base import ( - ComponentConfiguration, ComponentType, ProtocolConfig, PublicId, ) -from aea.helpers.base import load_aea_package +from aea.configurations.loader import load_component_configuration +from aea.exceptions import enforce logger = logging.getLogger(__name__) @@ -67,10 +66,8 @@ def __init__(self, body: Optional[Dict] = None, **kwargs): """ self._to = None # type: Optional[Address] self._sender = None # type: Optional[Address] - self._counterparty = None # type: Optional[Address] self._body = copy(body) if body else {} # type: Dict[str, Any] self._body.update(kwargs) - self._is_incoming = False try: self._is_consistent() except Exception as e: # pylint: disable=broad-except @@ -88,13 +85,14 @@ def sender(self) -> Address: :return the address """ - assert self._sender is not None, "Sender must not be None." + if self._sender is None: + raise ValueError("Message's 'Sender' field must be set.") # pragma: nocover return self._sender @sender.setter def sender(self, sender: Address) -> None: """Set the sender of the message.""" - # assert self._sender is None, "Sender already set." + enforce(self._sender is None, "Sender already set.") self._sender = sender @property @@ -105,49 +103,16 @@ def has_to(self) -> bool: @property def to(self) -> Address: """Get address of receiver.""" - assert self._to is not None, "To must not be None." + if self._to is None: + raise ValueError("Message's 'To' field must be set.") return self._to @to.setter def to(self, to: Address) -> None: """Set address of receiver.""" - assert self._to is None, "To already set." + enforce(self._to is None, "To already set.") self._to = to - @property - def has_counterparty(self) -> bool: - """Check if the counterparty is set.""" - return self._counterparty is not None - - @property - def counterparty(self) -> Address: - """ - Get the counterparty of the message in Address form. - - :return the address - """ - assert self._counterparty is not None, "Counterparty must not be None." - return self._counterparty - - @counterparty.setter - def counterparty(self, counterparty: Address) -> None: - """Set the counterparty of the message.""" - self._counterparty = counterparty - - @property - def is_incoming(self) -> bool: - """ - Get the is_incoming value of the message. - - :return whether the message is incoming or is out going - """ - return self._is_incoming - - @is_incoming.setter - def is_incoming(self, is_incoming: bool) -> None: - """Set the is_incoming of the message.""" - self._is_incoming = is_incoming - @property def body(self) -> Dict: """ @@ -170,25 +135,29 @@ def body(self, body: Dict) -> None: @property def dialogue_reference(self) -> Tuple[str, str]: """Get the dialogue_reference of the message.""" - assert self.is_set("dialogue_reference"), "dialogue_reference is not set." + if not self.is_set("dialogue_reference"): + raise ValueError("dialogue_reference is not set.") # pragma: nocover return cast(Tuple[str, str], self.get("dialogue_reference")) @property def message_id(self) -> int: """Get the message_id of the message.""" - assert self.is_set("message_id"), "message_id is not set." + if not self.is_set("message_id"): + raise ValueError("message_id is not set.") # pragma: nocover return cast(int, self.get("message_id")) @property def performative(self) -> "Performative": """Get the performative of the message.""" - assert self.is_set("performative"), "performative is not set." + if not self.is_set("performative"): + raise ValueError("performative is not set.") # pragma: nocover return cast(Message.Performative, self.get("performative")) @property def target(self) -> int: """Get the target of the message.""" - assert self.is_set("target"), "target is not set." + if not self.is_set("target"): + raise ValueError("target is not set.") # pragma: nocover return cast(int, self.get("target")) def set(self, key: str, value: Any) -> None: @@ -205,10 +174,6 @@ def get(self, key: str) -> Optional[Any]: """Get value for key.""" return self._body.get(key, None) - def unset(self, key: str) -> None: - """Unset valye for key.""" - self._body.pop(key, None) - def is_set(self, key: str) -> bool: """Check value is set for key.""" return key in self._body @@ -221,15 +186,20 @@ def __eq__(self, other): """Compare with another object.""" return ( isinstance(other, Message) + and self._sender == other._sender + and self._to == other._to + # and self.dialogue_reference == other.dialogue_reference # noqa: E800 + # and self.message_id == other.message_id # noqa: E800 + # and self.target == other.target # noqa: E800 + # and self.performative == other.performative # noqa: E800 and self.body == other.body - and self._counterparty == other._counterparty ) def __str__(self): """Get the string representation of the message.""" return ( - "Message(" - + " ".join( + "Message(sender={},to={},".format(self._sender, self._to) + + ",".join( map( lambda key_value: str(key_value[0]) + "=" + str(key_value[1]), self.body.items(), @@ -301,36 +271,6 @@ def decode(obj: bytes) -> Message: return msg -class JSONSerializer(Serializer): - """ - Default serialization in JSON for the Message object. - - It assumes that the Message contains a JSON-serializable body. - """ - - @staticmethod - def encode(msg: Message) -> bytes: - """ - Encode a message into bytes using JSON format. - - :param msg: the message to be encoded. - :return: the serialized message. - """ - bytes_msg = json.dumps(msg.body).encode("utf-8") - return bytes_msg - - @staticmethod - def decode(obj: bytes) -> Message: - """ - Decode bytes into a message using JSON. - - :param obj: the serialized message. - :return: the decoded message. - """ - json_msg = json.loads(obj.decode("utf-8")) - return Message(json_msg) - - class Protocol(Component): """ This class implements a specifications for a protocol. @@ -366,7 +306,7 @@ def from_dir(cls, directory: str, **kwargs) -> "Protocol": """ configuration = cast( ProtocolConfig, - ComponentConfiguration.load(ComponentType.PROTOCOL, Path(directory)), + load_component_configuration(ComponentType.PROTOCOL, Path(directory)), ) configuration.directory = Path(directory) return Protocol.from_config(configuration, **kwargs) @@ -379,9 +319,8 @@ def from_config(cls, configuration: ProtocolConfig, **kwargs) -> "Protocol": :param configuration: the protocol configuration. :return: the protocol object. """ - assert ( - configuration.directory is not None - ), "Configuration must be associated with a directory." + if configuration.directory is None: # pragma: nocover + raise ValueError("Configuration must be associated with a directory.") load_aea_package(configuration) class_module = importlib.import_module( configuration.prefix_import_path + ".message" @@ -395,7 +334,7 @@ def from_config(cls, configuration: ProtocolConfig, **kwargs) -> "Protocol": lambda x: re.match("{}Message".format(name_camel_case), x[0]), classes ) ) - assert len(message_classes) == 1, "Not exactly one message class detected." + enforce(len(message_classes) == 1, "Not exactly one message class detected.") message_class = message_classes[0][1] class_module = importlib.import_module( configuration.prefix_import_path + ".serialization" @@ -407,9 +346,9 @@ def from_config(cls, configuration: ProtocolConfig, **kwargs) -> "Protocol": classes, ) ) - assert ( - len(serializer_classes) == 1 - ), "Not exactly one serializer class detected." + enforce( + len(serializer_classes) == 1, "Not exactly one serializer class detected." + ) serialize_class = serializer_classes[0][1] message_class.serializer = serialize_class diff --git a/aea/protocols/default/README.md b/aea/protocols/default/README.md index 1a4e445a58..ef5a05da42 100644 --- a/aea/protocols/default/README.md +++ b/aea/protocols/default/README.md @@ -1,15 +1,5 @@ # Default Protocol -**Name:** default - -**Author**: fetchai - -**Version**: 0.4.0 - -**Short Description**: A protocol for exchanging any bytes message. - -**License**: Apache-2.0 - ## Description This is a protocol for two agents exchanging any bytes messages. @@ -20,10 +10,10 @@ This is a protocol for two agents exchanging any bytes messages. --- name: default author: fetchai -version: 0.4.0 +version: 0.5.0 description: A protocol for exchanging any bytes message. license: Apache-2.0 -aea_version: '>=0.5.0, <0.6.0' +aea_version: '>=0.6.0, <0.7.0' speech_acts: bytes: content: pt:bytes diff --git a/aea/protocols/default/dialogues.py b/aea/protocols/default/dialogues.py index 402304c5f3..9ac396bf19 100644 --- a/aea/protocols/default/dialogues.py +++ b/aea/protocols/default/dialogues.py @@ -25,12 +25,12 @@ """ from abc import ABC -from typing import Dict, FrozenSet, Optional, cast +from typing import Callable, FrozenSet, Type, cast -from aea.helpers.dialogue.base import Dialogue, DialogueLabel, Dialogues -from aea.mail.base import Address +from aea.common import Address from aea.protocols.base import Message from aea.protocols.default.message import DefaultMessage +from aea.protocols.dialogue.base import Dialogue, DialogueLabel, Dialogues class DefaultDialogue(Dialogue): @@ -63,44 +63,26 @@ class EndState(Dialogue.EndState): def __init__( self, dialogue_label: DialogueLabel, - agent_address: Optional[Address] = None, - role: Optional[Dialogue.Role] = None, + self_address: Address, + role: Dialogue.Role, + message_class: Type[DefaultMessage] = DefaultMessage, ) -> None: """ Initialize a dialogue. :param dialogue_label: the identifier of the dialogue - :param agent_address: the address of the agent for whom this dialogue is maintained + :param self_address: the address of the entity for whom this dialogue is maintained :param role: the role of the agent this dialogue is maintained for :return: None """ Dialogue.__init__( self, dialogue_label=dialogue_label, - agent_address=agent_address, + message_class=message_class, + self_address=self_address, role=role, - rules=Dialogue.Rules( - cast(FrozenSet[Message.Performative], self.INITIAL_PERFORMATIVES), - cast(FrozenSet[Message.Performative], self.TERMINAL_PERFORMATIVES), - cast( - Dict[Message.Performative, FrozenSet[Message.Performative]], - self.VALID_REPLIES, - ), - ), ) - def is_valid(self, message: Message) -> bool: - """ - Check whether 'message' is a valid next message in the dialogue. - - These rules capture specific constraints designed for dialogues which are instances of a concrete sub-class of this class. - Override this method with your additional dialogue rules. - - :param message: the message to be validated - :return: True if valid, False otherwise - """ - return True - class DefaultDialogues(Dialogues, ABC): """This class keeps track of all default dialogues.""" @@ -109,31 +91,23 @@ class DefaultDialogues(Dialogues, ABC): {DefaultDialogue.EndState.SUCCESSFUL, DefaultDialogue.EndState.FAILED} ) - def __init__(self, agent_address: Address) -> None: + def __init__( + self, + self_address: Address, + role_from_first_message: Callable[[Message, Address], Dialogue.Role], + dialogue_class: Type[DefaultDialogue] = DefaultDialogue, + ) -> None: """ Initialize dialogues. - :param agent_address: the address of the agent for whom dialogues are maintained + :param self_address: the address of the entity for whom dialogues are maintained :return: None """ Dialogues.__init__( self, - agent_address=agent_address, + self_address=self_address, end_states=cast(FrozenSet[Dialogue.EndState], self.END_STATES), + message_class=DefaultMessage, + dialogue_class=dialogue_class, + role_from_first_message=role_from_first_message, ) - - def create_dialogue( - self, dialogue_label: DialogueLabel, role: Dialogue.Role, - ) -> DefaultDialogue: - """ - Create an instance of default dialogue. - - :param dialogue_label: the identifier of the dialogue - :param role: the role of the agent this dialogue is maintained for - - :return: the created dialogue - """ - dialogue = DefaultDialogue( - dialogue_label=dialogue_label, agent_address=self.agent_address, role=role - ) - return dialogue diff --git a/aea/protocols/default/message.py b/aea/protocols/default/message.py index 4bba6f7706..547286e375 100644 --- a/aea/protocols/default/message.py +++ b/aea/protocols/default/message.py @@ -20,10 +20,10 @@ """This module contains default's message definition.""" import logging -from enum import Enum from typing import Dict, Set, Tuple, cast from aea.configurations.base import ProtocolId +from aea.exceptions import AEAEnforceError, enforce from aea.protocols.base import Message from aea.protocols.default.custom_types import ErrorCode as CustomErrorCode @@ -35,11 +35,11 @@ class DefaultMessage(Message): """A protocol for exchanging any bytes message.""" - protocol_id = ProtocolId.from_str("fetchai/default:0.4.0") + protocol_id = ProtocolId.from_str("fetchai/default:0.5.0") ErrorCode = CustomErrorCode - class Performative(Enum): + class Performative(Message.Performative): """Performatives for the default protocol.""" BYTES = "bytes" @@ -65,6 +65,7 @@ def __init__( :param target: the message target. :param performative: the message performative. """ + self._performatives = {"bytes", "error"} super().__init__( dialogue_reference=dialogue_reference, message_id=message_id, @@ -72,7 +73,6 @@ def __init__( performative=DefaultMessage.Performative(performative), **kwargs, ) - self._performatives = {"bytes", "error"} @property def valid_performatives(self) -> Set[str]: @@ -82,86 +82,92 @@ def valid_performatives(self) -> Set[str]: @property def dialogue_reference(self) -> Tuple[str, str]: """Get the dialogue_reference of the message.""" - assert self.is_set("dialogue_reference"), "dialogue_reference is not set." + enforce(self.is_set("dialogue_reference"), "dialogue_reference is not set.") return cast(Tuple[str, str], self.get("dialogue_reference")) @property def message_id(self) -> int: """Get the message_id of the message.""" - assert self.is_set("message_id"), "message_id is not set." + enforce(self.is_set("message_id"), "message_id is not set.") return cast(int, self.get("message_id")) @property def performative(self) -> Performative: # type: ignore # noqa: F821 """Get the performative of the message.""" - assert self.is_set("performative"), "performative is not set." + enforce(self.is_set("performative"), "performative is not set.") return cast(DefaultMessage.Performative, self.get("performative")) @property def target(self) -> int: """Get the target of the message.""" - assert self.is_set("target"), "target is not set." + enforce(self.is_set("target"), "target is not set.") return cast(int, self.get("target")) @property def content(self) -> bytes: """Get the 'content' content from the message.""" - assert self.is_set("content"), "'content' content is not set." + enforce(self.is_set("content"), "'content' content is not set.") return cast(bytes, self.get("content")) @property def error_code(self) -> CustomErrorCode: """Get the 'error_code' content from the message.""" - assert self.is_set("error_code"), "'error_code' content is not set." + enforce(self.is_set("error_code"), "'error_code' content is not set.") return cast(CustomErrorCode, self.get("error_code")) @property def error_data(self) -> Dict[str, bytes]: """Get the 'error_data' content from the message.""" - assert self.is_set("error_data"), "'error_data' content is not set." + enforce(self.is_set("error_data"), "'error_data' content is not set.") return cast(Dict[str, bytes], self.get("error_data")) @property def error_msg(self) -> str: """Get the 'error_msg' content from the message.""" - assert self.is_set("error_msg"), "'error_msg' content is not set." + enforce(self.is_set("error_msg"), "'error_msg' content is not set.") return cast(str, self.get("error_msg")) def _is_consistent(self) -> bool: """Check that the message follows the default protocol.""" try: - assert ( - type(self.dialogue_reference) == tuple - ), "Invalid type for 'dialogue_reference'. Expected 'tuple'. Found '{}'.".format( - type(self.dialogue_reference) + enforce( + type(self.dialogue_reference) == tuple, + "Invalid type for 'dialogue_reference'. Expected 'tuple'. Found '{}'.".format( + type(self.dialogue_reference) + ), ) - assert ( - type(self.dialogue_reference[0]) == str - ), "Invalid type for 'dialogue_reference[0]'. Expected 'str'. Found '{}'.".format( - type(self.dialogue_reference[0]) + enforce( + type(self.dialogue_reference[0]) == str, + "Invalid type for 'dialogue_reference[0]'. Expected 'str'. Found '{}'.".format( + type(self.dialogue_reference[0]) + ), ) - assert ( - type(self.dialogue_reference[1]) == str - ), "Invalid type for 'dialogue_reference[1]'. Expected 'str'. Found '{}'.".format( - type(self.dialogue_reference[1]) + enforce( + type(self.dialogue_reference[1]) == str, + "Invalid type for 'dialogue_reference[1]'. Expected 'str'. Found '{}'.".format( + type(self.dialogue_reference[1]) + ), ) - assert ( - type(self.message_id) == int - ), "Invalid type for 'message_id'. Expected 'int'. Found '{}'.".format( - type(self.message_id) + enforce( + type(self.message_id) == int, + "Invalid type for 'message_id'. Expected 'int'. Found '{}'.".format( + type(self.message_id) + ), ) - assert ( - type(self.target) == int - ), "Invalid type for 'target'. Expected 'int'. Found '{}'.".format( - type(self.target) + enforce( + type(self.target) == int, + "Invalid type for 'target'. Expected 'int'. Found '{}'.".format( + type(self.target) + ), ) # Light Protocol Rule 2 # Check correct performative - assert ( - type(self.performative) == DefaultMessage.Performative - ), "Invalid 'performative'. Expected either of '{}'. Found '{}'.".format( - self.valid_performatives, self.performative + enforce( + type(self.performative) == DefaultMessage.Performative, + "Invalid 'performative'. Expected either of '{}'. Found '{}'.".format( + self.valid_performatives, self.performative + ), ) # Check correct contents @@ -169,61 +175,70 @@ def _is_consistent(self) -> bool: expected_nb_of_contents = 0 if self.performative == DefaultMessage.Performative.BYTES: expected_nb_of_contents = 1 - assert ( - type(self.content) == bytes - ), "Invalid type for content 'content'. Expected 'bytes'. Found '{}'.".format( - type(self.content) + enforce( + type(self.content) == bytes, + "Invalid type for content 'content'. Expected 'bytes'. Found '{}'.".format( + type(self.content) + ), ) elif self.performative == DefaultMessage.Performative.ERROR: expected_nb_of_contents = 3 - assert ( - type(self.error_code) == CustomErrorCode - ), "Invalid type for content 'error_code'. Expected 'ErrorCode'. Found '{}'.".format( - type(self.error_code) + enforce( + type(self.error_code) == CustomErrorCode, + "Invalid type for content 'error_code'. Expected 'ErrorCode'. Found '{}'.".format( + type(self.error_code) + ), ) - assert ( - type(self.error_msg) == str - ), "Invalid type for content 'error_msg'. Expected 'str'. Found '{}'.".format( - type(self.error_msg) + enforce( + type(self.error_msg) == str, + "Invalid type for content 'error_msg'. Expected 'str'. Found '{}'.".format( + type(self.error_msg) + ), ) - assert ( - type(self.error_data) == dict - ), "Invalid type for content 'error_data'. Expected 'dict'. Found '{}'.".format( - type(self.error_data) + enforce( + type(self.error_data) == dict, + "Invalid type for content 'error_data'. Expected 'dict'. Found '{}'.".format( + type(self.error_data) + ), ) for key_of_error_data, value_of_error_data in self.error_data.items(): - assert ( - type(key_of_error_data) == str - ), "Invalid type for dictionary keys in content 'error_data'. Expected 'str'. Found '{}'.".format( - type(key_of_error_data) + enforce( + type(key_of_error_data) == str, + "Invalid type for dictionary keys in content 'error_data'. Expected 'str'. Found '{}'.".format( + type(key_of_error_data) + ), ) - assert ( - type(value_of_error_data) == bytes - ), "Invalid type for dictionary values in content 'error_data'. Expected 'bytes'. Found '{}'.".format( - type(value_of_error_data) + enforce( + type(value_of_error_data) == bytes, + "Invalid type for dictionary values in content 'error_data'. Expected 'bytes'. Found '{}'.".format( + type(value_of_error_data) + ), ) # Check correct content count - assert ( - expected_nb_of_contents == actual_nb_of_contents - ), "Incorrect number of contents. Expected {}. Found {}".format( - expected_nb_of_contents, actual_nb_of_contents + enforce( + expected_nb_of_contents == actual_nb_of_contents, + "Incorrect number of contents. Expected {}. Found {}".format( + expected_nb_of_contents, actual_nb_of_contents + ), ) # Light Protocol Rule 3 if self.message_id == 1: - assert ( - self.target == 0 - ), "Invalid 'target'. Expected 0 (because 'message_id' is 1). Found {}.".format( - self.target + enforce( + self.target == 0, + "Invalid 'target'. Expected 0 (because 'message_id' is 1). Found {}.".format( + self.target + ), ) else: - assert ( - 0 < self.target < self.message_id - ), "Invalid 'target'. Expected an integer between 1 and {} inclusive. Found {}.".format( - self.message_id - 1, self.target, + enforce( + 0 < self.target < self.message_id, + "Invalid 'target'. Expected an integer between 1 and {} inclusive. Found {}.".format( + self.message_id - 1, self.target, + ), ) - except (AssertionError, ValueError, KeyError) as e: + except (AEAEnforceError, ValueError, KeyError) as e: logger.error(str(e)) return False diff --git a/aea/protocols/default/protocol.yaml b/aea/protocols/default/protocol.yaml index fcfa1bd6b4..6019f072c0 100644 --- a/aea/protocols/default/protocol.yaml +++ b/aea/protocols/default/protocol.yaml @@ -1,17 +1,18 @@ name: default author: fetchai -version: 0.4.0 +version: 0.5.0 +type: protocol description: A protocol for exchanging any bytes message. license: Apache-2.0 -aea_version: '>=0.5.0, <0.6.0' +aea_version: '>=0.6.0, <0.7.0' fingerprint: - README.md: QmfMJ6iNNLWJLqHwjvypw5pyjWXwjDH9rnrrhQizLxDYKZ + README.md: QmP3q9463opixzdv17QXkCSQvgR8KJXgLAVkfUPpdHJzPv __init__.py: QmPMtKUrzVJp594VqNuapJzCesWLQ6Awjqv2ufG3wKNRmH custom_types.py: QmRcgwDdTxkSHyfF9eoMtsb5P5GJDm4oyLq5W6ZBko1MFU default.proto: QmNzMUvXkBm5bbitR5Yi49ADiwNn1FhCvXqSKKoqAPZyXv default_pb2.py: QmSRFi1s3jcqnPuk4yopJeNuC6o58RL7dvEdt85uns3B3N - dialogues.py: QmS3gaDAaoTD9s3YLGvXVxv9RV864TNN8Q87xQ5CALkMBm - message.py: QmbC95LcUY1pwbWtgx9no88Tuh8j2TfNQfvU9x4DjACmBR + dialogues.py: Qmc991snbS7DwFxo1cKcq1rQ2uj7y8ukp14kfe2zve387C + message.py: QmeaadvKib9QqpjZgd7NiDUqGRpC2eZPVpgq1dY3PYacht serialization.py: QmRnajc9BNCftjGkYTKCP9LnD3rq197jM3Re1GDVJTHh2y fingerprint_ignore_patterns: [] dependencies: diff --git a/aea/helpers/dialogue/__init__.py b/aea/protocols/dialogue/__init__.py similarity index 100% rename from aea/helpers/dialogue/__init__.py rename to aea/protocols/dialogue/__init__.py diff --git a/aea/helpers/dialogue/base.py b/aea/protocols/dialogue/base.py similarity index 53% rename from aea/helpers/dialogue/base.py rename to aea/protocols/dialogue/base.py index 614ce370ba..3df355ba25 100644 --- a/aea/helpers/dialogue/base.py +++ b/aea/protocols/dialogue/base.py @@ -27,14 +27,20 @@ import itertools import secrets -from abc import ABC, abstractmethod +from abc import ABC from enum import Enum +from inspect import signature from typing import Callable, Dict, FrozenSet, List, Optional, Set, Tuple, Type, cast -from aea.mail.base import Address +from aea.common import Address +from aea.exceptions import enforce from aea.protocols.base import Message +class InvalidDialogueMessage(Exception): + """Exception for adding invalid message to a dialogue.""" + + class DialogueLabel: """The dialogue label class acts as an identifier for dialogues.""" @@ -130,7 +136,7 @@ def from_json(cls, obj: Dict[str, str]) -> "DialogueLabel": def get_incomplete_version(self) -> "DialogueLabel": """Get the incomplete version of the label.""" dialogue_label = DialogueLabel( - (self.dialogue_starter_reference, Dialogue.OPPONENT_STARTER_REFERENCE), + (self.dialogue_starter_reference, Dialogue.UNASSIGNED_DIALOGUE_REFERENCE), self.dialogue_opponent_addr, self.dialogue_starter_addr, ) @@ -167,7 +173,13 @@ class Dialogue(ABC): STARTING_MESSAGE_ID = 1 STARTING_TARGET = 0 - OPPONENT_STARTER_REFERENCE = "" + UNASSIGNED_DIALOGUE_REFERENCE = "" + + INITIAL_PERFORMATIVES = frozenset() # type: FrozenSet[Message.Performative] + TERMINAL_PERFORMATIVES = frozenset() # type: FrozenSet[Message.Performative] + VALID_REPLIES = ( + dict() + ) # type: Dict[Message.Performative, FrozenSet[Message.Performative]] class Rules: """This class defines the rules for the dialogue.""" @@ -229,9 +241,10 @@ def get_valid_replies( :param performative: the performative in a message :return: list of valid performative replies """ - assert ( - performative in self.valid_replies - ), "this performative '{}' is not supported".format(performative) + enforce( + performative in self.valid_replies, + "this performative '{}' is not supported".format(performative), + ) return self.valid_replies[performative] class Role(Enum): @@ -251,25 +264,26 @@ def __str__(self): def __init__( self, dialogue_label: DialogueLabel, - message_class: Optional[Type[Message]] = None, - agent_address: Optional[Address] = None, - role: Optional[Role] = None, - rules: Optional[Rules] = None, + message_class: Type[Message], + self_address: Address, + role: Role, ) -> None: """ Initialize a dialogue. :param dialogue_label: the identifier of the dialogue - :param agent_address: the address of the agent for whom this dialogue is maintained + :param self_address: the address of the entity for whom this dialogue is maintained :param role: the role of the agent this dialogue is maintained for - :param rules: the rules of the dialogue :return: None """ - self._agent_address = agent_address + self._self_address = self_address self._incomplete_dialogue_label = dialogue_label.get_incomplete_version() self._dialogue_label = dialogue_label self._role = role + self._rules = self.Rules( + self.INITIAL_PERFORMATIVES, self.TERMINAL_PERFORMATIVES, self.VALID_REPLIES + ) self._is_self_initiated = ( dialogue_label.dialogue_opponent_addr @@ -278,10 +292,11 @@ def __init__( self._outgoing_messages = [] # type: List[Message] self._incoming_messages = [] # type: List[Message] - self._rules = rules - if message_class is not None: - assert issubclass(message_class, Message) + enforce( + issubclass(message_class, Message), + "Message class provided not a subclass of `Message`.", + ) self._message_class = message_class @property @@ -312,23 +327,15 @@ def dialogue_labels(self) -> Set[DialogueLabel]: return {self._dialogue_label, self._incomplete_dialogue_label} @property - def agent_address(self) -> Address: - """ - Get the address of the agent for whom this dialogues is maintained. - - :return: the agent address + def self_address(self) -> Address: """ - assert self._agent_address is not None, "agent_address is not set." - return self._agent_address + Get the address of the entity for whom this dialogues is maintained. - @agent_address.setter - def agent_address(self, agent_address: Address) -> None: + :return: the address of this entity """ - Set the address of the agent for whom this dialogues is maintained. - - :param: the agent address - """ - self._agent_address = agent_address + if self._self_address is None: # pragma: nocover + raise ValueError("self_address is not set.") + return self._self_address @property def role(self) -> "Role": @@ -337,19 +344,10 @@ def role(self) -> "Role": :return: the agent's role """ - assert self._role is not None, "Role is not set." + if self._role is None: # pragma: nocover + raise ValueError("Role is not set.") return self._role - @role.setter - def role(self, role: "Role") -> None: - """ - Set the agent's role in the dialogue. - - :param role: the agent's role - :return: None - """ - self._role = role - @property def rules(self) -> "Rules": """ @@ -357,7 +355,8 @@ def rules(self) -> "Rules": :return: the rules """ - assert self._rules is not None, "Rules is not set." + if self._rules is None: # pragma: nocover + raise ValueError("Rules is not set.") return self._rules @property @@ -412,147 +411,255 @@ def last_message(self) -> Optional[Message]: return last_message - def get_message(self, message_id_to_find: int) -> Optional[Message]: + @property + def is_empty(self) -> bool: """ - Get the message whose id is 'message_id'. + Check whether the dialogue is empty. + + :return: True if empty, False otherwise + """ + return len(self._outgoing_messages) == 0 and len(self._incoming_messages) == 0 + + def _counterparty_from_message(self, message: Message) -> Address: + """ + Determine the counterparty of the agent in the dialogue from a message. + + :param message: the message + :return: The address of the counterparty + """ + counterparty = ( + message.to if self._is_message_by_self(message) else message.sender + ) + return counterparty + + def _is_message_by_self(self, message: Message) -> bool: + """ + Check whether the message is by this agent or not. - :param message_id_to_find: the id of the message + :param message: the message + :return: True if message is by this agent, False otherwise + """ + return message.sender == self.self_address + + def _is_message_by_other(self, message: Message) -> bool: + """ + Check whether the message is by the counterparty agent in this dialogue or not. + + :param message: the message + :return: True if message is by the counterparty agent in this dialogue, False otherwise + """ + return not self._is_message_by_self(message) + + def _try_get_message(self, message_id: int) -> Optional[Message]: + """ + Try to get the message whose id is 'message_id'. + + :param message_id: the id of the message :return: the message if it exists, None otherwise """ result = None # type: Optional[Message] list_of_all_messages = self._outgoing_messages + self._incoming_messages for message in list_of_all_messages: - if message.message_id == message_id_to_find: + if message.message_id == message_id: result = message break - return result - @property - def is_empty(self) -> bool: + def _get_message(self, message_id: int) -> Message: """ - Check whether the dialogue is empty. + Get the message whose id is 'message_id'. - :return: True if empty, False otherwise + :param message_id: the id of the message + :return: the message + :raises: AssertionError if message is not present """ - return len(self._outgoing_messages) == 0 and len(self._incoming_messages) == 0 + message = self._try_get_message(message_id) + if message is None: + raise ValueError("Message not present.") + return message - def update(self, message: Message) -> bool: + def _has_message(self, message: Message) -> bool: """ - Extend the list of incoming/outgoing messages with 'message', if 'message' belongs to dialogue and is valid. + Check whether a message exists in this dialogue. - :param message: a message to be added - :return: True if message successfully added, false otherwise + :param message: the message + :return: True if message exists in this dialogue, False otherwise """ - if not message.is_incoming: - message.sender = self.agent_address + if self.is_empty: + return False + + if not self._has_message_id(message.message_id): + return False + + retrieved_message = self._get_message(message.message_id) + return message == retrieved_message - self.ensure_counterparty(message) + def _has_message_id(self, message_id: int) -> bool: + """ + Check whether a message with the supplied message id exists in this dialogue. - if not self.is_belonging_to_dialogue(message): + :param message_id: the message id + :return: True if message with that id exists in this dialogue, False otherwise + """ + if self.is_empty: return False - is_extendable = self.is_valid_next_message(message) - if is_extendable: - if message.is_incoming: - self._incoming_messages.extend([message]) - else: - self._outgoing_messages.extend([message]) - return is_extendable + return self.STARTING_MESSAGE_ID <= message_id <= self.last_message.message_id # type: ignore - def ensure_counterparty(self, message: Message) -> None: + def _update(self, message: Message) -> None: """ - Ensure the counterparty is set (set if not) correctly. + Extend the list of incoming/outgoing messages with 'message', if 'message' belongs to dialogue and is valid. - :param message: a message + :param message: a message to be added :return: None - """ - counterparty = None # type: Optional[str] - try: - counterparty = message.counterparty - except AssertionError: - # assume message belongs to dialogue - message.counterparty = self.dialogue_label.dialogue_opponent_addr + :raises: InvalidDialogueMessage: if message does not belong to this dialogue, or if message is invalid + """ + if not message.has_sender: + message.sender = self.self_address # pragma: nocover + + if not self._is_belonging_to_dialogue(message): + raise InvalidDialogueMessage( + "The message {} does not belong to this dialogue." + "The dialogue reference of the message is {}, while the dialogue reference of the dialogue is {}".format( + message.message_id, + message.dialogue_reference, + self.dialogue_label.dialogue_reference, + ) + ) - if counterparty is not None: - assert ( - message.counterparty == self.dialogue_label.dialogue_opponent_addr - ), "The counterparty specified in the message is different from the opponent in this dialogue." + is_valid_result, validation_message = self._validate_next_message(message) - def is_belonging_to_dialogue(self, message: Message) -> bool: + if not is_valid_result: + raise InvalidDialogueMessage( + "Message {} is invalid with respect to this dialogue. Error: {}".format( + message.message_id, validation_message, + ) + ) + + if self._is_message_by_self(message): + self._outgoing_messages.extend([message]) + else: + self._incoming_messages.extend([message]) + + def _is_belonging_to_dialogue(self, message: Message) -> bool: """ Check if the message is belonging to the dialogue. :param message: the message - :return: Ture if message is part of the dialogue, False otherwise + :return: True if message is part of the dialogue, False otherwise """ + opponent = self._counterparty_from_message(message) if self.is_self_initiated: self_initiated_dialogue_label = DialogueLabel( - (message.dialogue_reference[0], Dialogue.OPPONENT_STARTER_REFERENCE), - message.counterparty, - self.agent_address, + ( + message.dialogue_reference[0], + Dialogue.UNASSIGNED_DIALOGUE_REFERENCE, + ), + opponent, + self.self_address, ) result = self_initiated_dialogue_label in self.dialogue_labels else: other_initiated_dialogue_label = DialogueLabel( - message.dialogue_reference, message.counterparty, message.counterparty + message.dialogue_reference, opponent, opponent, ) result = other_initiated_dialogue_label in self.dialogue_labels return result - def reply(self, target_message: Message, performative, **kwargs) -> Message: + def reply( + self, + performative: Message.Performative, + target_message: Optional[Message] = None, + **kwargs, + ) -> Message: """ Reply to the 'target_message' in this dialogue with a message with 'performative', and contents from kwargs. + Note if no target_message is provided, the last message in the dialogue will be replied to. + :param target_message: the message to reply to. :param performative: the performative of the reply message. :param kwargs: the content of the reply message. :return: the reply message if it was successfully added as a reply, None otherwise. """ - assert ( - self._message_class is not None - ), "No 'message_class' argument was provided to this class on construction." - assert self.last_message is not None, "Cannot reply in an empty dialogue!" + last_message = self.last_message + if last_message is None: + raise ValueError("Cannot reply in an empty dialogue!") + + if target_message is None: + target_message = last_message + else: + enforce( + self._has_message( + target_message # type: ignore + ), + "The target message does not exist in this dialogue.", + ) reply = self._message_class( dialogue_reference=self.dialogue_label.dialogue_reference, - message_id=self.last_message.message_id + 1, + message_id=last_message.message_id + 1, target=target_message.message_id, performative=performative, **kwargs, ) - reply.counterparty = self.dialogue_label.dialogue_opponent_addr - result = self.update(reply) + reply.sender = self.self_address + reply.to = self.dialogue_label.dialogue_opponent_addr - if result: - return reply - else: - raise Exception("Invalid message from performative and contents.") + self._update(reply) + + return reply - def is_valid_next_message(self, message: Message) -> bool: + def _validate_next_message(self, message: Message) -> Tuple[bool, str]: """ Check whether 'message' is a valid next message in this dialogue. The evaluation of a message validity involves performing several categories of checks. Each category of checks resides in a separate method. - Currently, basic rules are fundamental structural constraints, - additional rules are applied for the time being, and more specific rules are captured in the is_valid method. + Currently, basic rules are general fundamental structural constraints, + additional rules are applied for the time being, and more specific rules to each dialogue are captured in the is_valid method. :param message: the message to be validated - :return: True if yes, False otherwise. + :return: Boolean result, and associated message. """ - return ( - self._basic_rules(message) - and self._additional_rules(message) - and self.is_valid(message) - ) + is_basic_validated, msg_basic_validation = self._basic_validation(message) + if not is_basic_validated: + return False, msg_basic_validation + + ( + result_additional_validation, + msg_additional_validation, + ) = self._additional_validation(message) + if not result_additional_validation: + return False, msg_additional_validation - def _basic_rules(self, message: Message) -> bool: + result_is_valid, msg_is_valid = self._custom_validation(message) + if not result_is_valid: + return False, msg_is_valid + + return True, "Message is valid with respect to this dialogue." + + def _basic_validation(self, message: Message) -> Tuple[bool, str]: """ Check whether 'message' is a valid next message in the dialogue, according to basic rules. + This method redirects the checks to two other methods based on whether the message + is the first in the dialogue or not. + + :param message: the message to be validated + :return: Boolean result, and associated message. + """ + if self.is_empty: # initial message + return self._basic_validation_initial_message(message) + + return self._basic_validation_non_initial_message(message) + + def _basic_validation_initial_message(self, message: Message) -> Tuple[bool, str]: + """ + Check whether an initial 'message' is a valid next message in the dialogue, according to basic rules. + These rules are designed to be fundamental to all dialogues, and enforce the following: - message ids are consistent @@ -560,37 +667,111 @@ def _basic_rules(self, message: Message) -> bool: - message targets are according to the reply structure of performatives :param message: the message to be validated - :return: True if valid, False otherwise. + :return: Boolean result, and associated message. """ dialogue_reference = message.dialogue_reference message_id = message.message_id target = message.target performative = message.performative - if self.last_message is None: - result = ( - dialogue_reference[0] == self.dialogue_label.dialogue_reference[0] - and message_id == Dialogue.STARTING_MESSAGE_ID - and target == Dialogue.STARTING_TARGET - and performative in self.rules.initial_performatives + if dialogue_reference[0] != self.dialogue_label.dialogue_reference[0]: + return ( + False, + "Invalid dialogue_reference[0]. Expected {}. Found {}.".format( + self.dialogue_label.dialogue_reference[0], dialogue_reference[0] + ), ) - else: - last_message_id = self.last_message.message_id - target_message = self.get_message(target) - if target_message is not None: - target_performative = target_message.performative - result = ( - dialogue_reference[0] == self.dialogue_label.dialogue_reference[0] - and message_id == last_message_id + 1 - and 1 <= target <= last_message_id - and performative - in self.rules.get_valid_replies(target_performative) - ) - else: - result = False - return result - def _additional_rules(self, message: Message) -> bool: + if message_id != Dialogue.STARTING_MESSAGE_ID: + return ( + False, + "Invalid message_id. Expected {}. Found {}.".format( + Dialogue.STARTING_MESSAGE_ID, message_id + ), + ) + + if target != Dialogue.STARTING_TARGET: + return ( + False, + "Invalid target. Expected {}. Found {}.".format( + Dialogue.STARTING_TARGET, target + ), + ) + + if performative not in self.rules.initial_performatives: + return ( + False, + "Invalid initial performative. Expected one of {}. Found {}.".format( + self.rules.initial_performatives, performative + ), + ) + + return True, "The initial message passes basic validation." + + def _basic_validation_non_initial_message( + self, message: Message + ) -> Tuple[bool, str]: + """ + Check whether a non-initial 'message' is a valid next message in the dialogue, according to basic rules. + + These rules are designed to be fundamental to all dialogues, and enforce the following: + + - message ids are consistent + - targets are consistent + - message targets are according to the reply structure of performatives + + :param message: the message to be validated + :return: Boolean result, and associated message. + """ + dialogue_reference = message.dialogue_reference + message_id = message.message_id + target = message.target + performative = message.performative + + if dialogue_reference[0] != self.dialogue_label.dialogue_reference[0]: + return ( + False, + "Invalid dialogue_reference[0]. Expected {}. Found {}.".format( + self.dialogue_label.dialogue_reference[0], dialogue_reference[0] + ), + ) + + last_message_id = self.last_message.message_id # type: ignore + if message_id != last_message_id + 1: + return ( + False, + "Invalid message_id. Expected {}. Found {}.".format( + last_message_id + 1, message_id + ), + ) + if target < 1: + return ( + False, + "Invalid target. Expected a value greater than or equal to 1. Found {}.".format( + target + ), + ) + if last_message_id < target: + return ( + False, + "Invalid target. Expected a value less than or equal to {}. Found {}.".format( + last_message_id, target + ), + ) + + target_message = self._get_message(target) + target_performative = target_message.performative + if performative not in self.rules.get_valid_replies(target_performative): + return ( + False, + "Invalid performative. Expected one of {}. Found {}.".format( + self.rules.get_valid_replies(target_performative), performative + ), + ) + + return True, "The non-initial message passes basic validation." + + def _additional_validation(self, message: Message) -> Tuple[bool, str]: """ Check whether 'message' is a valid next message in the dialogue, according to additional rules. @@ -600,31 +781,40 @@ def _additional_rules(self, message: Message) -> bool: - A message targets the message strictly before it in the dialogue :param message: the message to be validated - :return: True if valid, False otherwise. + :return: Boolean result, and associated message. """ - if self.last_message is None: - result = True - else: - target = message.target - last_target = self.last_message.target - result = target == last_target + 1 - return result + if self.is_empty: + return True, "The message passes additional validation." - def update_dialogue_label(self, final_dialogue_label: DialogueLabel) -> None: + last_target = self.last_message.target # type: ignore + if message.target == last_target + 1: + return True, "The message passes additional validation." + + return ( + False, + "Invalid target. Expected {}. Found {}.".format( + last_target + 1, message.target + ), + ) + + def _update_dialogue_label(self, final_dialogue_label: DialogueLabel) -> None: """ Update the dialogue label of the dialogue. :param final_dialogue_label: the final dialogue label """ - assert ( - self.dialogue_label.dialogue_reference[1] == self.OPPONENT_STARTER_REFERENCE + enforce( + self.dialogue_label.dialogue_reference[1] + == self.UNASSIGNED_DIALOGUE_REFERENCE and final_dialogue_label.dialogue_reference[1] - != self.OPPONENT_STARTER_REFERENCE - ), "Dialogue label cannot be updated." + != self.UNASSIGNED_DIALOGUE_REFERENCE, + "Dialogue label cannot be updated.", + ) self._dialogue_label = final_dialogue_label - @abstractmethod - def is_valid(self, message: Message) -> bool: + def _custom_validation( # pylint: disable=no-self-use,unused-argument + self, message: Message + ) -> Tuple[bool, str]: """ Check whether 'message' is a valid next message in the dialogue. @@ -633,6 +823,7 @@ def is_valid(self, message: Message) -> bool: :param message: the message to be validated :return: True if valid, False otherwise. """ + return True, "The message passes custom validation." @staticmethod def _interleave(list_1, list_2) -> List: @@ -706,10 +897,10 @@ def add_dialogue_endstate( :return: None """ if is_self_initiated: - assert end_state in self._self_initiated, "End state not present!" + enforce(end_state in self._self_initiated, "End state not present!") self._self_initiated[end_state] += 1 else: - assert end_state in self._other_initiated, "End state not present!" + enforce(end_state in self._other_initiated, "End state not present!") self._other_initiated[end_state] += 1 @@ -718,40 +909,72 @@ class Dialogues(ABC): def __init__( self, - agent_address: Address, + self_address: Address, end_states: FrozenSet[Dialogue.EndState], - message_class: Optional[Type[Message]] = None, - dialogue_class: Optional[Type[Dialogue]] = None, - role_from_first_message: Optional[Callable[[Message], Dialogue.Role]] = None, + message_class: Type[Message], + dialogue_class: Type[Dialogue], + role_from_first_message: Callable[[Message, Address], Dialogue.Role], ) -> None: """ Initialize dialogues. - :param agent_address: the address of the agent for whom dialogues are maintained + :param self_address: the address of the entity for whom dialogues are maintained :param end_states: the list of dialogue endstates :return: None """ self._dialogues_by_dialogue_label = {} # type: Dict[DialogueLabel, Dialogue] + self._dialogue_by_address = {} # type: Dict[Address, List[Dialogue]] self._incomplete_to_complete_dialogue_labels = ( {} ) # type: Dict[DialogueLabel, DialogueLabel] - self._agent_address = agent_address + self._self_address = self_address self._dialogue_stats = DialogueStats(end_states) - if message_class is not None: - assert issubclass(message_class, Message) + enforce( + issubclass(message_class, Message), + "message_class is not a subclass of Message.", + ) self._message_class = message_class - if dialogue_class is not None: - assert issubclass(dialogue_class, Dialogue) + enforce( + issubclass(dialogue_class, Dialogue), + "dialogue_class is not a subclass of Dialogue.", + ) self._dialogue_class = dialogue_class - if role_from_first_message is not None: - self._role_from_first_message = role_from_first_message - else: - self._role_from_first_message = ( - self.role_from_first_message - ) # pragma: no cover + # Note the following might be too restrictive; if the supplied role_from_first_message function + # does not have the type hinting for its parameter or its return value, the second and third checks + # below would fail. + sig = signature(role_from_first_message) + parameter_length = len(sig.parameters.keys()) + enforce( + parameter_length == 2, + "Invalid number of parameters for role_from_first_message. Expected 2. Found {}.".format( + parameter_length + ), + ) + parameter_1_type = list(sig.parameters.values())[0].annotation + enforce( + parameter_1_type == Message, + "Invalid type for the first parameter of role_from_first_message. Expected 'Message'. Found {}.".format( + parameter_1_type + ), + ) + parameter_2_type = list(sig.parameters.values())[1].annotation + enforce( + parameter_2_type == Address, + "Invalid type for the second parameter of role_from_first_message. Expected 'Address'. Found {}.".format( + parameter_2_type + ), + ) + return_type = sig.return_annotation + enforce( + return_type == Dialogue.Role, + "Invalid return type for role_from_first_message. Expected 'Dialogue.Role'. Found {}.".format( + return_type + ), + ) + self._role_from_first_message = role_from_first_message @property def dialogues(self) -> Dict[DialogueLabel, Dialogue]: @@ -759,10 +982,10 @@ def dialogues(self) -> Dict[DialogueLabel, Dialogue]: return self._dialogues_by_dialogue_label @property - def agent_address(self) -> Address: + def self_address(self) -> Address: """Get the address of the agent for whom dialogues are maintained.""" - assert self._agent_address != "", "agent_address is not set." - return self._agent_address + enforce(self._self_address != "", "self_address is not set.") + return self._self_address @property def dialogue_stats(self) -> DialogueStats: @@ -773,13 +996,52 @@ def dialogue_stats(self) -> DialogueStats: """ return self._dialogue_stats + def get_dialogues_with_counterparty(self, counterparty: Address) -> List[Dialogue]: + """ + Get the dialogues by address. + + :param counterparty: the counterparty + :return: The dialogues with the counterparty. + """ + return self._dialogue_by_address.get(counterparty, []) + + def _is_message_by_self(self, message: Message) -> bool: + """ + Check whether the message is by this agent or not. + + :param message: the message + :return: True if message is by this agent, False otherwise + """ + return message.sender == self.self_address + + def _is_message_by_other(self, message: Message) -> bool: + """ + Check whether the message is by the counterparty agent in this dialogue or not. + + :param message: the message + :return: True if message is by the counterparty agent in this dialogue, False otherwise + """ + return not self._is_message_by_self(message) + + def _counterparty_from_message(self, message: Message) -> Address: + """ + Determine the counterparty of the agent in the dialogue from a message. + + :param message: the message + :return: The address of the counterparty + """ + counterparty = ( + message.to if self._is_message_by_self(message) else message.sender + ) + return counterparty + def new_self_initiated_dialogue_reference(self) -> Tuple[str, str]: """ Return a dialogue label for a new self initiated dialogue. :return: the next nonce """ - return self._generate_dialogue_nonce(), Dialogue.OPPONENT_STARTER_REFERENCE + return self._generate_dialogue_nonce(), Dialogue.UNASSIGNED_DIALOGUE_REFERENCE def create( self, counterparty: Address, performative: Message.Performative, **kwargs, @@ -793,10 +1055,6 @@ def create( :return: the initial message and the dialogue. """ - assert ( - self._message_class is not None - ), "No 'message_class' argument was provided to this class on construction." - initial_message = self._message_class( dialogue_reference=self.new_self_initiated_dialogue_reference(), message_id=Dialogue.STARTING_MESSAGE_ID, @@ -804,23 +1062,64 @@ def create( performative=performative, **kwargs, ) - initial_message.counterparty = counterparty + initial_message.sender = self.self_address + initial_message.to = counterparty + + dialogue = self._create_dialogue(counterparty, initial_message) + + return initial_message, dialogue + + def create_with_message( + self, counterparty: Address, initial_message: Message + ) -> Dialogue: + """ + Create a dialogue with 'counterparty', with an initial message provided. + + :param counterparty: the counterparty of the dialogue. + :param initial_message: the initial_message. + + :return: the initial message and the dialogue. + """ + enforce( + not initial_message.has_sender, + "The message's 'sender' field is already set {}".format(initial_message), + ) + enforce( + not initial_message.has_to, + "The message's 'to' field is already set {}".format(initial_message), + ) + initial_message.sender = self.self_address + initial_message.to = counterparty + dialogue = self._create_dialogue(counterparty, initial_message) + + return dialogue + + def _create_dialogue( + self, counterparty: Address, initial_message: Message + ) -> Dialogue: + """ + Create a dialogue from an initial message provided. + + :param counterparty: the counterparty of the dialogue. + :param initial_message: the initial_message. + + :return: the dialogue. + """ dialogue = self._create_self_initiated( dialogue_opponent_addr=counterparty, dialogue_reference=initial_message.dialogue_reference, - role=self._role_from_first_message(initial_message), + role=self._role_from_first_message(initial_message, self.self_address), ) - successfully_updated = dialogue.update(initial_message) - - if not successfully_updated: + try: + dialogue._update(initial_message) # pylint: disable=protected-access + except InvalidDialogueMessage as e: self._dialogues_by_dialogue_label.pop(dialogue.dialogue_label) - raise Exception( - "Cannot create the a dialogue with the specified performative and contents." - ) - - return initial_message, dialogue + raise SyntaxError( + "Cannot create a dialogue with the specified performative and contents." + ) from e + return dialogue def update(self, message: Message) -> Optional[Dialogue]: """ @@ -830,106 +1129,107 @@ def update(self, message: Message) -> Optional[Dialogue]: If the message is addressed to an existing dialogue, the dialogue is retrieved, extended with this message and returned. If there are any errors, e.g. the message dialogue reference does not exists or the message is invalid w.r.t. the dialogue, return None. - :param message: a new message + :param message: a new incoming message :return: the new or existing dialogue the message is intended for, or None in case of any errors. """ - dialogue_reference = message.dialogue_reference + enforce( + message.has_sender and self._is_message_by_other(message), + "Invalid 'update' usage. Update must only be used with a message by another agent.", + ) + enforce( + message.has_to, "The message's 'to' field is not set {}".format(message) + ) - if not message.has_counterparty: - raise ValueError( - "The message counterparty field is not set {}".format(message) - ) - if message.is_incoming and not message.has_sender: - raise ValueError("The message sender field is not set {}".format(message)) + dialogue_reference = message.dialogue_reference is_invalid_label = ( - dialogue_reference[0] == Dialogue.OPPONENT_STARTER_REFERENCE - and dialogue_reference[1] == Dialogue.OPPONENT_STARTER_REFERENCE + dialogue_reference[0] == Dialogue.UNASSIGNED_DIALOGUE_REFERENCE + and dialogue_reference[1] == Dialogue.UNASSIGNED_DIALOGUE_REFERENCE ) is_new_dialogue = ( - dialogue_reference[0] != Dialogue.OPPONENT_STARTER_REFERENCE - and dialogue_reference[1] == Dialogue.OPPONENT_STARTER_REFERENCE + dialogue_reference[0] != Dialogue.UNASSIGNED_DIALOGUE_REFERENCE + and dialogue_reference[1] == Dialogue.UNASSIGNED_DIALOGUE_REFERENCE and message.message_id == 1 - and message.target == 0 ) is_incomplete_label_and_non_initial_msg = ( - dialogue_reference[0] != Dialogue.OPPONENT_STARTER_REFERENCE - and dialogue_reference[1] == Dialogue.OPPONENT_STARTER_REFERENCE + dialogue_reference[0] != Dialogue.UNASSIGNED_DIALOGUE_REFERENCE + and dialogue_reference[1] == Dialogue.UNASSIGNED_DIALOGUE_REFERENCE and message.message_id > 1 ) + if is_invalid_label: dialogue = None # type: Optional[Dialogue] - elif is_new_dialogue and message.is_incoming: # new dialogue by other + elif is_new_dialogue: # initial message for new dialogue dialogue = self._create_opponent_initiated( - dialogue_opponent_addr=message.counterparty, - dialogue_reference=dialogue_reference, - role=self._role_from_first_message(message), - ) - elif is_new_dialogue and not message.is_incoming: # new dialogue by self - dialogue = self._create_self_initiated( - dialogue_opponent_addr=message.counterparty, + dialogue_opponent_addr=message.sender, dialogue_reference=dialogue_reference, - role=self._role_from_first_message(message), + role=self._role_from_first_message(message, self.self_address), ) - elif ( # non-initial message with incomplete label - is_incomplete_label_and_non_initial_msg - ): + elif is_incomplete_label_and_non_initial_msg: # we can allow a dialogue to have incomplete reference # as multiple messages can be sent before one is received with complete reference dialogue = self.get_dialogue(message) - else: - self._update_self_initiated_dialogue_label_on_message_with_complete_reference( - message - ) + else: # non-initial message for existing dialogue + self._complete_dialogue_reference(message) dialogue = self.get_dialogue(message) - if dialogue is not None and dialogue.update(message): - result = dialogue # type: Optional[Dialogue] - else: # couldn't find the dialogue or invalid message + if dialogue is not None: + try: + dialogue._update(message) # pylint: disable=protected-access + result = dialogue # type: Optional[Dialogue] + except InvalidDialogueMessage: + # invalid message for the dialogue found + result = None + if ( + is_new_dialogue + ): # remove the newly created dialogue if the initial message is invalid + self._dialogues_by_dialogue_label.pop(dialogue.dialogue_label) + else: + # couldn't find the dialogue referenced by the message result = None return result - def _update_self_initiated_dialogue_label_on_message_with_complete_reference( - self, message: Message - ) -> None: + def _complete_dialogue_reference(self, message: Message) -> None: """ Update a self initiated dialogue label with a complete dialogue reference from counterparty's first message. :param message: A message in the dialogue (the first by the counterparty with a complete reference) :return: None """ - dialogue_reference = message.dialogue_reference - assert ( - dialogue_reference[0] != Dialogue.OPPONENT_STARTER_REFERENCE - and dialogue_reference[1] != Dialogue.OPPONENT_STARTER_REFERENCE - ), "Only complete dialogue references allowed." - self_initiated_dialogue_reference = ( - dialogue_reference[0], - Dialogue.OPPONENT_STARTER_REFERENCE, + complete_dialogue_reference = message.dialogue_reference + enforce( + complete_dialogue_reference[0] != Dialogue.UNASSIGNED_DIALOGUE_REFERENCE + and complete_dialogue_reference[1] + != Dialogue.UNASSIGNED_DIALOGUE_REFERENCE, + "Only complete dialogue references allowed.", ) - self_initiated_dialogue_label = DialogueLabel( - self_initiated_dialogue_reference, message.counterparty, self.agent_address, + + incomplete_dialogue_reference = ( + complete_dialogue_reference[0], + Dialogue.UNASSIGNED_DIALOGUE_REFERENCE, + ) + incomplete_dialogue_label = DialogueLabel( + incomplete_dialogue_reference, message.sender, self.self_address, ) - if self_initiated_dialogue_label in self.dialogues: - self_initiated_dialogue = self.dialogues.pop(self_initiated_dialogue_label) + if ( + incomplete_dialogue_label in self.dialogues + and incomplete_dialogue_label + not in self._incomplete_to_complete_dialogue_labels + ): + dialogue = self.dialogues.pop(incomplete_dialogue_label) final_dialogue_label = DialogueLabel( - dialogue_reference, - self_initiated_dialogue_label.dialogue_opponent_addr, - self_initiated_dialogue_label.dialogue_starter_addr, + complete_dialogue_reference, + incomplete_dialogue_label.dialogue_opponent_addr, + incomplete_dialogue_label.dialogue_starter_addr, ) - self_initiated_dialogue.update_dialogue_label(final_dialogue_label) - assert ( - self_initiated_dialogue.dialogue_label not in self.dialogues - and self_initiated_dialogue_label - not in self._incomplete_to_complete_dialogue_labels - ), "DialogueLabel already present in dialogues." - self.dialogues.update( - {self_initiated_dialogue.dialogue_label: self_initiated_dialogue} + dialogue._update_dialogue_label( # pylint: disable=protected-access + final_dialogue_label ) + self.dialogues.update({dialogue.dialogue_label: dialogue}) self._incomplete_to_complete_dialogue_labels[ - self_initiated_dialogue_label + incomplete_dialogue_label ] = final_dialogue_label def get_dialogue(self, message: Message) -> Optional[Dialogue]: @@ -939,34 +1239,35 @@ def get_dialogue(self, message: Message) -> Optional[Dialogue]: :param message: a message :return: the dialogue, or None in case such a dialogue does not exist """ - dialogue_reference = message.dialogue_reference - counterparty = message.counterparty - self_initiated_dialogue_label = DialogueLabel( - dialogue_reference, counterparty, self.agent_address + message.dialogue_reference, + self._counterparty_from_message(message), + self.self_address, ) other_initiated_dialogue_label = DialogueLabel( - dialogue_reference, counterparty, counterparty + message.dialogue_reference, + self._counterparty_from_message(message), + self._counterparty_from_message(message), ) - self_initiated_dialogue_label = self.get_latest_label( + self_initiated_dialogue_label = self._get_latest_label( self_initiated_dialogue_label ) - other_initiated_dialogue_label = self.get_latest_label( + other_initiated_dialogue_label = self._get_latest_label( other_initiated_dialogue_label ) - self_initiated_dialogue = self.get_dialogue_from_label( + self_initiated_dialogue = self._get_dialogue_from_label( self_initiated_dialogue_label ) - other_initiated_dialogue = self.get_dialogue_from_label( + other_initiated_dialogue = self._get_dialogue_from_label( other_initiated_dialogue_label ) result = self_initiated_dialogue or other_initiated_dialogue return result - def get_latest_label(self, dialogue_label: DialogueLabel) -> DialogueLabel: + def _get_latest_label(self, dialogue_label: DialogueLabel) -> DialogueLabel: """ Retrieve the latest dialogue label if present otherwise return same label. @@ -978,7 +1279,7 @@ def get_latest_label(self, dialogue_label: DialogueLabel) -> DialogueLabel: ) return result - def get_dialogue_from_label( + def _get_dialogue_from_label( self, dialogue_label: DialogueLabel ) -> Optional[Dialogue]: """ @@ -1004,12 +1305,13 @@ def _create_self_initiated( :return: the created dialogue. """ - assert ( - dialogue_reference[0] != Dialogue.OPPONENT_STARTER_REFERENCE - and dialogue_reference[1] == Dialogue.OPPONENT_STARTER_REFERENCE - ), "Cannot initiate dialogue with preassigned dialogue_responder_reference!" + enforce( + dialogue_reference[0] != Dialogue.UNASSIGNED_DIALOGUE_REFERENCE + and dialogue_reference[1] == Dialogue.UNASSIGNED_DIALOGUE_REFERENCE, + "Cannot initiate dialogue with preassigned dialogue_responder_reference!", + ) incomplete_dialogue_label = DialogueLabel( - dialogue_reference, dialogue_opponent_addr, self.agent_address + dialogue_reference, dialogue_opponent_addr, self.self_address ) dialogue = self._create(incomplete_dialogue_label, role) return dialogue @@ -1029,10 +1331,11 @@ def _create_opponent_initiated( :return: the created dialogue """ - assert ( - dialogue_reference[0] != Dialogue.OPPONENT_STARTER_REFERENCE - and dialogue_reference[1] == Dialogue.OPPONENT_STARTER_REFERENCE - ), "Cannot initiate dialogue with preassigned dialogue_responder_reference!" + enforce( + dialogue_reference[0] != Dialogue.UNASSIGNED_DIALOGUE_REFERENCE + and dialogue_reference[1] == Dialogue.UNASSIGNED_DIALOGUE_REFERENCE, + "Cannot initiate dialogue with preassigned dialogue_responder_reference!", + ) incomplete_dialogue_label = DialogueLabel( dialogue_reference, dialogue_opponent_addr, dialogue_opponent_addr ) @@ -1063,10 +1366,11 @@ def _create( :return: the created dialogue """ - assert ( + enforce( incomplete_dialogue_label - not in self._incomplete_to_complete_dialogue_labels - ), "Incomplete dialogue label already present." + not in self._incomplete_to_complete_dialogue_labels, + "Incomplete dialogue label already present.", + ) if complete_dialogue_label is None: dialogue_label = incomplete_dialogue_label else: @@ -1074,49 +1378,27 @@ def _create( incomplete_dialogue_label ] = complete_dialogue_label dialogue_label = complete_dialogue_label - assert ( - dialogue_label not in self.dialogues - ), "Dialogue label already present in dialogues." - if self._message_class is not None and self._dialogue_class is not None: - dialogue = self._dialogue_class( - dialogue_label=dialogue_label, - message_class=self._message_class, - agent_address=self.agent_address, - role=role, - ) - else: - # TODO: remove this approach - dialogue = self.create_dialogue( - dialogue_label=dialogue_label, role=role, - ) # pragma: no cover + enforce( + dialogue_label not in self.dialogues, + "Dialogue label already present in dialogues.", + ) + dialogue = self._dialogue_class( + dialogue_label=dialogue_label, + message_class=self._message_class, + self_address=self.self_address, + role=role, + ) self.dialogues.update({dialogue_label: dialogue}) + if ( + self._dialogue_by_address.get(dialogue_label.dialogue_opponent_addr, None) + is None + ): + self._dialogue_by_address[dialogue_label.dialogue_opponent_addr] = [] + self._dialogue_by_address[dialogue_label.dialogue_opponent_addr].append( + dialogue + ) return dialogue - @abstractmethod - def create_dialogue( - self, dialogue_label: DialogueLabel, role: Dialogue.Role, - ) -> Dialogue: - """ - THIS METHOD IS DEPRECATED AND WILL BE REMOVED IN THE NEXT VERSION. USE THE NEW CONSTRUCTOR ARGUMENTS INSTEAD. - - Create a dialogue instance. - - :param dialogue_label: the identifier of the dialogue - :param role: the role of the agent this dialogue is maintained for - - :return: the created dialogue - """ - - @staticmethod - def role_from_first_message(message: Message) -> Dialogue.Role: - """ - Infer the role of the agent from an incoming or outgoing first message. - - :param message: an incoming/outgoing first message - :return: the agent's role - """ - pass # pragma: no cover - @staticmethod def _generate_dialogue_nonce() -> str: """ diff --git a/aea/protocols/generator/base.py b/aea/protocols/generator/base.py index 3e87db8eb8..9468a86200 100644 --- a/aea/protocols/generator/base.py +++ b/aea/protocols/generator/base.py @@ -235,7 +235,7 @@ def _performatives_enum_str(self) -> str: :return: the performatives Enum string """ - enum_str = self.indent + "class Performative(Enum):\n" + enum_str = self.indent + "class Performative(Message.Performative):\n" self._change_indent(1) enum_str += self.indent + '"""Performatives for the {} protocol."""\n\n'.format( self.protocol_specification.name @@ -307,13 +307,13 @@ def _check_content_type_str(self, content_name: str, content_type: str) -> str: unique_standard_types_set.add(typing_content_type) unique_standard_types_list = sorted(unique_standard_types_set) check_str += self.indent - check_str += "assert " + check_str += "enforce(" for unique_type in unique_standard_types_list: check_str += "type({}) == {} or ".format( content_variable, self._to_custom_custom(unique_type) ) check_str = check_str[:-4] - check_str += ", \"Invalid type for content '{}'. Expected either of '{}'. Found '{{}}'.\".format(type({}))\n".format( + check_str += ", \"Invalid type for content '{}'. Expected either of '{}'. Found '{{}}'.\".format(type({})))\n".format( content_name, [ unique_standard_type @@ -326,7 +326,7 @@ def _check_content_type_str(self, content_name: str, content_type: str) -> str: content_variable ) self._change_indent(1) - check_str += self.indent + "assert (\n" + check_str += self.indent + "enforce(\n" self._change_indent(1) frozen_set_element_types_set = set() for element_type in element_types: @@ -349,7 +349,7 @@ def _check_content_type_str(self, content_name: str, content_type: str) -> str: if len(frozen_set_element_types) == 1: check_str += ( self.indent - + "), \"Invalid type for elements of content '{}'. Expected ".format( + + ", \"Invalid type for elements of content '{}'. Expected ".format( content_name ) ) @@ -357,11 +357,11 @@ def _check_content_type_str(self, content_name: str, content_type: str) -> str: check_str += "'{}'".format( self._to_custom_custom(frozen_set_element_type) ) - check_str += '."\n' + check_str += '.")\n' else: check_str += ( self.indent - + "), \"Invalid type for frozenset elements in content '{}'. Expected either ".format( + + ", \"Invalid type for frozenset elements in content '{}'. Expected either ".format( content_name ) ) @@ -370,14 +370,14 @@ def _check_content_type_str(self, content_name: str, content_type: str) -> str: self._to_custom_custom(frozen_set_element_type) ) check_str = check_str[:-4] - check_str += '."\n' + check_str += '.")\n' self._change_indent(-1) if "tuple" in unique_standard_types_list: check_str += self.indent + "if type({}) == tuple:\n".format( content_variable ) self._change_indent(1) - check_str += self.indent + "assert (\n" + check_str += self.indent + "enforce(\n" self._change_indent(1) tuple_element_types_set = set() for element_type in element_types: @@ -399,7 +399,7 @@ def _check_content_type_str(self, content_name: str, content_type: str) -> str: if len(tuple_element_types) == 1: check_str += ( self.indent - + "), \"Invalid type for tuple elements in content '{}'. Expected ".format( + + ", \"Invalid type for tuple elements in content '{}'. Expected ".format( content_name ) ) @@ -407,11 +407,11 @@ def _check_content_type_str(self, content_name: str, content_type: str) -> str: check_str += "'{}'".format( self._to_custom_custom(tuple_element_type) ) - check_str += '."\n' + check_str += '.")\n' else: check_str += ( self.indent - + "), \"Invalid type for tuple elements in content '{}'. Expected either ".format( + + ", \"Invalid type for tuple elements in content '{}'. Expected either ".format( content_name ) ) @@ -420,7 +420,7 @@ def _check_content_type_str(self, content_name: str, content_type: str) -> str: self._to_custom_custom(tuple_element_type) ) check_str = check_str[:-4] - check_str += '."\n' + check_str += '.")\n' self._change_indent(-1) if "dict" in unique_standard_types_list: check_str += self.indent + "if type({}) == dict:\n".format( @@ -434,7 +434,7 @@ def _check_content_type_str(self, content_name: str, content_type: str) -> str: ) ) self._change_indent(1) - check_str += self.indent + "assert (\n" + check_str += self.indent + "enforce(\n" self._change_indent(1) dict_key_value_types = dict() for element_type in element_types: @@ -459,17 +459,17 @@ def _check_content_type_str(self, content_name: str, content_type: str) -> str: if len(dict_key_value_types) == 1: check_str += ( self.indent - + "), \"Invalid type for dictionary key, value in content '{}'. Expected ".format( + + ", \"Invalid type for dictionary key, value in content '{}'. Expected ".format( content_name ) ) for key in sorted(dict_key_value_types.keys()): check_str += "'{}', '{}'".format(key, dict_key_value_types[key]) - check_str += '."\n' + check_str += '.")\n' else: check_str += ( self.indent - + "), \"Invalid type for dictionary key, value in content '{}'. Expected ".format( + + ", \"Invalid type for dictionary key, value in content '{}'. Expected ".format( content_name ) ) @@ -478,18 +478,18 @@ def _check_content_type_str(self, content_name: str, content_type: str) -> str: key, dict_key_value_types[key] ) check_str = check_str[:-4] - check_str += '."\n' + check_str += '.")\n' self._change_indent(-2) elif content_type.startswith("FrozenSet["): # check the type check_str += ( self.indent - + "assert type({}) == frozenset, \"Invalid type for content '{}'. Expected 'frozenset'. Found '{{}}'.\".format(type({}))\n".format( + + "enforce(type({}) == frozenset, \"Invalid type for content '{}'. Expected 'frozenset'. Found '{{}}'.\".format(type({})))\n".format( content_variable, content_name, content_variable ) ) element_type = _get_sub_types_of_compositional_types(content_type)[0] - check_str += self.indent + "assert all(\n" + check_str += self.indent + "enforce(all(\n" self._change_indent(1) check_str += self.indent + "type(element) == {} for element in {}\n".format( self._to_custom_custom(element_type), content_variable @@ -497,7 +497,7 @@ def _check_content_type_str(self, content_name: str, content_type: str) -> str: self._change_indent(-1) check_str += ( self.indent - + "), \"Invalid type for frozenset elements in content '{}'. Expected '{}'.\"\n".format( + + "), \"Invalid type for frozenset elements in content '{}'. Expected '{}'.\")\n".format( content_name, element_type ) ) @@ -505,12 +505,12 @@ def _check_content_type_str(self, content_name: str, content_type: str) -> str: # check the type check_str += ( self.indent - + "assert type({}) == tuple, \"Invalid type for content '{}'. Expected 'tuple'. Found '{{}}'.\".format(type({}))\n".format( + + "enforce(type({}) == tuple, \"Invalid type for content '{}'. Expected 'tuple'. Found '{{}}'.\".format(type({})))\n".format( content_variable, content_name, content_variable ) ) element_type = _get_sub_types_of_compositional_types(content_type)[0] - check_str += self.indent + "assert all(\n" + check_str += self.indent + "enforce(all(\n" self._change_indent(1) check_str += self.indent + "type(element) == {} for element in {}\n".format( self._to_custom_custom(element_type), content_variable @@ -518,7 +518,7 @@ def _check_content_type_str(self, content_name: str, content_type: str) -> str: self._change_indent(-1) check_str += ( self.indent - + "), \"Invalid type for tuple elements in content '{}'. Expected '{}'.\"\n".format( + + "), \"Invalid type for tuple elements in content '{}'. Expected '{}'.\")\n".format( content_name, element_type ) ) @@ -526,7 +526,7 @@ def _check_content_type_str(self, content_name: str, content_type: str) -> str: # check the type check_str += ( self.indent - + "assert type({}) == dict, \"Invalid type for content '{}'. Expected 'dict'. Found '{{}}'.\".format(type({}))\n".format( + + "enforce(type({}) == dict, \"Invalid type for content '{}'. Expected 'dict'. Found '{{}}'.\".format(type({})))\n".format( content_variable, content_name, content_variable ) ) @@ -540,7 +540,7 @@ def _check_content_type_str(self, content_name: str, content_type: str) -> str: ) ) self._change_indent(1) - check_str += self.indent + "assert (\n" + check_str += self.indent + "enforce(\n" self._change_indent(1) check_str += self.indent + "type(key_of_{}) == {}\n".format( content_name, self._to_custom_custom(element_type_1) @@ -548,12 +548,12 @@ def _check_content_type_str(self, content_name: str, content_type: str) -> str: self._change_indent(-1) check_str += ( self.indent - + "), \"Invalid type for dictionary keys in content '{}'. Expected '{}'. Found '{{}}'.\".format(type(key_of_{}))\n".format( + + ", \"Invalid type for dictionary keys in content '{}'. Expected '{}'. Found '{{}}'.\".format(type(key_of_{})))\n".format( content_name, element_type_1, content_name ) ) - check_str += self.indent + "assert (\n" + check_str += self.indent + "enforce(\n" self._change_indent(1) check_str += self.indent + "type(value_of_{}) == {}\n".format( content_name, self._to_custom_custom(element_type_2) @@ -561,7 +561,7 @@ def _check_content_type_str(self, content_name: str, content_type: str) -> str: self._change_indent(-1) check_str += ( self.indent - + "), \"Invalid type for dictionary values in content '{}'. Expected '{}'. Found '{{}}'.\".format(type(value_of_{}))\n".format( + + ", \"Invalid type for dictionary values in content '{}'. Expected '{}'. Found '{{}}'.\".format(type(value_of_{})))\n".format( content_name, element_type_2, content_name ) ) @@ -569,7 +569,7 @@ def _check_content_type_str(self, content_name: str, content_type: str) -> str: else: check_str += ( self.indent - + "assert type({}) == {}, \"Invalid type for content '{}'. Expected '{}'. Found '{{}}'.\".format(type({}))\n".format( + + "enforce(type({}) == {}, \"Invalid type for content '{}'. Expected '{}'. Found '{{}}'.\".format(type({})))\n".format( content_variable, self._to_custom_custom(content_type), content_name, @@ -602,9 +602,9 @@ def _message_class_str(self) -> str: # Imports cls_str += self.indent + "import logging\n" - cls_str += self.indent + "from enum import Enum\n" cls_str += self._import_from_typing_module() + "\n\n" cls_str += self.indent + "from aea.configurations.base import ProtocolId\n" + cls_str += self.indent + "from aea.exceptions import AEAEnforceError, enforce\n" cls_str += MESSAGE_IMPORT + "\n" if self._import_from_custom_types_module() != "": cls_str += "\n" + self._import_from_custom_types_module() + "\n" @@ -664,6 +664,9 @@ def _message_class_str(self) -> str: cls_str += self.indent + ":param target: the message target.\n" cls_str += self.indent + ":param performative: the message performative.\n" cls_str += self.indent + '"""\n' + cls_str += self.indent + "self._performatives = {}\n".format( + self._performatives_str() + ) cls_str += self.indent + "super().__init__(\n" self._change_indent(1) cls_str += self.indent + "dialogue_reference=dialogue_reference,\n" @@ -678,9 +681,7 @@ def _message_class_str(self) -> str: cls_str += self.indent + "**kwargs,\n" self._change_indent(-1) cls_str += self.indent + ")\n" - cls_str += self.indent + "self._performatives = {}\n".format( - self._performatives_str() - ) + self._change_indent(-1) # Instance properties @@ -696,7 +697,7 @@ def _message_class_str(self) -> str: cls_str += self.indent + '"""Get the dialogue_reference of the message."""\n' cls_str += ( self.indent - + 'assert self.is_set("dialogue_reference"), "dialogue_reference is not set."\n' + + 'enforce(self.is_set("dialogue_reference"), "dialogue_reference is not set.")\n' ) cls_str += ( self.indent @@ -708,7 +709,8 @@ def _message_class_str(self) -> str: self._change_indent(1) cls_str += self.indent + '"""Get the message_id of the message."""\n' cls_str += ( - self.indent + 'assert self.is_set("message_id"), "message_id is not set."\n' + self.indent + + 'enforce(self.is_set("message_id"), "message_id is not set.")\n' ) cls_str += self.indent + 'return cast(int, self.get("message_id"))\n\n' self._change_indent(-1) @@ -721,7 +723,7 @@ def _message_class_str(self) -> str: cls_str += self.indent + '"""Get the performative of the message."""\n' cls_str += ( self.indent - + 'assert self.is_set("performative"), "performative is not set."\n' + + 'enforce(self.is_set("performative"), "performative is not set.")\n' ) cls_str += ( self.indent @@ -734,7 +736,9 @@ def _message_class_str(self) -> str: cls_str += self.indent + "def target(self) -> int:\n" self._change_indent(1) cls_str += self.indent + '"""Get the target of the message."""\n' - cls_str += self.indent + 'assert self.is_set("target"), "target is not set."\n' + cls_str += ( + self.indent + 'enforce(self.is_set("target"), "target is not set.")\n' + ) cls_str += self.indent + 'return cast(int, self.get("target"))\n\n' self._change_indent(-1) @@ -754,7 +758,7 @@ def _message_class_str(self) -> str: if not content_type.startswith("Optional"): cls_str += ( self.indent - + 'assert self.is_set("{}"), "\'{}\' content is not set."\n'.format( + + 'enforce(self.is_set("{}"), "\'{}\' content is not set.")\n'.format( content_name, content_name ) ) @@ -776,35 +780,35 @@ def _message_class_str(self) -> str: self._change_indent(1) cls_str += ( self.indent - + "assert type(self.dialogue_reference) == tuple, \"Invalid type for 'dialogue_reference'. Expected 'tuple'. Found '{}'.\"" - ".format(type(self.dialogue_reference))\n" + + "enforce(type(self.dialogue_reference) == tuple, \"Invalid type for 'dialogue_reference'. Expected 'tuple'. Found '{}'.\"" + ".format(type(self.dialogue_reference)))\n" ) cls_str += ( self.indent - + "assert type(self.dialogue_reference[0]) == str, \"Invalid type for 'dialogue_reference[0]'. Expected 'str'. Found '{}'.\"" - ".format(type(self.dialogue_reference[0]))\n" + + "enforce(type(self.dialogue_reference[0]) == str, \"Invalid type for 'dialogue_reference[0]'. Expected 'str'. Found '{}'.\"" + ".format(type(self.dialogue_reference[0])))\n" ) cls_str += ( self.indent - + "assert type(self.dialogue_reference[1]) == str, \"Invalid type for 'dialogue_reference[1]'. Expected 'str'. Found '{}'.\"" - ".format(type(self.dialogue_reference[1]))\n" + + "enforce(type(self.dialogue_reference[1]) == str, \"Invalid type for 'dialogue_reference[1]'. Expected 'str'. Found '{}'.\"" + ".format(type(self.dialogue_reference[1])))\n" ) cls_str += ( self.indent - + "assert type(self.message_id) == int, \"Invalid type for 'message_id'. Expected 'int'. Found '{}'.\"" - ".format(type(self.message_id))\n" + + "enforce(type(self.message_id) == int, \"Invalid type for 'message_id'. Expected 'int'. Found '{}'.\"" + ".format(type(self.message_id)))\n" ) cls_str += ( self.indent - + "assert type(self.target) == int, \"Invalid type for 'target'. Expected 'int'. Found '{}'.\"" - ".format(type(self.target))\n\n" + + "enforce(type(self.target) == int, \"Invalid type for 'target'. Expected 'int'. Found '{}'.\"" + ".format(type(self.target)))\n\n" ) cls_str += self.indent + "# Light Protocol Rule 2\n" cls_str += self.indent + "# Check correct performative\n" cls_str += ( self.indent - + "assert type(self.performative) == {}Message.Performative".format( + + "enforce(type(self.performative) == {}Message.Performative".format( self.protocol_specification_in_camel_case ) ) @@ -812,7 +816,7 @@ def _message_class_str(self) -> str: ", \"Invalid 'performative'. Expected either of '{}'. Found '{}'.\".format(" ) cls_str += "self.valid_performatives, self.performative" - cls_str += ")\n\n" + cls_str += "))\n\n" cls_str += self.indent + "# Check correct contents\n" cls_str += ( @@ -845,9 +849,9 @@ def _message_class_str(self) -> str: cls_str += "\n" cls_str += self.indent + "# Check correct content count\n" cls_str += ( - self.indent + "assert expected_nb_of_contents == actual_nb_of_contents, " + self.indent + "enforce(expected_nb_of_contents == actual_nb_of_contents, " '"Incorrect number of contents. Expected {}. Found {}"' - ".format(expected_nb_of_contents, actual_nb_of_contents)\n\n" + ".format(expected_nb_of_contents, actual_nb_of_contents))\n\n" ) cls_str += self.indent + "# Light Protocol Rule 3\n" @@ -855,18 +859,20 @@ def _message_class_str(self) -> str: self._change_indent(1) cls_str += ( self.indent - + "assert self.target == 0, \"Invalid 'target'. Expected 0 (because 'message_id' is 1). Found {}.\".format(self.target)\n" + + "enforce(self.target == 0, \"Invalid 'target'. Expected 0 (because 'message_id' is 1). Found {}.\".format(self.target))\n" ) self._change_indent(-1) cls_str += self.indent + "else:\n" self._change_indent(1) cls_str += ( - self.indent + "assert 0 < self.target < self.message_id, " + self.indent + "enforce(0 < self.target < self.message_id, " "\"Invalid 'target'. Expected an integer between 1 and {} inclusive. Found {}.\"" - ".format(self.message_id - 1, self.target,)\n" + ".format(self.message_id - 1, self.target,))\n" ) self._change_indent(-2) - cls_str += self.indent + "except (AssertionError, ValueError, KeyError) as e:\n" + cls_str += ( + self.indent + "except (AEAEnforceError, ValueError, KeyError) as e:\n" + ) self._change_indent(1) cls_str += self.indent + "logger.error(str(e))\n" cls_str += self.indent + "return False\n\n" @@ -983,14 +989,14 @@ def _dialogue_class_str(self) -> str: # Imports cls_str += self.indent + "from abc import ABC\n" cls_str += ( - self.indent + "from typing import Dict, FrozenSet, Optional, cast\n\n" + self.indent + "from typing import Callable, FrozenSet, Type, cast\n\n" ) + cls_str += self.indent + "from aea.common import Address\n" + cls_str += self.indent + "from aea.protocols.base import Message\n" cls_str += ( self.indent - + "from aea.helpers.dialogue.base import Dialogue, DialogueLabel, Dialogues\n" + + "from aea.protocols.dialogue.base import Dialogue, DialogueLabel, Dialogues\n\n" ) - cls_str += self.indent + "from aea.mail.base import Address\n" - cls_str += self.indent + "from aea.protocols.base import Message\n\n" cls_str += self.indent + "from {}.message import {}Message\n".format( self.dotted_path_to_protocol_package, self.protocol_specification_in_camel_case, @@ -1047,8 +1053,12 @@ def _dialogue_class_str(self) -> str: self._change_indent(1) cls_str += self.indent + "self,\n" cls_str += self.indent + "dialogue_label: DialogueLabel,\n" - cls_str += self.indent + "agent_address: Optional[Address] = None,\n" - cls_str += self.indent + "role: Optional[Dialogue.Role] = None,\n" + cls_str += self.indent + "self_address: Address,\n" + cls_str += self.indent + "role: Dialogue.Role,\n" + cls_str += self.indent + "message_class: Type[{}Message] = {}Message,\n".format( + self.protocol_specification_in_camel_case, + self.protocol_specification_in_camel_case, + ) self._change_indent(-1) cls_str += self.indent + ") -> None:\n" self._change_indent(1) @@ -1059,7 +1069,7 @@ def _dialogue_class_str(self) -> str: ) cls_str += ( self.indent - + ":param agent_address: the address of the agent for whom this dialogue is maintained\n" + + ":param self_address: the address of the entity for whom this dialogue is maintained\n" ) cls_str += ( self.indent @@ -1070,54 +1080,11 @@ def _dialogue_class_str(self) -> str: cls_str += self.indent + "Dialogue.__init__(\n" cls_str += self.indent + "self,\n" cls_str += self.indent + "dialogue_label=dialogue_label,\n" - cls_str += self.indent + "agent_address=agent_address,\n" + cls_str += self.indent + "message_class=message_class,\n" + cls_str += self.indent + "self_address=self_address,\n" cls_str += self.indent + "role=role,\n" - cls_str += self.indent + "rules=Dialogue.Rules(\n" - self._change_indent(1) - cls_str += ( - self.indent - + "cast(FrozenSet[Message.Performative], self.INITIAL_PERFORMATIVES),\n" - ) - cls_str += ( - self.indent - + "cast(FrozenSet[Message.Performative], self.TERMINAL_PERFORMATIVES),\n" - ) - cls_str += self.indent + "cast(\n" - self._change_indent(1) - cls_str += ( - self.indent - + "Dict[Message.Performative, FrozenSet[Message.Performative]],\n" - ) - cls_str += self.indent + "self.VALID_REPLIES,\n" - self._change_indent(-1) - cls_str += self.indent + "),\n" - self._change_indent(-1) - cls_str += self.indent + "),\n" cls_str += self.indent + ")\n" - self._change_indent(-1) - - # is_valid method - cls_str += self.indent + "def is_valid(self, message: Message) -> bool:\n" - self._change_indent(1) - cls_str += self.indent + '"""\n' - cls_str += ( - self.indent - + "Check whether 'message' is a valid next message in the dialogue.\n\n" - ) - cls_str += ( - self.indent - + "These rules capture specific constraints designed for dialogues which are instances of a concrete sub-class of this class.\n" - ) - cls_str += ( - self.indent - + "Override this method with your additional dialogue rules.\n\n" - ) - cls_str += self.indent + ":param message: the message to be validated\n" - cls_str += self.indent + ":return: True if valid, False otherwise\n" - cls_str += self.indent + '"""\n' - cls_str += self.indent + "return True\n\n" - self._change_indent(-1) - self._change_indent(-1) + self._change_indent(-2) # dialogues class cls_str += self.indent + "class {}Dialogues(Dialogues, ABC):\n".format( @@ -1141,55 +1108,48 @@ def _dialogue_class_str(self) -> str: cls_str += self.indent + "END_STATES = frozenset(\n" cls_str += self.indent + "{" + end_states_str + "}" cls_str += self.indent + ")\n\n" - cls_str += self.indent + "def __init__(self, agent_address: Address) -> None:\n" + + cls_str += self.indent + "def __init__(\n" self._change_indent(1) - cls_str += self.indent + '"""\n' - cls_str += self.indent + "Initialize dialogues.\n\n" + cls_str += self.indent + "self,\n" + cls_str += self.indent + "self_address: Address,\n" cls_str += ( self.indent - + ":param agent_address: the address of the agent for whom dialogues are maintained\n" + + "role_from_first_message: Callable[[Message, Address], Dialogue.Role],\n" ) - cls_str += self.indent + ":return: None\n" - cls_str += self.indent + '"""\n' cls_str += ( self.indent - + "Dialogues.__init__(self, agent_address=agent_address, end_states=cast(FrozenSet[Dialogue.EndState], self.END_STATES))\n" - ) - self._change_indent(-1) - cls_str += self.indent + "def create_dialogue(\n" - self._change_indent(1) - cls_str += ( - self.indent + "self, dialogue_label: DialogueLabel, role: Dialogue.Role,\n" + + "dialogue_class: Type[{}Dialogue] = {}Dialogue,\n".format( + self.protocol_specification_in_camel_case, + self.protocol_specification_in_camel_case, + ) ) self._change_indent(-1) - cls_str += self.indent + ") -> {}Dialogue:\n".format( - self.protocol_specification_in_camel_case - ) + cls_str += self.indent + ") -> None:\n" self._change_indent(1) cls_str += self.indent + '"""\n' - cls_str += self.indent + "Create an instance of {} dialogue.\n\n".format( - self.protocol_specification.name - ) - cls_str += ( - self.indent + ":param dialogue_label: the identifier of the dialogue\n" - ) + cls_str += self.indent + "Initialize dialogues.\n\n" cls_str += ( self.indent - + ":param role: the role of the agent this dialogue is maintained for\n\n" + + ":param self_address: the address of the entity for whom dialogues are maintained\n" ) - cls_str += self.indent + ":return: the created dialogue\n" + cls_str += self.indent + ":return: None\n" cls_str += self.indent + '"""\n' - cls_str += self.indent + "dialogue = {}Dialogue(\n".format( - self.protocol_specification_in_camel_case - ) + cls_str += self.indent + "Dialogues.__init__(\n" self._change_indent(1) + cls_str += self.indent + "self,\n" + cls_str += self.indent + "self_address=self_address,\n" cls_str += ( self.indent - + "dialogue_label=dialogue_label, agent_address=self.agent_address, role=role\n" + + "end_states=cast(FrozenSet[Dialogue.EndState], self.END_STATES),\n" + ) + cls_str += self.indent + "message_class={}Message,\n".format( + self.protocol_specification_in_camel_case ) + cls_str += self.indent + "dialogue_class=dialogue_class,\n" + cls_str += self.indent + "role_from_first_message=role_from_first_message,\n" self._change_indent(-1) cls_str += self.indent + ")\n" - cls_str += self.indent + "return dialogue\n" self._change_indent(-2) cls_str += self.indent + "\n" @@ -1487,7 +1447,6 @@ def _decoding_message_content_from_protobuf_to_python( self.protocol_specification.name, performative, content_name ) self._change_indent(1) - # no_indents += 1 decoding_str += self._decoding_message_content_from_protobuf_to_python( performative, content_name, sub_type ) @@ -1862,7 +1821,6 @@ def _protocol_buffer_schema_str(self) -> str: self._change_indent(-1) proto_buff_schema_str += self.indent + "}\n\n" proto_buff_schema_str += "\n" - # self._change_indent(-1) # meta-data proto_buff_schema_str += self.indent + "// Standard {}Message fields\n".format( @@ -1900,6 +1858,9 @@ def _protocol_yaml_str(self) -> str: protocol_yaml_str = "name: {}\n".format(self.protocol_specification.name) protocol_yaml_str += "author: {}\n".format(self.protocol_specification.author) protocol_yaml_str += "version: {}\n".format(self.protocol_specification.version) + protocol_yaml_str += "type: {}\n".format( + self.protocol_specification.component_type + ) protocol_yaml_str += "description: {}\n".format( self.protocol_specification.description ) diff --git a/aea/protocols/generator/common.py b/aea/protocols/generator/common.py index ba03d82894..226c29ece9 100644 --- a/aea/protocols/generator/common.py +++ b/aea/protocols/generator/common.py @@ -285,10 +285,7 @@ def is_installed(programme: str) -> bool: :return: True if installed, False otherwise """ res = shutil.which(programme) - if res is None: - return False - else: - return True + return res is not None def check_prerequisites() -> None: @@ -364,7 +361,6 @@ def try_run_protoc(path_to_generated_protocol_package, name) -> None: :return: A completed process object. """ - # command: "protoc -I={} --python_out={} {}/{}.proto" subprocess.run( # nosec [ "protoc", diff --git a/aea/protocols/generator/validate.py b/aea/protocols/generator/validate.py index 073fb18d94..bae664d719 100644 --- a/aea/protocols/generator/validate.py +++ b/aea/protocols/generator/validate.py @@ -63,10 +63,7 @@ def _is_valid_regex(regex_pattern: str, text: str) -> bool: :return: Boolean result """ match = re.match(regex_pattern, text) - if match is not None: - return True - else: - return False + return match is not None def _has_brackets(content_type: str) -> bool: @@ -81,11 +78,7 @@ def _has_brackets(content_type: str) -> bool: content_type = content_type[len(compositional_type) :] if len(content_type) < 2: return False - else: - return ( - content_type[0] == "[" - and content_type[len(content_type) - 1] == "]" - ) + return content_type[0] == "[" and content_type[len(content_type) - 1] == "]" raise SyntaxError("Content type must be a compositional type!") diff --git a/aea/protocols/scaffold/message.py b/aea/protocols/scaffold/message.py index ecb37e9f06..763bd67970 100644 --- a/aea/protocols/scaffold/message.py +++ b/aea/protocols/scaffold/message.py @@ -22,6 +22,7 @@ from enum import Enum from aea.configurations.base import ProtocolId +from aea.exceptions import enforce from aea.protocols.base import Message from aea.protocols.scaffold.serialization import MyScaffoldSerializer @@ -46,9 +47,9 @@ def __init__(self, performative: Performative, **kwargs): :param performative: the type of message. """ super().__init__(performative=performative, **kwargs) - assert ( - self._is_consistent() - ), "MyScaffoldMessage initialization inconsistent." # pragma: no cover + enforce( # pragma: no cover + self._is_consistent(), "MyScaffoldMessage initialization inconsistent." + ) def _is_consistent(self) -> bool: """Check that the data is consistent.""" diff --git a/aea/protocols/scaffold/protocol.yaml b/aea/protocols/scaffold/protocol.yaml index bd236f8c6b..a3b8c82f35 100644 --- a/aea/protocols/scaffold/protocol.yaml +++ b/aea/protocols/scaffold/protocol.yaml @@ -1,12 +1,13 @@ name: scaffold author: fetchai version: 0.1.0 +type: protocol description: The scaffold protocol scaffolds a protocol to be implemented by the developer. license: Apache-2.0 -aea_version: '>=0.5.0, <0.6.0' +aea_version: '>=0.6.0, <0.7.0' fingerprint: __init__.py: QmedGZfo1UqT6UJoRkHys9kmquia9BQcK17y2touwSENDU - message.py: QmQBEHSHTH19n3dBr2WKAW9vqjytCTHCB2avExs9wMGPxw + message.py: QmQRGUakU9MGVAXy8Hmte5DEAvNYuzw8znt1h9Jg62ZEpM serialization.py: QmNjyzqmoYnCxiLoBeZjXMhYkQzJpbDSFm7A9wytyRa2Xn fingerprint_ignore_patterns: [] dependencies: {} diff --git a/aea/protocols/signing/README.md b/aea/protocols/signing/README.md index 7875b0552f..77b43d1b22 100644 --- a/aea/protocols/signing/README.md +++ b/aea/protocols/signing/README.md @@ -1,15 +1,5 @@ # Signing Protocol -**Name:** signing - -**Author**: fetchai - -**Version**: 0.2.0 - -**Short Description**: A protocol for communication between skills and decision maker. - -**License**: Apache-2.0 - ## Description This is a protocol for communication between a skill and a decision maker. @@ -20,32 +10,22 @@ This is a protocol for communication between a skill and a decision maker. --- name: signing author: fetchai -version: 0.2.0 +version: 0.3.0 description: A protocol for communication between skills and decision maker. license: Apache-2.0 -aea_version: '>=0.5.0, <0.6.0' +aea_version: '>=0.6.0, <0.7.0' speech_acts: sign_transaction: - skill_callback_ids: pt:list[pt:str] - skill_callback_info: pt:dict[pt:str, pt:str] terms: ct:Terms raw_transaction: ct:RawTransaction sign_message: - skill_callback_ids: pt:list[pt:str] - skill_callback_info: pt:dict[pt:str, pt:str] terms: ct:Terms raw_message: ct:RawMessage signed_transaction: - skill_callback_ids: pt:list[pt:str] - skill_callback_info: pt:dict[pt:str, pt:str] signed_transaction: ct:SignedTransaction signed_message: - skill_callback_ids: pt:list[pt:str] - skill_callback_info: pt:dict[pt:str, pt:str] signed_message: ct:SignedMessage error: - skill_callback_ids: pt:list[pt:str] - skill_callback_info: pt:dict[pt:str, pt:str] error_code: ct:ErrorCode ... --- diff --git a/aea/protocols/signing/dialogues.py b/aea/protocols/signing/dialogues.py index 40bde9680b..2602cbb7d9 100644 --- a/aea/protocols/signing/dialogues.py +++ b/aea/protocols/signing/dialogues.py @@ -25,11 +25,11 @@ """ from abc import ABC -from typing import Dict, FrozenSet, Optional, cast +from typing import Callable, FrozenSet, Type, cast -from aea.helpers.dialogue.base import Dialogue, DialogueLabel, Dialogues -from aea.mail.base import Address +from aea.common import Address from aea.protocols.base import Message +from aea.protocols.dialogue.base import Dialogue, DialogueLabel, Dialogues from aea.protocols.signing.message import SigningMessage @@ -82,44 +82,26 @@ class EndState(Dialogue.EndState): def __init__( self, dialogue_label: DialogueLabel, - agent_address: Optional[Address] = None, - role: Optional[Dialogue.Role] = None, + self_address: Address, + role: Dialogue.Role, + message_class: Type[SigningMessage] = SigningMessage, ) -> None: """ Initialize a dialogue. :param dialogue_label: the identifier of the dialogue - :param agent_address: the address of the agent for whom this dialogue is maintained + :param self_address: the address of the entity for whom this dialogue is maintained :param role: the role of the agent this dialogue is maintained for :return: None """ Dialogue.__init__( self, dialogue_label=dialogue_label, - agent_address=agent_address, + message_class=message_class, + self_address=self_address, role=role, - rules=Dialogue.Rules( - cast(FrozenSet[Message.Performative], self.INITIAL_PERFORMATIVES), - cast(FrozenSet[Message.Performative], self.TERMINAL_PERFORMATIVES), - cast( - Dict[Message.Performative, FrozenSet[Message.Performative]], - self.VALID_REPLIES, - ), - ), ) - def is_valid(self, message: Message) -> bool: - """ - Check whether 'message' is a valid next message in the dialogue. - - These rules capture specific constraints designed for dialogues which are instances of a concrete sub-class of this class. - Override this method with your additional dialogue rules. - - :param message: the message to be validated - :return: True if valid, False otherwise - """ - return True - class SigningDialogues(Dialogues, ABC): """This class keeps track of all signing dialogues.""" @@ -128,31 +110,23 @@ class SigningDialogues(Dialogues, ABC): {SigningDialogue.EndState.SUCCESSFUL, SigningDialogue.EndState.FAILED} ) - def __init__(self, agent_address: Address) -> None: + def __init__( + self, + self_address: Address, + role_from_first_message: Callable[[Message, Address], Dialogue.Role], + dialogue_class: Type[SigningDialogue] = SigningDialogue, + ) -> None: """ Initialize dialogues. - :param agent_address: the address of the agent for whom dialogues are maintained + :param self_address: the address of the entity for whom dialogues are maintained :return: None """ Dialogues.__init__( self, - agent_address=agent_address, + self_address=self_address, end_states=cast(FrozenSet[Dialogue.EndState], self.END_STATES), + message_class=SigningMessage, + dialogue_class=dialogue_class, + role_from_first_message=role_from_first_message, ) - - def create_dialogue( - self, dialogue_label: DialogueLabel, role: Dialogue.Role, - ) -> SigningDialogue: - """ - Create an instance of signing dialogue. - - :param dialogue_label: the identifier of the dialogue - :param role: the role of the agent this dialogue is maintained for - - :return: the created dialogue - """ - dialogue = SigningDialogue( - dialogue_label=dialogue_label, agent_address=self.agent_address, role=role - ) - return dialogue diff --git a/aea/protocols/signing/message.py b/aea/protocols/signing/message.py index 5150a27d5a..68a46db489 100644 --- a/aea/protocols/signing/message.py +++ b/aea/protocols/signing/message.py @@ -20,10 +20,10 @@ """This module contains signing's message definition.""" import logging -from enum import Enum -from typing import Dict, Set, Tuple, cast +from typing import Set, Tuple, cast from aea.configurations.base import ProtocolId +from aea.exceptions import AEAEnforceError, enforce from aea.protocols.base import Message from aea.protocols.signing.custom_types import ErrorCode as CustomErrorCode from aea.protocols.signing.custom_types import RawMessage as CustomRawMessage @@ -42,7 +42,7 @@ class SigningMessage(Message): """A protocol for communication between skills and decision maker.""" - protocol_id = ProtocolId.from_str("fetchai/signing:0.2.0") + protocol_id = ProtocolId.from_str("fetchai/signing:0.3.0") ErrorCode = CustomErrorCode @@ -56,7 +56,7 @@ class SigningMessage(Message): Terms = CustomTerms - class Performative(Enum): + class Performative(Message.Performative): """Performatives for the signing protocol.""" ERROR = "error" @@ -85,13 +85,6 @@ def __init__( :param target: the message target. :param performative: the message performative. """ - super().__init__( - dialogue_reference=dialogue_reference, - message_id=message_id, - target=target, - performative=SigningMessage.Performative(performative), - **kwargs, - ) self._performatives = { "error", "sign_message", @@ -99,6 +92,13 @@ def __init__( "signed_message", "signed_transaction", } + super().__init__( + dialogue_reference=dialogue_reference, + message_id=message_id, + target=target, + performative=SigningMessage.Performative(performative), + **kwargs, + ) @property def valid_performatives(self) -> Set[str]: @@ -108,323 +108,189 @@ def valid_performatives(self) -> Set[str]: @property def dialogue_reference(self) -> Tuple[str, str]: """Get the dialogue_reference of the message.""" - assert self.is_set("dialogue_reference"), "dialogue_reference is not set." + enforce(self.is_set("dialogue_reference"), "dialogue_reference is not set.") return cast(Tuple[str, str], self.get("dialogue_reference")) @property def message_id(self) -> int: """Get the message_id of the message.""" - assert self.is_set("message_id"), "message_id is not set." + enforce(self.is_set("message_id"), "message_id is not set.") return cast(int, self.get("message_id")) @property def performative(self) -> Performative: # type: ignore # noqa: F821 """Get the performative of the message.""" - assert self.is_set("performative"), "performative is not set." + enforce(self.is_set("performative"), "performative is not set.") return cast(SigningMessage.Performative, self.get("performative")) @property def target(self) -> int: """Get the target of the message.""" - assert self.is_set("target"), "target is not set." + enforce(self.is_set("target"), "target is not set.") return cast(int, self.get("target")) @property def error_code(self) -> CustomErrorCode: """Get the 'error_code' content from the message.""" - assert self.is_set("error_code"), "'error_code' content is not set." + enforce(self.is_set("error_code"), "'error_code' content is not set.") return cast(CustomErrorCode, self.get("error_code")) @property def raw_message(self) -> CustomRawMessage: """Get the 'raw_message' content from the message.""" - assert self.is_set("raw_message"), "'raw_message' content is not set." + enforce(self.is_set("raw_message"), "'raw_message' content is not set.") return cast(CustomRawMessage, self.get("raw_message")) @property def raw_transaction(self) -> CustomRawTransaction: """Get the 'raw_transaction' content from the message.""" - assert self.is_set("raw_transaction"), "'raw_transaction' content is not set." + enforce(self.is_set("raw_transaction"), "'raw_transaction' content is not set.") return cast(CustomRawTransaction, self.get("raw_transaction")) @property def signed_message(self) -> CustomSignedMessage: """Get the 'signed_message' content from the message.""" - assert self.is_set("signed_message"), "'signed_message' content is not set." + enforce(self.is_set("signed_message"), "'signed_message' content is not set.") return cast(CustomSignedMessage, self.get("signed_message")) @property def signed_transaction(self) -> CustomSignedTransaction: """Get the 'signed_transaction' content from the message.""" - assert self.is_set( - "signed_transaction" - ), "'signed_transaction' content is not set." + enforce( + self.is_set("signed_transaction"), + "'signed_transaction' content is not set.", + ) return cast(CustomSignedTransaction, self.get("signed_transaction")) - @property - def skill_callback_ids(self) -> Tuple[str, ...]: - """Get the 'skill_callback_ids' content from the message.""" - assert self.is_set( - "skill_callback_ids" - ), "'skill_callback_ids' content is not set." - return cast(Tuple[str, ...], self.get("skill_callback_ids")) - - @property - def skill_callback_info(self) -> Dict[str, str]: - """Get the 'skill_callback_info' content from the message.""" - assert self.is_set( - "skill_callback_info" - ), "'skill_callback_info' content is not set." - return cast(Dict[str, str], self.get("skill_callback_info")) - @property def terms(self) -> CustomTerms: """Get the 'terms' content from the message.""" - assert self.is_set("terms"), "'terms' content is not set." + enforce(self.is_set("terms"), "'terms' content is not set.") return cast(CustomTerms, self.get("terms")) def _is_consistent(self) -> bool: """Check that the message follows the signing protocol.""" try: - assert ( - type(self.dialogue_reference) == tuple - ), "Invalid type for 'dialogue_reference'. Expected 'tuple'. Found '{}'.".format( - type(self.dialogue_reference) + enforce( + type(self.dialogue_reference) == tuple, + "Invalid type for 'dialogue_reference'. Expected 'tuple'. Found '{}'.".format( + type(self.dialogue_reference) + ), ) - assert ( - type(self.dialogue_reference[0]) == str - ), "Invalid type for 'dialogue_reference[0]'. Expected 'str'. Found '{}'.".format( - type(self.dialogue_reference[0]) + enforce( + type(self.dialogue_reference[0]) == str, + "Invalid type for 'dialogue_reference[0]'. Expected 'str'. Found '{}'.".format( + type(self.dialogue_reference[0]) + ), ) - assert ( - type(self.dialogue_reference[1]) == str - ), "Invalid type for 'dialogue_reference[1]'. Expected 'str'. Found '{}'.".format( - type(self.dialogue_reference[1]) + enforce( + type(self.dialogue_reference[1]) == str, + "Invalid type for 'dialogue_reference[1]'. Expected 'str'. Found '{}'.".format( + type(self.dialogue_reference[1]) + ), ) - assert ( - type(self.message_id) == int - ), "Invalid type for 'message_id'. Expected 'int'. Found '{}'.".format( - type(self.message_id) + enforce( + type(self.message_id) == int, + "Invalid type for 'message_id'. Expected 'int'. Found '{}'.".format( + type(self.message_id) + ), ) - assert ( - type(self.target) == int - ), "Invalid type for 'target'. Expected 'int'. Found '{}'.".format( - type(self.target) + enforce( + type(self.target) == int, + "Invalid type for 'target'. Expected 'int'. Found '{}'.".format( + type(self.target) + ), ) # Light Protocol Rule 2 # Check correct performative - assert ( - type(self.performative) == SigningMessage.Performative - ), "Invalid 'performative'. Expected either of '{}'. Found '{}'.".format( - self.valid_performatives, self.performative + enforce( + type(self.performative) == SigningMessage.Performative, + "Invalid 'performative'. Expected either of '{}'. Found '{}'.".format( + self.valid_performatives, self.performative + ), ) # Check correct contents actual_nb_of_contents = len(self.body) - DEFAULT_BODY_SIZE expected_nb_of_contents = 0 if self.performative == SigningMessage.Performative.SIGN_TRANSACTION: - expected_nb_of_contents = 4 - assert ( - type(self.skill_callback_ids) == tuple - ), "Invalid type for content 'skill_callback_ids'. Expected 'tuple'. Found '{}'.".format( - type(self.skill_callback_ids) + expected_nb_of_contents = 2 + enforce( + type(self.terms) == CustomTerms, + "Invalid type for content 'terms'. Expected 'Terms'. Found '{}'.".format( + type(self.terms) + ), ) - assert all( - type(element) == str for element in self.skill_callback_ids - ), "Invalid type for tuple elements in content 'skill_callback_ids'. Expected 'str'." - assert ( - type(self.skill_callback_info) == dict - ), "Invalid type for content 'skill_callback_info'. Expected 'dict'. Found '{}'.".format( - type(self.skill_callback_info) - ) - for ( - key_of_skill_callback_info, - value_of_skill_callback_info, - ) in self.skill_callback_info.items(): - assert ( - type(key_of_skill_callback_info) == str - ), "Invalid type for dictionary keys in content 'skill_callback_info'. Expected 'str'. Found '{}'.".format( - type(key_of_skill_callback_info) - ) - assert ( - type(value_of_skill_callback_info) == str - ), "Invalid type for dictionary values in content 'skill_callback_info'. Expected 'str'. Found '{}'.".format( - type(value_of_skill_callback_info) - ) - assert ( - type(self.terms) == CustomTerms - ), "Invalid type for content 'terms'. Expected 'Terms'. Found '{}'.".format( - type(self.terms) - ) - assert ( - type(self.raw_transaction) == CustomRawTransaction - ), "Invalid type for content 'raw_transaction'. Expected 'RawTransaction'. Found '{}'.".format( - type(self.raw_transaction) + enforce( + type(self.raw_transaction) == CustomRawTransaction, + "Invalid type for content 'raw_transaction'. Expected 'RawTransaction'. Found '{}'.".format( + type(self.raw_transaction) + ), ) elif self.performative == SigningMessage.Performative.SIGN_MESSAGE: - expected_nb_of_contents = 4 - assert ( - type(self.skill_callback_ids) == tuple - ), "Invalid type for content 'skill_callback_ids'. Expected 'tuple'. Found '{}'.".format( - type(self.skill_callback_ids) - ) - assert all( - type(element) == str for element in self.skill_callback_ids - ), "Invalid type for tuple elements in content 'skill_callback_ids'. Expected 'str'." - assert ( - type(self.skill_callback_info) == dict - ), "Invalid type for content 'skill_callback_info'. Expected 'dict'. Found '{}'.".format( - type(self.skill_callback_info) + expected_nb_of_contents = 2 + enforce( + type(self.terms) == CustomTerms, + "Invalid type for content 'terms'. Expected 'Terms'. Found '{}'.".format( + type(self.terms) + ), ) - for ( - key_of_skill_callback_info, - value_of_skill_callback_info, - ) in self.skill_callback_info.items(): - assert ( - type(key_of_skill_callback_info) == str - ), "Invalid type for dictionary keys in content 'skill_callback_info'. Expected 'str'. Found '{}'.".format( - type(key_of_skill_callback_info) - ) - assert ( - type(value_of_skill_callback_info) == str - ), "Invalid type for dictionary values in content 'skill_callback_info'. Expected 'str'. Found '{}'.".format( - type(value_of_skill_callback_info) - ) - assert ( - type(self.terms) == CustomTerms - ), "Invalid type for content 'terms'. Expected 'Terms'. Found '{}'.".format( - type(self.terms) - ) - assert ( - type(self.raw_message) == CustomRawMessage - ), "Invalid type for content 'raw_message'. Expected 'RawMessage'. Found '{}'.".format( - type(self.raw_message) + enforce( + type(self.raw_message) == CustomRawMessage, + "Invalid type for content 'raw_message'. Expected 'RawMessage'. Found '{}'.".format( + type(self.raw_message) + ), ) elif self.performative == SigningMessage.Performative.SIGNED_TRANSACTION: - expected_nb_of_contents = 3 - assert ( - type(self.skill_callback_ids) == tuple - ), "Invalid type for content 'skill_callback_ids'. Expected 'tuple'. Found '{}'.".format( - type(self.skill_callback_ids) - ) - assert all( - type(element) == str for element in self.skill_callback_ids - ), "Invalid type for tuple elements in content 'skill_callback_ids'. Expected 'str'." - assert ( - type(self.skill_callback_info) == dict - ), "Invalid type for content 'skill_callback_info'. Expected 'dict'. Found '{}'.".format( - type(self.skill_callback_info) - ) - for ( - key_of_skill_callback_info, - value_of_skill_callback_info, - ) in self.skill_callback_info.items(): - assert ( - type(key_of_skill_callback_info) == str - ), "Invalid type for dictionary keys in content 'skill_callback_info'. Expected 'str'. Found '{}'.".format( - type(key_of_skill_callback_info) - ) - assert ( - type(value_of_skill_callback_info) == str - ), "Invalid type for dictionary values in content 'skill_callback_info'. Expected 'str'. Found '{}'.".format( - type(value_of_skill_callback_info) - ) - assert ( - type(self.signed_transaction) == CustomSignedTransaction - ), "Invalid type for content 'signed_transaction'. Expected 'SignedTransaction'. Found '{}'.".format( - type(self.signed_transaction) + expected_nb_of_contents = 1 + enforce( + type(self.signed_transaction) == CustomSignedTransaction, + "Invalid type for content 'signed_transaction'. Expected 'SignedTransaction'. Found '{}'.".format( + type(self.signed_transaction) + ), ) elif self.performative == SigningMessage.Performative.SIGNED_MESSAGE: - expected_nb_of_contents = 3 - assert ( - type(self.skill_callback_ids) == tuple - ), "Invalid type for content 'skill_callback_ids'. Expected 'tuple'. Found '{}'.".format( - type(self.skill_callback_ids) - ) - assert all( - type(element) == str for element in self.skill_callback_ids - ), "Invalid type for tuple elements in content 'skill_callback_ids'. Expected 'str'." - assert ( - type(self.skill_callback_info) == dict - ), "Invalid type for content 'skill_callback_info'. Expected 'dict'. Found '{}'.".format( - type(self.skill_callback_info) - ) - for ( - key_of_skill_callback_info, - value_of_skill_callback_info, - ) in self.skill_callback_info.items(): - assert ( - type(key_of_skill_callback_info) == str - ), "Invalid type for dictionary keys in content 'skill_callback_info'. Expected 'str'. Found '{}'.".format( - type(key_of_skill_callback_info) - ) - assert ( - type(value_of_skill_callback_info) == str - ), "Invalid type for dictionary values in content 'skill_callback_info'. Expected 'str'. Found '{}'.".format( - type(value_of_skill_callback_info) - ) - assert ( - type(self.signed_message) == CustomSignedMessage - ), "Invalid type for content 'signed_message'. Expected 'SignedMessage'. Found '{}'.".format( - type(self.signed_message) + expected_nb_of_contents = 1 + enforce( + type(self.signed_message) == CustomSignedMessage, + "Invalid type for content 'signed_message'. Expected 'SignedMessage'. Found '{}'.".format( + type(self.signed_message) + ), ) elif self.performative == SigningMessage.Performative.ERROR: - expected_nb_of_contents = 3 - assert ( - type(self.skill_callback_ids) == tuple - ), "Invalid type for content 'skill_callback_ids'. Expected 'tuple'. Found '{}'.".format( - type(self.skill_callback_ids) - ) - assert all( - type(element) == str for element in self.skill_callback_ids - ), "Invalid type for tuple elements in content 'skill_callback_ids'. Expected 'str'." - assert ( - type(self.skill_callback_info) == dict - ), "Invalid type for content 'skill_callback_info'. Expected 'dict'. Found '{}'.".format( - type(self.skill_callback_info) - ) - for ( - key_of_skill_callback_info, - value_of_skill_callback_info, - ) in self.skill_callback_info.items(): - assert ( - type(key_of_skill_callback_info) == str - ), "Invalid type for dictionary keys in content 'skill_callback_info'. Expected 'str'. Found '{}'.".format( - type(key_of_skill_callback_info) - ) - assert ( - type(value_of_skill_callback_info) == str - ), "Invalid type for dictionary values in content 'skill_callback_info'. Expected 'str'. Found '{}'.".format( - type(value_of_skill_callback_info) - ) - assert ( - type(self.error_code) == CustomErrorCode - ), "Invalid type for content 'error_code'. Expected 'ErrorCode'. Found '{}'.".format( - type(self.error_code) + expected_nb_of_contents = 1 + enforce( + type(self.error_code) == CustomErrorCode, + "Invalid type for content 'error_code'. Expected 'ErrorCode'. Found '{}'.".format( + type(self.error_code) + ), ) # Check correct content count - assert ( - expected_nb_of_contents == actual_nb_of_contents - ), "Incorrect number of contents. Expected {}. Found {}".format( - expected_nb_of_contents, actual_nb_of_contents + enforce( + expected_nb_of_contents == actual_nb_of_contents, + "Incorrect number of contents. Expected {}. Found {}".format( + expected_nb_of_contents, actual_nb_of_contents + ), ) # Light Protocol Rule 3 if self.message_id == 1: - assert ( - self.target == 0 - ), "Invalid 'target'. Expected 0 (because 'message_id' is 1). Found {}.".format( - self.target + enforce( + self.target == 0, + "Invalid 'target'. Expected 0 (because 'message_id' is 1). Found {}.".format( + self.target + ), ) else: - assert ( - 0 < self.target < self.message_id - ), "Invalid 'target'. Expected an integer between 1 and {} inclusive. Found {}.".format( - self.message_id - 1, self.target, + enforce( + 0 < self.target < self.message_id, + "Invalid 'target'. Expected an integer between 1 and {} inclusive. Found {}.".format( + self.message_id - 1, self.target, + ), ) - except (AssertionError, ValueError, KeyError) as e: + except (AEAEnforceError, ValueError, KeyError) as e: logger.error(str(e)) return False diff --git a/aea/protocols/signing/protocol.yaml b/aea/protocols/signing/protocol.yaml index 15ce140129..15bf3690d9 100644 --- a/aea/protocols/signing/protocol.yaml +++ b/aea/protocols/signing/protocol.yaml @@ -1,18 +1,19 @@ name: signing author: fetchai -version: 0.2.0 +version: 0.3.0 +type: protocol description: A protocol for communication between skills and decision maker. license: Apache-2.0 -aea_version: '>=0.5.0, <0.6.0' +aea_version: '>=0.6.0, <0.7.0' fingerprint: - README.md: QmSYzpWru7bswJGW1DBtuLgYUrbF5MXZ3KkDwnwxfB2yYk + README.md: QmSoa5dnxz53GWpWT2VvcRG4asVbzA8JzguiVgwqiLtguM __init__.py: QmcCL3TTdvd8wxYKzf2d3cgKEtY9RzLjPCn4hex4wmb6h6 custom_types.py: Qmc7sAyCQbAaVs5dZf9hFkTrB2BG8VAioWzbyKBAybrQ1J - dialogues.py: QmaoSYB1baPGuVa64H7xtMkNjTybQguPbFBSzBwLsTVT8x - message.py: QmRXGbAy2oYWecxXmdxfQW9dNspinwhxVuSK4RqR4WZTvE - serialization.py: QmPUWHUpQ9pst42s1naM5nTbsxxko5HxPi2gB86FQnMGnL - signing.proto: QmT59ZVsevFoJ51uiuAzCgHGowmwfo3bLAKRSgXV1qyXFo - signing_pb2.py: QmPZFneKLZUipxAZ3usnmUm1br6VvetzvBpid6GU4JjR39 + dialogues.py: QmQ1WKs3Dn15oDSwpc4N8hdADLxrn76U4X5SiLAmyGiPPY + message.py: QmRLWAYfjCQmdg2hH8R5R63DKYaDrzuX4dVTFqNHuyjawq + serialization.py: QmZrztNBaWA6B5wJHQWfM2g6opPtvsEXtqytzmxjKWm7Sb + signing.proto: QmcxyLzqhTE9xstAEzCVH17osbLxmSdALx9njmuPjhjrvZ + signing_pb2.py: QmY3Ak5ih5zGvKjeZ5EnzrGX4tMYn5dWpjPArQwFeJpVKu fingerprint_ignore_patterns: [] dependencies: protobuf: {} diff --git a/aea/protocols/signing/serialization.py b/aea/protocols/signing/serialization.py index fce5d7c9f8..88acd2f7a4 100644 --- a/aea/protocols/signing/serialization.py +++ b/aea/protocols/signing/serialization.py @@ -55,10 +55,6 @@ def encode(msg: Message) -> bytes: performative_id = msg.performative if performative_id == SigningMessage.Performative.SIGN_TRANSACTION: performative = signing_pb2.SigningMessage.Sign_Transaction_Performative() # type: ignore - skill_callback_ids = msg.skill_callback_ids - performative.skill_callback_ids.extend(skill_callback_ids) - skill_callback_info = msg.skill_callback_info - performative.skill_callback_info.update(skill_callback_info) terms = msg.terms Terms.encode(performative.terms, terms) raw_transaction = msg.raw_transaction @@ -66,10 +62,6 @@ def encode(msg: Message) -> bytes: signing_msg.sign_transaction.CopyFrom(performative) elif performative_id == SigningMessage.Performative.SIGN_MESSAGE: performative = signing_pb2.SigningMessage.Sign_Message_Performative() # type: ignore - skill_callback_ids = msg.skill_callback_ids - performative.skill_callback_ids.extend(skill_callback_ids) - skill_callback_info = msg.skill_callback_info - performative.skill_callback_info.update(skill_callback_info) terms = msg.terms Terms.encode(performative.terms, terms) raw_message = msg.raw_message @@ -77,10 +69,6 @@ def encode(msg: Message) -> bytes: signing_msg.sign_message.CopyFrom(performative) elif performative_id == SigningMessage.Performative.SIGNED_TRANSACTION: performative = signing_pb2.SigningMessage.Signed_Transaction_Performative() # type: ignore - skill_callback_ids = msg.skill_callback_ids - performative.skill_callback_ids.extend(skill_callback_ids) - skill_callback_info = msg.skill_callback_info - performative.skill_callback_info.update(skill_callback_info) signed_transaction = msg.signed_transaction SignedTransaction.encode( performative.signed_transaction, signed_transaction @@ -88,19 +76,11 @@ def encode(msg: Message) -> bytes: signing_msg.signed_transaction.CopyFrom(performative) elif performative_id == SigningMessage.Performative.SIGNED_MESSAGE: performative = signing_pb2.SigningMessage.Signed_Message_Performative() # type: ignore - skill_callback_ids = msg.skill_callback_ids - performative.skill_callback_ids.extend(skill_callback_ids) - skill_callback_info = msg.skill_callback_info - performative.skill_callback_info.update(skill_callback_info) signed_message = msg.signed_message SignedMessage.encode(performative.signed_message, signed_message) signing_msg.signed_message.CopyFrom(performative) elif performative_id == SigningMessage.Performative.ERROR: performative = signing_pb2.SigningMessage.Error_Performative() # type: ignore - skill_callback_ids = msg.skill_callback_ids - performative.skill_callback_ids.extend(skill_callback_ids) - skill_callback_info = msg.skill_callback_info - performative.skill_callback_info.update(skill_callback_info) error_code = msg.error_code ErrorCode.encode(performative.error_code, error_code) signing_msg.error.CopyFrom(performative) @@ -131,12 +111,6 @@ def decode(obj: bytes) -> Message: performative_id = SigningMessage.Performative(str(performative)) performative_content = dict() # type: Dict[str, Any] if performative_id == SigningMessage.Performative.SIGN_TRANSACTION: - skill_callback_ids = signing_pb.sign_transaction.skill_callback_ids - skill_callback_ids_tuple = tuple(skill_callback_ids) - performative_content["skill_callback_ids"] = skill_callback_ids_tuple - skill_callback_info = signing_pb.sign_transaction.skill_callback_info - skill_callback_info_dict = dict(skill_callback_info) - performative_content["skill_callback_info"] = skill_callback_info_dict pb2_terms = signing_pb.sign_transaction.terms terms = Terms.decode(pb2_terms) performative_content["terms"] = terms @@ -144,12 +118,6 @@ def decode(obj: bytes) -> Message: raw_transaction = RawTransaction.decode(pb2_raw_transaction) performative_content["raw_transaction"] = raw_transaction elif performative_id == SigningMessage.Performative.SIGN_MESSAGE: - skill_callback_ids = signing_pb.sign_message.skill_callback_ids - skill_callback_ids_tuple = tuple(skill_callback_ids) - performative_content["skill_callback_ids"] = skill_callback_ids_tuple - skill_callback_info = signing_pb.sign_message.skill_callback_info - skill_callback_info_dict = dict(skill_callback_info) - performative_content["skill_callback_info"] = skill_callback_info_dict pb2_terms = signing_pb.sign_message.terms terms = Terms.decode(pb2_terms) performative_content["terms"] = terms @@ -157,32 +125,14 @@ def decode(obj: bytes) -> Message: raw_message = RawMessage.decode(pb2_raw_message) performative_content["raw_message"] = raw_message elif performative_id == SigningMessage.Performative.SIGNED_TRANSACTION: - skill_callback_ids = signing_pb.signed_transaction.skill_callback_ids - skill_callback_ids_tuple = tuple(skill_callback_ids) - performative_content["skill_callback_ids"] = skill_callback_ids_tuple - skill_callback_info = signing_pb.signed_transaction.skill_callback_info - skill_callback_info_dict = dict(skill_callback_info) - performative_content["skill_callback_info"] = skill_callback_info_dict pb2_signed_transaction = signing_pb.signed_transaction.signed_transaction signed_transaction = SignedTransaction.decode(pb2_signed_transaction) performative_content["signed_transaction"] = signed_transaction elif performative_id == SigningMessage.Performative.SIGNED_MESSAGE: - skill_callback_ids = signing_pb.signed_message.skill_callback_ids - skill_callback_ids_tuple = tuple(skill_callback_ids) - performative_content["skill_callback_ids"] = skill_callback_ids_tuple - skill_callback_info = signing_pb.signed_message.skill_callback_info - skill_callback_info_dict = dict(skill_callback_info) - performative_content["skill_callback_info"] = skill_callback_info_dict pb2_signed_message = signing_pb.signed_message.signed_message signed_message = SignedMessage.decode(pb2_signed_message) performative_content["signed_message"] = signed_message elif performative_id == SigningMessage.Performative.ERROR: - skill_callback_ids = signing_pb.error.skill_callback_ids - skill_callback_ids_tuple = tuple(skill_callback_ids) - performative_content["skill_callback_ids"] = skill_callback_ids_tuple - skill_callback_info = signing_pb.error.skill_callback_info - skill_callback_info_dict = dict(skill_callback_info) - performative_content["skill_callback_info"] = skill_callback_info_dict pb2_error_code = signing_pb.error.error_code error_code = ErrorCode.decode(pb2_error_code) performative_content["error_code"] = error_code diff --git a/aea/protocols/signing/signing.proto b/aea/protocols/signing/signing.proto index e1243dd50f..1d773613b0 100644 --- a/aea/protocols/signing/signing.proto +++ b/aea/protocols/signing/signing.proto @@ -36,35 +36,25 @@ message SigningMessage{ // Performatives and contents message Sign_Transaction_Performative{ - repeated string skill_callback_ids = 1; - map skill_callback_info = 2; - Terms terms = 3; - RawTransaction raw_transaction = 4; + Terms terms = 1; + RawTransaction raw_transaction = 2; } message Sign_Message_Performative{ - repeated string skill_callback_ids = 1; - map skill_callback_info = 2; - Terms terms = 3; - RawMessage raw_message = 4; + Terms terms = 1; + RawMessage raw_message = 2; } message Signed_Transaction_Performative{ - repeated string skill_callback_ids = 1; - map skill_callback_info = 2; - SignedTransaction signed_transaction = 3; + SignedTransaction signed_transaction = 1; } message Signed_Message_Performative{ - repeated string skill_callback_ids = 1; - map skill_callback_info = 2; - SignedMessage signed_message = 3; + SignedMessage signed_message = 1; } message Error_Performative{ - repeated string skill_callback_ids = 1; - map skill_callback_info = 2; - ErrorCode error_code = 3; + ErrorCode error_code = 1; } diff --git a/aea/protocols/signing/signing_pb2.py b/aea/protocols/signing/signing_pb2.py index 590bec884c..f88113b01e 100644 --- a/aea/protocols/signing/signing_pb2.py +++ b/aea/protocols/signing/signing_pb2.py @@ -17,7 +17,7 @@ package="fetch.aea.Signing", syntax="proto3", serialized_options=None, - serialized_pb=b"\n\rsigning.proto\x12\x11\x66\x65tch.aea.Signing\"\x93\x14\n\x0eSigningMessage\x12\x12\n\nmessage_id\x18\x01 \x01(\x05\x12\"\n\x1a\x64ialogue_starter_reference\x18\x02 \x01(\t\x12$\n\x1c\x64ialogue_responder_reference\x18\x03 \x01(\t\x12\x0e\n\x06target\x18\x04 \x01(\x05\x12\x45\n\x05\x65rror\x18\x05 \x01(\x0b\x32\x34.fetch.aea.Signing.SigningMessage.Error_PerformativeH\x00\x12S\n\x0csign_message\x18\x06 \x01(\x0b\x32;.fetch.aea.Signing.SigningMessage.Sign_Message_PerformativeH\x00\x12[\n\x10sign_transaction\x18\x07 \x01(\x0b\x32?.fetch.aea.Signing.SigningMessage.Sign_Transaction_PerformativeH\x00\x12W\n\x0esigned_message\x18\x08 \x01(\x0b\x32=.fetch.aea.Signing.SigningMessage.Signed_Message_PerformativeH\x00\x12_\n\x12signed_transaction\x18\t \x01(\x0b\x32\x41.fetch.aea.Signing.SigningMessage.Signed_Transaction_PerformativeH\x00\x1a\xb3\x01\n\tErrorCode\x12M\n\nerror_code\x18\x01 \x01(\x0e\x32\x39.fetch.aea.Signing.SigningMessage.ErrorCode.ErrorCodeEnum\"W\n\rErrorCodeEnum\x12 \n\x1cUNSUCCESSFUL_MESSAGE_SIGNING\x10\x00\x12$\n UNSUCCESSFUL_TRANSACTION_SIGNING\x10\x01\x1a!\n\nRawMessage\x12\x13\n\x0braw_message\x18\x01 \x01(\x0c\x1a)\n\x0eRawTransaction\x12\x17\n\x0fraw_transaction\x18\x01 \x01(\x0c\x1a'\n\rSignedMessage\x12\x16\n\x0esigned_message\x18\x01 \x01(\x0c\x1a/\n\x11SignedTransaction\x12\x1a\n\x12signed_transaction\x18\x01 \x01(\x0c\x1a\x16\n\x05Terms\x12\r\n\x05terms\x18\x01 \x01(\x0c\x1a\xed\x02\n\x1dSign_Transaction_Performative\x12\x1a\n\x12skill_callback_ids\x18\x01 \x03(\t\x12s\n\x13skill_callback_info\x18\x02 \x03(\x0b\x32V.fetch.aea.Signing.SigningMessage.Sign_Transaction_Performative.SkillCallbackInfoEntry\x12\x36\n\x05terms\x18\x03 \x01(\x0b\x32'.fetch.aea.Signing.SigningMessage.Terms\x12I\n\x0fraw_transaction\x18\x04 \x01(\x0b\x32\x30.fetch.aea.Signing.SigningMessage.RawTransaction\x1a\x38\n\x16SkillCallbackInfoEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\xdd\x02\n\x19Sign_Message_Performative\x12\x1a\n\x12skill_callback_ids\x18\x01 \x03(\t\x12o\n\x13skill_callback_info\x18\x02 \x03(\x0b\x32R.fetch.aea.Signing.SigningMessage.Sign_Message_Performative.SkillCallbackInfoEntry\x12\x36\n\x05terms\x18\x03 \x01(\x0b\x32'.fetch.aea.Signing.SigningMessage.Terms\x12\x41\n\x0braw_message\x18\x04 \x01(\x0b\x32,.fetch.aea.Signing.SigningMessage.RawMessage\x1a\x38\n\x16SkillCallbackInfoEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\xbf\x02\n\x1fSigned_Transaction_Performative\x12\x1a\n\x12skill_callback_ids\x18\x01 \x03(\t\x12u\n\x13skill_callback_info\x18\x02 \x03(\x0b\x32X.fetch.aea.Signing.SigningMessage.Signed_Transaction_Performative.SkillCallbackInfoEntry\x12O\n\x12signed_transaction\x18\x03 \x01(\x0b\x32\x33.fetch.aea.Signing.SigningMessage.SignedTransaction\x1a\x38\n\x16SkillCallbackInfoEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\xaf\x02\n\x1bSigned_Message_Performative\x12\x1a\n\x12skill_callback_ids\x18\x01 \x03(\t\x12q\n\x13skill_callback_info\x18\x02 \x03(\x0b\x32T.fetch.aea.Signing.SigningMessage.Signed_Message_Performative.SkillCallbackInfoEntry\x12G\n\x0esigned_message\x18\x03 \x01(\x0b\x32/.fetch.aea.Signing.SigningMessage.SignedMessage\x1a\x38\n\x16SkillCallbackInfoEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\x95\x02\n\x12\x45rror_Performative\x12\x1a\n\x12skill_callback_ids\x18\x01 \x03(\t\x12h\n\x13skill_callback_info\x18\x02 \x03(\x0b\x32K.fetch.aea.Signing.SigningMessage.Error_Performative.SkillCallbackInfoEntry\x12?\n\nerror_code\x18\x03 \x01(\x0b\x32+.fetch.aea.Signing.SigningMessage.ErrorCode\x1a\x38\n\x16SkillCallbackInfoEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x0e\n\x0cperformativeb\x06proto3", + serialized_pb=b"\n\rsigning.proto\x12\x11\x66\x65tch.aea.Signing\"\xa8\x0c\n\x0eSigningMessage\x12\x12\n\nmessage_id\x18\x01 \x01(\x05\x12\"\n\x1a\x64ialogue_starter_reference\x18\x02 \x01(\t\x12$\n\x1c\x64ialogue_responder_reference\x18\x03 \x01(\t\x12\x0e\n\x06target\x18\x04 \x01(\x05\x12\x45\n\x05\x65rror\x18\x05 \x01(\x0b\x32\x34.fetch.aea.Signing.SigningMessage.Error_PerformativeH\x00\x12S\n\x0csign_message\x18\x06 \x01(\x0b\x32;.fetch.aea.Signing.SigningMessage.Sign_Message_PerformativeH\x00\x12[\n\x10sign_transaction\x18\x07 \x01(\x0b\x32?.fetch.aea.Signing.SigningMessage.Sign_Transaction_PerformativeH\x00\x12W\n\x0esigned_message\x18\x08 \x01(\x0b\x32=.fetch.aea.Signing.SigningMessage.Signed_Message_PerformativeH\x00\x12_\n\x12signed_transaction\x18\t \x01(\x0b\x32\x41.fetch.aea.Signing.SigningMessage.Signed_Transaction_PerformativeH\x00\x1a\xb3\x01\n\tErrorCode\x12M\n\nerror_code\x18\x01 \x01(\x0e\x32\x39.fetch.aea.Signing.SigningMessage.ErrorCode.ErrorCodeEnum\"W\n\rErrorCodeEnum\x12 \n\x1cUNSUCCESSFUL_MESSAGE_SIGNING\x10\x00\x12$\n UNSUCCESSFUL_TRANSACTION_SIGNING\x10\x01\x1a!\n\nRawMessage\x12\x13\n\x0braw_message\x18\x01 \x01(\x0c\x1a)\n\x0eRawTransaction\x12\x17\n\x0fraw_transaction\x18\x01 \x01(\x0c\x1a'\n\rSignedMessage\x12\x16\n\x0esigned_message\x18\x01 \x01(\x0c\x1a/\n\x11SignedTransaction\x12\x1a\n\x12signed_transaction\x18\x01 \x01(\x0c\x1a\x16\n\x05Terms\x12\r\n\x05terms\x18\x01 \x01(\x0c\x1a\xa2\x01\n\x1dSign_Transaction_Performative\x12\x36\n\x05terms\x18\x01 \x01(\x0b\x32'.fetch.aea.Signing.SigningMessage.Terms\x12I\n\x0fraw_transaction\x18\x02 \x01(\x0b\x32\x30.fetch.aea.Signing.SigningMessage.RawTransaction\x1a\x96\x01\n\x19Sign_Message_Performative\x12\x36\n\x05terms\x18\x01 \x01(\x0b\x32'.fetch.aea.Signing.SigningMessage.Terms\x12\x41\n\x0braw_message\x18\x02 \x01(\x0b\x32,.fetch.aea.Signing.SigningMessage.RawMessage\x1ar\n\x1fSigned_Transaction_Performative\x12O\n\x12signed_transaction\x18\x01 \x01(\x0b\x32\x33.fetch.aea.Signing.SigningMessage.SignedTransaction\x1a\x66\n\x1bSigned_Message_Performative\x12G\n\x0esigned_message\x18\x01 \x01(\x0b\x32/.fetch.aea.Signing.SigningMessage.SignedMessage\x1aU\n\x12\x45rror_Performative\x12?\n\nerror_code\x18\x01 \x01(\x0b\x32+.fetch.aea.Signing.SigningMessage.ErrorCodeB\x0e\n\x0cperformativeb\x06proto3", ) @@ -278,62 +278,6 @@ serialized_end=972, ) -_SIGNINGMESSAGE_SIGN_TRANSACTION_PERFORMATIVE_SKILLCALLBACKINFOENTRY = _descriptor.Descriptor( - name="SkillCallbackInfoEntry", - full_name="fetch.aea.Signing.SigningMessage.Sign_Transaction_Performative.SkillCallbackInfoEntry", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="fetch.aea.Signing.SigningMessage.Sign_Transaction_Performative.SkillCallbackInfoEntry.key", - 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, - ), - _descriptor.FieldDescriptor( - name="value", - full_name="fetch.aea.Signing.SigningMessage.Sign_Transaction_Performative.SkillCallbackInfoEntry.value", - 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, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=b"8\001", - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=1284, - serialized_end=1340, -) - _SIGNINGMESSAGE_SIGN_TRANSACTION_PERFORMATIVE = _descriptor.Descriptor( name="Sign_Transaction_Performative", full_name="fetch.aea.Signing.SigningMessage.Sign_Transaction_Performative", @@ -341,47 +285,11 @@ file=DESCRIPTOR, containing_type=None, fields=[ - _descriptor.FieldDescriptor( - name="skill_callback_ids", - full_name="fetch.aea.Signing.SigningMessage.Sign_Transaction_Performative.skill_callback_ids", - index=0, - number=1, - type=9, - 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, - ), - _descriptor.FieldDescriptor( - name="skill_callback_info", - full_name="fetch.aea.Signing.SigningMessage.Sign_Transaction_Performative.skill_callback_info", - 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, - serialized_options=None, - file=DESCRIPTOR, - ), _descriptor.FieldDescriptor( name="terms", full_name="fetch.aea.Signing.SigningMessage.Sign_Transaction_Performative.terms", - index=2, - number=3, + index=0, + number=1, type=11, cpp_type=10, label=1, @@ -398,8 +306,8 @@ _descriptor.FieldDescriptor( name="raw_transaction", full_name="fetch.aea.Signing.SigningMessage.Sign_Transaction_Performative.raw_transaction", - index=3, - number=4, + index=1, + number=2, type=11, cpp_type=10, label=1, @@ -415,9 +323,7 @@ ), ], extensions=[], - nested_types=[ - _SIGNINGMESSAGE_SIGN_TRANSACTION_PERFORMATIVE_SKILLCALLBACKINFOENTRY, - ], + nested_types=[], enum_types=[], serialized_options=None, is_extendable=False, @@ -425,63 +331,7 @@ extension_ranges=[], oneofs=[], serialized_start=975, - serialized_end=1340, -) - -_SIGNINGMESSAGE_SIGN_MESSAGE_PERFORMATIVE_SKILLCALLBACKINFOENTRY = _descriptor.Descriptor( - name="SkillCallbackInfoEntry", - full_name="fetch.aea.Signing.SigningMessage.Sign_Message_Performative.SkillCallbackInfoEntry", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="fetch.aea.Signing.SigningMessage.Sign_Message_Performative.SkillCallbackInfoEntry.key", - 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, - ), - _descriptor.FieldDescriptor( - name="value", - full_name="fetch.aea.Signing.SigningMessage.Sign_Message_Performative.SkillCallbackInfoEntry.value", - 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, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=b"8\001", - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=1284, - serialized_end=1340, + serialized_end=1137, ) _SIGNINGMESSAGE_SIGN_MESSAGE_PERFORMATIVE = _descriptor.Descriptor( @@ -491,47 +341,11 @@ file=DESCRIPTOR, containing_type=None, fields=[ - _descriptor.FieldDescriptor( - name="skill_callback_ids", - full_name="fetch.aea.Signing.SigningMessage.Sign_Message_Performative.skill_callback_ids", - index=0, - number=1, - type=9, - 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, - ), - _descriptor.FieldDescriptor( - name="skill_callback_info", - full_name="fetch.aea.Signing.SigningMessage.Sign_Message_Performative.skill_callback_info", - 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, - serialized_options=None, - file=DESCRIPTOR, - ), _descriptor.FieldDescriptor( name="terms", full_name="fetch.aea.Signing.SigningMessage.Sign_Message_Performative.terms", - index=2, - number=3, + index=0, + number=1, type=11, cpp_type=10, label=1, @@ -548,8 +362,8 @@ _descriptor.FieldDescriptor( name="raw_message", full_name="fetch.aea.Signing.SigningMessage.Sign_Message_Performative.raw_message", - index=3, - number=4, + index=1, + number=2, type=11, cpp_type=10, label=1, @@ -565,71 +379,15 @@ ), ], extensions=[], - nested_types=[_SIGNINGMESSAGE_SIGN_MESSAGE_PERFORMATIVE_SKILLCALLBACKINFOENTRY,], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=1343, - serialized_end=1692, -) - -_SIGNINGMESSAGE_SIGNED_TRANSACTION_PERFORMATIVE_SKILLCALLBACKINFOENTRY = _descriptor.Descriptor( - name="SkillCallbackInfoEntry", - full_name="fetch.aea.Signing.SigningMessage.Signed_Transaction_Performative.SkillCallbackInfoEntry", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="fetch.aea.Signing.SigningMessage.Signed_Transaction_Performative.SkillCallbackInfoEntry.key", - 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, - ), - _descriptor.FieldDescriptor( - name="value", - full_name="fetch.aea.Signing.SigningMessage.Signed_Transaction_Performative.SkillCallbackInfoEntry.value", - 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, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], nested_types=[], enum_types=[], - serialized_options=b"8\001", + serialized_options=None, is_extendable=False, syntax="proto3", extension_ranges=[], oneofs=[], - serialized_start=1284, - serialized_end=1340, + serialized_start=1140, + serialized_end=1290, ) _SIGNINGMESSAGE_SIGNED_TRANSACTION_PERFORMATIVE = _descriptor.Descriptor( @@ -639,47 +397,11 @@ file=DESCRIPTOR, containing_type=None, fields=[ - _descriptor.FieldDescriptor( - name="skill_callback_ids", - full_name="fetch.aea.Signing.SigningMessage.Signed_Transaction_Performative.skill_callback_ids", - index=0, - number=1, - type=9, - 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, - ), - _descriptor.FieldDescriptor( - name="skill_callback_info", - full_name="fetch.aea.Signing.SigningMessage.Signed_Transaction_Performative.skill_callback_info", - 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, - serialized_options=None, - file=DESCRIPTOR, - ), _descriptor.FieldDescriptor( name="signed_transaction", full_name="fetch.aea.Signing.SigningMessage.Signed_Transaction_Performative.signed_transaction", - index=2, - number=3, + index=0, + number=1, type=11, cpp_type=10, label=1, @@ -695,73 +417,15 @@ ), ], extensions=[], - nested_types=[ - _SIGNINGMESSAGE_SIGNED_TRANSACTION_PERFORMATIVE_SKILLCALLBACKINFOENTRY, - ], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=1695, - serialized_end=2014, -) - -_SIGNINGMESSAGE_SIGNED_MESSAGE_PERFORMATIVE_SKILLCALLBACKINFOENTRY = _descriptor.Descriptor( - name="SkillCallbackInfoEntry", - full_name="fetch.aea.Signing.SigningMessage.Signed_Message_Performative.SkillCallbackInfoEntry", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="fetch.aea.Signing.SigningMessage.Signed_Message_Performative.SkillCallbackInfoEntry.key", - 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, - ), - _descriptor.FieldDescriptor( - name="value", - full_name="fetch.aea.Signing.SigningMessage.Signed_Message_Performative.SkillCallbackInfoEntry.value", - 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, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], nested_types=[], enum_types=[], - serialized_options=b"8\001", + serialized_options=None, is_extendable=False, syntax="proto3", extension_ranges=[], oneofs=[], - serialized_start=1284, - serialized_end=1340, + serialized_start=1292, + serialized_end=1406, ) _SIGNINGMESSAGE_SIGNED_MESSAGE_PERFORMATIVE = _descriptor.Descriptor( @@ -771,47 +435,11 @@ file=DESCRIPTOR, containing_type=None, fields=[ - _descriptor.FieldDescriptor( - name="skill_callback_ids", - full_name="fetch.aea.Signing.SigningMessage.Signed_Message_Performative.skill_callback_ids", - index=0, - number=1, - type=9, - 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, - ), - _descriptor.FieldDescriptor( - name="skill_callback_info", - full_name="fetch.aea.Signing.SigningMessage.Signed_Message_Performative.skill_callback_info", - 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, - serialized_options=None, - file=DESCRIPTOR, - ), _descriptor.FieldDescriptor( name="signed_message", full_name="fetch.aea.Signing.SigningMessage.Signed_Message_Performative.signed_message", - index=2, - number=3, + index=0, + number=1, type=11, cpp_type=10, label=1, @@ -827,71 +455,15 @@ ), ], extensions=[], - nested_types=[_SIGNINGMESSAGE_SIGNED_MESSAGE_PERFORMATIVE_SKILLCALLBACKINFOENTRY,], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=2017, - serialized_end=2320, -) - -_SIGNINGMESSAGE_ERROR_PERFORMATIVE_SKILLCALLBACKINFOENTRY = _descriptor.Descriptor( - name="SkillCallbackInfoEntry", - full_name="fetch.aea.Signing.SigningMessage.Error_Performative.SkillCallbackInfoEntry", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="fetch.aea.Signing.SigningMessage.Error_Performative.SkillCallbackInfoEntry.key", - 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, - ), - _descriptor.FieldDescriptor( - name="value", - full_name="fetch.aea.Signing.SigningMessage.Error_Performative.SkillCallbackInfoEntry.value", - 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, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], nested_types=[], enum_types=[], - serialized_options=b"8\001", + serialized_options=None, is_extendable=False, syntax="proto3", extension_ranges=[], oneofs=[], - serialized_start=1284, - serialized_end=1340, + serialized_start=1408, + serialized_end=1510, ) _SIGNINGMESSAGE_ERROR_PERFORMATIVE = _descriptor.Descriptor( @@ -901,47 +473,11 @@ file=DESCRIPTOR, containing_type=None, fields=[ - _descriptor.FieldDescriptor( - name="skill_callback_ids", - full_name="fetch.aea.Signing.SigningMessage.Error_Performative.skill_callback_ids", - index=0, - number=1, - type=9, - 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, - ), - _descriptor.FieldDescriptor( - name="skill_callback_info", - full_name="fetch.aea.Signing.SigningMessage.Error_Performative.skill_callback_info", - 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, - serialized_options=None, - file=DESCRIPTOR, - ), _descriptor.FieldDescriptor( name="error_code", full_name="fetch.aea.Signing.SigningMessage.Error_Performative.error_code", - index=2, - number=3, + index=0, + number=1, type=11, cpp_type=10, label=1, @@ -957,15 +493,15 @@ ), ], extensions=[], - nested_types=[_SIGNINGMESSAGE_ERROR_PERFORMATIVE_SKILLCALLBACKINFOENTRY,], + nested_types=[], enum_types=[], serialized_options=None, is_extendable=False, syntax="proto3", extension_ranges=[], oneofs=[], - serialized_start=2323, - serialized_end=2600, + serialized_start=1512, + serialized_end=1597, ) _SIGNINGMESSAGE = _descriptor.Descriptor( @@ -1167,7 +703,7 @@ ), ], serialized_start=37, - serialized_end=2616, + serialized_end=1613, ) _SIGNINGMESSAGE_ERRORCODE.fields_by_name[ @@ -1180,12 +716,6 @@ _SIGNINGMESSAGE_SIGNEDMESSAGE.containing_type = _SIGNINGMESSAGE _SIGNINGMESSAGE_SIGNEDTRANSACTION.containing_type = _SIGNINGMESSAGE _SIGNINGMESSAGE_TERMS.containing_type = _SIGNINGMESSAGE -_SIGNINGMESSAGE_SIGN_TRANSACTION_PERFORMATIVE_SKILLCALLBACKINFOENTRY.containing_type = ( - _SIGNINGMESSAGE_SIGN_TRANSACTION_PERFORMATIVE -) -_SIGNINGMESSAGE_SIGN_TRANSACTION_PERFORMATIVE.fields_by_name[ - "skill_callback_info" -].message_type = _SIGNINGMESSAGE_SIGN_TRANSACTION_PERFORMATIVE_SKILLCALLBACKINFOENTRY _SIGNINGMESSAGE_SIGN_TRANSACTION_PERFORMATIVE.fields_by_name[ "terms" ].message_type = _SIGNINGMESSAGE_TERMS @@ -1193,12 +723,6 @@ "raw_transaction" ].message_type = _SIGNINGMESSAGE_RAWTRANSACTION _SIGNINGMESSAGE_SIGN_TRANSACTION_PERFORMATIVE.containing_type = _SIGNINGMESSAGE -_SIGNINGMESSAGE_SIGN_MESSAGE_PERFORMATIVE_SKILLCALLBACKINFOENTRY.containing_type = ( - _SIGNINGMESSAGE_SIGN_MESSAGE_PERFORMATIVE -) -_SIGNINGMESSAGE_SIGN_MESSAGE_PERFORMATIVE.fields_by_name[ - "skill_callback_info" -].message_type = _SIGNINGMESSAGE_SIGN_MESSAGE_PERFORMATIVE_SKILLCALLBACKINFOENTRY _SIGNINGMESSAGE_SIGN_MESSAGE_PERFORMATIVE.fields_by_name[ "terms" ].message_type = _SIGNINGMESSAGE_TERMS @@ -1206,32 +730,14 @@ "raw_message" ].message_type = _SIGNINGMESSAGE_RAWMESSAGE _SIGNINGMESSAGE_SIGN_MESSAGE_PERFORMATIVE.containing_type = _SIGNINGMESSAGE -_SIGNINGMESSAGE_SIGNED_TRANSACTION_PERFORMATIVE_SKILLCALLBACKINFOENTRY.containing_type = ( - _SIGNINGMESSAGE_SIGNED_TRANSACTION_PERFORMATIVE -) -_SIGNINGMESSAGE_SIGNED_TRANSACTION_PERFORMATIVE.fields_by_name[ - "skill_callback_info" -].message_type = _SIGNINGMESSAGE_SIGNED_TRANSACTION_PERFORMATIVE_SKILLCALLBACKINFOENTRY _SIGNINGMESSAGE_SIGNED_TRANSACTION_PERFORMATIVE.fields_by_name[ "signed_transaction" ].message_type = _SIGNINGMESSAGE_SIGNEDTRANSACTION _SIGNINGMESSAGE_SIGNED_TRANSACTION_PERFORMATIVE.containing_type = _SIGNINGMESSAGE -_SIGNINGMESSAGE_SIGNED_MESSAGE_PERFORMATIVE_SKILLCALLBACKINFOENTRY.containing_type = ( - _SIGNINGMESSAGE_SIGNED_MESSAGE_PERFORMATIVE -) -_SIGNINGMESSAGE_SIGNED_MESSAGE_PERFORMATIVE.fields_by_name[ - "skill_callback_info" -].message_type = _SIGNINGMESSAGE_SIGNED_MESSAGE_PERFORMATIVE_SKILLCALLBACKINFOENTRY _SIGNINGMESSAGE_SIGNED_MESSAGE_PERFORMATIVE.fields_by_name[ "signed_message" ].message_type = _SIGNINGMESSAGE_SIGNEDMESSAGE _SIGNINGMESSAGE_SIGNED_MESSAGE_PERFORMATIVE.containing_type = _SIGNINGMESSAGE -_SIGNINGMESSAGE_ERROR_PERFORMATIVE_SKILLCALLBACKINFOENTRY.containing_type = ( - _SIGNINGMESSAGE_ERROR_PERFORMATIVE -) -_SIGNINGMESSAGE_ERROR_PERFORMATIVE.fields_by_name[ - "skill_callback_info" -].message_type = _SIGNINGMESSAGE_ERROR_PERFORMATIVE_SKILLCALLBACKINFOENTRY _SIGNINGMESSAGE_ERROR_PERFORMATIVE.fields_by_name[ "error_code" ].message_type = _SIGNINGMESSAGE_ERRORCODE @@ -1346,15 +852,6 @@ "Sign_Transaction_Performative", (_message.Message,), { - "SkillCallbackInfoEntry": _reflection.GeneratedProtocolMessageType( - "SkillCallbackInfoEntry", - (_message.Message,), - { - "DESCRIPTOR": _SIGNINGMESSAGE_SIGN_TRANSACTION_PERFORMATIVE_SKILLCALLBACKINFOENTRY, - "__module__": "signing_pb2" - # @@protoc_insertion_point(class_scope:fetch.aea.Signing.SigningMessage.Sign_Transaction_Performative.SkillCallbackInfoEntry) - }, - ), "DESCRIPTOR": _SIGNINGMESSAGE_SIGN_TRANSACTION_PERFORMATIVE, "__module__": "signing_pb2" # @@protoc_insertion_point(class_scope:fetch.aea.Signing.SigningMessage.Sign_Transaction_Performative) @@ -1364,15 +861,6 @@ "Sign_Message_Performative", (_message.Message,), { - "SkillCallbackInfoEntry": _reflection.GeneratedProtocolMessageType( - "SkillCallbackInfoEntry", - (_message.Message,), - { - "DESCRIPTOR": _SIGNINGMESSAGE_SIGN_MESSAGE_PERFORMATIVE_SKILLCALLBACKINFOENTRY, - "__module__": "signing_pb2" - # @@protoc_insertion_point(class_scope:fetch.aea.Signing.SigningMessage.Sign_Message_Performative.SkillCallbackInfoEntry) - }, - ), "DESCRIPTOR": _SIGNINGMESSAGE_SIGN_MESSAGE_PERFORMATIVE, "__module__": "signing_pb2" # @@protoc_insertion_point(class_scope:fetch.aea.Signing.SigningMessage.Sign_Message_Performative) @@ -1382,15 +870,6 @@ "Signed_Transaction_Performative", (_message.Message,), { - "SkillCallbackInfoEntry": _reflection.GeneratedProtocolMessageType( - "SkillCallbackInfoEntry", - (_message.Message,), - { - "DESCRIPTOR": _SIGNINGMESSAGE_SIGNED_TRANSACTION_PERFORMATIVE_SKILLCALLBACKINFOENTRY, - "__module__": "signing_pb2" - # @@protoc_insertion_point(class_scope:fetch.aea.Signing.SigningMessage.Signed_Transaction_Performative.SkillCallbackInfoEntry) - }, - ), "DESCRIPTOR": _SIGNINGMESSAGE_SIGNED_TRANSACTION_PERFORMATIVE, "__module__": "signing_pb2" # @@protoc_insertion_point(class_scope:fetch.aea.Signing.SigningMessage.Signed_Transaction_Performative) @@ -1400,15 +879,6 @@ "Signed_Message_Performative", (_message.Message,), { - "SkillCallbackInfoEntry": _reflection.GeneratedProtocolMessageType( - "SkillCallbackInfoEntry", - (_message.Message,), - { - "DESCRIPTOR": _SIGNINGMESSAGE_SIGNED_MESSAGE_PERFORMATIVE_SKILLCALLBACKINFOENTRY, - "__module__": "signing_pb2" - # @@protoc_insertion_point(class_scope:fetch.aea.Signing.SigningMessage.Signed_Message_Performative.SkillCallbackInfoEntry) - }, - ), "DESCRIPTOR": _SIGNINGMESSAGE_SIGNED_MESSAGE_PERFORMATIVE, "__module__": "signing_pb2" # @@protoc_insertion_point(class_scope:fetch.aea.Signing.SigningMessage.Signed_Message_Performative) @@ -1418,15 +888,6 @@ "Error_Performative", (_message.Message,), { - "SkillCallbackInfoEntry": _reflection.GeneratedProtocolMessageType( - "SkillCallbackInfoEntry", - (_message.Message,), - { - "DESCRIPTOR": _SIGNINGMESSAGE_ERROR_PERFORMATIVE_SKILLCALLBACKINFOENTRY, - "__module__": "signing_pb2" - # @@protoc_insertion_point(class_scope:fetch.aea.Signing.SigningMessage.Error_Performative.SkillCallbackInfoEntry) - }, - ), "DESCRIPTOR": _SIGNINGMESSAGE_ERROR_PERFORMATIVE, "__module__": "signing_pb2" # @@protoc_insertion_point(class_scope:fetch.aea.Signing.SigningMessage.Error_Performative) @@ -1445,26 +906,10 @@ _sym_db.RegisterMessage(SigningMessage.SignedTransaction) _sym_db.RegisterMessage(SigningMessage.Terms) _sym_db.RegisterMessage(SigningMessage.Sign_Transaction_Performative) -_sym_db.RegisterMessage( - SigningMessage.Sign_Transaction_Performative.SkillCallbackInfoEntry -) _sym_db.RegisterMessage(SigningMessage.Sign_Message_Performative) -_sym_db.RegisterMessage(SigningMessage.Sign_Message_Performative.SkillCallbackInfoEntry) _sym_db.RegisterMessage(SigningMessage.Signed_Transaction_Performative) -_sym_db.RegisterMessage( - SigningMessage.Signed_Transaction_Performative.SkillCallbackInfoEntry -) _sym_db.RegisterMessage(SigningMessage.Signed_Message_Performative) -_sym_db.RegisterMessage( - SigningMessage.Signed_Message_Performative.SkillCallbackInfoEntry -) _sym_db.RegisterMessage(SigningMessage.Error_Performative) -_sym_db.RegisterMessage(SigningMessage.Error_Performative.SkillCallbackInfoEntry) -_SIGNINGMESSAGE_SIGN_TRANSACTION_PERFORMATIVE_SKILLCALLBACKINFOENTRY._options = None -_SIGNINGMESSAGE_SIGN_MESSAGE_PERFORMATIVE_SKILLCALLBACKINFOENTRY._options = None -_SIGNINGMESSAGE_SIGNED_TRANSACTION_PERFORMATIVE_SKILLCALLBACKINFOENTRY._options = None -_SIGNINGMESSAGE_SIGNED_MESSAGE_PERFORMATIVE_SKILLCALLBACKINFOENTRY._options = None -_SIGNINGMESSAGE_ERROR_PERFORMATIVE_SKILLCALLBACKINFOENTRY._options = None # @@protoc_insertion_point(module_scope) diff --git a/aea/protocols/state_update/README.md b/aea/protocols/state_update/README.md index 4caf292f7f..3ba3eedfd7 100644 --- a/aea/protocols/state_update/README.md +++ b/aea/protocols/state_update/README.md @@ -1,15 +1,5 @@ # State Update Protocol -**Name:** state_update - -**Author**: fetchai - -**Version**: 0.2.0 - -**Short Description**: A protocol for state updates to the decision maker state. - -**License**: Apache-2.0 - ## Description This is a protocol for updating the state of a decision maker. @@ -20,10 +10,10 @@ This is a protocol for updating the state of a decision maker. --- name: state_update author: fetchai -version: 0.2.0 +version: 0.3.0 description: A protocol for state updates to the decision maker state. license: Apache-2.0 -aea_version: '>=0.5.0, <0.6.0' +aea_version: '>=0.6.0, <0.7.0' speech_acts: initialize: exchange_params_by_currency_id: pt:dict[pt:str, pt:float] diff --git a/aea/protocols/state_update/dialogues.py b/aea/protocols/state_update/dialogues.py index ad8d959dad..53b5fdebda 100644 --- a/aea/protocols/state_update/dialogues.py +++ b/aea/protocols/state_update/dialogues.py @@ -25,11 +25,11 @@ """ from abc import ABC -from typing import Dict, FrozenSet, Optional, cast +from typing import Callable, FrozenSet, Type, cast -from aea.helpers.dialogue.base import Dialogue, DialogueLabel, Dialogues -from aea.mail.base import Address +from aea.common import Address from aea.protocols.base import Message +from aea.protocols.dialogue.base import Dialogue, DialogueLabel, Dialogues from aea.protocols.state_update.message import StateUpdateMessage @@ -61,75 +61,49 @@ class EndState(Dialogue.EndState): def __init__( self, dialogue_label: DialogueLabel, - agent_address: Optional[Address] = None, - role: Optional[Dialogue.Role] = None, + self_address: Address, + role: Dialogue.Role, + message_class: Type[StateUpdateMessage] = StateUpdateMessage, ) -> None: """ Initialize a dialogue. :param dialogue_label: the identifier of the dialogue - :param agent_address: the address of the agent for whom this dialogue is maintained + :param self_address: the address of the entity for whom this dialogue is maintained :param role: the role of the agent this dialogue is maintained for :return: None """ Dialogue.__init__( self, dialogue_label=dialogue_label, - agent_address=agent_address, + message_class=message_class, + self_address=self_address, role=role, - rules=Dialogue.Rules( - cast(FrozenSet[Message.Performative], self.INITIAL_PERFORMATIVES), - cast(FrozenSet[Message.Performative], self.TERMINAL_PERFORMATIVES), - cast( - Dict[Message.Performative, FrozenSet[Message.Performative]], - self.VALID_REPLIES, - ), - ), ) - def is_valid(self, message: Message) -> bool: - """ - Check whether 'message' is a valid next message in the dialogue. - - These rules capture specific constraints designed for dialogues which are instances of a concrete sub-class of this class. - Override this method with your additional dialogue rules. - - :param message: the message to be validated - :return: True if valid, False otherwise - """ - return True - class StateUpdateDialogues(Dialogues, ABC): """This class keeps track of all state_update dialogues.""" END_STATES = frozenset({StateUpdateDialogue.EndState.SUCCESSFUL}) - def __init__(self, agent_address: Address) -> None: + def __init__( + self, + self_address: Address, + role_from_first_message: Callable[[Message, Address], Dialogue.Role], + dialogue_class: Type[StateUpdateDialogue] = StateUpdateDialogue, + ) -> None: """ Initialize dialogues. - :param agent_address: the address of the agent for whom dialogues are maintained + :param self_address: the address of the entity for whom dialogues are maintained :return: None """ Dialogues.__init__( self, - agent_address=agent_address, + self_address=self_address, end_states=cast(FrozenSet[Dialogue.EndState], self.END_STATES), + message_class=StateUpdateMessage, + dialogue_class=dialogue_class, + role_from_first_message=role_from_first_message, ) - - def create_dialogue( - self, dialogue_label: DialogueLabel, role: Dialogue.Role, - ) -> StateUpdateDialogue: - """ - Create an instance of state_update dialogue. - - :param dialogue_label: the identifier of the dialogue - :param role: the role of the agent this dialogue is maintained for - - :return: the created dialogue - """ - dialogue = StateUpdateDialogue( - dialogue_label=dialogue_label, agent_address=self.agent_address, role=role - ) - return dialogue diff --git a/aea/protocols/state_update/message.py b/aea/protocols/state_update/message.py index cd11ba9a48..2444c40c0f 100644 --- a/aea/protocols/state_update/message.py +++ b/aea/protocols/state_update/message.py @@ -20,10 +20,10 @@ """This module contains state_update's message definition.""" import logging -from enum import Enum from typing import Dict, Set, Tuple, cast from aea.configurations.base import ProtocolId +from aea.exceptions import AEAEnforceError, enforce from aea.protocols.base import Message logger = logging.getLogger("aea.packages.fetchai.protocols.state_update.message") @@ -34,9 +34,9 @@ class StateUpdateMessage(Message): """A protocol for state updates to the decision maker state.""" - protocol_id = ProtocolId.from_str("fetchai/state_update:0.2.0") + protocol_id = ProtocolId.from_str("fetchai/state_update:0.3.0") - class Performative(Enum): + class Performative(Message.Performative): """Performatives for the state_update protocol.""" APPLY = "apply" @@ -62,6 +62,7 @@ def __init__( :param target: the message target. :param performative: the message performative. """ + self._performatives = {"apply", "initialize"} super().__init__( dialogue_reference=dialogue_reference, message_id=message_id, @@ -69,7 +70,6 @@ def __init__( performative=StateUpdateMessage.Performative(performative), **kwargs, ) - self._performatives = {"apply", "initialize"} @property def valid_performatives(self) -> Set[str]: @@ -79,94 +79,104 @@ def valid_performatives(self) -> Set[str]: @property def dialogue_reference(self) -> Tuple[str, str]: """Get the dialogue_reference of the message.""" - assert self.is_set("dialogue_reference"), "dialogue_reference is not set." + enforce(self.is_set("dialogue_reference"), "dialogue_reference is not set.") return cast(Tuple[str, str], self.get("dialogue_reference")) @property def message_id(self) -> int: """Get the message_id of the message.""" - assert self.is_set("message_id"), "message_id is not set." + enforce(self.is_set("message_id"), "message_id is not set.") return cast(int, self.get("message_id")) @property def performative(self) -> Performative: # type: ignore # noqa: F821 """Get the performative of the message.""" - assert self.is_set("performative"), "performative is not set." + enforce(self.is_set("performative"), "performative is not set.") return cast(StateUpdateMessage.Performative, self.get("performative")) @property def target(self) -> int: """Get the target of the message.""" - assert self.is_set("target"), "target is not set." + enforce(self.is_set("target"), "target is not set.") return cast(int, self.get("target")) @property def amount_by_currency_id(self) -> Dict[str, int]: """Get the 'amount_by_currency_id' content from the message.""" - assert self.is_set( - "amount_by_currency_id" - ), "'amount_by_currency_id' content is not set." + enforce( + self.is_set("amount_by_currency_id"), + "'amount_by_currency_id' content is not set.", + ) return cast(Dict[str, int], self.get("amount_by_currency_id")) @property def exchange_params_by_currency_id(self) -> Dict[str, float]: """Get the 'exchange_params_by_currency_id' content from the message.""" - assert self.is_set( - "exchange_params_by_currency_id" - ), "'exchange_params_by_currency_id' content is not set." + enforce( + self.is_set("exchange_params_by_currency_id"), + "'exchange_params_by_currency_id' content is not set.", + ) return cast(Dict[str, float], self.get("exchange_params_by_currency_id")) @property def quantities_by_good_id(self) -> Dict[str, int]: """Get the 'quantities_by_good_id' content from the message.""" - assert self.is_set( - "quantities_by_good_id" - ), "'quantities_by_good_id' content is not set." + enforce( + self.is_set("quantities_by_good_id"), + "'quantities_by_good_id' content is not set.", + ) return cast(Dict[str, int], self.get("quantities_by_good_id")) @property def utility_params_by_good_id(self) -> Dict[str, float]: """Get the 'utility_params_by_good_id' content from the message.""" - assert self.is_set( - "utility_params_by_good_id" - ), "'utility_params_by_good_id' content is not set." + enforce( + self.is_set("utility_params_by_good_id"), + "'utility_params_by_good_id' content is not set.", + ) return cast(Dict[str, float], self.get("utility_params_by_good_id")) def _is_consistent(self) -> bool: """Check that the message follows the state_update protocol.""" try: - assert ( - type(self.dialogue_reference) == tuple - ), "Invalid type for 'dialogue_reference'. Expected 'tuple'. Found '{}'.".format( - type(self.dialogue_reference) + enforce( + type(self.dialogue_reference) == tuple, + "Invalid type for 'dialogue_reference'. Expected 'tuple'. Found '{}'.".format( + type(self.dialogue_reference) + ), ) - assert ( - type(self.dialogue_reference[0]) == str - ), "Invalid type for 'dialogue_reference[0]'. Expected 'str'. Found '{}'.".format( - type(self.dialogue_reference[0]) + enforce( + type(self.dialogue_reference[0]) == str, + "Invalid type for 'dialogue_reference[0]'. Expected 'str'. Found '{}'.".format( + type(self.dialogue_reference[0]) + ), ) - assert ( - type(self.dialogue_reference[1]) == str - ), "Invalid type for 'dialogue_reference[1]'. Expected 'str'. Found '{}'.".format( - type(self.dialogue_reference[1]) + enforce( + type(self.dialogue_reference[1]) == str, + "Invalid type for 'dialogue_reference[1]'. Expected 'str'. Found '{}'.".format( + type(self.dialogue_reference[1]) + ), ) - assert ( - type(self.message_id) == int - ), "Invalid type for 'message_id'. Expected 'int'. Found '{}'.".format( - type(self.message_id) + enforce( + type(self.message_id) == int, + "Invalid type for 'message_id'. Expected 'int'. Found '{}'.".format( + type(self.message_id) + ), ) - assert ( - type(self.target) == int - ), "Invalid type for 'target'. Expected 'int'. Found '{}'.".format( - type(self.target) + enforce( + type(self.target) == int, + "Invalid type for 'target'. Expected 'int'. Found '{}'.".format( + type(self.target) + ), ) # Light Protocol Rule 2 # Check correct performative - assert ( - type(self.performative) == StateUpdateMessage.Performative - ), "Invalid 'performative'. Expected either of '{}'. Found '{}'.".format( - self.valid_performatives, self.performative + enforce( + type(self.performative) == StateUpdateMessage.Performative, + "Invalid 'performative'. Expected either of '{}'. Found '{}'.".format( + self.valid_performatives, self.performative + ), ) # Check correct contents @@ -174,144 +184,165 @@ def _is_consistent(self) -> bool: expected_nb_of_contents = 0 if self.performative == StateUpdateMessage.Performative.INITIALIZE: expected_nb_of_contents = 4 - assert ( - type(self.exchange_params_by_currency_id) == dict - ), "Invalid type for content 'exchange_params_by_currency_id'. Expected 'dict'. Found '{}'.".format( - type(self.exchange_params_by_currency_id) + enforce( + type(self.exchange_params_by_currency_id) == dict, + "Invalid type for content 'exchange_params_by_currency_id'. Expected 'dict'. Found '{}'.".format( + type(self.exchange_params_by_currency_id) + ), ) for ( key_of_exchange_params_by_currency_id, value_of_exchange_params_by_currency_id, ) in self.exchange_params_by_currency_id.items(): - assert ( - type(key_of_exchange_params_by_currency_id) == str - ), "Invalid type for dictionary keys in content 'exchange_params_by_currency_id'. Expected 'str'. Found '{}'.".format( - type(key_of_exchange_params_by_currency_id) + enforce( + type(key_of_exchange_params_by_currency_id) == str, + "Invalid type for dictionary keys in content 'exchange_params_by_currency_id'. Expected 'str'. Found '{}'.".format( + type(key_of_exchange_params_by_currency_id) + ), ) - assert ( - type(value_of_exchange_params_by_currency_id) == float - ), "Invalid type for dictionary values in content 'exchange_params_by_currency_id'. Expected 'float'. Found '{}'.".format( - type(value_of_exchange_params_by_currency_id) + enforce( + type(value_of_exchange_params_by_currency_id) == float, + "Invalid type for dictionary values in content 'exchange_params_by_currency_id'. Expected 'float'. Found '{}'.".format( + type(value_of_exchange_params_by_currency_id) + ), ) - assert ( - type(self.utility_params_by_good_id) == dict - ), "Invalid type for content 'utility_params_by_good_id'. Expected 'dict'. Found '{}'.".format( - type(self.utility_params_by_good_id) + enforce( + type(self.utility_params_by_good_id) == dict, + "Invalid type for content 'utility_params_by_good_id'. Expected 'dict'. Found '{}'.".format( + type(self.utility_params_by_good_id) + ), ) for ( key_of_utility_params_by_good_id, value_of_utility_params_by_good_id, ) in self.utility_params_by_good_id.items(): - assert ( - type(key_of_utility_params_by_good_id) == str - ), "Invalid type for dictionary keys in content 'utility_params_by_good_id'. Expected 'str'. Found '{}'.".format( - type(key_of_utility_params_by_good_id) + enforce( + type(key_of_utility_params_by_good_id) == str, + "Invalid type for dictionary keys in content 'utility_params_by_good_id'. Expected 'str'. Found '{}'.".format( + type(key_of_utility_params_by_good_id) + ), ) - assert ( - type(value_of_utility_params_by_good_id) == float - ), "Invalid type for dictionary values in content 'utility_params_by_good_id'. Expected 'float'. Found '{}'.".format( - type(value_of_utility_params_by_good_id) + enforce( + type(value_of_utility_params_by_good_id) == float, + "Invalid type for dictionary values in content 'utility_params_by_good_id'. Expected 'float'. Found '{}'.".format( + type(value_of_utility_params_by_good_id) + ), ) - assert ( - type(self.amount_by_currency_id) == dict - ), "Invalid type for content 'amount_by_currency_id'. Expected 'dict'. Found '{}'.".format( - type(self.amount_by_currency_id) + enforce( + type(self.amount_by_currency_id) == dict, + "Invalid type for content 'amount_by_currency_id'. Expected 'dict'. Found '{}'.".format( + type(self.amount_by_currency_id) + ), ) for ( key_of_amount_by_currency_id, value_of_amount_by_currency_id, ) in self.amount_by_currency_id.items(): - assert ( - type(key_of_amount_by_currency_id) == str - ), "Invalid type for dictionary keys in content 'amount_by_currency_id'. Expected 'str'. Found '{}'.".format( - type(key_of_amount_by_currency_id) + enforce( + type(key_of_amount_by_currency_id) == str, + "Invalid type for dictionary keys in content 'amount_by_currency_id'. Expected 'str'. Found '{}'.".format( + type(key_of_amount_by_currency_id) + ), ) - assert ( - type(value_of_amount_by_currency_id) == int - ), "Invalid type for dictionary values in content 'amount_by_currency_id'. Expected 'int'. Found '{}'.".format( - type(value_of_amount_by_currency_id) + enforce( + type(value_of_amount_by_currency_id) == int, + "Invalid type for dictionary values in content 'amount_by_currency_id'. Expected 'int'. Found '{}'.".format( + type(value_of_amount_by_currency_id) + ), ) - assert ( - type(self.quantities_by_good_id) == dict - ), "Invalid type for content 'quantities_by_good_id'. Expected 'dict'. Found '{}'.".format( - type(self.quantities_by_good_id) + enforce( + type(self.quantities_by_good_id) == dict, + "Invalid type for content 'quantities_by_good_id'. Expected 'dict'. Found '{}'.".format( + type(self.quantities_by_good_id) + ), ) for ( key_of_quantities_by_good_id, value_of_quantities_by_good_id, ) in self.quantities_by_good_id.items(): - assert ( - type(key_of_quantities_by_good_id) == str - ), "Invalid type for dictionary keys in content 'quantities_by_good_id'. Expected 'str'. Found '{}'.".format( - type(key_of_quantities_by_good_id) + enforce( + type(key_of_quantities_by_good_id) == str, + "Invalid type for dictionary keys in content 'quantities_by_good_id'. Expected 'str'. Found '{}'.".format( + type(key_of_quantities_by_good_id) + ), ) - assert ( - type(value_of_quantities_by_good_id) == int - ), "Invalid type for dictionary values in content 'quantities_by_good_id'. Expected 'int'. Found '{}'.".format( - type(value_of_quantities_by_good_id) + enforce( + type(value_of_quantities_by_good_id) == int, + "Invalid type for dictionary values in content 'quantities_by_good_id'. Expected 'int'. Found '{}'.".format( + type(value_of_quantities_by_good_id) + ), ) elif self.performative == StateUpdateMessage.Performative.APPLY: expected_nb_of_contents = 2 - assert ( - type(self.amount_by_currency_id) == dict - ), "Invalid type for content 'amount_by_currency_id'. Expected 'dict'. Found '{}'.".format( - type(self.amount_by_currency_id) + enforce( + type(self.amount_by_currency_id) == dict, + "Invalid type for content 'amount_by_currency_id'. Expected 'dict'. Found '{}'.".format( + type(self.amount_by_currency_id) + ), ) for ( key_of_amount_by_currency_id, value_of_amount_by_currency_id, ) in self.amount_by_currency_id.items(): - assert ( - type(key_of_amount_by_currency_id) == str - ), "Invalid type for dictionary keys in content 'amount_by_currency_id'. Expected 'str'. Found '{}'.".format( - type(key_of_amount_by_currency_id) + enforce( + type(key_of_amount_by_currency_id) == str, + "Invalid type for dictionary keys in content 'amount_by_currency_id'. Expected 'str'. Found '{}'.".format( + type(key_of_amount_by_currency_id) + ), ) - assert ( - type(value_of_amount_by_currency_id) == int - ), "Invalid type for dictionary values in content 'amount_by_currency_id'. Expected 'int'. Found '{}'.".format( - type(value_of_amount_by_currency_id) + enforce( + type(value_of_amount_by_currency_id) == int, + "Invalid type for dictionary values in content 'amount_by_currency_id'. Expected 'int'. Found '{}'.".format( + type(value_of_amount_by_currency_id) + ), ) - assert ( - type(self.quantities_by_good_id) == dict - ), "Invalid type for content 'quantities_by_good_id'. Expected 'dict'. Found '{}'.".format( - type(self.quantities_by_good_id) + enforce( + type(self.quantities_by_good_id) == dict, + "Invalid type for content 'quantities_by_good_id'. Expected 'dict'. Found '{}'.".format( + type(self.quantities_by_good_id) + ), ) for ( key_of_quantities_by_good_id, value_of_quantities_by_good_id, ) in self.quantities_by_good_id.items(): - assert ( - type(key_of_quantities_by_good_id) == str - ), "Invalid type for dictionary keys in content 'quantities_by_good_id'. Expected 'str'. Found '{}'.".format( - type(key_of_quantities_by_good_id) + enforce( + type(key_of_quantities_by_good_id) == str, + "Invalid type for dictionary keys in content 'quantities_by_good_id'. Expected 'str'. Found '{}'.".format( + type(key_of_quantities_by_good_id) + ), ) - assert ( - type(value_of_quantities_by_good_id) == int - ), "Invalid type for dictionary values in content 'quantities_by_good_id'. Expected 'int'. Found '{}'.".format( - type(value_of_quantities_by_good_id) + enforce( + type(value_of_quantities_by_good_id) == int, + "Invalid type for dictionary values in content 'quantities_by_good_id'. Expected 'int'. Found '{}'.".format( + type(value_of_quantities_by_good_id) + ), ) # Check correct content count - assert ( - expected_nb_of_contents == actual_nb_of_contents - ), "Incorrect number of contents. Expected {}. Found {}".format( - expected_nb_of_contents, actual_nb_of_contents + enforce( + expected_nb_of_contents == actual_nb_of_contents, + "Incorrect number of contents. Expected {}. Found {}".format( + expected_nb_of_contents, actual_nb_of_contents + ), ) # Light Protocol Rule 3 if self.message_id == 1: - assert ( - self.target == 0 - ), "Invalid 'target'. Expected 0 (because 'message_id' is 1). Found {}.".format( - self.target + enforce( + self.target == 0, + "Invalid 'target'. Expected 0 (because 'message_id' is 1). Found {}.".format( + self.target + ), ) else: - assert ( - 0 < self.target < self.message_id - ), "Invalid 'target'. Expected an integer between 1 and {} inclusive. Found {}.".format( - self.message_id - 1, self.target, + enforce( + 0 < self.target < self.message_id, + "Invalid 'target'. Expected an integer between 1 and {} inclusive. Found {}.".format( + self.message_id - 1, self.target, + ), ) - except (AssertionError, ValueError, KeyError) as e: + except (AEAEnforceError, ValueError, KeyError) as e: logger.error(str(e)) return False diff --git a/aea/protocols/state_update/protocol.yaml b/aea/protocols/state_update/protocol.yaml index 34dd71115a..99162fad2e 100644 --- a/aea/protocols/state_update/protocol.yaml +++ b/aea/protocols/state_update/protocol.yaml @@ -1,14 +1,15 @@ name: state_update author: fetchai -version: 0.2.0 +version: 0.3.0 +type: protocol description: A protocol for state updates to the decision maker state. license: Apache-2.0 -aea_version: '>=0.5.0, <0.6.0' +aea_version: '>=0.6.0, <0.7.0' fingerprint: - README.md: QmQ2sQno2YXxkHx9copJsb9wwhwfryz3oJmnXnarP4Pcsb + README.md: Qmc12hnCshAE3TL9ba4vo6L8ZZhynyfhEUoStJggRrbimc __init__.py: Qma2opyN54gwTpkVV1E14jjeMmMfoqgE6XMM9LsvGuTdkm - dialogues.py: QmUo2zDoSShCy6dY6HWR1i2yb7rfBSSyHK1xpAp1WmMpLT - message.py: QmZo4tX6fjyJmcRezrVC8EQ882iV1y3sm2RyWK6ByhTUdY + dialogues.py: Qmd59WgpFccLn1zhpLdwm3zDCmCsjSoQXVn6M7PgFwwkgR + message.py: QmbXbxXbu1vzrCjzDy6qDtEvssmiHKFLWGgCEenrE4TPZW serialization.py: QmQDdbN4pgfdL1LUhV4J7xMUhdqUJ2Tamz7Nheca3yGw2G state_update.proto: QmdmEUSa7PDxJ98ZmGE7bLFPmUJv8refgbkHPejw6uDdwD state_update_pb2.py: QmQr5KXhapRv9AnfQe7Xbr5bBqYWp9DEMLjxX8UWmK75Z4 diff --git a/aea/registries/base.py b/aea/registries/base.py index 673daa188f..60fce0f86f 100644 --- a/aea/registries/base.py +++ b/aea/registries/base.py @@ -120,12 +120,12 @@ def __init__(self) -> None: self._components_by_type: Dict[ComponentType, Dict[PublicId, Component]] = {} self._registered_keys: Set[ComponentId] = set() - def register( + def register( # pylint: disable=arguments-differ,unused-argument self, component_id: ComponentId, component: Component, is_dynamically_added: bool = False, - ) -> None: # pylint: disable=arguments-differ + ) -> None: """ Register a component. @@ -174,9 +174,9 @@ def _unregister(self, component_id: ComponentId) -> None: "Component '{}' has been removed.".format(item.component_id) ) - def unregister( + def unregister( # pylint: disable=arguments-differ self, component_id: ComponentId - ) -> None: # pylint: disable=arguments-differ + ) -> None: """ Unregister a component. @@ -188,9 +188,9 @@ def unregister( ) self._unregister(component_id) - def fetch( + def fetch( # pylint: disable=arguments-differ self, component_id: ComponentId - ) -> Optional[Component]: # pylint: disable=arguments-differ + ) -> Optional[Component]: """ Fetch the component by id. diff --git a/aea/registries/filter.py b/aea/registries/filter.py index d362568890..5ceaff0d3a 100644 --- a/aea/registries/filter.py +++ b/aea/registries/filter.py @@ -16,21 +16,17 @@ # limitations under the License. # # ------------------------------------------------------------------------------ - """This module contains registries.""" -import copy import logging -import queue -from queue import Queue -from typing import List, Optional, cast +from typing import List, Optional from aea.configurations.base import ( PublicId, SkillId, ) +from aea.helpers.async_friendly_queue import AsyncFriendlyQueue from aea.protocols.base import Message -from aea.protocols.signing.message import SigningMessage from aea.registries.resources import Resources from aea.skills.base import Behaviour, Handler @@ -40,7 +36,9 @@ class Filter: """This class implements the filter of an AEA.""" - def __init__(self, resources: Resources, decision_maker_out_queue: Queue): + def __init__( + self, resources: Resources, decision_maker_out_queue: AsyncFriendlyQueue + ): """ Instantiate the filter. @@ -56,7 +54,7 @@ def resources(self) -> Resources: return self._resources @property - def decision_maker_out_queue(self) -> Queue: + def decision_maker_out_queue(self) -> AsyncFriendlyQueue: """Get decision maker (out) queue.""" return self._decision_maker_out_queue @@ -94,40 +92,25 @@ def get_active_behaviours(self) -> List[Behaviour]: ) return active_behaviour - def handle_internal_messages(self) -> None: + def handle_new_handlers_and_behaviours(self) -> None: """ Handle the messages from the decision maker. :return: None """ - self._handle_decision_maker_out_queue() - # get new behaviours and handlers from the agent skills self._handle_new_behaviours() self._handle_new_handlers() - def _handle_decision_maker_out_queue(self) -> None: - """Process descision maker's messages.""" - while not self.decision_maker_out_queue.empty(): - try: - internal_message = ( - self.decision_maker_out_queue.get_nowait() - ) # type: Optional[Message] - self._process_internal_message(internal_message) - except queue.Empty: - logger.warning("The decision maker out queue is unexpectedly empty.") - continue - - def _process_internal_message(self, internal_message: Optional[Message]) -> None: + async def get_internal_message(self) -> Optional[Message]: + """Get a message from decision_maker_out_queue.""" + return await self.decision_maker_out_queue.async_get() + + def handle_internal_message(self, internal_message: Optional[Message]) -> None: + """Handlle internal message.""" if internal_message is None: logger.warning("Got 'None' while processing internal messages.") - elif isinstance( - internal_message, SigningMessage - ): # TODO: remove; all messages allowed - internal_message = cast(SigningMessage, internal_message) - self._handle_signing_message(internal_message) - else: - # TODO: is it expected unknown data type here? - logger.warning("Cannot handle a {} message.".format(type(internal_message))) + return + self._handle_internal_message(internal_message) def _handle_new_behaviours(self) -> None: """Register new behaviours added to skills.""" @@ -161,31 +144,22 @@ def _handle_new_handlers(self) -> None: "Error when trying to add a new handler: {}".format(str(e)) ) - def _handle_signing_message(self, signing_message: SigningMessage): - """Handle transaction message from the Decision Maker.""" - skill_callback_ids = [ - PublicId.from_str(skill_id) - for skill_id in signing_message.skill_callback_ids - ] - for skill_id in skill_callback_ids: - handler = self.resources.handler_registry.fetch_by_protocol_and_skill( - signing_message.protocol_id, - skill_id, # TODO: route based on component id specified on message + def _handle_internal_message(self, message: Message): + """Handle message from the Decision Maker.""" + try: + skill_id = PublicId.from_str(message.to) + except ValueError: + logger.warning("Invalid public id as destination={}".format(message.to)) + return + handler = self.resources.handler_registry.fetch_by_protocol_and_skill( + message.protocol_id, skill_id, + ) + if handler is not None: + logger.debug( + "Calling handler {} of skill {}".format(type(handler), skill_id) + ) + handler.handle(message) + else: + logger.warning( + "No internal handler fetched for skill_id={}".format(skill_id) ) - if handler is not None: - logger.debug( - "Calling handler {} of skill {}".format(type(handler), skill_id) - ) - # TODO: remove next three lines - copy_signing_message = copy.copy( - signing_message - ) # we do a shallow copy as we only need the message object to be copied; not its referenced objects - copy_signing_message.counterparty = signing_message.sender - copy_signing_message.sender = signing_message.sender - # copy_signing_message.to = signing_message.to - copy_signing_message.is_incoming = True - handler.handle(cast(Message, copy_signing_message)) - else: - logger.warning( - "No internal handler fetched for skill_id={}".format(skill_id) - ) diff --git a/aea/runtime.py b/aea/runtime.py index 66a11b4b0b..f7cecef87e 100644 --- a/aea/runtime.py +++ b/aea/runtime.py @@ -16,7 +16,6 @@ # limitations under the License. # # ------------------------------------------------------------------------------ - """This module contains the implementation of runtime for economic agent (AEA).""" import asyncio @@ -25,14 +24,14 @@ from asyncio.events import AbstractEventLoop from contextlib import suppress from enum import Enum -from typing import Optional, TYPE_CHECKING, cast +from typing import Dict, Optional, Type, cast -from aea.agent_loop import AsyncState +from aea.abstract_agent import AbstractAgent +from aea.agent_loop import AsyncAgentLoop, AsyncState, BaseAgentLoop, SyncAgentLoop +from aea.decision_maker.base import DecisionMaker, DecisionMakerHandler from aea.helpers.async_utils import ensure_loop -from aea.multiplexer import AsyncMultiplexer - -if TYPE_CHECKING: # pragma: nocover - from aea.agent import Agent +from aea.multiplexer import AsyncMultiplexer, Multiplexer +from aea.skills.tasks import TaskManager logger = logging.getLogger(__name__) @@ -51,25 +50,108 @@ class RuntimeStates(Enum): class BaseRuntime(ABC): """Abstract runtime class to create implementations.""" + RUN_LOOPS: Dict[str, Type[BaseAgentLoop]] = { + "async": AsyncAgentLoop, + "sync": SyncAgentLoop, + } + DEFAULT_RUN_LOOP: str = "async" + def __init__( - self, agent: "Agent", loop: Optional[AbstractEventLoop] = None + self, + agent: AbstractAgent, + loop_mode: Optional[str] = None, + loop: Optional[AbstractEventLoop] = None, ) -> None: """ Init runtime. :param agent: Agent to run. + :param loop_mode: agent main loop mode. :param loop: optional event loop. if not provided a new one will be created. :return: None """ - self._agent: "Agent" = agent - self._loop = ensure_loop( - loop - ) # TODO: decide who constructs loop: agent, runtime, multiplexer. + self._agent: AbstractAgent = agent + self._loop: AbstractEventLoop = ensure_loop(loop) self._state: AsyncState = AsyncState(RuntimeStates.stopped, RuntimeStates) self._state.add_callback(self._log_runtime_state) - self._was_started = False + + self._multiplexer: Multiplexer = self._get_multiplexer_instance() + self._task_manager = TaskManager() + self._decision_maker: Optional[DecisionMaker] = None + + self._loop_mode = loop_mode or self.DEFAULT_RUN_LOOP + self.main_loop: BaseAgentLoop = self._get_main_loop_instance(self._loop_mode) + + @property + def loop_mode(self) -> str: # pragma: nocover + """Get current loop mode.""" + return self._loop_mode + + def setup_multiplexer(self) -> None: + """Set up the multiplexer.""" + setup_options = self._agent.get_multiplexer_setup_options() + if setup_options: + self.multiplexer.setup(**setup_options) + + @property + def task_manager(self) -> TaskManager: + """Get the task manager.""" + return self._task_manager + + @property + def loop(self) -> AbstractEventLoop: + """Get event loop.""" + return self._loop + + @property + def multiplexer(self) -> Multiplexer: + """Get multiplexer.""" + return self._multiplexer + + def _get_multiplexer_instance(self) -> Multiplexer: + """Create multiplexer instance.""" + return Multiplexer(self._agent.connections, loop=self.loop) + + def _get_main_loop_class(self, loop_mode: str) -> Type[BaseAgentLoop]: + """ + Get main loop class based on loop mode. + + :param: loop_mode: str. + + :return: MainLoop class + """ + if loop_mode not in self.RUN_LOOPS: # pragma: nocover + raise ValueError( + f"Loop `{loop_mode} is not supported. valid are: `{list(self.RUN_LOOPS.keys())}`" + ) + return self.RUN_LOOPS[loop_mode] + + @property + def decision_maker(self) -> DecisionMaker: + """Return decision maker if set.""" + if self._decision_maker is None: # pragma: nocover + raise ValueError("call `set_decision_maker` first!") + return self._decision_maker + + def set_decision_maker(self, decision_maker_handler: DecisionMakerHandler) -> None: + """Set decision maker with handler provided.""" + self._decision_maker = DecisionMaker( + decision_maker_handler=decision_maker_handler + ) + + def _get_main_loop_instance(self, loop_mode: str) -> BaseAgentLoop: + """ + Construct main loop instance. + + :param: loop_mode: str. + + :return: AgentLoop instance + """ + loop_cls = self._get_main_loop_class(loop_mode) + return loop_cls(self._agent) def _log_runtime_state(self, state) -> None: + """Log a runtime state changed.""" logger.debug(f"[{self._agent.name}]: Runtime state changed to {state}.") def start(self) -> None: @@ -81,7 +163,6 @@ def start(self) -> None: ) ) return - self._was_started = True self._start() def stop(self) -> None: @@ -99,6 +180,9 @@ def stop(self) -> None: def _teardown(self) -> None: """Tear down runtime.""" logger.debug("[{}]: Runtime teardown...".format(self._agent.name)) + if self._decision_maker is not None: # pragma: nocover + self.decision_maker.stop() + self.task_manager.stop() self._agent.teardown() logger.debug("[{}]: Runtime teardown completed".format(self._agent.name)) @@ -145,16 +229,20 @@ class AsyncRuntime(BaseRuntime): """Asynchronous runtime: uses asyncio loop for multiplexer and async agent main loop.""" def __init__( - self, agent: "Agent", loop: Optional[AbstractEventLoop] = None + self, + agent: AbstractAgent, + loop_mode: Optional[str] = None, + loop: Optional[AbstractEventLoop] = None, ) -> None: """ Init runtime. :param agent: Agent to run. + :param loop_mode: agent main loop mode. :param loop: optional event loop. if not provided a new one will be created. :return: None """ - super().__init__(agent=agent, loop=loop) + super().__init__(agent=agent, loop_mode=loop_mode, loop=loop) self._stopping_task: Optional[asyncio.Task] = None self._async_stop_lock: Optional[asyncio.Lock] = None self._task: Optional[asyncio.Task] = None @@ -166,8 +254,8 @@ def set_loop(self, loop: AbstractEventLoop) -> None: :param loop: event loop to use. """ super().set_loop(loop) - self._agent.multiplexer.set_loop(self._loop) - self._agent.main_loop.set_loop(self._loop) + self.multiplexer.set_loop(self.loop) + self.main_loop.set_loop(self.loop) self._async_stop_lock = asyncio.Lock() def _start(self) -> None: @@ -178,13 +266,13 @@ def _start(self) -> None: Start runtime asynchonously in own event loop. """ - self.set_loop(self._loop) + self.set_loop(self.loop) - logger.debug(f"Start runtime event loop {self._loop}: {id(self._loop)}") - self._task = self._loop.create_task(self.run_runtime()) + logger.debug(f"Start runtime event loop {self.loop}: {id(self.loop)}") + self._task = self.loop.create_task(self.run_runtime()) try: - self._loop.run_until_complete(self._task) + self.loop.run_until_complete(self._task) logger.debug("Runtime loop stopped!") except Exception: logger.exception("Exception raised during runtime processing") @@ -208,19 +296,24 @@ async def run_runtime(self) -> None: async def _multiplexer_disconnect(self) -> None: """Call multiplexer disconnect asynchronous way.""" - await AsyncMultiplexer.disconnect(self._agent.multiplexer) + await AsyncMultiplexer.disconnect(self.multiplexer) async def _start_multiplexer(self) -> None: """Call multiplexer connect asynchronous way.""" - self._agent.setup_multiplexer() - await AsyncMultiplexer.connect(self._agent.multiplexer) + self.setup_multiplexer() + await AsyncMultiplexer.connect(self.multiplexer) async def _start_agent_loop(self) -> None: """Start agent main loop asynchronous way.""" logger.debug("[{}]: Runtime started".format(self._agent.name)) - self._agent.start_setup() + self.task_manager.start() + if self._decision_maker is not None: # pragma: nocover + self.decision_maker.start() + logger.debug("[{}]: Calling setup method...".format(self._agent.name)) + self._agent.setup() self._state.set(RuntimeStates.running) - await self._agent.main_loop.run_loop() + logger.debug("[{}]: Run main loop...".format(self._agent.name)) + await self.main_loop.run_loop() async def _stop_runtime(self) -> None: """ @@ -243,10 +336,10 @@ async def _stop_runtime(self) -> None: return self._state.set(RuntimeStates.stopping) - self._agent.main_loop.stop() + self.main_loop.stop() with suppress(BaseException): - await self._agent.main_loop.wait_run_loop_stopped() + await self.main_loop.wait_run_loop_stopped() self._teardown() @@ -266,21 +359,21 @@ def _stop(self) -> None: This one calls async functions and does not guarantee to wait till runtime stopped. """ logger.debug("Stop runtime coroutine.") - if not self._loop.is_running(): # pragma: nocover + if not self.loop.is_running(): # pragma: nocover logger.debug( "Runtime event loop is not running, start loop with `stop` coroutine" ) with suppress(BaseException): - self._loop.run_until_complete(asyncio.sleep(0.01)) + self.loop.run_until_complete(asyncio.sleep(0.01)) - self._loop.run_until_complete(self._stop_runtime()) + self.loop.run_until_complete(self._stop_runtime()) return def set_task(): - self._stopping_task = self._loop.create_task(self._stop_runtime()) + self._stopping_task = self.loop.create_task(self._stop_runtime()) - self._loop.call_soon_threadsafe(set_task) + self.loop.call_soon_threadsafe(set_task) class ThreadedRuntime(BaseRuntime): @@ -290,19 +383,22 @@ def _start(self) -> None: """Implement runtime start function here.""" self._state.set(RuntimeStates.starting) - self._agent.multiplexer.set_loop(asyncio.new_event_loop()) + self.multiplexer.set_loop(asyncio.new_event_loop()) - self._agent.setup_multiplexer() - self._agent.multiplexer.connect() - self._agent.start_setup() + self.setup_multiplexer() + self.multiplexer.connect() + self._agent.setup() self._start_agent_loop() def _start_agent_loop(self) -> None: """Start aget's main loop.""" logger.debug("[{}]: Runtime started".format(self._agent.name)) + self.task_manager.start() + if self._decision_maker is not None: # pragma: nocover + self.decision_maker.start() try: self._state.set(RuntimeStates.running) - self._agent.main_loop.start() + self.main_loop.start() logger.debug("[{}]: Runtime stopped".format(self._agent.name)) except KeyboardInterrupt: # pragma: nocover raise @@ -314,8 +410,8 @@ def _start_agent_loop(self) -> None: def _stop(self) -> None: """Implement runtime stop function here.""" self._state.set(RuntimeStates.stopping) - self._agent.main_loop.stop() + self.main_loop.stop() self._teardown() - self._agent.multiplexer.disconnect() + self.multiplexer.disconnect() logger.debug("[{}]: Runtime stopped".format(self._agent.name)) self._state.set(RuntimeStates.stopped) diff --git a/aea/skills/base.py b/aea/skills/base.py index 1dbdc59579..e1df177495 100644 --- a/aea/skills/base.py +++ b/aea/skills/base.py @@ -31,21 +31,21 @@ from types import SimpleNamespace from typing import Any, Dict, Optional, Sequence, Set, Tuple, Type, cast -from aea.components.base import Component +from aea.common import Address +from aea.components.base import Component, load_aea_package from aea.configurations.base import ( - ComponentConfiguration, ComponentType, ProtocolId, PublicId, SkillComponentConfiguration, SkillConfig, ) +from aea.configurations.loader import load_component_configuration from aea.context.base import AgentContext -from aea.exceptions import AEAException -from aea.helpers.base import load_aea_package, load_module +from aea.exceptions import AEAException, enforce +from aea.helpers.base import load_module from aea.helpers.logging import AgentLoggerAdapter -from aea.mail.base import Address -from aea.multiplexer import ConnectionStatus, OutBox +from aea.multiplexer import MultiplexerStatus, OutBox from aea.protocols.base import Message from aea.skills.tasks import TaskManager @@ -84,12 +84,13 @@ def logger(self) -> Logger: @logger.setter def logger(self, logger_: Logger) -> None: - assert self._logger is None, "Logger already set." + """Set the logger.""" self._logger = logger_ def _get_agent_context(self) -> AgentContext: """Get the agent context.""" - assert self._agent_context is not None, "Agent context not set yet." + if self._agent_context is None: # pragma: nocover + raise ValueError("Agent context not set yet.") return self._agent_context def set_agent_context(self, agent_context: AgentContext) -> None: @@ -109,7 +110,8 @@ def agent_name(self) -> str: @property def skill_id(self) -> PublicId: """Get the skill id of the skill context.""" - assert self._skill is not None, "Skill not set yet." + if self._skill is None: + raise ValueError("Skill not set yet.") # pragma: nocover return self._skill.configuration.public_id @property @@ -162,7 +164,7 @@ def agent_address(self) -> str: return self._get_agent_context().address @property - def connection_status(self) -> ConnectionStatus: + def connection_status(self) -> MultiplexerStatus: """Get connection status.""" return self._get_agent_context().connection_status @@ -191,7 +193,8 @@ def decision_maker_handler_context(self) -> SimpleNamespace: @property def task_manager(self) -> TaskManager: """Get behaviours of the skill.""" - assert self._skill is not None, "Skill not initialized." + if self._skill is None: + raise ValueError("Skill not initialized.") return self._get_agent_context().task_manager @property @@ -202,13 +205,15 @@ def search_service_address(self) -> Address: @property def handlers(self) -> SimpleNamespace: """Get handlers of the skill.""" - assert self._skill is not None, "Skill not initialized." + if self._skill is None: + raise ValueError("Skill not initialized.") return SimpleNamespace(**self._skill.handlers) @property def behaviours(self) -> SimpleNamespace: """Get behaviours of the skill.""" - assert self._skill is not None, "Skill not initialized." + if self._skill is None: + raise ValueError("Skill not initialized.") return SimpleNamespace(**self._skill.behaviours) @property @@ -238,8 +243,10 @@ def __init__( :param configuration: the configuration for the component. :param skill_context: the skill context. """ - assert name is not None, "SkillComponent name is not provided." - assert skill_context is not None, "SkillConext is not provided" + if name is None: + raise ValueError("SkillComponent name is not provided.") + if skill_context is None: + raise ValueError("SkillConext is not provided") if configuration is None: class_name = type(self).__name__ configuration = SkillComponentConfiguration(class_name=class_name, **kwargs) @@ -259,7 +266,6 @@ def name(self) -> str: @property def context(self) -> SkillContext: """Get the context of the skill component.""" - assert self._context is not None, "Skill context not set yet." return self._context @property @@ -270,10 +276,10 @@ def skill_id(self) -> PublicId: @property def configuration(self) -> SkillComponentConfiguration: """Get the skill component configuration.""" - assert self._configuration is not None, "Configuration not set." + if self._configuration is None: + raise ValueError("Configuration not set.") # pragma: nocover return self._configuration - # TODO consider rename this property @property def config(self) -> Dict[Any, Any]: """Get the config of the skill component.""" @@ -348,7 +354,7 @@ def act_wrapper(self) -> None: self.act() @classmethod - def parse_module( + def parse_module( # pylint: disable=arguments-differ cls, path: str, behaviour_configs: Dict[str, SkillComponentConfiguration], @@ -401,9 +407,10 @@ def parse_module( skill_context.logger.debug( "Processing behaviour {}".format(behaviour_class_name) ) - assert ( - behaviour_id.isidentifier() - ), "'{}' is not a valid identifier.".format(behaviour_id) + enforce( + behaviour_id.isidentifier(), + "'{}' is not a valid identifier.".format(behaviour_id), + ) behaviour_class = name_to_class.get(behaviour_class_name, None) if behaviour_class is None: skill_context.logger.warning( @@ -436,7 +443,7 @@ def handle(self, message: Message) -> None: """ @classmethod - def parse_module( + def parse_module( # pylint: disable=arguments-differ cls, path: str, handler_configs: Dict[str, SkillComponentConfiguration], @@ -481,8 +488,9 @@ def parse_module( skill_context.logger.debug( "Processing handler {}".format(handler_class_name) ) - assert handler_id.isidentifier(), "'{}' is not a valid identifier.".format( - handler_id + enforce( + handler_id.isidentifier(), + "'{}' is not a valid identifier.".format(handler_id), ) handler_class = name_to_class.get(handler_class_name, None) if handler_class is None: @@ -511,7 +519,7 @@ def teardown(self) -> None: """Tear the class down.""" @classmethod - def parse_module( + def parse_module( # pylint: disable=arguments-differ cls, path: str, model_configs: Dict[str, SkillComponentConfiguration], @@ -577,8 +585,9 @@ def parse_module( skill_context.logger.debug( "Processing model id={}, class={}".format(model_id, model_class_name) ) - assert model_id.isidentifier(), "'{}' is not a valid identifier.".format( - model_id + enforce( + model_id.isidentifier(), + "'{}' is not a valid identifier.".format(model_id), ) model = name_to_class.get(model_class_name, None) if model is None: @@ -626,6 +635,7 @@ def __init__( handlers: Optional[Dict[str, Handler]] = None, behaviours: Optional[Dict[str, Behaviour]] = None, models: Optional[Dict[str, Model]] = None, + **kwargs, ): """ Initialize a skill. @@ -636,6 +646,8 @@ def __init__( :param behaviours: dictionary of behaviours. :param models: dictionary of models. """ + if kwargs is not None: + pass super().__init__(configuration) self.config = configuration self._skill_context = ( @@ -662,7 +674,8 @@ def _set_models_on_context(self) -> None: @property def skill_context(self) -> SkillContext: """Get the skill context.""" - assert self._skill_context is not None, "Skill context not set." + if self._skill_context is None: + raise ValueError("Skill context not set.") # pragma: nocover return self._skill_context @property @@ -691,10 +704,10 @@ def from_dir(cls, directory: str, agent_context: AgentContext, **kwargs) -> "Ski """ configuration = cast( SkillConfig, - ComponentConfiguration.load(ComponentType.SKILL, Path(directory)), + load_component_configuration(ComponentType.SKILL, Path(directory)), ) configuration.directory = Path(directory) - return Skill.from_config(configuration, agent_context) + return Skill.from_config(configuration, agent_context, **kwargs) @property def logger(self) -> Logger: @@ -722,9 +735,8 @@ def from_config( :param agent_context: the agent context. :return: the skill. """ - assert ( - configuration.directory is not None - ), "Configuration must be associated with a directory." + if configuration.directory is None: # pragma: nocover + raise ValueError("Configuration must be associated with a directory.") # we put the initialization here because some skill components # might need some info from the skill @@ -737,7 +749,7 @@ def from_config( ) skill_context.logger = cast(Logger, _logger) - skill = Skill(configuration, skill_context) + skill = Skill(configuration, skill_context, **kwargs) directory = configuration.directory load_aea_package(configuration) diff --git a/aea/skills/behaviours.py b/aea/skills/behaviours.py index 986059ecdf..06b0b47e4f 100644 --- a/aea/skills/behaviours.py +++ b/aea/skills/behaviours.py @@ -22,6 +22,7 @@ from abc import ABC, abstractmethod from typing import Callable, Dict, List, Optional, Set +from aea.exceptions import enforce from aea.skills.base import Behaviour @@ -62,6 +63,11 @@ def __init__(self, **kwargs): super().__init__(**kwargs) self._number_of_executions = 0 + @property + def number_of_executions(self) -> int: + """Get the number of executions.""" + return self._number_of_executions + def act_wrapper(self) -> None: """Wrap the call of the action. This method must be called only by the framework.""" if not self.is_done(): @@ -169,7 +175,7 @@ def __init__(self, behaviour_sequence: List[Behaviour], **kwargs): super().__init__(**kwargs) self._behaviour_sequence = behaviour_sequence - assert len(self._behaviour_sequence) > 0, "at least one behaviour." + enforce(len(self._behaviour_sequence) > 0, "at least one behaviour.") self._index = 0 @property @@ -352,10 +358,9 @@ def act(self): # we reached a final state - return. self.current = None return - else: - event = current_state.event - next_state = self.transitions.get(self.current, {}).get(event, None) - self.current = next_state + event = current_state.event + next_state = self.transitions.get(self.current, {}).get(event, None) + self.current = next_state def is_done(self) -> bool: """Return True if the behaviour is terminated, False otherwise.""" diff --git a/aea/skills/error/handlers.py b/aea/skills/error/handlers.py index 9ff282306a..c6f34207d8 100644 --- a/aea/skills/error/handlers.py +++ b/aea/skills/error/handlers.py @@ -81,8 +81,8 @@ def send_unsupported_protocol(self, envelope: Envelope) -> None: "envelope": encoded_envelope, }, ) - reply.counterparty = envelope.sender reply.sender = self.context.agent_address + reply.to = envelope.sender self.context.outbox.put_message(message=reply) def send_decoding_error(self, envelope: Envelope) -> None: @@ -107,8 +107,8 @@ def send_decoding_error(self, envelope: Envelope) -> None: error_msg="Decoding error.", error_data={"envelope": encoded_envelope}, ) - reply.counterparty = envelope.sender reply.sender = self.context.agent_address + reply.to = envelope.sender self.context.outbox.put_message(message=reply) def send_unsupported_skill(self, envelope: Envelope) -> None: @@ -140,6 +140,6 @@ def send_unsupported_skill(self, envelope: Envelope) -> None: error_msg="Unsupported skill.", error_data={"envelope": encoded_envelope}, ) - reply.counterparty = envelope.sender reply.sender = self.context.agent_address + reply.to = envelope.sender self.context.outbox.put_message(message=reply) diff --git a/aea/skills/error/skill.yaml b/aea/skills/error/skill.yaml index f089e39ade..66d180ffd2 100644 --- a/aea/skills/error/skill.yaml +++ b/aea/skills/error/skill.yaml @@ -1,16 +1,17 @@ name: error author: fetchai -version: 0.4.0 +version: 0.5.0 +type: skill description: The error skill implements basic error handling required by all AEAs. license: Apache-2.0 -aea_version: '>=0.5.0, <0.6.0' +aea_version: '>=0.6.0, <0.7.0' fingerprint: __init__.py: QmYm7UaWVmRy2i35MBKZRnBrpWBJswLdEH6EY1QQKXdQES - handlers.py: QmTHJ2EFdyRPdDb93po118eqkVMpxxVLZeF4XitLq76yPx + handlers.py: QmU5PviCqLGX7h9nSAAjcSMs1xsLc8TckSu4KcnbLPCaBG fingerprint_ignore_patterns: [] contracts: [] protocols: -- fetchai/default:0.4.0 +- fetchai/default:0.5.0 skills: [] behaviours: {} handlers: diff --git a/aea/skills/scaffold/skill.yaml b/aea/skills/scaffold/skill.yaml index 1f4ac58a33..bdda9507e7 100644 --- a/aea/skills/scaffold/skill.yaml +++ b/aea/skills/scaffold/skill.yaml @@ -1,9 +1,10 @@ name: scaffold author: fetchai version: 0.1.0 +type: skill description: The scaffold skill is a scaffold for your own skill implementation. license: Apache-2.0 -aea_version: '>=0.5.0, <0.6.0' +aea_version: '>=0.6.0, <0.7.0' fingerprint: __init__.py: QmNkZAetyctaZCUf6ACxP5onGWsSxu2hjSNoFmJ3ta6Lta behaviours.py: QmNgDDAmBzWBeBF7e5gUCny38kdqVVfpvHGaAZVZcMtm9Q diff --git a/aea/skills/tasks.py b/aea/skills/tasks.py index 7a01b025f4..6b0350285c 100644 --- a/aea/skills/tasks.py +++ b/aea/skills/tasks.py @@ -115,8 +115,6 @@ def init_worker() -> None: :return: None """ signal.signal(signal.SIGINT, signal.SIG_IGN) - # TODO check how to disable CTRL_C_EVENT signal on Windows - # signal.signal(signal.CTRL_C_EVENT, signal.SIG_IGN) class TaskManager(WithLogger): diff --git a/aea/test_tools/generic.py b/aea/test_tools/generic.py index 64067d7c72..74743bdd15 100644 --- a/aea/test_tools/generic.py +++ b/aea/test_tools/generic.py @@ -26,6 +26,7 @@ from aea.cli.utils.config import handle_dotted_path from aea.configurations.base import PublicId from aea.connections.stub.connection import write_envelope +from aea.exceptions import enforce from aea.mail.base import Envelope @@ -54,13 +55,13 @@ def read_envelope_from_file(file_path: str): with open(Path(file_path), "rb+") as f: lines.extend(f.readlines()) - assert len(lines) == 2, "Did not find two lines." + enforce(len(lines) == 2, "Did not find two lines.") line = lines[0] + lines[1] to_b, sender_b, protocol_id_b, message, end = line.strip().split(b",", maxsplit=4) to = to_b.decode("utf-8") sender = sender_b.decode("utf-8") protocol_id = PublicId.from_str(protocol_id_b.decode("utf-8")) - assert end in [b"", b"\n"] + enforce(end in [b"", b"\n"], "Envelope improperly formatted.") return Envelope(to=to, sender=sender, protocol_id=protocol_id, message=message,) diff --git a/aea/test_tools/test_cases.py b/aea/test_tools/test_cases.py index e64d793c70..53953a37c6 100644 --- a/aea/test_tools/test_cases.py +++ b/aea/test_tools/test_cases.py @@ -22,7 +22,6 @@ import os import random import shutil -import signal # pylint: disable=unused-import import string import subprocess # nosec import sys @@ -48,6 +47,7 @@ DEFAULT_INPUT_FILE_NAME, DEFAULT_OUTPUT_FILE_NAME, ) +from aea.exceptions import enforce from aea.helpers.base import cd, send_control_c, win_popen_kwargs from aea.mail.base import Envelope from aea.test_tools.click_testing import CliRunner, Result @@ -64,7 +64,6 @@ logger = logging.getLogger(__name__) CLI_LOG_OPTION = ["-v", "OFF"] -PROJECT_ROOT_DIR = "." DEFAULT_PROCESS_TIMEOUT = 120 DEFAULT_LAUNCH_TIMEOUT = 10 @@ -293,8 +292,7 @@ def is_allowed_diff_in_agent_config( ) if result: return result, {}, {} - else: - return result, content1, content2 + return result, content1, content2 path_to_manually_created_aea = os.path.join(cls.t, agent_name) new_cwd = os.path.join(cls.t, "fetch_dir") @@ -381,10 +379,7 @@ def _start_cli_process(cls, *args: str) -> subprocess.Popen: @classmethod def terminate_agents( - cls, - *subprocesses: subprocess.Popen, - signal: signal.Signals = signal.SIGINT, - timeout: int = 10, + cls, *subprocesses: subprocess.Popen, timeout: int = 10, ) -> None: """ Terminate agent subprocesses. @@ -392,7 +387,6 @@ def terminate_agents( Run from agent's directory. :param subprocesses: the subprocesses running the agents - :param signal: the signal for interruption :param timeout: the timeout for interruption """ if not subprocesses: @@ -541,10 +535,9 @@ def add_private_key( "--connection", cwd=cls._get_cwd(), ) - else: - return cls.run_cli_command( - "add-key", ledger_api_id, private_key_filepath, cwd=cls._get_cwd() - ) + return cls.run_cli_command( + "add-key", ledger_api_id, private_key_filepath, cwd=cls._get_cwd() + ) @classmethod def replace_private_key_in_file( @@ -590,7 +583,8 @@ def get_wealth(cls, ledger_api_id: str = DEFAULT_LEDGER) -> str: :return: command line output """ cls.run_cli_command("get-wealth", ledger_api_id, cwd=cls._get_cwd()) - assert cls.last_cli_runner_result is not None, "Runner result not set!" + if cls.last_cli_runner_result is None: + raise ValueError("Runner result not set!") # pragma: nocover return str(cls.last_cli_runner_result.stdout_bytes, "utf-8") @classmethod @@ -602,7 +596,9 @@ def replace_file_content(cls, src: Path, dest: Path) -> None: # pragma: nocover :param dest: the destination file. :return: None """ - assert src.is_file() and dest.is_file(), "Source or destination is not a file." + enforce( + src.is_file() and dest.is_file(), "Source or destination is not a file." + ) dest.write_text(src.read_text()) @classmethod @@ -687,8 +683,8 @@ def send_envelope_to_agent(cls, envelope: Envelope, agent: str): """Send an envelope to an agent, using the stub connection.""" # check added cause sometimes fails on win with permission error dir_path = Path(cls.t / agent) - assert dir_path.exists() - assert dir_path.is_dir() + enforce(dir_path.exists(), "Dir path does not exist.") + enforce(dir_path.is_dir(), "Dir path is not a directory.") write_envelope_to_file(envelope, str(cls.t / agent / DEFAULT_INPUT_FILE_NAME)) @classmethod @@ -858,7 +854,6 @@ def setup_class(cls): """Set up the test class.""" # make paths absolute cls.path_to_aea = cls.path_to_aea.absolute() - # TODO: decide whether to keep optionally: cls.packages_dir_path = cls.packages_dir_path.absolute() # load agent configuration with Path(cls.path_to_aea, DEFAULT_AEA_CONFIG_FILE).open( mode="r", encoding="utf-8" @@ -869,7 +864,6 @@ def setup_class(cls): cls.agent_name = agent_configuration.agent_name # this will create a temporary directory and move into it - # TODO: decide whether to keep optionally: BaseAEATestCase.packages_dir_path = cls.packages_dir_path cls.use_packages_dir = False super(AEATestCase, cls).setup_class() @@ -881,6 +875,5 @@ def setup_class(cls): def teardown_class(cls): """Teardown the test class.""" cls.path_to_aea = Path(".") - # TODO: decide whether to keep optionally: cls.packages_dir_path = Path("..", "packages") cls.agent_configuration = None super(AEATestCase, cls).teardown_class() diff --git a/benchmark/cases/react_multi_agents_fake_connection.py b/benchmark/cases/react_multi_agents_fake_connection.py index 5ad01306de..fedb5800f3 100644 --- a/benchmark/cases/react_multi_agents_fake_connection.py +++ b/benchmark/cases/react_multi_agents_fake_connection.py @@ -50,7 +50,7 @@ def _make_skill(id_): ) return { - "name": "dummy_a", + "name": name, "components": [_make_skill(i) for i in range(skills_num)], } diff --git a/benchmark/cases/react_speed_multi_agents.py b/benchmark/cases/react_speed_multi_agents.py index 45ff4bc318..df34bd360d 100644 --- a/benchmark/cases/react_speed_multi_agents.py +++ b/benchmark/cases/react_speed_multi_agents.py @@ -45,7 +45,7 @@ def _make_skill(id_): ) return { - "name": "dummy_a", + "name": name, "components": [_make_skill(i) for i in range(skills_num)], } diff --git a/benchmark/framework/aea_test_wrapper.py b/benchmark/framework/aea_test_wrapper.py index fc9644e621..059b6e664c 100644 --- a/benchmark/framework/aea_test_wrapper.py +++ b/benchmark/framework/aea_test_wrapper.py @@ -53,7 +53,9 @@ def __init__(self, name: str = "my_aea", components: List[Component] = None): self.aea = self.make_aea(self.name, self.components) self._thread = None # type: Optional[Thread] - def make_aea(self, name: str = "my_aea", components: List[Component] = None) -> AEA: + def make_aea( + self, name: Optional[str] = None, components: List[Component] = None + ) -> AEA: """ Create AEA from name and already loaded components. @@ -65,7 +67,7 @@ def make_aea(self, name: str = "my_aea", components: List[Component] = None) -> components = components or [] builder = AEABuilder() - builder.set_name(self.name) + builder.set_name(name or self.name) builder.add_private_key(FetchAICrypto.identifier, private_key_path=None) @@ -160,7 +162,7 @@ def dummy_envelope( message=DefaultSerializer().encode(message), ) - def set_loop_timeout(self, timeout: float) -> None: + def set_loop_timeout(self, period: float) -> None: """ Set agent's loop timeout. @@ -168,7 +170,7 @@ def set_loop_timeout(self, timeout: float) -> None: :return: None """ - self.aea._timeout = timeout # pylint: disable=protected-access + self.aea._period = period # pylint: disable=protected-access def setup(self) -> None: """ @@ -176,7 +178,7 @@ def setup(self) -> None: :return: None """ - self.aea.start_setup() + self.aea.setup() def stop(self) -> None: """ @@ -194,7 +196,7 @@ def put_inbox(self, envelope: Envelope) -> None: :return: None """ - self.aea.multiplexer.in_queue.put(envelope) + self.aea.runtime.multiplexer.in_queue.put(envelope) def is_inbox_empty(self) -> bool: """ @@ -202,15 +204,7 @@ def is_inbox_empty(self) -> bool: :return: None """ - return self.aea.multiplexer.in_queue.empty() - - def react(self) -> None: - """ - One time process of react for incoming message. - - :return: None - """ - self.aea.react() + return self.aea.runtime.multiplexer.in_queue.empty() def __enter__(self) -> None: """Contenxt manager enter.""" @@ -238,7 +232,8 @@ def start_loop(self) -> None: def stop_loop(self) -> None: """Stop agents loop in dedicated thread, close thread.""" - assert self._thread is not None, "Thread not set, call start_loop first." + if self._thread is None: + raise ValueError("Thread not set, call start_loop first.") self.aea.stop() self._thread.join() diff --git a/benchmark/framework/cli.py b/benchmark/framework/cli.py index fa838f6cd0..8474b46daf 100644 --- a/benchmark/framework/cli.py +++ b/benchmark/framework/cli.py @@ -109,14 +109,15 @@ def test_fn(benchmark: BenchmarkControl, list_size: int = 1000000): self.func_details = BenchmarkFuncDetails(func) self.func_details.check() self.func = func - self.executor_class = Executor + self.executor_class = executor_class self.report_printer_class = report_printer_class self._report_printer = None # type: Optional[ReportPrinter] @property def report_printer(self) -> ReportPrinter: """Get report printer.""" - assert self._report_printer is not None, "report printer not set!" + if self._report_printer is None: + raise ValueError("report printer not set!") return self._report_printer def _make_command(self) -> Command: diff --git a/benchmark/framework/executor.py b/benchmark/framework/executor.py index c3703c80b7..206580b557 100644 --- a/benchmark/framework/executor.py +++ b/benchmark/framework/executor.py @@ -145,7 +145,8 @@ def _prepare(func: Callable, args: tuple) -> Process: process = Process(target=func, args=(control, *args)) process.start() msg = control.wait_msg() - assert msg == control.START_MSG + if msg != control.START_MSG: + raise ValueError("Msg does not match control start message.") return process def _measure( diff --git a/benchmark/framework/report_printer.py b/benchmark/framework/report_printer.py index e5adbc97a9..6dfd4a2e16 100644 --- a/benchmark/framework/report_printer.py +++ b/benchmark/framework/report_printer.py @@ -75,7 +75,6 @@ def __init__(self, exec_reports: List[ExecReport]): :param exec_reports: tested function execution reports with measurements """ - assert exec_reports self.exec_reports = exec_reports @property diff --git a/deploy-image/Dockerfile b/deploy-image/Dockerfile index a4e8128c9d..a6f86af05a 100644 --- a/deploy-image/Dockerfile +++ b/deploy-image/Dockerfile @@ -12,12 +12,12 @@ ENV PYTHONPATH "$PYTHONPATH:/usr/lib/python3.7/site-packages" RUN pip install --upgrade pip # other oef dependences RUN pip install protobuf colorlog graphviz -RUN pip install aea[all] +RUN pip install aea[all] --upgrade --force-reinstall # golang RUN apk add --no-cache go -COPY ./packages /home/packages +# COPY ./packages /home/packages # enable to add packages dir WORKDIR home WORKDIR /home/myagent diff --git a/deploy-image/docker-env.sh b/deploy-image/docker-env.sh index 96f6958a50..9190f2f23c 100755 --- a/deploy-image/docker-env.sh +++ b/deploy-image/docker-env.sh @@ -1,7 +1,7 @@ #!/bin/bash # Swap the following lines if you want to work with 'latest' -DOCKER_IMAGE_TAG=aea-deploy:0.5.4 +DOCKER_IMAGE_TAG=aea-deploy:0.6.0 # DOCKER_IMAGE_TAG=aea-deploy:latest DOCKER_BUILD_CONTEXT_DIR=.. diff --git a/deploy-image/entrypoint.sh b/deploy-image/entrypoint.sh index 98ad684258..ac18f3a371 100755 --- a/deploy-image/entrypoint.sh +++ b/deploy-image/entrypoint.sh @@ -1,15 +1,16 @@ #!/bin/bash set -e +aea --version + if [ -z ${AGENT_REPO_URL+x} ] ; then rm myagent -rf - aea create myagent - cd myagent - aea add skill fetchai/echo:0.3.0 + aea fetch fetchai/my_first_aea:0.10.0 + cd my_first_aea else - echo "cloning $AGENT_REPO_URL inside '$(pwd)/myagent'" - echo git clone $AGENT_REPO_URL myagent - git clone $AGENT_REPO_URL myagent && cd myagent + echo "cloning $AGENT_REPO_URL inside '$(pwd)/my_aea'" + echo git clone $AGENT_REPO_URL my_aea + git clone $AGENT_REPO_URL my_aea && cd my_aea fi echo /usr/local/bin/aea run diff --git a/develop-image/docker-env.sh b/develop-image/docker-env.sh index b152507381..e7ea8df9ec 100755 --- a/develop-image/docker-env.sh +++ b/develop-image/docker-env.sh @@ -1,7 +1,7 @@ #!/bin/bash # Swap the following lines if you want to work with 'latest' -DOCKER_IMAGE_TAG=aea-develop:0.5.4 +DOCKER_IMAGE_TAG=aea-develop:0.6.0 # DOCKER_IMAGE_TAG=aea-develop:latest DOCKER_BUILD_CONTEXT_DIR=.. diff --git a/docs/aea-vs-mvc.md b/docs/aea-vs-mvc.md index 41f272117d..fb2c558e0f 100644 --- a/docs/aea-vs-mvc.md +++ b/docs/aea-vs-mvc.md @@ -1,8 +1,8 @@ -The AEA framework borrows several concepts from popular web frameworks like Django and Ruby on Rails. +The AEA framework borrows several concepts from popular web frameworks like Django and Ruby on Rails. ## MVC -Both aforementioned web frameworks use the MVC (model-view-controller) architecture. +Both aforementioned web frameworks use the MVC (model-view-controller) architecture. - Models: contain business logic and data representations - View: contain the html templates @@ -10,7 +10,7 @@ Both aforementioned web frameworks use the asynchronous messaging. Hence, there is not a direct 1-1 relationship between MVC based architectures and the AEA framework. Nevertheless, there are some parallels which can help a developer familiar with MVC make progress in the AEA framework in particular, the development of `Skills`, quickly: +The AEA framework is based on asynchronous messaging. Hence, there is not a direct 1-1 relationship between MVC based architectures and the AEA framework. Nevertheless, there are some parallels which can help a developer familiar with MVC make progress in the AEA framework in particular, the development of `Skills`, quickly: - `Handler`: receive the messages for the protocol they are registered against and are supposed to handle these messages. They are the reactive parts of a skill and can be thought of as similar to the `Controller` in MVC. They can also send new messages. - `Behaviour`: a behaviour encapsulates pro-active components of the agent. Since web apps do not have any goals or intentions they do not pro-actively pursue an objective. Therefore, there is no equivalent concept in MVC. Behaviours can but do not have to send messages. @@ -19,7 +19,7 @@ The AEA framework is based on -The `View` concept is probably best compared to the `Message` of a given `Protocol` in the AEA framework. Whilst views represent information to the client, messages represent information sent to other agents and services. +The `View` concept is probably best compared to the `Message` of a given `Protocol` in the AEA framework. Whilst views represent information to the client, messages represent information sent to other agents, other agent components and services. ## Next steps diff --git a/docs/agent-oriented-development.md b/docs/agent-oriented-development.md index fba5b3ac5c..fdbe553c11 100644 --- a/docs/agent-oriented-development.md +++ b/docs/agent-oriented-development.md @@ -1,7 +1,9 @@ # Agent-oriented development + In this section, we highlight some of the most fundamental characteristics of the agent-oriented approach to solution development, which might be different from some of the existing paradigms and methodologies you may be used to. We hope that with this, we can guide you towards having the right mindset when you are designing your own agent-based solutions to real world problems. ## Decentralisation + Multi-Agent Systems (**MAS**) are inherently decentralised. The vision is, an environment in which every agent is able to directly connect with everyone else and interact with them without having to rely on third-parties to facilitate this. This is in direct contrast to centralised systems in which a single entity is the central point of authority, through which all interactions happen. For example systems based on the client-server architecture, in which clients interact with one another, regarding a specific service (e.g. communication, trade), only through the server. Note, this is not to say that facilitators and middlemen have no place in a multi-agent system; rather it is the 'commanding reliance on middlemen' that MAS disagrees with. @@ -38,7 +40,7 @@ The conflicting nature of multi-agent systems, consisting of self-interested aut **Objects vs agents:** In object-oriented systems, objects are entities that encapsulate state and perform actions, i.e. call methods, on this state. In object-oriented languages, like C++ and Java, it is common practice to declare methods as public, so they can be invoked by other objects in the system whenever they wish. This implies that an object does not control its own behaviour. If an object’s method is public, the object has no control over whether or not that method is executed. -We cannot take for granted that an agent _j_ will execute an action (the equivalent of a method in object-oriented systems) just because another agent _i_ wants it to; this action may not be in the best interests of agent _j_. So we do not think of agents as invoking methods on one another, rather as _requesting_ actions. If _i_ requests _j_ to perform an action, then _j_ may or may not perform the action. It may choose to do it later or do it in exchange for something. The locus of control is therefore different in object-oriented and agent-oriented systems. In the former, the decision lies with the object invoking the method, whereas in the latter, the decision lies with the agent receiving the request. This distinction could be summarised by the following slogan (from An Introduction to MultiAgent Systems by Michael Wooldridge): +We cannot take for granted that an agent _j_ will execute an action (the equivalent of a method in object-oriented systems) just because another agent _i_ wants it to; this action may not be in the best interests of agent _j_. So we do not think of agents as invoking methods on one another, rather as _requesting_ actions. If _i_ requests _j_ to perform an action, then _j_ may or may not perform the action. It may choose to do it later or do it in exchange for something. The locus of control is therefore different in object-oriented and agent-oriented systems. In the former, the decision lies with the object invoking the method, whereas in the latter, the decision lies with the agent receiving the request. This distinction could be summarised by the following slogan (from An Introduction to MultiAgent Systems by Michael Wooldridge): >objects do it for free; agents do it because they want to. All of this makes asynchronisation the preferred method for designing agent processes and interactions. An agent's interactions should be independent of each other, as much as possible, and of the agent's decision making processes and actions. This means the success or failure of, or delay in any single interaction does not block the agent's other tasks. diff --git a/docs/agent-vs-aea.md b/docs/agent-vs-aea.md index 6d8bcc1a58..7ec078fb9a 100644 --- a/docs/agent-vs-aea.md +++ b/docs/agent-vs-aea.md @@ -11,7 +11,7 @@ First, import the python and application specific libraries. import os import time from threading import Thread -from typing import List, Optional +from typing import List from aea.agent import Agent from aea.configurations.base import ConnectionConfig @@ -24,11 +24,10 @@ from aea.protocols.default.message import DefaultMessage Unlike an `AEA`, an `Agent` does not require a `Wallet`, `LedgerApis` or `Resources` module. -However, we need to implement 5 abstract methods: +However, we need to implement 4 abstract methods: - `setup()` - `act()` -- `react()` -- `update()` +- `handle_envelope()` - `teardown()` @@ -45,7 +44,10 @@ OUTPUT_FILE = "output_file" class MyAgent(Agent): + """A simple agent.""" + def __init__(self, identity: Identity, connections: List[Connection]): + """Initialise the agent.""" super().__init__(identity, connections) def setup(self): @@ -54,30 +56,22 @@ class MyAgent(Agent): def act(self): print("Act called for tick {}".format(self.tick)) - def react(self): + def handle_envelope(self, envelope: Envelope) -> None: print("React called for tick {}".format(self.tick)) - while not self.inbox.empty(): - envelope = self.inbox.get_nowait() # type: Optional[Envelope] - if ( - envelope is not None - and envelope.protocol_id == DefaultMessage.protocol_id - ): - sender = envelope.sender - receiver = envelope.to - envelope.to = sender - envelope.sender = receiver - envelope.message = DefaultMessage.serializer.decode(envelope.message) - envelope.message.sender = receiver - envelope.message.counterparty = sender - print( - "Received envelope from {} with protocol_id={}".format( - sender, envelope.protocol_id - ) + if envelope is not None and envelope.protocol_id == DefaultMessage.protocol_id: + sender = envelope.sender + receiver = envelope.to + envelope.to = sender + envelope.sender = receiver + envelope.message = DefaultMessage.serializer.decode(envelope.message_bytes) + envelope.message.sender = receiver + envelope.message.to = sender + print( + "Received envelope from {} with protocol_id={}".format( + sender, envelope.protocol_id ) - self.outbox.put(envelope) - - def update(self): - print("Update called for tick {}".format(self.tick)) + ) + self.outbox.put(envelope) def teardown(self): pass @@ -124,7 +118,7 @@ We use the input and output text files to send an envelope to our agent and rece ``` python # Create a message inside an envelope and get the stub connection to pass it into the agent message_text = ( - b"my_agent,other_agent,fetchai/default:0.4.0,\x08\x01*\x07\n\x05hello," + b"my_agent,other_agent,fetchai/default:0.5.0,\x08\x01*\x07\n\x05hello," ) with open(INPUT_FILE, "wb") as f: write_with_lock(f, message_text) @@ -160,7 +154,7 @@ If you just want to copy and paste the entire script in you can find it here: import os import time from threading import Thread -from typing import List, Optional +from typing import List from aea.agent import Agent from aea.configurations.base import ConnectionConfig @@ -176,7 +170,10 @@ OUTPUT_FILE = "output_file" class MyAgent(Agent): + """A simple agent.""" + def __init__(self, identity: Identity, connections: List[Connection]): + """Initialise the agent.""" super().__init__(identity, connections) def setup(self): @@ -185,30 +182,22 @@ class MyAgent(Agent): def act(self): print("Act called for tick {}".format(self.tick)) - def react(self): + def handle_envelope(self, envelope: Envelope) -> None: print("React called for tick {}".format(self.tick)) - while not self.inbox.empty(): - envelope = self.inbox.get_nowait() # type: Optional[Envelope] - if ( - envelope is not None - and envelope.protocol_id == DefaultMessage.protocol_id - ): - sender = envelope.sender - receiver = envelope.to - envelope.to = sender - envelope.sender = receiver - envelope.message = DefaultMessage.serializer.decode(envelope.message) - envelope.message.sender = receiver - envelope.message.counterparty = sender - print( - "Received envelope from {} with protocol_id={}".format( - sender, envelope.protocol_id - ) + if envelope is not None and envelope.protocol_id == DefaultMessage.protocol_id: + sender = envelope.sender + receiver = envelope.to + envelope.to = sender + envelope.sender = receiver + envelope.message = DefaultMessage.serializer.decode(envelope.message_bytes) + envelope.message.sender = receiver + envelope.message.to = sender + print( + "Received envelope from {} with protocol_id={}".format( + sender, envelope.protocol_id ) - self.outbox.put(envelope) - - def update(self): - print("Update called for tick {}".format(self.tick)) + ) + self.outbox.put(envelope) def teardown(self): pass @@ -245,7 +234,7 @@ def run(): # Create a message inside an envelope and get the stub connection to pass it into the agent message_text = ( - b"my_agent,other_agent,fetchai/default:0.4.0,\x08\x01*\x07\n\x05hello," + b"my_agent,other_agent,fetchai/default:0.5.0,\x08\x01*\x07\n\x05hello," ) with open(INPUT_FILE, "wb") as f: write_with_lock(f, message_text) diff --git a/docs/api/abstract_agent.md b/docs/api/abstract_agent.md new file mode 100644 index 0000000000..435cfa2bbe --- /dev/null +++ b/docs/api/abstract_agent.md @@ -0,0 +1,183 @@ + +# aea.abstract`_`agent + +This module contains the interface definition of the abstract agent. + + +## AbstractAgent Objects + +```python +class AbstractAgent(ABC) +``` + +This class provides an abstract base interface for an agent. + + +#### name + +```python + | @abstractproperty + | name() -> str +``` + +Get agent's name. + + +#### start + +```python + | @abstractmethod + | start() -> None +``` + +Start the agent. + +**Returns**: + +None + + +#### stop + +```python + | @abstractmethod + | stop() -> None +``` + +Stop the agent. + +**Returns**: + +None + + +#### setup + +```python + | @abstractmethod + | setup() -> None +``` + +Set up the agent. + +**Returns**: + +None + + +#### act + +```python + | @abstractmethod + | act() -> None +``` + +Perform actions on period. + +**Returns**: + +None + + +#### handle`_`envelope + +```python + | @abstractmethod + | handle_envelope(envelope: Envelope) -> None +``` + +Handle an envelope. + +**Arguments**: + +- `envelope`: the envelope to handle. + +**Returns**: + +None + + +#### get`_`periodic`_`tasks + +```python + | @abstractmethod + | get_periodic_tasks() -> Dict[Callable, Tuple[float, Optional[datetime.datetime]]] +``` + +Get all periodic tasks for agent. + +**Returns**: + +dict of callable with period specified + + +#### get`_`message`_`handlers + +```python + | @abstractmethod + | get_message_handlers() -> List[Tuple[Callable[[Any], None], Callable]] +``` + +Get handlers with message getters. + +**Returns**: + +List of tuples of callables: handler and coroutine to get a message + + +#### get`_`multiplexer`_`setup`_`options + +```python + | @abstractmethod + | get_multiplexer_setup_options() -> Optional[Dict] +``` + +Get options to pass to Multiplexer.setup. + +**Returns**: + +dict of kwargs + + +#### connections + +```python + | @abstractproperty + | connections() -> List[Connection] +``` + +Return list of connections. + + +#### exception`_`handler + +```python + | @abstractmethod + | exception_handler(exception: Exception, function: Callable) -> Optional[bool] +``` + +Handle exception raised during agent main loop execution. + +**Arguments**: + +- `exception`: exception raised +- `function`: a callable exception raised in. + +**Returns**: + +skip exception if True, otherwise re-raise it + + +#### teardown + +```python + | @abstractmethod + | teardown() -> None +``` + +Tear down the agent. + +**Returns**: + +None + diff --git a/docs/api/aea.md b/docs/api/aea.md index 0ddaa454cb..089630f9a9 100644 --- a/docs/api/aea.md +++ b/docs/api/aea.md @@ -16,7 +16,7 @@ This class implements an autonomous economic agent. #### `__`init`__` ```python - | __init__(identity: Identity, wallet: Wallet, resources: Resources, loop: Optional[AbstractEventLoop] = None, timeout: float = 0.05, execution_timeout: float = 0, max_reactions: int = 20, decision_maker_handler_class: Type[ + | __init__(identity: Identity, wallet: Wallet, resources: Resources, loop: Optional[AbstractEventLoop] = None, period: float = 0.05, execution_timeout: float = 0, max_reactions: int = 20, decision_maker_handler_class: Type[ | DecisionMakerHandler | ] = DefaultDecisionMakerHandler, skill_exception_policy: ExceptionPolicyEnum = ExceptionPolicyEnum.propagate, loop_mode: Optional[str] = None, runtime_mode: Optional[str] = None, default_connection: Optional[PublicId] = None, default_routing: Optional[Dict[PublicId, PublicId]] = None, connection_ids: Optional[Collection[PublicId]] = None, search_service_address: str = "fetchai/soef:*", **kwargs, ,) -> None ``` @@ -29,7 +29,7 @@ Instantiate the agent. - `wallet`: the wallet of the agent. - `resources`: the resources (protocols and skills) of the agent. - `loop`: the event loop to run the connections. -- `timeout`: the time in (fractions of) seconds to time out an agent between act and react +- `period`: period to call agent's act - `exeution_timeout`: amount of time to limit single act/handle to execute. - `max_reactions`: the processing rate of envelopes per tick (i.e. single loop). - `decision_maker_handler_class`: the class implementing the decision maker handler to be used. @@ -46,16 +46,6 @@ Instantiate the agent. None - -#### decision`_`maker - -```python - | @property - | decision_maker() -> DecisionMaker -``` - -Get decision maker. - #### context @@ -86,25 +76,6 @@ Get resources. Set resources. - -#### task`_`manager - -```python - | @property - | task_manager() -> TaskManager -``` - -Get the task manager. - - -#### setup`_`multiplexer - -```python - | setup_multiplexer() -> None -``` - -Set up the multiplexer. - #### filter @@ -137,8 +108,6 @@ Set up the agent. Performs the following: - loads the resources (unless in programmatic mode) -- starts the task manager -- starts the decision maker - calls setup() on the resources **Returns**: @@ -160,17 +129,37 @@ Calls act() of each active behaviour. None - -#### react + +#### active`_`connections + +```python + | @property + | active_connections() -> List[Connection] +``` + +Return list of active connections. + + +#### get`_`multiplexer`_`setup`_`options ```python - | react() -> None + | get_multiplexer_setup_options() -> Optional[Dict] ``` -React to incoming envelopes. +Get options to pass to Multiplexer.setup. + +**Returns**: + +dict of kwargs + + +#### handle`_`envelope -Gets up to max_reactions number of envelopes from the inbox and -handles each envelope, which entailes: +```python + | handle_envelope(envelope: Envelope) -> None +``` + +Handle an envelope. - fetching the protocol referenced by the envelope, and - returning an envelope to sender if the protocol is unsupported, using the error handler, or @@ -178,22 +167,57 @@ handles each envelope, which entailes: - returning an envelope to sender if no active handler is available for the specified protocol, using the error handler, or - handling the message recovered from the envelope with all active handlers for the specified protocol. +**Arguments**: + +- `envelope`: the envelope to handle. + **Returns**: None - -#### update + +#### get`_`periodic`_`tasks ```python - | update() -> None + | get_periodic_tasks() -> Dict[Callable, Tuple[float, Optional[datetime.datetime]]] ``` -Update the current state of the agent. +Get all periodic tasks for agent. + +**Returns**: -Handles the internal messages from the skills to the decision maker. +dict of callable with period specified -:return None + +#### get`_`message`_`handlers + +```python + | get_message_handlers() -> List[Tuple[Callable[[Any], None], Callable]] +``` + +Get handlers with message getters. + +**Returns**: + +List of tuples of callables: handler and coroutine to get a message + + +#### exception`_`handler + +```python + | exception_handler(exception: Exception, function: Callable) -> bool +``` + +Handle exception raised during agent main loop execution. + +**Arguments**: + +- `exception`: exception raised +- `function`: a callable exception raised in. + +**Returns**: + +bool, propagate exception if True otherwise skip it. #### teardown @@ -206,11 +230,42 @@ Tear down the agent. Performs the following: -- stops the decision maker -- stops the task manager - tears down the resources. **Returns**: None + +#### get`_`task`_`result + +```python + | get_task_result(task_id: int) -> AsyncResult +``` + +Get the result from a task. + +**Returns**: + +async result for task_id + + +#### enqueue`_`task + +```python + | enqueue_task(func: Callable, args: Sequence = (), kwds: Optional[Dict[str, Any]] = None) -> int +``` + +Enqueue a task with the task manager. + +**Arguments**: + +- `func`: the callable instance to be enqueued +- `args`: the positional arguments to be passed to the function. +- `kwds`: the keyword arguments to be passed to the function. +:return the task id to get the the result. + +**Raises**: + +- `ValueError`: if the task manager is not running. + diff --git a/docs/api/aea_builder.md b/docs/api/aea_builder.md index ab0dd9c4fb..635fc39ec7 100644 --- a/docs/api/aea_builder.md +++ b/docs/api/aea_builder.md @@ -227,18 +227,18 @@ only resets: None - -#### set`_`timeout + +#### set`_`period ```python - | set_timeout(timeout: Optional[float]) -> "AEABuilder" + | set_period(period: Optional[float]) -> "AEABuilder" ``` -Set agent loop idle timeout in seconds. +Set agent act period. **Arguments**: -- `timeout`: timeout in seconds +- `period`: period in seconds **Returns**: diff --git a/docs/api/agent.md b/docs/api/agent.md index a1c5153b10..a3a96b151d 100644 --- a/docs/api/agent.md +++ b/docs/api/agent.md @@ -7,7 +7,7 @@ This module contains the implementation of a generic agent. ## Agent Objects ```python -class Agent(ABC) +class Agent(AbstractAgent) ``` This class provides an abstract base class for a generic agent. @@ -16,7 +16,7 @@ This class provides an abstract base class for a generic agent. #### `__`init`__` ```python - | __init__(identity: Identity, connections: List[Connection], loop: Optional[AbstractEventLoop] = None, timeout: float = 1.0, loop_mode: Optional[str] = None, runtime_mode: Optional[str] = None) -> None + | __init__(identity: Identity, connections: List[Connection], loop: Optional[AbstractEventLoop] = None, period: float = 1.0, loop_mode: Optional[str] = None, runtime_mode: Optional[str] = None) -> None ``` Instantiate the agent. @@ -26,7 +26,7 @@ Instantiate the agent. - `identity`: the identity of the agent. - `connections`: the list of connections of the agent. - `loop`: the event loop to run the connections. -- `timeout`: the time in (fractions of) seconds to time out an agent between act and react +- `period`: period to call agent's act - `loop_mode`: loop_mode to choose agent run loop. - `runtime_mode`: runtime mode to up agent. @@ -34,12 +34,32 @@ Instantiate the agent. None + +#### connections + +```python + | @property + | connections() -> List[Connection] +``` + +Return list of connections. + + +#### active`_`connections + +```python + | @property + | active_connections() -> List[Connection] +``` + +Return list of active connections. + #### is`_`running ```python | @property - | is_running() + | is_running() -> bool ``` Get running state of the runtime and agent. @@ -49,30 +69,33 @@ Get running state of the runtime and agent. ```python | @property - | is_stopped() + | is_stopped() -> bool ``` Get running state of the runtime and agent. - -#### identity + +#### get`_`multiplexer`_`setup`_`options ```python - | @property - | identity() -> Identity + | get_multiplexer_setup_options() -> Optional[Dict] ``` -Get the identity. +Get options to pass to Multiplexer.setup. + +**Returns**: - -#### multiplexer +dict of kwargs + + +#### identity ```python | @property - | multiplexer() -> Multiplexer + | identity() -> Identity ``` -Get the multiplexer. +Get the identity. #### inbox @@ -122,35 +145,32 @@ Get the tick or agent loop count. Each agent loop (one call to each one of act(), react(), update()) increments the tick. - -#### timeout + +#### handle`_`envelope ```python - | @property - | timeout() -> float + | handle_envelope(envelope: Envelope) -> None ``` -Get the time in (fractions of) seconds to time out an agent between act and react. +Handle an envelope. - -#### loop`_`mode +**Arguments**: -```python - | @property - | loop_mode() -> str -``` +- `envelope`: the envelope to handle. -Get the agent loop mode. +**Returns**: + +None - -#### main`_`loop + +#### period ```python | @property - | main_loop() -> BaseAgentLoop + | period() -> float ``` -Get the main agent loop. +Get a period to call act. #### runtime @@ -162,15 +182,6 @@ Get the main agent loop. Get the runtime. - -#### setup`_`multiplexer - -```python - | setup_multiplexer() -> None -``` - -Set up the multiplexer. - #### start @@ -199,23 +210,6 @@ While the liveness of the agent is not stopped it continues to loop over: None - -#### start`_`setup - -```python - | start_setup() -> None -``` - -Set up Agent on start. - -- connect Multiplexer -- call agent.setup -- set liveness to started - -**Returns**: - -None - #### stop @@ -235,85 +229,61 @@ Performs the following: None - -#### setup + +#### state ```python - | @abstractmethod - | setup() -> None + | @property + | state() -> RuntimeStates ``` -Set up the agent. +Get state of the agent's runtime. **Returns**: -None +RuntimeStates - -#### act + +#### get`_`periodic`_`tasks ```python - | @abstractmethod - | act() -> None + | get_periodic_tasks() -> Dict[Callable, Tuple[float, Optional[datetime.datetime]]] ``` -Perform actions. +Get all periodic tasks for agent. **Returns**: -None +dict of callable with period specified - -#### react + +#### get`_`message`_`handlers ```python - | @abstractmethod - | react() -> None + | get_message_handlers() -> List[Tuple[Callable[[Any], None], Callable]] ``` -React to events. +Get handlers with message getters. **Returns**: -None - - -#### update +List of tuples of callables: handler and coroutine to get a message -```python - | @abstractmethod - | update() -> None -``` - -Update the internals of the agent which are not exposed to the skills. - -:return None - - -#### teardown + +#### exception`_`handler ```python - | @abstractmethod - | teardown() -> None + | exception_handler(exception: Exception, function: Callable) -> bool ``` -Tear down the agent. +Handle exception raised during agent main loop execution. -**Returns**: - -None - - -#### state - -```python - | @property - | state() -> RuntimeStates -``` +**Arguments**: -Get state of the agent's runtime. +- `exception`: exception raised +- `function`: a callable exception raised in. **Returns**: -RuntimeStates +bool, propagate exception if True otherwise skip it. diff --git a/docs/api/agent_loop.md b/docs/api/agent_loop.md index 09ac5f31d6..9860d50b64 100644 --- a/docs/api/agent_loop.md +++ b/docs/api/agent_loop.md @@ -16,7 +16,7 @@ Base abstract agent loop class. #### `__`init`__` ```python - | __init__(agent: "Agent", loop: Optional[AbstractEventLoop] = None) -> None + | __init__(agent: AbstractAgent, loop: Optional[AbstractEventLoop] = None) -> None ``` Init loop. @@ -24,6 +24,16 @@ Init loop. :params agent: Agent or AEA to run. :params loop: optional asyncio event loop. if not specified a new loop will be created. + +#### agent + +```python + | @property + | agent() -> AbstractAgent +``` + +Get agent. + #### set`_`loop @@ -42,6 +52,24 @@ Set event loop and all event loopp related objects. Start agent loop synchronously in own asyncio loop. + +#### setup + +```python + | setup() -> None +``` + +Set up loop before started. + + +#### teardown + +```python + | teardown() +``` + +Tear down loop on stop. + #### run`_`loop @@ -110,30 +138,7 @@ Asyncio based agent loop suitable only for AEA. #### `__`init`__` ```python - | __init__(agent: "AEA", loop: AbstractEventLoop = None) -``` - -Init agent loop. - -**Arguments**: - -- `agent`: AEA instance -- `loop`: asyncio loop to use. optional - - -## SyncAgentLoop Objects - -```python -class SyncAgentLoop(BaseAgentLoop) -``` - -Synchronous agent loop. - - -#### `__`init`__` - -```python - | __init__(agent: "Agent", loop: AbstractEventLoop = None) + | __init__(agent: AbstractAgent, loop: AbstractEventLoop = None) ``` Init agent loop. diff --git a/docs/api/common.md b/docs/api/common.md new file mode 100644 index 0000000000..144a00aeb7 --- /dev/null +++ b/docs/api/common.md @@ -0,0 +1,5 @@ + +# aea.common + +This module contains the common types and interfaces used in the aea framework. + diff --git a/docs/api/components/base.md b/docs/api/components/base.md index 53f1e4b718..ce4d60e508 100644 --- a/docs/api/components/base.md +++ b/docs/api/components/base.md @@ -16,7 +16,7 @@ Abstract class for an agent component. #### `__`init`__` ```python - | __init__(configuration: Optional[ComponentConfiguration] = None, is_vendor: bool = False, **kwargs) + | __init__(configuration: Optional[ComponentConfiguration] = None, is_vendor: bool = False, **kwargs, ,) ``` Initialize a package. @@ -106,3 +106,22 @@ Get the directory. Raise error if it has not been set yet. Set the directory. Raise error if already set. + +#### load`_`aea`_`package + +```python +load_aea_package(configuration: ComponentConfiguration) -> None +``` + +Load the AEA package. + +It adds all the __init__.py modules into `sys.modules`. + +**Arguments**: + +- `configuration`: the configuration object. + +**Returns**: + +None + diff --git a/docs/api/configurations/base.md b/docs/api/configurations/base.md index 8b5a5d58c7..3cda203a4b 100644 --- a/docs/api/configurations/base.md +++ b/docs/api/configurations/base.md @@ -52,6 +52,15 @@ Get the plural name. >>> PackageType.CONTRACT.to_plural() 'contracts' + +#### configuration`_`class + +```python + | configuration_class() -> Type["PackageConfiguration"] +``` + +Get the configuration class. + #### `__`str`__` @@ -612,6 +621,51 @@ Get the version of the package. Get the package identifier without the version. + +#### from`_`uri`_`path + +```python + | @classmethod + | from_uri_path(cls, package_id_uri_path: str) -> "PackageId" +``` + +Initialize the public id from the string. + +>>> str(PackageId.from_uri_path("skill/author/package_name/0.1.0")) +'(skill, author/package_name:0.1.0)' + +A bad formatted input raises value error: +>>> PackageId.from_uri_path("very/bad/formatted:input") +Traceback (most recent call last): +... +ValueError: Input 'very/bad/formatted:input' is not well formatted. + +**Arguments**: + +- `public_id_uri_path`: the public id in uri path string format. + +**Returns**: + +the public id object. + +**Raises**: + +- `ValueError`: if the string in input is not well formatted. + + +#### to`_`uri`_`path + +```python + | @property + | to_uri_path() -> str +``` + +Turn the package id into a uri path string. + +**Returns**: + +uri path string + #### `__`hash`__` @@ -832,7 +886,6 @@ Get PyPI dependencies. ```python | @property - | @abstractmethod | component_type() -> ComponentType ``` @@ -868,26 +921,6 @@ Get the prefix import path for this component. Check whether the component is abstract. - -#### load - -```python - | @staticmethod - | load(component_type: ComponentType, directory: Path, skip_consistency_check: bool = False) -> "ComponentConfiguration" -``` - -Load configuration and check that it is consistent against the directory. - -**Arguments**: - -- `component_type`: the component type. -- `directory`: the root of the package -- `skip_consistency_check`: if True, the consistency check are skipped. - -**Returns**: - -the configuration object. - #### check`_`fingerprint @@ -912,6 +945,23 @@ Check that the AEA version matches the specifier set. :raises ValueError if the version of the aea framework falls within a specifier. + +#### update + +```python + | update(data: Dict) -> None +``` + +Update configuration with other data. + +**Arguments**: + +- `data`: the data to replace. + +**Returns**: + +None + ## ConnectionConfig Objects @@ -930,16 +980,6 @@ Handle connection configuration. Initialize a connection configuration object. - -#### component`_`type - -```python - | @property - | component_type() -> ComponentType -``` - -Get the component type. - #### package`_`dependencies @@ -970,6 +1010,23 @@ Return the JSON representation. Initialize from a JSON object. + +#### update + +```python + | update(data: Dict) -> None +``` + +Update configuration with other data. + +**Arguments**: + +- `data`: the data to replace. + +**Returns**: + +None + ## ProtocolConfig Objects @@ -988,16 +1045,6 @@ Handle protocol configuration. Initialize a connection configuration object. - -#### component`_`type - -```python - | @property - | component_type() -> ComponentType -``` - -Get the component type. - #### json @@ -1080,16 +1127,6 @@ Class to represent a skill configuration file. Initialize a skill configuration. - -#### component`_`type - -```python - | @property - | component_type() -> ComponentType -``` - -Get the component type. - #### package`_`dependencies @@ -1130,6 +1167,23 @@ Return the JSON representation. Initialize from a JSON object. + +#### update + +```python + | update(data: Dict) -> None +``` + +Update configuration with other data. + +**Arguments**: + +- `data`: the data to replace. + +**Returns**: + +None + ## AgentConfig Objects @@ -1143,11 +1197,31 @@ Class to represent the agent configuration file. #### `__`init`__` ```python - | __init__(agent_name: str, author: str, version: str = "", license_: str = "", aea_version: str = "", fingerprint: Optional[Dict[str, str]] = None, fingerprint_ignore_patterns: Optional[Sequence[str]] = None, registry_path: str = DEFAULT_REGISTRY_PATH, description: str = "", logging_config: Optional[Dict] = None, timeout: Optional[float] = None, execution_timeout: Optional[float] = None, max_reactions: Optional[int] = None, decision_maker_handler: Optional[Dict] = None, skill_exception_policy: Optional[str] = None, default_routing: Optional[Dict] = None, loop_mode: Optional[str] = None, runtime_mode: Optional[str] = None) + | __init__(agent_name: str, author: str, version: str = "", license_: str = "", aea_version: str = "", fingerprint: Optional[Dict[str, str]] = None, fingerprint_ignore_patterns: Optional[Sequence[str]] = None, registry_path: str = DEFAULT_REGISTRY_PATH, description: str = "", logging_config: Optional[Dict] = None, period: Optional[float] = None, execution_timeout: Optional[float] = None, max_reactions: Optional[int] = None, decision_maker_handler: Optional[Dict] = None, skill_exception_policy: Optional[str] = None, default_routing: Optional[Dict] = None, loop_mode: Optional[str] = None, runtime_mode: Optional[str] = None, component_configurations: Optional[Dict[ComponentId, Dict]] = None) ``` Instantiate the agent configuration object. + +#### component`_`configurations + +```python + | @property + | component_configurations() -> Dict[ComponentId, Dict] +``` + +Get the custom component configurations. + + +#### component`_`configurations + +```python + | @component_configurations.setter + | component_configurations(d: Dict[ComponentId, Dict]) -> None +``` + +Set the component configurations. + #### package`_`dependencies @@ -1234,6 +1308,15 @@ Set the default ledger. None + +#### component`_`configurations`_`json + +```python + | component_configurations_json() -> List[OrderedDict] +``` + +Get the component configurations in JSON format. + #### json @@ -1388,16 +1471,6 @@ Handle contract configuration. Initialize a protocol configuration object. - -#### component`_`type - -```python - | @property - | component_type() -> ComponentType -``` - -Get the component type. - #### contract`_`interfaces diff --git a/docs/api/configurations/components.md b/docs/api/configurations/components.md deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs/api/configurations/loader.md b/docs/api/configurations/loader.md index 495e700588..5898403b87 100644 --- a/docs/api/configurations/loader.md +++ b/docs/api/configurations/loader.md @@ -95,6 +95,23 @@ Load an agent configuration file. the configuration object. :raises + +#### validate + +```python + | validate(json_data: Dict) -> None +``` + +Validate a JSON object. + +**Arguments**: + +- `json_data`: the JSON data. + +**Returns**: + +None. + #### load @@ -102,7 +119,7 @@ the configuration object. | load(file_pointer: TextIO) -> T ``` -Load an agent configuration file. +Load a configuration file. **Arguments**: @@ -111,7 +128,6 @@ Load an agent configuration file. **Returns**: the configuration object. -:raises #### dump @@ -164,3 +180,22 @@ Get a config loader from the configuration type. - `configuration_type`: the configuration type + +#### load`_`component`_`configuration + +```python +load_component_configuration(component_type: ComponentType, directory: Path, skip_consistency_check: bool = False) -> "ComponentConfiguration" +``` + +Load configuration and check that it is consistent against the directory. + +**Arguments**: + +- `component_type`: the component type. +- `directory`: the root of the package +- `skip_consistency_check`: if True, the consistency check are skipped. + +**Returns**: + +the configuration object. + diff --git a/docs/api/helpers/pypi.md b/docs/api/configurations/pypi.md similarity index 85% rename from docs/api/helpers/pypi.md rename to docs/api/configurations/pypi.md index cb2c248f87..101e5abd5f 100644 --- a/docs/api/helpers/pypi.md +++ b/docs/api/configurations/pypi.md @@ -1,9 +1,9 @@ - -# aea.helpers.pypi + +# aea.configurations.pypi This module contains a checker for PyPI version consistency. - + #### and`_` ```python @@ -12,7 +12,7 @@ and_(s1: SpecifierSet, s2: SpecifierSet) Do the and between two specifier sets. - + #### is`_`satisfiable ```python @@ -55,7 +55,7 @@ https://www.python.org/dev/peps/pep-0440 False if the constraints are surely non-satisfiable, True if we don't know. - + #### is`_`simple`_`dep ```python @@ -74,7 +74,7 @@ Namely, if it has no field specified, or only the 'version' field set. whether it is a simple dependency or not - + #### to`_`set`_`specifier ```python @@ -83,7 +83,7 @@ to_set_specifier(dep: Dependency) -> SpecifierSet Get the set specifier. It assumes to be a simple dependency (see above). - + #### merge`_`dependencies ```python diff --git a/docs/api/connections/base.md b/docs/api/connections/base.md index 704f128a97..0d6b04372f 100644 --- a/docs/api/connections/base.md +++ b/docs/api/connections/base.md @@ -46,29 +46,11 @@ parameters are None: connection_id, excluded_protocols or restricted_to_protocol ```python | @property - | loop() -> Optional[AbstractEventLoop] + | loop() -> asyncio.AbstractEventLoop ``` Get the event loop. - -#### loop - -```python - | @loop.setter - | loop(loop: AbstractEventLoop) -> None -``` - -Set the event loop. - -**Arguments**: - -- `loop`: the event loop. - -**Returns**: - -None - #### address @@ -251,6 +233,16 @@ an instance of the concrete connection class. Return is connected state. + +#### is`_`connecting + +```python + | @property + | is_connecting() -> bool +``` + +Return is connecting state. + #### is`_`disconnected diff --git a/docs/api/context/base.md b/docs/api/context/base.md index 41b516b7c1..88e2c86721 100644 --- a/docs/api/context/base.md +++ b/docs/api/context/base.md @@ -16,7 +16,7 @@ Provide read access to relevant objects of the agent for the skills. #### `__`init`__` ```python - | __init__(identity: Identity, connection_status: ConnectionStatus, outbox: OutBox, decision_maker_message_queue: Queue, decision_maker_handler_context: SimpleNamespace, task_manager: TaskManager, default_connection: Optional[PublicId], default_routing: Dict[PublicId, PublicId], search_service_address: Address, **kwargs) + | __init__(identity: Identity, connection_status: MultiplexerStatus, outbox: OutBox, decision_maker_message_queue: Queue, decision_maker_handler_context: SimpleNamespace, task_manager: TaskManager, default_connection: Optional[PublicId], default_routing: Dict[PublicId, PublicId], search_service_address: Address, **kwargs) ``` Initialize an agent context. @@ -90,7 +90,7 @@ Get the default address. ```python | @property - | connection_status() -> ConnectionStatus + | connection_status() -> MultiplexerStatus ``` Get connection status of the multiplexer. diff --git a/docs/api/crypto/base.md b/docs/api/crypto/base.md index 5c2ba1a2ef..2b5d26b2d6 100644 --- a/docs/api/crypto/base.md +++ b/docs/api/crypto/base.md @@ -255,9 +255,9 @@ return the hash in hex. #### get`_`address`_`from`_`public`_`key ```python - | @staticmethod + | @classmethod | @abstractmethod - | get_address_from_public_key(public_key: str) -> str + | get_address_from_public_key(cls, public_key: str) -> str ``` Get the address from the public key. @@ -274,9 +274,9 @@ str #### recover`_`message ```python - | @staticmethod + | @classmethod | @abstractmethod - | recover_message(message: bytes, signature: str, is_deprecated_mode: bool = False) -> Tuple[Address, ...] + | recover_message(cls, message: bytes, signature: str, is_deprecated_mode: bool = False) -> Tuple[Address, ...] ``` Recover the addresses from the hash. diff --git a/docs/api/crypto/cosmos.md b/docs/api/crypto/cosmos.md index bb60dae0ea..8e4566442f 100644 --- a/docs/api/crypto/cosmos.md +++ b/docs/api/crypto/cosmos.md @@ -3,6 +3,130 @@ Cosmos module wrapping the public and private key cryptography and ledger api. + +## CosmosHelper Objects + +```python +class CosmosHelper(Helper) +``` + +Helper class usable as Mixin for CosmosApi or as standalone class. + + +#### is`_`transaction`_`settled + +```python + | @staticmethod + | is_transaction_settled(tx_receipt: Any) -> bool +``` + +Check whether a transaction is settled or not. + +**Arguments**: + +- `tx_digest`: the digest associated to the transaction. + +**Returns**: + +True if the transaction has been settled, False o/w. + + +#### is`_`transaction`_`valid + +```python + | @staticmethod + | is_transaction_valid(tx: Any, seller: Address, client: Address, tx_nonce: str, amount: int) -> bool +``` + +Check whether a transaction is valid or not. + +**Arguments**: + +- `tx`: the transaction. +- `seller`: the address of the seller. +- `client`: the address of the client. +- `tx_nonce`: the transaction nonce. +- `amount`: the amount we expect to get from the transaction. + +**Returns**: + +True if the random_message is equals to tx['input'] + + +#### generate`_`tx`_`nonce + +```python + | @staticmethod + | generate_tx_nonce(seller: Address, client: Address) -> str +``` + +Generate a unique hash to distinguish txs with the same terms. + +**Arguments**: + +- `seller`: the address of the seller. +- `client`: the address of the client. + +**Returns**: + +return the hash in hex. + + +#### get`_`address`_`from`_`public`_`key + +```python + | @classmethod + | get_address_from_public_key(cls, public_key: str) -> str +``` + +Get the address from the public key. + +**Arguments**: + +- `public_key`: the public key + +**Returns**: + +str + + +#### recover`_`message + +```python + | @classmethod + | recover_message(cls, message: bytes, signature: str, is_deprecated_mode: bool = False) -> Tuple[Address, ...] +``` + +Recover the addresses from the hash. + +**Arguments**: + +- `message`: the message we expect +- `signature`: the transaction signature +- `is_deprecated_mode`: if the deprecated signing was used + +**Returns**: + +the recovered addresses + + +#### get`_`hash + +```python + | @staticmethod + | get_hash(message: bytes) -> str +``` + +Get the hash of a message. + +**Arguments**: + +- `message`: the message to be hashed. + +**Returns**: + +the hash of the message. + ## CosmosCrypto Objects @@ -111,7 +235,7 @@ signature of the message in string form | format_default_transaction(transaction: Any, signature: str, base64_pbk: str) -> Any ``` -Format default CosmosSDK transaction and add signature +Format default CosmosSDK transaction and add signature. **Arguments**: @@ -131,7 +255,7 @@ formatted transaction with signature | format_wasm_transaction(transaction: Any, signature: str, base64_pbk: str) -> Any ``` -Format CosmWasm transaction and add signature +Format CosmWasm transaction and add signature. **Arguments**: @@ -187,149 +311,25 @@ Serialize crypto object as binary stream to `fp` (a `.write()`-supporting file-l None - -## CosmosHelper Objects + +## `_`CosmosApi Objects ```python -class CosmosHelper(Helper) -``` - -Helper class usable as Mixin for CosmosApi or as standalone class. - - -#### is`_`transaction`_`settled - -```python - | @staticmethod - | is_transaction_settled(tx_receipt: Any) -> bool -``` - -Check whether a transaction is settled or not. - -**Arguments**: - -- `tx_digest`: the digest associated to the transaction. - -**Returns**: - -True if the transaction has been settled, False o/w. - - -#### is`_`transaction`_`valid - -```python - | @staticmethod - | is_transaction_valid(tx: Any, seller: Address, client: Address, tx_nonce: str, amount: int) -> bool -``` - -Check whether a transaction is valid or not. - -**Arguments**: - -- `tx`: the transaction. -- `seller`: the address of the seller. -- `client`: the address of the client. -- `tx_nonce`: the transaction nonce. -- `amount`: the amount we expect to get from the transaction. - -**Returns**: - -True if the random_message is equals to tx['input'] - - -#### generate`_`tx`_`nonce - -```python - | @staticmethod - | generate_tx_nonce(seller: Address, client: Address) -> str -``` - -Generate a unique hash to distinguish txs with the same terms. - -**Arguments**: - -- `seller`: the address of the seller. -- `client`: the address of the client. - -**Returns**: - -return the hash in hex. - - -#### get`_`address`_`from`_`public`_`key - -```python - | @staticmethod - | get_address_from_public_key(public_key: str) -> str -``` - -Get the address from the public key. - -**Arguments**: - -- `public_key`: the public key - -**Returns**: - -str - - -#### recover`_`message - -```python - | @staticmethod - | recover_message(message: bytes, signature: str, is_deprecated_mode: bool = False) -> Tuple[Address, ...] -``` - -Recover the addresses from the hash. - -**Arguments**: - -- `message`: the message we expect -- `signature`: the transaction signature -- `is_deprecated_mode`: if the deprecated signing was used - -**Returns**: - -the recovered addresses - - -#### get`_`hash - -```python - | @staticmethod - | get_hash(message: bytes) -> str -``` - -Get the hash of a message. - -**Arguments**: - -- `message`: the message to be hashed. - -**Returns**: - -the hash of the message. - - -## CosmosApi Objects - -```python -class CosmosApi(LedgerApi, CosmosHelper) +class _CosmosApi(LedgerApi) ``` Class to interact with the Cosmos SDK via a HTTP APIs. - + #### `__`init`__` ```python | __init__(**kwargs) ``` -Initialize the Ethereum ledger APIs. +Initialize the Cosmos ledger APIs. - + #### api ```python @@ -339,7 +339,7 @@ Initialize the Ethereum ledger APIs. Get the underlying API object. - + #### get`_`balance ```python @@ -348,7 +348,7 @@ Get the underlying API object. Get the balance of a given account. - + #### get`_`deploy`_`transaction ```python @@ -369,7 +369,7 @@ Create a CosmWasm bytecode deployment transaction. the unsigned CosmWasm contract deploy message - + #### get`_`init`_`transaction ```python @@ -394,7 +394,7 @@ Create a CosmWasm InitMsg transaction. the unsigned CosmWasm InitMsg - + #### get`_`handle`_`transaction ```python @@ -416,7 +416,7 @@ Create a CosmWasm HandleMsg transaction. the unsigned CosmWasm HandleMsg - + #### try`_`execute`_`wasm`_`transaction ```python @@ -438,7 +438,7 @@ Execute a CosmWasm Transaction. QueryMsg doesn't require signing. the transaction digest - + #### try`_`execute`_`wasm`_`query ```python @@ -461,7 +461,7 @@ Execute a CosmWasm QueryMsg. QueryMsg doesn't require signing. the message receipt - + #### get`_`transfer`_`transaction ```python @@ -486,7 +486,7 @@ Submit a transfer transaction to the ledger. the transfer transaction - + #### send`_`signed`_`transaction ```python @@ -503,7 +503,7 @@ Send a signed transaction and wait for confirmation. tx_digest, if present - + #### is`_`cosmwasm`_`transaction ```python @@ -513,7 +513,7 @@ tx_digest, if present Check whether it is a cosmwasm tx. - + #### is`_`transfer`_`transaction ```python @@ -523,7 +523,7 @@ Check whether it is a cosmwasm tx. Check whether it is a transfer tx. - + #### get`_`transaction`_`receipt ```python @@ -540,7 +540,7 @@ Get the transaction receipt for a transaction digest. the tx receipt, if present - + #### get`_`transaction ```python @@ -557,7 +557,7 @@ Get the transaction for a transaction digest. the tx, if present - + #### get`_`contract`_`instance ```python @@ -575,6 +575,45 @@ Get the instance of a contract. the contract instance + +#### get`_`last`_`code`_`id + +```python + | get_last_code_id() -> int +``` + +Get ID of latest deployed .wasm bytecode. + +**Returns**: + +code id of last deployed .wasm bytecode + + +#### get`_`contract`_`address + +```python + | get_contract_address(code_id: int) -> str +``` + +Get contract address of latest initialised contract by its ID. + +**Arguments**: + +- `code_id`: id of deployed CosmWasm bytecode + +**Returns**: + +contract address of last initialised contract + + +## CosmosApi Objects + +```python +class CosmosApi(_CosmosApi, CosmosHelper) +``` + +Class to interact with the Cosmos SDK via a HTTP APIs. + ## CosmWasmCLIWrapper Objects @@ -593,6 +632,15 @@ class CosmosFaucetApi(FaucetApi) Cosmos testnet faucet API. + +#### `__`init`__` + +```python + | __init__(poll_interval=None) +``` + +Initialize CosmosFaucetApi. + #### get`_`wealth @@ -609,4 +657,5 @@ Get wealth from the faucet for the provided address. **Returns**: None +:raises: RuntimeError of explicit faucet failures diff --git a/docs/api/crypto/ethereum.md b/docs/api/crypto/ethereum.md index f539a47e21..c6f1dc926e 100644 --- a/docs/api/crypto/ethereum.md +++ b/docs/api/crypto/ethereum.md @@ -219,8 +219,8 @@ return the hash in hex. #### get`_`address`_`from`_`public`_`key ```python - | @staticmethod - | get_address_from_public_key(public_key: str) -> str + | @classmethod + | get_address_from_public_key(cls, public_key: str) -> str ``` Get the address from the public key. @@ -237,8 +237,8 @@ str #### recover`_`message ```python - | @staticmethod - | recover_message(message: bytes, signature: str, is_deprecated_mode: bool = False) -> Tuple[Address, ...] + | @classmethod + | recover_message(cls, message: bytes, signature: str, is_deprecated_mode: bool = False) -> Tuple[Address, ...] ``` Recover the addresses from the hash. diff --git a/docs/api/crypto/fetchai.md b/docs/api/crypto/fetchai.md index 0f77b38019..1a32e4c55f 100644 --- a/docs/api/crypto/fetchai.md +++ b/docs/api/crypto/fetchai.md @@ -3,279 +3,29 @@ Fetchai module wrapping the public and private key cryptography and ledger api. - -## FetchAICrypto Objects - -```python -class FetchAICrypto(Crypto[Entity]) -``` - -Class wrapping the Entity Generation from Fetch.AI ledger. - - -#### `__`init`__` - -```python - | __init__(private_key_path: Optional[str] = None) -``` - -Instantiate a fetchai crypto object. - -**Arguments**: - -- `private_key_path`: the private key path of the agent - - -#### private`_`key - -```python - | @property - | private_key() -> str -``` - -Return a private key. - -**Returns**: - -a private key string - - -#### public`_`key - -```python - | @property - | public_key() -> str -``` - -Return a public key in hex format. - -**Returns**: - -a public key string in hex format - - -#### address - -```python - | @property - | address() -> str -``` - -Return the address for the key pair. - -**Returns**: - -a display_address str - - -#### load`_`private`_`key`_`from`_`path - -```python - | @classmethod - | load_private_key_from_path(cls, file_name: str) -> Entity -``` - -Load a private key in hex format from a file. - -**Arguments**: - -- `file_name`: the path to the hex file. - -**Returns**: - -the Entity. - - -#### generate`_`private`_`key - -```python - | @classmethod - | generate_private_key(cls) -> Entity -``` - -Generate a key pair for fetchai network. - - -#### sign`_`message - -```python - | sign_message(message: bytes, is_deprecated_mode: bool = False) -> str -``` - -Sign a message in bytes string form. - -**Arguments**: - -- `message`: the message we want to send -- `is_deprecated_mode`: if the deprecated signing is used - -**Returns**: - -signature of the message in string form - - -#### sign`_`transaction - -```python - | sign_transaction(transaction: Any) -> Any -``` - -Sign a transaction in bytes string form. - -**Arguments**: - -- `transaction`: the transaction to be signed - -**Returns**: - -signed transaction - - -#### dump - -```python - | dump(fp: BinaryIO) -> None -``` - -Serialize crypto object as binary stream to `fp` (a `.write()`-supporting file-like object). - -**Arguments**: - -- `fp`: the output file pointer. Must be set in binary mode (mode='wb') - -**Returns**: - -None - ## FetchAIHelper Objects ```python -class FetchAIHelper(Helper) +class FetchAIHelper(CosmosHelper) ``` Helper class usable as Mixin for FetchAIApi or as standalone class. - -#### is`_`transaction`_`settled - -```python - | @staticmethod - | is_transaction_settled(tx_receipt: Any) -> bool -``` - -Check whether a transaction is settled or not. - -**Arguments**: - -- `tx_digest`: the digest associated to the transaction. - -**Returns**: - -True if the transaction has been settled, False o/w. - - -#### is`_`transaction`_`valid - -```python - | @staticmethod - | is_transaction_valid(tx: Any, seller: Address, client: Address, tx_nonce: str, amount: int) -> bool -``` - -Check whether a transaction is valid or not. - -**Arguments**: - -- `tx`: the transaction. -- `seller`: the address of the seller. -- `client`: the address of the client. -- `tx_nonce`: the transaction nonce. -- `amount`: the amount we expect to get from the transaction. - -**Returns**: - -True if the random_message is equals to tx['input'] - - -#### generate`_`tx`_`nonce - -```python - | @staticmethod - | generate_tx_nonce(seller: Address, client: Address) -> str -``` - -Generate a unique hash to distinguish txs with the same terms. - -**Arguments**: - -- `seller`: the address of the seller. -- `client`: the address of the client. - -**Returns**: - -return the hash in hex. - - -#### get`_`address`_`from`_`public`_`key - -```python - | @staticmethod - | get_address_from_public_key(public_key: str) -> Address -``` - -Get the address from the public key. - -**Arguments**: - -- `public_key`: the public key - -**Returns**: - -str - - -#### recover`_`message - -```python - | @staticmethod - | recover_message(message: bytes, signature: str, is_deprecated_mode: bool = False) -> Tuple[Address, ...] -``` - -Recover the addresses from the hash. - -**Arguments**: - -- `message`: the message we expect -- `signature`: the transaction signature -- `is_deprecated_mode`: if the deprecated signing was used - -**Returns**: - -the recovered addresses - - -#### get`_`hash + +## FetchAICrypto Objects ```python - | @staticmethod - | get_hash(message: bytes) -> str +class FetchAICrypto(CosmosCrypto) ``` -Get the hash of a message. - -**Arguments**: - -- `message`: the message to be hashed. - -**Returns**: - -the hash of the message. +Class wrapping the Entity Generation from Fetch.AI ledger. ## FetchAIApi Objects ```python -class FetchAIApi(LedgerApi, FetchAIHelper) +class FetchAIApi(_CosmosApi, FetchAIHelper) ``` Class to interact with the Fetch ledger APIs. @@ -287,163 +37,14 @@ Class to interact with the Fetch ledger APIs. | __init__(**kwargs) ``` -Initialize the Fetch.AI ledger APIs. - -**Arguments**: - -- `kwargs`: key word arguments (expects either a pair of 'host' and 'port' or a 'network') - - -#### api - -```python - | @property - | api() -> FetchaiLedgerApi -``` - -Get the underlying API object. - - -#### get`_`balance - -```python - | get_balance(address: Address) -> Optional[int] -``` - -Get the balance of a given account. - -**Arguments**: - -- `address`: the address for which to retrieve the balance. - -**Returns**: - -the balance, if retrivable, otherwise None - - -#### get`_`transfer`_`transaction - -```python - | get_transfer_transaction(sender_address: Address, destination_address: Address, amount: int, tx_fee: int, tx_nonce: str, **kwargs, ,) -> Optional[Any] -``` - -Submit a transfer transaction to the ledger. - -**Arguments**: - -- `sender_address`: the sender address of the payer. -- `destination_address`: the destination address of the payee. -- `amount`: the amount of wealth to be transferred. -- `tx_fee`: the transaction fee. -- `tx_nonce`: verifies the authenticity of the tx - -**Returns**: - -the transfer transaction - - -#### send`_`signed`_`transaction - -```python - | send_signed_transaction(tx_signed: Any) -> Optional[str] -``` - -Send a signed transaction and wait for confirmation. - -**Arguments**: - -- `tx_signed`: the signed transaction - - -#### get`_`transaction`_`receipt - -```python - | get_transaction_receipt(tx_digest: str) -> Optional[Any] -``` - -Get the transaction receipt for a transaction digest (non-blocking). - -**Arguments**: - -- `tx_digest`: the digest associated to the transaction. - -**Returns**: - -the tx receipt, if present - - -#### get`_`transaction - -```python - | get_transaction(tx_digest: str) -> Optional[Any] -``` - -Get the transaction for a transaction digest. - -**Arguments**: - -- `tx_digest`: the digest associated to the transaction. - -**Returns**: - -the tx, if present - - -#### get`_`contract`_`instance - -```python - | get_contract_instance(contract_interface: Dict[str, str], contract_address: Optional[str] = None) -> Any -``` - -Get the instance of a contract. - -**Arguments**: - -- `contract_interface`: the contract interface. -- `contract_address`: the contract address. - -**Returns**: - -the contract instance - - -#### get`_`deploy`_`transaction - -```python - | get_deploy_transaction(contract_interface: Dict[str, str], deployer_address: Address, **kwargs, ,) -> Dict[str, Any] -``` - -Get the transaction to deploy the smart contract. - -**Arguments**: - -- `contract_interface`: the contract interface. -- `deployer_address`: The address that will deploy the contract. -:returns tx: the transaction dictionary. +Initialize the Fetch.ai ledger APIs. ## FetchAIFaucetApi Objects ```python -class FetchAIFaucetApi(FaucetApi) +class FetchAIFaucetApi(CosmosFaucetApi) ``` Fetchai testnet faucet API. - -#### get`_`wealth - -```python - | get_wealth(address: Address) -> None -``` - -Get wealth from the faucet for the provided address. - -**Arguments**: - -- `address`: the address. - -**Returns**: - -None - diff --git a/docs/api/crypto/helpers.md b/docs/api/crypto/helpers.md index ac2323abfb..a586c2b0df 100644 --- a/docs/api/crypto/helpers.md +++ b/docs/api/crypto/helpers.md @@ -14,7 +14,12 @@ Verify or create private keys. **Arguments**: -- `ctx`: Context +- `aea_project_path`: path to an AEA project. +- `exit_on_error`: whether we should exit the program on error. + +**Returns**: + +the agent configuration. #### try`_`validate`_`private`_`key`_`path @@ -58,7 +63,7 @@ None #### try`_`generate`_`testnet`_`wealth ```python -try_generate_testnet_wealth(identifier: str, address: str) -> None +try_generate_testnet_wealth(identifier: str, address: str, _sync: bool = True) -> None ``` Try generate wealth on a testnet. @@ -67,6 +72,7 @@ Try generate wealth on a testnet. - `identifier`: the identifier of the ledger - `address`: the address to check for +- `_sync`: whether to wait to sync or not; currently unused **Returns**: diff --git a/docs/api/crypto/registries/base.md b/docs/api/crypto/registries/base.md index e4b435a360..35d4a355aa 100644 --- a/docs/api/crypto/registries/base.md +++ b/docs/api/crypto/registries/base.md @@ -12,15 +12,6 @@ class ItemId(RegexConstrainedString) The identifier of an item class. - -#### `__`init`__` - -```python - | __init__(seq) -``` - -Initialize the item id. - #### name diff --git a/docs/api/decision_maker/default.md b/docs/api/decision_maker/default.md index 1af944e0e3..722dcf4d70 100644 --- a/docs/api/decision_maker/default.md +++ b/docs/api/decision_maker/default.md @@ -29,42 +29,6 @@ Initialize dialogues. None - -#### role`_`from`_`first`_`message - -```python - | @staticmethod - | role_from_first_message(message: Message) -> BaseDialogue.Role -``` - -Infer the role of the agent from an incoming/outgoing first message - -**Arguments**: - -- `message`: an incoming/outgoing first message - -**Returns**: - -The role of the agent - - -#### create`_`dialogue - -```python - | create_dialogue(dialogue_label: BaseDialogueLabel, role: BaseDialogue.Role) -> SigningDialogue -``` - -Create an instance of fipa dialogue. - -**Arguments**: - -- `dialogue_label`: the identifier of the dialogue -- `role`: the role of the agent this dialogue is maintained for - -**Returns**: - -the created dialogue - ## StateUpdateDialogues Objects @@ -91,42 +55,6 @@ Initialize dialogues. None - -#### role`_`from`_`first`_`message - -```python - | @staticmethod - | role_from_first_message(message: Message) -> BaseDialogue.Role -``` - -Infer the role of the agent from an incoming/outgoing first message - -**Arguments**: - -- `message`: an incoming/outgoing first message - -**Returns**: - -The role of the agent - - -#### create`_`dialogue - -```python - | create_dialogue(dialogue_label: BaseDialogueLabel, role: BaseDialogue.Role) -> StateUpdateDialogue -``` - -Create an instance of fipa dialogue. - -**Arguments**: - -- `dialogue_label`: the identifier of the dialogue -- `role`: the role of the agent this dialogue is maintained for - -**Returns**: - -the created dialogue - ## GoalPursuitReadiness Objects diff --git a/docs/api/decision_maker/messages/base.md b/docs/api/decision_maker/messages/base.md deleted file mode 100644 index e1997c15d7..0000000000 --- a/docs/api/decision_maker/messages/base.md +++ /dev/null @@ -1,117 +0,0 @@ - -# aea.decision`_`maker.messages.base - -This module contains the base message and serialization definition. - - -## InternalMessage Objects - -```python -class InternalMessage() -``` - -This class implements a message. - - -#### `__`init`__` - -```python - | __init__(body: Optional[Dict] = None, **kwargs) -``` - -Initialize a Message object. - -**Arguments**: - -- `body`: the dictionary of values to hold. -- `kwargs`: any additional value to add to the body. It will overwrite the body values. - - -#### body - -```python - | @body.setter - | body(body: Dict) -> None -``` - -Set the body of hte message. - -**Arguments**: - -- `body`: the body. - -**Returns**: - -None - - -#### set - -```python - | set(key: str, value: Any) -> None -``` - -Set key and value pair. - -**Arguments**: - -- `key`: the key. -- `value`: the value. - -**Returns**: - -None - - -#### get - -```python - | get(key: str) -> Optional[Any] -``` - -Get value for key. - - -#### unset - -```python - | unset(key: str) -> None -``` - -Unset value for key. - -**Arguments**: - -- `key`: the key to unset the value of - - -#### is`_`set - -```python - | is_set(key: str) -> bool -``` - -Check value is set for key. - -**Arguments**: - -- `key`: the key to check - - -#### `__`eq`__` - -```python - | __eq__(other) -``` - -Compare with another object. - - -#### `__`str`__` - -```python - | __str__() -``` - -Get the string representation of the message. - diff --git a/docs/api/decision_maker/messages/state_update.md b/docs/api/decision_maker/messages/state_update.md deleted file mode 100644 index 9a66ed9878..0000000000 --- a/docs/api/decision_maker/messages/state_update.md +++ /dev/null @@ -1,98 +0,0 @@ - -# aea.decision`_`maker.messages.state`_`update - -The state update message module. - - -## StateUpdateMessage Objects - -```python -class StateUpdateMessage(InternalMessage) -``` - -The state update message class. - - -## Performative Objects - -```python -class Performative(Enum) -``` - -State update performative. - - -#### `__`init`__` - -```python - | __init__(performative: Performative, amount_by_currency_id: Currencies, quantities_by_good_id: Goods, **kwargs) -``` - -Instantiate transaction message. - -**Arguments**: - -- `performative`: the performative -- `amount_by_currency_id`: the amounts of currencies. -- `quantities_by_good_id`: the quantities of goods. - - -#### performative - -```python - | @property - | performative() -> Performative -``` - -Get the performative of the message. - - -#### amount`_`by`_`currency`_`id - -```python - | @property - | amount_by_currency_id() -> Currencies -``` - -Get the amount by currency. - - -#### quantities`_`by`_`good`_`id - -```python - | @property - | quantities_by_good_id() -> Goods -``` - -Get the quantities by good id. - - -#### exchange`_`params`_`by`_`currency`_`id - -```python - | @property - | exchange_params_by_currency_id() -> ExchangeParams -``` - -Get the exchange parameters by currency from the message. - - -#### utility`_`params`_`by`_`good`_`id - -```python - | @property - | utility_params_by_good_id() -> UtilityParams -``` - -Get the utility parameters by good id. - - -#### tx`_`fee - -```python - | @property - | tx_fee() -> int -``` - -Get the transaction fee. - diff --git a/docs/api/decision_maker/messages/transaction.md b/docs/api/decision_maker/messages/transaction.md deleted file mode 100644 index 83352755bf..0000000000 --- a/docs/api/decision_maker/messages/transaction.md +++ /dev/null @@ -1,286 +0,0 @@ - -# aea.decision`_`maker.messages.transaction - -The transaction message module. - - -## TransactionMessage Objects - -```python -class TransactionMessage(InternalMessage) -``` - -The transaction message class. - - -## Performative Objects - -```python -class Performative(Enum) -``` - -Transaction performative. - - -#### `__`init`__` - -```python - | __init__(performative: Performative, skill_callback_ids: Sequence[PublicId], tx_id: TransactionId, tx_sender_addr: Address, tx_counterparty_addr: Address, tx_amount_by_currency_id: Dict[str, int], tx_sender_fee: int, tx_counterparty_fee: int, tx_quantities_by_good_id: Dict[str, int], ledger_id: LedgerId, info: Dict[str, Any], **kwargs) -``` - -Instantiate transaction message. - -**Arguments**: - -- `performative`: the performative -- `skill_callback_ids`: the list public ids of skills to receive the transaction message response -- `tx_id`: the id of the transaction. -- `tx_sender_addr`: the sender address of the transaction. -- `tx_counterparty_addr`: the counterparty address of the transaction. -- `tx_amount_by_currency_id`: the amount by the currency of the transaction. -- `tx_sender_fee`: the part of the tx fee paid by the sender -- `tx_counterparty_fee`: the part of the tx fee paid by the counterparty -- `tx_quantities_by_good_id`: a map from good id to the quantity of that good involved in the transaction. -- `ledger_id`: the ledger id -- `info`: a dictionary for arbitrary information - - -#### performative - -```python - | @property - | performative() -> Performative -``` - -Get the performative of the message. - - -#### skill`_`callback`_`ids - -```python - | @property - | skill_callback_ids() -> List[PublicId] -``` - -Get the list of skill_callback_ids from the message. - - -#### tx`_`id - -```python - | @property - | tx_id() -> str -``` - -Get the transaction id. - - -#### tx`_`sender`_`addr - -```python - | @property - | tx_sender_addr() -> Address -``` - -Get the address of the sender. - - -#### tx`_`counterparty`_`addr - -```python - | @property - | tx_counterparty_addr() -> Address -``` - -Get the counterparty of the message. - - -#### tx`_`amount`_`by`_`currency`_`id - -```python - | @property - | tx_amount_by_currency_id() -> Dict[str, int] -``` - -Get the currency id. - - -#### tx`_`sender`_`fee - -```python - | @property - | tx_sender_fee() -> int -``` - -Get the fee for the sender from the messgae. - - -#### tx`_`counterparty`_`fee - -```python - | @property - | tx_counterparty_fee() -> int -``` - -Get the fee for the counterparty from the messgae. - - -#### tx`_`quantities`_`by`_`good`_`id - -```python - | @property - | tx_quantities_by_good_id() -> Dict[str, int] -``` - -Get the quantities by good ids. - - -#### ledger`_`id - -```python - | @property - | ledger_id() -> LedgerId -``` - -Get the ledger_id. - - -#### info - -```python - | @property - | info() -> Dict[str, Any] -``` - -Get the infos from the message. - - -#### tx`_`nonce - -```python - | @property - | tx_nonce() -> str -``` - -Get the tx_nonce from the message. - - -#### tx`_`digest - -```python - | @property - | tx_digest() -> str -``` - -Get the transaction digest. - - -#### signing`_`payload - -```python - | @property - | signing_payload() -> Dict[str, Any] -``` - -Get the signing payload. - - -#### signed`_`payload - -```python - | @property - | signed_payload() -> Dict[str, Any] -``` - -Get the signed payload. - - -#### amount - -```python - | @property - | amount() -> int -``` - -Get the amount. - - -#### currency`_`id - -```python - | @property - | currency_id() -> str -``` - -Get the currency id. - - -#### sender`_`amount - -```python - | @property - | sender_amount() -> int -``` - -Get the amount which the sender gets/pays as part of the tx. - - -#### counterparty`_`amount - -```python - | @property - | counterparty_amount() -> int -``` - -Get the amount which the counterparty gets/pays as part of the tx. - - -#### fees - -```python - | @property - | fees() -> int -``` - -Get the tx fees. - - -#### respond`_`settlement - -```python - | @classmethod - | respond_settlement(cls, other: "TransactionMessage", performative: Performative, tx_digest: Optional[str] = None) -> "TransactionMessage" -``` - -Create response message. - -**Arguments**: - -- `other`: TransactionMessage -- `performative`: the performative -- `tx_digest`: the transaction digest - -**Returns**: - -a transaction message object - - -#### respond`_`signing - -```python - | @classmethod - | respond_signing(cls, other: "TransactionMessage", performative: Performative, signed_payload: Optional[Dict[str, Any]] = None) -> "TransactionMessage" -``` - -Create response message. - -**Arguments**: - -- `other`: TransactionMessage -- `performative`: the performative -- `signed_payload`: the signed payload - -**Returns**: - -a transaction message object - diff --git a/docs/api/exceptions.md b/docs/api/exceptions.md new file mode 100644 index 0000000000..50e3253bb6 --- /dev/null +++ b/docs/api/exceptions.md @@ -0,0 +1,47 @@ + +# aea.exceptions + +Exceptions for the AEA package. + + +## AEAException Objects + +```python +class AEAException(Exception) +``` + +User-defined exception for the AEA framework. + + +## AEAPackageLoadingError Objects + +```python +class AEAPackageLoadingError(AEAException) +``` + +Class for exceptions that are raised for loading errors of AEA packages. + + +## AEAEnforceError Objects + +```python +class AEAEnforceError(AEAException) +``` + +Class for enforcement errors. + + +#### enforce + +```python +enforce(is_valid_condition: bool, exception_text: str, exception_class: Type[Exception] = AEAEnforceError) -> None +``` + +Evaluate a condition and raise an exception with the provided text if it is not satisfied. + +**Arguments**: + +- `is_valid_condition`: the valid condition +- `exception_text`: the exception to be raised +- `exception_class`: the class of exception + diff --git a/docs/api/helpers/async_utils.md b/docs/api/helpers/async_utils.md index 1d9339df08..8160fc9ae6 100644 --- a/docs/api/helpers/async_utils.md +++ b/docs/api/helpers/async_utils.md @@ -85,6 +85,26 @@ Wait state to be set. tuple of previous state and new state. + +#### transit + +```python + | @contextmanager + | transit(initial: Any = not_set, success: Any = not_set, fail: Any = not_set) -> Generator +``` + +Change state context according to success or not. + +**Arguments**: + +- `initial`: set state on context enter, not_set by default +- `success`: set state on context block done, not_set by default +- `fail`: set state on context block raises exception, not_set by default + +**Returns**: + +None + ## PeriodicCaller Objects @@ -135,7 +155,7 @@ Remove from schedule. #### ensure`_`loop ```python -ensure_loop(loop: AbstractEventLoop = None) -> AbstractEventLoop +ensure_loop(loop: Optional[AbstractEventLoop] = None) -> AbstractEventLoop ``` Use loop provided or create new if not provided or closed. @@ -286,6 +306,15 @@ class AwaitableProc() Async-friendly subprocess.Popen + +#### `__`init`__` + +```python + | __init__(*args, **kwargs) +``` + +Initialise awaitable proc. + #### start @@ -295,3 +324,56 @@ Async-friendly subprocess.Popen Start the subprocess + +## ItemGetter Objects + +```python +class ItemGetter() +``` + +Virtual queue like object to get items from getters function. + + +#### `__`init`__` + +```python + | __init__(getters: List[Callable]) -> None +``` + +Init ItemGetter. + +**Arguments**: + +- `getters`: List of couroutines to be awaited. + + +#### get + +```python + | async get() -> Any +``` + +Get item. + + +## HandlerItemGetter Objects + +```python +class HandlerItemGetter(ItemGetter) +``` + +ItemGetter with handler passed. + + +#### `__`init`__` + +```python + | __init__(getters: List[Tuple[Callable[[Any], None], Callable]]) +``` + +Init HandlerItemGetter. + +**Arguments**: + +- `getters`: List of tuples of handler and couroutine to be awaiteed for an item. + diff --git a/docs/api/helpers/base.md b/docs/api/helpers/base.md index 7e683c9b6f..708e1a5c7d 100644 --- a/docs/api/helpers/base.md +++ b/docs/api/helpers/base.md @@ -7,60 +7,62 @@ Miscellaneous helpers. #### yaml`_`load ```python -yaml_load(stream: TextIO) -> Dict[str, str] +@_ordered_loading +yaml_load(*args, **kwargs) -> Dict[str, Any] ``` Load a yaml from a file pointer in an ordered way. -**Arguments**: - -- `stream`: the file pointer - **Returns**: the yaml - -#### yaml`_`dump + +#### yaml`_`load`_`all ```python -yaml_dump(data, stream: TextIO) -> None +@_ordered_loading +yaml_load_all(*args, **kwargs) -> List[Dict[str, Any]] ``` -Dump data to a yaml file in an ordered way. +Load a multi-paged yaml from a file pointer in an ordered way. -**Arguments**: +**Returns**: -- `data`: the data to be dumped -- `stream`: the file pointer +the yaml - -#### locate + +#### yaml`_`dump ```python -locate(path: str) -> Any +@_ordered_dumping +yaml_dump(*args, **kwargs) -> None ``` -Locate an object by name or dotted path, importing as necessary. +Dump multi-paged yaml data to a yaml file in an ordered way. + +:return None - -#### load`_`aea`_`package + +#### yaml`_`dump`_`all ```python -load_aea_package(configuration: ComponentConfiguration) -> None +@_ordered_dumping +yaml_dump_all(*args, **kwargs) -> None ``` -Load the AEA package. - -It adds all the __init__.py modules into `sys.modules`. +Dump multi-paged yaml data to a yaml file in an ordered way. -**Arguments**: +:return None -- `configuration`: the configuration object. + +#### locate -**Returns**: +```python +locate(path: str) -> Any +``` -None +Locate an object by name or dotted path, importing as necessary. #### load`_`module diff --git a/docs/api/helpers/dialogue/base.md b/docs/api/helpers/dialogue/base.md index 2ddad349bd..45cebfd2dd 100644 --- a/docs/api/helpers/dialogue/base.md +++ b/docs/api/helpers/dialogue/base.md @@ -1,5 +1,5 @@ - -# aea.helpers.dialogue.base + +# aea.protocols.dialogue.base This module contains the classes required for dialogue management. @@ -7,7 +7,16 @@ This module contains the classes required for dialogue management. - Dialogue: The dialogue class maintains state of a dialogue and manages it. - Dialogues: The dialogues class keeps track of all dialogues. - + +## InvalidDialogueMessage Objects + +```python +class InvalidDialogueMessage(Exception) +``` + +Exception for adding invalid message to a dialogue. + + ## DialogueLabel Objects ```python @@ -16,7 +25,7 @@ class DialogueLabel() The dialogue label class acts as an identifier for dialogues. - + #### `__`init`__` ```python @@ -35,7 +44,7 @@ Initialize a dialogue label. None - + #### dialogue`_`reference ```python @@ -45,7 +54,7 @@ None Get the dialogue reference. - + #### dialogue`_`starter`_`reference ```python @@ -55,7 +64,7 @@ Get the dialogue reference. Get the dialogue starter reference. - + #### dialogue`_`responder`_`reference ```python @@ -65,7 +74,7 @@ Get the dialogue starter reference. Get the dialogue responder reference. - + #### dialogue`_`opponent`_`addr ```python @@ -75,7 +84,7 @@ Get the dialogue responder reference. Get the address of the dialogue opponent. - + #### dialogue`_`starter`_`addr ```python @@ -85,7 +94,7 @@ Get the address of the dialogue opponent. Get the address of the dialogue starter. - + #### `__`eq`__` ```python @@ -94,7 +103,7 @@ Get the address of the dialogue starter. Check for equality between two DialogueLabel objects. - + #### `__`hash`__` ```python @@ -103,7 +112,7 @@ Check for equality between two DialogueLabel objects. Turn object into hash. - + #### json ```python @@ -113,7 +122,7 @@ Turn object into hash. Return the JSON representation. - + #### from`_`json ```python @@ -123,7 +132,7 @@ Return the JSON representation. Get dialogue label from json. - + #### get`_`incomplete`_`version ```python @@ -132,7 +141,7 @@ Get dialogue label from json. Get the incomplete version of the label. - + #### `__`str`__` ```python @@ -141,7 +150,7 @@ Get the incomplete version of the label. Get the string representation. - + #### from`_`str ```python @@ -151,7 +160,7 @@ Get the string representation. Get the dialogue label from string representation. - + ## Dialogue Objects ```python @@ -160,7 +169,7 @@ class Dialogue(ABC) The dialogue class maintains state of a dialogue and manages it. - + ## Rules Objects ```python @@ -169,7 +178,7 @@ class Rules() This class defines the rules for the dialogue. - + #### `__`init`__` ```python @@ -188,7 +197,7 @@ Initialize a dialogue. None - + #### initial`_`performatives ```python @@ -202,7 +211,7 @@ Get the performatives one of which the terminal message in the dialogue must hav the valid performatives of an terminal message - + #### terminal`_`performatives ```python @@ -216,7 +225,7 @@ Get the performatives one of which the terminal message in the dialogue must hav the valid performatives of an terminal message - + #### valid`_`replies ```python @@ -230,7 +239,7 @@ Get all the valid performatives which are a valid replies to performatives. the full valid reply structure. - + #### get`_`valid`_`replies ```python @@ -247,7 +256,7 @@ Given a `performative`, return the list of performatives which are its valid rep list of valid performative replies - + ## Role Objects ```python @@ -256,7 +265,7 @@ class Role(Enum) This class defines the agent's role in a dialogue. - + #### `__`str`__` ```python @@ -265,7 +274,7 @@ This class defines the agent's role in a dialogue. Get the string representation. - + ## EndState Objects ```python @@ -274,7 +283,7 @@ class EndState(Enum) This class defines the end states of a dialogue. - + #### `__`str`__` ```python @@ -283,11 +292,11 @@ This class defines the end states of a dialogue. Get the string representation. - + #### `__`init`__` ```python - | __init__(dialogue_label: DialogueLabel, message_class: Optional[Type[Message]] = None, agent_address: Optional[Address] = None, role: Optional[Role] = None, rules: Optional[Rules] = None) -> None + | __init__(dialogue_label: DialogueLabel, message_class: Type[Message], self_address: Address, role: Role) -> None ``` Initialize a dialogue. @@ -295,15 +304,14 @@ Initialize a dialogue. **Arguments**: - `dialogue_label`: the identifier of the dialogue -- `agent_address`: the address of the agent for whom this dialogue is maintained +- `self_address`: the address of the entity for whom this dialogue is maintained - `role`: the role of the agent this dialogue is maintained for -- `rules`: the rules of the dialogue **Returns**: None - + #### dialogue`_`label ```python @@ -317,7 +325,7 @@ Get the dialogue label. The dialogue label - + #### incomplete`_`dialogue`_`label ```python @@ -331,7 +339,7 @@ Get the dialogue label. The incomplete dialogue label - + #### dialogue`_`labels ```python @@ -345,33 +353,21 @@ Get the dialogue labels (incomplete and complete, if it exists) the dialogue labels - -#### agent`_`address + +#### self`_`address ```python | @property - | agent_address() -> Address + | self_address() -> Address ``` -Get the address of the agent for whom this dialogues is maintained. +Get the address of the entity for whom this dialogues is maintained. **Returns**: -the agent address - - -#### agent`_`address - -```python - | @agent_address.setter - | agent_address(agent_address: Address) -> None -``` - -Set the address of the agent for whom this dialogues is maintained. - -:param: the agent address +the address of this entity - + #### role ```python @@ -385,25 +381,7 @@ Get the agent's role in the dialogue. the agent's role - -#### role - -```python - | @role.setter - | role(role: "Role") -> None -``` - -Set the agent's role in the dialogue. - -**Arguments**: - -- `role`: the agent's role - -**Returns**: - -None - - + #### rules ```python @@ -417,7 +395,7 @@ Get the dialogue rules. the rules - + #### is`_`self`_`initiated ```python @@ -431,7 +409,7 @@ Check whether the agent initiated the dialogue. True if the agent initiated the dialogue, False otherwise - + #### last`_`incoming`_`message ```python @@ -445,7 +423,7 @@ Get the last incoming message. the last incoming message if it exists, None otherwise - + #### last`_`outgoing`_`message ```python @@ -459,7 +437,7 @@ Get the last outgoing message. the last outgoing message if it exists, None otherwise - + #### last`_`message ```python @@ -473,24 +451,7 @@ Get the last message. the last message if it exists, None otherwise - -#### get`_`message - -```python - | get_message(message_id_to_find: int) -> Optional[Message] -``` - -Get the message whose id is 'message_id'. - -**Arguments**: - -- `message_id_to_find`: the id of the message - -**Returns**: - -the message if it exists, None otherwise - - + #### is`_`empty ```python @@ -504,66 +465,17 @@ Check whether the dialogue is empty. True if empty, False otherwise - -#### update - -```python - | update(message: Message) -> bool -``` - -Extend the list of incoming/outgoing messages with 'message', if 'message' belongs to dialogue and is valid. - -**Arguments**: - -- `message`: a message to be added - -**Returns**: - -True if message successfully added, false otherwise - - -#### ensure`_`counterparty - -```python - | ensure_counterparty(message: Message) -> None -``` - -Ensure the counterparty is set (set if not) correctly. - -**Arguments**: - -- `message`: a message - -**Returns**: - -None - - -#### is`_`belonging`_`to`_`dialogue - -```python - | is_belonging_to_dialogue(message: Message) -> bool -``` - -Check if the message is belonging to the dialogue. - -**Arguments**: - -- `message`: the message - -**Returns**: - -Ture if message is part of the dialogue, False otherwise - - + #### reply ```python - | reply(target_message: Message, performative, **kwargs) -> Message + | reply(performative: Message.Performative, target_message: Optional[Message] = None, **kwargs, ,) -> Message ``` Reply to the 'target_message' in this dialogue with a message with 'performative', and contents from kwargs. +Note if no target_message is provided, the last message in the dialogue will be replied to. + **Arguments**: - `target_message`: the message to reply to. @@ -574,63 +486,7 @@ Reply to the 'target_message' in this dialogue with a message with 'performative the reply message if it was successfully added as a reply, None otherwise. - -#### is`_`valid`_`next`_`message - -```python - | is_valid_next_message(message: Message) -> bool -``` - -Check whether 'message' is a valid next message in this dialogue. - -The evaluation of a message validity involves performing several categories of checks. -Each category of checks resides in a separate method. - -Currently, basic rules are fundamental structural constraints, -additional rules are applied for the time being, and more specific rules are captured in the is_valid method. - -**Arguments**: - -- `message`: the message to be validated - -**Returns**: - -True if yes, False otherwise. - - -#### update`_`dialogue`_`label - -```python - | update_dialogue_label(final_dialogue_label: DialogueLabel) -> None -``` - -Update the dialogue label of the dialogue. - -**Arguments**: - -- `final_dialogue_label`: the final dialogue label - - -#### is`_`valid - -```python - | @abstractmethod - | is_valid(message: Message) -> bool -``` - -Check whether 'message' is a valid next message in the dialogue. - -These rules capture specific constraints designed for dialogues which are instance of a concrete sub-class of this class. - -**Arguments**: - -- `message`: the message to be validated - -**Returns**: - -True if valid, False otherwise. - - + #### `__`str`__` ```python @@ -643,7 +499,7 @@ Get the string representation. The string representation of the dialogue - + ## DialogueStats Objects ```python @@ -652,7 +508,7 @@ class DialogueStats(ABC) Class to handle statistics on default dialogues. - + #### `__`init`__` ```python @@ -665,7 +521,7 @@ Initialize a StatsManager. - `end_states`: the list of dialogue endstates - + #### self`_`initiated ```python @@ -675,7 +531,7 @@ Initialize a StatsManager. Get the stats dictionary on self initiated dialogues. - + #### other`_`initiated ```python @@ -685,7 +541,7 @@ Get the stats dictionary on self initiated dialogues. Get the stats dictionary on other initiated dialogues. - + #### add`_`dialogue`_`endstate ```python @@ -703,7 +559,7 @@ Add dialogue endstate stats. None - + ## Dialogues Objects ```python @@ -712,25 +568,25 @@ class Dialogues(ABC) The dialogues class keeps track of all dialogues for an agent. - + #### `__`init`__` ```python - | __init__(agent_address: Address, end_states: FrozenSet[Dialogue.EndState], message_class: Optional[Type[Message]] = None, dialogue_class: Optional[Type[Dialogue]] = None, role_from_first_message: Optional[Callable[[Message], Dialogue.Role]] = None) -> None + | __init__(self_address: Address, end_states: FrozenSet[Dialogue.EndState], message_class: Type[Message], dialogue_class: Type[Dialogue], role_from_first_message: Callable[[Message, Address], Dialogue.Role]) -> None ``` Initialize dialogues. **Arguments**: -- `agent_address`: the address of the agent for whom dialogues are maintained +- `self_address`: the address of the entity for whom dialogues are maintained - `end_states`: the list of dialogue endstates **Returns**: None - + #### dialogues ```python @@ -740,17 +596,17 @@ None Get dictionary of dialogues in which the agent engages. - -#### agent`_`address + +#### self`_`address ```python | @property - | agent_address() -> Address + | self_address() -> Address ``` Get the address of the agent for whom dialogues are maintained. - + #### dialogue`_`stats ```python @@ -764,7 +620,24 @@ Get the dialogue statistics. dialogue stats object - + +#### get`_`dialogues`_`with`_`counterparty + +```python + | get_dialogues_with_counterparty(counterparty: Address) -> List[Dialogue] +``` + +Get the dialogues by address. + +**Arguments**: + +- `counterparty`: the counterparty + +**Returns**: + +The dialogues with the counterparty. + + #### new`_`self`_`initiated`_`dialogue`_`reference ```python @@ -777,7 +650,7 @@ Return a dialogue label for a new self initiated dialogue. the next nonce - + #### create ```python @@ -796,7 +669,25 @@ Create a dialogue with 'counterparty', with an initial message whose performativ the initial message and the dialogue. - + +#### create`_`with`_`message + +```python + | create_with_message(counterparty: Address, initial_message: Message) -> Dialogue +``` + +Create a dialogue with 'counterparty', with an initial message provided. + +**Arguments**: + +- `counterparty`: the counterparty of the dialogue. +- `initial_message`: the initial_message. + +**Returns**: + +the initial message and the dialogue. + + #### update ```python @@ -811,13 +702,13 @@ If there are any errors, e.g. the message dialogue reference does not exists or **Arguments**: -- `message`: a new message +- `message`: a new incoming message **Returns**: the new or existing dialogue the message is intended for, or None in case of any errors. - + #### get`_`dialogue ```python @@ -834,73 +725,3 @@ Retrieve the dialogue 'message' belongs to. the dialogue, or None in case such a dialogue does not exist - -#### get`_`latest`_`label - -```python - | get_latest_label(dialogue_label: DialogueLabel) -> DialogueLabel -``` - -Retrieve the latest dialogue label if present otherwise return same label. - -**Arguments**: - -- `dialogue_label`: the dialogue label -:return dialogue_label: the dialogue label - - -#### get`_`dialogue`_`from`_`label - -```python - | get_dialogue_from_label(dialogue_label: DialogueLabel) -> Optional[Dialogue] -``` - -Retrieve a dialogue based on its label. - -**Arguments**: - -- `dialogue_label`: the dialogue label - -**Returns**: - -the dialogue if present - - -#### create`_`dialogue - -```python - | @abstractmethod - | create_dialogue(dialogue_label: DialogueLabel, role: Dialogue.Role) -> Dialogue -``` - -THIS METHOD IS DEPRECATED AND WILL BE REMOVED IN THE NEXT VERSION. USE THE NEW CONSTRUCTOR ARGUMENTS INSTEAD. - -Create a dialogue instance. - -**Arguments**: - -- `dialogue_label`: the identifier of the dialogue -- `role`: the role of the agent this dialogue is maintained for - -**Returns**: - -the created dialogue - - -#### role`_`from`_`first`_`message - -```python - | @staticmethod - | role_from_first_message(message: Message) -> Dialogue.Role -``` - -Infer the role of the agent from an incoming or outgoing first message. - -**Arguments**: - -- `message`: an incoming/outgoing first message - -**Returns**: - -the agent's role - diff --git a/docs/api/helpers/multiaddr/base.md b/docs/api/helpers/multiaddr/base.md new file mode 100644 index 0000000000..d7a9062ce3 --- /dev/null +++ b/docs/api/helpers/multiaddr/base.md @@ -0,0 +1,88 @@ + +# aea.helpers.multiaddr.base + +This module contains multiaddress class. + + +## MultiAddr Objects + +```python +class MultiAddr() +``` + +Protocol Labs' Multiaddress representation of a network address + + +#### `__`init`__` + +```python + | __init__(host: str, port: int, public_key: str) +``` + +Initialize a multiaddress + +**Arguments**: + +- `host`: ip host of the address +- `host`: port number of the address +- `host`: hex encoded public key. Must conform to Bitcoin EC encoding standard for Secp256k1 + + +#### compute`_`peerid + +```python + | @staticmethod + | compute_peerid(public_key: str) -> str +``` + +Compute the peer id from a public key. + +In particular, compute the base58 representation of +libp2p PeerID from Bitcoin EC encoded Secp256k1 public key. + +**Arguments**: + +- `public_key`: the public key. + +**Returns**: + +the peer id. + + +#### public`_`key + +```python + | @property + | public_key() -> str +``` + +Get the public key. + + +#### peer`_`id + +```python + | @property + | peer_id() -> str +``` + +Get the peer id. + + +#### format + +```python + | format() -> str +``` + +Canonical representation of a multiaddress + + +#### `__`str`__` + +```python + | __str__() -> str +``` + +Default string representation of a mutliaddress. + diff --git a/docs/api/helpers/pipe.md b/docs/api/helpers/pipe.md index 857fbafce2..0597f6acfc 100644 --- a/docs/api/helpers/pipe.md +++ b/docs/api/helpers/pipe.md @@ -3,16 +3,16 @@ Portable pipe implementation for Linux, MacOS, and Windows. - -## LocalPortablePipe Objects + +## IPCChannelClient Objects ```python -class LocalPortablePipe(ABC) +class IPCChannelClient(ABC) ``` -Multi-platform interprocess communication channel +Multi-platform interprocess communication channel for the client side - + #### connect ```python @@ -20,9 +20,13 @@ Multi-platform interprocess communication channel | async connect(timeout=PIPE_CONN_TIMEOUT) -> bool ``` -Setup the communication channel with the other process +Connect to communication channel + +**Arguments**: - +- `timeout`: timeout for other end to connect + + #### write ```python @@ -33,7 +37,11 @@ Setup the communication channel with the other process Write `data` bytes to the other end of the channel Will first write the size than the actual data - +**Arguments**: + +- `data`: bytes to write + + #### read ```python @@ -44,7 +52,11 @@ Will first write the size than the actual data Read bytes from the other end of the channel Will first read the size than the actual data - +**Returns**: + +read bytes + + #### close ```python @@ -54,7 +66,16 @@ Will first read the size than the actual data Close the communication channel - + +## IPCChannel Objects + +```python +class IPCChannel(IPCChannelClient) +``` + +Multi-platform interprocess communication channel + + #### in`_`path ```python @@ -63,9 +84,9 @@ Close the communication channel | in_path() -> str ``` -Returns the rendezvous point for incoming communication +Rendezvous point for incoming communication - + #### out`_`path ```python @@ -74,58 +95,475 @@ Returns the rendezvous point for incoming communication | out_path() -> str ``` -Returns the rendezvous point for outgoing communication +Rendezvous point for outgoing communication + + +## PosixNamedPipeProtocol Objects + +```python +class PosixNamedPipeProtocol() +``` + +Posix named pipes async wrapper communication protocol + + +#### `__`init`__` + +```python + | __init__(in_path: str, out_path: str, logger: logging.Logger = _default_logger, loop: Optional[AbstractEventLoop] = None) +``` + +Initialize a new posix named pipe + +**Arguments**: + +- `in_path`: rendezvous point for incoming data +- `out_path`: rendezvous point for outgoing daa + + +#### connect + +```python + | async connect(timeout: float = PIPE_CONN_TIMEOUT) -> bool +``` + +Connect to the other end of the pipe + +**Arguments**: + +- `timeout`: timeout before failing + +**Returns**: + +connection success + + +#### write + +```python + | async write(data: bytes) -> None +``` + +Write to pipe. + +**Arguments**: + +- `data`: bytes to write to pipe + + +#### read + +```python + | async read() -> Optional[bytes] +``` + +Read from pipe. + +**Returns**: + +read bytes + + +#### close + +```python + | async close() -> None +``` + +Disconnect pipe + + +## TCPSocketProtocol Objects + +```python +class TCPSocketProtocol() +``` + +TCP socket communication protocol + + +#### `__`init`__` + +```python + | __init__(reader: asyncio.StreamReader, writer: asyncio.StreamWriter, logger: logging.Logger = _default_logger, loop: Optional[AbstractEventLoop] = None) +``` + +Initialize the tcp socket protocol + +**Arguments**: + +- `reader`: established asyncio reader +- `writer`: established asyncio writer + + +#### write + +```python + | async write(data: bytes) -> None +``` + +Write to socket. + +**Arguments**: + +- `data`: bytes to write + + +#### read + +```python + | async read() -> Optional[bytes] +``` + +Read from socket. + +**Returns**: + +read bytes + + +#### close + +```python + | async close() -> None +``` + +Disconnect socket + + +## TCPSocketChannel Objects + +```python +class TCPSocketChannel(IPCChannel) +``` + +Interprocess communication channel implementation using tcp sockets + + +#### `__`init`__` + +```python + | __init__(logger: logging.Logger = _default_logger, loop: Optional[AbstractEventLoop] = None) +``` + +Initialize tcp socket interprocess communication channel + + +#### connect + +```python + | async connect(timeout: float = PIPE_CONN_TIMEOUT) -> bool +``` + +Setup communication channel and wait for other end to connect + +**Arguments**: + +- `timeout`: timeout for the connection to be established + + +#### write + +```python + | async write(data: bytes) -> None +``` + +Write to channel. + +**Arguments**: + +- `data`: bytes to write + + +#### read + +```python + | async read() -> Optional[bytes] +``` + +Read from channel. + +**Arguments**: + +- `data`: read bytes + + +#### close + +```python + | async close() -> None +``` + +Disconnect from channel and clean it up + + +#### in`_`path + +```python + | @property + | in_path() -> str +``` + +Rendezvous point for incoming communication + + +#### out`_`path + +```python + | @property + | out_path() -> str +``` + +Rendezvous point for outgoing communication + + +## PosixNamedPipeChannel Objects + +```python +class PosixNamedPipeChannel(IPCChannel) +``` + +Interprocess communication channel implementation using Posix named pipes + + +#### `__`init`__` + +```python + | __init__(logger: logging.Logger = _default_logger, loop: Optional[AbstractEventLoop] = None) +``` + +Initialize posix named pipe interprocess communication channel + + +#### connect + +```python + | async connect(timeout: float = PIPE_CONN_TIMEOUT) -> bool +``` + +Setup communication channel and wait for other end to connect + +**Arguments**: + +- `timeout`: timeout for connection to be established + + +#### write + +```python + | async write(data: bytes) -> None +``` + +Write to the channel. + +**Arguments**: + +- `data`: data to write to channel + + +#### read + +```python + | async read() -> Optional[bytes] +``` + +Read from the channel. + +**Returns**: + +read bytes + + +#### close + +```python + | async close() -> None +``` + +Close the channel and clean it up + + +#### in`_`path + +```python + | @property + | in_path() -> str +``` + +Rendezvous point for incoming communication + + +#### out`_`path + +```python + | @property + | out_path() -> str +``` + +Rendezvous point for outgoing communication + + +## TCPSocketChannelClient Objects + +```python +class TCPSocketChannelClient(IPCChannelClient) +``` + +Interprocess communication channel client using tcp sockets - -## TCPSocketPipe Objects + +#### `__`init`__` ```python -class TCPSocketPipe(LocalPortablePipe) + | __init__(in_path: str, out_path: str, logger: logging.Logger = _default_logger, loop: Optional[AbstractEventLoop] = None) ``` -Interprocess communication implementation using tcp sockets +Initialize a tcp socket communication channel client + +**Arguments**: - -## PosixNamedPipe Objects +- `in_path`: rendezvous point for incoming data +- `out_path`: rendezvous point for outgoing data + + +#### connect ```python -class PosixNamedPipe(LocalPortablePipe) + | async connect(timeout: float = PIPE_CONN_TIMEOUT) -> bool ``` -Interprocess communication implementation using Posix named pipes +Connect to the other end of the communication channel - +**Arguments**: + +- `timeout`: timeout for connection to be established + + #### write ```python | async write(data: bytes) -> None ``` -Write to the writer stream. +Write data to channel **Arguments**: -- `data`: data to write to stream +- `data`: bytes to write - + #### read ```python | async read() -> Optional[bytes] ``` -Read from the reader stream. +Read data from channel **Returns**: -bytes +read bytes + + +#### close + +```python + | async close() -> None +``` + +Disconnect from communication channel - -#### make`_`pipe + +## PosixNamedPipeChannelClient Objects ```python -make_pipe(logger: logging.Logger = _default_logger) -> LocalPortablePipe +class PosixNamedPipeChannelClient(IPCChannelClient) ``` -Build a portable bidirectional Interprocess Communication Channel +Interprocess communication channel client using Posix named pipes + + +#### `__`init`__` + +```python + | __init__(in_path: str, out_path: str, logger: logging.Logger = _default_logger, loop: Optional[AbstractEventLoop] = None) +``` + +Initialize a posix named pipe communication channel client + +**Arguments**: + +- `in_path`: rendezvous point for incoming data +- `out_path`: rendezvous point for outgoing data + + +#### connect + +```python + | async connect(timeout: float = PIPE_CONN_TIMEOUT) -> bool +``` + +Connect to the other end of the communication channel + +**Arguments**: + +- `timeout`: timeout for connection to be established + + +#### write + +```python + | async write(data: bytes) -> None +``` + +Write data to channel + +**Arguments**: + +- `data`: bytes to write + + +#### read + +```python + | async read() -> Optional[bytes] +``` + +Read data from channel + +**Returns**: + +read bytes + + +#### close + +```python + | async close() -> None +``` + +Disconnect from communication channel + + +#### make`_`ipc`_`channel + +```python +make_ipc_channel(logger: logging.Logger = _default_logger, loop: Optional[AbstractEventLoop] = None) -> IPCChannel +``` + +Build a portable bidirectional InterProcess Communication channel + + +#### make`_`ipc`_`channel`_`client + +```python +make_ipc_channel_client(in_path: str, out_path: str, logger: logging.Logger = _default_logger, loop: Optional[AbstractEventLoop] = None) -> IPCChannelClient +``` + +Build a portable bidirectional InterProcess Communication client channel + +**Arguments**: + +- `in_path`: rendezvous point for incoming communication +- `out_path`: rendezvous point for outgoing outgoing diff --git a/docs/api/helpers/test_cases.md b/docs/api/helpers/test_cases.md index e7e5d36768..3fe8546cd7 100644 --- a/docs/api/helpers/test_cases.md +++ b/docs/api/helpers/test_cases.md @@ -259,7 +259,7 @@ subprocess object. ```python | @classmethod - | terminate_agents(cls, *subprocesses: subprocess.Popen, *, signal: signal.Signals = signal.SIGINT, timeout: int = 10) -> None + | terminate_agents(cls, *subprocesses: subprocess.Popen, *, timeout: int = 10) -> None ``` Terminate agent subprocesses. @@ -269,7 +269,6 @@ Run from agent's directory. **Arguments**: - `subprocesses`: the subprocesses running the agents -- `signal`: the signal for interruption - `timeout`: the timeout for interruption diff --git a/docs/api/mail/base.md b/docs/api/mail/base.md index fb54aa8439..285d595974 100644 --- a/docs/api/mail/base.md +++ b/docs/api/mail/base.md @@ -180,7 +180,7 @@ Extra information for the handling of an envelope. #### `__`init`__` ```python - | __init__(connection_id: Optional[PublicId] = None, uri: Optional[URI] = None) + | __init__(connection_id: Optional[PublicId] = None, skill_id: Optional[PublicId] = None, uri: Optional[URI] = None) ``` Initialize the envelope context. @@ -188,8 +188,29 @@ Initialize the envelope context. **Arguments**: - `connection_id`: the connection id used for routing the outgoing envelope in the multiplexer. +- `skill_id`: the skill id used for routing the incoming envelope in the AEA. - `uri`: the URI sent with the envelope. + +#### connection`_`id + +```python + | @property + | connection_id() -> Optional[PublicId] +``` + +Get the connection id. + + +#### skill`_`id + +```python + | @property + | skill_id() -> Optional[PublicId] +``` + +Get the skill id. + #### uri`_`raw @@ -437,7 +458,7 @@ Get the envelope context. ```python | @property - | skill_id() -> Optional[SkillId] + | skill_id() -> Optional[PublicId] ``` Get the skill id from an envelope context, if set. @@ -446,6 +467,20 @@ Get the skill id from an envelope context, if set. skill id + +#### connection`_`id + +```python + | @property + | connection_id() -> Optional[PublicId] +``` + +Get the connection id from an envelope context, if set. + +**Returns**: + +connection id + #### `__`eq`__` diff --git a/docs/api/multiplexer.md b/docs/api/multiplexer.md index f07ab99ca0..0f743e83dd 100644 --- a/docs/api/multiplexer.md +++ b/docs/api/multiplexer.md @@ -3,16 +3,16 @@ Module for the multiplexer class and related classes. - -## ConnectionStatus Objects + +## MultiplexerStatus Objects ```python -class ConnectionStatus() +class MultiplexerStatus(AsyncState) ``` The connection status class. - + #### `__`init`__` ```python @@ -21,6 +21,46 @@ The connection status class. Initialize the connection status. + +#### is`_`connected + +```python + | @property + | is_connected() -> bool +``` + +Return is connected. + + +#### is`_`connecting + +```python + | @property + | is_connecting() -> bool +``` + +Return is connecting. + + +#### is`_`disconnected + +```python + | @property + | is_disconnected() -> bool +``` + +Return is disconnected. + + +#### is`_`disconnecting + +```python + | @property + | is_disconnecting() -> bool +``` + +Return is disconnected. + ## AsyncMultiplexer Objects @@ -158,7 +198,7 @@ Set the default routing. ```python | @property - | connection_status() -> ConnectionStatus + | connection_status() -> MultiplexerStatus ``` Get the connection status. @@ -334,7 +374,7 @@ None #### setup ```python - | setup(connections: Collection[Connection], default_routing: Dict[PublicId, PublicId], default_connection: Optional[PublicId] = None) -> None + | setup(connections: Collection[Connection], default_routing: Optional[Dict[PublicId, PublicId]] = None, default_connection: Optional[PublicId] = None) -> None ``` Set up the multiplexer. @@ -458,7 +498,7 @@ A queue from where you can only enqueue envelopes. #### `__`init`__` ```python - | __init__(multiplexer: Multiplexer, default_address: Address) + | __init__(multiplexer: Multiplexer) ``` Initialize the outbox. @@ -466,7 +506,6 @@ Initialize the outbox. **Arguments**: - `multiplexer`: the multiplexer -- `default_address`: the default address of the agent #### empty @@ -502,15 +541,13 @@ None #### put`_`message ```python - | put_message(message: Message, context: Optional[EnvelopeContext] = None, sender: Optional[str] = None) -> None + | put_message(message: Message, context: Optional[EnvelopeContext] = None) -> None ``` Put a message in the outbox. This constructs an envelope with the input arguments. -"sender" is a deprecated kwarg and will be removed in the next version - **Arguments**: - `message`: the message diff --git a/docs/api/protocols/base.md b/docs/api/protocols/base.md index 57170e2faf..b1b5559031 100644 --- a/docs/api/protocols/base.md +++ b/docs/api/protocols/base.md @@ -106,60 +106,6 @@ Get address of receiver. Set address of receiver. - -#### has`_`counterparty - -```python - | @property - | has_counterparty() -> bool -``` - -Check if the counterparty is set. - - -#### counterparty - -```python - | @property - | counterparty() -> Address -``` - -Get the counterparty of the message in Address form. - -:return the address - - -#### counterparty - -```python - | @counterparty.setter - | counterparty(counterparty: Address) -> None -``` - -Set the counterparty of the message. - - -#### is`_`incoming - -```python - | @property - | is_incoming() -> bool -``` - -Get the is_incoming value of the message. - -:return whether the message is incoming or is out going - - -#### is`_`incoming - -```python - | @is_incoming.setter - | is_incoming(is_incoming: bool) -> None -``` - -Set the is_incoming of the message. - #### body @@ -259,15 +205,6 @@ None Get value for key. - -#### unset - -```python - | unset(key: str) -> None -``` - -Unset valye for key. - #### is`_`set @@ -400,53 +337,6 @@ Encode a message into bytes using Protobuf. Decode bytes into a message using Protobuf. - -## JSONSerializer Objects - -```python -class JSONSerializer(Serializer) -``` - -Default serialization in JSON for the Message object. - -It assumes that the Message contains a JSON-serializable body. - - -#### encode - -```python - | @staticmethod - | encode(msg: Message) -> bytes -``` - -Encode a message into bytes using JSON format. - -**Arguments**: - -- `msg`: the message to be encoded. - -**Returns**: - -the serialized message. - - -#### decode - -```python - | @staticmethod - | decode(obj: bytes) -> Message -``` - -Decode bytes into a message using JSON. - -**Arguments**: - -- `obj`: the serialized message. - -**Returns**: - -the decoded message. - ## Protocol Objects diff --git a/docs/api/protocols/default/dialogues.md b/docs/api/protocols/default/dialogues.md index 560b7d3c95..51c89ac9e9 100644 --- a/docs/api/protocols/default/dialogues.md +++ b/docs/api/protocols/default/dialogues.md @@ -37,7 +37,7 @@ This class defines the end states of a default dialogue. #### `__`init`__` ```python - | __init__(dialogue_label: DialogueLabel, agent_address: Optional[Address] = None, role: Optional[Dialogue.Role] = None) -> None + | __init__(dialogue_label: DialogueLabel, self_address: Address, role: Dialogue.Role, message_class: Type[DefaultMessage] = DefaultMessage) -> None ``` Initialize a dialogue. @@ -45,33 +45,13 @@ Initialize a dialogue. **Arguments**: - `dialogue_label`: the identifier of the dialogue -- `agent_address`: the address of the agent for whom this dialogue is maintained +- `self_address`: the address of the entity for whom this dialogue is maintained - `role`: the role of the agent this dialogue is maintained for **Returns**: None - -#### is`_`valid - -```python - | is_valid(message: Message) -> bool -``` - -Check whether 'message' is a valid next message in the dialogue. - -These rules capture specific constraints designed for dialogues which are instances of a concrete sub-class of this class. -Override this method with your additional dialogue rules. - -**Arguments**: - -- `message`: the message to be validated - -**Returns**: - -True if valid, False otherwise - ## DefaultDialogues Objects @@ -85,34 +65,16 @@ This class keeps track of all default dialogues. #### `__`init`__` ```python - | __init__(agent_address: Address) -> None + | __init__(self_address: Address, role_from_first_message: Callable[[Message, Address], Dialogue.Role], dialogue_class: Type[DefaultDialogue] = DefaultDialogue) -> None ``` Initialize dialogues. **Arguments**: -- `agent_address`: the address of the agent for whom dialogues are maintained +- `self_address`: the address of the entity for whom dialogues are maintained **Returns**: None - -#### create`_`dialogue - -```python - | create_dialogue(dialogue_label: DialogueLabel, role: Dialogue.Role) -> DefaultDialogue -``` - -Create an instance of default dialogue. - -**Arguments**: - -- `dialogue_label`: the identifier of the dialogue -- `role`: the role of the agent this dialogue is maintained for - -**Returns**: - -the created dialogue - diff --git a/docs/api/protocols/default/message.md b/docs/api/protocols/default/message.md index 43b80e69be..8f8b127f1f 100644 --- a/docs/api/protocols/default/message.md +++ b/docs/api/protocols/default/message.md @@ -16,7 +16,7 @@ A protocol for exchanging any bytes message. ## Performative Objects ```python -class Performative(Enum) +class Performative(Message.Performative) ``` Performatives for the default protocol. diff --git a/docs/api/protocols/signing/dialogues.md b/docs/api/protocols/signing/dialogues.md index b942f049a8..03ab402e21 100644 --- a/docs/api/protocols/signing/dialogues.md +++ b/docs/api/protocols/signing/dialogues.md @@ -37,7 +37,7 @@ This class defines the end states of a signing dialogue. #### `__`init`__` ```python - | __init__(dialogue_label: DialogueLabel, agent_address: Optional[Address] = None, role: Optional[Dialogue.Role] = None) -> None + | __init__(dialogue_label: DialogueLabel, self_address: Address, role: Dialogue.Role, message_class: Type[SigningMessage] = SigningMessage) -> None ``` Initialize a dialogue. @@ -45,33 +45,13 @@ Initialize a dialogue. **Arguments**: - `dialogue_label`: the identifier of the dialogue -- `agent_address`: the address of the agent for whom this dialogue is maintained +- `self_address`: the address of the entity for whom this dialogue is maintained - `role`: the role of the agent this dialogue is maintained for **Returns**: None - -#### is`_`valid - -```python - | is_valid(message: Message) -> bool -``` - -Check whether 'message' is a valid next message in the dialogue. - -These rules capture specific constraints designed for dialogues which are instances of a concrete sub-class of this class. -Override this method with your additional dialogue rules. - -**Arguments**: - -- `message`: the message to be validated - -**Returns**: - -True if valid, False otherwise - ## SigningDialogues Objects @@ -85,34 +65,16 @@ This class keeps track of all signing dialogues. #### `__`init`__` ```python - | __init__(agent_address: Address) -> None + | __init__(self_address: Address, role_from_first_message: Callable[[Message, Address], Dialogue.Role], dialogue_class: Type[SigningDialogue] = SigningDialogue) -> None ``` Initialize dialogues. **Arguments**: -- `agent_address`: the address of the agent for whom dialogues are maintained +- `self_address`: the address of the entity for whom dialogues are maintained **Returns**: None - -#### create`_`dialogue - -```python - | create_dialogue(dialogue_label: DialogueLabel, role: Dialogue.Role) -> SigningDialogue -``` - -Create an instance of signing dialogue. - -**Arguments**: - -- `dialogue_label`: the identifier of the dialogue -- `role`: the role of the agent this dialogue is maintained for - -**Returns**: - -the created dialogue - diff --git a/docs/api/protocols/signing/message.md b/docs/api/protocols/signing/message.md index f3ccf396f2..5db73d6bd1 100644 --- a/docs/api/protocols/signing/message.md +++ b/docs/api/protocols/signing/message.md @@ -16,7 +16,7 @@ A protocol for communication between skills and decision maker. ## Performative Objects ```python -class Performative(Enum) +class Performative(Message.Performative) ``` Performatives for the signing protocol. @@ -146,26 +146,6 @@ Get the 'signed_message' content from the message. Get the 'signed_transaction' content from the message. - -#### skill`_`callback`_`ids - -```python - | @property - | skill_callback_ids() -> Tuple[str, ...] -``` - -Get the 'skill_callback_ids' content from the message. - - -#### skill`_`callback`_`info - -```python - | @property - | skill_callback_info() -> Dict[str, str] -``` - -Get the 'skill_callback_info' content from the message. - #### terms diff --git a/docs/api/protocols/state_update/dialogues.md b/docs/api/protocols/state_update/dialogues.md index 6c68ca4042..5b3eea5ea5 100644 --- a/docs/api/protocols/state_update/dialogues.md +++ b/docs/api/protocols/state_update/dialogues.md @@ -37,7 +37,7 @@ This class defines the end states of a state_update dialogue. #### `__`init`__` ```python - | __init__(dialogue_label: DialogueLabel, agent_address: Optional[Address] = None, role: Optional[Dialogue.Role] = None) -> None + | __init__(dialogue_label: DialogueLabel, self_address: Address, role: Dialogue.Role, message_class: Type[StateUpdateMessage] = StateUpdateMessage) -> None ``` Initialize a dialogue. @@ -45,33 +45,13 @@ Initialize a dialogue. **Arguments**: - `dialogue_label`: the identifier of the dialogue -- `agent_address`: the address of the agent for whom this dialogue is maintained +- `self_address`: the address of the entity for whom this dialogue is maintained - `role`: the role of the agent this dialogue is maintained for **Returns**: None - -#### is`_`valid - -```python - | is_valid(message: Message) -> bool -``` - -Check whether 'message' is a valid next message in the dialogue. - -These rules capture specific constraints designed for dialogues which are instances of a concrete sub-class of this class. -Override this method with your additional dialogue rules. - -**Arguments**: - -- `message`: the message to be validated - -**Returns**: - -True if valid, False otherwise - ## StateUpdateDialogues Objects @@ -85,34 +65,16 @@ This class keeps track of all state_update dialogues. #### `__`init`__` ```python - | __init__(agent_address: Address) -> None + | __init__(self_address: Address, role_from_first_message: Callable[[Message, Address], Dialogue.Role], dialogue_class: Type[StateUpdateDialogue] = StateUpdateDialogue) -> None ``` Initialize dialogues. **Arguments**: -- `agent_address`: the address of the agent for whom dialogues are maintained +- `self_address`: the address of the entity for whom dialogues are maintained **Returns**: None - -#### create`_`dialogue - -```python - | create_dialogue(dialogue_label: DialogueLabel, role: Dialogue.Role) -> StateUpdateDialogue -``` - -Create an instance of state_update dialogue. - -**Arguments**: - -- `dialogue_label`: the identifier of the dialogue -- `role`: the role of the agent this dialogue is maintained for - -**Returns**: - -the created dialogue - diff --git a/docs/api/protocols/state_update/message.md b/docs/api/protocols/state_update/message.md index 35085a6d77..e2c03202cf 100644 --- a/docs/api/protocols/state_update/message.md +++ b/docs/api/protocols/state_update/message.md @@ -16,7 +16,7 @@ A protocol for state updates to the decision maker state. ## Performative Objects ```python -class Performative(Enum) +class Performative(Message.Performative) ``` Performatives for the state_update protocol. diff --git a/docs/api/registries/filter.md b/docs/api/registries/filter.md index 8f8fcfbeda..859c5646d3 100644 --- a/docs/api/registries/filter.md +++ b/docs/api/registries/filter.md @@ -16,7 +16,7 @@ This class implements the filter of an AEA. #### `__`init`__` ```python - | __init__(resources: Resources, decision_maker_out_queue: Queue) + | __init__(resources: Resources, decision_maker_out_queue: AsyncFriendlyQueue) ``` Instantiate the filter. @@ -41,7 +41,7 @@ Get resources. ```python | @property - | decision_maker_out_queue() -> Queue + | decision_maker_out_queue() -> AsyncFriendlyQueue ``` Get decision maker (out) queue. @@ -77,11 +77,11 @@ Get the active behaviours. the list of behaviours currently active - -#### handle`_`internal`_`messages + +#### handle`_`new`_`handlers`_`and`_`behaviours ```python - | handle_internal_messages() -> None + | handle_new_handlers_and_behaviours() -> None ``` Handle the messages from the decision maker. @@ -90,3 +90,21 @@ Handle the messages from the decision maker. None + +#### get`_`internal`_`message + +```python + | async get_internal_message() -> Optional[Message] +``` + +Get a message from decision_maker_out_queue. + + +#### handle`_`internal`_`message + +```python + | handle_internal_message(internal_message: Optional[Message]) -> None +``` + +Handlle internal message. + diff --git a/docs/api/runtime.md b/docs/api/runtime.md index 50b3f3f33b..64d57ce31a 100644 --- a/docs/api/runtime.md +++ b/docs/api/runtime.md @@ -25,7 +25,7 @@ Abstract runtime class to create implementations. #### `__`init`__` ```python - | __init__(agent: "Agent", loop: Optional[AbstractEventLoop] = None) -> None + | __init__(agent: AbstractAgent, loop_mode: Optional[str] = None, loop: Optional[AbstractEventLoop] = None) -> None ``` Init runtime. @@ -33,12 +33,81 @@ Init runtime. **Arguments**: - `agent`: Agent to run. +- `loop_mode`: agent main loop mode. - `loop`: optional event loop. if not provided a new one will be created. **Returns**: None + +#### loop`_`mode + +```python + | @property + | loop_mode() -> str +``` + +Get current loop mode. + + +#### setup`_`multiplexer + +```python + | setup_multiplexer() -> None +``` + +Set up the multiplexer. + + +#### task`_`manager + +```python + | @property + | task_manager() -> TaskManager +``` + +Get the task manager. + + +#### loop + +```python + | @property + | loop() -> AbstractEventLoop +``` + +Get event loop. + + +#### multiplexer + +```python + | @property + | multiplexer() -> Multiplexer +``` + +Get multiplexer. + + +#### decision`_`maker + +```python + | @property + | decision_maker() -> DecisionMaker +``` + +Return decision maker if set. + + +#### set`_`decision`_`maker + +```python + | set_decision_maker(decision_maker_handler: DecisionMakerHandler) -> None +``` + +Set decision maker with handler provided. + #### start @@ -117,7 +186,7 @@ Asynchronous runtime: uses asyncio loop for multiplexer and async agent main loo #### `__`init`__` ```python - | __init__(agent: "Agent", loop: Optional[AbstractEventLoop] = None) -> None + | __init__(agent: AbstractAgent, loop_mode: Optional[str] = None, loop: Optional[AbstractEventLoop] = None) -> None ``` Init runtime. @@ -125,6 +194,7 @@ Init runtime. **Arguments**: - `agent`: Agent to run. +- `loop_mode`: agent main loop mode. - `loop`: optional event loop. if not provided a new one will be created. **Returns**: diff --git a/docs/api/skills/base.md b/docs/api/skills/base.md index 73e024a191..b0b26dec58 100644 --- a/docs/api/skills/base.md +++ b/docs/api/skills/base.md @@ -34,6 +34,16 @@ Initialize a skill context. Get the logger. + +#### logger + +```python + | @logger.setter + | logger(logger_: Logger) -> None +``` + +Set the logger. + #### set`_`agent`_`context @@ -148,7 +158,7 @@ Get address. ```python | @property - | connection_status() -> ConnectionStatus + | connection_status() -> MultiplexerStatus ``` Get connection status. @@ -565,7 +575,7 @@ This class implements a skill. #### `__`init`__` ```python - | __init__(configuration: SkillConfig, skill_context: Optional[SkillContext] = None, handlers: Optional[Dict[str, Handler]] = None, behaviours: Optional[Dict[str, Behaviour]] = None, models: Optional[Dict[str, Model]] = None) + | __init__(configuration: SkillConfig, skill_context: Optional[SkillContext] = None, handlers: Optional[Dict[str, Handler]] = None, behaviours: Optional[Dict[str, Behaviour]] = None, models: Optional[Dict[str, Model]] = None, **kwargs, ,) ``` Initialize a skill. diff --git a/docs/api/skills/behaviours.md b/docs/api/skills/behaviours.md index 71db207932..99ad74f7cc 100644 --- a/docs/api/skills/behaviours.md +++ b/docs/api/skills/behaviours.md @@ -80,6 +80,16 @@ This behaviour is executed until the agent is stopped. Initialize the cyclic behaviour. + +#### number`_`of`_`executions + +```python + | @property + | number_of_executions() -> int +``` + +Get the number of executions. + #### act`_`wrapper diff --git a/docs/app-areas.md b/docs/app-areas.md index 595176ad54..0f721d08f1 100644 --- a/docs/app-areas.md +++ b/docs/app-areas.md @@ -1,5 +1,14 @@ An AEA is an intelligent agent whose goal is generating economic value for its owner. It can represent machines, humans, or data. +## General application areas + +As described in the guide on agent-oriented development, AEAs are designed to operate in an envrionment with: + +* multiple stakeholders, which +* are represented by AEAs, that +* interact autonomously and +* communicate decentrally. + There are at least five general application areas for AEAs: * **Inhabitants**: agents paired with real world hardware devices such as drones, laptops, heat sensors, etc. An example is the theremometer agent that can be found here. @@ -23,6 +32,6 @@ In the short-term we see AEAs primarily deployed in three areas: The Fetch.ai multi-agent system is a real world multi-agent technological system and, although there is some overlap, it is not the same as agent based modelling where the goal is scientific behavioural observation rather than practical economic gain. -Moreover, there is no restriction to *multi*. Single-agent applications are also possible. +Moreover, there is no restriction to *multi*. Single-agent applications are also supported.
diff --git a/docs/aries-cloud-agent-demo.md b/docs/aries-cloud-agent-demo.md index 86375dfa4b..3a24b6ff48 100644 --- a/docs/aries-cloud-agent-demo.md +++ b/docs/aries-cloud-agent-demo.md @@ -8,7 +8,7 @@ Demonstrating an entire decentralised identity scenario involving AEAs and insta ## Discussion -This demo corresponds with the one here from aries cloud agent repository . +This demo corresponds with the one here from aries cloud agent repository . The aim of this demo is to illustrate how AEAs can connect to ACAs, thus gaining all of their capabilities, such as issuing and requesting verifiable credentials, selective disclosure and zero knowledge proofs. @@ -81,7 +81,7 @@ All messages from an AEA to another AEA utilise the p2p communication network ac All messages initiated from an ACA to an AEA are webhooks (using `webhook` connection). -This is the extent of the demo at this point. The rest of the interactions require an instance of the Indy ledger to run. This is what will be implemented next. +This is the extent of the demo at this point. The rest of the interactions require an instance of the Indy ledger to run. This is what will be implemented next. The rest of the interactions are broadly as follows: @@ -101,7 +101,7 @@ At this point, the two ACAs are connected to each other. Follow the Preliminaries and Installation sections from the AEA quick start. -Install Aries cloud-agents (for more info see here) if you do not have it on your machine: +Install Aries cloud-agents (for more info see here) if you do not have it on your machine: ``` bash pip install aries-cloudagent @@ -109,7 +109,7 @@ pip install aries-cloudagent This demo has been successfully tested with aca-py version 0.4.5. -This demo requires an instance of von network running in docker locally (for more info see here) +This demo requires an instance of von network running in docker locally (for more info see here) This demo has been successfully tested with the von-network git repository pulled on 07 Aug 2020 (commit number `ad1f84f64d4f4c106a81462f5fbff496c5fbf10e`). @@ -121,13 +121,13 @@ Open five terminals. The first terminal is used to run an instance of von-networ In the first terminal move to the `von-network` directory and run an instance of `von-network` locally in docker. -This tutorial has information on starting (and stopping) the network locally. +This tutorial has information on starting (and stopping) the network locally. ``` bash ./manage build ./manage start --logs ``` -Once the ledger is running, you can see the ledger by going to the web server running on port 9000. On localhost, that means going to http://localhost:9000. +Once the ledger is running, you can see the ledger by going to the web server running on port 9000. On localhost, that means going to http://localhost:9000. ## Alice and Faber ACAs @@ -180,7 +180,7 @@ Now you can create **Alice_AEA** and **Faber_AEA** in terminals 3 and 4 respecti In the third terminal, fetch **Alice_AEA** and move into its project folder: ``` bash -aea fetch fetchai/aries_alice:0.8.0 +aea fetch fetchai/aries_alice:0.9.0 cd aries_alice ``` @@ -191,11 +191,11 @@ The following steps create **Alice_AEA** from scratch: ``` bash aea create aries_alice cd aries_alice -aea add connection fetchai/p2p_libp2p:0.7.0 -aea add connection fetchai/soef:0.6.0 -aea add connection fetchai/http_client:0.7.0 -aea add connection fetchai/webhook:0.5.0 -aea add skill fetchai/aries_alice:0.5.0 +aea add connection fetchai/p2p_libp2p:0.8.0 +aea add connection fetchai/soef:0.7.0 +aea add connection fetchai/http_client:0.8.0 +aea add connection fetchai/webhook:0.6.0 +aea add skill fetchai/aries_alice:0.6.0 ```

@@ -265,7 +265,7 @@ Once you see a message of the form `My libp2p addresses: ['SOME_ADDRESS']` take In the fourth terminal, fetch **Faber_AEA** and move into its project folder: ``` bash -aea fetch fetchai/aries_faber:0.8.0 +aea fetch fetchai/aries_faber:0.9.0 cd aries_faber ``` @@ -276,11 +276,11 @@ The following steps create **Faber_AEA** from scratch: ``` bash aea create aries_faber cd aries_faber -aea add connection fetchai/p2p_libp2p:0.7.0 -aea add connection fetchai/soef:0.6.0 -aea add connection fetchai/http_client:0.7.0 -aea add connection fetchai/webhook:0.5.0 -aea add skill fetchai/aries_faber:0.4.0 +aea add connection fetchai/p2p_libp2p:0.8.0 +aea add connection fetchai/soef:0.7.0 +aea add connection fetchai/http_client:0.8.0 +aea add connection fetchai/webhook:0.6.0 +aea add skill fetchai/aries_faber:0.5.0 ```

@@ -371,6 +371,6 @@ aea delete aries_alice In the next update to this demo, the remaining interactions between AEAs and ACAs must be implemented. This means: -* An instance of Indy ledger must be installed and running. See here for more detail. +* An instance of Indy ledger must be installed and running. See here for more detail. * The commands for running the ACAs need to be adjusted. Additional options relating to a wallet (wallet-name, type, key, storage-type, config, creds) need to be fed to the ACAs as well as the ledger's genesis file so the ACAs can connect to the ledger. * The remaining interactions between the AEAs and ACAs as described here need to be implemented. diff --git a/docs/build-aea-programmatically.md b/docs/build-aea-programmatically.md index 6445a5f636..4d0b25c748 100644 --- a/docs/build-aea-programmatically.md +++ b/docs/build-aea-programmatically.md @@ -17,8 +17,8 @@ Then, import the application specific libraries. from aea.aea_builder import AEABuilder from aea.configurations.base import SkillConfig from aea.connections.stub.connection import write_with_lock -from aea.crypto.cosmos import CosmosCrypto -from aea.crypto.helpers import COSMOS_PRIVATE_KEY_FILE, create_private_key +from aea.crypto.fetchai import FetchAICrypto +from aea.crypto.helpers import PRIVATE_KEY_PATH_SCHEMA, create_private_key from aea.skills.base import Skill ``` @@ -27,13 +27,14 @@ Set up a variable pointing to where the packages directory is located - this sho ROOT_DIR = "./" INPUT_FILE = "input_file" OUTPUT_FILE = "output_file" +FETCHAI_PRIVATE_KEY_FILE = PRIVATE_KEY_PATH_SCHEMA.format(FetchAICrypto.identifier) ``` ## Create a private key We need a private key to populate the AEA's wallet. ``` python # Create a private key - create_private_key(CosmosCrypto.identifier, COSMOS_PRIVATE_KEY_FILE) + create_private_key(FetchAICrypto.identifier, FETCHAI_PRIVATE_KEY_FILE) ``` ## Clearing the input and output files @@ -47,7 +48,7 @@ We will use the stub connection to pass envelopes in and out of the AEA. Ensure ``` ## Initialise the AEA -We use the `AEABuilder` to readily build an AEA. By default, the `AEABuilder` adds the `fetchai/default:0.4.0` protocol, the `fetchai/stub:0.8.0` connection and the `fetchai/error:0.4.0` skill. +We use the `AEABuilder` to readily build an AEA. By default, the `AEABuilder` adds the `fetchai/default:0.5.0` protocol, the `fetchai/stub:0.9.0` connection and the `fetchai/error:0.5.0` skill. ``` python # Instantiate the builder and build the AEA # By default, the default protocol, error skill and stub connection are added @@ -58,7 +59,7 @@ We set the name, add the private key for the AEA to use and set the ledger confi ``` python builder.set_name("my_aea") - builder.add_private_key(CosmosCrypto.identifier, COSMOS_PRIVATE_KEY_FILE) + builder.add_private_key(FetchAICrypto.identifier, FETCHAI_PRIVATE_KEY_FILE) ``` Next, we add the echo skill which will bounce our messages back to us. We first need to place the echo skill into a relevant directory (see path), either by downloading the `packages` directory from the AEA repo or by getting the package from the registry. @@ -120,7 +121,7 @@ We run the AEA from a different thread so that we can still use the main thread We use the input and output text files to send an envelope to our AEA and receive a response (from the echo skill) ``` python # Create a message inside an envelope and get the stub connection to pass it on to the echo skill - message_text = b"my_aea,other_agent,fetchai/default:0.4.0,\x08\x01\x12\x011*\x07\n\x05hello," + message_text = b"my_aea,other_agent,fetchai/default:0.5.0,\x08\x01\x12\x011*\x07\n\x05hello," with open(INPUT_FILE, "wb") as f: write_with_lock(f, message_text) print(b"input message: " + message_text) @@ -146,8 +147,8 @@ Finally stop our AEA and wait for it to finish ## Running the AEA If you now run this python script file, you should see this output: - input message: my_aea,other_agent,fetchai/default:0.4.0,\x08\x01*\x07\n\x05hello - output message: other_agent,my_aea,fetchai/default:0.4.0,...\x05hello + input message: my_aea,other_agent,fetchai/default:0.5.0,\x08\x01*\x07\n\x05hello + output message: other_agent,my_aea,fetchai/default:0.5.0,...\x05hello ## Entire code listing @@ -164,18 +165,19 @@ from threading import Thread from aea.aea_builder import AEABuilder from aea.configurations.base import SkillConfig from aea.connections.stub.connection import write_with_lock -from aea.crypto.cosmos import CosmosCrypto -from aea.crypto.helpers import COSMOS_PRIVATE_KEY_FILE, create_private_key +from aea.crypto.fetchai import FetchAICrypto +from aea.crypto.helpers import PRIVATE_KEY_PATH_SCHEMA, create_private_key from aea.skills.base import Skill ROOT_DIR = "./" INPUT_FILE = "input_file" OUTPUT_FILE = "output_file" +FETCHAI_PRIVATE_KEY_FILE = PRIVATE_KEY_PATH_SCHEMA.format(FetchAICrypto.identifier) def run(): # Create a private key - create_private_key(CosmosCrypto.identifier, COSMOS_PRIVATE_KEY_FILE) + create_private_key(FetchAICrypto.identifier, FETCHAI_PRIVATE_KEY_FILE) # Ensure the input and output files do not exist initially if os.path.isfile(INPUT_FILE): @@ -189,7 +191,7 @@ def run(): builder.set_name("my_aea") - builder.add_private_key(CosmosCrypto.identifier, COSMOS_PRIVATE_KEY_FILE) + builder.add_private_key(FetchAICrypto.identifier, FETCHAI_PRIVATE_KEY_FILE) # Add the echo skill (assuming it is present in the local directory 'packages') builder.add_skill("./packages/fetchai/skills/echo") @@ -234,7 +236,7 @@ def run(): time.sleep(4) # Create a message inside an envelope and get the stub connection to pass it on to the echo skill - message_text = b"my_aea,other_agent,fetchai/default:0.4.0,\x08\x01\x12\x011*\x07\n\x05hello," + message_text = b"my_aea,other_agent,fetchai/default:0.5.0,\x08\x01\x12\x011*\x07\n\x05hello," with open(INPUT_FILE, "wb") as f: write_with_lock(f, message_text) print(b"input message: " + message_text) diff --git a/docs/build-aea-step-by-step.md b/docs/build-aea-step-by-step.md index eb79354c75..cf0dc255bb 100644 --- a/docs/build-aea-step-by-step.md +++ b/docs/build-aea-step-by-step.md @@ -11,4 +11,4 @@ Building an AEA step by step (ensure you have followed the here for all the available commands. +See information on the CLI tool here for all the available commands. diff --git a/docs/car-park-skills.md b/docs/car-park-skills.md index 06bdcb9d8f..603ae37a60 100644 --- a/docs/car-park-skills.md +++ b/docs/car-park-skills.md @@ -5,7 +5,7 @@ The AEA car-park skills demonstrate an interaction between two AEAs. ## Discussion -The full Fetch.ai car park AEA demo is documented in its own repo here. +The full Fetch.ai car park AEA demo is documented in its own repo here. This demo allows you to test the AEA functionality of the car park AEA demo without the detection logic. It demonstrates how the AEAs trade car park information. @@ -55,7 +55,7 @@ Follow the Preliminaries and @@ -89,7 +89,7 @@ default_routing: Then, fetch the car data client AEA: ``` bash -aea fetch fetchai/car_data_buyer:0.10.0 +aea fetch fetchai/car_data_buyer:0.11.0 cd car_data_buyer aea install ``` @@ -101,19 +101,19 @@ The following steps create the car data client from scratch: ``` bash aea create car_data_buyer cd car_data_buyer -aea add connection fetchai/p2p_libp2p:0.7.0 -aea add connection fetchai/soef:0.6.0 -aea add connection fetchai/ledger:0.3.0 -aea add skill fetchai/carpark_client:0.9.0 +aea add connection fetchai/p2p_libp2p:0.8.0 +aea add connection fetchai/soef:0.7.0 +aea add connection fetchai/ledger:0.4.0 +aea add skill fetchai/carpark_client:0.10.0 aea install -aea config set agent.default_connection fetchai/p2p_libp2p:0.7.0 +aea config set agent.default_connection fetchai/p2p_libp2p:0.8.0 ``` In `car_data_buyer/aea-config.yaml` add ``` yaml default_routing: - fetchai/ledger_api:0.2.0: fetchai/ledger:0.3.0 - fetchai/oef_search:0.4.0: fetchai/soef:0.6.0 + fetchai/ledger_api:0.3.0: fetchai/ledger:0.4.0 + fetchai/oef_search:0.5.0: fetchai/soef:0.7.0 ```

@@ -123,9 +123,9 @@ default_routing: First, create the private key for the car data seller AEA based on the network you want to transact. To generate and add a private-public key pair for Fetch.ai `AgentLand` use: ``` bash -aea generate-key cosmos -aea add-key cosmos cosmos_private_key.txt -aea add-key cosmos cosmos_private_key.txt --connection +aea generate-key fetchai +aea add-key fetchai fetchai_private_key.txt +aea add-key fetchai fetchai_private_key.txt --connection ``` ### Add keys and generate wealth for the car data buyer AEA @@ -134,14 +134,14 @@ The buyer needs to have some wealth to purchase the service from the seller. First, create the private key for the car data buyer AEA based on the network you want to transact. To generate and add a private-public key pair for Fetch.ai `AgentLand` use: ``` bash -aea generate-key cosmos -aea add-key cosmos cosmos_private_key.txt -aea add-key cosmos cosmos_private_key.txt --connection +aea generate-key fetchai +aea add-key fetchai fetchai_private_key.txt +aea add-key fetchai fetchai_private_key.txt --connection ``` Then, create some wealth for your car data buyer based on the network you want to transact with. On the Fetch.ai `AgentLand` network: ``` bash -aea generate-wealth cosmos +aea generate-wealth fetchai ``` ## Run the AEAs diff --git a/docs/cli-commands.md b/docs/cli-commands.md index 2a2c5e48ae..8d6da496a2 100644 --- a/docs/cli-commands.md +++ b/docs/cli-commands.md @@ -17,6 +17,7 @@ | `generate-key [ledger_id]` | Generate private keys. The AEA uses a private key to derive the associated public key and address. | | `generate-wealth [ledger_id]` | Generate wealth for address on test network. | | `get-address [ledger_id]` | Get the address associated with the private key. | +| `get-multiaddress [ledger_id]`... | Get the multiaddress associated with a private key or connection. | | `get-wealth [ledger_id]` | Get the wealth associated with the private key. | | `install [-r ]` | Install the dependencies. (With `--install-deps` to install dependencies.) | | `init` | Initialize your AEA configurations. (With `--author` to define author.) | diff --git a/docs/cli-gui.md b/docs/cli-gui.md index ba1a612d0e..7d554b8ff8 100644 --- a/docs/cli-gui.md +++ b/docs/cli-gui.md @@ -4,7 +4,7 @@ These instructions will take you through building an AEA and running the AEA - a ## Preliminaries -Follow the Preliminaries and Installation instructions
here. +Follow the Preliminaries and Installation instructions here. Install the extra dependencies for the CLI GUI. @@ -20,7 +20,7 @@ Start the local web-server. ``` bash aea gui ``` -Open this page in a browser: http://127.0.0.1:8001 +Open this page in a browser: http://127.0.0.1:8001 You should see the following page. diff --git a/docs/cli-vs-programmatic-aeas.md b/docs/cli-vs-programmatic-aeas.md index 02f543ab97..03381533b6 100644 --- a/docs/cli-vs-programmatic-aeas.md +++ b/docs/cli-vs-programmatic-aeas.md @@ -28,7 +28,7 @@ If you want to create the weather station AEA step by step you can follow this g Fetch the weather station AEA with the following command : ``` bash -aea fetch fetchai/weather_station:0.10.0 +aea fetch fetchai/weather_station:0.11.0 cd weather_station ``` @@ -44,9 +44,9 @@ The `is_ledger_tx` will prevent the AEA to communicate with a ledger. Add keys for the weather station. ``` bash -aea generate-key cosmos -aea add-key cosmos cosmos_private_key.txt -aea add-key cosmos cosmos_private_key.txt --connection +aea generate-key fetchai +aea add-key fetchai fetchai_private_key.txt +aea add-key fetchai fetchai_private_key.txt --connection ``` ### Run the weather station AEA @@ -73,8 +73,8 @@ from typing import cast from aea import AEA_DIR from aea.aea import AEA from aea.configurations.base import ConnectionConfig, PublicId -from aea.crypto.cosmos import CosmosCrypto -from aea.crypto.helpers import COSMOS_PRIVATE_KEY_FILE, create_private_key +from aea.crypto.fetchai import FetchAICrypto +from aea.crypto.helpers import PRIVATE_KEY_PATH_SCHEMA, create_private_key from aea.crypto.wallet import Wallet from aea.identity.base import Identity from aea.protocols.base import Protocol @@ -90,9 +90,12 @@ API_KEY = "TwiCIriSl0mLahw17pyqoA" SOEF_ADDR = "soef.fetch.ai" SOEF_PORT = 9002 ENTRY_PEER_ADDRESS = ( - "/dns4/127.0.0.1/tcp/9000/p2p/16Uiu2HAmAzvu5uNbcnD2qaqrkSULhJsc6GJUg3iikWerJkoD72pr" + "/dns4/127.0.0.1/tcp/9000/p2p/16Uiu2HAmLBCAqHL8SuFosyDhAKYsLKXBZBWXBsB9oFw2qU4Kckun" +) +FETCHAI_PRIVATE_KEY_FILE = PRIVATE_KEY_PATH_SCHEMA.format(FetchAICrypto.identifier) +FETCHAI_PRIVATE_KEY_FILE_CONNECTION = PRIVATE_KEY_PATH_SCHEMA.format( + "fetchai_connection" ) -COSMOS_PRIVATE_KEY_FILE_CONNECTION = "cosmos_connection_private_key.txt" ROOT_DIR = os.getcwd() logger = logging.getLogger("aea") @@ -101,23 +104,25 @@ logging.basicConfig(stream=sys.stdout, level=logging.INFO) def run(): # Create a private key - create_private_key(CosmosCrypto.identifier, COSMOS_PRIVATE_KEY_FILE) - create_private_key(CosmosCrypto.identifier, COSMOS_PRIVATE_KEY_FILE_CONNECTION) + create_private_key(FetchAICrypto.identifier, FETCHAI_PRIVATE_KEY_FILE) + create_private_key(FetchAICrypto.identifier, FETCHAI_PRIVATE_KEY_FILE_CONNECTION) # Set up the wallet, identity and (empty) resources wallet = Wallet( - private_key_paths={CosmosCrypto.identifier: COSMOS_PRIVATE_KEY_FILE}, + private_key_paths={FetchAICrypto.identifier: FETCHAI_PRIVATE_KEY_FILE}, connection_private_key_paths={ - CosmosCrypto.identifier: COSMOS_PRIVATE_KEY_FILE_CONNECTION + FetchAICrypto.identifier: FETCHAI_PRIVATE_KEY_FILE_CONNECTION }, ) - identity = Identity("my_aea", address=wallet.addresses.get(CosmosCrypto.identifier)) + identity = Identity( + "my_aea", address=wallet.addresses.get(FetchAICrypto.identifier) + ) resources = Resources() # specify the default routing for some protocols default_routing = { - PublicId.from_str("fetchai/ledger_api:0.2.0"): LedgerConnection.connection_id, - PublicId.from_str("fetchai/oef_search:0.4.0"): SOEFConnection.connection_id, + PublicId.from_str("fetchai/ledger_api:0.3.0"): LedgerConnection.connection_id, + PublicId.from_str("fetchai/oef_search:0.5.0"): SOEFConnection.connection_id, } default_connection = P2PLibp2pConnection.connection_id @@ -184,7 +189,7 @@ def run(): api_key=API_KEY, soef_addr=SOEF_ADDR, soef_port=SOEF_PORT, - restricted_to_protocols={PublicId.from_str("fetchai/oef_search:0.4.0")}, + restricted_to_protocols={PublicId.from_str("fetchai/oef_search:0.5.0")}, connection_id=SOEFConnection.connection_id, ) soef_connection = SOEFConnection(configuration=configuration, identity=identity) diff --git a/docs/config.md b/docs/config.md index 4ecfe350ac..f3f6fffa4e 100644 --- a/docs/config.md +++ b/docs/config.md @@ -17,25 +17,25 @@ author: fetchai # Author handle of the project's version: 0.1.0 # Version of the AEA project (a semantic version number, see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string") description: A demo project # Description of the AEA project license: Apache-2.0 # License of the AEA project -aea_version: '>=0.5.0, <0.6.0' # AEA framework version(s) compatible with the AEA project (a version number that matches PEP 440 version schemes, or a comma-separated list of PEP 440 version specifiers, see https://www.python.org/dev/peps/pep-0440/#version-specifiers) +aea_version: '>=0.6.0, <0.7.0' # AEA framework version(s) compatible with the AEA project (a version number that matches PEP 440 version schemes, or a comma-separated list of PEP 440 version specifiers, see https://www.python.org/dev/peps/pep-0440/#version-specifiers) fingerprint: {} # Fingerprint of AEA project components. fingerprint_ignore_patterns: [] # Ignore pattern for the fingerprinting tool. connections: # The list of connection public ids the AEA project depends on (each public id must satisfy PUBLIC_ID_REGEX) -- fetchai/stub:0.8.0 +- fetchai/stub:0.9.0 contracts: [] # The list of contract public ids the AEA project depends on (each public id must satisfy PUBLIC_ID_REGEX). protocols: # The list of protocol public ids the AEA project depends on (each public id must satisfy PUBLIC_ID_REGEX). -- fetchai/default:0.4.0 +- fetchai/default:0.5.0 skills: # The list of skill public ids the AEA project depends on (each public id must satisfy PUBLIC_ID_REGEX). -- fetchai/error:0.4.0 -default_connection: fetchai/p2p_libp2p:0.7.0 # The default connection used for envelopes sent by the AEA (must satisfy PUBLIC_ID_REGEX). -default_ledger: cosmos # The default ledger identifier the AEA project uses (must satisfy LEDGER_ID_REGEX) +- fetchai/error:0.5.0 +default_connection: fetchai/p2p_libp2p:0.8.0 # The default connection used for envelopes sent by the AEA (must satisfy PUBLIC_ID_REGEX). +default_ledger: fetchai # The default ledger identifier the AEA project uses (must satisfy LEDGER_ID_REGEX) logging_config: # The logging configurations the AEA project uses disable_existing_loggers: false version: 1 private_key_paths: # The private key paths the AEA project uses (keys must satisfy LEDGER_ID_REGEX, values must be file paths) - cosmos: cosmos_private_key.txt + fetchai: fetchai_private_key.txt connection_private_key_paths: # The private key paths the AEA project uses for its connections (keys must satisfy LEDGER_ID_REGEX, values must be file paths) - cosmos: cosmos_private_key.txt + fetchai: fetchai_private_key.txt registry_path: ../packages # The path to the local package registry (must be a directory path and point to a directory called `packages`) ``` @@ -57,9 +57,10 @@ The `connection.yaml`, which is present in each connection package, has the foll name: scaffold # Name of the package (must satisfy PACKAGE_REGEX) author: fetchai # Author handle of the package's author (must satisfy AUTHOR_REGEX) version: 0.1.0 # Version of the package (a semantic version number, see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string") +type: connection # The type of the package; for connections, it must be "connection" description: A scaffold connection # Description of the package license: Apache-2.0 # License of the package -aea_version: '>=0.5.0, <0.6.0' # AEA framework version(s) compatible with the AEA project (a version number that matches PEP 440 version schemes, or a comma-separated list of PEP 440 version specifiers, see https://www.python.org/dev/peps/pep-0440/#version-specifiers) +aea_version: '>=0.6.0, <0.7.0' # AEA framework version(s) compatible with the AEA project (a version number that matches PEP 440 version schemes, or a comma-separated list of PEP 440 version specifiers, see https://www.python.org/dev/peps/pep-0440/#version-specifiers) fingerprint: # Fingerprint of package components. __init__.py: QmZvYZ5ECcWwqiNGh8qNTg735wu51HqaLxTSifUxkQ4KGj connection.py: QmagwVgaPgfeXqVTgcpFESA4DYsteSbojz94SLtmnHNAze @@ -80,9 +81,10 @@ The `contract.yaml`, which is present in each contract package, has the followin name: scaffold # Name of the package (must satisfy PACKAGE_REGEX) author: fetchai # Author handle of the package's author (must satisfy AUTHOR_REGEX) version: 0.1.0 # Version of the package (a semantic version number, see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string") +type: contract # The type of the package; for contracts, it must be "contract" description: A scaffold contract # Description of the package license: Apache-2.0 # License of the package -aea_version: '>=0.5.0, <0.6.0' # AEA framework version(s) compatible with the AEA project (a version number that matches PEP 440 version schemes, or a comma-separated list of PEP 440 version specifiers, see https://www.python.org/dev/peps/pep-0440/#version-specifiers) +aea_version: '>=0.6.0, <0.7.0' # AEA framework version(s) compatible with the AEA project (a version number that matches PEP 440 version schemes, or a comma-separated list of PEP 440 version specifiers, see https://www.python.org/dev/peps/pep-0440/#version-specifiers) fingerprint: # Fingerprint of package components. __init__.py: QmPBwWhEg3wcH1q9612srZYAYdANVdWLDFWKs7TviZmVj6 contract.py: QmXvjkD7ZVEJDJspEz5YApe5bRUxvZHNi8vfyeVHPyQD5G @@ -101,9 +103,10 @@ The `protocol.yaml`, which is present in each protocol package, has the followin name: scaffold # Name of the package (must satisfy PACKAGE_REGEX) author: fetchai # Author handle of the package's author (must satisfy AUTHOR_REGEX) version: 0.1.0 # Version of the package (a semantic version number, see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string") +type: protocol # The type of the package; for protocols, it must be "protocol" description: A scaffold protocol # Description of the package license: Apache-2.0 # License of the package -aea_version: '>=0.5.0, <0.6.0' # AEA framework version(s) compatible with the AEA project (a version number that matches PEP 440 version schemes, or a comma-separated list of PEP 440 version specifiers, see https://www.python.org/dev/peps/pep-0440/#version-specifiers) +aea_version: '>=0.6.0, <0.7.0' # AEA framework version(s) compatible with the AEA project (a version number that matches PEP 440 version schemes, or a comma-separated list of PEP 440 version specifiers, see https://www.python.org/dev/peps/pep-0440/#version-specifiers) fingerprint: # Fingerprint of package components. __init__.py: Qmay9PmfeHqqVa3rdgiJYJnzZzTStboQEfpwXDpcgJMHTJ message.py: QmdvAdYSHNdZyUMrK3ue7quHAuSNwgZZSHqxYXyvh8Nie4 @@ -119,9 +122,10 @@ The `skill.yaml`, which is present in each protocol package, has the following r name: scaffold # Name of the package (must satisfy PACKAGE_REGEX) author: fetchai # Author handle of the package's author (must satisfy AUTHOR_REGEX) version: 0.1.0 # Version of the package (a semantic version number, see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string") +type: skill # The type of the package; for skills, it must be "skill" description: A scaffold skill # Description of the package license: Apache-2.0 # License of the package -aea_version: '>=0.5.0, <0.6.0' # AEA framework version(s) compatible with the AEA project (a version number that matches PEP 440 version schemes, or a comma-separated list of PEP 440 version specifiers, see https://www.python.org/dev/peps/pep-0440/#version-specifiers) +aea_version: '>=0.6.0, <0.7.0' # AEA framework version(s) compatible with the AEA project (a version number that matches PEP 440 version schemes, or a comma-separated list of PEP 440 version specifiers, see https://www.python.org/dev/peps/pep-0440/#version-specifiers) fingerprint: # Fingerprint of package components. __init__.py: QmNkZAetyctaZCUf6ACxP5onGWsSxu2hjSNoFmJ3ta6Lta behaviours.py: QmYa1rczhGTtMJBgCd1QR9uZhhkf45orm7TnGTE5Eizjpy diff --git a/docs/connect-a-frontend.md b/docs/connect-a-frontend.md index 2c1e9119d1..bcb045a201 100644 --- a/docs/connect-a-frontend.md +++ b/docs/connect-a-frontend.md @@ -3,7 +3,7 @@ This demo discusses the options we have to connect a front-end to the AEA. The f How to connect frontend to your AEA ## Case 1 -The first option we have is to create a `Connection` that will handle the incoming requests from the rest API. In this scenario, the rest API communicates with the AEA and requests are handled by the `HTTP Server` Connection package. The rest API should send CRUD requests to the `HTTP Server` Connection (`fetchai/http_server:0.6.0`) which translates these into Envelopes to be consumed by the correct skill. +The first option we have is to create a `Connection` that will handle the incoming requests from the rest API. In this scenario, the rest API communicates with the AEA and requests are handled by the `HTTP Server` Connection package. The rest API should send CRUD requests to the `HTTP Server` Connection (`fetchai/http_server:0.7.0`) which translates these into Envelopes to be consumed by the correct skill. ## Case 2 -The other option we have is to create a stand-alone `Multiplexer` with a `P2P` connection (`fetchai/p2p_libp2p:0.7.0`). In this scenario, the front-end needs to incorporate a Multiplexer with an `P2P` Connection. Then the Agent Communication Network can be used to send Envelopes from the AEA to the front-end. +The other option we have is to create a stand-alone `Multiplexer` with a `P2P` connection (`fetchai/p2p_libp2p:0.8.0`). In this scenario, the front-end needs to incorporate a Multiplexer with an `P2P` Connection. Then the Agent Communication Network can be used to send Envelopes from the AEA to the front-end. diff --git a/docs/contract.md b/docs/contract.md index 1c8609b0b2..7876c43589 100644 --- a/docs/contract.md +++ b/docs/contract.md @@ -1,6 +1,102 @@ -`Contracts` wrap smart contracts for third-party decentralized ledgers. In particular, they provide wrappers around the API or ABI of a smart contract. They expose an API to abstract implementation specifics of the ABI from the skills. +`Contracts` wrap smart contracts for Fetch.ai and third-party decentralized ledgers. In particular, they provide wrappers around the API or ABI of a smart contract and its byte code. They implement a translation between framework messages (in the `fetchai/contract_api:0.3.0` protocol) and the implementation specifics of the ABI. + +Contracts usually implement four types of methods: + +- a method to create a smart contract deployment transaction, +- methods to create transactions to modify state in the deployed smart contract, +- methods to create contract calls to execute static methods on the deployed smart contract, and +- methods to query the state of the deployed smart contract. + +Contracts can be added as packages which means they become reusable across AEA projects. + +The smart contract wrapped in a AEA contract package might be a third-party smart contract or your own smart contract potentially interacting with a third-party contract on-chain. + + +## Interacting with contracts from skills + +Interacting with contracts in almost all cases requires network access. Therefore, the framework executes contract related logic in a Connection. + +In particular, the `fetchai/ledger:0.4.0` connection can be used to execute contract related logic. The skills communicate with the `fetchai/ledger:0.4.0` connection via the `fetchai/contract_api:0.3.0` protocol. This protocol implements a request-response pattern to serve the four types of methods listed above: + +- the `get_deploy_transaction` message is used to request a deploy transaction for a specific contract. For instance, to request a deploy transaction for the deployment of the smart contract wrapped in the `fetchai/erc1155:0.9.0` package, we send the following message to the `fetchai/ledger:0.4.0`: + +``` python +contract_api_msg = ContractApiMessage( + performative=ContractApiMessage.Performative.GET_DEPLOY_TRANSACTION, + dialogue_reference=contract_api_dialogues.new_self_initiated_dialogue_reference(), + ledger_id=strategy.ledger_id, + contract_id="fetchai/erc1155:0.9.0", + callable="get_deploy_transaction", + kwargs=ContractApiMessage.Kwargs( + {"deployer_address": self.context.agent_address} + ), +) +``` +This message will be handled by the `fetchai/ledger:0.4.0` connection and then a `raw_transaction` message will be returned with the matching raw transaction. To send this transaction to the ledger for processing, we first sign the message with the decision maker and then send the signed transaction to the `fetchai/ledger:0.4.0` connection using the `fetchai/ledger_api:0.3.0` protocol. + +- the `get_raw_transaction` message is used to request any transaction for a specific contract which changes state in the contract. For instance, to request a transaction for the creation of token in the deployed `erc1155` smart contract wrapped in the `fetchai/erc1155:0.9.0` package, we send the following message to the `fetchai/ledger:0.4.0`: + +``` python +contract_api_msg = ContractApiMessage( + performative=ContractApiMessage.Performative.GET_RAW_TRANSACTION, + dialogue_reference=contract_api_dialogues.new_self_initiated_dialogue_reference(), + ledger_id=strategy.ledger_id, + contract_id="fetchai/erc1155:0.9.0", + contract_address=strategy.contract_address, + callable="get_create_batch_transaction", + kwargs=ContractApiMessage.Kwargs( + { + "deployer_address": self.context.agent_address, + "token_ids": strategy.token_ids, + } + ), +) +``` +This message will be handled by the `fetchai/ledger:0.4.0` connection and then a `raw_transaction` message will be returned with the matching raw transaction. For this to be executed correctly, the `fetchai/erc1155:0.9.0` contract package needs to implement the `get_create_batch_transaction` method with the specified key word arguments. Similarly to above, to send this transaction to the ledger for processing, we first sign the message with the decision maker and then send the signed transaction to the `fetchai/ledger:0.4.0` connection using the `fetchai/ledger_api:0.3.0` protocol. + +- the `get_raw_message` message is used to request any contract method call for a specific contract which does not change state in the contract. For instance, to request a call to get a hash from some input data in the deployed `erc1155` smart contract wrapped in the `fetchai/erc1155:0.9.0` package, we send the following message to the `fetchai/ledger:0.4.0`: + +``` python +contract_api_msg = ContractApiMessage( + performative=ContractApiMessage.Performative.GET_RAW_MESSAGE, + dialogue_reference=contract_api_dialogues.new_self_initiated_dialogue_reference(), + ledger_id=strategy.ledger_id, + contract_id="fetchai/erc1155:0.9.0", + contract_address=strategy.contract_address, + callable="get_hash_single", + kwargs=ContractApiMessage.Kwargs( + { + "from_address": from_address, + "to_address": to_address, + "token_id": token_id, + "from_supply": from_supply, + "to_supply": to_supply, + "value": value, + "trade_nonce": trade_nonce, + } + ), +) +``` +This message will be handled by the `fetchai/ledger:0.4.0` connection and then a `raw_message` message will be returned with the matching raw message. For this to be executed correctly, the `fetchai/erc1155:0.9.0` contract package needs to implement the `get_hash_single` method with the specified key word arguments. We can then send the raw message to the `fetchai/ledger:0.4.0` connection using the `fetchai/ledger_api:0.3.0` protocol. In this case, signing is not required. + + +- the `get_state` message is used to request any contract method call to query state in the deployed contract. For instance, to request a call to get the balances in the deployed `erc1155` smart contract wrapped in the `fetchai/erc1155:0.9.0` package, we send the following message to the `fetchai/ledger:0.4.0`: + +``` python +contract_api_msg = ContractApiMessage( + performative=ContractApiMessage.Performative.GET_STATE, + dialogue_reference=contract_api_dialogues.new_self_initiated_dialogue_reference(), + ledger_id=strategy.ledger_id, + contract_id="fetchai/erc1155:0.9.0", + contract_address=strategy.contract_address, + callable="get_balance", + kwargs=ContractApiMessage.Kwargs( + {"agent_address": address, "token_id": token_id} + ), +) +``` +This message will be handled by the `fetchai/ledger:0.4.0` connection and then a `state` message will be returned with the matching state. For this to be executed correctly, the `fetchai/erc1155:0.9.0` contract package needs to implement the `get_balance` method with the specified key word arguments. We can then send the raw message to the `fetchai/ledger:0.4.0` connection using the `fetchai/ledger_api:0.3.0` protocol. In this case, signing is not required. -Contracts usually contain the logic to create contract transactions. Contracts can be added as packages. ## Developing your own @@ -15,3 +111,63 @@ This will scaffold a contract package called `my_new_contract` with three files: * `__init__.py` * `contract.py`, containing the scaffolded contract class * `contract.yaml` containing the scaffolded configuration file + + +Once your scaffold is in place, you can create a `build` folder in the package and copy the smart contract interface (e.g. bytes code and ABI) to it. Then, specify the path to the interfaces in the `contract.yaml`. For instance, if you use Ethereum, then you might specify the following: + +``` yaml +contract_interface_paths: + ethereum: build/my_contract.json +``` +where `ethereum` is the ledger id and `my_contract.json` is the file containing the byte code and ABI. + + +Finally, you will want to implement the part of the contract interface you need in `contract.py`: + +``` python +from aea.contracts.base import Contract +from aea.crypto.base import LedgerApi + + +class MyContract(Contract): + """The MyContract contract class which acts as a bridge between AEA framework and ERC1155 ABI.""" + + @classmethod + def get_create_batch_transaction( + cls, + ledger_api: LedgerApi, + contract_address: str, + deployer_address: str, + token_ids: List[int], + data: Optional[bytes] = b"", + gas: int = 300000, + ) -> Dict[str, Any]: + """ + Get the transaction to create a batch of tokens. + + :param ledger_api: the ledger API + :param contract_address: the address of the contract + :param deployer_address: the address of the deployer + :param token_ids: the list of token ids for creation + :param data: the data to include in the transaction + :param gas: the gas to be used + :return: the transaction object + """ + # create the transaction dict + nonce = ledger_api.api.eth.getTransactionCount(deployer_address) + instance = cls.get_instance(ledger_api, contract_address) + tx = instance.functions.createBatch( + deployer_address, token_ids + ).buildTransaction( + { + "gas": gas, + "gasPrice": ledger_api.api.toWei("50", "gwei"), + "nonce": nonce, + } + ) + tx = cls._try_estimate_gas(ledger_api, tx) + return tx +``` +Above, we implement a method to create a transaction, in this case a transaction to create a batch of tokens. The method will be called by the framework, specifically the `fetchai/ledger:0.4.0` connection once it receives a message (see bullet point 2 above). The method first gets the latest transaction nonce of the `deployer_address`, then constracts the contract instance, then uses the instance to build the transaction and finally updates the gas on the transaction. + +It helps to look at existing contract packages, like `fetchai/erc1155:0.9.0`, and skills using them, like `fetchai/erc1155_client:0.11.0` and `fetchai/erc1155_deploy:0.12.0`, for inspiration and guidance. diff --git a/docs/core-components-1.md b/docs/core-components-1.md index 675b859aab..78126f2783 100644 --- a/docs/core-components-1.md +++ b/docs/core-components-1.md @@ -24,7 +24,7 @@ Messages must adhere to a protocol. ### Protocol -`Protocols` define agent to agent interactions, which include: +`Protocols` define agent to agent as well as agent component to component interactions, which include: * messages, which define the representation; @@ -32,11 +32,11 @@ Messages must adhere to a protocol. * dialogues, which define rules over message sequences. -The framework provides one default protocol, called `default`. This protocol provides a bare bones implementation for an AEA protocol which includes a `DefaultMessage` class and associated `DefaultSerializer` and `DefaultDialogue` classes. +The framework provides one default protocol, called `default` (current version `fetchai/default:0.5.0`). This protocol provides a bare bones implementation for an AEA protocol which includes a `DefaultMessage` class and associated `DefaultSerializer` and `DefaultDialogue` classes. Additional protocols - i.e. a new type of interaction - can be added as packages or generated with the protocol generator. For more details on protocols also read the protocol guide here. -Protocol specific messages, wrapped in envelopes, are sent and received to other agents and services via connections. +Protocol specific messages, wrapped in envelopes, are sent and received to other agents, agent components and services via connections. ### Connection @@ -54,7 +54,7 @@ An AEA can run connections via a multiplexer. The `Multiplexer` is responsible for maintaining potentially multiple connections. -It maintains an `InBox` and `OutBox`, which are, respectively, queues for incoming and outgoing envelopes. +It maintains an `InBox` and `OutBox`, which are, respectively, queues for incoming and outgoing envelopes from the perspective of skills. ### Skill @@ -64,14 +64,14 @@ It maintains an `InBox` and `Handler`: each skill has none, one or more `Handler` objects, each responsible for the registered messaging protocol. Handlers implement AEAs' **reactive** behaviour. If the AEA understands the protocol referenced in a received `Envelope`, the `Handler` reacts appropriately to the corresponding message. Each `Handler` is responsible for only one protocol. A `Handler` is also capable of dealing with internal messages (see next section). +* `Handler`: each skill has none, one or more `Handler` objects, each responsible for the registered messaging protocol. Handlers implement AEAs' **reactive** behaviour. If the AEA understands the protocol referenced in a received `Envelope`, the `Handler` reacts appropriately to the corresponding message. Each `Handler` is responsible for only one protocol. * `Behaviour`: none, one or more `Behaviours` encapsulate actions which futher the AEAs goal and are initiated by internals of the AEA, rather than external events. Behaviours implement AEAs' **pro-activeness**. The framework provides a number of abstract base classes implementing different types of behaviours (e.g. cyclic/one-shot/finite-state-machine/etc.). * `Model`: none, one or more `Models` that inherit from the `Model` can be accessed via the `SkillContext`. -* `Task`: none, one or more `Tasks` encapsulate background work internal to the AEA. `Task` differs from the other three in that it is not a part of skills, but `Task`s are declared in or from skills if a packaging approach for AEA creation is used. +* `Task`: none, one or more `Tasks` encapsulate background work internal to the AEA. `Task` differs from the other three in that it is not a part of skills, but `Tasks` are declared in or from skills if a packaging approach for AEA creation is used. A skill can read (parts of) the state of the the AEA (as summarised in the `AgentContext`), and suggest actions to the AEA according to its specific logic. As such, more than one skill could exist per protocol, competing with each other in suggesting to the AEA the best course of actions to take. In technical terms this means skills are horizontally arranged. -For instance, an AEA who is trading goods, could subscribe to more than one skill, where each skill corresponds to a different trading strategy. The skills could then read the preference and ownership state of the AEA, and independently suggest profitable transactions. +For instance, an AEA which is trading goods, could subscribe to more than one skill, where each skill corresponds to a different trading strategy. The skills could then read the preference and ownership state of the AEA, and independently suggest profitable transactions. The framework places no limits on the complexity of skills. They can implement simple (e.g. `if-this-then-that`) or complex (e.g. a deep learning model or reinforcement learning agent). diff --git a/docs/core-components-2.md b/docs/core-components-2.md index 6c54d88bce..a149e265f0 100644 --- a/docs/core-components-2.md +++ b/docs/core-components-2.md @@ -10,7 +10,7 @@ In Core Components - Part 1 we discussed the The `DecisionMaker` can be thought of like a wallet manager plus "economic brain" of the AEA. It is responsible for the AEA's crypto-economic security and goal management, and it contains the preference and ownership representation of the AEA. The decision maker is the only component which has access to the wallet's private keys. -You can learn more about the decision maker here. +You can learn more about the decision maker here. In the simplest form, it acts like a `Handler` with a `Wallet`. ### Wallet diff --git a/docs/decision-maker-transaction.md b/docs/decision-maker-transaction.md index 640bcb8053..9801eef68f 100644 --- a/docs/decision-maker-transaction.md +++ b/docs/decision-maker-transaction.md @@ -10,14 +10,14 @@ from typing import Optional, cast from aea.aea_builder import AEABuilder from aea.configurations.base import ProtocolId, SkillConfig -from aea.crypto.cosmos import CosmosCrypto +from aea.crypto.fetchai import FetchAICrypto from aea.crypto.helpers import create_private_key from aea.crypto.ledger_apis import LedgerApis from aea.crypto.wallet import Wallet -from aea.helpers.dialogue.base import Dialogue, DialogueLabel from aea.helpers.transaction.base import RawTransaction, Terms from aea.identity.base import Identity -from aea.protocols.base import Message +from aea.protocols.base import Address, Message +from aea.protocols.dialogue.base import Dialogue from aea.protocols.signing.dialogues import SigningDialogue from aea.protocols.signing.dialogues import SigningDialogues as BaseSigningDialogues from aea.protocols.signing.message import SigningMessage @@ -26,8 +26,8 @@ from aea.skills.base import Handler, Model, Skill, SkillContext logger = logging.getLogger("aea") logging.basicConfig(level=logging.INFO) -COSMOS_PRIVATE_KEY_FILE_1 = "cosmos_private_key_1.txt" -COSMOS_PRIVATE_KEY_FILE_2 = "cosmos_private_key_2.txt" +FETCHAI_PRIVATE_KEY_FILE_1 = "fetchai_private_key_1.txt" +FETCHAI_PRIVATE_KEY_FILE_2 = "fetchai_private_key_2.txt" ``` ## Create a private key and an AEA @@ -37,7 +37,7 @@ To have access to the decision-maker, which is responsible for signing transacti ``` python # Create a private key create_private_key( - CosmosCrypto.identifier, private_key_file=COSMOS_PRIVATE_KEY_FILE_1 + FetchAICrypto.identifier, private_key_file=FETCHAI_PRIVATE_KEY_FILE_1 ) # Instantiate the builder and build the AEA @@ -46,7 +46,7 @@ To have access to the decision-maker, which is responsible for signing transacti builder.set_name("my_aea") - builder.add_private_key(CosmosCrypto.identifier, COSMOS_PRIVATE_KEY_FILE_1) + builder.add_private_key(FetchAICrypto.identifier, FETCHAI_PRIVATE_KEY_FILE_1) # Create our AEA my_aea = builder.build() @@ -64,7 +64,9 @@ Add a simple skill with a signing handler and the signing dialogues. skill_context=skill_context, name="signing_handler" ) signing_dialogues_model = SigningDialogues( - skill_context=skill_context, name="signing_dialogues" + skill_context=skill_context, + name="signing_dialogues", + self_address=str(skill_config.public_id), ) simple_skill = Skill( @@ -79,15 +81,15 @@ Add a simple skill with a signing handler and the signing dialogues. ## Create a second identity ``` python create_private_key( - CosmosCrypto.identifier, private_key_file=COSMOS_PRIVATE_KEY_FILE_2 + FetchAICrypto.identifier, private_key_file=FETCHAI_PRIVATE_KEY_FILE_2 ) - counterparty_wallet = Wallet({CosmosCrypto.identifier: COSMOS_PRIVATE_KEY_FILE_2}) + counterparty_wallet = Wallet({FetchAICrypto.identifier: FETCHAI_PRIVATE_KEY_FILE_2}) counterparty_identity = Identity( name="counterparty_aea", addresses=counterparty_wallet.addresses, - default_address_key=CosmosCrypto.identifier, + default_address_key=FetchAICrypto.identifier, ) ``` @@ -97,7 +99,7 @@ Next, we are creating the signing message and we send it to the decision-maker. ``` python # create signing message for decision maker to sign terms = Terms( - ledger_id=CosmosCrypto.identifier, + ledger_id=FetchAICrypto.identifier, sender_address=my_aea.identity.address, counterparty_address=counterparty_identity.address, amount_by_currency_id={"FET": -1}, @@ -117,14 +119,12 @@ Next, we are creating the signing message and we send it to the decision-maker. signing_msg = SigningMessage( performative=SigningMessage.Performative.SIGN_TRANSACTION, dialogue_reference=signing_dialogues.new_self_initiated_dialogue_reference(), - skill_callback_ids=(str(skill_context.skill_id),), - raw_transaction=RawTransaction(CosmosCrypto.identifier, stub_transaction), + raw_transaction=RawTransaction(FetchAICrypto.identifier, stub_transaction), terms=terms, - skill_callback_info={"some_info_key": "some_info_value"}, ) - signing_msg.counterparty = "decision_maker" signing_dialogue = cast( - Optional[SigningDialogue], signing_dialogues.update(signing_msg) + Optional[SigningDialogue], + signing_dialogues.create_with_message("decision_maker", signing_msg), ) assert signing_dialogue is not None my_aea.context.decision_maker_message_queue.put_nowait(signing_msg) @@ -157,39 +157,32 @@ After the completion of the signing, we get the signed transaction. To be able to register a handler that reads the internal messages, we have to create a class at the end of the file which processes the signing messages. ``` python class SigningDialogues(Model, BaseSigningDialogues): - def __init__(self, **kwargs) -> None: + """Signing dialogues model.""" + + def __init__(self, self_address: Address, **kwargs) -> None: """ Initialize dialogues. :return: None """ Model.__init__(self, **kwargs) - BaseSigningDialogues.__init__(self, self.context.agent_address) - - @staticmethod - def role_from_first_message(message: Message) -> Dialogue.Role: - """Infer the role of the agent from an incoming/outgoing first message - - :param message: an incoming/outgoing first message - :return: The role of the agent - """ - return SigningDialogue.Role.SKILL - def create_dialogue( - self, dialogue_label: DialogueLabel, role: Dialogue.Role, - ) -> SigningDialogue: - """ - Create an instance of fipa dialogue. - - :param dialogue_label: the identifier of the dialogue - :param role: the role of the agent this dialogue is maintained for - - :return: the created dialogue - """ - dialogue = SigningDialogue( - dialogue_label=dialogue_label, agent_address=self.agent_address, role=role + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> Dialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message + + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return SigningDialogue.Role.SKILL + + BaseSigningDialogues.__init__( + self, + self_address=self_address, + role_from_first_message=role_from_first_message, ) - return dialogue class SigningHandler(Handler): @@ -305,14 +298,14 @@ from typing import Optional, cast from aea.aea_builder import AEABuilder from aea.configurations.base import ProtocolId, SkillConfig -from aea.crypto.cosmos import CosmosCrypto +from aea.crypto.fetchai import FetchAICrypto from aea.crypto.helpers import create_private_key from aea.crypto.ledger_apis import LedgerApis from aea.crypto.wallet import Wallet -from aea.helpers.dialogue.base import Dialogue, DialogueLabel from aea.helpers.transaction.base import RawTransaction, Terms from aea.identity.base import Identity -from aea.protocols.base import Message +from aea.protocols.base import Address, Message +from aea.protocols.dialogue.base import Dialogue from aea.protocols.signing.dialogues import SigningDialogue from aea.protocols.signing.dialogues import SigningDialogues as BaseSigningDialogues from aea.protocols.signing.message import SigningMessage @@ -321,14 +314,14 @@ from aea.skills.base import Handler, Model, Skill, SkillContext logger = logging.getLogger("aea") logging.basicConfig(level=logging.INFO) -COSMOS_PRIVATE_KEY_FILE_1 = "cosmos_private_key_1.txt" -COSMOS_PRIVATE_KEY_FILE_2 = "cosmos_private_key_2.txt" +FETCHAI_PRIVATE_KEY_FILE_1 = "fetchai_private_key_1.txt" +FETCHAI_PRIVATE_KEY_FILE_2 = "fetchai_private_key_2.txt" def run(): # Create a private key create_private_key( - CosmosCrypto.identifier, private_key_file=COSMOS_PRIVATE_KEY_FILE_1 + FetchAICrypto.identifier, private_key_file=FETCHAI_PRIVATE_KEY_FILE_1 ) # Instantiate the builder and build the AEA @@ -337,7 +330,7 @@ def run(): builder.set_name("my_aea") - builder.add_private_key(CosmosCrypto.identifier, COSMOS_PRIVATE_KEY_FILE_1) + builder.add_private_key(FetchAICrypto.identifier, FETCHAI_PRIVATE_KEY_FILE_1) # Create our AEA my_aea = builder.build() @@ -349,7 +342,9 @@ def run(): skill_context=skill_context, name="signing_handler" ) signing_dialogues_model = SigningDialogues( - skill_context=skill_context, name="signing_dialogues" + skill_context=skill_context, + name="signing_dialogues", + self_address=str(skill_config.public_id), ) simple_skill = Skill( @@ -362,20 +357,20 @@ def run(): # create a second identity create_private_key( - CosmosCrypto.identifier, private_key_file=COSMOS_PRIVATE_KEY_FILE_2 + FetchAICrypto.identifier, private_key_file=FETCHAI_PRIVATE_KEY_FILE_2 ) - counterparty_wallet = Wallet({CosmosCrypto.identifier: COSMOS_PRIVATE_KEY_FILE_2}) + counterparty_wallet = Wallet({FetchAICrypto.identifier: FETCHAI_PRIVATE_KEY_FILE_2}) counterparty_identity = Identity( name="counterparty_aea", addresses=counterparty_wallet.addresses, - default_address_key=CosmosCrypto.identifier, + default_address_key=FetchAICrypto.identifier, ) # create signing message for decision maker to sign terms = Terms( - ledger_id=CosmosCrypto.identifier, + ledger_id=FetchAICrypto.identifier, sender_address=my_aea.identity.address, counterparty_address=counterparty_identity.address, amount_by_currency_id={"FET": -1}, @@ -395,14 +390,12 @@ def run(): signing_msg = SigningMessage( performative=SigningMessage.Performative.SIGN_TRANSACTION, dialogue_reference=signing_dialogues.new_self_initiated_dialogue_reference(), - skill_callback_ids=(str(skill_context.skill_id),), - raw_transaction=RawTransaction(CosmosCrypto.identifier, stub_transaction), + raw_transaction=RawTransaction(FetchAICrypto.identifier, stub_transaction), terms=terms, - skill_callback_info={"some_info_key": "some_info_value"}, ) - signing_msg.counterparty = "decision_maker" signing_dialogue = cast( - Optional[SigningDialogue], signing_dialogues.update(signing_msg) + Optional[SigningDialogue], + signing_dialogues.create_with_message("decision_maker", signing_msg), ) assert signing_dialogue is not None my_aea.context.decision_maker_message_queue.put_nowait(signing_msg) @@ -423,39 +416,32 @@ def run(): class SigningDialogues(Model, BaseSigningDialogues): - def __init__(self, **kwargs) -> None: + """Signing dialogues model.""" + + def __init__(self, self_address: Address, **kwargs) -> None: """ Initialize dialogues. :return: None """ Model.__init__(self, **kwargs) - BaseSigningDialogues.__init__(self, self.context.agent_address) - - @staticmethod - def role_from_first_message(message: Message) -> Dialogue.Role: - """Infer the role of the agent from an incoming/outgoing first message - - :param message: an incoming/outgoing first message - :return: The role of the agent - """ - return SigningDialogue.Role.SKILL - def create_dialogue( - self, dialogue_label: DialogueLabel, role: Dialogue.Role, - ) -> SigningDialogue: - """ - Create an instance of fipa dialogue. - - :param dialogue_label: the identifier of the dialogue - :param role: the role of the agent this dialogue is maintained for - - :return: the created dialogue - """ - dialogue = SigningDialogue( - dialogue_label=dialogue_label, agent_address=self.agent_address, role=role + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> Dialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message + + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return SigningDialogue.Role.SKILL + + BaseSigningDialogues.__init__( + self, + self_address=self_address, + role_from_first_message=role_from_first_message, ) - return dialogue class SigningHandler(Handler): diff --git a/docs/decision-maker.md b/docs/decision-maker.md index f5046e44be..ad3b694386 100644 --- a/docs/decision-maker.md +++ b/docs/decision-maker.md @@ -2,31 +2,31 @@ The `DecisionMaker``InternalMessages`. There exist two types of these: +Skills communicate with the decision maker via `Messages`. At present, the decision maker processes messages of two protocols: -- `TransactionMessage`: it is used by skills to propose a transaction to the decision-maker. It can be used either for settling the transaction on-chain or to sign a transaction to be used within a negotiation. +- `SigningMessage`: it is used by skills to propose a transaction to the decision-maker for signing. -- `StateUpdateMessage`: it is used to initialize the decision maker with preferences and ownership states. It can also be used to update the ownership states in the decision maker if the settlement of transaction takes place off chain. +- `StateUpdateMessage`: it is used to initialize the decision maker with preferences and ownership states. It can also be used to update the ownership states in the decision maker if the settlement of transaction takes place. -An `InternalMessage`, say `tx_msg` is sent to the decision maker like so from any skill: +A message, say `msg`, is sent to the decision maker like so from any skill: ``` -self.context.decision_maker_message_queue.put_nowait(tx_msg) +self.context.decision_maker_message_queue.put_nowait(msg) ``` The decision maker processes messages and can accept or reject them. -To process `InternalMessages` from the decision maker in a given skill you need to create a `TransactionHandler` like so: +To process `Messages` from the decision maker in a given skill you need to create a `Handler`, in particular a `SigningHandler` like so: ``` python -class TransactionHandler(Handler): +class SigningHandler(Handler): - protocol_id = InternalMessage.protocol_id + protocol_id = SigningMessage.protocol_id def handle(self, message: Message): """ - Handle an internal message. + Handle a signing message. - :param message: the internal message from the decision maker. + :param message: the signing message from the decision maker. """ # code to handle the message ``` @@ -39,7 +39,7 @@ The framework implements a default

Note

diff --git a/docs/demos.md b/docs/demos.md new file mode 100644 index 0000000000..6d33a2fb6f --- /dev/null +++ b/docs/demos.md @@ -0,0 +1,5 @@ +We provide demo guides for multiple use-cases, each one involving several AEAs interacting in a different scenario. + +These demos serve to highlight the concept of AEAs as well as provide inspiration for developers. + +Demos are alphabetically sorted, we recommend you start with the
weather skills demo. \ No newline at end of file diff --git a/docs/deployment.md b/docs/deployment.md index e6d64b2b4d..dc6ab0f443 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,9 +1,39 @@ + +The easiest way to run an AEA is using your development environment. + +If you would like to run an AEA from a browser you can use Google Colab. This gist can be opened in Colab and implements the quickstart. + +For deployment, we recommend you use Docker. + +## Building a Docker Image + +First, we fetch a directory containing a Dockerfile and some dependencies: +``` bash +svn export https://github.com/fetchai/agents-aea/branches/master/deploy-image +cd deploy-image +rm -rf scripts +svn export https://github.com/fetchai/docker-images/branches/master/scripts +cd .. +``` + +Next, we build the image: +``` bash +./deploy-image/scripts/docker-build-img.sh -t aea-deploy:latest -- +``` + +## Running a Docker Image + +Finally, we run it: +``` bash +docker run -it aea-deploy:latest +``` + +This will run the `fetchai/my_first_aea:0.10.0` demo project. You can edit `entrypoint.sh` to run whatever project you would like. + +## Deployment +

Note

This section is incomplete and will soon be updated.

- -The easiest way to run an AEA is using your development environment. - -If you would like to run an AEA from a browser you can use Google Colab. This gist can be opened in Colab and implements the quickstart. diff --git a/docs/erc1155-skills.md b/docs/erc1155-skills.md index bc39def129..7b4d26a595 100644 --- a/docs/erc1155-skills.md +++ b/docs/erc1155-skills.md @@ -26,7 +26,7 @@ with a one-step atomic swap functionality. That means the trade between the two Fetch the AEA that will deploy the contract. ``` bash -aea fetch fetchai/erc1155_deployer:0.11.0 +aea fetch fetchai/erc1155_deployer:0.12.0 cd erc1155_deployer aea install ``` @@ -39,20 +39,20 @@ Create the AEA that will deploy the contract. ``` bash aea create erc1155_deployer cd erc1155_deployer -aea add connection fetchai/p2p_libp2p:0.7.0 -aea add connection fetchai/soef:0.6.0 -aea add connection fetchai/ledger:0.3.0 -aea add skill fetchai/erc1155_deploy:0.11.0 +aea add connection fetchai/p2p_libp2p:0.8.0 +aea add connection fetchai/soef:0.7.0 +aea add connection fetchai/ledger:0.4.0 +aea add skill fetchai/erc1155_deploy:0.12.0 aea install -aea config set agent.default_connection fetchai/p2p_libp2p:0.7.0 +aea config set agent.default_connection fetchai/p2p_libp2p:0.8.0 ``` Then update the agent config (`aea-config.yaml`) with the default routing: ``` yaml default_routing: - fetchai/contract_api:0.2.0: fetchai/ledger:0.3.0 - fetchai/ledger_api:0.2.0: fetchai/ledger:0.3.0 - fetchai/oef_search:0.4.0: fetchai/soef:0.6.0 + fetchai/contract_api:0.3.0: fetchai/ledger:0.4.0 + fetchai/ledger_api:0.3.0: fetchai/ledger:0.4.0 + fetchai/oef_search:0.5.0: fetchai/soef:0.7.0 ``` And change the default ledger: @@ -67,7 +67,7 @@ Additionally, create the private key for the deployer AEA. Generate and add a ke ``` bash aea generate-key ethereum -aea add-key ethereum eth_private_key.txt +aea add-key ethereum ethereum_private_key.txt ``` And one for the P2P connection: @@ -81,7 +81,7 @@ aea add-key cosmos cosmos_private_key.txt --connection In another terminal, fetch the AEA that will get some tokens from the deployer. ``` bash -aea fetch fetchai/erc1155_client:0.11.0 +aea fetch fetchai/erc1155_client:0.12.0 cd erc1155_client aea install ``` @@ -94,20 +94,20 @@ Create the AEA that will get some tokens from the deployer. ``` bash aea create erc1155_client cd erc1155_client -aea add connection fetchai/p2p_libp2p:0.7.0 -aea add connection fetchai/soef:0.6.0 -aea add connection fetchai/ledger:0.3.0 -aea add skill fetchai/erc1155_client:0.10.0 +aea add connection fetchai/p2p_libp2p:0.8.0 +aea add connection fetchai/soef:0.7.0 +aea add connection fetchai/ledger:0.4.0 +aea add skill fetchai/erc1155_client:0.11.0 aea install -aea config set agent.default_connection fetchai/p2p_libp2p:0.7.0 +aea config set agent.default_connection fetchai/p2p_libp2p:0.8.0 ``` Then update the agent config (`aea-config.yaml`) with the default routing: ``` yaml default_routing: - fetchai/contract_api:0.2.0: fetchai/ledger:0.3.0 - fetchai/ledger_api:0.2.0: fetchai/ledger:0.3.0 - fetchai/oef_search:0.4.0: fetchai/soef:0.6.0 + fetchai/contract_api:0.3.0: fetchai/ledger:0.4.0 + fetchai/ledger_api:0.3.0: fetchai/ledger:0.4.0 + fetchai/oef_search:0.5.0: fetchai/soef:0.7.0 ``` And change the default ledger: @@ -122,7 +122,7 @@ Additionally, create the private key for the client AEA. Generate and add a key ``` bash aea generate-key ethereum -aea add-key ethereum eth_private_key.txt +aea add-key ethereum ethereum_private_key.txt ``` And one for the P2P connection: diff --git a/docs/generic-skills-step-by-step.md b/docs/generic-skills-step-by-step.md index 7be9a3bdc7..70fbe0733f 100644 --- a/docs/generic-skills-step-by-step.md +++ b/docs/generic-skills-step-by-step.md @@ -1,4 +1,4 @@ -This guide is a step-by-step introduction to building an AEA that represents static, and dynamic data to be advertised on the Open Economic Framework. +This guide is a step-by-step introduction to building an AEA that represents static, and dynamic data to be advertised on the Open Economic Framework. If you simply want to run the resulting AEAs go here. @@ -20,7 +20,7 @@ If you simply want to follow the software part of the guide then you only requir ### Setup the environment (Optional) -You can follow the guide here in order to setup your environment and prepare your Raspberry Pi. +You can follow the guide here in order to setup your environment and prepare your Raspberry Pi. Once you setup your Raspberry Pi, open a terminal and navigate to `/etc/udev/rules.d/`. Create a new file there (I named mine `99-hidraw-permissions.rules`) ``` bash @@ -41,16 +41,16 @@ Follow the Preliminaries and None: """ @@ -170,13 +167,11 @@ class GenericServiceRegistrationBehaviour(TickerBehaviour): oef_search_dialogues = cast( OefSearchDialogues, self.context.oef_search_dialogues ) - oef_search_msg = OefSearchMessage( + oef_search_msg, _ = oef_search_dialogues.create( + counterparty=self.context.search_service_address, performative=OefSearchMessage.Performative.REGISTER_SERVICE, - dialogue_reference=oef_search_dialogues.new_self_initiated_dialogue_reference(), service_description=description, ) - oef_search_msg.counterparty = self.context.search_service_address - oef_search_dialogues.update(oef_search_msg) self.context.outbox.put_message(message=oef_search_msg) self.context.logger.info("registering agent on SOEF.") @@ -191,13 +186,11 @@ class GenericServiceRegistrationBehaviour(TickerBehaviour): oef_search_dialogues = cast( OefSearchDialogues, self.context.oef_search_dialogues ) - oef_search_msg = OefSearchMessage( + oef_search_msg, _ = oef_search_dialogues.create( + counterparty=self.context.search_service_address, performative=OefSearchMessage.Performative.REGISTER_SERVICE, - dialogue_reference=oef_search_dialogues.new_self_initiated_dialogue_reference(), service_description=description, ) - oef_search_msg.counterparty = self.context.search_service_address - oef_search_dialogues.update(oef_search_msg) self.context.outbox.put_message(message=oef_search_msg) self.context.logger.info("registering service on SOEF.") @@ -212,13 +205,11 @@ class GenericServiceRegistrationBehaviour(TickerBehaviour): oef_search_dialogues = cast( OefSearchDialogues, self.context.oef_search_dialogues ) - oef_search_msg = OefSearchMessage( + oef_search_msg, _ = oef_search_dialogues.create( + counterparty=self.context.search_service_address, performative=OefSearchMessage.Performative.UNREGISTER_SERVICE, - dialogue_reference=oef_search_dialogues.new_self_initiated_dialogue_reference(), service_description=description, ) - oef_search_msg.counterparty = self.context.search_service_address - oef_search_dialogues.update(oef_search_msg) self.context.outbox.put_message(message=oef_search_msg) self.context.logger.info("unregistering service from SOEF.") @@ -233,13 +224,11 @@ class GenericServiceRegistrationBehaviour(TickerBehaviour): oef_search_dialogues = cast( OefSearchDialogues, self.context.oef_search_dialogues ) - oef_search_msg = OefSearchMessage( + oef_search_msg, _ = oef_search_dialogues.create( + counterparty=self.context.search_service_address, performative=OefSearchMessage.Performative.UNREGISTER_SERVICE, - dialogue_reference=oef_search_dialogues.new_self_initiated_dialogue_reference(), service_description=description, ) - oef_search_msg.counterparty = self.context.search_service_address - oef_search_dialogues.update(oef_search_msg) self.context.outbox.put_message(message=oef_search_msg) self.context.logger.info("unregistering agent from SOEF.") ``` @@ -324,7 +313,7 @@ from packages.fetchai.skills.generic_seller.dialogues import ( ) from packages.fetchai.skills.generic_seller.strategy import GenericStrategy -LEDGER_API_ADDRESS = "fetchai/ledger:0.3.0" +LEDGER_API_ADDRESS = "fetchai/ledger:0.4.0" class GenericFipaHandler(Handler): @@ -389,15 +378,13 @@ Below the unused `teardown` function, we continue by adding the following code: "received invalid fipa message={}, unidentified dialogue.".format(fipa_msg) ) default_dialogues = cast(DefaultDialogues, self.context.default_dialogues) - default_msg = DefaultMessage( + default_msg, _ = default_dialogues.create( + counterparty=fipa_msg.sender, performative=DefaultMessage.Performative.ERROR, - dialogue_reference=default_dialogues.new_self_initiated_dialogue_reference(), error_code=DefaultMessage.ErrorCode.INVALID_DIALOGUE, error_msg="Invalid dialogue.", error_data={"fipa_message": fipa_msg.encode()}, ) - default_msg.counterparty = fipa_msg.counterparty - default_dialogues.update(default_msg) self.context.outbox.put_message(message=default_msg) ``` @@ -417,42 +404,33 @@ The next code block handles the `CFP` message, paste the code below the `_handle :return: None """ self.context.logger.info( - "received CFP from sender={}".format(fipa_msg.counterparty[-5:]) + "received CFP from sender={}".format(fipa_msg.sender[-5:]) ) strategy = cast(GenericStrategy, self.context.strategy) if strategy.is_matching_supply(fipa_msg.query): proposal, terms, data_for_sale = strategy.generate_proposal_terms_and_data( - fipa_msg.query, fipa_msg.counterparty + fipa_msg.query, fipa_msg.sender ) fipa_dialogue.data_for_sale = data_for_sale fipa_dialogue.terms = terms self.context.logger.info( "sending a PROPOSE with proposal={} to sender={}".format( - proposal.values, fipa_msg.counterparty[-5:] + proposal.values, fipa_msg.sender[-5:] ) ) - proposal_msg = FipaMessage( + proposal_msg = fipa_dialogue.reply( performative=FipaMessage.Performative.PROPOSE, - message_id=fipa_msg.message_id + 1, - dialogue_reference=fipa_dialogue.dialogue_label.dialogue_reference, - target=fipa_msg.message_id, + target_message=fipa_msg, proposal=proposal, ) - proposal_msg.counterparty = fipa_msg.counterparty - fipa_dialogue.update(proposal_msg) self.context.outbox.put_message(message=proposal_msg) else: self.context.logger.info( - "declined the CFP from sender={}".format(fipa_msg.counterparty[-5:]) + "declined the CFP from sender={}".format(fipa_msg.sender[-5:]) ) - decline_msg = FipaMessage( - message_id=fipa_msg.message_id + 1, - dialogue_reference=fipa_dialogue.dialogue_label.dialogue_reference, - target=fipa_msg.message_id, - performative=FipaMessage.Performative.DECLINE, + decline_msg = fipa_dialogue.reply( + performative=FipaMessage.Performative.DECLINE, target_message=fipa_msg, ) - decline_msg.counterparty = fipa_msg.counterparty - fipa_dialogue.update(decline_msg) self.context.outbox.put_message(message=decline_msg) ``` @@ -477,7 +455,7 @@ The next code-block handles the decline message we receive from the buyer. Add :return: None """ self.context.logger.info( - "received DECLINE from sender={}".format(fipa_msg.counterparty[-5:]) + "received DECLINE from sender={}".format(fipa_msg.sender[-5:]) ) fipa_dialogues.dialogue_stats.add_dialogue_endstate( FipaDialogue.EndState.DECLINED_PROPOSE, fipa_dialogue.is_self_initiated @@ -501,22 +479,19 @@ Alternatively, we might receive an `ACCEPT` message. In order to handle this opt :return: None """ self.context.logger.info( - "received ACCEPT from sender={}".format(fipa_msg.counterparty[-5:]) + "received ACCEPT from sender={}".format(fipa_msg.sender[-5:]) ) - match_accept_msg = FipaMessage( + info = {"address": fipa_dialogue.terms.sender_address} + match_accept_msg = fipa_dialogue.reply( performative=FipaMessage.Performative.MATCH_ACCEPT_W_INFORM, - message_id=fipa_msg.message_id + 1, - dialogue_reference=fipa_dialogue.dialogue_label.dialogue_reference, - target=fipa_msg.message_id, - info={"address": fipa_dialogue.terms.sender_address}, + target_message=fipa_msg, + info=info, ) self.context.logger.info( "sending MATCH_ACCEPT_W_INFORM to sender={} with info={}".format( - fipa_msg.counterparty[-5:], match_accept_msg.info, + fipa_msg.sender[-5:], info, ) ) - match_accept_msg.counterparty = fipa_msg.counterparty - fipa_dialogue.update(match_accept_msg) self.context.outbox.put_message(message=match_accept_msg) ``` When the `my_generic_buyer` accepts the `Proposal` we send it, and therefores sends us an `ACCEPT` message, we have to respond with another message (`MATCH_ACCEPT_W_INFORM` ) to inform the buyer about the address we would like it to send the funds to. @@ -537,10 +512,8 @@ Lastly, we handle the `INFORM` message, which the buyer uses to inform us that i :param fipa_dialogue: the dialogue object :return: None """ - new_message_id = fipa_msg.message_id + 1 - new_target = fipa_msg.message_id self.context.logger.info( - "received INFORM from sender={}".format(fipa_msg.counterparty[-5:]) + "received INFORM from sender={}".format(fipa_msg.sender[-5:]) ) strategy = cast(GenericStrategy, self.context.strategy) @@ -553,38 +526,28 @@ Lastly, we handle the `INFORM` message, which the buyer uses to inform us that i ledger_api_dialogues = cast( LedgerApiDialogues, self.context.ledger_api_dialogues ) - ledger_api_msg = LedgerApiMessage( + ledger_api_msg, ledger_api_dialogue = ledger_api_dialogues.create( + counterparty=LEDGER_API_ADDRESS, performative=LedgerApiMessage.Performative.GET_TRANSACTION_RECEIPT, - dialogue_reference=ledger_api_dialogues.new_self_initiated_dialogue_reference(), transaction_digest=TransactionDigest( fipa_dialogue.terms.ledger_id, fipa_msg.info["transaction_digest"] ), ) - ledger_api_msg.counterparty = LEDGER_API_ADDRESS - ledger_api_dialogue = cast( - Optional[LedgerApiDialogue], ledger_api_dialogues.update(ledger_api_msg) - ) - assert ( - ledger_api_dialogue is not None - ), "LedgerApiDialogue construction failed." + ledger_api_dialogue = cast(LedgerApiDialogue, ledger_api_dialogue) ledger_api_dialogue.associated_fipa_dialogue = fipa_dialogue self.context.outbox.put_message(message=ledger_api_msg) elif strategy.is_ledger_tx: self.context.logger.warning( "did not receive transaction digest from sender={}.".format( - fipa_msg.counterparty[-5:] + fipa_msg.sender[-5:] ) ) elif not strategy.is_ledger_tx and "Done" in fipa_msg.info.keys(): - inform_msg = FipaMessage( - message_id=new_message_id, - dialogue_reference=fipa_dialogue.dialogue_label.dialogue_reference, - target=new_target, + inform_msg = fipa_dialogue.reply( performative=FipaMessage.Performative.INFORM, + target_message=fipa_msg, info=fipa_dialogue.data_for_sale, ) - inform_msg.counterparty = fipa_msg.counterparty - fipa_dialogue.update(inform_msg) self.context.outbox.put_message(message=inform_msg) fipa_dialogues = cast(FipaDialogues, self.context.fipa_dialogues) fipa_dialogues.dialogue_stats.add_dialogue_endstate( @@ -592,13 +555,13 @@ Lastly, we handle the `INFORM` message, which the buyer uses to inform us that i ) self.context.logger.info( "transaction confirmed, sending data={} to buyer={}.".format( - fipa_dialogue.data_for_sale, fipa_msg.counterparty[-5:], + fipa_dialogue.data_for_sale, fipa_msg.sender[-5:], ) ) else: self.context.logger.warning( "did not receive transaction confirmation from sender={}.".format( - fipa_msg.counterparty[-5:] + fipa_msg.sender[-5:] ) ) ``` @@ -654,7 +617,7 @@ class GenericLedgerApiHandler(Handler): # handle message if ledger_api_msg.performative is LedgerApiMessage.Performative.BALANCE: - self._handle_balance(ledger_api_msg, ledger_api_dialogue) + self._handle_balance(ledger_api_msg) elif ( ledger_api_msg.performative is LedgerApiMessage.Performative.TRANSACTION_RECEIPT @@ -685,14 +648,11 @@ class GenericLedgerApiHandler(Handler): ) ) - def _handle_balance( - self, ledger_api_msg: LedgerApiMessage, ledger_api_dialogue: LedgerApiDialogue - ) -> None: + def _handle_balance(self, ledger_api_msg: LedgerApiMessage) -> None: """ Handle a message of balance performative. :param ledger_api_message: the ledger api message - :param ledger_api_dialogue: the ledger api dialogue """ self.context.logger.info( "starting balance on {} ledger={}.".format( @@ -725,16 +685,13 @@ class GenericLedgerApiHandler(Handler): last_message = cast( Optional[FipaMessage], fipa_dialogue.last_incoming_message ) - assert last_message is not None, "Cannot retrieve last fipa message." - inform_msg = FipaMessage( - message_id=last_message.message_id + 1, - dialogue_reference=fipa_dialogue.dialogue_label.dialogue_reference, - target=last_message.message_id, + if last_message is None: + raise ValueError("Cannot retrieve last fipa message.") + inform_msg = fipa_dialogue.reply( performative=FipaMessage.Performative.INFORM, + target_message=last_message, info=fipa_dialogue.data_for_sale, ) - inform_msg.counterparty = last_message.counterparty - fipa_dialogue.update(inform_msg) self.context.outbox.put_message(message=inform_msg) fipa_dialogues = cast(FipaDialogues, self.context.fipa_dialogues) fipa_dialogues.dialogue_stats.add_dialogue_endstate( @@ -742,7 +699,7 @@ class GenericLedgerApiHandler(Handler): ) self.context.logger.info( "transaction confirmed, sending data={} to buyer={}.".format( - fipa_dialogue.data_for_sale, last_message.counterparty[-5:], + fipa_dialogue.data_for_sale, last_message.sender[-5:], ) ) else: @@ -881,8 +838,10 @@ Next, we are going to create the strategy that we want our `my_generic_seller` A import uuid from typing import Any, Dict, Optional, Tuple +from aea.common import Address from aea.configurations.constants import DEFAULT_LEDGER from aea.crypto.ledger_apis import LedgerApis +from aea.exceptions import enforce from aea.helpers.search.generic import ( AGENT_LOCATION_MODEL, AGENT_REMOVE_SERVICE_MODEL, @@ -891,7 +850,6 @@ from aea.helpers.search.generic import ( ) from aea.helpers.search.models import Description, Location, Query from aea.helpers.transaction.base import Terms -from aea.mail.base import Address from aea.skills.base import Model DEFAULT_LEDGER_ID = DEFAULT_LEDGER @@ -936,11 +894,12 @@ class GenericStrategy(Model): ) } self._set_service_data = kwargs.pop("service_data", DEFAULT_SERVICE_DATA) - assert ( + enforce( len(self._set_service_data) == 2 and "key" in self._set_service_data - and "value" in self._set_service_data - ), "service_data must contain keys `key` and `value`" + and "value" in self._set_service_data, + "service_data must contain keys `key` and `value`", + ) self._remove_service_data = {"key": self._set_service_data["key"]} self._simple_service_data = { self._set_service_data["key"]: self._set_service_data["value"] @@ -953,9 +912,10 @@ class GenericStrategy(Model): } super().__init__(**kwargs) - assert ( - self.context.agent_addresses.get(self._ledger_id, None) is not None - ), "Wallet does not contain cryptos for provided ledger id." + enforce( + self.context.agent_addresses.get(self._ledger_id, None) is not None, + "Wallet does not contain cryptos for provided ledger id.", + ) if self._has_data_source: self._data_for_sale = self.collect_from_data_source() @@ -1032,7 +992,7 @@ The following properties and methods deal with different aspects of the strategy """ return query.check(self.get_service_description()) - def generate_proposal_terms_and_data( + def generate_proposal_terms_and_data( # pylint: disable=unused-argument self, query: Query, counterparty_address: Address ) -> Tuple[Description, Terms, Dict[str, str]]: """ @@ -1086,25 +1046,28 @@ Before the creation of the actual proposal, we have to check if the sale generat When we are negotiating with other AEAs we would like to keep track of the state of these negotiations. To this end we create a new file in the skill folder (`my_generic_seller/skills/generic_seller/`) and name it `dialogues.py`. Inside this file add the following code: ``` python -from typing import Dict, Optional +from typing import Dict, Optional, Type -from aea.helpers.dialogue.base import Dialogue as BaseDialogue -from aea.helpers.dialogue.base import DialogueLabel as BaseDialogueLabel +from aea.common import Address +from aea.exceptions import AEAEnforceError, enforce from aea.helpers.transaction.base import Terms -from aea.mail.base import Address from aea.protocols.base import Message from aea.protocols.default.dialogues import DefaultDialogue as BaseDefaultDialogue from aea.protocols.default.dialogues import DefaultDialogues as BaseDefaultDialogues +from aea.protocols.dialogue.base import Dialogue as BaseDialogue +from aea.protocols.dialogue.base import DialogueLabel as BaseDialogueLabel from aea.skills.base import Model from packages.fetchai.protocols.fipa.dialogues import FipaDialogue as BaseFipaDialogue from packages.fetchai.protocols.fipa.dialogues import FipaDialogues as BaseFipaDialogues +from packages.fetchai.protocols.fipa.message import FipaMessage from packages.fetchai.protocols.ledger_api.dialogues import ( LedgerApiDialogue as BaseLedgerApiDialogue, ) from packages.fetchai.protocols.ledger_api.dialogues import ( LedgerApiDialogues as BaseLedgerApiDialogues, ) +from packages.fetchai.protocols.ledger_api.message import LedgerApiMessage from packages.fetchai.protocols.oef_search.dialogues import ( OefSearchDialogue as BaseOefSearchDialogue, ) @@ -1125,32 +1088,23 @@ class DefaultDialogues(Model, BaseDefaultDialogues): :return: None """ Model.__init__(self, **kwargs) - BaseDefaultDialogues.__init__(self, self.context.agent_address) - @staticmethod - def role_from_first_message(message: Message) -> BaseDialogue.Role: - """Infer the role of the agent from an incoming/outgoing first message + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> BaseDialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message - :param message: an incoming/outgoing first message - :return: The role of the agent - """ - return DefaultDialogue.Role.AGENT - - def create_dialogue( - self, dialogue_label: BaseDialogueLabel, role: BaseDialogue.Role, - ) -> DefaultDialogue: - """ - Create an instance of fipa dialogue. + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return DefaultDialogue.Role.AGENT - :param dialogue_label: the identifier of the dialogue - :param role: the role of the agent this dialogue is maintained for - - :return: the created dialogue - """ - dialogue = DefaultDialogue( - dialogue_label=dialogue_label, agent_address=self.agent_address, role=role + BaseDefaultDialogues.__init__( + self, + self_address=self.context.agent_address, + role_from_first_message=role_from_first_message, ) - return dialogue class FipaDialogue(BaseFipaDialogue): @@ -1159,20 +1113,25 @@ class FipaDialogue(BaseFipaDialogue): def __init__( self, dialogue_label: BaseDialogueLabel, - agent_address: Address, + self_address: Address, role: BaseDialogue.Role, + message_class: Type[FipaMessage] = FipaMessage, ) -> None: """ Initialize a dialogue. :param dialogue_label: the identifier of the dialogue - :param agent_address: the address of the agent for whom this dialogue is maintained + :param self_address: the address of the entity for whom this dialogue is maintained :param role: the role of the agent this dialogue is maintained for :return: None """ BaseFipaDialogue.__init__( - self, dialogue_label=dialogue_label, agent_address=agent_address, role=role + self, + dialogue_label=dialogue_label, + self_address=self_address, + role=role, + message_class=message_class, ) self.data_for_sale = None # type: Optional[Dict[str, str]] self._terms = None # type: Optional[Terms] @@ -1180,13 +1139,14 @@ class FipaDialogue(BaseFipaDialogue): @property def terms(self) -> Terms: """Get terms.""" - assert self._terms is not None, "Terms not set!" + if self._terms is None: + raise AEAEnforceError("Terms not set!") return self._terms @terms.setter def terms(self, terms: Terms) -> None: """Set terms.""" - assert self._terms is None, "Terms already set!" + enforce(self._terms is None, "Terms already set!") self._terms = terms @@ -1200,33 +1160,24 @@ class FipaDialogues(Model, BaseFipaDialogues): :return: None """ Model.__init__(self, **kwargs) - BaseFipaDialogues.__init__(self, self.context.agent_address) - @staticmethod - def role_from_first_message(message: Message) -> BaseDialogue.Role: - """ - Infer the role of the agent from an incoming or outgoing first message + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> BaseDialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message - :param message: an incoming/outgoing first message - :return: the agent's role - """ - return FipaDialogue.Role.SELLER - - def create_dialogue( - self, dialogue_label: BaseDialogueLabel, role: BaseDialogue.Role, - ) -> FipaDialogue: - """ - Create an instance of dialogue. - - :param dialogue_label: the identifier of the dialogue - :param role: the role of the agent this dialogue is maintained for + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return FipaDialogue.Role.SELLER - :return: the created dialogue - """ - dialogue = FipaDialogue( - dialogue_label=dialogue_label, agent_address=self.agent_address, role=role + BaseFipaDialogues.__init__( + self, + self_address=self.context.agent_address, + role_from_first_message=role_from_first_message, + dialogue_class=FipaDialogue, ) - return dialogue class LedgerApiDialogue(BaseLedgerApiDialogue): @@ -1235,33 +1186,39 @@ class LedgerApiDialogue(BaseLedgerApiDialogue): def __init__( self, dialogue_label: BaseDialogueLabel, - agent_address: Address, + self_address: Address, role: BaseDialogue.Role, + message_class: Type[LedgerApiMessage] = LedgerApiMessage, ) -> None: """ Initialize a dialogue. :param dialogue_label: the identifier of the dialogue - :param agent_address: the address of the agent for whom this dialogue is maintained + :param self_address: the address of the entity for whom this dialogue is maintained :param role: the role of the agent this dialogue is maintained for :return: None """ BaseLedgerApiDialogue.__init__( - self, dialogue_label=dialogue_label, agent_address=agent_address, role=role + self, + dialogue_label=dialogue_label, + self_address=self_address, + role=role, + message_class=message_class, ) self._associated_fipa_dialogue = None # type: Optional[FipaDialogue] @property def associated_fipa_dialogue(self) -> FipaDialogue: """Get associated_fipa_dialogue.""" - assert self._associated_fipa_dialogue is not None, "FipaDialogue not set!" + if self._associated_fipa_dialogue is None: + raise AEAEnforceError("FipaDialogue not set!") return self._associated_fipa_dialogue @associated_fipa_dialogue.setter def associated_fipa_dialogue(self, fipa_dialogue: FipaDialogue) -> None: """Set associated_fipa_dialogue""" - assert self._associated_fipa_dialogue is None, "FipaDialogue already set!" + enforce(self._associated_fipa_dialogue is None, "FipaDialogue already set!") self._associated_fipa_dialogue = fipa_dialogue @@ -1275,32 +1232,24 @@ class LedgerApiDialogues(Model, BaseLedgerApiDialogues): :return: None """ Model.__init__(self, **kwargs) - BaseLedgerApiDialogues.__init__(self, self.context.agent_address) - - @staticmethod - def role_from_first_message(message: Message) -> BaseDialogue.Role: - """Infer the role of the agent from an incoming/outgoing first message - - :param message: an incoming/outgoing first message - :return: The role of the agent - """ - return BaseLedgerApiDialogue.Role.AGENT - def create_dialogue( - self, dialogue_label: BaseDialogueLabel, role: BaseDialogue.Role, - ) -> LedgerApiDialogue: - """ - Create an instance of fipa dialogue. + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> BaseDialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message - :param dialogue_label: the identifier of the dialogue - :param role: the role of the agent this dialogue is maintained for + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return BaseLedgerApiDialogue.Role.AGENT - :return: the created dialogue - """ - dialogue = LedgerApiDialogue( - dialogue_label=dialogue_label, agent_address=self.agent_address, role=role + BaseLedgerApiDialogues.__init__( + self, + self_address=self.context.agent_address, + role_from_first_message=role_from_first_message, + dialogue_class=LedgerApiDialogue, ) - return dialogue OefSearchDialogue = BaseOefSearchDialogue @@ -1317,32 +1266,23 @@ class OefSearchDialogues(Model, BaseOefSearchDialogues): :return: None """ Model.__init__(self, **kwargs) - BaseOefSearchDialogues.__init__(self, self.context.agent_address) - - @staticmethod - def role_from_first_message(message: Message) -> BaseDialogue.Role: - """Infer the role of the agent from an incoming/outgoing first message - :param message: an incoming/outgoing first message - :return: The role of the agent - """ - return BaseOefSearchDialogue.Role.AGENT + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> BaseDialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message - def create_dialogue( - self, dialogue_label: BaseDialogueLabel, role: BaseDialogue.Role, - ) -> OefSearchDialogue: - """ - Create an instance of fipa dialogue. + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return BaseOefSearchDialogue.Role.AGENT - :param dialogue_label: the identifier of the dialogue - :param role: the role of the agent this dialogue is maintained for - - :return: the created dialogue - """ - dialogue = OefSearchDialogue( - dialogue_label=dialogue_label, agent_address=self.agent_address, role=role + BaseOefSearchDialogues.__init__( + self, + self_address=self.context.agent_address, + role_from_first_message=role_from_first_message, ) - return dialogue ``` The `FipaDialogues` class stores dialogue with each `my_generic_buyer` (and other AEAs) and exposes a number of helpful methods to manage them. This helps us match messages to a dialogue, access previous messages and enable us to identify possible communications problems between the `my_generic_seller` AEA and the `my_generic_buyer` AEA. It also keeps track of the data that we offer for sale during the proposal phase. @@ -1357,10 +1297,11 @@ Since we made so many changes to our AEA we have to update the `skill.yaml` (at name: generic_seller author: fetchai version: 0.1.0 +type: skill description: The weather station skill implements the functionality to sell weather data. license: Apache-2.0 -aea_version: '>=0.5.0, <0.6.0' +aea_version: '>=0.6.0, <0.7.0' fingerprint: __init__.py: QmNkZAetyctaZCUf6ACxP5onGWsSxu2hjSNoFmJ3ta6Lta behaviours.py: QmcFahpL4DZ1rsTNEK1BT3e5T8TEJJg2hP4ytkzdqKuJnZ @@ -1370,10 +1311,10 @@ fingerprint: fingerprint_ignore_patterns: [] contracts: [] protocols: -- fetchai/default:0.4.0 -- fetchai/fipa:0.5.0 -- fetchai/ledger_api:0.2.0 -- fetchai/oef_search:0.4.0 +- fetchai/default:0.5.0 +- fetchai/fipa:0.6.0 +- fetchai/ledger_api:0.3.0 +- fetchai/oef_search:0.5.0 skills: [] behaviours: service_registration: @@ -1410,7 +1351,7 @@ models: generic: data has_data_source: false is_ledger_tx: true - ledger_id: cosmos + ledger_id: fetchai location: latitude: 0.127 longitude: 51.5194 @@ -1479,7 +1420,7 @@ from packages.fetchai.skills.generic_buyer.dialogues import ( from packages.fetchai.skills.generic_buyer.strategy import GenericStrategy DEFAULT_SEARCH_INTERVAL = 5.0 -LEDGER_API_ADDRESS = "fetchai/ledger:0.3.0" +LEDGER_API_ADDRESS = "fetchai/ledger:0.4.0" class GenericSearchBehaviour(TickerBehaviour): @@ -1499,14 +1440,12 @@ class GenericSearchBehaviour(TickerBehaviour): ledger_api_dialogues = cast( LedgerApiDialogues, self.context.ledger_api_dialogues ) - ledger_api_msg = LedgerApiMessage( + ledger_api_msg, _ = ledger_api_dialogues.create( + counterparty=LEDGER_API_ADDRESS, performative=LedgerApiMessage.Performative.GET_BALANCE, - dialogue_reference=ledger_api_dialogues.new_self_initiated_dialogue_reference(), ledger_id=strategy.ledger_id, address=cast(str, self.context.agent_addresses.get(strategy.ledger_id)), ) - ledger_api_msg.counterparty = LEDGER_API_ADDRESS - ledger_api_dialogues.update(ledger_api_msg) self.context.outbox.put_message(message=ledger_api_msg) else: strategy.is_searching = True @@ -1523,13 +1462,11 @@ class GenericSearchBehaviour(TickerBehaviour): oef_search_dialogues = cast( OefSearchDialogues, self.context.oef_search_dialogues ) - oef_search_msg = OefSearchMessage( + oef_search_msg, _ = oef_search_dialogues.create( + counterparty=self.context.search_service_address, performative=OefSearchMessage.Performative.SEARCH_SERVICES, - dialogue_reference=oef_search_dialogues.new_self_initiated_dialogue_reference(), query=query, ) - oef_search_msg.counterparty = self.context.search_service_address - oef_search_dialogues.update(oef_search_msg) self.context.outbox.put_message(message=oef_search_msg) def teardown(self) -> None: @@ -1575,7 +1512,7 @@ from packages.fetchai.skills.generic_buyer.dialogues import ( ) from packages.fetchai.skills.generic_buyer.strategy import GenericStrategy -LEDGER_API_ADDRESS = "fetchai/ledger:0.3.0" +LEDGER_API_ADDRESS = "fetchai/ledger:0.4.0" class GenericFipaHandler(Handler): @@ -1640,15 +1577,13 @@ You will see that we are following similar logic to the `generic_seller` when we "received invalid fipa message={}, unidentified dialogue.".format(fipa_msg) ) default_dialogues = cast(DefaultDialogues, self.context.default_dialogues) - default_msg = DefaultMessage( + default_msg, _ = default_dialogues.create( + counterparty=fipa_msg.sender, performative=DefaultMessage.Performative.ERROR, - dialogue_reference=default_dialogues.new_self_initiated_dialogue_reference(), error_code=DefaultMessage.ErrorCode.INVALID_DIALOGUE, error_msg="Invalid dialogue.", error_data={"fipa_message": fipa_msg.encode()}, ) - default_msg.counterparty = fipa_msg.counterparty - default_dialogues.update(default_msg) self.context.outbox.put_message(message=default_msg) ``` The above code handles the unidentified dialogues. And responds with an error message to the sender. Next we will handle the `PROPOSE` message that we receive from the `my_generic_seller` AEA: @@ -1666,7 +1601,7 @@ The above code handles the unidentified dialogues. And responds with an error me """ self.context.logger.info( "received proposal={} from sender={}".format( - fipa_msg.proposal.values, fipa_msg.counterparty[-5:], + fipa_msg.proposal.values, fipa_msg.sender[-5:], ) ) strategy = cast(GenericStrategy, self.context.strategy) @@ -1674,37 +1609,21 @@ The above code handles the unidentified dialogues. And responds with an error me affordable = strategy.is_affordable_proposal(fipa_msg.proposal) if acceptable and affordable: self.context.logger.info( - "accepting the proposal from sender={}".format( - fipa_msg.counterparty[-5:] - ) - ) - terms = strategy.terms_from_proposal( - fipa_msg.proposal, fipa_msg.counterparty + "accepting the proposal from sender={}".format(fipa_msg.sender[-5:]) ) + terms = strategy.terms_from_proposal(fipa_msg.proposal, fipa_msg.sender) fipa_dialogue.terms = terms - accept_msg = FipaMessage( - message_id=fipa_msg.message_id + 1, - dialogue_reference=fipa_dialogue.dialogue_label.dialogue_reference, - target=fipa_msg.message_id, - performative=FipaMessage.Performative.ACCEPT, - ) - accept_msg.counterparty = fipa_msg.counterparty - fipa_dialogue.update(accept_msg) + accept_msg = fipa_dialogue.reply( + performative=FipaMessage.Performative.ACCEPT, target_message=fipa_msg, + ) self.context.outbox.put_message(message=accept_msg) else: self.context.logger.info( - "declining the proposal from sender={}".format( - fipa_msg.counterparty[-5:] - ) + "declining the proposal from sender={}".format(fipa_msg.sender[-5:]) ) - decline_msg = FipaMessage( - message_id=fipa_msg.message_id + 1, - dialogue_reference=fipa_dialogue.dialogue_label.dialogue_reference, - target=fipa_msg.message_id, - performative=FipaMessage.Performative.DECLINE, + decline_msg = fipa_dialogue.reply( + performative=FipaMessage.Performative.DECLINE, target_message=fipa_msg, ) - decline_msg.counterparty = fipa_msg.counterparty - fipa_dialogue.update(decline_msg) self.context.outbox.put_message(message=decline_msg) ``` When we receive a proposal we have to check if we have the funds to complete the transaction and if the proposal is acceptable based on our strategy. If the proposal is not affordable or acceptable we respond with a `DECLINE` message. Otherwise, we send an `ACCEPT` message to the seller. @@ -1727,7 +1646,7 @@ The next code-block handles the `DECLINE` message that we may receive from the b :return: None """ self.context.logger.info( - "received DECLINE from sender={}".format(fipa_msg.counterparty[-5:]) + "received DECLINE from sender={}".format(fipa_msg.sender[-5:]) ) if fipa_msg.target == 1: fipa_dialogues.dialogue_stats.add_dialogue_endstate( @@ -1755,7 +1674,7 @@ In case we do not receive any `DECLINE` message that means that the `my_generic_ """ self.context.logger.info( "received MATCH_ACCEPT_W_INFORM from sender={} with info={}".format( - fipa_msg.counterparty[-5:], fipa_msg.info + fipa_msg.sender[-5:], fipa_msg.info ) ) strategy = cast(GenericStrategy, self.context.strategy) @@ -1766,18 +1685,12 @@ In case we do not receive any `DECLINE` message that means that the `my_generic_ ledger_api_dialogues = cast( LedgerApiDialogues, self.context.ledger_api_dialogues ) - ledger_api_msg = LedgerApiMessage( + ledger_api_msg, ledger_api_dialogue = ledger_api_dialogues.create( + counterparty=LEDGER_API_ADDRESS, performative=LedgerApiMessage.Performative.GET_RAW_TRANSACTION, - dialogue_reference=ledger_api_dialogues.new_self_initiated_dialogue_reference(), terms=fipa_dialogue.terms, ) - ledger_api_msg.counterparty = LEDGER_API_ADDRESS - ledger_api_dialogue = cast( - Optional[LedgerApiDialogue], ledger_api_dialogues.update(ledger_api_msg) - ) - assert ( - ledger_api_dialogue is not None - ), "Error when creating ledger api dialogue." + ledger_api_dialogue = cast(LedgerApiDialogue, ledger_api_dialogue) ledger_api_dialogue.associated_fipa_dialogue = fipa_dialogue fipa_dialogue.associated_ledger_api_dialogue = ledger_api_dialogue self.context.outbox.put_message(message=ledger_api_msg) @@ -1785,20 +1698,14 @@ In case we do not receive any `DECLINE` message that means that the `my_generic_ "requesting transfer transaction from ledger api..." ) else: - inform_msg = FipaMessage( - message_id=fipa_msg.message_id + 1, - dialogue_reference=fipa_dialogue.dialogue_label.dialogue_reference, - target=fipa_msg.message_id, + inform_msg = fipa_dialogue.reply( performative=FipaMessage.Performative.INFORM, + target_message=fipa_msg, info={"Done": "Sending payment via bank transfer"}, ) - inform_msg.counterparty = fipa_msg.counterparty - fipa_dialogue.update(inform_msg) self.context.outbox.put_message(message=inform_msg) self.context.logger.info( - "informing counterparty={} of payment.".format( - fipa_msg.counterparty[-5:] - ) + "informing counterparty={} of payment.".format(fipa_msg.sender[-5:]) ) ``` The first thing we are checking is if we enabled our AEA to transact with a ledger. If we can transact with a ledger we generate a `LedgerApiMessage` of performative `GET_RAW_TRANSACTION` and send it to the ledger connection. The ledger connection will construct a raw transaction for us, using the relevant ledger api. @@ -1821,7 +1728,7 @@ Lastly, we need to handle the `INFORM` message. This is the message that will ha :return: None """ self.context.logger.info( - "received INFORM from sender={}".format(fipa_msg.counterparty[-5:]) + "received INFORM from sender={}".format(fipa_msg.sender[-5:]) ) if len(fipa_msg.info.keys()) >= 1: data = fipa_msg.info @@ -1833,7 +1740,7 @@ Lastly, we need to handle the `INFORM` message. This is the message that will ha ) else: self.context.logger.info( - "received no data from sender={}".format(fipa_msg.counterparty[-5:]) + "received no data from sender={}".format(fipa_msg.sender[-5:]) ) def _handle_invalid( @@ -1942,7 +1849,9 @@ class GenericOefSearchHandler(Handler): :return: None """ if len(oef_search_msg.agents) == 0: - self.context.logger.info("found no agents, continue searching.") + self.context.logger.info( + f"found no agents in dialogue={oef_search_dialogue}, continue searching." + ) return self.context.logger.info( @@ -1957,13 +1866,11 @@ class GenericOefSearchHandler(Handler): for idx, counterparty in enumerate(oef_search_msg.agents): if idx >= strategy.max_negotiations: continue - cfp_msg = FipaMessage( + cfp_msg, _ = fipa_dialogues.create( + counterparty=counterparty, performative=FipaMessage.Performative.CFP, - dialogue_reference=fipa_dialogues.new_self_initiated_dialogue_reference(), query=query, ) - cfp_msg.counterparty = counterparty - fipa_dialogues.update(cfp_msg) self.context.outbox.put_message(message=cfp_msg) self.context.logger.info( "sending CFP to agent={}".format(counterparty[-5:]) @@ -2059,18 +1966,13 @@ class GenericSigningHandler(Handler): fipa_dialogue = signing_dialogue.associated_fipa_dialogue ledger_api_dialogue = fipa_dialogue.associated_ledger_api_dialogue last_ledger_api_msg = ledger_api_dialogue.last_incoming_message - assert ( - last_ledger_api_msg is not None - ), "Could not retrieve last message in ledger api dialogue" - ledger_api_msg = LedgerApiMessage( + if last_ledger_api_msg is None: + raise ValueError("Could not retrieve last message in ledger api dialogue") + ledger_api_msg = ledger_api_dialogue.reply( performative=LedgerApiMessage.Performative.SEND_SIGNED_TRANSACTION, - dialogue_reference=ledger_api_dialogue.dialogue_label.dialogue_reference, - target=last_ledger_api_msg.message_id, - message_id=last_ledger_api_msg.message_id + 1, + target_message=last_ledger_api_msg, signed_transaction=signing_msg.signed_transaction, ) - ledger_api_msg.counterparty = LEDGER_API_ADDRESS - ledger_api_dialogue.update(ledger_api_msg) self.context.outbox.put_message(message=ledger_api_msg) self.context.logger.info("sending transaction to ledger.") @@ -2138,7 +2040,7 @@ class GenericLedgerApiHandler(Handler): # handle message if ledger_api_msg.performative is LedgerApiMessage.Performative.BALANCE: - self._handle_balance(ledger_api_msg, ledger_api_dialogue) + self._handle_balance(ledger_api_msg) elif ( ledger_api_msg.performative is LedgerApiMessage.Performative.RAW_TRANSACTION ): @@ -2173,14 +2075,11 @@ class GenericLedgerApiHandler(Handler): ) ) - def _handle_balance( - self, ledger_api_msg: LedgerApiMessage, ledger_api_dialogue: LedgerApiDialogue - ) -> None: + def _handle_balance(self, ledger_api_msg: LedgerApiMessage) -> None: """ Handle a message of balance performative. :param ledger_api_message: the ledger api message - :param ledger_api_dialogue: the ledger api dialogue """ strategy = cast(GenericStrategy, self.context.strategy) if ledger_api_msg.balance > 0: @@ -2208,19 +2107,13 @@ class GenericLedgerApiHandler(Handler): """ self.context.logger.info("received raw transaction={}".format(ledger_api_msg)) signing_dialogues = cast(SigningDialogues, self.context.signing_dialogues) - signing_msg = SigningMessage( + signing_msg, signing_dialogue = signing_dialogues.create( + counterparty="decision_maker", performative=SigningMessage.Performative.SIGN_TRANSACTION, - dialogue_reference=signing_dialogues.new_self_initiated_dialogue_reference(), - skill_callback_ids=(str(self.context.skill_id),), raw_transaction=ledger_api_msg.raw_transaction, terms=ledger_api_dialogue.associated_fipa_dialogue.terms, - skill_callback_info={}, - ) - signing_msg.counterparty = "decision_maker" - signing_dialogue = cast( - Optional[SigningDialogue], signing_dialogues.update(signing_msg) ) - assert signing_dialogue is not None, "Error when creating signing dialogue" + signing_dialogue = cast(SigningDialogue, signing_dialogue) signing_dialogue.associated_fipa_dialogue = ( ledger_api_dialogue.associated_fipa_dialogue ) @@ -2245,16 +2138,13 @@ class GenericLedgerApiHandler(Handler): ) ) fipa_msg = cast(Optional[FipaMessage], fipa_dialogue.last_incoming_message) - assert fipa_msg is not None, "Could not retrieve fipa message" - inform_msg = FipaMessage( + if fipa_msg is None: + raise ValueError("Could not retrieve fipa message") + inform_msg = fipa_dialogue.reply( performative=FipaMessage.Performative.INFORM, - message_id=fipa_msg.message_id + 1, - dialogue_reference=fipa_dialogue.dialogue_label.dialogue_reference, - target=fipa_msg.message_id, + target_message=fipa_msg, info={"transaction_digest": ledger_api_msg.transaction_digest.body}, ) - inform_msg.counterparty = fipa_dialogue.dialogue_label.dialogue_opponent_addr - fipa_dialogue.update(inform_msg) self.context.outbox.put_message(message=inform_msg) self.context.logger.info( "informing counterparty={} of transaction digest.".format( @@ -2298,7 +2188,9 @@ class GenericLedgerApiHandler(Handler): We are going to create the strategy that we want our AEA to follow. Rename the `my_model.py` file (in `my_generic_buyer/skills/generic_buyer/`) to `strategy.py` and paste the following code (replacing the stub code already present in the file): ``` python +from aea.common import Address from aea.configurations.constants import DEFAULT_LEDGER +from aea.exceptions import enforce from aea.helpers.search.generic import SIMPLE_SERVICE_MODEL from aea.helpers.search.models import ( Constraint, @@ -2308,7 +2200,6 @@ from aea.helpers.search.models import ( Query, ) from aea.helpers.transaction.base import Terms -from aea.mail.base import Address from aea.skills.base import Model DEFAULT_LEDGER_ID = DEFAULT_LEDGER @@ -2384,7 +2275,7 @@ We initialize the strategy class by trying to read the strategy variables from t @is_searching.setter def is_searching(self, is_searching: bool) -> None: """Check if the agent is searching.""" - assert isinstance(is_searching, bool), "Can only set bool on is_searching!" + enforce(isinstance(is_searching, bool), "Can only set bool on is_searching!") self._is_searching = is_searching @property @@ -2520,28 +2411,32 @@ The `is_affordable_proposal` method checks if we can afford the transaction base As mentioned, when we are negotiating with other AEA we would like to keep track of these negotiations for various reasons. Create a new file and name it `dialogues.py` (in `my_generic_buyer/skills/generic_buyer/`). Inside this file add the following code: ``` python -from typing import Optional +from typing import Optional, Type -from aea.helpers.dialogue.base import Dialogue as BaseDialogue -from aea.helpers.dialogue.base import DialogueLabel as BaseDialogueLabel +from aea.common import Address +from aea.exceptions import AEAEnforceError, enforce from aea.helpers.transaction.base import Terms -from aea.mail.base import Address from aea.protocols.base import Message from aea.protocols.default.dialogues import DefaultDialogue as BaseDefaultDialogue from aea.protocols.default.dialogues import DefaultDialogues as BaseDefaultDialogues +from aea.protocols.dialogue.base import Dialogue as BaseDialogue +from aea.protocols.dialogue.base import DialogueLabel as BaseDialogueLabel from aea.protocols.signing.dialogues import SigningDialogue as BaseSigningDialogue from aea.protocols.signing.dialogues import SigningDialogues as BaseSigningDialogues +from aea.protocols.signing.message import SigningMessage from aea.skills.base import Model from packages.fetchai.protocols.fipa.dialogues import FipaDialogue as BaseFipaDialogue from packages.fetchai.protocols.fipa.dialogues import FipaDialogues as BaseFipaDialogues +from packages.fetchai.protocols.fipa.message import FipaMessage from packages.fetchai.protocols.ledger_api.dialogues import ( LedgerApiDialogue as BaseLedgerApiDialogue, ) from packages.fetchai.protocols.ledger_api.dialogues import ( LedgerApiDialogues as BaseLedgerApiDialogues, ) +from packages.fetchai.protocols.ledger_api.message import LedgerApiMessage from packages.fetchai.protocols.oef_search.dialogues import ( OefSearchDialogue as BaseOefSearchDialogue, ) @@ -2562,32 +2457,23 @@ class DefaultDialogues(Model, BaseDefaultDialogues): :return: None """ Model.__init__(self, **kwargs) - BaseDefaultDialogues.__init__(self, self.context.agent_address) - @staticmethod - def role_from_first_message(message: Message) -> BaseDialogue.Role: - """Infer the role of the agent from an incoming/outgoing first message + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> BaseDialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message - :param message: an incoming/outgoing first message - :return: The role of the agent - """ - return DefaultDialogue.Role.AGENT - - def create_dialogue( - self, dialogue_label: BaseDialogueLabel, role: BaseDialogue.Role, - ) -> DefaultDialogue: - """ - Create an instance of fipa dialogue. + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return DefaultDialogue.Role.AGENT - :param dialogue_label: the identifier of the dialogue - :param role: the role of the agent this dialogue is maintained for - - :return: the created dialogue - """ - dialogue = DefaultDialogue( - dialogue_label=dialogue_label, agent_address=self.agent_address, role=role + BaseDefaultDialogues.__init__( + self, + self_address=self.context.agent_address, + role_from_first_message=role_from_first_message, ) - return dialogue class FipaDialogue(BaseFipaDialogue): @@ -2596,20 +2482,25 @@ class FipaDialogue(BaseFipaDialogue): def __init__( self, dialogue_label: BaseDialogueLabel, - agent_address: Address, + self_address: Address, role: BaseDialogue.Role, + message_class: Type[FipaMessage] = FipaMessage, ) -> None: """ Initialize a dialogue. :param dialogue_label: the identifier of the dialogue - :param agent_address: the address of the agent for whom this dialogue is maintained + :param self_address: the address of the entity for whom this dialogue is maintained :param role: the role of the agent this dialogue is maintained for :return: None """ BaseFipaDialogue.__init__( - self, dialogue_label=dialogue_label, agent_address=agent_address, role=role + self, + dialogue_label=dialogue_label, + self_address=self_address, + role=role, + message_class=message_class, ) self._terms = None # type: Optional[Terms] self._associated_ledger_api_dialogue = None # type: Optional[LedgerApiDialogue] @@ -2617,21 +2508,21 @@ class FipaDialogue(BaseFipaDialogue): @property def terms(self) -> Terms: """Get terms.""" - assert self._terms is not None, "Terms not set!" + if self._terms is None: + raise AEAEnforceError("Terms not set!") return self._terms @terms.setter def terms(self, terms: Terms) -> None: """Set terms.""" - assert self._terms is None, "Terms already set!" + enforce(self._terms is None, "Terms already set!") self._terms = terms @property def associated_ledger_api_dialogue(self) -> "LedgerApiDialogue": """Get associated_ledger_api_dialogue.""" - assert ( - self._associated_ledger_api_dialogue is not None - ), "LedgerApiDialogue not set!" + if self._associated_ledger_api_dialogue is None: + raise AEAEnforceError("LedgerApiDialogue not set!") return self._associated_ledger_api_dialogue @associated_ledger_api_dialogue.setter @@ -2639,9 +2530,10 @@ class FipaDialogue(BaseFipaDialogue): self, ledger_api_dialogue: "LedgerApiDialogue" ) -> None: """Set associated_ledger_api_dialogue""" - assert ( - self._associated_ledger_api_dialogue is None - ), "LedgerApiDialogue already set!" + enforce( + self._associated_ledger_api_dialogue is None, + "LedgerApiDialogue already set!", + ) self._associated_ledger_api_dialogue = ledger_api_dialogue @@ -2655,32 +2547,24 @@ class FipaDialogues(Model, BaseFipaDialogues): :return: None """ Model.__init__(self, **kwargs) - BaseFipaDialogues.__init__(self, self.context.agent_address) - @staticmethod - def role_from_first_message(message: Message) -> BaseDialogue.Role: - """Infer the role of the agent from an incoming/outgoing first message + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> BaseDialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message - :param message: an incoming/outgoing first message - :return: The role of the agent - """ - return BaseFipaDialogue.Role.BUYER - - def create_dialogue( - self, dialogue_label: BaseDialogueLabel, role: BaseDialogue.Role, - ) -> FipaDialogue: - """ - Create an instance of fipa dialogue. - - :param dialogue_label: the identifier of the dialogue - :param role: the role of the agent this dialogue is maintained for + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return BaseFipaDialogue.Role.BUYER - :return: the created dialogue - """ - dialogue = FipaDialogue( - dialogue_label=dialogue_label, agent_address=self.agent_address, role=role + BaseFipaDialogues.__init__( + self, + self_address=self.context.agent_address, + role_from_first_message=role_from_first_message, + dialogue_class=FipaDialogue, ) - return dialogue class LedgerApiDialogue(BaseLedgerApiDialogue): @@ -2689,33 +2573,39 @@ class LedgerApiDialogue(BaseLedgerApiDialogue): def __init__( self, dialogue_label: BaseDialogueLabel, - agent_address: Address, + self_address: Address, role: BaseDialogue.Role, + message_class: Type[LedgerApiMessage] = LedgerApiMessage, ) -> None: """ Initialize a dialogue. :param dialogue_label: the identifier of the dialogue - :param agent_address: the address of the agent for whom this dialogue is maintained + :param self_address: the address of the entity for whom this dialogue is maintained :param role: the role of the agent this dialogue is maintained for :return: None """ BaseLedgerApiDialogue.__init__( - self, dialogue_label=dialogue_label, agent_address=agent_address, role=role + self, + dialogue_label=dialogue_label, + self_address=self_address, + role=role, + message_class=message_class, ) self._associated_fipa_dialogue = None # type: Optional[FipaDialogue] @property def associated_fipa_dialogue(self) -> FipaDialogue: """Get associated_fipa_dialogue.""" - assert self._associated_fipa_dialogue is not None, "FipaDialogue not set!" + if self._associated_fipa_dialogue is None: + raise AEAEnforceError("FipaDialogue not set!") return self._associated_fipa_dialogue @associated_fipa_dialogue.setter def associated_fipa_dialogue(self, fipa_dialogue: FipaDialogue) -> None: """Set associated_fipa_dialogue""" - assert self._associated_fipa_dialogue is None, "FipaDialogue already set!" + enforce(self._associated_fipa_dialogue is None, "FipaDialogue already set!") self._associated_fipa_dialogue = fipa_dialogue @@ -2729,32 +2619,24 @@ class LedgerApiDialogues(Model, BaseLedgerApiDialogues): :return: None """ Model.__init__(self, **kwargs) - BaseLedgerApiDialogues.__init__(self, self.context.agent_address) - - @staticmethod - def role_from_first_message(message: Message) -> BaseDialogue.Role: - """Infer the role of the agent from an incoming/outgoing first message - :param message: an incoming/outgoing first message - :return: The role of the agent - """ - return BaseLedgerApiDialogue.Role.AGENT + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> BaseDialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message - def create_dialogue( - self, dialogue_label: BaseDialogueLabel, role: BaseDialogue.Role, - ) -> LedgerApiDialogue: - """ - Create an instance of fipa dialogue. + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return BaseLedgerApiDialogue.Role.AGENT - :param dialogue_label: the identifier of the dialogue - :param role: the role of the agent this dialogue is maintained for - - :return: the created dialogue - """ - dialogue = LedgerApiDialogue( - dialogue_label=dialogue_label, agent_address=self.agent_address, role=role + BaseLedgerApiDialogues.__init__( + self, + self_address=self.context.agent_address, + role_from_first_message=role_from_first_message, + dialogue_class=LedgerApiDialogue, ) - return dialogue OefSearchDialogue = BaseOefSearchDialogue @@ -2771,32 +2653,23 @@ class OefSearchDialogues(Model, BaseOefSearchDialogues): :return: None """ Model.__init__(self, **kwargs) - BaseOefSearchDialogues.__init__(self, self.context.agent_address) - - @staticmethod - def role_from_first_message(message: Message) -> BaseDialogue.Role: - """Infer the role of the agent from an incoming/outgoing first message - - :param message: an incoming/outgoing first message - :return: The role of the agent - """ - return BaseOefSearchDialogue.Role.AGENT - def create_dialogue( - self, dialogue_label: BaseDialogueLabel, role: BaseDialogue.Role, - ) -> OefSearchDialogue: - """ - Create an instance of fipa dialogue. + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> BaseDialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message - :param dialogue_label: the identifier of the dialogue - :param role: the role of the agent this dialogue is maintained for + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return BaseOefSearchDialogue.Role.AGENT - :return: the created dialogue - """ - dialogue = OefSearchDialogue( - dialogue_label=dialogue_label, agent_address=self.agent_address, role=role + BaseOefSearchDialogues.__init__( + self, + self_address=self.context.agent_address, + role_from_first_message=role_from_first_message, ) - return dialogue class SigningDialogue(BaseSigningDialogue): @@ -2805,33 +2678,39 @@ class SigningDialogue(BaseSigningDialogue): def __init__( self, dialogue_label: BaseDialogueLabel, - agent_address: Address, + self_address: Address, role: BaseDialogue.Role, + message_class: Type[SigningMessage] = SigningMessage, ) -> None: """ Initialize a dialogue. :param dialogue_label: the identifier of the dialogue - :param agent_address: the address of the agent for whom this dialogue is maintained + :param self_address: the address of the entity for whom this dialogue is maintained :param role: the role of the agent this dialogue is maintained for :return: None """ BaseSigningDialogue.__init__( - self, dialogue_label=dialogue_label, agent_address=agent_address, role=role + self, + dialogue_label=dialogue_label, + self_address=self_address, + role=role, + message_class=message_class, ) self._associated_fipa_dialogue = None # type: Optional[FipaDialogue] @property def associated_fipa_dialogue(self) -> FipaDialogue: """Get associated_fipa_dialogue.""" - assert self._associated_fipa_dialogue is not None, "FipaDialogue not set!" + if self._associated_fipa_dialogue is None: + raise AEAEnforceError("FipaDialogue not set!") return self._associated_fipa_dialogue @associated_fipa_dialogue.setter def associated_fipa_dialogue(self, fipa_dialogue: FipaDialogue) -> None: """Set associated_fipa_dialogue""" - assert self._associated_fipa_dialogue is None, "FipaDialogue already set!" + enforce(self._associated_fipa_dialogue is None, "FipaDialogue already set!") self._associated_fipa_dialogue = fipa_dialogue @@ -2846,32 +2725,24 @@ class SigningDialogues(Model, BaseSigningDialogues): :return: None """ Model.__init__(self, **kwargs) - BaseSigningDialogues.__init__(self, self.context.agent_address) - @staticmethod - def role_from_first_message(message: Message) -> BaseDialogue.Role: - """Infer the role of the agent from an incoming/outgoing first message + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> BaseDialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message - :param message: an incoming/outgoing first message - :return: The role of the agent - """ - return BaseSigningDialogue.Role.SKILL + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return BaseSigningDialogue.Role.SKILL - def create_dialogue( - self, dialogue_label: BaseDialogueLabel, role: BaseDialogue.Role, - ) -> SigningDialogue: - """ - Create an instance of fipa dialogue. - - :param dialogue_label: the identifier of the dialogue - :param role: the role of the agent this dialogue is maintained for - - :return: the created dialogue - """ - dialogue = SigningDialogue( - dialogue_label=dialogue_label, agent_address=self.agent_address, role=role + BaseSigningDialogues.__init__( + self, + self_address=str(self.skill_id), + role_from_first_message=role_from_first_message, + dialogue_class=SigningDialogue, ) - return dialogue ``` The dialogues class stores dialogue with each AEA and other AEA components so we can have access to previous messages and enable us to identify possible communications problems between the `my_generic_seller` AEA and the `my_generic_buyer` AEA. @@ -2886,9 +2757,10 @@ First, we update the `skill.yaml`. Make sure that your `skill.yaml` matches with name: generic_buyer author: fetchai version: 0.1.0 +type: skill description: The weather client skill implements the skill to purchase weather data. license: Apache-2.0 -aea_version: '>=0.5.0, <0.6.0' +aea_version: '>=0.6.0, <0.7.0' fingerprint: __init__.py: QmNkZAetyctaZCUf6ACxP5onGWsSxu2hjSNoFmJ3ta6Lta behaviours.py: QmUBQvZkoCcik71vqRZGP4JJBgFP2kj8o7C24dfkAphitP @@ -2898,11 +2770,11 @@ fingerprint: fingerprint_ignore_patterns: [] contracts: [] protocols: -- fetchai/default:0.4.0 -- fetchai/fipa:0.5.0 -- fetchai/ledger_api:0.2.0 -- fetchai/oef_search:0.4.0 -- fetchai/signing:0.2.0 +- fetchai/default:0.5.0 +- fetchai/fipa:0.6.0 +- fetchai/ledger_api:0.3.0 +- fetchai/oef_search:0.5.0 +- fetchai/signing:0.3.0 skills: [] behaviours: search: @@ -2942,7 +2814,7 @@ models: args: currency_id: FET is_ledger_tx: true - ledger_id: cosmos + ledger_id: fetchai location: latitude: 0.127 longitude: 51.5194 @@ -2993,9 +2865,9 @@ and replace it with your IP (the IP of the machine that runs the Slack. Speak to you there! +We recommend you build your own AEA next. There are many helpful guides on here and a developer community on Slack. Speak to you there!
diff --git a/docs/generic-skills.md b/docs/generic-skills.md index bf6a84bf65..f0da2b991e 100644 --- a/docs/generic-skills.md +++ b/docs/generic-skills.md @@ -59,7 +59,7 @@ Follow the Preliminaries and @@ -93,7 +93,7 @@ default_routing: Then, fetch the buyer AEA: ``` bash -aea fetch fetchai/generic_buyer:0.7.0 --alias my_buyer_aea +aea fetch fetchai/generic_buyer:0.8.0 --alias my_buyer_aea cd my_buyer_aea aea install ``` @@ -105,19 +105,19 @@ The following steps create the buyer from scratch: ``` bash aea create my_buyer_aea cd my_buyer_aea -aea add connection fetchai/p2p_libp2p:0.7.0 -aea add connection fetchai/soef:0.6.0 -aea add connection fetchai/ledger:0.3.0 -aea add skill fetchai/generic_buyer:0.9.0 +aea add connection fetchai/p2p_libp2p:0.8.0 +aea add connection fetchai/soef:0.7.0 +aea add connection fetchai/ledger:0.4.0 +aea add skill fetchai/generic_buyer:0.10.0 aea install -aea config set agent.default_connection fetchai/p2p_libp2p:0.7.0 +aea config set agent.default_connection fetchai/p2p_libp2p:0.8.0 ``` In `my_buyer_aea/aea-config.yaml` add ``` yaml default_routing: - fetchai/ledger_api:0.2.0: fetchai/ledger:0.3.0 - fetchai/oef_search:0.4.0: fetchai/soef:0.6.0 + fetchai/ledger_api:0.3.0: fetchai/ledger:0.4.0 + fetchai/oef_search:0.5.0: fetchai/soef:0.7.0 ```

@@ -128,9 +128,9 @@ default_routing: First, create the private key for the seller AEA based on the network you want to transact. To generate and add a private-public key pair for Fetch.ai `AgentLand` use: ``` bash -aea generate-key cosmos -aea add-key cosmos cosmos_private_key.txt -aea add-key cosmos cosmos_private_key.txt --connection +aea generate-key fetchai +aea add-key fetchai fetchai_private_key.txt +aea add-key fetchai fetchai_private_key.txt --connection ``` ### Add keys and generate wealth for the buyer AEA @@ -139,14 +139,14 @@ The buyer needs to have some wealth to purchase the service from the seller. First, create the private key for the buyer AEA based on the network you want to transact. To generate and add a private-public key pair for Fetch.ai `AgentLand` use: ``` bash -aea generate-key cosmos -aea add-key cosmos cosmos_private_key.txt -aea add-key cosmos cosmos_private_key.txt --connection +aea generate-key fetchai +aea add-key fetchai fetchai_private_key.txt +aea add-key fetchai fetchai_private_key.txt --connection ``` Then, create some wealth for your buyer based on the network you want to transact with. On the Fetch.ai `AgentLand` network: ``` bash -aea generate-wealth cosmos +aea generate-wealth fetchai ``` ### Update the skill configs @@ -164,7 +164,7 @@ models: generic: data has_data_source: false is_ledger_tx: true - ledger_id: cosmos + ledger_id: fetchai location: latitude: 0.127 longitude: 51.5194 @@ -186,7 +186,7 @@ models: args: currency_id: FET is_ledger_tx: true - ledger_id: cosmos + ledger_id: fetchai location: latitude: 0.127 longitude: 51.5194 diff --git a/docs/glossary.md b/docs/glossary.md new file mode 100644 index 0000000000..d9b632c063 --- /dev/null +++ b/docs/glossary.md @@ -0,0 +1,5 @@ +This glossary defines a number of common terms used throughout the documentation. For the definition of framework components consult the API docs. + +* AEA: an Autonomous Economic Agent (AEA) is a "an intelligent agent acting on an owner's behalf, with limited or no interference, and whose goal is to generate economic value to its owner". AEAs are a special type of agent. + +* (Software) Agent: a software agent is a computer program that acts on behalf of an entity (e.g. individual, organisation, business). diff --git a/docs/gym-skill.md b/docs/gym-skill.md index 36cb0f7e0d..bbeab1d53b 100644 --- a/docs/gym-skill.md +++ b/docs/gym-skill.md @@ -1,4 +1,4 @@ -The AEA gym skill demonstrates how a custom Reinforcement Learning agent, that uses OpenAI's
gym library, may be embedded into an AEA skill and connection. +The AEA gym skill demonstrates how a custom Reinforcement Learning agent, that uses OpenAI's gym library, may be embedded into an AEA skill and connection. ### Discussion @@ -19,7 +19,7 @@ Follow the Preliminaries and beta distribution. The beta distribution is initialized to the uniform distribution. Each time the price associated with a given `PriceBandit` is accepted or rejected the distribution maintained by the `PriceBandit` is updated. For each good, the agent can therefore over time learn which price is most likely. +In this particular skill, which chiefly serves for demonstration purposes, we implement a very basic RL agent. The agent trains a model of price of `n` goods: it aims to discover the most likely price of each good. To this end, the agent randomly selects one of the `n` goods on each training step and then chooses as an `action` the price which it deems is most likely accepted. Each good is represented by an id and the possible price range `[1,100]` divided into 100 integer bins. For each price bin, a `PriceBandit` is created which models the likelihood of this price. In particular, a price bandit maintains a beta distribution. The beta distribution is initialized to the uniform distribution. Each time the price associated with a given `PriceBandit` is accepted or rejected the distribution maintained by the `PriceBandit` is updated. For each good, the agent can therefore over time learn which price is most likely. Gym skill illustration diff --git a/docs/http-connection-and-skill.md b/docs/http-connection-and-skill.md index 599e71d150..a864edd0fc 100644 --- a/docs/http-connection-and-skill.md +++ b/docs/http-connection-and-skill.md @@ -14,13 +14,13 @@ cd my_aea Add the http server connection package ``` bash -aea add connection fetchai/http_server:0.6.0 +aea add connection fetchai/http_server:0.7.0 ``` Update the default connection: ``` bash -aea config set agent.default_connection fetchai/http_server:0.6.0 +aea config set agent.default_connection fetchai/http_server:0.7.0 ``` Modify the `api_spec_path`: @@ -166,7 +166,7 @@ handlers: Finally, we run the fingerprinter: ``` bash -aea fingerprint skill fetchai/http_echo:0.4.0 +aea fingerprint skill fetchai/http_echo:0.5.0 ``` Note, you will have to replace the author name with your author handle. diff --git a/docs/identity.md b/docs/identity.md index 6493a04a05..90016b8da1 100644 --- a/docs/identity.md +++ b/docs/identity.md @@ -10,6 +10,6 @@ The AEAs currently use the addresses associated with their private-public key pa To learn how to generate a private-public key pair check out this section. -To learn more about public-key cryptography check out Wikipedia. +To learn more about public-key cryptography check out Wikipedia. -AEAs can provide attestations of their identity using third-party solutions. We have implemented a demo using Aries Hyperledger Cloud Agent which is available here. +AEAs can provide attestations of their identity using third-party solutions. We have implemented a demo using Aries Hyperledger Cloud Agent which is available here. diff --git a/docs/index.md b/docs/index.md index 9b5341f951..8c807fed02 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,24 +8,27 @@ We define an autonomous economic agent or AEA as: > an intelligent agent acting on an owner's behalf, with limited or no interference, and whose goal is to generate economic value to its owner. -In short, "software that generates economic value for you". + -AEAs act independently of constant user input and autonomously execute actions to achieve their goal. Their goal is to create economic value for you, their owner. AEAs have a wide range of application areas and we provide demo guides for some examples. +AEAs act independently of constant input from their owner and autonomously execute actions to achieve their prescribed goal. Their goal is to create economic value for you, their owner, in a clearly defined domain. AEAs have a wide range of application areas and we provide demo guides for some examples. -Autonomous Economic Agents are digital entities that run complex dynamic decision-making algorithms for application owners and clients. + AEAs are not: -* just any agents: AEAs have an express purpose to generate economic value in a multi-stakeholder environment. +* just any agents: AEAs' purpose is to generate economic value in a multi-stakeholder environment with competing incentives between agents. * APIs or sensors which do not have agency. * smart contracts which do not display any proactiveness and are purely reactive to external requests (=contract calls). * artificial general intelligence (AGI): AEAs can have a very narrow goal directed focus involving some economic gain and implemented via simple conditional logic. ## What is the AEA Framework? -The AEA framework is a Python-based development suite which equips you with an efficient and accessible set of tools for building AEAs. The framework is modular, extensible, and composable. This framework attempts to make agent development as straightforward an experience as possible, similar to web development using popular web frameworks. +The AEA framework is a Python-based development suite which equips you with an efficient and accessible set of tools for building AEAs. The framework is modular, extensible, and composable. It attempts to make agent development as straightforward an experience as possible, similar to web development using popular web frameworks. + + +## Next steps To get started developing your own AEA, check out the getting started section. @@ -33,11 +36,14 @@ To learn more about some of the distinctive characteristics of agent-oriented de If you would like to develop an AEA in a language different to Python then check out our language agnostic AEA definition. -AEAs achieve their goals with the help of the Open Economic Framework (OEF) - a decentralized communication and search & discovery system for agents - and using Fetch.ai's blockchain as a financial settlement and commitment layer. Third-party blockchains, such as Ethereum, may also allow AEA integration. +AEAs achieve their goals with the help of the Open Economic Framework (OEF) - a decentralized communication and search & discovery system for agents - and using Fetch.ai's blockchain as a financial settlement and commitment layer. Third-party blockchains, such as Ethereum, may also allow AEA integration. + + +## Help us improve

Note

-

This developer documentation is a work in progress. If you spot any errors please open an issue here.

+

This developer documentation is a work in progress. If you spot any errors please open an issue on Github or contact us in the developer Slack channel.


diff --git a/docs/language-agnostic-definition.md b/docs/language-agnostic-definition.md index c67a7a6bdb..3b0d059ef9 100644 --- a/docs/language-agnostic-definition.md +++ b/docs/language-agnostic-definition.md @@ -1,7 +1,7 @@ An Autonomous Economic Agent is, in technical terms, defined by the following characteristics:
    -
  • It MUST be capable of receiving and sending `Envelopes` which satisfy the following protobuf schema: +
  • It MUST be capable of receiving and sending `Envelopes` which satisfy the following protobuf schema: ``` proto syntax = "proto3"; @@ -17,17 +17,17 @@ message Envelope{ } ``` -The format for the above fields, except `message`, is specified below. For those with `regexp`, the format is described in regular expression. +The format for the above fields, except `message`, is specified below. For those with `regexp`, the format is described in regular expression.
      -
    • to and sender: an address derived from the private key of a secp256k1-compatible elliptic curve
    • +
    • to and sender: an address derived from the private key of a secp256k1-compatible elliptic curve
    • protocol_id: (`regexp`) `^[a-zA-Z0-9_]*/[a-zA-Z_][a-zA-Z0-9_]*:(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`
    • -
    • bytes: any bytes string
    • -
    • URI: this syntax
    • +
    • bytes: a bytes string representing a serialized message in the specified protocol
    • +
    • URI: this syntax
  • -
  • It MUST implement each protocol with the required meta-fields: +
  • It MUST implement each protocol's message with the required meta-fields: ``` proto @@ -50,7 +50,7 @@ The format for the above fields, except `message`, is specified below. For those

    This section is incomplete, and will be updated soon!

  • -
  • It SHOULD implement the `fetchai/default:0.1.0` protocol which satisfies the following protobuf schema: +
  • It SHOULD implement the `fetchai/default:0.5.0` protocol which satisfies the following protobuf schema: ``` proto syntax = "proto3"; @@ -98,9 +98,15 @@ message DefaultMessage{
  • It is recommended that it processes `Envelopes` asynchronously. Note, the specification regarding the processing of messages does not impose any particular implementation choice/constraint; for example, the AEA can process envelopes either synchronously and asynchronously. However, due to the high level of activity that an AEA might be subject to, other AEAs expect a certain minimum level of responsiveness and reactivity of an AEA's implementation, especially in the case of many concurrent dialogues with other peers. That could imply the need for asynchronous programming to make the AEA's implementation scalable.
  • -
  • It MUST have an identity in the form of, at a minimum, an address derived from a public key and its associated private key (where the eliptic curve must be of type SECP256k1). +
  • It MUST have an identity in the form of, at a minimum, an address derived from a public key and its associated private key (where the eliptic curve must be of type SECP256k1).
  • -
  • It SHOULD implement handling of errors using the `default` protocol. The protobuf schema is given above. +
  • It SHOULD implement handling of errors using the `fetchai/default:0.5.0` protocol. The protobuf schema is given above. +
  • +
  • It MUST implement the following principles when handling messages: +
      +
    • It MUST ALWAYS handle incoming envelopes/messages and NEVER raise. This ensures another AEA cannot take it down by sending an incompatible envelope/message.
    • +
    • It MUST NEVER handle outgoing messages and ALWAYS raise. This implies own business logic mistakes are not handled by business logic.
    • +
diff --git a/docs/logging.md b/docs/logging.md index ea3c8e82dc..a183b4378c 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -1,4 +1,4 @@ -The AEA framework supports flexible logging capabilities with the standard Python logging library. +The AEA framework supports flexible logging capabilities with the standard Python logging library. In this tutorial, we configure logging for an AEA. @@ -18,18 +18,18 @@ author: fetchai version: 0.1.0 description: '' license: Apache-2.0 -aea_version: 0.5.4 +aea_version: 0.6.0 fingerprint: {} fingerprint_ignore_patterns: [] connections: -- fetchai/stub:0.8.0 +- fetchai/stub:0.9.0 contracts: [] protocols: -- fetchai/default:0.4.0 +- fetchai/default:0.5.0 skills: -- fetchai/error:0.4.0 -default_connection: fetchai/stub:0.8.0 -default_ledger: cosmos +- fetchai/error:0.5.0 +default_connection: fetchai/stub:0.9.0 +default_ledger: fetchai logging_config: disable_existing_loggers: false version: 1 @@ -39,9 +39,9 @@ registry_path: ../packages By updating the `logging_config` section, you can configure the loggers of your application. -The format of this section is specified in the `logging.config` module. +The format of this section is specified in the `logging.config` module. -At this section +At this section you'll find the definition of the configuration dictionary schema. Below is an example of the `logging_config` value. diff --git a/docs/ml-skills.md b/docs/ml-skills.md index 7daf91c9b2..834ab3d5f5 100644 --- a/docs/ml-skills.md +++ b/docs/ml-skills.md @@ -62,7 +62,7 @@ Follow the Preliminaries and @@ -96,7 +96,7 @@ default_routing: Then, fetch the model trainer AEA: ``` bash -aea fetch fetchai/ml_model_trainer:0.10.0 +aea fetch fetchai/ml_model_trainer:0.11.0 cd ml_model_trainer aea install ``` @@ -108,19 +108,19 @@ The following steps create the model trainer from scratch: ``` bash aea create ml_model_trainer cd ml_model_trainer -aea add connection fetchai/p2p_libp2p:0.7.0 -aea add connection fetchai/soef:0.6.0 -aea add connection fetchai/ledger:0.3.0 -aea add skill fetchai/ml_train:0.9.0 -aea config set agent.default_connection fetchai/p2p_libp2p:0.7.0 +aea add connection fetchai/p2p_libp2p:0.8.0 +aea add connection fetchai/soef:0.7.0 +aea add connection fetchai/ledger:0.4.0 +aea add skill fetchai/ml_train:0.10.0 +aea config set agent.default_connection fetchai/p2p_libp2p:0.8.0 aea install ``` In `ml_model_trainer/aea-config.yaml` add ``` yaml default_routing: - fetchai/ledger_api:0.2.0: fetchai/ledger:0.3.0 - fetchai/oef_search:0.4.0: fetchai/soef:0.6.0 + fetchai/ledger_api:0.3.0: fetchai/ledger:0.4.0 + fetchai/oef_search:0.5.0: fetchai/soef:0.7.0 ```

@@ -130,9 +130,9 @@ default_routing: First, create the private key for the data provider AEA based on the network you want to transact. To generate and add a private-public key pair for Fetch.ai `AgentLand` use: ``` bash -aea generate-key cosmos -aea add-key cosmos cosmos_private_key.txt -aea add-key cosmos cosmos_private_key.txt --connection +aea generate-key fetchai +aea add-key fetchai fetchai_private_key.txt +aea add-key fetchai fetchai_private_key.txt --connection ``` ### Add keys and generate wealth for the model trainer AEA @@ -141,14 +141,14 @@ The model trainer needs to have some wealth to purchase the data from the data p First, create the private key for the model trainer AEA based on the network you want to transact. To generate and add a private-public key pair for Fetch.ai `AgentLand` use: ``` bash -aea generate-key cosmos -aea add-key cosmos cosmos_private_key.txt -aea add-key cosmos cosmos_private_key.txt --connection +aea generate-key fetchai +aea add-key fetchai fetchai_private_key.txt +aea add-key fetchai fetchai_private_key.txt --connection ``` Then, create some wealth for your model trainer based on the network you want to transact with. On the Fetch.ai `AgentLand` network: ``` bash -aea generate-wealth cosmos +aea generate-wealth fetchai ``` ### Run both AEAs diff --git a/docs/multiplexer-standalone.md b/docs/multiplexer-standalone.md index 0293958843..1c6d91fccc 100644 --- a/docs/multiplexer-standalone.md +++ b/docs/multiplexer-standalone.md @@ -60,7 +60,7 @@ We use the input and output text files to send an envelope to our agent and rece ``` python # Create a message inside an envelope and get the stub connection to pass it into the multiplexer message_text = ( - "multiplexer,some_agent,fetchai/default:0.4.0,\x08\x01*\x07\n\x05hello," + "multiplexer,some_agent,fetchai/default:0.5.0,\x08\x01*\x07\n\x05hello," ) with open(INPUT_FILE, "w") as f: write_with_lock(f, message_text) @@ -155,7 +155,7 @@ def run(): # Create a message inside an envelope and get the stub connection to pass it into the multiplexer message_text = ( - "multiplexer,some_agent,fetchai/default:0.4.0,\x08\x01*\x07\n\x05hello," + "multiplexer,some_agent,fetchai/default:0.5.0,\x08\x01*\x07\n\x05hello," ) with open(INPUT_FILE, "w") as f: write_with_lock(f, message_text) diff --git a/docs/oef-ledger.md b/docs/oef-ledger.md index 629baab288..ba7439a891 100644 --- a/docs/oef-ledger.md +++ b/docs/oef-ledger.md @@ -14,7 +14,8 @@ The 'Open Economic Framework' (OEF) consists of protocols, languages and market At present, the OEF's capabilities are fulfilled by two components: -- a permissionless, public peer to peer (agent to agent) communication network, called the Agent Communication Network; and +- a permissionless, public peer to peer (agent to agent) communication network, called the Agent Communication Network; +- a set of
agent interaction protocols; and - a centralized search and discovery system. The latter will be decentralized over time. @@ -29,9 +30,9 @@ Agents can receive messages from other agents if they are both connected to the ### Centralized search and discovery -A `simple OEF search node` allows agents to search and discover each other. In particular, agents can register themselves and their services as well as send search requests. +A simple OEF (SOEF) search node allows agents to search and discover each other. In particular, agents can register themselves and their services as well as send search requests. -For two agents to be able to find each other, at least one must register themselves and the other must query the `simple OEF search node` for it. Detailed documentation is provided `here`. +For two agents to be able to find each other, at least one must register themselves and the other must query the SOEF search node for it. Detailed documentation is provided here.