diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..de9cab0237 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + + # Maintain dependencies for Pip + - package-ecosystem: "pip" + directory: "." + schedule: + interval: "weekly" + target-branch: "develop" + labels: + - "dependencies" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index bc8affdffe..30d6a0c9e9 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -49,7 +49,7 @@ Describe in short the main changes with the new release. _Put an `x` in the boxes that apply._ - [ ] I have read the [CONTRIBUTING](../CONTRIBUTING.md) doc -- [ ] I am making a pull request against the `master` branch (left side), from `develop` +- [ ] I am making a pull request against the `main` branch (left side), from `develop` - [ ] Lint and unit tests pass locally - [ ] I have checked the fingerprint hashes are correct by running (`scripts/generate_ipfs_hashes.py`) - [ ] I have regenerated the latest API docs diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 5dce59cbd3..f2d0dc3cb7 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -4,7 +4,7 @@ on: push: branches: - develop - - master + - main pull_request: jobs: @@ -26,6 +26,20 @@ jobs: pip install pipenv - name: Pipenv lock run: pipenv lock + - name: Check plugin consistency + run: | + # these two files should not be different; + # we vendorize main "cosmos.py" module in fetchai crypto plugin package + diff plugins/aea-ledger-cosmos/aea_ledger_cosmos/cosmos.py plugins/aea-ledger-fetchai/aea_ledger_fetchai/_cosmos.py + + # check diff between plugins' LICENSE and main LICENSE + diff LICENSE plugins/aea-ledger-cosmos/LICENSE + diff LICENSE plugins/aea-ledger-ethereum/LICENSE + diff LICENSE plugins/aea-ledger-fetchai/LICENSE + - name: Check go code consistency + run: | + # check diff between go code in libs and packages + diff libs/go/libp2p_node packages/fetchai/connections/p2p_libp2p/libp2p_node -r common_checks_2: continue-on-error: False @@ -58,13 +72,20 @@ jobs: run: tox -e vulture - name: Static type check run: tox -e mypy - - name: Golang code style check + - name: Golang code style check (libp2p_node) + uses: golangci/golangci-lint-action@v1 + env: + ACTIONS_ALLOW_UNSECURE_COMMANDS: true + with: + version: v1.28 + working-directory: libs/go/libp2p_node + - name: Golang code style check (aealite) uses: golangci/golangci-lint-action@v1 env: ACTIONS_ALLOW_UNSECURE_COMMANDS: true with: version: v1.28 - working-directory: packages/fetchai/connections/p2p_libp2p/ + working-directory: libs/go/aealite common_checks_3: continue-on-error: False @@ -100,6 +121,7 @@ jobs: sudo apt-get autoremove sudo apt-get autoclean pip install tox + pip install --user --upgrade setuptools # install Protobuf compiler wget https://github.com/protocolbuffers/protobuf/releases/download/v3.11.4/protoc-3.11.4-linux-x86_64.zip unzip protoc-3.11.4-linux-x86_64.zip -d protoc @@ -130,7 +152,7 @@ jobs: continue-on-error: False runs-on: ubuntu-latest timeout-minutes: 10 - if: github.base_ref == 'master' + if: github.base_ref == 'main' steps: - uses: actions/checkout@master - uses: actions/setup-python@master @@ -350,6 +372,9 @@ jobs: name: Unit tests run: | tox -e py${{ matrix.python_version }} -- -m 'not integration and not unstable' + - name: Plugin tests + run: | + tox -e plugins-py${{ matrix.python_version }} -- --cov-append -m 'not integration and not unstable' platform_checks_sync_aea_loop: continue-on-error: True @@ -404,8 +429,12 @@ jobs: with: go-version: '^1.14.0' - if: matrix.python-version == '3.6' - name: Golang unit tests - working-directory: ./packages/fetchai/connections/p2p_libp2p + name: Golang unit tests (libp2p_node) + working-directory: ./libs/go/libp2p_node + run: go test -p 1 -timeout 0 -count 1 -v ./... + - if: matrix.python-version == '3.6' + name: Golang unit tests (aealite) + working-directory: ./libs/go/aealite run: go test -p 1 -timeout 0 -count 1 -v ./... coverage_checks: @@ -438,7 +467,9 @@ jobs: make protolint_install # sudo apt-get install -y protobuf-compiler - name: Run all tests - run: tox -e py3.7-cov -- --ignore=tests/test_docs --ignore=tests/test_examples --ignore=tests/test_packages/test_contracts --ignore=tests/test_packages/test_skills_integration -m 'not unstable' + run: | + tox -e py3.7-cov -- --ignore=tests/test_docs --ignore=tests/test_examples --ignore=tests/test_packages/test_contracts --ignore=tests/test_packages/test_skills_integration -m 'not unstable' + tox -e plugins-py3.7-cov -- --cov-append -m 'not unstable' continue-on-error: true - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 diff --git a/.gitignore b/.gitignore index d736bd090b..ad656d5e38 100644 --- a/.gitignore +++ b/.gitignore @@ -124,6 +124,8 @@ output_file !packages/fetchai/contracts/oracle/build !packages/fetchai/contracts/oracle_client/build !packages/fetchai/contracts/fet_erc20/build -packages/fetchai/connections/p2p_libp2p/libp2p_node +packages/fetchai/connections/p2p_libp2p/libp2p_node/libp2p_node !tests/data/dummy_contract/build +!plugins/aea-ledger-ethereum/tests/data/dummy_contract/build +!plugins/aea-ledger-cosmos/tests/data/dummy_contract/build diff --git a/.pylintrc b/.pylintrc index 76f042c633..fb3b657e18 100644 --- a/.pylintrc +++ b/.pylintrc @@ -22,7 +22,7 @@ disable=C0103,C0201,C0301,C0302,C0330,W0105,W0107,W0707,W1202,W1203,R0801 # 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,aioprometheus +ignored-modules=bech32,ecdsa,lru,eth_typing,eth_keys,eth_account,ipfshttpclient,werkzeug,openapi_spec_validator,aiohttp,yoti_python_sdk,defusedxml,gym,fetch,matplotlib,memory_profiler,numpy,oef,openapi_core,psutil,tensorflow,temper,skimage,vyper,web3,aioprometheus [DESIGN] min-public-methods=1 diff --git a/.spelling b/.spelling index df2409fe57..c5a054c27e 100644 --- a/.spelling +++ b/.spelling @@ -170,6 +170,8 @@ Yoti PowerShell deregisters plugin +Fetch.AI +AEALite - docs/language-agnostic-definition.md fetchai protocol_id diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c72ea7545f..55ead6b088 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,4 +7,4 @@ If you need support, want to report/fix a bug, ask for/implement features, you c or [submit a Pull request](https://github.com/fetchai/agents-aea/pulls) For other kinds of feedback, you can contact one of the -[authors](https://github.com/fetchai/agents-aea/blob/master/AUTHORS.md) by email. +[authors](https://github.com/fetchai/agents-aea/blob/main/AUTHORS.md) by email. diff --git a/HISTORY.md b/HISTORY.md index 600e5b7fa8..be3366bff6 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,30 @@ # Release History +## 0.11.0 (2020-03-04) + +- Adds slots usage in frequently used framework objects, including `Dialogue` +- Fixes a bug in `aea upgrade` command where eject prompt was not offered +- Refactors skill component configurations to allow for skill components (`Handler`, `Behaviour`, `Model`) to be placed anywhere in a skill +- Extends skill component configuration to specify optional `file_path` field +- Extracts all ledger specific functionality in plugins +- Improves error logging in http server connection +- Updates `Development - Use case` documentation +- Adds restart support to `p2p_libp2p` connection on read/write failure +- Adds validation of default routing and default connection configuration +- Refactors and significantly simplifies routing between components +- Limits usage of `EnvelopeContext` +- Adds support for new CosmWasm message format in ledger plugins +- Adds project loading checks and optional auto removal in `MultiAgentManager` +- Adds support for reuse of threaded `Multiplexer` +- Fixes bug in TAC which caused agents to make suboptimal trades +- Adds support to specify dependencies on `aea-config.yaml` level +- Improves release scripts +- Adds lightweight Golang AEALite library +- Adds support for skill-to-skill messages +- Removes CLI GUI +- Multiple docs updates based on user feedback +- Multiple additional tests and test stability fixes + ## 0.10.1 (2020-02-21) - Changes default URL of `soef` connection to https diff --git a/MANIFEST.in b/MANIFEST.in index cf69a128e5..dd44b74cb2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,5 +4,6 @@ recursive-include aea *.json *.yaml *.proto *.ico *png *.html *.js *.css *.md *. recursive-include docs * recursive-include examples * recursive-include packages * +recursive-include plugins * recursive-include scripts * recursive-include tests * diff --git a/Makefile b/Makefile index debe1b410a..43988bb1a1 100644 --- a/Makefile +++ b/Makefile @@ -40,24 +40,30 @@ clean-test: .PHONY: lint lint: - black aea benchmark examples packages scripts tests - isort aea benchmark examples packages scripts tests - flake8 aea benchmark examples packages scripts tests + black aea benchmark examples packages plugins scripts tests + isort aea benchmark examples packages plugins scripts tests + flake8 aea benchmark examples packages plugins scripts tests vulture aea scripts/whitelist.py --exclude "*_pb2.py" .PHONY: pylint pylint: - pylint -j4 aea benchmark packages scripts examples/* + pylint -j4 aea benchmark packages scripts plugins/aea-ledger-fetchai/aea_ledger_fetchai plugins/aea-ledger-ethereum/aea_ledger_ethereum plugins/aea-ledger-cosmos/aea_ledger_cosmos examples/* .PHONY: security security: - bandit -r aea benchmark examples packages - bandit -s B101 -r tests scripts + bandit -r aea benchmark examples packages \ + plugins/aea-ledger-fetchai/aea_ledger_fetchai \ + plugins/aea-ledger-ethereum/aea_ledger_ethereum \ + plugins/aea-ledger-cosmos/aea_ledger_cosmos + bandit -s B101 -r tests scripts \ + plugins/aea-ledger-fetchai/tests \ + plugins/aea-ledger-ethereum/tests \ + plugins/aea-ledger-cosmos/tests safety check -i 37524 -i 38038 -i 37776 -i 38039 .PHONY: static static: - mypy aea benchmark examples packages scripts --disallow-untyped-defs + mypy aea benchmark examples packages plugins/aea-ledger-fetchai/aea_ledger_fetchai plugins/aea-ledger-ethereum/aea_ledger_ethereum plugins/aea-ledger-cosmos/aea_ledger_cosmos scripts --disallow-untyped-defs mypy tests .PHONY: package_checks @@ -75,6 +81,9 @@ common_checks: security misc_checks lint static docs .PHONY: test test: + pytest -rfE plugins/aea-ledger-fetchai/tests --cov=aea_ledger_fetchai --cov-report=term --cov-report=term-missing --cov-config=.coveragerc + pytest -rfE plugins/aea-ledger-ethereum/tests --cov=aea_ledger_ethereum --cov-report=term --cov-report=term-missing --cov-config=.coveragerc + pytest -rfE plugins/aea-ledger-cosmos/tests --cov=aea_ledger_cosmos --cov-report=term --cov-report=term-missing --cov-config=.coveragerc pytest -rfE --doctest-modules aea packages/fetchai/protocols packages/fetchai/connections packages/fetchai/skills/confirmation_aw1 packages/fetchai/skills/confirmation_aw2 packages/fetchai/skills/confirmation_aw3 packages/fetchai/skills/generic_buyer packages/fetchai/skills/generic_seller packages/fetchai/skills/tac_control packages/fetchai/skills/tac_control_contract packages/fetchai/skills/tac_participation packages/fetchai/skills/tac_negotiation packages/fetchai/skills/simple_buyer packages/fetchai/skills/simple_data_request packages/fetchai/skills/simple_seller packages/fetchai/skills/simple_service_registration packages/fetchai/skills/simple_service_search packages/fetchai/skills/coin_price packages/fetchai/skills/fetch_beacon packages/fetchai/skills/simple_oracle packages/fetchai/skills/simple_oracle_client tests/ --cov-report=html --cov-report=xml --cov-report=term-missing --cov-report=term --cov=aea --cov=packages/fetchai/protocols --cov=packages/fetchai/connections --cov=packages/fetchai/skills/confirmation_aw1 --cov=packages/fetchai/skills/confirmation_aw2 --cov=packages/fetchai/skills/confirmation_aw3 --cov=packages/fetchai/skills/generic_buyer --cov=packages/fetchai/skills/generic_seller --cov=packages/fetchai/skills/tac_control --cov=packages/fetchai/skills/tac_control_contract --cov=packages/fetchai/skills/tac_participation --cov=packages/fetchai/skills/tac_negotiation --cov=packages/fetchai/skills/simple_buyer --cov=packages/fetchai/skills/simple_data_request --cov=packages/fetchai/skills/simple_seller --cov=packages/fetchai/skills/simple_service_registration --cov=packages/fetchai/skills/simple_service_search --cov=packages/fetchai/skills/coin_price --cov=packages/fetchai/skills/fetch_beacon --cov=packages/fetchai/skills/simple_oracle --cov=packages/fetchai/skills/simple_oracle_client --cov-config=.coveragerc find . -name ".coverage*" -not -name ".coveragerc" -exec rm -fr "{}" \; @@ -106,13 +115,13 @@ h := $(shell git rev-parse --abbrev-ref HEAD) .PHONY: release_check release: - if [ "$h" = "master" ];\ + if [ "$h" = "main" ];\ then\ - echo "Please ensure everything is merged into master & tagged there";\ + echo "Please ensure everything is merged into main & tagged there";\ pip install twine;\ twine upload dist/*;\ else\ - echo "Please change to master branch for release.";\ + echo "Please change to main branch for release.";\ fi v := $(shell pip -V | grep virtualenvs) @@ -123,9 +132,12 @@ new_env: clean then\ pipenv --rm;\ pipenv --python 3.7;\ - pipenv install --dev --skip-lock;\ - pipenv run pip uninstall typing -y;\ - pipenv run pip install -e .[all];\ + pipenv install --dev --skip-lock;\ + pipenv run pip uninstall typing -y;\ + pipenv run pip install -e .[all];\ + pipenv run pip install --no-deps file:plugins/aea-ledger-ethereum;\ + pipenv run pip install --no-deps file:plugins/aea-ledger-cosmos;\ + pipenv run pip install --no-deps file:plugins/aea-ledger-fetchai;\ echo "Enter virtual environment with all development dependencies now: 'pipenv shell'.";\ else\ echo "In a virtual environment! Exit first: 'exit'.";\ @@ -137,4 +149,4 @@ protolint: protolint_install_win: powershell -command '$$env:GO111MODULE="on"; go get -u -v github.com/yoheimuta/protolint/cmd/protolint@v0.27.0' protolint_win: - protolint lint -config_path=./protolint.yaml -fix ./aea/mail ./packages/fetchai/protocols + protolint lint -config_path=./protolint.yaml -fix ./aea/mail ./packages/fetchai/protocols \ No newline at end of file diff --git a/Pipfile b/Pipfile index 727352ae33..92bc96260a 100644 --- a/Pipfile +++ b/Pipfile @@ -12,11 +12,14 @@ name = "test-pypi" aiohttp = "==3.6.2" aioprometheus = "==20.0.1" bandit = "==1.6.2" +bech32 = "==1.2.0" black = "==19.10b0" bs4 = "==0.0.1" colorlog = "==4.1.0" defusedxml = "==0.6.0" docker = "==4.2.0" +ecdsa = ">=0.15" +eth-account = "==0.5.2" flake8 = "==3.7.9" flake8-bugbear = "==20.1.4" flake8-docstrings = "==1.5.0" @@ -55,7 +58,9 @@ tox = "==3.15.1" vulture = "==2.1" vyper = "==0.1.0b12" isort = "==5.5.2" +web3 = "==5.12.0" yoti = "==2.14.0" +pytest-custom-exit-code = "==0.3.0" [packages] # we don't specify dependencies for the library here for intallation as per: https://pipenv-fork.readthedocs.io/en/latest/advanced.html#pipfile-vs-setuppy diff --git a/README.md b/README.md index 46f341f8d0..629ccc7b16 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ PyPI - Wheel - + License @@ -24,7 +24,7 @@

- AEA framework sanity checks and tests + AEA framework sanity checks and tests Codecov @@ -125,7 +125,7 @@ You can have more control on the installed dependencies by leveraging the setupt The following dependency is **only relevant if you intend to contribute** to the repository: -- All Pull Requests should be opened against the `develop` branch. Do **not** open a Pull Request against `master`! +- All Pull Requests should be opened against the `develop` branch. Do **not** open a Pull Request against `main`! - The project uses [Google Protocol Buffers](https://developers.google.com/protocol-buffers/) compiler for message serialization. A guide on how to install it is found [here](https://fetchai.github.io/oef-sdk-python/user/install.html#protobuf-compiler). @@ -183,15 +183,15 @@ The following steps are **only relevant if you intend to contribute** to the rep ### Go Development -The `fetchai/p2p_libp2p` package is partially developed in Go. +- The `fetchai/p2p_libp2p` package is partially developed in Go. - To install Go visit the [Golang site](https://golang.org/doc/install). - We use [`golines`](https://github.com/segmentio/golines) and [`golangci-lint`](https://golangci-lint.run) for linting. -- To run tests, use `go test -p 1 -timeout 0 -count 1 -v ./...` from the root directory of the package. +- To run tests, use `go test -p 1 -timeout 0 -count 1 -v ./...` from the root directory of the package. If you experience installation or build issues run `go clean -modcache`. -### Documentation +### Documentation - To start a live-reloading docs server on localhost: `mkdocs serve`. To amend the docs, create a new documentation file in `docs/` and add a reference to it in `mkdocs.yml`. diff --git a/SECURITY.md b/SECURITY.md index d26fec8f96..8805f9c830 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,8 +8,8 @@ The following table shows which versions of `aea` are currently being supported | Version | Supported | | --------- | ------------------ | -| `0.10.x` | :white_check_mark: | -| `< 0.10.0` | :x: | +| `0.11.x` | :white_check_mark: | +| `< 0.11.0` | :x: | ## Reporting a Vulnerability diff --git a/aea/__init__.py b/aea/__init__.py index edfc9acaef..2ee575b9cd 100644 --- a/aea/__init__.py +++ b/aea/__init__.py @@ -34,10 +34,13 @@ __url__, __version__, ) +from aea.crypto.plugin import load_all_plugins AEA_DIR = os.path.dirname(inspect.getfile(inspect.currentframe())) # type: ignore +load_all_plugins() + def get_current_aea_version() -> Version: """Get current version.""" diff --git a/aea/__version__.py b/aea/__version__.py index a83a7caca1..e9d93369c4 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.10.1" +__version__ = "0.11.0" __author__ = "Fetch.AI Limited" __license__ = "Apache-2.0" __copyright__ = "2019 Fetch.AI Limited" diff --git a/aea/aea.py b/aea/aea.py index ebb4e074a2..217964ceff 100644 --- a/aea/aea.py +++ b/aea/aea.py @@ -211,6 +211,7 @@ def __init__( data_dir, storage_callable=lambda: self.runtime.storage, build_dir=self.get_build_dir(), + send_to_skill=self.runtime.agent_loop.send_to_skill, **kwargs, ) self._execution_timeout = execution_timeout @@ -282,26 +283,34 @@ def _get_msg_and_handlers_for_envelope( envelope.protocol_specification_id ) - msg, handlers = self._handle_decoding(envelope, protocol) + error_handler = self._get_error_handler() + + if protocol is None: + error_handler.send_unsupported_protocol(envelope, self.logger) + return None, [] + + msg, handlers = self._handle_decoding(envelope, protocol, error_handler) return msg, handlers def _handle_decoding( - self, envelope: Envelope, protocol: Optional[Protocol] + self, + envelope: Envelope, + protocol: Protocol, + error_handler: Type[AbstractErrorHandler], ) -> Tuple[Optional[Message], List[Handler]]: - handler = self._get_error_handler() - - if protocol is None: - handler.send_unsupported_protocol(envelope, self.logger) - return None, [] # Tuple[Optional[Message], List[Handler]] - handlers = self.filter.get_active_handlers( - protocol.public_id, envelope.skill_id + protocol.public_id, envelope.to_as_public_id ) if len(handlers) == 0: - handler.send_unsupported_skill(envelope, self.logger) + reason = ( + f"no active handler for protocol={protocol.public_id} in skill={envelope.to_as_public_id}" + if envelope.is_component_to_component_message + else f"no active handler for protocol={protocol.public_id}" + ) + error_handler.send_no_active_handler(envelope, reason, self.logger) return None, [] if isinstance(envelope.message, Message): @@ -313,8 +322,7 @@ def _handle_decoding( msg.to = envelope.to return msg, handlers 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))) - handler.send_decoding_error(envelope, self.logger) + error_handler.send_decoding_error(envelope, e, self.logger) return None, [] def handle_envelope(self, envelope: Envelope) -> None: @@ -393,6 +401,7 @@ def get_message_handlers(self) -> List[Tuple[Callable[[Any], None], Callable]]: """ return super().get_message_handlers() + [ (self.filter.handle_internal_message, self.filter.get_internal_message,), + (self.handle_envelope, self.runtime.agent_loop.skill2skill_queue.get), ] def exception_handler(self, exception: Exception, function: Callable) -> bool: diff --git a/aea/aea_builder.py b/aea/aea_builder.py index aa572e5c33..f1860a0a6f 100644 --- a/aea/aea_builder.py +++ b/aea/aea_builder.py @@ -64,11 +64,11 @@ ) from aea.configurations.constants import ( DOTTED_PATH_MODULE_ELEMENT_SEPARATOR, - FETCHAI, PROTOCOLS, SIGNING_PROTOCOL, SKILLS, STATE_UPDATE_PROTOCOL, + _FETCHAI_IDENTIFIER, ) from aea.configurations.loader import ConfigLoader, load_component_configuration from aea.configurations.manager import ( @@ -560,6 +560,22 @@ def set_default_routing( :return: self """ + for protocol_id, connection_id in default_routing.items(): + if ( + ComponentId("protocol", protocol_id) + not in self._package_dependency_manager.protocols + ): + raise ValueError( + f"Protocol {protocol_id} specified in `default_routing` is not a project dependency!" + ) + if ( + ComponentId("connection", connection_id) + not in self._package_dependency_manager.connections + ): + raise ValueError( + f"Connection {connection_id} specified in `default_routing` is not a project dependency!" + ) + self._default_routing = default_routing # pragma: nocover return self @@ -642,17 +658,26 @@ def _add_default_packages(self) -> None: # add default protocol default_protocol = PublicId.from_str(DEFAULT_PROTOCOL) self.add_protocol( - Path(self.registry_dir, FETCHAI, PROTOCOLS, default_protocol.name) + Path( + self.registry_dir, _FETCHAI_IDENTIFIER, PROTOCOLS, default_protocol.name + ) ) # add signing protocol signing_protocol = PublicId.from_str(SIGNING_PROTOCOL) self.add_protocol( - Path(self.registry_dir, FETCHAI, PROTOCOLS, signing_protocol.name) + Path( + self.registry_dir, _FETCHAI_IDENTIFIER, PROTOCOLS, signing_protocol.name + ) ) # add state update protocol state_update_protocol = PublicId.from_str(STATE_UPDATE_PROTOCOL) self.add_protocol( - Path(self.registry_dir, FETCHAI, PROTOCOLS, state_update_protocol.name) + Path( + self.registry_dir, + _FETCHAI_IDENTIFIER, + PROTOCOLS, + state_update_protocol.name, + ) ) def _check_can_remove(self, component_id: ComponentId) -> None: @@ -700,6 +725,14 @@ def set_default_connection( :param public_id: the public id of the default connection package. :return: the AEABuilder """ + if ( + public_id + and ComponentId("connection", public_id) + not in self._package_dependency_manager.connections + ): + raise ValueError( + f"Connection {public_id} specified as `default_connection` is not a project dependency!" + ) self._default_connection = public_id return self @@ -1533,10 +1566,11 @@ def set_from_configuration( self.set_default_ledger(agent_configuration.default_ledger) self.set_build_entrypoint(agent_configuration.build_entrypoint) self.set_currency_denominations(agent_configuration.currency_denominations) - self.set_default_connection(agent_configuration.default_connection) + 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 != {}: dotted_path = agent_configuration.decision_maker_handler["dotted_path"] file_path = agent_configuration.decision_maker_handler["file_path"] @@ -1553,7 +1587,7 @@ def set_from_configuration( self.set_connection_exception_policy( ExceptionPolicyEnum(agent_configuration.connection_exception_policy) ) - self.set_default_routing(agent_configuration.default_routing) + self.set_loop_mode(agent_configuration.loop_mode) self.set_runtime_mode(agent_configuration.runtime_mode) self.set_storage_uri(agent_configuration.storage_uri) @@ -1593,6 +1627,9 @@ def set_from_configuration( agent_configuration.component_configurations ) + self.set_default_connection(agent_configuration.default_connection) + self.set_default_routing(agent_configuration.default_routing) + @staticmethod def _find_import_order( component_ids: List[ComponentId], diff --git a/aea/agent.py b/aea/agent.py index 74f5f0d5b5..375e910316 100644 --- a/aea/agent.py +++ b/aea/agent.py @@ -273,7 +273,9 @@ def get_message_handlers(self) -> List[Tuple[Callable[[Any], None], Callable]]: :return: List of tuples of callables: handler and coroutine to get a message """ - return [(self.handle_envelope, self.inbox.async_get)] + return [ + (self.handle_envelope, self.inbox.async_get), + ] def exception_handler( self, exception: Exception, function: Callable diff --git a/aea/agent_loop.py b/aea/agent_loop.py index d4272ec76f..dba8acd11b 100644 --- a/aea/agent_loop.py +++ b/aea/agent_loop.py @@ -16,19 +16,18 @@ # limitations under the License. # # ------------------------------------------------------------------------------ - - """This module contains the implementation of an agent loop using asyncio.""" import asyncio import datetime from abc import ABC, abstractmethod from asyncio import CancelledError from asyncio.events import AbstractEventLoop +from asyncio.queues import Queue from asyncio.tasks import Task from contextlib import suppress from enum import Enum from functools import partial -from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union, cast from aea.abstract_agent import AbstractAgent from aea.configurations.constants import LAUNCH_SUCCEED_MESSAGE @@ -41,6 +40,8 @@ ) from aea.helpers.exec_timeout import ExecTimeoutThreadGuard, TimeoutException from aea.helpers.logging import WithLogger, get_logger +from aea.mail.base import Envelope, EnvelopeContext +from aea.protocols.base import Message class AgentLoopException(AEAException): @@ -91,6 +92,19 @@ def state(self) -> AgentLoopStates: """Get current main loop state.""" return self._state.get() + async def wait_state( + self, state_or_states: Union[Any, Sequence[Any]] + ) -> Tuple[Any, Any]: + """ + Wait state to be set. + + :param state_or_states: state or list of states. + + :return: tuple of previous state and new state. + """ + + return await self._state.wait(state_or_states) + @property def is_running(self) -> bool: """Get running state of the loop.""" @@ -101,7 +115,7 @@ def set_loop(self, loop: AbstractEventLoop) -> None: self._loop: AbstractEventLoop = loop def _setup(self) -> None: # pylint: disable=no-self-use - """Set up loop before started.""" + """Set up agent loop before started.""" # start and stop methods are classmethods cause one instance shared across muiltiple threads ExecTimeoutThreadGuard.start() @@ -112,7 +126,7 @@ def _teardown(self) -> None: # pylint: disable=no-self-use async def run(self) -> None: """Run agent loop.""" - self.logger.debug("agent loop started") + self.logger.debug("agent loop starting...") self._state.set(AgentLoopStates.starting) self._setup() self._set_tasks() @@ -149,6 +163,26 @@ def _stop_tasks(self) -> None: continue #  pragma: nocover task.cancel() + @abstractmethod + def send_to_skill( + self, + message_or_envelope: Union[Message, Envelope], + context: Optional[EnvelopeContext] = None, + ) -> None: + """ + Send message or envelope to another skill. + + :param message_or_envelope: envelope to send to another skill. + if message passed it will be wrapped into envelope with optional envelope context. + + :return: None + """ + + @property + @abstractmethod + def skill2skill_queue(self) -> Queue: + """Get skill to skill message queue.""" + class AsyncAgentLoop(BaseAgentLoop): """Asyncio based agent loop suitable only for AEA.""" @@ -166,11 +200,58 @@ def __init__( :param agent: AEA instance :param loop: asyncio loop to use. optional + :param threaded: is a new thread to be started for the agent loop """ super().__init__(agent=agent, loop=loop, threaded=threaded) self._agent: AbstractAgent = self._agent self._periodic_tasks: Dict[Callable, PeriodicCaller] = {} + self._skill2skill_message_queue: Optional[asyncio.Queue] = None + + def _setup(self) -> None: + """Set up agent loop before started.""" + self._skill2skill_message_queue = asyncio.Queue() + super()._setup() + + @property + def skill2skill_queue(self) -> Queue: + """Get skill to skill message queue.""" + if not self._skill2skill_message_queue: # pragma: nocover + raise ValueError("_skill2skill_message_queue is not set!") + return self._skill2skill_message_queue + + def send_to_skill( + self, + message_or_envelope: Union[Message, Envelope], + context: Optional[EnvelopeContext] = None, + ) -> None: + """ + Send message or envelope to another skill. + + :param message_or_envelope: envelope to send to another skill. + if message passed it will be wrapped into envelope with optional envelope context. + + :return: None + """ + if isinstance(message_or_envelope, Envelope): + envelope = message_or_envelope + message = cast(Message, envelope.message) + elif isinstance(message_or_envelope, Message): + message = message_or_envelope + envelope = Envelope( + to=message.to, sender=message.sender, message=message, context=context, + ) + else: + raise ValueError( + f"Unsupported message or envelope type: {type(message_or_envelope)}" + ) + + 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.skill2skill_queue.put_nowait(envelope) def _periodic_task_exception_callback( # pylint: disable=unused-argument self, task_callable: Callable, exc: Exception diff --git a/aea/cli/core.py b/aea/cli/core.py index 899901d3b9..96feecac3e 100644 --- a/aea/cli/core.py +++ b/aea/cli/core.py @@ -60,8 +60,6 @@ from aea.cli.transfer import transfer from aea.cli.upgrade import upgrade from aea.cli.utils.click_utils import registry_path_option -from aea.cli.utils.config import get_or_create_cli_config -from aea.cli.utils.constants import AUTHOR_KEY from aea.cli.utils.context import Context from aea.cli.utils.loggers import logger, simple_verbosity_option from aea.helpers.win32 import enable_ctrl_c_support @@ -96,36 +94,6 @@ def cli( enable_ctrl_c_support() -@cli.command() -@click.option("-p", "--port", default=8080) -@click.option("--local", is_flag=True, help="For using local folder.") -@click.pass_context -def gui( # pylint: disable=unused-argument - click_context: click.Context, port: int, local: bool -) -> None: # pragma: no cover - """Run the CLI GUI.""" - _init_gui() - import aea.cli_gui # pylint: disable=import-outside-toplevel,redefined-outer-name - - click.echo("Running the GUI.....(press Ctrl+C to exit)") - aea.cli_gui.run(port) - - -def _init_gui() -> None: - """ - Initialize GUI before start. - - :return: None - :raisees: ClickException if author is not set up. - """ - 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." - ) - - cli.add_command(_list) cli.add_command(add_key) cli.add_command(add) diff --git a/aea/cli/upgrade.py b/aea/cli/upgrade.py index f5c824af48..c1f0b0af56 100644 --- a/aea/cli/upgrade.py +++ b/aea/cli/upgrade.py @@ -511,12 +511,15 @@ def get_latest_versions(self) -> None: Stores the result in 'item_to_new_version'. """ for package_id in self.adjacency_list.keys(): - new_item = get_latest_version_available_in_registry( - self.ctx, - str(package_id.package_type), - package_id.public_id.to_latest(), - aea_version=self._current_aea_version, - ) + try: + new_item = get_latest_version_available_in_registry( + self.ctx, + str(package_id.package_type), + package_id.public_id.to_latest(), + aea_version=self._current_aea_version, + ) + except click.ClickException: + continue if package_id.public_id.version == new_item.version: continue new_version = new_item.version diff --git a/aea/cli/utils/context.py b/aea/cli/utils/context.py index e9aae87ced..ef2cbcbc60 100644 --- a/aea/cli/utils/context.py +++ b/aea/cli/utils/context.py @@ -124,6 +124,9 @@ def get_dependencies(self) -> Dependencies: :return a list of dependency version specification. e.g. ["gym >= 1.0.0"] """ dependencies = {} # type: Dependencies + + dependencies.update(self.agent_config.dependencies) + for protocol_id in self.agent_config.protocols: dependencies.update(self._get_item_dependencies(PROTOCOL, protocol_id)) diff --git a/aea/cli/utils/package_utils.py b/aea/cli/utils/package_utils.py index 998045436d..cd276e26d1 100644 --- a/aea/cli/utils/package_utils.py +++ b/aea/cli/utils/package_utils.py @@ -68,7 +68,7 @@ get_wallet_from_agent_config, private_key_verify_or_create, ) -from aea.crypto.ledger_apis import DEFAULT_LEDGER_CONFIGS, LedgerApis +from aea.crypto.ledger_apis import LedgerApis from aea.crypto.wallet import Wallet from aea.exceptions import AEAEnforceError from aea.helpers.base import compute_specifier_from_version, recursive_update @@ -635,7 +635,7 @@ def try_get_balance( # pylint: disable=unused-argument :retun: token balance. """ try: - if type_ not in DEFAULT_LEDGER_CONFIGS: # pragma: no cover + if not LedgerApis.has_ledger(type_): # pragma: no cover raise ValueError("No ledger api config for {} available.".format(type_)) address = wallet.addresses.get(type_) if address is None: # pragma: no cover diff --git a/aea/cli_gui/__init__.py b/aea/cli_gui/__init__.py deleted file mode 100644 index 51b0bbd579..0000000000 --- a/aea/cli_gui/__init__.py +++ /dev/null @@ -1,457 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ -"""Key pieces of functionality for CLI GUI.""" - -import glob -import os -import sys -import threading -from typing import Any, Dict, List, Tuple, Union - -import connexion -import flask -from click import ClickException - -from aea.cli.add import add_item as cli_add_item -from aea.cli.create import create_aea as cli_create_aea -from aea.cli.delete import delete_aea as cli_delete_aea -from aea.cli.fetch import fetch_agent_locally as cli_fetch_agent_locally -from aea.cli.list import list_agent_items as cli_list_agent_items -from aea.cli.registry.fetch import fetch_agent as cli_fetch_agent -from aea.cli.remove import remove_item as cli_remove_item -from aea.cli.scaffold import scaffold_item as cli_scaffold_item -from aea.cli.search import search_items as cli_search_items -from aea.cli.search import setup_search_ctx as cli_setup_search_ctx -from aea.cli.utils.config import try_to_load_agent_config -from aea.cli.utils.context import Context -from aea.cli.utils.formatting import sort_items -from aea.cli_gui.utils import ( - AppContext, - ProcessState, - call_aea_async, - get_process_status, - is_agent_dir, - read_error, - read_tty, - stop_agent_process, - terminate_processes, -) -from aea.common import JSONLike -from aea.configurations.base import PublicId -from aea.configurations.constants import AGENT, CONNECTION, CONTRACT, PROTOCOL, SKILL - - -elements = [ - ["local", AGENT, "localAgents"], - ["registered", PROTOCOL, "registeredProtocols"], - ["registered", CONNECTION, "registeredConections"], - ["registered", SKILL, "registeredSkills"], - ["local", PROTOCOL, "localProtocols"], - ["local", CONNECTION, "localConnections"], - ["local", CONTRACT, "localContracts"], - ["local", SKILL, "localSkills"], -] - - -max_log_lines = 100 - - -app_context = AppContext() - - -def get_agents() -> List[Dict]: - """Return list of all local agents.""" - file_list = glob.glob(os.path.join(app_context.agents_dir, "*")) - - agent_list = [] - - for path in file_list: - if is_agent_dir(path): - _head, tail = os.path.split(path) - agent_list.append( - { - "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 - # change it when we will have a separate view for an agent. - "description": "placeholder description", - } - ) - - return agent_list - - -def get_registered_items( - item_type: str, -) -> Union[Tuple[List[Dict[Any, Any]], int], Tuple[Dict[str, str], int]]: - """Create a new AEA project.""" - # need to place ourselves one directory down so the cher can find the packages - ctx = Context(cwd=app_context.agents_dir) - try: - cli_setup_search_ctx(ctx, local=app_context.local) - result, _ = cli_search_items(ctx, item_type, query="", page=1) - except ClickException: - return {"detail": "Failed to search items."}, 400 # 400 Bad request - else: - sorted_items = sort_items(result) - return sorted_items, 200 # 200 (Success) - - -def search_registered_items( - item_type: str, search_term: str -) -> Union[Tuple[str, int], Tuple[Dict[str, Any], int]]: - """Create a new AEA project.""" - # need to place ourselves one directory down so the searcher can find the packages - ctx = Context(cwd=app_context.agents_dir) - try: - cli_setup_search_ctx(ctx, local=app_context.local) - result, _ = cli_search_items(ctx, item_type, query=search_term, page=1) - except ClickException: - return {"detail": "Failed to search items."}, 400 # 400 Bad request - else: - sorted_items = sort_items(result) - response = { - "search_result": sorted_items, - "item_type": item_type, - "search_term": search_term, - } - return response, 200 # 200 (Success) - - -def create_agent(agent_id: str) -> Union[Tuple[str, int], Tuple[Dict[str, str], int]]: - """Create a new AEA project.""" - ctx = Context(cwd=app_context.agents_dir) - try: - cli_create_aea(ctx, agent_id, local=app_context.local) - except ClickException as e: - return ( - {"detail": "Failed to create Agent. {}".format(str(e))}, - 400, - ) # 400 Bad request - else: - return agent_id, 201 # 201 (Created) - - -def delete_agent(agent_id: str) -> Union[Tuple[str, int], Tuple[Dict[str, str], int]]: - """Delete an existing AEA project.""" - ctx = Context(cwd=app_context.agents_dir) - try: - cli_delete_aea(ctx, agent_id) - except ClickException: - return ( - {"detail": "Failed to delete Agent {} - it may not exist".format(agent_id)}, - 400, - ) # 400 Bad request - else: - return "Agent {} deleted".format(agent_id), 200 # 200 (OK) - - -def add_item( - agent_id: str, item_type: str, item_id: str -) -> Union[Tuple[str, int], Tuple[Dict[str, str], int]]: - """Add a protocol, skill or connection to the register to a local agent.""" - ctx = Context(cwd=os.path.join(app_context.agents_dir, agent_id)) - ctx.set_config("is_local", app_context.local) - try: - try_to_load_agent_config(ctx) - cli_add_item(ctx, item_type, PublicId.from_str(item_id)) - except ClickException as e: - return ( - { - "detail": "Failed to add {} {} to agent {}. {}".format( - item_type, item_id, agent_id, str(e) - ) - }, - 400, - ) # 400 Bad request - else: - return agent_id, 201 # 200 (OK) - - -def fetch_agent(agent_id: str) -> Union[Tuple[str, int], Tuple[Dict[str, str], int]]: - """Fetch an agent.""" - ctx = Context(cwd=app_context.agents_dir) - 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) - except ClickException as e: - return ( - {"detail": "Failed to fetch an agent {}. {}".format(agent_id, str(e))}, - 400, - ) # 400 Bad request - else: - return agent_public_id.name, 201 # 200 (OK) - - -def remove_local_item( - agent_id: str, item_type: str, item_id: str -) -> Union[Tuple[str, int], Tuple[Dict[str, str], int]]: - """Remove a protocol, skill or connection from a local agent.""" - agent_dir = os.path.join(app_context.agents_dir, agent_id) - ctx = Context(cwd=agent_dir) - try: - try_to_load_agent_config(ctx) - cli_remove_item(ctx, item_type, PublicId.from_str(item_id)) - except ClickException: - return ( - { - "detail": "Failed to remove {} {} from agent {}".format( - item_type, item_id, agent_id - ) - }, - 400, - ) # 400 Bad request - else: - return agent_id, 201 # 200 (OK) - - -def get_local_items(agent_id: str, item_type: str) -> Tuple[List[Dict[Any, Any]], int]: - """ - Return a list of protocols, skills or connections supported by a local agent. - - :param agent_id: the id of the agent - :param item_type: the type of item - """ - if agent_id == "NONE": - return [], 200 # 200 (Success) - - # need to place ourselves one directory down so the searcher can find the packages - ctx = Context(cwd=os.path.join(app_context.agents_dir, agent_id)) - try: - try_to_load_agent_config(ctx) - result = cli_list_agent_items(ctx, item_type) - except ClickException: - return [{"detail": "Failed to list agent items."}], 400 # 400 Bad request - else: - sorted_items = sort_items(result) - return sorted_items, 200 # 200 (Success) - - -def scaffold_item( - agent_id: str, item_type: str, item_id: str -) -> Union[Tuple[str, int], Tuple[Dict[str, str], int]]: - """Scaffold a moslty empty item on an agent (either protocol, skill or connection).""" - agent_dir = os.path.join(app_context.agents_dir, agent_id) - ctx = Context(cwd=agent_dir) - try: - try_to_load_agent_config(ctx) - cli_scaffold_item(ctx, item_type, item_id) - except ClickException: - return ( - { - "detail": "Failed to scaffold a new {} in to agent {}".format( - item_type, agent_id - ) - }, - 400, - ) # 400 Bad request - else: - return agent_id, 201 # 200 (OK) - - -def start_agent( - agent_id: str, connection_id: PublicId -) -> Union[Tuple[str, int], Tuple[Dict[str, str], int]]: - """Start a local agent running.""" - # Test if it is already running in some form - if agent_id in app_context.agent_processes: - if ( - get_process_status(app_context.agent_processes[agent_id]) - != ProcessState.RUNNING - ): # pragma: no cover - if app_context.agent_processes[agent_id] is not None: - app_context.agent_processes[agent_id].terminate() - app_context.agent_processes[agent_id].wait() - del app_context.agent_processes[agent_id] - del app_context.agent_tty[agent_id] - del app_context.agent_error[agent_id] - else: - return ( - {"detail": "Agent {} is already running".format(agent_id)}, - 400, - ) # 400 Bad request - - agent_dir = os.path.join(app_context.agents_dir, agent_id) - - if connection_id is not None and connection_id != "": - connections = get_local_items(agent_id, CONNECTION)[0] - has_named_connection = False - for element in connections: - if element["public_id"] == connection_id: - has_named_connection = True - if has_named_connection: - agent_process = call_aea_async( - [ - sys.executable, - "-m", - "aea.cli", - "run", - "--connections", - str(connection_id), - ], - agent_dir, - ) - else: - return ( - { - "detail": "Trying to run agent {} with non-existent connection: {}".format( - agent_id, connection_id - ) - }, - 400, - ) # 400 Bad request - else: - agent_process = call_aea_async( - [sys.executable, "-m", "aea.cli", "run", "--install-deps"], agent_dir - ) - - if agent_process is None: - return ( - {"detail": "Failed to run agent {}".format(agent_id)}, - 400, - ) # 400 Bad request - app_context.agent_processes[agent_id] = agent_process - app_context.agent_tty[agent_id] = [] - app_context.agent_error[agent_id] = [] - - # we don't seem to ever join this - 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() - - # we don't seem to ever join this - 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) - - -def get_agent_status(agent_id: str) -> Tuple[JSONLike, int]: - """Get the status of the running agent Node.""" - status_str = str(ProcessState.NOT_STARTED).replace("ProcessState.", "") - tty_str = "" - error_str = "" - - # agent_id will not be in lists if we haven't run it yet - if ( - agent_id in app_context.agent_processes - and app_context.agent_processes[agent_id] is not None - ): - status_str = str( - get_process_status(app_context.agent_processes[agent_id]) - ).replace("ProcessState.", "") - - if agent_id in app_context.agent_tty: - total_num_lines = len(app_context.agent_tty[agent_id]) - for i in range(max(0, total_num_lines - max_log_lines), total_num_lines): - tty_str += app_context.agent_tty[agent_id][i] - - else: - tty_str = "" - - tty_str = tty_str.replace("\n", "
") - - if agent_id in app_context.agent_error: - total_num_lines = len(app_context.agent_error[agent_id]) - for i in range(max(0, total_num_lines - max_log_lines), total_num_lines): - error_str += app_context.agent_error[agent_id][i] - - else: - error_str = "" - - error_str = error_str.replace("\n", "
") - - return {"status": status_str, "tty": tty_str, "error": error_str}, 200 # (OK) - - -def stop_agent(agent_id: str) -> Tuple[str, int]: - """Stop agent running.""" - # pass to private function to make it easier to mock - return stop_agent_process(agent_id, app_context) - - -def create_app() -> connexion.FlaskApp: - """Run the flask server.""" - CUR_DIR = os.path.abspath(os.path.dirname(__file__)) - app = connexion.FlaskApp(__name__, specification_dir=CUR_DIR) - global app_context # pylint: disable=global-statement - app_context = AppContext() - - app_context.agent_processes = {} - app_context.agent_tty = {} - app_context.agent_error = {} - app_context.ui_is_starting = False - app_context.agents_dir = os.path.abspath(os.getcwd()) - app_context.module_dir = os.path.join( - os.path.abspath(os.path.dirname(__file__)), "../../" - ) - - app.add_api("aea_cli_rest.yaml") - - @app.route("/") - def home() -> str: # pylint: disable=unused-variable - """Respond to browser URL: localhost:5000/.""" - return flask.render_template( - "home.html", len=len(elements), htmlElements=elements - ) - - @app.route("/static/js/home.js") - def homejs() -> str: # pylint: disable=unused-variable - """Serve the home.js file (as it needs templating).""" - return flask.render_template( - "home.js", len=len(elements), htmlElements=elements - ) - - @app.route("/favicon.ico") - def favicon() -> str: # pylint: disable=unused-variable - """Return an icon to be displayed in the browser.""" - return flask.send_from_directory( - os.path.join(app.root_path, "static"), - "favicon.ico", - mimetype="image/vnd.microsoft.icon", - ) - - return app - - -def run(port: int, host: str = "127.0.0.1") -> connexion.FlaskApp: - """Run the GUI.""" - - app = create_app() - try: - app.run(host=host, port=port, debug=False) - finally: - terminate_processes() - - return app - - -def run_test() -> connexion.FlaskApp: - """Run the gui in the form where we can run tests against it.""" - app = create_app() - return app.app.test_client() diff --git a/aea/cli_gui/__main__.py b/aea/cli_gui/__main__.py deleted file mode 100644 index 7bed31763d..0000000000 --- a/aea/cli_gui/__main__.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Main entry point for CLI GUI.""" # pragma: no cover - -import argparse # pragma: no cover - -import aea.cli_gui # pragma: no cover - - -parser = argparse.ArgumentParser( - description="Launch the gui through python" -) # pragma: no cover -parser.add_argument( - "-p", "--port", help="Port that the web server listens on", type=int, default=8080 -) # pragma: no cover - -parser.add_argument( - "-H", - "--host", - help="host that the web server serves from", - type=str, - default="127.0.0.1", -) # pragma: no cover - -args, unknown = parser.parse_known_args() # pragma: no cover - -# If we're running in stand alone mode, run the application -if __name__ == "__main__": # pragma: no cover - aea.cli_gui.run(args.port, args.host) diff --git a/aea/cli_gui/aea_cli_rest.yaml b/aea/cli_gui/aea_cli_rest.yaml deleted file mode 100644 index 5bde8807ef..0000000000 --- a/aea/cli_gui/aea_cli_rest.yaml +++ /dev/null @@ -1,343 +0,0 @@ -swagger: "2.0" -info: - description: This is the swagger file that goes with our server code - version: "1.0.0" - title: Swagger Rest Article -consumes: - - application/json -produces: - - application/json - -basePath: /api - -# Paths supported by the server application -paths: - /agent: - get: - operationId: aea.cli_gui.get_agents - tags: - - agents - summary: Return list of all aea projects - description: List of local folders under the user name (which correspond to aea projects) - responses: - 200: - description: Successfully read agent list operation - schema: - type: array - items: - type: string - post: - operationId: aea.cli_gui.create_agent - tags: - - People - summary: Create a new AEA project (an agent) - parameters: - - name: agent_id - in: body - description: Name of aea project to create - required: True - schema: - type: string - - responses: - 201: - description: Successfully created person in list - - 400: - description: Cannot create agent - schema: - type: string - - /agent/{agent_id}: - delete: - operationId: aea.cli_gui.delete_agent - tags: - - agents - summary: Delete an aea project - parameters: - - name: agent_id - in: path - description: id of agent to delete - type: string - required: True - responses: - 200: - description: Agent deleted successfully - schema: - type: string - - 400: - description: Cannot delete agent - schema: - type: string - - /agent/{agent_id}/{item_type}: - post: - operationId: aea.cli_gui.add_item - tags: - - agents - summary: Fetch a protocol from the registry to the currently selected agent - parameters: - - name: agent_id - in: path - description: id of agent to add protocol to - type: string - required: True - - name: item_type - in: path - description: type of item to add ("protocol", "connection" or "skill") - type: string - required: True - - name: item_id - in: body - description: id of protocol to add - schema: - type: string - required: True - responses: - 201: - description: Protocol added successfully - schema: - type: string - - 400: - description: Cannot add protocol to agent - schema: - type: string - get: - operationId: aea.cli_gui.get_local_items - tags: - - agents - summary: Return list of all items if a given type supported by this agent - parameters: - - name: agent_id - in: path - description: id of agent to searh in - type: string - required: True - - name: item_type - in: path - description: type of item to list ("protocol", "connection" or "skill") - type: string - required: True - - responses: - 200: - description: Successfully read protocol list operation - schema: - type: array - items: - type: string - 400: - description: Cannot find protocols in agent - schema: - type: string - - /agent/{agent_id}/{item_type}/remove: - post: - operationId: aea.cli_gui.remove_local_item - tags: - - agents - summary: Delete a protocol, connection or skill from an agent - parameters: - - name: agent_id - in: path - description: id of agent to delete - type: string - required: True - - name: item_type - in: path - description: type of item to remove - type: string - required: True - - name: item_id - in: body - description: id of item to remove - schema: - type: string - required: True - - responses: - 201: - description: Agent deleted successfully - schema: - type: string - - 400: - description: Cannot delete agent - schema: - type: string - - /agent/{agent_id}/{item_type}/scaffold: - post: - operationId: aea.cli_gui.scaffold_item - tags: - - agents - summary: Scaffold a new (mostly empty) item (either a protocol, connection or skill) on to an agent - parameters: - - name: agent_id - in: path - description: id of agent to delete - type: string - required: True - - name: item_type - in: path - description: type of item to remove - type: string - required: True - - name: item_id - in: body - description: id of item to scaffold - schema: - type: string - required: True - - responses: - 201: - description: Agent deleted successfully - schema: - type: string - - 400: - description: Cannot delete agent - schema: - type: string - - /fetch-agent: - post: - operationId: aea.cli_gui.fetch_agent - tags: - - agents - summary: Fetch an agent from the registry - parameters: - - name: agent_id - in: body - description: id of agent to fetch - schema: - type: string - required: True - responses: - 201: - description: Agent fetched successfully - schema: - type: string - - 400: - description: Cannot fetch agent - schema: - type: string - - /{item_type}: - get: - operationId: aea.cli_gui.get_registered_items - tags: - - agents - summary: Return list of all registered items (protocols, connections, skills) - parameters: - - name: item_type - in: path - description: type of item to remove - type: string - required: True - responses: - 200: - description: Successfully read item list operation - schema: - type: array - items: - type: string - - /{item_type}/{search_term}: - get: - operationId: aea.cli_gui.search_registered_items - tags: - - agents - summary: Return list of all registered items (protocols, connections, skills) - parameters: - - name: item_type - in: path - description: type of item to remove - type: string - required: True - - name: search_term - in: path - description: type of item to remove - type: string - required: True - responses: - 200: - description: Successfully read item list operation - schema: - type: object - - /agent/{agent_id}/run: - post: - operationId: aea.cli_gui.start_agent - tags: - - agents - summary: Start an agent - parameters: - - name: agent_id - in: path - description: id of agent to run - type: string - required: True - - name: connection_id - in: body - description: id f the connection to activate when running - schema: - type: string - required: True - responses: - 201: - description: Start the agent - schema: - type: string - 400: - description: Cannot start agent - schema: - type: string - get: - operationId: aea.cli_gui.get_agent_status - tags: - - agents - summary: Get status of a running agent - parameters: - - name: agent_id - in: path - description: get status of agent - type: string - required: True - responses: - 200: - description: successfully got status data - schema: - type: string - - 400: - description: Cannot get status data - schema: - type: string - - delete: - operationId: aea.cli_gui.stop_agent - tags: - - agents - summary: Stops an agent - parameters: - - name: agent_id - in: path - description: id of agent to stop - type: string - required: True - - responses: - 200: - description: successfully started agent - schema: - type: string - - 400: - description: Cannot stop agent - schema: - type: string diff --git a/aea/cli_gui/static/css/home.css b/aea/cli_gui/static/css/home.css deleted file mode 100644 index e392c656a6..0000000000 --- a/aea/cli_gui/static/css/home.css +++ /dev/null @@ -1,157 +0,0 @@ -hr { - border: 1px solid #FFFFFF; - opacity: 1; -} - -nav { - background: #1E2844 0% 0% no-repeat padding-box; - height: 80px; -} - -body { - background: #161E33 0% 0% no-repeat padding-box; -} - -table { - color: #A1B6C0 !important; - border-color: #A1B6C0; -} - -thead { - color: #FFFFFF; -} - -button { - height: 26px; - border-radius: 2px; - text-align: center; - letter-spacing: -0.14px; - color: #FFFFFF; - border: none; -} - -button:hover { - color: #161E33; - background: #FFFFFF; -} - -input[type="text"] { - height: 23px; - border-radius: 2px; - border: 1px solid #FFFFFF; - color: #FFFFFF; - background: transparent; - text-indent: 4px; -} - -input[type="radio"] { - width: 19px; - height: 19px; - border: 1px solid #FFFFFF; -} - -input:focus { - outline: none !important; -} - -::selection { - color: #FFFFFF; - background: #b3d7ff; -} - -/* header items */ -.fetchai-logo { - width: auto; - margin-left: 40px; - top: 20px; - left: 47px; - width: 164px; - height: 51px; -} - -.app-title { - text-align: right; - letter-spacing: -0.2px; - color: #FFFFFF !important; -} - -/* boxes */ -.app-box { - border-radius: 4px; -} - -.app-box-green { - background: rgba(0, 185, 162, 0.1) 0% 0% no-repeat padding-box; -} - -.app-box-blue { - background: rgba(76, 130, 220, 0.1) 0% 0% no-repeat padding-box; -} - -.app-box-violet { - background: rgba(164, 90, 149, 0.1) 0% 0% no-repeat padding-box; -} - -.box-title { - text-align: center; - letter-spacing: -0.18px; -} - -/* buttons */ -.app-btn-green { - background-color: rgba(0, 185, 162, 1); -} - -.app-btn-blue { - background-color: rgba(76, 130, 220, 1); -} - -.app-btn-s { - width: 82px; -} - -.app-btn-m { - width: 110px; -} - -.app-btn-l { - width: 172px; -} - -/* inputs */ -.app-create-agent-input { - width: 183px; -} - -.app-search-input { - width: 203px; -} - -.app-connection-id-input { - width: 183px; -} - -/* miscellaneous */ -.app-output { - height: 230px; - border: 1px solid #FFFFFF; - border-radius: 4px; - overflow: auto !important; -} - -.error { - position: fixed; - /* Sit on top of the page content */ - width: 100%; - /* Full width (cover the whole page) */ - height: 10%; - /* A bit of the page */ - bottom: 0; - visibility: hidden; - border: 1px solid lightgrey; - border-radius: 0px; - background-color: #fbbd; - text-align: center; - font-weight: bold; - opacity: 50%; -} diff --git a/aea/cli_gui/static/favicon.ico b/aea/cli_gui/static/favicon.ico deleted file mode 100644 index 7b45f6eedb..0000000000 Binary files a/aea/cli_gui/static/favicon.ico and /dev/null differ diff --git a/aea/cli_gui/static/logo.png b/aea/cli_gui/static/logo.png deleted file mode 100644 index 051c4501f5..0000000000 Binary files a/aea/cli_gui/static/logo.png and /dev/null differ diff --git a/aea/cli_gui/templates/home.html b/aea/cli_gui/templates/home.html deleted file mode 100644 index 104c61a51e..0000000000 --- a/aea/cli_gui/templates/home.html +++ /dev/null @@ -1,276 +0,0 @@ - - - - - - AEA GUI - - - - - - - - - - - -

- - -
- - -
- -
- - -
-
-

Local

-
- - -
-
-
- -
-
- -
-
- -
-
-
- -
- - -
Local agents
-
- - - - - - -
Agent nameDescription
-
- - Selected agent: NONE -
-
- -
- -
- - -
-
-

Registry

-
-
Search in Registry
- - -
- - -
-
- -
-
- -
-
- -
-
- - -
-
-
- -
-
- -
-
- - -
-
- -
-
- -
-
- -
-
- -
- -
- - -
Search results
-
- - - - - - - -
NONE IDDescription
- -
- - -
-
-
- -
-
- -
-
- -
-
-
- -
-
- -
- -
- - -
- -
- - -
-
- - - {% for i in range(0, len) %} - {% if htmlElements[i][0] == "local" and htmlElements[i][1] != "agent" %} -
- -

NONE's {{htmlElements[i][1]}}s

- - - - - - -
{{htmlElements[i][1]}} IDDescription
- - -
-
-
- -
-
- -
-
-
- -
- {% endif %} - {% endfor %} - -
-
- -
- -
- - -
-
-

Run "NONE" agent

-
- - -
-
-
- -
- - - -
-
- - -
-
-
- -
-
-
- - -
- Agent status: NONE -
- - -
- - -
-
-
- -
- -
- -
- - -

- - - - - - - - - - diff --git a/aea/cli_gui/templates/home.js b/aea/cli_gui/templates/home.js deleted file mode 100644 index 849e937414..0000000000 --- a/aea/cli_gui/templates/home.js +++ /dev/null @@ -1,696 +0,0 @@ -/* - * JavaScript file for the application to demonstrate - * using the API - */ - -// Create the namespace instance -var ns = {}; - -// Get elements passed in from python as this is how they need to be passed into the html -// so best to have it in one place -var elements = [] - -{%for i in range(0, len)%} - elements.push({"location": "{{htmlElements[i][0]}}", "type": "{{htmlElements[i][1]}}", "combined": "{{htmlElements[i][2]}}"}); - {% endfor %} - - - -'use strict'; - -class Model{ - constructor(){ - this.$event_pump = $('body'); - } - - readData(element) { - // This is massively hacky! - if (element["location"] == "local" && element["type"] != "agent"){ - return; - } - var ajax_options = { - type: 'GET', - url: 'api/' + element["type"], - accepts: 'application/json', - dataType: 'json' - }; - var self = this; - $.ajax(ajax_options) - .done(function(data) { - self.$event_pump.trigger('model_' + element["combined"] + 'ReadSuccess', [data]); - }) - .fail(function(xhr, textStatus, errorThrown) { - self.$event_pump.trigger('model_error', [xhr, textStatus, errorThrown]); - }) - } - - searchItems(itemType, searchTerm){ - var ajax_options = { - type: 'GET', - url: 'api/' + itemType + "/" + searchTerm, - accepts: 'application/json', - dataType: 'json' - }; - var self = this; - $.ajax(ajax_options) - .done(function(data) { - self.$event_pump.trigger('model_searchReadSuccess', [data]); - }) - .fail(function(xhr, textStatus, errorThrown) { - self.$event_pump.trigger('model_error', [xhr, textStatus, errorThrown]); - }) - } - - readAgentStatus(agentId) { - var ajax_options = { - type: 'GET', - url: 'api/agent/' + agentId + '/run', - accepts: 'application/json', - contentType: 'plain/text' - }; - var self = this; - $.ajax(ajax_options) - .done(function(data) { - self.$event_pump.trigger('model_AgentStatusReadSuccess', [data]); - }) - .fail(function(xhr, textStatus, errorThrown) { - self.$event_pump.trigger('model_error', [xhr, textStatus, errorThrown]); - }) - } - - createItem(element, id){ - var ajax_options = { - type: 'POST', - url: 'api/' + element["type"], - accepts: 'application/json', - contentType: 'application/json', - dataType: 'json', - data: JSON.stringify(id) - }; - var self = this; - $.ajax(ajax_options) - .done(function(data) { - self.$event_pump.trigger('model_' + element["combined"] + 'CreateSuccess', [data]); - }) - .fail(function(xhr, textStatus, errorThrown) { - self.$event_pump.trigger('model_error', [xhr, textStatus, errorThrown]); - }) - } - - deleteItem(element, id){ - var ajax_options = { - type: 'DELETE', - url: 'api/' + element["type"] +'/' + id, - accepts: 'application/json', - contentType: 'plain/text' - }; - var self = this; - $.ajax(ajax_options) - .done(function(data) { - self.$event_pump.trigger('model_' + element["combined"] + 'DeleteSuccess', [data]); - }) - .fail(function(xhr, textStatus, errorThrown) { - self.$event_pump.trigger('model_error', [xhr, textStatus, errorThrown]); - }) - } - - addItem(element, agentId, itemId) { - var propertyName = element["type"] + "_id" - var ajax_options = { - type: 'POST', - url: 'api/agent/' + agentId + '/' + element["type"], - accepts: 'application/json', - contentType: 'application/json', - dataType: 'json', - data: JSON.stringify(itemId) - }; - var self = this; - $.ajax(ajax_options) - .done(function(data) { - self.$event_pump.trigger('model_' + element["combined"] + 'AddSuccess', [data]); - }) - .fail(function(xhr, textStatus, errorThrown) { - self.$event_pump.trigger('model_error', [xhr, textStatus, errorThrown]); - }) - } - - fetchAgent(agentId) { - var ajax_options = { - type: 'POST', - url: 'api/fetch-agent', - accepts: 'application/json', - contentType: 'application/json', - dataType: 'json', - data: JSON.stringify(agentId) - }; - var self = this; - $.ajax(ajax_options) - .done(function(data) { - var element = {"type": $("#searchItemTypeSelected").html(), "combined": "localSkills"} - self.$event_pump.trigger('model_' + element["combined"] + 'AddSuccess', [data]); - }) - .fail(function(xhr, textStatus, errorThrown) { - self.$event_pump.trigger('model_error', [xhr, textStatus, errorThrown]); - }) - } - - - removeItem(element, agentId, itemId) { - var propertyName = element["type"] + "_id" - var ajax_options = { - type: 'POST', - url: 'api/agent/' + agentId + '/' + element["type"]+ '/remove', - accepts: 'application/json', - contentType: 'application/json', - dataType: 'json', - data: JSON.stringify(itemId) - }; - var self = this; - $.ajax(ajax_options) - .done(function(data) { - self.$event_pump.trigger('model_' + element["combined"] + 'RemoveSuccess', [data]); - }) - .fail(function(xhr, textStatus, errorThrown) { - self.$event_pump.trigger('model_error', [xhr, textStatus, errorThrown]); - }) - } - - readLocalData(element, agentId) { - var ajax_options = { - type: 'GET', - url: 'api/agent/'+agentId+'/' + element["type"], - accepts: 'application/json', - dataType: 'json' - }; - var self = this; - $.ajax(ajax_options) - .done(function(data) { - self.$event_pump.trigger('model_' + element["combined"] + 'ReadSuccess', [data]); - }) - .fail(function(xhr, textStatus, errorThrown) { - self.$event_pump.trigger('model_error', [xhr, textStatus, errorThrown]); - }) - } - scaffoldItem(element, agentId, itemId){ - var ajax_options = { - type: 'POST', - url: 'api/agent/' + agentId + "/" + element["type"] + "/scaffold", - accepts: 'application/json', - contentType: 'application/json', - dataType: 'json', - data: JSON.stringify(itemId) - }; - var self = this; - $.ajax(ajax_options) - .done(function(data) { - self.$event_pump.trigger('model_' + element["combined"] + 'ScaffoldSuccess', [data]); - }) - .fail(function(xhr, textStatus, errorThrown) { - self.$event_pump.trigger('model_error', [xhr, textStatus, errorThrown]); - }) - } - startAgent(agentId, runConnectionId){ - var ajax_options = { - type: 'POST', - url: 'api/agent/' + agentId + '/run', - accepts: 'application/json', - contentType: 'application/json', - dataType: 'json', - data: JSON.stringify(runConnectionId) - }; - var self = this; - $.ajax(ajax_options) - .done(function(data) { - self.$event_pump.trigger('model_StartAgentSuccess', [data]); - }) - .fail(function(xhr, textStatus, errorThrown) { - self.$event_pump.trigger('model_error', [xhr, textStatus, errorThrown]); - }) - } - stopAgent(agentId){ - var ajax_options = { - type: 'DELETE', - url: 'api/agent/' + agentId + '/run', - accepts: 'application/json', - contentType: 'plain/text' - }; - var self = this; - $.ajax(ajax_options) - .done(function(data) { - self.$event_pump.trigger('model_StopAgentSuccess', [data]); - }) - .fail(function(xhr, textStatus, errorThrown) { - self.$event_pump.trigger('model_error', [xhr, textStatus, errorThrown]); - }) - } - - -} - -class View{ - constructor(){ - this.$event_pump = $('body'); - - } - setAgentStatus(status){ - $('#agentStatus').html(status); - } - setAgentTTY(tty){ - $('#agentTTY').html(tty); - $('#agentTTY').scrollTop($('#agentTTY')[0].scrollHeight); - } - setAgentError(error){ - $('#agentError').html(error); - $('#agentError').scrollTop($('#agentError')[0].scrollHeight); - } - setSearchType(itemType){ - $('#searchItemTypeTable').html(itemType); - $('#searchItemTypeSelected').html(itemType); - } - - setCreateId(tag, id) { - $('#'+tag+'CreateId').val(id); - } - - setSelectedId(tag, id) { - $('#'+tag+'SelectionId').html(id); - } - - setScaffoldId(tag, id) { - $('#'+tag+'ScaffoldId').val(id); - } - - build_table(data, tableName) { - var rows = '' - - // clear the table - $('.' + tableName + ' table > tbody').empty(); - - // did we get a people array? - if (tableName) { - for (let i=0, l=data.length; i < l; i++) { - rows += `${data[i].public_id}${data[i].description}`; - } - $('.' + tableName + ' table > tbody').append(rows); - } - } - - error(error_msg) { - $('.error') - .html("
" + error_msg) - .css('visibility', 'visible'); - setTimeout(function() { - $('.error').css('visibility', 'hidden'); - }, 3000) - } - -} - -class Controller{ - constructor(m, v){ - this.model = m; - this.view = v; - this.$event_pump = $('body'); - - // Get the data from the model after the controller is done initializing - var self = this; - setTimeout(function() { - for (var i = 0; i < elements.length; ++i){ - self.model.readData(elements[i]); - } - }, 100) - - // Go through each of the element types setting up call-back and table building functions on the - // Items which exist - var self = this; - for (var i = 0; i < elements.length; i++) { - var element = elements[i] - var combineName = element["combined"] - $('#' + combineName + 'Create').click({el: element}, function(e){ - var id =$('#' + e.data.el["combined"] + 'CreateId').val(); - - e.preventDefault(); - - if (self.validateId(id)){ - self.model.createItem(e.data.el, id) - } else { - alert('Error: Problem with id'); - } - }); - - $('#' + combineName + 'Delete').click({el: element}, function(e) { - var id =$('#' + e.data.el["combined"] + 'SelectionId').html(); - if (confirm("This will completely remove agent: " + id + "'s code and is non-recoverable. Press OK to do this - otherwise press cancel")){ - - e.preventDefault(); - - if (self.validateId(id)) { - self.model.deleteItem(e.data.el, id) - self.view.setSelectedId(e.data.el["combined"], "NONE") - } else { - alert('Error: Problem with selected id'); - } - e.preventDefault(); - } - }); - - $('#' + combineName + 'Add').click({el: element}, function(e) { - var agentId = $('#localAgentsSelectionId').html(); - var itemId =$('#' + e.data.el["combined"] + 'SelectionId').html(); - - e.preventDefault(); - - if (self.validateId(agentId) && self.validateId(itemId) ) { - self.model.addItem(e.data.el, agentId, itemId) - self.view.setSelectedId(e.data.el["combined"], "NONE") - var tableBody = $("."+ e.data.el["combined"] +"registeredTable"); - self.clearTable(tableBody); - - - } else { - alert('Error: Problem with one of the selected ids (either agent or ' + element['type']); - } - e.preventDefault(); - }); - $('#' + combineName + 'Remove').click({el: element}, function(e) { - var agentId = $('#localAgentsSelectionId').html(); - var itemId =$('#' + e.data.el["combined"] + 'SelectionId').html(); - - e.preventDefault(); - - if (self.validateId(agentId) && self.validateId(itemId) ) { - self.model.removeItem(e.data.el, agentId, itemId) - self.view.setSelectedId(e.data.el["combined"], "NONE") - - - } else { - alert('Error: Problem with one of the selected ids (either agent or ' + element['type']); - } - e.preventDefault(); - }); - - $('.' + combineName + ' table > tbody ').on('click', 'tr', {el: element}, function(e) { - - var $target = $(e.target), - id, - description; - - id = $target - .parent() - .find('td.id') - .text(); - - - self.view.setSelectedId(e.data.el["combined"], id); - - // Select the appropriate row - var tableBody = $(e.target).closest("."+ e.data.el["combined"] +"registeredTable"); - self.clearTable(tableBody); - - $(this).addClass("aea_selected") - if (e.data.el["combined"] == "localAgents"){ - self.refreshAgentData(id) - } - self.handleButtonStates() - }); - - $('#' + combineName + 'Scaffold').click({el: element}, function(e){ - var agentId = $('#localAgentsSelectionId').html(); - var itemId =$('#' + e.data.el["combined"] + 'ScaffoldId').val(); - - e.preventDefault(); - - if (self.validateId(agentId) && self.validateId(itemId)){ - self.model.scaffoldItem(e.data.el, agentId, itemId) - } else { - alert('Error: Problem with id'); - } - }); - - // Handle the model events - this.$event_pump.on('model_'+ combineName + 'ReadSuccess', {el: element}, function(e, data) { - self.view.build_table(data, e.data.el["combined"]); - }); - - this.$event_pump.on('model_'+ combineName + 'CreateSuccess', {el: element}, function(e, data) { - self.model.readData(e.data.el); - self.view.setSelectedId(e.data.el["combined"], data) - self.view.setCreateId(e.data.el["combined"], "") - self.refreshAgentData(data) - self.handleButtonStates() - }); - - this.$event_pump.on('model_'+ combineName + 'DeleteSuccess', {el: element}, function(e, data) { - self.model.readData(e.data.el); - - self.refreshAgentData("NONE") - self.handleButtonStates() - - }); - this.$event_pump.on('model_'+ combineName + 'AddSuccess', {el: element}, function(e, data) { - self.refreshAgentData(data) - self.handleButtonStates() - - }); - this.$event_pump.on('model_'+ combineName + 'RemoveSuccess', {el: element}, function(e, data) { - self.refreshAgentData(data) - self.handleButtonStates() - - }); - this.$event_pump.on('model_'+ combineName + 'ScaffoldSuccess', {el: element}, function(e, data) { - self.refreshAgentData(data) - self.view.setScaffoldId(e.data.el["combined"], "") - self.handleButtonStates() - }); - - } - - this.$event_pump.on('model_AgentStatusReadSuccess', function(e, data) { - self.view.setAgentStatus("Agent Status: " + data["status"]) - self.view.setAgentTTY(data["tty"]) - self.view.setAgentError(data["error"]) - self.handleButtonStates() - }); - - this.$event_pump.on('model_searchReadSuccess', function(e, data) { - self.view.setSearchType(data["item_type"]) - self.view.build_table(data["search_result"], 'searchItemsTable'); - self.handleButtonStates() - }); - - $('#startAgent').click({el: element}, function(e) { - e.preventDefault(); - var agentId = $('#localAgentsSelectionId').html() - var connectionId = $('#runConnectionId').val() - if (self.validateId(agentId)){ - self.model.startAgent(agentId, connectionId) - } - else{ - alert('Error: Attempting to start agent with ID: ' + agentId); - } - - e.preventDefault(); - }); - $('#stopAgent').click({el: element}, function(e) { - e.preventDefault(); - var agentId = $('#localAgentsSelectionId').html() - if (self.validateId(agentId)){ - self.model.stopAgent(agentId) - } - else{ - alert('Error: Attempting to stop agent with ID: ' + agentId); - } - - e.preventDefault(); - }); - $('#searchInputButton').click({el: element}, function(e) { - e.preventDefault(); - var searchTerm = $('#searchInput').val() - if (self.validateId(searchTerm)){ - var itemType = $("input[name='itemType']:checked").attr('id') - self.model.searchItems(itemType, searchTerm) - } - else{ - alert('Error: Attempting to stop search for: ' + searchTerm); - } - - e.preventDefault(); - }); - - $('.searchItemsTable table > tbody ').on('click', 'tr', {el: element}, function(e) { - - var $target = $(e.target), - id, - description; - - id = $target - .parent() - .find('td.id') - .text(); - - - self.view.setSelectedId("searchItemsTable", id); - - // Select the appropriate row - var tableBody = $(e.target).closest(".searchItemsTableRegisteredTable"); - self.clearTable(tableBody); - - $(this).addClass("aea_selected") - - self.handleButtonStates() - }); - - - $('#searchItemsAdd').click({el: element}, function(e) { - var agentId = $('#localAgentsSelectionId').html(); - var itemId = $('#searchItemsTableSelectionId').html(); - // It doesn't matter too much what the combined name is as long as it exists - var itemType = {"type": $("#searchItemTypeSelected").html(), "combined": "localSkills"} - - e.preventDefault(); - - if (self.validateId(agentId) && self.validateId(itemId) ) { - self.model.addItem(itemType, agentId, itemId) - self.view.setSelectedId("searchItemsTable", "NONE") - var tableBody = $(e.target).closest(".searchItemsTableRegisteredTable"); - self.clearTable(tableBody); - } else { - alert('Error: Problem with one of the selected ids (either agent or ' + itemType); - } - e.preventDefault(); - }); - - $('#searchAgentsFetch').click({el: element}, function(e) { - var agentId = $('#searchItemsTableSelectionId').html(); - // It doesn't matter too much what the combined name is as long as it exists - var itemType = {"type": $("#searchItemTypeSelected").html(), "combined": "localSkills"} - - e.preventDefault(); - - if (self.validateId(agentId) ) { - self.model.fetchAgent(agentId) - self.view.setSelectedId("searchItemsTable", "NONE") - var tableBody = $(e.target).closest(".searchItemsTableRegisteredTable"); - self.clearTable(tableBody); - } else { - alert('Error: Problem with one of the selected ids (either agent or ' + itemType); - } - e.preventDefault(); - }); - - - this.$event_pump.on('model_error', {el: element}, function(e, xhr, textStatus, errorThrown) { - var error_msg = textStatus + ': ' + errorThrown + ' - ' + xhr.responseJSON.detail; - self.view.error(error_msg); - console.log(error_msg); - }) - - this.handleButtonStates(this); - - - $('#localAgentsCreateId').on('input', function(e){ - self.handleButtonStates() - }); - $('#localAgentsSelectionId').on('input', function(e){ - self.handleButtonStates() - }); - $('#searchInput').on('input', function(e){ - self.handleButtonStates() - }); - - for (var j = 0; j < elements.length; j++) { - $('#'+ elements[j]["combined"] + 'ScaffoldId').on('input', function(e){ - self.handleButtonStates()}); - } - - this.getAgentStatus(); - - } - - clearTable (tableBody) { - tableBody.children().each(function(i) { - $(this).removeClass("aea_selected") - }); - } - - handleButtonStates(){ - var agentCreateId = $('#localAgentsCreateId').val(); - var agentSelectionId = $('#localAgentsSelectionId').html(); - $('#localAgentsCreate').prop('disabled', !this.validateId(agentCreateId)); - $('#localAgentsDelete').prop('disabled', !this.validateId(agentSelectionId)); - - for (var j = 0; j < elements.length; j++) { - if (elements[j]["location"] == "local" && elements[j]["type"] != "agent"){ - var itemSelectionId = $('#' + elements[j]["combined"] + 'SelectionId').html(); - var isDisabled = !this.validateId(itemSelectionId); - $('#' + elements[j]["combined"] + 'Remove').prop('disabled', isDisabled); - - var itemScaffoldId = $('#' + elements[j]["combined"] + 'ScaffoldId').val(); - $('#' + elements[j]["combined"] + 'Scaffold').prop('disabled', - !this.validateId(itemScaffoldId) || - !this.validateId(agentSelectionId)); - - - } - if (elements[j]["location"] == "registered"){ - var itemSelectionId = $('#' + elements[j]["combined"] + 'SelectionId').html(); - var isDisabled = !this.validateId(itemSelectionId) || !this.validateId(agentSelectionId); - $('#' + elements[j]["combined"] + 'Add').prop('disabled', isDisabled); - } - } - // Search buttons - var searchTerm = $('#searchInput').val(); - $('#searchInputButton').prop('disabled', !this.validateId(searchTerm)); - var searchItem = $('#searchItemsTableSelectionId').html(); - var itemType = $("#searchItemTypeSelected").html(); - var isDisabled = !this.validateId(searchItem) || !this.validateId(agentSelectionId) || (itemType == "agent"); - $('#searchItemsAdd').prop('disabled', isDisabled); - - var isDisabled = !this.validateId(searchItem) || (itemType != "agent"); - $('#searchAgentsFetch').prop('disabled', isDisabled); - if (agentSelectionId != "NONE"){ - $('.localItemHeading').html(agentSelectionId); - } - else{ - $('.localItemHeading').html("Local"); - - } - } - - getAgentStatus(){ - var agentId = $('#localAgentsSelectionId').html() - self = this - if (self.validateId(agentId)){ - this.model.readAgentStatus(agentId) - } - else{ - self.view.setAgentStatus("Agent Status: NONE") - self.view.setAgentTTY("




") - self.view.setAgentError("




") - } - setTimeout(function() { - self.getAgentStatus() - }, 500) - - } - - // Update lists of protocols, connections and skills for the selected agent - refreshAgentData(agentId){ - for (var j = 0; j < elements.length; j++) { - if (elements[j]["location"] == "local" && elements[j]["type"] != "agent"){ - this.model.readLocalData(elements[j], agentId); - } - } - } - - - validateId(agentId){ - return agentId != "" && agentId != "NONE"; - } - - -} - -$( document ).ready(function() { - c = new Controller(new Model(), new View()) -}); diff --git a/aea/cli_gui/utils.py b/aea/cli_gui/utils.py deleted file mode 100644 index 4003c97881..0000000000 --- a/aea/cli_gui/utils.py +++ /dev/null @@ -1,193 +0,0 @@ -# -*- 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. -# -# ------------------------------------------------------------------------------ -"""Module with utils for CLI GUI.""" - -import io -import logging -import os -import subprocess # nosec -import sys -import threading -from enum import Enum -from typing import Any, Dict, List, Optional, Set, Tuple - - -class AppContext: # pylint: disable=too-few-public-methods - """Store useful global information about the app. - - Can't add it into the app object itself because mypy complains. - """ - - agent_processes: Dict[str, subprocess.Popen] = {} - agent_tty: Dict[str, List[str]] = {} - agent_error: Dict[str, List[str]] = {} - - ui_is_starting = False - agents_dir = os.path.abspath(os.getcwd()) - module_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), "../../") - - local = "--local" in sys.argv # a hack to get "local" option from cli args - - -class ProcessState(Enum): - """The state of execution of the agent.""" - - NOT_STARTED = "Not started yet" - RUNNING = "Running" - STOPPING = "Stopping" - FINISHED = "Finished" - FAILED = "Failed" - - -_processes = set() # type: Set[subprocess.Popen] -lock = threading.Lock() - - -def _call_subprocess(*args: Any, timeout: Optional[float] = None, **kwargs: Any) -> int: - """ - Create a subprocess.Popen, but with error handling. - - :return the exit code, or -1 if the call raises exception. - """ - process = subprocess.Popen(*args) # nosec - ret = -1 - try: - ret = process.wait(timeout=timeout) - except subprocess.TimeoutExpired: - logging.exception( - "TimeoutError occurred when calling with args={} and kwargs={}".format( - args, kwargs - ) - ) - finally: - _terminate_process(process) - return ret - - -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 - 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: - """Call the aea in a subprocess.""" - # Should lock here to prevent multiple calls coming in at once and changing the current working directory weirdly - with lock: - old_cwd = os.getcwd() - - os.chdir(dir_arg) - env = os.environ.copy() - env["PYTHONUNBUFFERED"] = "1" - ret = subprocess.Popen( # nosec - param_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env - ) - _processes.add(ret) - os.chdir(old_cwd) - return ret - - -def read_tty(pid: subprocess.Popen, str_list: List[str]) -> None: - """ - Read tty. - - :param pid: the process id - :param str_list: the output list to append to. - """ - for line in io.TextIOWrapper(pid.stdout, encoding="utf-8"): - out = line.replace("\n", "") - logging.info("stdout: {}".format(out)) - str_list.append(line) - - str_list.append("process terminated\n") - - -def read_error(pid: subprocess.Popen, str_list: List[str]) -> None: - """ - Read error. - - :param pid: the process id - :param str_list: the output list to append to. - """ - for line in io.TextIOWrapper(pid.stderr, encoding="utf-8"): - out = line.replace("\n", "") - logging.error("stderr: {}".format(out)) - str_list.append(line) - - str_list.append("process terminated\n") - - -def stop_agent_process(agent_id: str, app_context: AppContext) -> Tuple[str, int]: - """ - Stop an agent processs. - - :param agent_id: the agent id - :param app_context: the app context - """ - # Test if we have the process id - if agent_id not in app_context.agent_processes: - return ( - "detail: Agent {} is not running".format(agent_id), - 400, - ) # 400 Bad request - - app_context.agent_processes[agent_id].terminate() - app_context.agent_processes[agent_id].wait() - del app_context.agent_processes[agent_id] - - return "stop_agent: All fine {}".format(agent_id), 200 # 200 (OK) - - -def _terminate_process(process: subprocess.Popen) -> None: - """Try to process gracefully.""" - poll = process.poll() - if poll is None: - # send SIGTERM - process.terminate() - try: - # wait for termination - process.wait(3) - except subprocess.TimeoutExpired: - # send SIGKILL - process.kill() - - -def terminate_processes() -> None: - """Terminate all the (async) processes instantiated by the GUI.""" - logging.info("Cleaning up...") - for process in _processes: # pragma: no cover - _terminate_process(process) - - -def get_process_status(process_id: subprocess.Popen) -> ProcessState: - """ - Return the state of the execution. - - :param process_id: the process id - """ - 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 - if return_code <= 0: - return ProcessState.FINISHED - return ProcessState.FAILED diff --git a/aea/components/base.py b/aea/components/base.py index 404ce8ca23..eb21d67613 100644 --- a/aea/components/base.py +++ b/aea/components/base.py @@ -42,6 +42,8 @@ class Component(ABC, WithLogger): """Abstract class for an agent component.""" + __slots__ = ("_configuration", "_directory", "_is_vendor") + def __init__( self, configuration: Optional[ComponentConfiguration] = None, diff --git a/aea/configurations/base.py b/aea/configurations/base.py index 9a68980ee0..5cc72acc38 100644 --- a/aea/configurations/base.py +++ b/aea/configurations/base.py @@ -148,6 +148,8 @@ class ProtocolSpecificationParseError(Exception): class Configuration(JSONSerializable, ABC): """Configuration class.""" + __slots__ = ("_key_order",) + def __init__(self) -> None: """Initialize a configuration object.""" # a list of keys that remembers the key order of the configuration file. @@ -205,6 +207,19 @@ class PackageConfiguration(Configuration, ABC): - contracts """ + __slots__ = ( + "_name", + "_author", + "version", + "license", + "fingerprint", + "fingerprint_ignore_patterns", + "build_entrypoint", + "_aea_version", + "_aea_version_specifiers", + "_directory", + ) + default_configuration_filename: str package_type: PackageType FIELDS_ALLOWED_TO_UPDATE: FrozenSet[str] = frozenset(["build_directory"]) @@ -289,6 +304,14 @@ def aea_version(self, new_aea_version: str) -> None: ) self._aea_version = new_aea_version + def check_aea_version(self) -> None: + """ + Check that the AEA version matches the specifier set. + + :raises ValueError if the version of the aea framework falls within a specifier. + """ + _check_aea_version(self) + @property def directory(self) -> Optional[Path]: """Get the path to the configuration file associated to this file, if any.""" @@ -430,6 +453,8 @@ class ComponentConfiguration(PackageConfiguration, ABC): package_type: PackageType + __slots__ = ("pypi_dependencies", "_build_directory") + def __init__( self, name: SimpleIdOrStr, @@ -509,14 +534,6 @@ def check_fingerprint(self, directory: Path) -> None: self, directory, False, self.component_type.to_package_type() ) - def check_aea_version(self) -> None: - """ - Check that the AEA version matches the specifier set. - - :raises ValueError if the version of the aea framework falls within a specifier. - """ - _check_aea_version(self) - def check_public_id_consistency(self, directory: Path) -> None: """ Check that the public ids in the init file match the config. @@ -541,6 +558,19 @@ class ConnectionConfig(ComponentConfiguration): ["config", "cert_requests", "is_abstract", "build_directory"] ) + __slots__ = ( + "class_name", + "protocols", + "connections", + "restricted_to_protocols", + "excluded_protocols", + "dependencies", + "description", + "config", + "is_abstract", + "cert_requests", + ) + def __init__( self, name: SimpleIdOrStr = "", @@ -729,6 +759,8 @@ class ProtocolConfig(ComponentConfiguration): schema = "protocol-config_schema.json" FIELDS_ALLOWED_TO_UPDATE: FrozenSet[str] = frozenset() + __slots__ = ("dependencies", "description", "protocol_specification_id") + def __init__( self, name: SimpleIdOrStr, @@ -824,7 +856,11 @@ def _create_or_update_from_json( class SkillComponentConfiguration: """This class represent a skill component configuration.""" - def __init__(self, class_name: str, **args: Any) -> None: + __slots__ = ("class_name", "file_path", "args") + + def __init__( + self, class_name: str, file_path: Optional[str] = None, **args: Any + ) -> None: """ Initialize a skill component configuration. @@ -833,12 +869,16 @@ def __init__(self, class_name: str, **args: Any) -> None: :param args: keyword arguments. """ self.class_name = class_name + self.file_path: Optional[Path] = Path(file_path) if file_path else None self.args = args @property def json(self) -> Dict: """Return the JSON representation.""" - return {"class_name": self.class_name, "args": self.args} + result = {"class_name": self.class_name, "args": self.args} + if self.file_path is not None: + result["file_path"] = str(self.file_path.as_posix()) + return result @classmethod def from_json(cls, obj: Dict) -> "SkillComponentConfiguration": @@ -852,7 +892,8 @@ def _create_or_update_from_json( """Initialize from a JSON object.""" obj = {**(instance.json if instance else {}), **copy(obj)} class_name = cast(str, obj.get("class_name")) - params = dict(class_name=class_name, **obj.get("args", {})) + file_path = cast(Optional[str], obj.get("file_path")) + params = dict(class_name=class_name, file_path=file_path, **obj.get("args", {})) instance = cast( SkillComponentConfiguration, cls._apply_params_to_instance(params, instance) @@ -888,6 +929,19 @@ class SkillConfig(ComponentConfiguration): ) NESTED_FIELDS_ALLOWED_TO_UPDATE: FrozenSet[str] = frozenset(["args"]) + __slots__ = ( + "connections", + "protocols", + "contracts", + "skills", + "dependencies", + "description", + "handlers", + "behaviours", + "models", + "is_abstract", + ) + def __init__( self, name: SimpleIdOrStr, @@ -1103,10 +1157,42 @@ class AgentConfig(PackageConfiguration): CHECK_EXCLUDES = [ ("private_key_paths",), ("connection_private_key_paths",), + ("decision_maker_handler",), ("default_routing",), + ("dependencies",), ("logging_config",), ] + __slots__ = ( + "agent_name", + "registry_path", + "description", + "private_key_paths", + "connection_private_key_paths", + "logging_config", + "default_ledger", + "currency_denominations", + "default_connection", + "connections", + "protocols", + "skills", + "contracts", + "period", + "execution_timeout", + "max_reactions", + "skill_exception_policy", + "connection_exception_policy", + "error_handler", + "decision_maker_handler", + "default_routing", + "loop_mode", + "runtime_mode", + "storage_uri", + "data_dir", + "_component_configurations", + "dependencies", + ) + def __init__( # pylint: disable=too-many-arguments self, agent_name: SimpleIdOrStr, @@ -1136,6 +1222,7 @@ def __init__( # pylint: disable=too-many-arguments storage_uri: Optional[str] = None, data_dir: Optional[str] = None, component_configurations: Optional[Dict[ComponentId, Dict]] = None, + dependencies: Optional[Dependencies] = None, ) -> None: """Instantiate the agent configuration object.""" super().__init__( @@ -1198,6 +1285,7 @@ def __init__( # pylint: disable=too-many-arguments self.component_configurations = ( component_configurations if component_configurations is not None else {} ) + self.dependencies = dependencies or {} @property def component_configurations(self) -> Dict[ComponentId, Dict]: @@ -1302,6 +1390,7 @@ def json(self) -> Dict: "logging_config": self.logging_config, "registry_path": self.registry_path, "component_configurations": self.component_configurations_json(), + "dependencies": dependencies_to_json(self.dependencies), } ) # type: Dict[str, Any] @@ -1374,12 +1463,15 @@ def _create_or_update_from_json( storage_uri=cast(str, obj.get("storage_uri")), data_dir=cast(str, obj.get("data_dir")), component_configurations=None, + dependencies=cast( + Dependencies, dependencies_from_json(obj.get("dependencies", {})) + ), ) instance = cast(AgentConfig, cls._apply_params_to_instance(params, instance)) agent_config = instance - #  parse private keys + # Parse private keys for crypto_id, path in obj.get("private_key_paths", {}).items(): agent_config.private_key_paths.create(crypto_id, path) @@ -1463,6 +1555,8 @@ def update(self, data: Dict, env_vars_friendly: bool = False) -> None: class SpeechActContentConfig(Configuration): """Handle a speech_act content configuration.""" + __slots__ = ("args",) + def __init__(self, **args: Any) -> None: """Initialize a speech_act content configuration.""" super().__init__() @@ -1482,6 +1576,8 @@ def from_json(cls, obj: Dict) -> "SpeechActContentConfig": class ProtocolSpecification(ProtocolConfig): """Handle protocol specification.""" + __slots__ = ("speech_acts", "_protobuf_snippets", "_dialogue_config") + def __init__( self, name: SimpleIdOrStr, @@ -1586,6 +1682,13 @@ class ContractConfig(ComponentConfiguration): FIELDS_ALLOWED_TO_UPDATE: FrozenSet[str] = frozenset(["build_directory"]) + __slots__ = ( + "dependencies", + "description", + "contract_interface_paths", + "class_name", + ) + def __init__( self, name: SimpleIdOrStr, @@ -1758,17 +1861,29 @@ def _compare_fingerprints( ) +class AEAVersionError(ValueError): + """Special Exception for version error.""" + + def __init__( + self, package_id: PublicId, aea_version_specifiers: SpecifierSet + ) -> None: + """Init exception.""" + self.package_id = package_id + self.aea_version_specifiers = aea_version_specifiers + self.current_aea_version = Version(__aea_version__) + super().__init__( + f"The CLI version is {self.current_aea_version}, but package {self.package_id} requires version {self.aea_version_specifiers}" + ) + + def _check_aea_version(package_configuration: PackageConfiguration) -> None: """Check the package configuration version against the version of the framework.""" current_aea_version = Version(__aea_version__) version_specifiers = package_configuration.aea_version_specifiers if current_aea_version not in version_specifiers: - raise ValueError( - "The CLI version is {}, but package {} requires version {}".format( - current_aea_version, - package_configuration.public_id, - package_configuration.aea_version_specifiers, - ) + raise AEAVersionError( + package_configuration.public_id, + package_configuration.aea_version_specifiers, ) diff --git a/aea/configurations/constants.py b/aea/configurations/constants.py index 0c876d7830..92d019ddfe 100644 --- a/aea/configurations/constants.py +++ b/aea/configurations/constants.py @@ -22,12 +22,14 @@ from typing import Dict, List -FETCHAI = "fetchai" +_FETCHAI_IDENTIFIER = "fetchai" +_ETHEREUM_IDENTIFIER = "ethereum" +_COSMOS_IDENTIFIER = "cosmos" DEFAULT_PROTOCOL = "fetchai/default:latest" SIGNING_PROTOCOL = "fetchai/signing:latest" STATE_UPDATE_PROTOCOL = "fetchai/state_update:latest" LEDGER_CONNECTION = "fetchai/ledger:latest" -DEFAULT_LEDGER = FETCHAI +DEFAULT_LEDGER = _FETCHAI_IDENTIFIER PRIVATE_KEY_PATH_SCHEMA = "{}_private_key.txt" DEFAULT_PRIVATE_KEY_FILE = PRIVATE_KEY_PATH_SCHEMA.format(DEFAULT_LEDGER) DEFAULT_LICENSE = "Apache-2.0" @@ -87,6 +89,14 @@ DEFAULT_AEA_CONFIG_FILE: AGENT, } # type: Dict[str, str] +CRYPTO_PLUGIN_GROUP = "aea.cryptos" +LEDGER_APIS_PLUGIN_GROUP = "aea.ledger_apis" +FAUCET_APIS_PLUGIN_GROUP = "aea.faucet_apis" +ALLOWED_GROUPS = { + CRYPTO_PLUGIN_GROUP, + LEDGER_APIS_PLUGIN_GROUP, + FAUCET_APIS_PLUGIN_GROUP, +} AEA_MANAGER_DATA_DIRNAME = "data" LAUNCH_SUCCEED_MESSAGE = "Start processing messages..." diff --git a/aea/configurations/data_types.py b/aea/configurations/data_types.py index 46a399eec9..f9d23ed11f 100644 --- a/aea/configurations/data_types.py +++ b/aea/configurations/data_types.py @@ -223,6 +223,8 @@ class PublicId(JSONSerializable): True """ + __slots__ = ("_author", "_name", "_package_version") + AUTHOR_REGEX = fr"[a-zA-Z_][a-zA-Z0-9_]{{0,{STRING_LENGTH_LIMIT - 1}}}" PACKAGE_NAME_REGEX = fr"[a-zA-Z_][a-zA-Z0-9_]{{0,{STRING_LENGTH_LIMIT - 1}}}" VERSION_NUMBER_PART_REGEX = r"(0|[1-9]\d*)" @@ -321,6 +323,21 @@ def from_str(cls, public_id_string: str) -> "PublicId": version = match.group(3)[1:] if ":" in public_id_string else None return PublicId(username, package_name, version) + @classmethod + def try_from_str(cls, public_id_string: str) -> Optional["PublicId"]: + """ + Safely try to get public id from string. + + :param public_id_string: the public id in string format. + :return: the public id object or None + """ + result: Optional[PublicId] = None + try: + result = cls.from_str(public_id_string) + except ValueError: + pass + return result + @classmethod def from_uri_path(cls, public_id_uri_path: str) -> "PublicId": """ @@ -437,6 +454,8 @@ class PackageId: PACKAGE_TYPE_REGEX, PublicId.PUBLIC_ID_URI_REGEX[1:-1] ) + __slots__ = ("_package_type", "_public_id") + def __init__( self, package_type: Union[PackageType, str], public_id: PublicId ) -> None: @@ -482,7 +501,7 @@ def package_prefix(self) -> Tuple[PackageType, str, str]: @classmethod def from_uri_path(cls, package_id_uri_path: str) -> "PackageId": """ - Initialize the public id from the string. + Initialize the package id from the string. >>> str(PackageId.from_uri_path("skill/author/package_name/0.1.0")) '(skill, author/package_name:0.1.0)' @@ -493,8 +512,8 @@ def from_uri_path(cls, package_id_uri_path: str) -> "PackageId": ... 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. + :param package_id_uri_path: the package id in uri path string format. + :return: the package 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): @@ -640,6 +659,8 @@ class Dependency: These fields will be forwarded to the 'pip' command. """ + __slots__ = ("_name", "_version", "_index", "_git", "_ref") + def __init__( self, name: Union[PyPIPackageName, str], @@ -774,6 +795,8 @@ def __eq__(self, other: Any) -> bool: class CRUDCollection(Generic[T]): """Interface of a CRUD collection.""" + __slots__ = ("_items_by_id",) + def __init__(self) -> None: """Instantiate a CRUD collection.""" self._items_by_id = {} # type: Dict[str, T] diff --git a/aea/configurations/schemas/aea-config_schema.json b/aea/configurations/schemas/aea-config_schema.json index 43b1ad3a6a..bfe11333e1 100644 --- a/aea/configurations/schemas/aea-config_schema.json +++ b/aea/configurations/schemas/aea-config_schema.json @@ -151,6 +151,9 @@ }, "data_dir": { "type": "string" + }, + "dependencies": { + "$ref": "definitions.json#/definitions/dependencies" } } } diff --git a/aea/configurations/schemas/skill-config_schema.json b/aea/configurations/schemas/skill-config_schema.json index 91d36a10be..47ff0d7c93 100644 --- a/aea/configurations/schemas/skill-config_schema.json +++ b/aea/configurations/schemas/skill-config_schema.json @@ -87,13 +87,13 @@ } }, "handlers": { - "$ref": "#/definitions/handlers" + "$ref": "#/definitions/skill_component_list" }, "behaviours": { - "$ref": "#/definitions/behaviours" + "$ref": "#/definitions/skill_component_list" }, "models": { - "$ref": "#/definitions/models" + "$ref": "#/definitions/skill_component_list" }, "dependencies": { "$ref": "definitions.json#/definitions/dependencies" @@ -106,37 +106,16 @@ } }, "definitions": { - "handlers": { - "type": "object", - "additionalProperties": false, - "uniqueItems": true, - "patternProperties": { - "^[^\\d\\W]\\w*\\Z": { - "$ref": "#/definitions/handler" - } - } - }, - "behaviours": { + "skill_component_list": { "type": "object", - "uniqueItems": true, "patternProperties": { "^[^\\d\\W]\\w*\\Z": { - "$ref": "#/definitions/behaviour" + "$ref": "#/definitions/skill_component_configuration" } } }, - "models": { + "skill_component_configuration": { "type": "object", - "uniqueItems": true, - "patternProperties": { - "^[^\\d\\W]\\w*\\Z": { - "$ref": "#/definitions/model" - } - } - }, - "behaviour": { - "type": "object", - "additionalProperties": false, "required": [ "class_name" ], @@ -146,36 +125,9 @@ }, "args": { "type": "object" - } - } - }, - "handler": { - "type": "object", - "additionalProperties": false, - "required": [ - "class_name" - ], - "properties": { - "class_name": { - "type": "string" }, - "args": { - "type": "object" - } - } - }, - "model": { - "type": "object", - "additionalProperties": false, - "required": [ - "class_name" - ], - "properties": { - "class_name": { + "file_path": { "type": "string" - }, - "args": { - "type": "object" } } } diff --git a/aea/connections/base.py b/aea/connections/base.py index f164938d86..2cd6d3996f 100644 --- a/aea/connections/base.py +++ b/aea/connections/base.py @@ -122,12 +122,8 @@ def _ensure_valid_envelope_for_external_comms(envelope: "Envelope") -> None: :param envelope: the envelope """ enforce( - not envelope.is_sender_public_id, - f"Sender field of envelope is public id, needs to be address. Found={envelope.sender}", - ) - enforce( - not envelope.is_to_public_id, - f"To field of envelope is public id, needs to be address. Found={envelope.to}", + not envelope.is_sender_public_id and not envelope.is_to_public_id, + f"Sender and to field of envelope is public id, needs to be address. Found: sender={envelope.sender}, to={envelope.to}", ) @contextmanager diff --git a/aea/connections/scaffold/connection.yaml b/aea/connections/scaffold/connection.yaml index c4e07e9553..dad3bcb05d 100644 --- a/aea/connections/scaffold/connection.yaml +++ b/aea/connections/scaffold/connection.yaml @@ -5,7 +5,7 @@ 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.10.0, <0.11.0' +aea_version: '>=0.11.0, <0.12.0' fingerprint: __init__.py: QmZvYZ5ECcWwqiNGh8qNTg735wu51HqaLxTSifUxkQ4KGj connection.py: QmRQASdBp3e7d7oDoEnxHcVfM7wvVQFWUE44DXbqpc3kDU diff --git a/aea/context/base.py b/aea/context/base.py index 3bff5e2155..7a3362917a 100644 --- a/aea/context/base.py +++ b/aea/context/base.py @@ -16,24 +16,44 @@ # limitations under the License. # # ------------------------------------------------------------------------------ - - """This module contains the agent context class.""" from queue import Queue from types import SimpleNamespace -from typing import Any, Callable, Dict, Optional +from typing import Any, Callable, Dict, Optional, Union from aea.common import Address from aea.configurations.base import PublicId from aea.helpers.storage.generic_storage import Storage from aea.identity.base import Identity +from aea.mail.base import Envelope, EnvelopeContext from aea.multiplexer import MultiplexerStatus, OutBox +from aea.protocols.base import Message from aea.skills.tasks import TaskManager class AgentContext: """Provide read access to relevant objects of the agent for the skills.""" + __slots__ = ( + "_shared_state", + "_identity", + "_connection_status", + "_outbox", + "_decision_maker_message_queue", + "_decision_maker_handler_context", + "_task_manager", + "_search_service_address", + "_decision_maker_address", + "_default_ledger_id", + "_currency_denominations", + "_default_connection", + "_default_routing", + "_storage_callable", + "_data_dir", + "_namespace", + "_send_to_skill", + ) + def __init__( self, identity: Identity, @@ -50,6 +70,7 @@ def __init__( decision_maker_address: Address, data_dir: str, storage_callable: Callable[[], Optional[Storage]] = lambda: None, + send_to_skill: Optional[Callable] = None, **kwargs: Any ) -> None: """ @@ -87,6 +108,24 @@ def __init__( self._storage_callable = storage_callable self._data_dir = data_dir self._namespace = SimpleNamespace(**kwargs) + self._send_to_skill = send_to_skill + + def send_to_skill( + self, + message_or_envelope: Union[Message, Envelope], + context: Optional[EnvelopeContext] = None, + ) -> None: + """ + Send message or envelope to another skill. + + :param message_or_envelope: envelope to send to another skill. + if message passed it will be wrapped into envelope with optional envelope context. + + :return: None + """ + if self._send_to_skill is None: # pragma: nocover + raise ValueError("Send to skill feature is not supported") + return self._send_to_skill(message_or_envelope, context) @property def storage(self) -> Optional[Storage]: diff --git a/aea/contracts/scaffold/contract.yaml b/aea/contracts/scaffold/contract.yaml index aa7e1c68eb..06a6cc2dfb 100644 --- a/aea/contracts/scaffold/contract.yaml +++ b/aea/contracts/scaffold/contract.yaml @@ -4,7 +4,7 @@ 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.10.0, <0.11.0' +aea_version: '>=0.11.0, <0.12.0' fingerprint: __init__.py: QmPBwWhEg3wcH1q9612srZYAYdANVdWLDFWKs7TviZmVj6 contract.py: QmbaG1cJbzb1oNAce78n8UuLKqquHFLvHKKBfHL5F5Ci7G diff --git a/aea/crypto/__init__.py b/aea/crypto/__init__.py index 3006b6a9a8..e82693d2c0 100644 --- a/aea/crypto/__init__.py +++ b/aea/crypto/__init__.py @@ -19,39 +19,8 @@ """This module contains the crypto modules.""" -from aea.crypto.cosmos import CosmosCrypto -from aea.crypto.ethereum import EthereumCrypto -from aea.crypto.fetchai import FetchAICrypto -from aea.crypto.registries import register_crypto # noqa -from aea.crypto.registries import register_faucet_api, register_ledger_api - - -register_crypto( - id_=FetchAICrypto.identifier, entry_point="aea.crypto.fetchai:FetchAICrypto" -) -register_crypto( - id_=EthereumCrypto.identifier, entry_point="aea.crypto.ethereum:EthereumCrypto" -) -register_crypto( - id_=CosmosCrypto.identifier, entry_point="aea.crypto.cosmos:CosmosCrypto" -) - -register_faucet_api( - id_=FetchAICrypto.identifier, entry_point="aea.crypto.fetchai:FetchAIFaucetApi" -) -register_faucet_api( - id_=EthereumCrypto.identifier, entry_point="aea.crypto.ethereum:EthereumFaucetApi" -) -register_faucet_api( - id_=CosmosCrypto.identifier, entry_point="aea.crypto.cosmos:CosmosFaucetApi" -) - -register_ledger_api( - id_=FetchAICrypto.identifier, entry_point="aea.crypto.fetchai:FetchAIApi", -) -register_ledger_api( - id_=EthereumCrypto.identifier, entry_point="aea.crypto.ethereum:EthereumApi" -) -register_ledger_api( - id_=CosmosCrypto.identifier, entry_point="aea.crypto.cosmos:CosmosApi", +from aea.crypto.registries import ( # noqa + register_crypto, + register_faucet_api, + register_ledger_api, ) diff --git a/aea/crypto/ledger_apis.py b/aea/crypto/ledger_apis.py index e6a0060eb1..0431675a07 100644 --- a/aea/crypto/ledger_apis.py +++ b/aea/crypto/ledger_apis.py @@ -21,22 +21,13 @@ 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.cosmos import DEFAULT_CHAIN_ID as COSMOS_DEFAULT_CHAIN_ID -from aea.crypto.cosmos import DEFAULT_CURRENCY_DENOM as COSMOS_DEFAULT_CURRENCY_DENOM -from aea.crypto.ethereum import DEFAULT_ADDRESS as ETHEREUM_DEFAULT_ADDRESS -from aea.crypto.ethereum import DEFAULT_CHAIN_ID as ETHEREUM_DEFAULT_CHAIN_ID -from aea.crypto.ethereum import ( - DEFAULT_CURRENCY_DENOM as ETHEREUM_DEFAULT_CURRENCY_DENOM, +from aea.configurations.constants import ( + DEFAULT_LEDGER, + _COSMOS_IDENTIFIER, + _ETHEREUM_IDENTIFIER, + _FETCHAI_IDENTIFIER, ) -from aea.crypto.ethereum import EthereumApi -from aea.crypto.fetchai import DEFAULT_ADDRESS as FETCHAI_DEFAULT_ADDRESS -from aea.crypto.fetchai import DEFAULT_CHAIN_ID as FETCHAI_DEFAULT_CHAIN_ID -from aea.crypto.fetchai import DEFAULT_CURRENCY_DENOM as FETCHAI_DEFAULT_CURRENCY_DENOM -from aea.crypto.fetchai import FetchAIApi +from aea.crypto.base import LedgerApi from aea.crypto.registries import ( ledger_apis_registry, make_ledger_api, @@ -45,27 +36,38 @@ from aea.exceptions import enforce -DEFAULT_LEDGER_CONFIGS = { - CosmosApi.identifier: { +COSMOS_DEFAULT_ADDRESS = "INVALID_URL" +COSMOS_DEFAULT_CURRENCY_DENOM = "INVALID_CURRENCY_DENOM" +COSMOS_DEFAULT_CHAIN_ID = "INVALID_CHAIN_ID" +ETHEREUM_DEFAULT_ADDRESS = "http://127.0.0.1:8545" +ETHEREUM_DEFAULT_CHAIN_ID = 1337 +ETHEREUM_DEFAULT_CURRENCY_DENOM = "wei" +FETCHAI_DEFAULT_ADDRESS = "https://rest-agent-land.fetch.ai" +FETCHAI_DEFAULT_CURRENCY_DENOM = "atestfet" +FETCHAI_DEFAULT_CHAIN_ID = "agent-land" + + +DEFAULT_LEDGER_CONFIGS: Dict[str, Dict[str, Union[str, int]]] = { + _COSMOS_IDENTIFIER: { "address": COSMOS_DEFAULT_ADDRESS, "chain_id": COSMOS_DEFAULT_CHAIN_ID, "denom": COSMOS_DEFAULT_CURRENCY_DENOM, }, - EthereumApi.identifier: { + _ETHEREUM_IDENTIFIER: { "address": ETHEREUM_DEFAULT_ADDRESS, "chain_id": ETHEREUM_DEFAULT_CHAIN_ID, "denom": ETHEREUM_DEFAULT_CURRENCY_DENOM, }, - FetchAIApi.identifier: { + _FETCHAI_IDENTIFIER: { "address": FETCHAI_DEFAULT_ADDRESS, "chain_id": FETCHAI_DEFAULT_CHAIN_ID, "denom": FETCHAI_DEFAULT_CURRENCY_DENOM, }, -} # type: Dict[str, Dict[str, Union[str, int]]] +} DEFAULT_CURRENCY_DENOMINATIONS = { - CosmosApi.identifier: COSMOS_DEFAULT_CURRENCY_DENOM, - EthereumApi.identifier: ETHEREUM_DEFAULT_CURRENCY_DENOM, - FetchAIApi.identifier: FETCHAI_DEFAULT_CURRENCY_DENOM, + _COSMOS_IDENTIFIER: COSMOS_DEFAULT_CURRENCY_DENOM, + _ETHEREUM_IDENTIFIER: ETHEREUM_DEFAULT_CURRENCY_DENOM, + _FETCHAI_IDENTIFIER: FETCHAI_DEFAULT_CURRENCY_DENOM, } diff --git a/aea/crypto/plugin.py b/aea/crypto/plugin.py new file mode 100644 index 0000000000..a3cd14b2c9 --- /dev/null +++ b/aea/crypto/plugin.py @@ -0,0 +1,169 @@ +# -*- 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 plug-in mechanism for cryptos.""" +import itertools +import pprint +from typing import Iterator, List, Set + +from pkg_resources import EntryPoint, iter_entry_points + +from aea.configurations.constants import ( + ALLOWED_GROUPS, + CRYPTO_PLUGIN_GROUP, + DOTTED_PATH_MODULE_ELEMENT_SEPARATOR, + FAUCET_APIS_PLUGIN_GROUP, + LEDGER_APIS_PLUGIN_GROUP, +) +from aea.crypto import register_crypto, register_faucet_api, register_ledger_api +from aea.crypto.registries.base import EntryPoint as EntryPointString +from aea.crypto.registries.base import ItemId +from aea.exceptions import AEAPluginError, enforce + + +_from_group_to_register_callable = { + CRYPTO_PLUGIN_GROUP: register_crypto, + LEDGER_APIS_PLUGIN_GROUP: register_ledger_api, + FAUCET_APIS_PLUGIN_GROUP: register_faucet_api, +} + + +class Plugin: + """Class that implements an AEA plugin.""" + + __slots__ = ("_group", "_entry_point") + + def __init__(self, group: str, entry_point: EntryPoint): + """ + Initialize the plugin. + + :param group: the group the plugin belongs to. + :param entry_point: the entrypoint. + """ + self._group = group + self._entry_point = entry_point + self._check_consistency() + + def _check_consistency(self) -> None: + """ + Check consistency of input. + + :raises AEAPluginError: if some input is not correct. + """ + _error_message_prefix = f"Error with plugin '{self._entry_point.name}':" + enforce( + self.group in ALLOWED_GROUPS, + f"{_error_message_prefix} '{self.group}' is not in the allowed groups: {pprint.pformat(ALLOWED_GROUPS)}", + AEAPluginError, + ) + enforce( + ItemId.REGEX.match(self._entry_point.name) is not None, + f"{_error_message_prefix} '{self._entry_point.name}' is not a valid identifier for a plugin.", + AEAPluginError, + ) + enforce( + len(self._entry_point.attrs) == 1, + f"{_error_message_prefix} Nested attributes currently not supported.", + AEAPluginError, + ) + enforce( + len(self._entry_point.extras) == 0, + f"{_error_message_prefix} Extras currently not supported.", + AEAPluginError, + ) + enforce( + EntryPointString.REGEX.match(self.entry_point_path) is not None, + f"{_error_message_prefix} Entry point path '{self.entry_point_path}' is not valid.", + ) + + @property + def name(self) -> str: + """Get the plugin identifier.""" + return self._entry_point.name + + @property + def group(self) -> str: + """Get the group.""" + return self._group + + @property + def attr(self) -> str: + """Get the class name.""" + return self._entry_point.attrs[0] + + @property + def entry_point_path(self) -> str: + """Get the entry point path.""" + class_name = self.attr + return f"{self._entry_point.module_name}{DOTTED_PATH_MODULE_ELEMENT_SEPARATOR}{class_name}" + + +def _check_no_duplicates(plugins: List[EntryPoint]) -> None: + """Check there are no two plugins with the same id.""" + seen: Set[str] = set() + duplicate_plugins = [p for p in plugins if p.name in seen or seen.add(p.name)] # type: ignore + error_msg = f"Found plugins with the same id: {pprint.pformat(duplicate_plugins)}" + enforce(len(duplicate_plugins) == 0, error_msg, AEAPluginError) + + +def _get_plugins(group: str) -> List[Plugin]: + """ + Return a dict of all installed plugins, by name. + + :param group: the plugin group. + :return: a mapping from plugin name to Plugin objects. + """ + entry_points: List[EntryPoint] = list(iter_entry_points(group=group)) + _check_no_duplicates(entry_points) + return [Plugin(group, entry_point) for entry_point in entry_points] + + +def _get_cryptos() -> List[Plugin]: + """Get cryptos plugins.""" + return _get_plugins(CRYPTO_PLUGIN_GROUP) + + +def _get_ledger_apis() -> List[Plugin]: + """Get ledgers plugins.""" + return _get_plugins(LEDGER_APIS_PLUGIN_GROUP) + + +def _get_faucet_apis() -> List[Plugin]: + """Get faucets plugins.""" + return _get_plugins(FAUCET_APIS_PLUGIN_GROUP) + + +def _iter_plugins() -> Iterator[Plugin]: + """Iterate over all the plugins.""" + for plugin in itertools.chain( + _get_cryptos(), _get_ledger_apis(), _get_faucet_apis() + ): + yield plugin + + +def _register_plugin(plugin: Plugin) -> None: + """Register a plugin to the right registry.""" + register_function = _from_group_to_register_callable[plugin.group] + register_function(plugin.name, entry_point=plugin.entry_point_path) + + +def load_all_plugins() -> None: + """Load all plugins.""" + for plugin in _iter_plugins(): + _register_plugin(plugin) diff --git a/aea/crypto/wallet.py b/aea/crypto/wallet.py index 0a1ae77ca6..ec1967748a 100644 --- a/aea/crypto/wallet.py +++ b/aea/crypto/wallet.py @@ -33,6 +33,8 @@ class CryptoStore: """Utility class to store and retrieve crypto objects.""" + __slots__ = ("_crypto_objects", "_public_keys", "_addresses", "_private_keys") + def __init__( self, crypto_id_to_path: Optional[Dict[str, Optional[str]]] = None ) -> None: diff --git a/aea/decision_maker/gop.py b/aea/decision_maker/gop.py index da746391af..1b82e03e1d 100644 --- a/aea/decision_maker/gop.py +++ b/aea/decision_maker/gop.py @@ -45,8 +45,6 @@ UtilityParams = Dict[str, float] # a map from identifier to quantity ExchangeParams = Dict[str, float] # a map from identifier to quantity -QUANTITY_SHIFT = 100 - _default_logger = logging.getLogger(__name__) @@ -288,7 +286,6 @@ def __init__(self) -> None: """Instantiate an agent preference object.""" self._exchange_params_by_currency_id = None # type: Optional[ExchangeParams] self._utility_params_by_good_id = None # type: Optional[UtilityParams] - self._quantity_shift = QUANTITY_SHIFT def set( # pylint: disable=arguments-differ self, @@ -348,7 +345,7 @@ def logarithmic_utility(self, quantities_by_good_id: GoodHoldings) -> float: """ enforce(self.is_initialized, "Preferences params not set!") result = logarithmic_utility( - self.utility_params_by_good_id, quantities_by_good_id, self._quantity_shift + self.utility_params_by_good_id, quantities_by_good_id, ) return result diff --git a/aea/error_handler/base.py b/aea/error_handler/base.py index 15ff4a67ec..7d868d3e0a 100644 --- a/aea/error_handler/base.py +++ b/aea/error_handler/base.py @@ -39,20 +39,28 @@ def send_unsupported_protocol(cls, envelope: Envelope, logger: Logger) -> None: @classmethod @abstractmethod - def send_decoding_error(cls, envelope: Envelope, logger: Logger) -> None: + def send_decoding_error( + cls, envelope: Envelope, exception: Exception, logger: Logger + ) -> None: """ Handle a decoding error. :param envelope: the envelope + :param exception: the exception raised during decoding + :param logger: the logger :return: None """ @classmethod @abstractmethod - def send_unsupported_skill(cls, envelope: Envelope, logger: Logger) -> None: + def send_no_active_handler( + cls, envelope: Envelope, reason: str, logger: Logger + ) -> None: """ - Handle the received envelope in case the skill is not supported. + Handle the received envelope in case the handler is not supported. :param envelope: the envelope + :param reason: the reason for the failure + :param logger: the logger :return: None """ diff --git a/aea/error_handler/default.py b/aea/error_handler/default.py index cd08759ccd..78c51a52b0 100644 --- a/aea/error_handler/default.py +++ b/aea/error_handler/default.py @@ -29,7 +29,7 @@ class ErrorHandler(AbstractErrorHandler): """Error handler class for handling problematic envelopes.""" unsupported_protocol_count = 0 - unsupported_skill_count = 0 + no_active_handler_count = 0 decoding_error_count = 0 @classmethod @@ -46,32 +46,34 @@ def send_unsupported_protocol(cls, envelope: Envelope, logger: Logger) -> None: ) @classmethod - def send_decoding_error(cls, envelope: Envelope, logger: Logger) -> None: + def send_decoding_error( + cls, envelope: Envelope, exception: Exception, logger: Logger + ) -> None: """ Handle a decoding error. :param envelope: the envelope + :param exception: the exception raised during decoding + :param logger: the logger :return: None """ cls.decoding_error_count += 1 logger.warning( - f"Decoding error for envelope: {envelope}. Protocol_specification_id='{envelope.protocol_specification_id}' and message are inconsistent. Sender={envelope.sender}, to={envelope.sender}." + f"Decoding error for envelope: {envelope}. Protocol_specification_id='{envelope.protocol_specification_id}' and message are inconsistent. Sender={envelope.sender}, to={envelope.sender}. Exception={exception}." ) @classmethod - def send_unsupported_skill(cls, envelope: Envelope, logger: Logger) -> None: + def send_no_active_handler( + cls, envelope: Envelope, reason: str, logger: Logger + ) -> None: """ - Handle the received envelope in case the skill is not supported. + Handle the received envelope in case the handler is not supported. :param envelope: the envelope + :param reason: the reason for the failure :return: None """ - cls.unsupported_skill_count += 1 - if envelope.skill_id is None: - logger.warning( - f"Cannot handle envelope: no active handler registered for the protocol_specification_id='{envelope.protocol_specification_id}'. Sender={envelope.sender}, to={envelope.sender}." - ) - else: - logger.warning( - f"Cannot handle envelope: no active handler registered for the protocol_specification_id='{envelope.protocol_specification_id}' and skill_id='{envelope.skill_id}'. Sender={envelope.sender}, to={envelope.sender}." - ) + cls.no_active_handler_count += 1 + logger.warning( + f"Cannot handle envelope: {reason}. Sender={envelope.sender}, to={envelope.sender}." + ) diff --git a/aea/error_handler/scaffold.py b/aea/error_handler/scaffold.py index 03e451dd95..07720941f9 100644 --- a/aea/error_handler/scaffold.py +++ b/aea/error_handler/scaffold.py @@ -39,21 +39,29 @@ def send_unsupported_protocol(cls, envelope: Envelope, logger: Logger) -> None: raise NotImplementedError @classmethod - def send_decoding_error(cls, envelope: Envelope, logger: Logger) -> None: + def send_decoding_error( + cls, envelope: Envelope, exception: Exception, logger: Logger + ) -> None: """ Handle a decoding error. :param envelope: the envelope + :param exception: the exception raised during decoding + :param logger: the logger :return: None """ raise NotImplementedError @classmethod - def send_unsupported_skill(cls, envelope: Envelope, logger: Logger) -> None: + def send_no_active_handler( + cls, envelope: Envelope, reason: str, logger: Logger + ) -> None: """ - Handle the received envelope in case the skill is not supported. + Handle the received envelope in case the handler is not supported. :param envelope: the envelope + :param reason: the reason for the failure + :param logger: the logger :return: None """ raise NotImplementedError diff --git a/aea/exceptions.py b/aea/exceptions.py index 22cbd5090f..e75b4769c0 100644 --- a/aea/exceptions.py +++ b/aea/exceptions.py @@ -51,6 +51,10 @@ class AEAInstantiationException(AEAException): """Class for exceptions that are raised for instantiation errors of AEA packages.""" +class AEAPluginError(AEAException): + """Class for exceptions that are raised for wrong plugin setup of the working set.""" + + class AEAEnforceError(AEAException): """Class for enforcement errors.""" diff --git a/aea/helpers/acn/agent_record.py b/aea/helpers/acn/agent_record.py index af0c1b6cd5..e2f6eda8cb 100644 --- a/aea/helpers/acn/agent_record.py +++ b/aea/helpers/acn/agent_record.py @@ -28,6 +28,15 @@ class AgentRecord: """Agent Proof-of-Representation to representative.""" + __slots__ = ( + "_address", + "_representative_public_key", + "_message", + "_signature", + "_ledger_id", + "_public_key", + ) + def __init__( self, address: str, diff --git a/aea/helpers/acn/uri.py b/aea/helpers/acn/uri.py index 606dd8ce1e..58ffc93e40 100644 --- a/aea/helpers/acn/uri.py +++ b/aea/helpers/acn/uri.py @@ -26,6 +26,8 @@ class Uri: """Holds a node address in format "host:port".""" + __slots__ = ("_host", "_port") + def __init__( self, uri: Optional[str] = None, diff --git a/aea/helpers/async_utils.py b/aea/helpers/async_utils.py index 3cded28691..9e38a0ac65 100644 --- a/aea/helpers/async_utils.py +++ b/aea/helpers/async_utils.py @@ -153,6 +153,7 @@ async def wait(self, state_or_states: Union[Any, Sequence[Any]]) -> Tuple[Any, A """Wait state to be set. :param state_or_states: state or list of states. + :return: tuple of previous state and new state. """ states = ensure_list(state_or_states) @@ -453,7 +454,7 @@ def __init__( """ if loop and threaded: raise ValueError( - "You can not set a loop in threaded mode. A separate loop will be created in each thread." + "You can not set a loop in threaded mode. A dedicated loop will be created for each thread." ) self._loop = loop self._threaded = threaded diff --git a/aea/helpers/logging.py b/aea/helpers/logging.py index f0bcd6692b..7e4c4d6c1d 100644 --- a/aea/helpers/logging.py +++ b/aea/helpers/logging.py @@ -51,6 +51,8 @@ def process( class WithLogger: """Interface to endow subclasses with a logger.""" + __slots__ = ("_logger", "_default_logger_name") + def __init__( self, logger: Optional[Logger] = None, default_logger_name: str = "aea", ) -> None: diff --git a/aea/helpers/preference_representations/base.py b/aea/helpers/preference_representations/base.py index 357df7af28..f15267b16b 100644 --- a/aea/helpers/preference_representations/base.py +++ b/aea/helpers/preference_representations/base.py @@ -28,20 +28,22 @@ def logarithmic_utility( utility_params_by_good_id: Dict[str, float], quantities_by_good_id: Dict[str, int], - quantity_shift: int = 1, + quantity_shift: int = 100, ) -> float: """ Compute agent's utility given her utility function params and a good bundle. :param utility_params_by_good_id: utility params by good identifier :param quantities_by_good_id: quantities by good identifier - :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) + :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 """ 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 a69ee6644a..9aa341db71 100644 --- a/aea/helpers/search/generic.py +++ b/aea/helpers/search/generic.py @@ -35,7 +35,7 @@ def __init__( self, data_model_name: str, data_model_attributes: Dict[str, Any] ) -> None: """Initialise the dataModel.""" - self.attributes = [] # type: List[Attribute] + attributes = [] # type: List[Attribute] for values in data_model_attributes.values(): enforce( values["type"] in SUPPORTED_TYPES.keys(), @@ -46,7 +46,7 @@ def __init__( isinstance(values["is_required"], bool), "Wrong type for is_required. Must be bool!", ) - self.attributes.append( + attributes.append( Attribute( name=values["name"], # type: ignore type_=SUPPORTED_TYPES[values["type"]], @@ -54,7 +54,7 @@ def __init__( ) ) - super().__init__(data_model_name, self.attributes) + super().__init__(data_model_name, attributes) AGENT_LOCATION_MODEL = DataModel( diff --git a/aea/helpers/search/models.py b/aea/helpers/search/models.py index 8b7ff00dcf..f0b847c1e9 100644 --- a/aea/helpers/search/models.py +++ b/aea/helpers/search/models.py @@ -96,6 +96,8 @@ class Location: """Data structure to represent locations (i.e. a pair of latitude and longitude).""" + __slots__ = ("latitude", "longitude") + def __init__(self, latitude: float, longitude: float) -> None: """ Initialize a location. @@ -185,6 +187,8 @@ class Attribute: Location: models_pb2.Query.Attribute.LOCATION, # type: ignore } + __slots__ = ("name", "type", "is_required", "description") + def __init__( self, name: str, @@ -254,6 +258,8 @@ def decode(cls, attribute_pb: models_pb2.Query.Attribute) -> "Attribute": # typ class DataModel: """Implements an OEF data model.""" + __slots__ = ("name", "attributes", "description") + def __init__( self, name: str, attributes: List[Attribute], description: str = "" ) -> None: @@ -268,9 +274,13 @@ def __init__( attributes, key=lambda x: x.name ) # type: List[Attribute] self._check_validity() - self.attributes_by_name = {a.name: a for a in self.attributes} self.description = description + @property + def attributes_by_name(self) -> Dict[str, Attribute]: + """Get the attributes by name.""" + return {a.name: a for a in self.attributes} + def _check_validity(self) -> None: # check if there are duplicated attribute names attribute_names = [attribute.name for attribute in self.attributes] @@ -344,6 +354,8 @@ def generate_data_model( class Description: """Implements an OEF description.""" + __slots__ = ("_values", "data_model") + def __init__( self, values: Mapping[str, ATTRIBUTE_TYPES], @@ -597,6 +609,8 @@ class ConstraintType: """ + __slots__ = ("type", "value") + def __init__(self, type_: Union[ConstraintTypes, str], value: Any) -> None: """ Initialize a constraint type. @@ -1115,6 +1129,8 @@ def _decode(constraint_expression_pb: Any) -> "ConstraintExpr": class And(ConstraintExpr): """Implementation of the 'And' constraint expression.""" + __slots__ = ("constraints",) + def __init__(self, constraints: List[ConstraintExpr]) -> None: """ Initialize an 'And' expression. @@ -1189,6 +1205,8 @@ def decode(cls, and_pb: Any) -> "And": class Or(ConstraintExpr): """Implementation of the 'Or' constraint expression.""" + __slots__ = ("constraints",) + def __init__(self, constraints: List[ConstraintExpr]) -> None: """ Initialize an 'Or' expression. @@ -1263,6 +1281,8 @@ def decode(cls, or_pb: Any) -> "Or": class Not(ConstraintExpr): """Implementation of the 'Not' constraint expression.""" + __slots__ = ("constraint",) + def __init__(self, constraint: ConstraintExpr) -> None: """ Initialize a 'Not' expression. @@ -1319,6 +1339,8 @@ def decode(cls, not_pb: Any) -> "Not": class Constraint(ConstraintExpr): """The atomic component of a constraint expression.""" + __slots__ = ("attribute_name", "constraint_type") + def __init__(self, attribute_name: str, constraint_type: ConstraintType) -> None: """ Initialize a constraint. @@ -1482,6 +1504,8 @@ def decode(cls, constraint_pb: Any) -> "Constraint": class Query: """This class lets you build a query for the OEF.""" + __slots__ = ("constraints", "model") + def __init__( self, constraints: List[ConstraintExpr], model: Optional[DataModel] = None ) -> None: diff --git a/aea/helpers/transaction/base.py b/aea/helpers/transaction/base.py index 4bee9e3241..e03e6554c1 100644 --- a/aea/helpers/transaction/base.py +++ b/aea/helpers/transaction/base.py @@ -35,6 +35,8 @@ class RawTransaction: """This class represents an instance of RawTransaction.""" + __slots__ = ("_ledger_id", "_body") + def __init__(self, ledger_id: str, body: JSONLike,) -> None: """Initialise an instance of RawTransaction.""" self._ledger_id = ledger_id @@ -112,6 +114,8 @@ def __str__(self) -> str: class RawMessage: """This class represents an instance of RawMessage.""" + __slots__ = ("_ledger_id", "_body", "_is_deprecated_mode") + def __init__( self, ledger_id: str, body: bytes, is_deprecated_mode: bool = False, ) -> None: @@ -206,6 +210,8 @@ def __str__(self) -> str: class SignedTransaction: """This class represents an instance of SignedTransaction.""" + __slots__ = ("_ledger_id", "_body") + def __init__(self, ledger_id: str, body: JSONLike,) -> None: """Initialise an instance of SignedTransaction.""" self._ledger_id = ledger_id @@ -285,6 +291,8 @@ def __str__(self) -> str: class SignedMessage: """This class represents an instance of RawMessage.""" + __slots__ = ("_ledger_id", "_body", "_is_deprecated_mode") + def __init__( self, ledger_id: str, body: str, is_deprecated_mode: bool = False, ) -> None: @@ -379,6 +387,8 @@ def __str__(self) -> str: class State: """This class represents an instance of State.""" + __slots__ = ("_ledger_id", "_body") + def __init__(self, ledger_id: str, body: JSONLike) -> None: """Initialise an instance of State.""" self._ledger_id = ledger_id @@ -447,6 +457,24 @@ def __str__(self) -> str: class Terms: """Class to represent the terms of a multi-currency & multi-token ledger transaction.""" + __slots__ = ( + "_ledger_id", + "_sender_address", + "_counterparty_address", + "_amount_by_currency_id", + "_quantities_by_good_id", + "_is_sender_payable_tx_fee", + "_nonce", + "_fee_by_currency_id", + "_is_strict", + "_kwargs", + "_good_ids", + "_sender_supplied_quantities", + "_counterparty_supplied_quantities", + "_sender_hash", + "_counterparty_hash", + ) + def __init__( self, ledger_id: str, @@ -942,6 +970,8 @@ def __str__(self) -> str: class TransactionDigest: """This class represents an instance of TransactionDigest.""" + __slots__ = ("_ledger_id", "_body") + def __init__(self, ledger_id: str, body: str) -> None: """Initialise an instance of TransactionDigest.""" self._ledger_id = ledger_id @@ -1022,6 +1052,8 @@ def __str__(self) -> str: class TransactionReceipt: """This class represents an instance of TransactionReceipt.""" + __slots__ = ("_ledger_id", "_receipt", "_transaction") + def __init__( self, ledger_id: str, receipt: JSONLike, transaction: JSONLike ) -> None: diff --git a/aea/identity/base.py b/aea/identity/base.py index 63f8db570d..27b24d322f 100644 --- a/aea/identity/base.py +++ b/aea/identity/base.py @@ -35,6 +35,8 @@ class Identity: - the addresses, a map from address identifier to address (can be a single key-value pair) """ + __slots__ = ("_name", "_address", "_addresses", "_default_address_key") + def __init__( self, name: str, diff --git a/aea/mail/base.proto b/aea/mail/base.proto index 5b6d97ee4f..5958ddb2a9 100644 --- a/aea/mail/base.proto +++ b/aea/mail/base.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package aea; +package aea.base.v0_1_0; import "google/protobuf/struct.proto"; diff --git a/aea/mail/base.py b/aea/mail/base.py index eb86672ade..7782dbaad7 100644 --- a/aea/mail/base.py +++ b/aea/mail/base.py @@ -20,12 +20,11 @@ import logging from abc import ABC, abstractmethod -from typing import Any, Optional, Tuple, Union +from typing import Any, Optional, Union from urllib.parse import urlparse from aea.common import Address -from aea.configurations.base import PackageId, PublicId -from aea.configurations.constants import CONNECTION, SKILL +from aea.configurations.base import PublicId from aea.exceptions import enforce from aea.mail import base_pb2 from aea.protocols.base import Message @@ -34,17 +33,11 @@ _default_logger = logging.getLogger(__name__) -class AEAConnectionError(Exception): - """Exception class for connection errors.""" - - -class Empty(Exception): - """Exception for when the inbox is empty.""" - - class URI: """URI following RFC3986.""" + __slots__ = ("_uri_raw",) + def __init__(self, uri_raw: str) -> None: """ Initialize the URI. @@ -54,175 +47,132 @@ def __init__(self, uri_raw: str) -> None: :param uri_raw: the raw form uri :raises ValueError: if uri_raw is not RFC3986 compliant """ - self.uri_raw = uri_raw - parsed = urlparse(uri_raw) - self._scheme = parsed.scheme - self._netloc = parsed.netloc - self._path = parsed.path - self._params = parsed.params - self._query = parsed.query - self._fragment = parsed.fragment - self._username = parsed.username - self._password = parsed.password - self._host = parsed.hostname - self._port = parsed.port + self._uri_raw = uri_raw @property def scheme(self) -> str: """Get the scheme.""" - return self._scheme + parsed = urlparse(self._uri_raw) + return parsed.scheme @property def netloc(self) -> str: """Get the netloc.""" - return self._netloc + parsed = urlparse(self._uri_raw) + return parsed.netloc @property def path(self) -> str: """Get the path.""" - return self._path + parsed = urlparse(self._uri_raw) + return parsed.path @property def params(self) -> str: """Get the params.""" - return self._params + parsed = urlparse(self._uri_raw) + return parsed.params @property def query(self) -> str: """Get the query.""" - return self._query + parsed = urlparse(self._uri_raw) + return parsed.query @property def fragment(self) -> str: """Get the fragment.""" - return self._fragment + parsed = urlparse(self._uri_raw) + return parsed.fragment @property def username(self) -> Optional[str]: """Get the username.""" - return self._username + parsed = urlparse(self._uri_raw) + return parsed.username @property def password(self) -> Optional[str]: """Get the password.""" - return self._password + parsed = urlparse(self._uri_raw) + return parsed.password @property def host(self) -> Optional[str]: """Get the host.""" - return self._host + parsed = urlparse(self._uri_raw) + return parsed.hostname @property def port(self) -> Optional[int]: """Get the port.""" - return self._port + parsed = urlparse(self._uri_raw) + return parsed.port def __str__(self) -> str: """Get string representation.""" - return self.uri_raw + return self._uri_raw def __eq__(self, other: Any) -> bool: """Compare with another object.""" - return ( - isinstance(other, URI) - and self.scheme == other.scheme - and self.netloc == other.netloc - and self.path == other.path - and self.params == other.params - and self.query == other.query - and self.fragment == other.fragment - and self.username == other.username - and self.password == other.password - and self.host == other.host - and self.port == other.port - ) + return isinstance(other, URI) and str(self) == str(other) class EnvelopeContext: - """Extra information for the handling of an envelope.""" + """Contains context information of an envelope.""" + + __slots__ = ("_connection_id", "_uri") def __init__( - self, - connection_id: Optional[PublicId] = None, - skill_id: Optional[PublicId] = None, - uri: Optional[URI] = None, + self, connection_id: Optional[PublicId] = None, uri: Optional[URI] = None, ) -> 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. """ - 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 + self._connection_id = connection_id + self._uri = uri @property - def connection_id(self) -> Optional[PublicId]: - """Get the connection id.""" - return self._connection_id + def uri(self) -> Optional[URI]: + """Get the URI.""" + return self._uri @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. + def connection_id(self) -> Optional[PublicId]: + """Get the connection id to route the envelope.""" + return self._connection_id - :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: - _default_logger.debug( - f"URI - {uri.path} - not a valid package_id id. Error: {e}" - ) - return (skill_id, connection_id) + @connection_id.setter + def connection_id(self, connection_id: PublicId) -> None: + """Set the 'via' connection id.""" + if self._connection_id is not None: + raise ValueError("connection_id already set!") # pragma: nocover + self._connection_id = connection_id def __str__(self) -> str: """Get the string representation.""" - return f"EnvelopeContext(connection_id={self.connection_id}, skill_id={self.skill_id}, uri_raw={self.uri_raw})" + return f"EnvelopeContext(connection_id={self.connection_id}, uri={self.uri})" def __eq__(self, other: Any) -> bool: """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 ) +class AEAConnectionError(Exception): + """Exception class for connection errors.""" + + +class Empty(Exception): + """Exception for when the inbox is empty.""" + + class EnvelopeSerializer(ABC): """Abstract class to specify the serialization layer for the envelope.""" @@ -260,8 +210,8 @@ def encode(self, envelope: "Envelope") -> bytes: envelope_pb.sender = envelope.sender envelope_pb.protocol_id = str(envelope.protocol_specification_id) envelope_pb.message = envelope.message_bytes - if envelope.context is not None and envelope.context.uri_raw != "": - envelope_pb.uri = envelope.context.uri_raw + if envelope.context is not None and envelope.context.uri is not None: + envelope_pb.uri = str(envelope.context.uri) envelope_bytes = envelope_pb.SerializeToString() return envelope_bytes @@ -314,6 +264,8 @@ class Envelope: default_serializer = DefaultEnvelopeSerializer() + __slots__ = ("_to", "_sender", "_protocol_specification_id", "_message", "_context") + def __init__( self, to: Address, @@ -346,6 +298,11 @@ def __init__( self._to = to self._sender = sender + enforce( + self.is_to_public_id == self.is_sender_public_id, + "To and sender must either both be agent addresses or both be public ids of AEA components.", + ) + if isinstance(message, bytes): if protocol_specification_id is None: raise ValueError( @@ -368,7 +325,12 @@ def __init__( self._protocol_specification_id: PublicId = protocol_specification_id self._message = message - self._context = context if context is not None else EnvelopeContext() + if self.is_component_to_component_message: + enforce( + context is None, + "EnvelopeContext must be None for component to component messages.", + ) + self._context = context @property def to(self) -> Address: @@ -417,33 +379,14 @@ def message_bytes(self) -> bytes: return self._message @property - def context(self) -> EnvelopeContext: + def context(self) -> Optional[EnvelopeContext]: """Get the envelope context.""" return self._context @property - 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: - 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 + def to_as_public_id(self) -> Optional[PublicId]: + """Get to as public id.""" + return PublicId.try_from_str(self.to) @property def is_sender_public_id(self) -> bool: @@ -455,6 +398,11 @@ def is_to_public_id(self) -> bool: """Check if to is a public id.""" return PublicId.is_valid_str(self.to) + @property + def is_component_to_component_message(self) -> bool: + """Whether or not the message contained is component to component.""" + return self.is_to_public_id and self.is_sender_public_id + @staticmethod def _check_consistency(message: Message, to: str, sender: str) -> Message: """Check consistency of sender and to.""" diff --git a/aea/mail/base_pb2.py b/aea/mail/base_pb2.py index 9c36ba2092..281d3b9f77 100644 --- a/aea/mail/base_pb2.py +++ b/aea/mail/base_pb2.py @@ -1,9 +1,7 @@ +# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: base.proto -import sys - -_b = sys.version_info[0] < 3 and (lambda x: x) or (lambda x: x.encode("latin1")) from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection @@ -16,28 +14,27 @@ from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 + DESCRIPTOR = _descriptor.FileDescriptor( name="base.proto", - package="aea", + package="aea.base.v0_1_0", syntax="proto3", serialized_options=None, - serialized_pb=_b( - '\n\nbase.proto\x12\x03\x61\x65\x61\x1a\x1cgoogle/protobuf/struct.proto"\x90\x01\n\x0f\x44ialogueMessage\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\x0f\n\x07\x63ontent\x18\x05 \x01(\x0c"o\n\x07Message\x12\'\n\x04\x62ody\x18\x01 \x01(\x0b\x32\x17.google.protobuf.StructH\x00\x12\x30\n\x10\x64ialogue_message\x18\x02 \x01(\x0b\x32\x14.aea.DialogueMessageH\x00\x42\t\n\x07message"Y\n\x08\x45nvelope\x12\n\n\x02to\x18\x01 \x01(\t\x12\x0e\n\x06sender\x18\x02 \x01(\t\x12\x13\n\x0bprotocol_id\x18\x03 \x01(\t\x12\x0f\n\x07message\x18\x04 \x01(\x0c\x12\x0b\n\x03uri\x18\x05 \x01(\tb\x06proto3' - ), + serialized_pb=b'\n\nbase.proto\x12\x0f\x61\x65\x61.base.v0_1_0\x1a\x1cgoogle/protobuf/struct.proto"\x90\x01\n\x0f\x44ialogueMessage\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\x0f\n\x07\x63ontent\x18\x05 \x01(\x0c"{\n\x07Message\x12\'\n\x04\x62ody\x18\x01 \x01(\x0b\x32\x17.google.protobuf.StructH\x00\x12<\n\x10\x64ialogue_message\x18\x02 \x01(\x0b\x32 .aea.base.v0_1_0.DialogueMessageH\x00\x42\t\n\x07message"Y\n\x08\x45nvelope\x12\n\n\x02to\x18\x01 \x01(\t\x12\x0e\n\x06sender\x18\x02 \x01(\t\x12\x13\n\x0bprotocol_id\x18\x03 \x01(\t\x12\x0f\n\x07message\x18\x04 \x01(\x0c\x12\x0b\n\x03uri\x18\x05 \x01(\tb\x06proto3', dependencies=[google_dot_protobuf_dot_struct__pb2.DESCRIPTOR,], ) _DIALOGUEMESSAGE = _descriptor.Descriptor( name="DialogueMessage", - full_name="aea.DialogueMessage", + full_name="aea.base.v0_1_0.DialogueMessage", filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name="message_id", - full_name="aea.DialogueMessage.message_id", + full_name="aea.base.v0_1_0.DialogueMessage.message_id", index=0, number=1, type=5, @@ -55,14 +52,14 @@ ), _descriptor.FieldDescriptor( name="dialogue_starter_reference", - full_name="aea.DialogueMessage.dialogue_starter_reference", + full_name="aea.base.v0_1_0.DialogueMessage.dialogue_starter_reference", index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, - default_value=_b("").decode("utf-8"), + default_value=b"".decode("utf-8"), message_type=None, enum_type=None, containing_type=None, @@ -73,14 +70,14 @@ ), _descriptor.FieldDescriptor( name="dialogue_responder_reference", - full_name="aea.DialogueMessage.dialogue_responder_reference", + full_name="aea.base.v0_1_0.DialogueMessage.dialogue_responder_reference", index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, - default_value=_b("").decode("utf-8"), + default_value=b"".decode("utf-8"), message_type=None, enum_type=None, containing_type=None, @@ -91,7 +88,7 @@ ), _descriptor.FieldDescriptor( name="target", - full_name="aea.DialogueMessage.target", + full_name="aea.base.v0_1_0.DialogueMessage.target", index=3, number=4, type=5, @@ -109,14 +106,14 @@ ), _descriptor.FieldDescriptor( name="content", - full_name="aea.DialogueMessage.content", + full_name="aea.base.v0_1_0.DialogueMessage.content", index=4, number=5, type=12, cpp_type=9, label=1, has_default_value=False, - default_value=_b(""), + default_value=b"", message_type=None, enum_type=None, containing_type=None, @@ -134,21 +131,21 @@ syntax="proto3", extension_ranges=[], oneofs=[], - serialized_start=50, - serialized_end=194, + serialized_start=62, + serialized_end=206, ) _MESSAGE = _descriptor.Descriptor( name="Message", - full_name="aea.Message", + full_name="aea.base.v0_1_0.Message", filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name="body", - full_name="aea.Message.body", + full_name="aea.base.v0_1_0.Message.body", index=0, number=1, type=11, @@ -166,7 +163,7 @@ ), _descriptor.FieldDescriptor( name="dialogue_message", - full_name="aea.Message.dialogue_message", + full_name="aea.base.v0_1_0.Message.dialogue_message", index=1, number=2, type=11, @@ -193,34 +190,34 @@ oneofs=[ _descriptor.OneofDescriptor( name="message", - full_name="aea.Message.message", + full_name="aea.base.v0_1_0.Message.message", index=0, containing_type=None, fields=[], ), ], - serialized_start=196, - serialized_end=307, + serialized_start=208, + serialized_end=331, ) _ENVELOPE = _descriptor.Descriptor( name="Envelope", - full_name="aea.Envelope", + full_name="aea.base.v0_1_0.Envelope", filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name="to", - full_name="aea.Envelope.to", + full_name="aea.base.v0_1_0.Envelope.to", index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, - default_value=_b("").decode("utf-8"), + default_value=b"".decode("utf-8"), message_type=None, enum_type=None, containing_type=None, @@ -231,14 +228,14 @@ ), _descriptor.FieldDescriptor( name="sender", - full_name="aea.Envelope.sender", + full_name="aea.base.v0_1_0.Envelope.sender", index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, - default_value=_b("").decode("utf-8"), + default_value=b"".decode("utf-8"), message_type=None, enum_type=None, containing_type=None, @@ -249,14 +246,14 @@ ), _descriptor.FieldDescriptor( name="protocol_id", - full_name="aea.Envelope.protocol_id", + full_name="aea.base.v0_1_0.Envelope.protocol_id", index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, - default_value=_b("").decode("utf-8"), + default_value=b"".decode("utf-8"), message_type=None, enum_type=None, containing_type=None, @@ -267,14 +264,14 @@ ), _descriptor.FieldDescriptor( name="message", - full_name="aea.Envelope.message", + full_name="aea.base.v0_1_0.Envelope.message", index=3, number=4, type=12, cpp_type=9, label=1, has_default_value=False, - default_value=_b(""), + default_value=b"", message_type=None, enum_type=None, containing_type=None, @@ -285,14 +282,14 @@ ), _descriptor.FieldDescriptor( name="uri", - full_name="aea.Envelope.uri", + full_name="aea.base.v0_1_0.Envelope.uri", index=4, number=5, type=9, cpp_type=9, label=1, has_default_value=False, - default_value=_b("").decode("utf-8"), + default_value=b"".decode("utf-8"), message_type=None, enum_type=None, containing_type=None, @@ -310,8 +307,8 @@ syntax="proto3", extension_ranges=[], oneofs=[], - serialized_start=309, - serialized_end=398, + serialized_start=333, + serialized_end=422, ) _MESSAGE.fields_by_name[ @@ -334,33 +331,33 @@ DialogueMessage = _reflection.GeneratedProtocolMessageType( "DialogueMessage", (_message.Message,), - dict( - DESCRIPTOR=_DIALOGUEMESSAGE, - __module__="base_pb2" - # @@protoc_insertion_point(class_scope:aea.DialogueMessage) - ), + { + "DESCRIPTOR": _DIALOGUEMESSAGE, + "__module__": "base_pb2" + # @@protoc_insertion_point(class_scope:aea.base.v0_1_0.DialogueMessage) + }, ) _sym_db.RegisterMessage(DialogueMessage) Message = _reflection.GeneratedProtocolMessageType( "Message", (_message.Message,), - dict( - DESCRIPTOR=_MESSAGE, - __module__="base_pb2" - # @@protoc_insertion_point(class_scope:aea.Message) - ), + { + "DESCRIPTOR": _MESSAGE, + "__module__": "base_pb2" + # @@protoc_insertion_point(class_scope:aea.base.v0_1_0.Message) + }, ) _sym_db.RegisterMessage(Message) Envelope = _reflection.GeneratedProtocolMessageType( "Envelope", (_message.Message,), - dict( - DESCRIPTOR=_ENVELOPE, - __module__="base_pb2" - # @@protoc_insertion_point(class_scope:aea.Envelope) - ), + { + "DESCRIPTOR": _ENVELOPE, + "__module__": "base_pb2" + # @@protoc_insertion_point(class_scope:aea.base.v0_1_0.Envelope) + }, ) _sym_db.RegisterMessage(Envelope) diff --git a/aea/manager/manager.py b/aea/manager/manager.py index 2879a3c1f4..16f55e4a3f 100644 --- a/aea/manager/manager.py +++ b/aea/manager/manager.py @@ -22,6 +22,7 @@ import os import threading from asyncio.tasks import FIRST_COMPLETED +from collections import defaultdict from shutil import rmtree from threading import Thread from typing import Any, Callable, Dict, List, Optional, Set, Tuple @@ -33,6 +34,19 @@ from aea.manager.project import AgentAlias, Project +class ProjectNotFoundError(ValueError): + """Project not found exception.""" + + +class ProjectCheckError(ValueError): + """Project check error exception.""" + + def __init__(self, msg: str, source_exception: Exception): + """Init exception.""" + super().__init__(msg) + self.source_exception = source_exception + + class AgentRunAsyncTask: """Async task wrapper for agent.""" @@ -138,13 +152,20 @@ def __init__( working_dir: str, mode: str = "async", registry_path: str = DEFAULT_REGISTRY_NAME, + auto_add_remove_project: bool = False, ) -> None: """ Initialize manager. :param working_dir: directory to store base agents. + :param mode: str. async or threaded + :param registry_path: str. path to the local packages registry + :param auto_add_remove_project: bool. add/remove project on the first agent add/last agent remove + + :return: None """ self.working_dir = working_dir + self._auto_add_remove_project = auto_add_remove_project self._save_path = os.path.join(self.working_dir, self.SAVE_FILENAME) self.registry_path = registry_path @@ -163,6 +184,13 @@ def __init__( self._event: Optional[asyncio.Event] = None self._error_callbacks: List[Callable[[str, BaseException], None]] = [] + self._last_start_status: Optional[ + Tuple[ + bool, + Dict[PublicId, List[Dict]], + List[Tuple[PublicId, List[Dict], Exception]], + ] + ] = None if mode not in self.MODES: raise ValueError( @@ -249,13 +277,14 @@ def start_manager( :param local: whether or not to fetch from local registry. :param remote: whether or not to fetch from remote registry. + :return: the MultiAgentManager instance. """ if self._is_running: return self self._ensure_working_dir() - self._load_state(local=local, remote=remote) + self._last_start_status = self._load_state(local=local, remote=remote) self._started_event.clear() self._is_running = True @@ -264,6 +293,17 @@ def start_manager( self._started_event.wait(self.DEFAULT_TIMEOUT_FOR_BLOCKING_OPERATIONS) return self + @property + def last_start_status( + self, + ) -> Tuple[ + bool, Dict[PublicId, List[Dict]], List[Tuple[PublicId, List[Dict], Exception]], + ]: + """Get status of the last agents start loading state.""" + if self._last_start_status is None: + raise ValueError("Manager was not started") + return self._last_start_status + def stop_manager( self, cleanup: bool = True, save: bool = False ) -> "MultiAgentManager": @@ -292,7 +332,7 @@ def stop_manager( self._save_state() for agent_name in self.list_agents(): - self.remove_agent(agent_name) + self.remove_agent(agent_name, skip_project_auto_remove=True) if cleanup: for project in list(self._projects.keys()): @@ -332,6 +372,7 @@ def add_project( registry, and then from remote registry in case of failure). :param public_id: the public if of the agent project. + :param local: whether or not to fetch from local registry. :param remote: whether or not to fetch from remote registry. :param restore: bool flag for restoring already fetched agent. @@ -356,6 +397,14 @@ def add_project( project.install_pypi_dependencies() project.build() + try: + project.check() + except Exception as e: + project.remove() + raise ProjectCheckError( + f"Failed to load project: {public_id} Error: {str(e)}", e + ) + self._projects[public_id] = project return self @@ -392,6 +441,9 @@ def add_agent( agent_name: Optional[str] = None, agent_overrides: Optional[dict] = None, component_overrides: Optional[List[dict]] = None, + local: bool = False, + remote: bool = False, + restore: bool = False, ) -> "MultiAgentManager": """ Create new agent configuration based on project with config overrides applied. @@ -404,6 +456,10 @@ def add_agent( :param component_overrides: overrides for component section. :param config: agent config (used for agent re-creation). + :param local: whether or not to fetch from local registry. + :param remote: whether or not to fetch from remote registry. + :param restore: bool flag for restoring already fetched agent. + :return: manager """ agent_name = agent_name or public_id.name @@ -411,10 +467,14 @@ def add_agent( if agent_name in self._agents: raise ValueError(f"Agent with name {agent_name} already exists!") - if public_id not in self._projects: - raise ValueError(f"{public_id} project is not added!") + project = self._projects.get(public_id, None) - project = self._projects[public_id] + if project is None and self._auto_add_remove_project: + self.add_project(public_id, local, remote, restore) + project = self._projects.get(public_id, None) + + if project is None: + raise ProjectNotFoundError(f"{public_id} project is not added!") agent_alias = AgentAlias( project=project, @@ -524,11 +584,14 @@ def list_agents(self, running_only: bool = False) -> List[str]: return [i for i in self._agents.keys() if self._is_agent_running(i)] return list(self._agents.keys()) - def remove_agent(self, agent_name: str) -> "MultiAgentManager": + def remove_agent( + self, agent_name: str, skip_project_auto_remove: bool = False + ) -> "MultiAgentManager": """ Remove agent alias definition from registry. :param agent_name: agent name to remove + :param skip_project_auto_remove: disable auto project remove on last agent removed. :return: None """ @@ -540,6 +603,15 @@ def remove_agent(self, agent_name: str) -> "MultiAgentManager": agent_alias = self._agents.pop(agent_name) agent_alias.remove_from_project() + project: Project = agent_alias.project + + if ( + not project.agents + and self._auto_add_remove_project + and not skip_project_auto_remove + ): + self.remove_project(project.public_id, keep_files=False) + return self def start_agent(self, agent_name: str) -> "MultiAgentManager": @@ -692,7 +764,11 @@ def _ensure_working_dir(self) -> None: if not os.path.exists(self.data_dir): os.makedirs(self.data_dir) - def _load_state(self, local: bool, remote: bool) -> None: + def _load_state( + self, local: bool, remote: bool + ) -> Tuple[ + bool, Dict[PublicId, List[Dict]], List[Tuple[PublicId, List[Dict], Exception]], + ]: """ Load saved state from file. @@ -709,32 +785,44 @@ def _load_state(self, local: bool, remote: bool) -> None: :raises: ValueError if failed to load state. """ if not os.path.exists(self._save_path): - return + return False, {}, [] save_json = {} with open_file(self._save_path) as f: save_json = json.load(f) if not save_json: - return # pragma: nocover + return False, {}, [] # pragma: nocover - try: - for public_id in save_json["projects"]: + projects_agents: Dict[PublicId, List] = defaultdict(list) + + for agent_settings in save_json["agents"]: + projects_agents[PublicId.from_str(agent_settings["public_id"])].append( + agent_settings + ) + + failed_to_load: List[Tuple[PublicId, List[Dict], Exception]] = [] + loaded_ok: Dict[PublicId, List[Dict]] = {} + for project_public_id, agents_settings in projects_agents.items(): + try: self.add_project( - PublicId.from_str(public_id), - local=local, - remote=remote, - restore=True, + project_public_id, local=local, remote=remote, restore=True, ) + except ProjectCheckError as e: + failed_to_load.append((project_public_id, agents_settings, e)) + break + + for agent_settings in agents_settings: - for agent_settings in save_json["agents"]: self.add_agent_with_config( public_id=PublicId.from_str(agent_settings["public_id"]), agent_name=agent_settings["agent_name"], config=agent_settings["config"], ) - except ValueError as e: # pragma: nocover - raise ValueError(f"Failed to load state. {e}") + + loaded_ok[project_public_id] = agents_settings + + return True, loaded_ok, failed_to_load def _save_state(self) -> None: """ diff --git a/aea/manager/project.py b/aea/manager/project.py index 8bab91ff5c..8294d74d03 100644 --- a/aea/manager/project.py +++ b/aea/manager/project.py @@ -41,7 +41,9 @@ class _Base: @classmethod def _get_agent_config(cls, path: Union[Path, str]) -> AgentConfig: """Get agent config instance.""" - return AEABuilder.try_to_load_agent_configuration_file(path) + agent_config = AEABuilder.try_to_load_agent_configuration_file(path) + agent_config.check_aea_version() + return agent_config @classmethod def _get_builder( @@ -72,6 +74,8 @@ def install_pypi_dependencies(self) -> None: class Project(_Base): """Agent project representation.""" + __slots__ = ("public_id", "path", "agents") + def __init__(self, public_id: PublicId, path: str) -> None: """Init project with public_id and project's path.""" self.public_id: PublicId = public_id @@ -126,10 +130,16 @@ def builder(self) -> AEABuilder: """Get builder instance.""" return self._get_builder(self._get_agent_config(self.path), self.path) + def check(self) -> None: + """Check we can still construct an AEA from the project with builder.build.""" + _ = self.builder + class AgentAlias(_Base): """Agent alias representation.""" + __slots__ = ("project", "agent_name", "_data_dir", "_agent_config") + def __init__( self, project: Project, agent_name: str, data_dir: str, ): diff --git a/aea/multiplexer.py b/aea/multiplexer.py index c336af4d96..1817778b95 100644 --- a/aea/multiplexer.py +++ b/aea/multiplexer.py @@ -37,6 +37,7 @@ cast, ) +from aea.common import Address from aea.configurations.base import PublicId from aea.connections.base import Connection, ConnectionStates from aea.exceptions import enforce @@ -141,6 +142,7 @@ def __init__( self._specification_id_to_protocol_id = { p.protocol_specification_id: p.protocol_id for p in protocols or [] } + self._routing_helper: Dict[Address, PublicId] = {} self._in_queue = AsyncFriendlyQueue() # type: AsyncFriendlyQueue self._out_queue = None # type: Optional[asyncio.Queue] @@ -482,10 +484,7 @@ async def _send_loop(self) -> None: ) return None self.logger.debug("Sending envelope {}".format(str(envelope))) - try: - await self._send(envelope) - except AEAConnectionError as e: - self.logger.error(str(e)) + await self._send(envelope) except asyncio.CancelledError: self.logger.debug("Sending loop cancelled.") @@ -509,12 +508,13 @@ async def _receiving_loop(self) -> None: # process completed receiving tasks. for task in done: + connection = task_to_connection.pop(task) envelope = task.result() if envelope is not None: + self._update_routing_helper(envelope, connection) self.in_queue.put_nowait(envelope) # reinstantiate receiving task, but only if the connection is still up. - connection = task_to_connection.pop(task) if connection.is_connected: new_task = asyncio.ensure_future(connection.receive()) task_to_connection[new_task] = connection @@ -538,36 +538,18 @@ async def _send(self, envelope: Envelope) -> None: :param envelope: the envelope to send. :return: None :raises ValueError: if the connection id provided is not valid. - :raises AEAConnectionError: if the connection id provided is not valid. """ - connection_id = None # type: Optional[PublicId] - envelope_context = envelope.context - # first, try to route by context - if envelope_context is not None: - connection_id = envelope_context.connection_id - envelope_protocol_id = self._get_protocol_id_for_envelope(envelope) + connection_id = self._get_connection_id_from_envelope( + envelope, envelope_protocol_id + ) - # second, try to route by default routing - if connection_id is None and envelope_protocol_id in self.default_routing: - connection_id = self.default_routing[envelope_protocol_id] - self.logger.debug("Using default routing: {}".format(connection_id)) - - if connection_id is not None and connection_id not in self._id_to_connection: - raise AEAConnectionError( - "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) - ) - connection = self.default_connection - else: - connection = self._id_to_connection[connection_id] + connection = ( + self._get_connection(connection_id) if connection_id is not None else None + ) if connection is None: + # we don't raise on dropping envelope as this can be a configuration issue only! self.logger.warning( f"Dropping envelope, no connection available for sending: {envelope}" ) @@ -581,6 +563,79 @@ async def _send(self, envelope: Envelope) -> None: except Exception as e: # pylint: disable=broad-except self._handle_exception(self._send, e) + def _get_connection_id_from_envelope( + self, envelope: Envelope, envelope_protocol_id: PublicId + ) -> Optional[PublicId]: + """ + Get the connection id from an envelope. + + Applies the following rules: + - component to component messages are routed by their component id + - agent to agent messages, are routed following four rules: + * first, try to route by envelope context connection id + * second, try to route by routing helper + * third, try to route by default routing + * forth, using default connection + + :param envelope: the Envelope + :param envelope_protocol_id: the protocol id of the message contained in the envelope + :return: public id if found + """ + # component to component messages are routed by their component id + if envelope.is_component_to_component_message: + connection_id = envelope.to_as_public_id + self.logger.debug( + "Using envelope `to` field as connection_id: {}".format(connection_id) + ) + enforce( + connection_id is not None, + "Connection id cannot be None by envelope construction.", + ) + return connection_id + + # agent to agent messages, are routed following four rules: + # first, try to route by envelope context connection id + if envelope.context is not None and envelope.context.connection_id is not None: + connection_id = envelope.context.connection_id + self.logger.debug( + "Using envelope context connection_id: {}".format(connection_id) + ) + return connection_id + + # second, try to route by routing helper + if envelope.to in self._routing_helper: + connection_id = self._routing_helper[envelope.to] + self.logger.debug( + "Using routing helper with connection_id: {}".format(connection_id) + ) + return connection_id + + # third, try to route by default routing + if envelope_protocol_id in self.default_routing: + connection_id = self.default_routing[envelope_protocol_id] + self.logger.debug("Using default routing: {}".format(connection_id)) + return connection_id + + # forth, using default connection + connection_id = ( + self.default_connection.connection_id + if self.default_connection is not None + else None + ) + self.logger.debug("Using default connection: {}".format(connection_id)) + return connection_id + + def _get_connection(self, connection_id: PublicId) -> Optional[Connection]: + """Check if the connection id is registered.""" + conn_ = self._id_to_connection.get(connection_id, None) + if conn_ is not None: + return conn_ + for id_, conn_ in self._id_to_connection.items(): + if id_.same_prefix(connection_id): + return conn_ + self.logger.error(f"No connection registered with id: {connection_id}") + return None + def _is_connection_supported_protocol( self, connection: Connection, protocol_id: PublicId ) -> bool: @@ -686,10 +741,28 @@ def _setup( for c in connections: self.add_connection(c, c.public_id == default_connection) + def _update_routing_helper( + self, envelope: Envelope, connection: Connection + ) -> None: + """ + Update the routing helper. + + Saves the source (connection) of an agent-to-agent envelope. + + :param envelope: the envelope to be updated + :param connection: the connection + """ + if envelope.is_component_to_component_message: + return + self._routing_helper[envelope.sender] = connection.public_id + class Multiplexer(AsyncMultiplexer): """Transit sync multiplexer for compatibility.""" + _thread_was_started: bool + _is_connected: bool + def __init__(self, *args: Any, **kwargs: Any) -> None: """ Initialize the connection multiplexer. @@ -702,6 +775,10 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: """ super().__init__(*args, **kwargs) self._sync_lock = threading.Lock() + self._init() + + def _init(self) -> None: + """Set initial variables.""" self._thread_was_started = False self._is_connected = False @@ -748,8 +825,13 @@ def disconnect(self) -> None: # type: ignore # cause overrides coroutine # pyli if self._thread_runner.is_alive() and self._thread_was_started: self._thread_runner.stop() self.logger.debug("Thread stopped") + self.logger.debug("Disconnected") + # reset thread runner and init variables + self._init() + self.set_loop(self._loop) + def put(self, envelope: Envelope) -> None: # type: ignore # cause overrides coroutine """ Schedule an envelope for sending it. diff --git a/aea/protocols/base.py b/aea/protocols/base.py index 23960d485d..c43d1b7925 100644 --- a/aea/protocols/base.py +++ b/aea/protocols/base.py @@ -334,6 +334,8 @@ class Protocol(Component): It includes a serializer to encode/decode a message. """ + __slots__ = ("_message_class",) + def __init__( self, configuration: ProtocolConfig, message_class: Type[Message], **kwargs: Any ) -> None: diff --git a/aea/protocols/dialogue/base.py b/aea/protocols/dialogue/base.py index 9d79740e0d..a7563165b8 100644 --- a/aea/protocols/dialogue/base.py +++ b/aea/protocols/dialogue/base.py @@ -16,7 +16,6 @@ # limitations under the License. # # ------------------------------------------------------------------------------ - """ This module contains the classes required for dialogue management. @@ -132,11 +131,7 @@ def dialogue_starter_addr(self) -> str: def __eq__(self, other: Any) -> bool: """Check for equality between two DialogueLabel objects.""" if isinstance(other, DialogueLabel): - return ( - self.dialogue_reference == other.dialogue_reference - and self.dialogue_starter_addr == other.dialogue_starter_addr - and self.dialogue_opponent_addr == other.dialogue_opponent_addr - ) + return hash(self) == hash(other) return False def __hash__(self) -> int: @@ -211,8 +206,7 @@ class _DialogueMeta(type): """ Metaclass for Dialogue. - Adds slot support forevery subclass - Creates classlevvel Rules instance + Creates class level Rules instance to share among instances """ def __new__(cls, name: str, bases: Tuple[Type], dct: Dict) -> "_DialogueMeta": @@ -241,6 +235,18 @@ class Dialogue(metaclass=_DialogueMeta): dict() ) # type: Dict[Message.Performative, FrozenSet[Message.Performative]] + __slots__ = ( + "_self_address", + "_dialogue_label", + "_role", + "_message_class", + "_outgoing_messages", + "_incoming_messages", + "_terminal_state_callbacks", + "_last_message_id", + "_ordered_message_ids", + ) + class Rules: """This class defines the rules for the dialogue.""" @@ -443,11 +449,11 @@ def incomplete_dialogue_label(self) -> DialogueLabel: @property def dialogue_labels(self) -> Set[DialogueLabel]: """ - Get the dialogue labels (incomplete and complete, if it exists) + Get the dialogue labels (incomplete and complete, if it exists). :return: the dialogue labels """ - return {self._dialogue_label, self.incomplete_dialogue_label} + return {self.dialogue_label, self.incomplete_dialogue_label} @property def self_address(self) -> Address: @@ -689,7 +695,10 @@ def reply( if last_message is None: raise ValueError("Cannot reply in an empty dialogue!") - if target_message is None and target is None: + if target_message is None and target is not None: + target_message = self.get_message_by_id(target) + elif target_message is None and target is None: + target_message = last_message target = last_message.message_id elif target_message is not None and target is None: target = target_message.message_id @@ -699,6 +708,8 @@ def reply( "The provided target and target_message do not match." ) + if target_message is None: + raise ValueError("No target message found!") enforce( self._has_message_id(target), # type: ignore "The target message does not exist in this dialogue.", diff --git a/aea/protocols/generator/base.py b/aea/protocols/generator/base.py index 7d3b87c1d9..6bd39273f1 100644 --- a/aea/protocols/generator/base.py +++ b/aea/protocols/generator/base.py @@ -1981,9 +1981,7 @@ def generate_protobuf_only_mode( self.protocol_specification.name, ) if not is_correctly_formatted and protolint_output != "": - protobuf_output = ( - "Protolint warnings:\n" + protolint_output - ) # pragma: no cover + protobuf_output = "Protolint warnings:\n" + protolint_output # Run black and isort formatting for python if language == PROTOCOL_LANGUAGE_PYTHON: @@ -2064,9 +2062,7 @@ def generate_full_mode(self, language: str) -> Optional[str]: self.spec.all_custom_types, CUSTOM_TYPES_DOT_PY_FILE_NAME ) if full_mode_output is not None: - full_mode_output += ( - incomplete_generation_warning_msg # pragma: no cover - ) + full_mode_output += incomplete_generation_warning_msg else: full_mode_output = incomplete_generation_warning_msg return full_mode_output diff --git a/aea/protocols/scaffold/protocol.yaml b/aea/protocols/scaffold/protocol.yaml index cf12156762..729ea57dd5 100644 --- a/aea/protocols/scaffold/protocol.yaml +++ b/aea/protocols/scaffold/protocol.yaml @@ -4,7 +4,7 @@ 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.10.0, <0.11.0' +aea_version: '>=0.11.0, <0.12.0' protocol_specification_id: fetchai/scaffold:0.1.0 fingerprint: __init__.py: Qmc9Ln8THrWmwou4nr3Acag7vcZ1fv8v5oRSkCWtv1aH6t diff --git a/aea/registries/base.py b/aea/registries/base.py index 12f2d31793..1c79fa6bb2 100644 --- a/aea/registries/base.py +++ b/aea/registries/base.py @@ -122,6 +122,8 @@ class PublicIdRegistry(Generic[Item], Registry[PublicId, Item]): points to the 'latest' version of a package. """ + __slots__ = ("_public_id_to_item",) + def __init__(self) -> None: """Initialize the registry.""" super().__init__() @@ -187,6 +189,8 @@ def teardown(self) -> None: class AgentComponentRegistry(Registry[ComponentId, Component]): """This class implements a simple dictionary-based registry for agent components.""" + __slots__ = ("_components_by_type", "_registered_keys") + def __init__(self, **kwargs: Any) -> None: """ Instantiate the registry. @@ -329,6 +333,8 @@ class ComponentRegistry( ): """This class implements a generic registry for skill components.""" + __slots__ = ("_items", "_dynamically_added") + def __init__(self, **kwargs: Any) -> None: """ Instantiate the registry. @@ -517,6 +523,8 @@ def teardown(self) -> None: class HandlerRegistry(ComponentRegistry[Handler]): """This class implements the handlers registry.""" + __slots__ = ("_items_by_protocol_and_skill",) + def __init__(self, **kwargs: Any) -> None: """ Instantiate the registry. diff --git a/aea/registries/resources.py b/aea/registries/resources.py index 06aabf8c3f..5d7b1195e9 100644 --- a/aea/registries/resources.py +++ b/aea/registries/resources.py @@ -38,6 +38,16 @@ class Resources: """This class implements the object that holds the resources of an AEA.""" + __slots__ = ( + "_agent_name", + "_component_registry", + "_specification_to_protocol_id", + "_handler_registry", + "_behaviour_registry", + "_model_registry", + "_registries", + ) + def __init__(self, agent_name: str = "standalone") -> None: """ Instantiate the resources. diff --git a/aea/runtime.py b/aea/runtime.py index 00614b353a..b065496550 100644 --- a/aea/runtime.py +++ b/aea/runtime.py @@ -25,7 +25,13 @@ from typing import Dict, Optional, Type, cast from aea.abstract_agent import AbstractAgent -from aea.agent_loop import AsyncAgentLoop, AsyncState, BaseAgentLoop, SyncAgentLoop +from aea.agent_loop import ( + AgentLoopStates, + AsyncAgentLoop, + AsyncState, + BaseAgentLoop, + SyncAgentLoop, +) from aea.connections.base import ConnectionStates from aea.decision_maker.base import DecisionMaker, DecisionMakerHandler from aea.exceptions import _StopRuntime @@ -238,6 +244,8 @@ def set_loop(self, loop: AbstractEventLoop) -> None: class AsyncRuntime(BaseRuntime): """Asynchronous runtime: uses asyncio loop for multiplexer and async agent main loop.""" + AGENT_LOOP_STARTED_TIMEOUT: float = 5 + def __init__( self, agent: AbstractAgent, @@ -351,7 +359,14 @@ async def _start_agent_loop(self) -> None: self.logger.debug("[{}] Calling setup method...".format(self._agent.name)) self._agent.setup() self.logger.debug("[{}] Run main loop...".format(self._agent.name)) + self.agent_loop.start() + + await asyncio.wait_for( + self.agent_loop.wait_state(AgentLoopStates.started), + timeout=self.AGENT_LOOP_STARTED_TIMEOUT, + ) + self._state.set(RuntimeStates.running) try: await self.agent_loop.wait_completed() diff --git a/aea/skills/base.py b/aea/skills/base.py index 6c66f6ed8a..8c90dd1b5a 100644 --- a/aea/skills/base.py +++ b/aea/skills/base.py @@ -22,12 +22,14 @@ import logging import queue import re +import types from abc import ABC, abstractmethod +from copy import copy from logging import Logger from pathlib import Path from queue import Queue from types import SimpleNamespace -from typing import Any, Dict, Optional, Sequence, Set, Tuple, Type, cast +from typing import Any, Dict, List, Optional, Set, Tuple, Type, Union, cast from aea.common import Address from aea.components.base import Component, load_aea_package @@ -42,15 +44,16 @@ from aea.exceptions import ( AEAActException, AEAComponentLoadException, - AEAException, AEAHandleException, AEAInstantiationException, _StopRuntime, + enforce, parse_exception, ) from aea.helpers.base import _get_aea_logger_name_prefix, load_module from aea.helpers.logging import AgentLoggerAdapter from aea.helpers.storage.generic_storage import Storage +from aea.mail.base import Envelope, EnvelopeContext from aea.multiplexer import MultiplexerStatus, OutBox from aea.protocols.base import Message from aea.skills.tasks import TaskManager @@ -252,6 +255,23 @@ def __getattr__(self, item: Any) -> Any: """Get attribute.""" return super().__getattribute__(item) # pragma: no cover + def send_to_skill( + self, + message_or_envelope: Union[Message, Envelope], + context: Optional[EnvelopeContext] = None, + ) -> None: + """ + Send message or envelope to another skill. + + :param message_or_envelope: envelope to send to another skill. + if message passed it will be wrapped into envelope with optional envelope context. + + :return: None + """ + if self._agent_context is None: # pragma: nocover + raise ValueError("agent context was not set!") + self._agent_context.send_to_skill(message_or_envelope, context) + class SkillComponent(ABC): """This class defines an abstract interface for skill component classes.""" @@ -409,70 +429,7 @@ def parse_module( # pylint: disable=arguments-differ :param skill_context: the skill context :return: a list of Behaviour. """ - behaviours = {} # type: Dict[str, "Behaviour"] - if behaviour_configs == {}: - return behaviours - behaviour_names = set( - config.class_name for _, config in behaviour_configs.items() - ) - behaviour_module = load_module("behaviours", Path(path)) - classes = inspect.getmembers(behaviour_module, inspect.isclass) - behaviours_classes = list( - filter( - lambda x: any( - re.match(behaviour, x[0]) for behaviour in behaviour_names - ) - and not str.startswith(x[1].__module__, "aea.") - and not str.startswith( - x[1].__module__, - f"packages.{skill_context.skill_id.author}.skills.{skill_context.skill_id.name}", - ), - classes, - ) - ) - - name_to_class = dict(behaviours_classes) - _print_warning_message_for_non_declared_skill_components( - skill_context, - set(name_to_class.keys()), - { - behaviour_config.class_name - for behaviour_config in behaviour_configs.values() - }, - "behaviours", - path, - ) - - for behaviour_id, behaviour_config in behaviour_configs.items(): - behaviour_class_name = cast(str, behaviour_config.class_name) - skill_context.logger.debug( - "Processing behaviour {}".format(behaviour_class_name) - ) - if not behaviour_id.isidentifier(): - raise AEAComponentLoadException( # pragma: nocover - f"'{behaviour_id}' is not a valid identifier." - ) - behaviour_class = name_to_class.get(behaviour_class_name, None) - if behaviour_class is None: - skill_context.logger.warning( - "Behaviour '{}' cannot be found.".format(behaviour_class_name) - ) - else: - try: - behaviour = behaviour_class( - name=behaviour_id, - configuration=behaviour_config, - skill_context=skill_context, - **dict(behaviour_config.args), - ) - except Exception as e: # pylint: disable=broad-except # pragma: nocover - e_str = parse_exception(e) - raise AEAInstantiationException( - f"An error occured during instantiation of behaviour {skill_context.skill_id}/{behaviour_config.class_name}:\n{e_str}" - ) - behaviours[behaviour_id] = behaviour - - return behaviours + return _parse_module(path, behaviour_configs, skill_context, Behaviour) class Handler(SkillComponent, ABC): @@ -516,62 +473,7 @@ def parse_module( # pylint: disable=arguments-differ :param skill_context: the skill context :return: an handler, or None if the parsing fails. """ - handlers = {} # type: Dict[str, "Handler"] - if handler_configs == {}: - return handlers - handler_names = set(config.class_name for _, config in handler_configs.items()) - handler_module = load_module("handlers", Path(path)) - classes = inspect.getmembers(handler_module, inspect.isclass) - handler_classes = list( - filter( - lambda x: any(re.match(handler, x[0]) for handler in handler_names) - and not str.startswith(x[1].__module__, "aea.") - and not str.startswith( - x[1].__module__, - f"packages.{skill_context.skill_id.author}.skills.{skill_context.skill_id.name}", - ), - classes, - ) - ) - - name_to_class = dict(handler_classes) - _print_warning_message_for_non_declared_skill_components( - skill_context, - set(name_to_class.keys()), - {handler_config.class_name for handler_config in handler_configs.values()}, - "handlers", - path, - ) - for handler_id, handler_config in handler_configs.items(): - handler_class_name = cast(str, handler_config.class_name) - skill_context.logger.debug( - "Processing handler {}".format(handler_class_name) - ) - if not handler_id.isidentifier(): - raise AEAComponentLoadException( # pragma: nocover - f"'{handler_id}' is not a valid identifier." - ) - handler_class = name_to_class.get(handler_class_name, None) - if handler_class is None: - skill_context.logger.warning( - "Handler '{}' cannot be found.".format(handler_class_name) - ) - else: - try: - handler = handler_class( - name=handler_id, - configuration=handler_config, - skill_context=skill_context, - **dict(handler_config.args), - ) - except Exception as e: # pylint: disable=broad-except # pragma: nocover - e_str = parse_exception(e) - raise AEAInstantiationException( - f"An error occured during instantiation of handler {skill_context.skill_id}/{handler_config.class_name}:\n{e_str}" - ) - handlers[handler_id] = handler - - return handlers + return _parse_module(path, handler_configs, skill_context, Handler) class Model(SkillComponent, ABC): @@ -621,113 +523,21 @@ def parse_module( # pylint: disable=arguments-differ skill_context: SkillContext, ) -> Dict[str, "Model"]: """ - Parse the tasks module. + Parse the model module. :param path: path to the Python skill module. :param model_configs: a list of model configurations. :param skill_context: the skill context :return: a list of Model. """ - instances = {} # type: Dict[str, "Model"] - if model_configs == {}: - return instances - models = [] - - model_names = set(config.class_name for _, config in model_configs.items()) - - # get all Python modules except the standard ones - ignore_regex = "|".join(["handlers.py", "behaviours.py", "tasks.py", "__.*"]) - all_python_modules = Path(path).glob("*.py") - module_paths = set( - map( - str, - filter( - lambda x: not re.match(ignore_regex, x.name), all_python_modules - ), - ) - ) - - for module_path in module_paths: - skill_context.logger.debug("Trying to load module {}".format(module_path)) - module_name = module_path.replace(".py", "") - model_module = load_module(module_name, Path(module_path)) - classes = inspect.getmembers(model_module, inspect.isclass) - filtered_classes = list( - filter( - lambda x: any(re.match(model, x[0]) for model in model_names) - and issubclass(x[1], Model) - and not str.startswith(x[1].__module__, "aea.") - and not str.startswith( - x[1].__module__, - f"packages.{skill_context.skill_id.author}.skills.{skill_context.skill_id.name}", - ), - classes, - ) - ) - models.extend(filtered_classes) - - _check_duplicate_classes(models) - name_to_class = dict(models) - _print_warning_message_for_non_declared_skill_components( - skill_context, - set(name_to_class.keys()), - {model_config.class_name for model_config in model_configs.values()}, - "models", - path, - ) - for model_id, model_config in model_configs.items(): - model_class_name = model_config.class_name - skill_context.logger.debug( - "Processing model id={}, class={}".format(model_id, model_class_name) - ) - if not model_id.isidentifier(): - raise AEAComponentLoadException( # pragma: nocover - f"'{model_id}' is not a valid identifier." - ) - model = name_to_class.get(model_class_name, None) - if model is None: - skill_context.logger.warning( - "Model '{}' cannot be found.".format(model_class_name) - ) - else: - try: - model_instance = model( - name=model_id, - skill_context=skill_context, - configuration=model_config, - **dict(model_config.args), - ) - except Exception as e: # pylint: disable=broad-except # pragma: nocover - e_str = parse_exception(e) - raise AEAInstantiationException( - f"An error occured during instantiation of model {skill_context.skill_id}/{model_config.class_name}:\n{e_str}" - ) - instances[model_id] = model_instance - setattr(skill_context, model_id, model_instance) - return instances - - -def _check_duplicate_classes(name_class_pairs: Sequence[Tuple[str, Type]]) -> None: - """ - Given a sequence of pairs (class_name, class_obj), check whether there are duplicates in the class names. - - :param name_class_pairs: the sequence of pairs (class_name, class_obj) - :return: None - :raises AEAException: if there are more than one definition of the same class. - """ - names_to_path: Dict[str, str] = {} - for class_name, class_obj in name_class_pairs: - module_path = class_obj.__module__ - if class_name in names_to_path: - raise AEAException( - f"Model '{class_name}' present both in {names_to_path[class_name]} and {module_path}. Remove one of them." - ) - names_to_path[class_name] = module_path + return _parse_module(path, model_configs, skill_context, Model) class Skill(Component): """This class implements a skill.""" + __slots__ = ("_skill_context", "_handlers", "_behaviours", "_models") + def __init__( self, configuration: SkillConfig, @@ -749,7 +559,6 @@ def __init__( if kwargs is not None: pass super().__init__(configuration) - self.config = configuration self._skill_context = ( skill_context if skill_context is not None else SkillContext() ) @@ -853,30 +662,93 @@ def from_config( ) skill_context.logger = cast(Logger, _logger) - skill = Skill(configuration, skill_context, **kwargs) - - directory = configuration.directory - load_aea_package(configuration) - handlers_by_id = dict(configuration.handlers.read_all()) - handlers = Handler.parse_module( - str(directory / "handlers.py"), handlers_by_id, skill_context + skill_component_loader = _SkillComponentLoader( + configuration, skill_context, **kwargs ) + skill = skill_component_loader.load_skill() + return skill - behaviours_by_id = dict(configuration.behaviours.read_all()) - behaviours = Behaviour.parse_module( - str(directory / "behaviours.py"), behaviours_by_id, skill_context, - ) - models_by_id = dict(configuration.models.read_all()) - model_instances = Model.parse_module( - str(directory), models_by_id, skill_context - ) +def _parse_module( + path: str, + component_configs: Dict[str, SkillComponentConfiguration], + skill_context: SkillContext, + component_class: Type, +) -> Dict[str, Any]: + """ + Parse a module to find skill component classes, and instantiate them. - skill.handlers.update(handlers) - skill.behaviours.update(behaviours) - skill.models.update(model_instances) + This is a private framework function, + used in SkillComponentClass.parse_module. - return skill + :param path: path to the Python module. + :param component_configs: the component configurations. + :param skill_context: the skill context. + :param component_class: the class of the skill components to be loaded. + :return: A mapping from skill component name to the skill component instance. + """ + components: Dict[str, Any] = {} + component_type_name = component_class.__name__.lower() + component_type_name_plural = component_type_name + "s" + if component_configs == {}: + return components + component_names = set(config.class_name for _, config in component_configs.items()) + component_module = load_module(component_type_name_plural, Path(path)) + classes = inspect.getmembers(component_module, inspect.isclass) + component_classes = list( + filter( + lambda x: any(re.match(component, x[0]) for component in component_names) + and issubclass(x[1], component_class) + and not str.startswith(x[1].__module__, "aea.") + and not str.startswith( + x[1].__module__, + f"packages.{skill_context.skill_id.author}.skills.{skill_context.skill_id.name}", + ), + classes, + ) + ) + + name_to_class = dict(component_classes) + _print_warning_message_for_non_declared_skill_components( + skill_context, + set(name_to_class.keys()), + { + component_config.class_name + for component_config in component_configs.values() + }, + component_type_name_plural, + path, + ) + for component_id, component_config in component_configs.items(): + component_class_name = cast(str, component_config.class_name) + skill_context.logger.debug( + f"Processing {component_type_name} {component_class_name}" + ) + if not component_id.isidentifier(): + raise AEAComponentLoadException( # pragma: nocover + f"'{component_id}' is not a valid identifier." + ) + component_class = name_to_class.get(component_class_name, None) + if component_class is None: + skill_context.logger.warning( + f"{component_type_name.capitalize()} '{component_class_name}' cannot be found." + ) + else: + try: + component = component_class( + name=component_id, + configuration=component_config, + skill_context=skill_context, + **dict(component_config.args), + ) + except Exception as e: # pylint: disable=broad-except # pragma: nocover + e_str = parse_exception(e) + raise AEAInstantiationException( + f"An error occured during instantiation of component {skill_context.skill_id}/{component_config.class_name}:\n{e_str}" + ) + components[component_id] = component + + return components def _print_warning_message_for_non_declared_skill_components( @@ -893,3 +765,356 @@ def _print_warning_message_for_non_declared_skill_components( class_name, item_type, skill_path ) ) + + +_SKILL_COMPONENT_TYPES = Type[Union[Handler, Behaviour, Model]] + +_ComponentsHelperIndex = Dict[_SKILL_COMPONENT_TYPES, Dict[str, SkillComponent]] +""" +Helper index to store component instances. +""" + + +class _SkillComponentLoadingItem: # pylint: disable=too-few-public-methods + """Class to represent a triple (component name, component configuration, component class).""" + + def __init__( + self, + name: str, + config: SkillComponentConfiguration, + class_: Type[SkillComponent], + type_: _SKILL_COMPONENT_TYPES, + ): + """Initialize the item.""" + self.name = name + self.config = config + self.class_ = class_ + self.type_ = type_ + + +class _SkillComponentLoader: + """This class implements the loading policy for skill components.""" + + def __init__( + self, configuration: SkillConfig, skill_context: SkillContext, **kwargs: Any + ): + """Initialize the helper class.""" + enforce( + configuration.directory is not None, + "Configuration not associated to directory.", + ) + self.configuration = configuration + self.skill_directory = cast(Path, configuration.directory) + self.skill_context = skill_context + self.kwargs = kwargs + + self._all_component_names: Set[str] = set() + + self.skill = Skill(self.configuration, self.skill_context, **self.kwargs) + self.skill_dotted_path = f"packages.{self.configuration.public_id.author}.skills.{self.configuration.public_id.name}" + + def load_skill(self) -> Skill: + """Load the skill.""" + load_aea_package(self.configuration) + python_modules: Set[Path] = self._get_python_modules() + declared_component_classes: Dict[ + _SKILL_COMPONENT_TYPES, Dict[str, SkillComponentConfiguration] + ] = self._get_declared_skill_component_configurations() + self._all_component_names = { + config.class_name + for _, types_ in declared_component_classes.items() + for _, config in types_.items() + } + component_classes_by_path: Dict[ + Path, Set[Tuple[str, Type[SkillComponent]]] + ] = self._load_component_classes(python_modules) + component_loading_items = self._match_class_and_configurations( + component_classes_by_path, declared_component_classes + ) + components = self._get_component_instances(component_loading_items) + self._update_skill(components) + return self.skill + + def _update_skill(self, components: _ComponentsHelperIndex) -> None: + self.skill.handlers.update( + cast(Dict[str, Handler], components.get(Handler, {})) + ) + self.skill.behaviours.update( + cast(Dict[str, Behaviour], components.get(Behaviour, {})) + ) + self.skill.models.update(cast(Dict[str, Model], components.get(Model, {}))) + self.skill._set_models_on_context() # pylint: disable=protected-access + + def _get_python_modules(self) -> Set[Path]: + """ + Get all the Python modules of the skill package. + + We ignore '__pycache__' Python modules as they are not relevant. + + :return: a set of paths pointing to all the Python modules in the skill. + """ + ignore_regex = "__pycache__*" + all_python_modules = self.skill_directory.rglob("*.py") + module_paths: Set[Path] = set( + map( + lambda p: Path(p).relative_to(self.skill_directory), + filter( + lambda x: not re.match(ignore_regex, x.name), all_python_modules + ), + ) + ) + return module_paths + + @classmethod + def _compute_module_dotted_path(cls, module_path: Path) -> str: + """Compute the dotted path for a skill module.""" + suffix = ".".join(module_path.with_name(module_path.stem).parts) + return suffix + + def _filter_classes( + self, classes: List[Tuple[str, Type]] + ) -> List[Tuple[str, Type[SkillComponent]]]: + """ + Filter classes of skill components. + + :param classes: a list of pairs (class name, class object) + :return: a list of the same kind, but filtered with only skill component classes. + """ + filtered_classes = filter( + lambda name_and_class: any( + re.match(component, name_and_class[0]) + for component in self._all_component_names + ) + and issubclass(name_and_class[1], SkillComponent) + and not str.startswith(name_and_class[1].__module__, "aea.") + and not str.startswith( + name_and_class[1].__module__, self.skill_dotted_path + ), + classes, + ) + classes = list(filtered_classes) + return cast(List[Tuple[str, Type[SkillComponent]]], classes) + + def _load_component_classes( + self, module_paths: Set[Path] + ) -> Dict[Path, Set[Tuple[str, Type[SkillComponent]]]]: + """ + Load component classes from Python modules. + + :param module_paths: a set of paths to Python modules. + :return: a mapping from path to skill component classes in that module + (containing potential duplicates). Skill components in one path + are + """ + module_to_classes: Dict[Path, Set[Tuple[str, Type[SkillComponent]]]] = {} + for module_path in module_paths: + self.skill_context.logger.debug(f"Trying to load module {module_path}") + module_dotted_path: str = self._compute_module_dotted_path(module_path) + component_module: types.ModuleType = load_module( + module_dotted_path, self.skill_directory / module_path + ) + classes: List[Tuple[str, Type]] = inspect.getmembers( + component_module, inspect.isclass + ) + filtered_classes: List[ + Tuple[str, Type[SkillComponent]] + ] = self._filter_classes(classes) + module_to_classes[module_path] = set(filtered_classes) + return module_to_classes + + def _get_declared_skill_component_configurations( + self, + ) -> Dict[_SKILL_COMPONENT_TYPES, Dict[str, SkillComponentConfiguration]]: + """ + Get all the declared skill component configurations. + + :return: + """ + handlers_by_id = dict(self.configuration.handlers.read_all()) + behaviours_by_id = dict(self.configuration.behaviours.read_all()) + models_by_id = dict(self.configuration.models.read_all()) + + result: Dict[ + _SKILL_COMPONENT_TYPES, Dict[str, SkillComponentConfiguration] + ] = {} + for component_type, components_by_id in [ + (Handler, handlers_by_id), + (Behaviour, behaviours_by_id), + (Model, models_by_id), + ]: + for component_id, component_config in components_by_id.items(): + result.setdefault(component_type, {})[component_id] = component_config # type: ignore + return result + + def _get_component_instances( + self, component_loading_items: List[_SkillComponentLoadingItem], + ) -> _ComponentsHelperIndex: + """ + Instantiate classes declared in configuration files. + + :param component_loading_items: a list of loading items. + :return: the instances of the skill components. + """ + result: _ComponentsHelperIndex = {} + for item in component_loading_items: + instance = item.class_( + name=item.name, + configuration=item.config, + skill_context=self.skill_context, + **item.config.args, + ) + result.setdefault(item.type_, {})[item.name] = instance + return result + + @classmethod + def _get_skill_component_type( + cls, skill_component_type: Type[SkillComponent], + ) -> Type[Union[Handler, Behaviour, Model]]: + """Get the concrete skill component type.""" + parent_skill_component_types = list( + filter( + lambda class_: class_ in (Handler, Behaviour, Model), + skill_component_type.__mro__, + ) + ) + enforce( + len(parent_skill_component_types) == 1, + f"Class {skill_component_type.__name__} in module {skill_component_type.__module__} is not allowed to inherit from more than one skill component type. Found: {parent_skill_component_types}.", + ) + return cast( + Type[Union[Handler, Behaviour, Model]], parent_skill_component_types[0] + ) + + def _match_class_and_configurations( + self, + component_classes_by_path: Dict[Path, Set[Tuple[str, Type[SkillComponent]]]], + declared_component_classes: Dict[ + _SKILL_COMPONENT_TYPES, Dict[str, SkillComponentConfiguration] + ], + ) -> List[_SkillComponentLoadingItem]: + """ + Match skill component classes to their configurations. + + Given a class of a skill component, we can disambiguate it in three ways: + - by its name + - by its type (one of 'Handler', 'Behaviour', 'Model') + - whether the user has set the 'file_path' field. + If one of the skill component cannot be disambiguated, we raise error. + + In this function, the above criteria are applied in that order. + + :param component_classes_by_path: + :return: None + """ + result: List[_SkillComponentLoadingItem] = [] + + class_index: Dict[ + str, Dict[_SKILL_COMPONENT_TYPES, Set[Type[SkillComponent]]] + ] = {} + used_classes = set() + not_resolved_configurations: Dict[ + Tuple[_SKILL_COMPONENT_TYPES, str], SkillComponentConfiguration + ] = {} + + # populate indexes + for _path, component_classes in component_classes_by_path.items(): + for (component_classname, _component_class) in component_classes: + type_ = self._get_skill_component_type(_component_class) + class_index.setdefault(component_classname, {}).setdefault( + type_, set() + ).add(_component_class) + + for component_type, by_id in declared_component_classes.items(): + for component_id, component_config in by_id.items(): + path = component_config.file_path + class_name = component_config.class_name + if path is not None: + classes_in_path = component_classes_by_path[path] + component_class_or_none: Optional[Type[SkillComponent]] = next( + ( + actual_class + for actual_class_name, actual_class in classes_in_path + if actual_class_name == class_name + ), + None, + ) + enforce( + component_class_or_none is not None, + self._get_error_message_prefix() + + f"Cannot find class '{class_name}' for component '{component_id}' of type '{self._type_to_str(component_type)}' of skill '{self.configuration.public_id}' in module {path}", + ) + component_class = cast( + Type[SkillComponent], component_class_or_none + ) + actual_component_type = self._get_skill_component_type( + component_class + ) + enforce( + actual_component_type == component_type, + self._get_error_message_prefix() + + f"Found class '{class_name}' for component '{component_id}' of type '{self._type_to_str(component_type)}' of skill '{self.configuration.public_id}' in module {path}, but the expected type was {self._type_to_str(component_type)}, found {self._type_to_str(actual_component_type)} ", + ) + used_classes.add(component_class) + result.append( + _SkillComponentLoadingItem( + component_id, + component_config, + component_class, + component_type, + ) + ) + else: + # process the configuration at the end of the loop + not_resolved_configurations[ + (component_type, component_id) + ] = component_config + + for (component_type, component_id), component_config in copy( + not_resolved_configurations + ).items(): + class_name = component_config.class_name + classes_by_type = class_index.get(class_name, {}) + enforce( + class_name in class_index and component_type in classes_by_type, + self._get_error_message_prefix() + + f"Cannot find class '{class_name}' for skill component '{component_id}' of type '{self._type_to_str(component_type)}'", + ) + classes = classes_by_type[component_type] + not_used_classes = classes.difference(used_classes) + enforce( + not_used_classes != 0, + f"Cannot find class of skill '{self.configuration.public_id}' for component configuration '{component_id}' of type '{self._type_to_str(component_type)}'.", + ) + enforce( + len(not_used_classes) == 1, + self._get_error_message_ambiguous_classes( + class_name, not_used_classes, component_type, component_id + ), + ) + not_used_class = list(not_used_classes)[0] + result.append( + _SkillComponentLoadingItem( + component_id, component_config, not_used_class, component_type + ) + ) + used_classes.add(not_used_class) + + return result + + @classmethod + def _type_to_str(cls, component_type: _SKILL_COMPONENT_TYPES) -> str: + """Get the string of a component type.""" + return component_type.__name__.lower() + + def _get_error_message_prefix(self) -> str: + """Get error message prefix.""" + return f"Error while loading skill '{self.configuration.public_id}': " + + def _get_error_message_ambiguous_classes( + self, + class_name: str, + not_used_classes: Set, + component_type: _SKILL_COMPONENT_TYPES, + component_id: str, + ) -> str: + return f"{self._get_error_message_prefix()}found many classes with name '{class_name}' for component '{component_id}' of type '{self._type_to_str(component_type)}' in the following modules: {', '.join([c.__module__ for c in not_used_classes])}" diff --git a/aea/skills/scaffold/skill.yaml b/aea/skills/scaffold/skill.yaml index 0f26f12e5f..aa311bc301 100644 --- a/aea/skills/scaffold/skill.yaml +++ b/aea/skills/scaffold/skill.yaml @@ -4,7 +4,7 @@ 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.10.0, <0.11.0' +aea_version: '>=0.11.0, <0.12.0' fingerprint: __init__.py: QmYRssFqDqb3uWDvfoXy93avisjKRx2yf9SbAQXnkRj1QB behaviours.py: QmNgDDAmBzWBeBF7e5gUCny38kdqVVfpvHGaAZVZcMtm9Q diff --git a/aea/test_tools/test_cases.py b/aea/test_tools/test_cases.py index bec9b912af..18edf24dc0 100644 --- a/aea/test_tools/test_cases.py +++ b/aea/test_tools/test_cases.py @@ -317,6 +317,7 @@ def is_allowed_diff_in_agent_config( "description", "version", "registry_path", + "dependencies", # temporary ] result = all([key in allowed_diff_keys for key in content1.keys()]) result = result and all( diff --git a/aea/test_tools/test_skill.py b/aea/test_tools/test_skill.py index 8dc292b430..ab8e056c61 100644 --- a/aea/test_tools/test_skill.py +++ b/aea/test_tools/test_skill.py @@ -38,13 +38,15 @@ from aea.skills.tasks import TaskManager -COUNTERPARTY_ADDRESS = "counterparty" +COUNTERPARTY_AGENT_ADDRESS = "counterparty" +COUNTERPARTY_SKILL_ADDRESS = "some_author/some_skill:0.1.0" class BaseSkillTestCase: """A class to test a skill.""" path_to_skill: Path = Path(".") + is_agent_to_agent_messages: bool = True _skill: Skill _multiplexer: AsyncMultiplexer _outbox: OutBox @@ -149,7 +151,8 @@ def build_incoming_message( message_id: Optional[int] = None, target: Optional[int] = None, to: Optional[Address] = None, - sender: Address = COUNTERPARTY_ADDRESS, + sender: Optional[Address] = None, + is_agent_to_agent_messages: Optional[bool] = None, **kwargs: Any, ) -> Message: """ @@ -164,10 +167,19 @@ def build_incoming_message( :param performative: the performative :param to: the 'to' address :param sender: the 'sender' address + :param is_agent_to_agent_messages: whether the dialogue is between agents or components :param kwargs: other attributes :return: the created incoming message """ + if is_agent_to_agent_messages is None: + is_agent_to_agent_messages = self.is_agent_to_agent_messages + if sender is None: + sender = ( + COUNTERPARTY_AGENT_ADDRESS + if is_agent_to_agent_messages + else COUNTERPARTY_SKILL_ADDRESS + ) message_attributes = dict() # type: Dict[str, Any] default_dialogue_reference = Dialogues.new_self_initiated_dialogue_reference() @@ -186,9 +198,12 @@ def build_incoming_message( incoming_message = message_type(**message_attributes) incoming_message.sender = sender - incoming_message.to = ( - self.skill.skill_context.agent_address if to is None else to + default_to = ( + self.skill.skill_context.agent_address + if is_agent_to_agent_messages + else str(self.skill.public_id) ) + incoming_message.to = default_to if to is None else to return incoming_message def build_incoming_message_for_skill_dialogue( @@ -332,7 +347,8 @@ def prepare_skill_dialogue( self, dialogues: Dialogues, messages: Tuple[DialogueMessage, ...], - counterparty: Address = COUNTERPARTY_ADDRESS, + counterparty: Optional[Address] = None, + is_agent_to_agent_messages: Optional[bool] = None, ) -> Dialogue: """ Quickly create a dialogue. @@ -347,9 +363,18 @@ def prepare_skill_dialogue( :param dialogues: a dialogues class :param counterparty: the message_id :param messages: the dialogue_reference + :param is_agent_to_agent_messages: whether the dialogue is between agents or components :return: the created incoming message """ + if is_agent_to_agent_messages is None: + is_agent_to_agent_messages = self.is_agent_to_agent_messages + if counterparty is None: + counterparty = ( + COUNTERPARTY_AGENT_ADDRESS + if is_agent_to_agent_messages + else COUNTERPARTY_SKILL_ADDRESS + ) if len(messages) == 0: raise AEAEnforceError("the list of messages must be positive.") @@ -363,14 +388,20 @@ def prepare_skill_dialogue( if is_incoming: # first message from the opponent dialogue_reference = dialogues.new_self_initiated_dialogue_reference() + default_to = ( + self.skill.skill_context.agent_address + if is_agent_to_agent_messages + else str(self.skill.public_id) + ) message = self.build_incoming_message( message_type=dialogues.message_class, dialogue_reference=dialogue_reference, message_id=Dialogue.STARTING_MESSAGE_ID, target=target or Dialogue.STARTING_TARGET, performative=performative, - to=self.skill.skill_context.agent_address, + to=default_to, sender=counterparty, + is_agent_to_agent_messages=is_agent_to_agent_messages, **contents, ) dialogue = cast(Dialogue, dialogues.update(message)) @@ -402,14 +433,20 @@ def prepare_skill_dialogue( ) message_id = dialogue.get_incoming_next_message_id() + default_to = ( + self.skill.skill_context.agent_address + if is_agent_to_agent_messages + else str(self.skill.public_id) + ) message = self.build_incoming_message( message_type=dialogues.message_class, dialogue_reference=dialogue_reference, message_id=message_id, target=target, performative=performative, - to=self.skill.skill_context.agent_address, + to=default_to, sender=counterparty, + is_agent_to_agent_messages=is_agent_to_agent_messages, **contents, ) dialogue = cast(Dialogue, dialogues.update(message)) @@ -434,28 +471,32 @@ def setup(cls, **kwargs: Any) -> None: asyncio.Queue() ) cls._outbox = OutBox(cast(Multiplexer, cls._multiplexer)) - _shared_state = cast(Dict[str, Any], kwargs.pop("shared_state", dict())) + _shared_state = cast(Optional[Dict[str, Any]], kwargs.pop("shared_state", None)) _skill_config_overrides = cast( - Dict[str, Any], kwargs.pop("config_overrides", dict()) + Optional[Dict[str, Any]], kwargs.pop("config_overrides", None) + ) + _dm_context_kwargs = cast( + Dict[str, Any], kwargs.pop("dm_context_kwargs", dict()) ) + agent_context = AgentContext( identity=identity, connection_status=cls._multiplexer.connection_status, outbox=cls._outbox, decision_maker_message_queue=Queue(), - decision_maker_handler_context=SimpleNamespace(), + decision_maker_handler_context=SimpleNamespace(**_dm_context_kwargs), task_manager=TaskManager(), default_ledger_id=identity.default_address_key, currency_denominations=DEFAULT_CURRENCY_DENOMINATIONS, default_connection=None, default_routing={}, - search_service_address="dummy_search_service_address", + search_service_address="dummy_author/dummy_search_skill:0.1.0", decision_maker_address="dummy_decision_maker_address", data_dir=os.getcwd(), ) - # This enables pre-populating the 'shared_state' prior to loading the skill - if _shared_state != dict(): + # Pre-populate the 'shared_state' prior to loading the skill + if _shared_state is not None: for key, value in _shared_state.items(): agent_context.shared_state[key] = value @@ -465,8 +506,8 @@ def setup(cls, **kwargs: Any) -> None: with open_file(skill_configuration_file_path) as fp: skill_config: SkillConfig = loader.load(fp) - # This enables overriding the skill's config prior to loading - if _skill_config_overrides != {}: + # Override skill's config prior to loading + if _skill_config_overrides is not None: skill_config.update(_skill_config_overrides) skill_config.directory = cls.path_to_skill diff --git a/benchmark/Dockerfile b/benchmark/Dockerfile index 73aaaa25a4..77fbbac6ec 100644 --- a/benchmark/Dockerfile +++ b/benchmark/Dockerfile @@ -10,5 +10,5 @@ RUN apk add --no-cache go RUN pip install --upgrade pip pipenv RUN pip install aea[all] --upgrade --force-reinstall -RUN wget https://raw.githubusercontent.com/fetchai/agents-aea/master/Pipfile +RUN wget https://raw.githubusercontent.com/fetchai/agents-aea/main/Pipfile RUN pipenv install -d --deploy --skip-lock --system diff --git a/benchmark/checks/check_mem_usage.py b/benchmark/checks/check_mem_usage.py index 9ab5e3e21b..a53ccc5bea 100755 --- a/benchmark/checks/check_mem_usage.py +++ b/benchmark/checks/check_mem_usage.py @@ -25,6 +25,7 @@ import click from aea.protocols.base import Message +from aea.registries.resources import Resources from aea.skills.base import Handler from benchmark.checks.utils import SyncedGeneratorConnection # noqa: I100 from benchmark.checks.utils import ( @@ -62,10 +63,12 @@ def run(duration: int, runtime_mode: str) -> List[Tuple[str, Union[int, float]]] # import manually due to some lazy imports in decision_maker import aea.decision_maker.default # noqa: F401 - agent = make_agent(runtime_mode=runtime_mode) connection = SyncedGeneratorConnection.make() - agent.resources.add_connection(connection) + resources = Resources() + resources.add_connection(connection) + agent = make_agent(runtime_mode=runtime_mode, resources=resources) agent.resources.add_skill(make_skill(agent, handlers={"test": TestHandler})) + t = Thread(target=agent.start, daemon=True) t.start() wait_for_condition(lambda: agent.is_running, timeout=5) diff --git a/benchmark/checks/check_multiagent.py b/benchmark/checks/check_multiagent.py index 6b6a76707f..aa22122a8d 100755 --- a/benchmark/checks/check_multiagent.py +++ b/benchmark/checks/check_multiagent.py @@ -28,7 +28,9 @@ import click from aea.configurations.base import ConnectionConfig +from aea.identity.base import Identity from aea.protocols.base import Message +from aea.registries.resources import Resources from aea.runner import AEARunner from aea.skills.base import Handler from benchmark.checks.utils import get_mem_usage_in_mb # noqa: I100 @@ -123,15 +125,24 @@ def run( skills = [] for i in range(num_of_agents): - agent = make_agent(agent_name=f"agent{i}", runtime_mode=runtime_mode) + resources = Resources() + agent_name = f"agent{i}" + identity = Identity(agent_name, address=agent_name) connection = OEFLocalConnection( local_node, configuration=ConnectionConfig( connection_id=OEFLocalConnection.connection_id, ), - identity=agent.identity, + identity=identity, + data_dir="tmp", + ) + resources.add_connection(connection) + agent = make_agent( + agent_name=agent_name, + runtime_mode=runtime_mode, + resources=resources, + identity=identity, ) - agent.resources.add_connection(connection) skill = make_skill(agent, handlers={"test": TestHandler}) agent.resources.add_skill(skill) agents.append(agent) @@ -156,7 +167,7 @@ def run( mem_usage = get_mem_usage_in_mb() local_node.stop() - runner.stop() + runner.stop(timeout=5) total_messages = sum( [cast(TestHandler, skill.handlers["test"]).count for skill in skills] diff --git a/benchmark/checks/check_multiagent_http_dialogues.py b/benchmark/checks/check_multiagent_http_dialogues.py index 9fe890ef2f..060edd5637 100755 --- a/benchmark/checks/check_multiagent_http_dialogues.py +++ b/benchmark/checks/check_multiagent_http_dialogues.py @@ -18,7 +18,6 @@ # # ------------------------------------------------------------------------------ """?Memory usage across the time.""" - import itertools import os import struct @@ -31,8 +30,10 @@ from aea.aea import AEA from aea.common import Address from aea.configurations.base import ConnectionConfig +from aea.identity.base import Identity from aea.protocols.base import Message, Protocol from aea.protocols.dialogue.base import Dialogue +from aea.registries.resources import Resources from aea.runner import AEARunner from aea.skills.base import Handler from benchmark.checks.utils import get_mem_usage_in_mb # noqa: I100 @@ -155,17 +156,26 @@ def run( agents = [] skills = {} handler_name = "httpingpong" + for i in range(num_of_agents): agent_name = f"agent{i}" - agent = make_agent(agent_name=agent_name, runtime_mode=runtime_mode) + identity = Identity(agent_name, address=agent_name) + resources = Resources() connection = OEFLocalConnection( local_node, configuration=ConnectionConfig( connection_id=OEFLocalConnection.connection_id, ), - identity=agent.identity, + identity=identity, + data_dir="tmp", + ) + resources.add_connection(connection) + agent = make_agent( + agent_name=agent_name, + runtime_mode=runtime_mode, + resources=resources, + identity=identity, ) - agent.resources.add_connection(connection) skill = make_skill(agent, handlers={handler_name: HttpPingPongHandler}) agent.resources.add_skill(skill) agents.append(agent) @@ -191,7 +201,7 @@ def run( mem_usage = get_mem_usage_in_mb() local_node.stop() - runner.stop() + runner.stop(timeout=5) total_messages = sum( [ diff --git a/benchmark/checks/check_proactive.py b/benchmark/checks/check_proactive.py index cf22109a6f..496b436224 100755 --- a/benchmark/checks/check_proactive.py +++ b/benchmark/checks/check_proactive.py @@ -24,6 +24,7 @@ import click +from aea.registries.resources import Resources from aea.skills.base import Behaviour from benchmark.checks.utils import SyncedGeneratorConnection # noqa: I100 from benchmark.checks.utils import ( @@ -66,15 +67,15 @@ def run(duration: int, runtime_mode: str) -> List[Tuple[str, Union[int, float]]] # import manually due to some lazy imports in decision_maker import aea.decision_maker.default # noqa: F401 - agent = make_agent(runtime_mode=runtime_mode) + resources = Resources() connection = SyncedGeneratorConnection.make() - agent.resources.add_connection(connection) + resources.add_connection(connection) + agent = make_agent(runtime_mode=runtime_mode, resources=resources) skill = make_skill(agent, behaviours={"test": TestBehaviour}) agent.resources.add_skill(skill) t = Thread(target=agent.start, daemon=True) t.start() wait_for_condition(lambda: agent.is_running, timeout=5) - time.sleep(duration) agent.stop() t.join(5) diff --git a/benchmark/checks/check_reactive.py b/benchmark/checks/check_reactive.py index e5f6e40435..52047af4ec 100755 --- a/benchmark/checks/check_reactive.py +++ b/benchmark/checks/check_reactive.py @@ -27,6 +27,7 @@ from aea.mail.base import Envelope from aea.protocols.base import Message +from aea.registries.resources import Resources from aea.skills.base import Handler from benchmark.checks.utils import GeneratorConnection # noqa: I100 from benchmark.checks.utils import ( @@ -90,8 +91,7 @@ def run( # import manually due to some lazy imports in decision_maker import aea.decision_maker.default # noqa: F401 - agent = make_agent(runtime_mode=runtime_mode) - + resources = Resources() if connection_mode not in CONNECTION_MODES: raise ValueError( f"bad connection mode {connection_mode}. valid is one of {list(CONNECTION_MODES.keys())}" @@ -101,7 +101,9 @@ def run( conn_cls = type("conn_cls", (TestConnectionMixIn, base_cls), {}) connection = conn_cls.make() # type: ignore # pylint: disable=no-member - agent.resources.add_connection(connection) + resources.add_connection(connection) + + agent = make_agent(runtime_mode=runtime_mode, resources=resources) agent.resources.add_skill(make_skill(agent, handlers={"test": TestHandler})) t = Thread(target=agent.start, daemon=True) t.start() @@ -110,6 +112,7 @@ def run( connection.enable() time.sleep(duration) connection.disable() + time.sleep(0.2) # possible race condition in stop? agent.stop() t.join(5) diff --git a/benchmark/checks/utils.py b/benchmark/checks/utils.py index 0d826c70ba..6a1e273086 100644 --- a/benchmark/checks/utils.py +++ b/benchmark/checks/utils.py @@ -37,10 +37,10 @@ from aea.configurations.constants import ( DEFAULT_LEDGER, DEFAULT_PROTOCOL, - FETCHAI, PACKAGES, PROTOCOLS, SKILLS, + _FETCHAI_IDENTIFIER, ) from aea.connections.base import Connection, ConnectionStates from aea.crypto.wallet import Wallet @@ -70,11 +70,16 @@ def wait_for_condition( raise TimeoutError(error_msg) -def make_agent(agent_name: str = "my_agent", runtime_mode: str = "threaded") -> AEA: +def make_agent( + agent_name: str = "my_agent", + runtime_mode: str = "threaded", + resources: Optional[Resources] = None, + identity: Optional[Identity] = None, +) -> AEA: """Make AEA instance.""" wallet = Wallet({DEFAULT_LEDGER: None}) - identity = Identity(agent_name, address=agent_name) - resources = Resources() + identity = identity or Identity(agent_name, address=agent_name) + resources = resources or Resources() datadir = os.getcwd() agent_context = MagicMock() agent_context.agent_name = agent_name @@ -82,7 +87,7 @@ def make_agent(agent_name: str = "my_agent", runtime_mode: str = "threaded") -> resources.add_skill( Skill.from_dir( - str(PACKAGES_DIR / FETCHAI / SKILLS / ERROR_SKILL_NAME), + str(PACKAGES_DIR / _FETCHAI_IDENTIFIER / SKILLS / ERROR_SKILL_NAME), agent_context=agent_context, ) ) @@ -90,7 +95,7 @@ def make_agent(agent_name: str = "my_agent", runtime_mode: str = "threaded") -> Protocol.from_dir( str( PACKAGES_DIR - / FETCHAI + / _FETCHAI_IDENTIFIER / PROTOCOLS / PublicId.from_str(DEFAULT_PROTOCOL).name ) @@ -159,11 +164,13 @@ async def receive(self, *args: Any, **kwargs: Any) -> Optional["Envelope"]: return envelope @classmethod - def make(cls) -> "GeneratorConnection": + def make(cls,) -> "GeneratorConnection": """Construct connection instance.""" configuration = ConnectionConfig(connection_id=cls.connection_id,) test_connection = cls( - configuration=configuration, identity=Identity("name", "address") + configuration=configuration, + identity=Identity("name", "address"), + data_dir=".tmp", ) return test_connection diff --git a/benchmark/framework/aea_test_wrapper.py b/benchmark/framework/aea_test_wrapper.py index daa2997455..a88328ba97 100644 --- a/benchmark/framework/aea_test_wrapper.py +++ b/benchmark/framework/aea_test_wrapper.py @@ -26,7 +26,7 @@ from aea.aea_builder import AEABuilder from aea.components.base import Component from aea.configurations.base import SkillConfig -from aea.crypto.fetchai import FetchAICrypto +from aea.configurations.constants import _FETCHAI_IDENTIFIER from aea.mail.base import Envelope from aea.protocols.base import Message from aea.skills.base import Handler, Skill, SkillContext @@ -69,7 +69,7 @@ def make_aea( builder.set_name(name or self.name) - builder.add_private_key(FetchAICrypto.identifier, private_key_path=None) + builder.add_private_key(_FETCHAI_IDENTIFIER, private_key_path=None) for component in components: builder.add_component_instance(component) diff --git a/benchmark/run_from_branch.sh b/benchmark/run_from_branch.sh index 10b8ac1975..234a129456 100755 --- a/benchmark/run_from_branch.sh +++ b/benchmark/run_from_branch.sh @@ -1,6 +1,6 @@ #!/bin/bash REPO=https://github.com/fetchai/agents-aea.git -BRANCH=master +BRANCH=main TMP_DIR=$(mktemp -d -t bench-XXXXXXXXXX) git clone --branch $BRANCH $REPO $TMP_DIR @@ -12,7 +12,7 @@ pip install pipenv # this is to install benchmark dependencies pipenv install --dev --skip-lock # this is to install the AEA in the Pipenv virtual env -pipenv run pip install --upgrade aea[all]=="0.10.1" +pipenv run pip install --upgrade aea[all]=="0.11.0" chmod +x benchmark/checks/run_benchmark.sh echo "Start the experiments." diff --git a/deploy-image/Dockerfile b/deploy-image/Dockerfile index 77cc1cdc06..97df93c005 100644 --- a/deploy-image/Dockerfile +++ b/deploy-image/Dockerfile @@ -13,7 +13,7 @@ ENV PYTHONPATH "$PYTHONPATH:/usr/lib/python3.7/site-packages" RUN apk add --no-cache go RUN pip install --upgrade pip -RUN pip install --upgrade --force-reinstall aea[all]==0.10.1 +RUN pip install --upgrade --force-reinstall aea[all]==0.11.0 # COPY ./packages /home/packages # enable to add packages dir WORKDIR home diff --git a/deploy-image/docker-env.sh b/deploy-image/docker-env.sh index 384106af66..20d63f4282 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=fetchai/aea-deploy:0.10.1 +DOCKER_IMAGE_TAG=fetchai/aea-deploy:0.11.0 # DOCKER_IMAGE_TAG=fetchai/aea-deploy:latest DOCKER_BUILD_CONTEXT_DIR=.. diff --git a/develop-image/docker-env.sh b/develop-image/docker-env.sh index 8a321f54d6..c76ce68503 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=fetchai/aea-develop:0.10.1 +DOCKER_IMAGE_TAG=fetchai/aea-develop:0.11.0 # DOCKER_IMAGE_TAG=aea-develop:latest DOCKER_BUILD_CONTEXT_DIR=.. diff --git a/docs/api/agent_loop.md b/docs/api/agent_loop.md index c40fa09fe8..95b5290b51 100644 --- a/docs/api/agent_loop.md +++ b/docs/api/agent_loop.md @@ -64,6 +64,23 @@ Get agent. Get current main loop state. + +#### wait`_`state + +```python + | async wait_state(state_or_states: Union[Any, Sequence[Any]]) -> Tuple[Any, Any] +``` + +Wait state to be set. + +**Arguments**: + +- `state_or_states`: state or list of states. + +**Returns**: + +tuple of previous state and new state. + #### is`_`running @@ -92,6 +109,36 @@ Set event loop and all event loopp related objects. Run agent loop. + +#### send`_`to`_`skill + +```python + | @abstractmethod + | send_to_skill(message_or_envelope: Union[Message, Envelope], context: Optional[EnvelopeContext] = None) -> None +``` + +Send message or envelope to another skill. + +**Arguments**: + +- `message_or_envelope`: envelope to send to another skill. +if message passed it will be wrapped into envelope with optional envelope context. + +**Returns**: + +None + + +#### skill2skill`_`queue + +```python + | @property + | @abstractmethod + | skill2skill_queue() -> Queue +``` + +Get skill to skill message queue. + ## AsyncAgentLoop Objects @@ -114,4 +161,33 @@ Init agent loop. - `agent`: AEA instance - `loop`: asyncio loop to use. optional +- `threaded`: is a new thread to be started for the agent loop + + +#### skill2skill`_`queue + +```python + | @property + | skill2skill_queue() -> Queue +``` + +Get skill to skill message queue. + + +#### send`_`to`_`skill + +```python + | send_to_skill(message_or_envelope: Union[Message, Envelope], context: Optional[EnvelopeContext] = None) -> None +``` + +Send message or envelope to another skill. + +**Arguments**: + +- `message_or_envelope`: envelope to send to another skill. +if message passed it will be wrapped into envelope with optional envelope context. + +**Returns**: + +None diff --git a/docs/api/configurations/base.md b/docs/api/configurations/base.md index f93d5191e3..59a0fdbb80 100644 --- a/docs/api/configurations/base.md +++ b/docs/api/configurations/base.md @@ -198,6 +198,17 @@ Get the 'aea_version' attribute. Set the 'aea_version' attribute. + +#### check`_`aea`_`version + +```python + | check_aea_version() -> None +``` + +Check that the AEA version matches the specifier set. + +:raises ValueError if the version of the aea framework falls within a specifier. + #### directory @@ -415,17 +426,6 @@ Check that the fingerprint are correct against a directory path. - the argument is not a valid package directory - the fingerprints do not match. - -#### check`_`aea`_`version - -```python - | check_aea_version() -> None -``` - -Check that the AEA version matches the specifier set. - -:raises ValueError if the version of the aea framework falls within a specifier. - #### check`_`public`_`id`_`consistency @@ -528,7 +528,7 @@ This class represent a skill component configuration. #### `__`init`__` ```python - | __init__(class_name: str, **args: Any) -> None + | __init__(class_name: str, file_path: Optional[str] = None, **args: Any) -> None ``` Initialize a skill component configuration. @@ -629,7 +629,7 @@ Class to represent the agent configuration file. #### `__`init`__` ```python - | __init__(agent_name: SimpleIdOrStr, author: SimpleIdOrStr, version: str = "", license_: str = "", aea_version: str = "", fingerprint: Optional[Dict[str, str]] = None, fingerprint_ignore_patterns: Optional[Sequence[str]] = None, build_entrypoint: Optional[str] = None, registry_path: str = DEFAULT_REGISTRY_NAME, description: str = "", logging_config: Optional[Dict] = None, period: Optional[float] = None, execution_timeout: Optional[float] = None, max_reactions: Optional[int] = None, error_handler: Optional[Dict] = None, decision_maker_handler: Optional[Dict] = None, skill_exception_policy: Optional[str] = None, connection_exception_policy: Optional[str] = None, default_ledger: Optional[str] = None, currency_denominations: Optional[Dict[str, str]] = None, default_connection: Optional[str] = None, default_routing: Optional[Dict[str, str]] = None, loop_mode: Optional[str] = None, runtime_mode: Optional[str] = None, storage_uri: Optional[str] = None, data_dir: Optional[str] = None, component_configurations: Optional[Dict[ComponentId, Dict]] = None) -> None + | __init__(agent_name: SimpleIdOrStr, author: SimpleIdOrStr, version: str = "", license_: str = "", aea_version: str = "", fingerprint: Optional[Dict[str, str]] = None, fingerprint_ignore_patterns: Optional[Sequence[str]] = None, build_entrypoint: Optional[str] = None, registry_path: str = DEFAULT_REGISTRY_NAME, description: str = "", logging_config: Optional[Dict] = None, period: Optional[float] = None, execution_timeout: Optional[float] = None, max_reactions: Optional[int] = None, error_handler: Optional[Dict] = None, decision_maker_handler: Optional[Dict] = None, skill_exception_policy: Optional[str] = None, connection_exception_policy: Optional[str] = None, default_ledger: Optional[str] = None, currency_denominations: Optional[Dict[str, str]] = None, default_connection: Optional[str] = None, default_routing: Optional[Dict[str, str]] = None, loop_mode: Optional[str] = None, runtime_mode: Optional[str] = None, storage_uri: Optional[str] = None, data_dir: Optional[str] = None, component_configurations: Optional[Dict[ComponentId, Dict]] = None, dependencies: Optional[Dependencies] = None) -> None ``` Instantiate the agent configuration object. @@ -867,3 +867,21 @@ Initialize a protocol configuration object. Return the JSON representation. + +## AEAVersionError Objects + +```python +class AEAVersionError(ValueError) +``` + +Special Exception for version error. + + +#### `__`init`__` + +```python + | __init__(package_id: PublicId, aea_version_specifiers: SpecifierSet) -> None +``` + +Init exception. + diff --git a/docs/api/configurations/data_types.md b/docs/api/configurations/data_types.md index 957c3985b3..e3252303d9 100644 --- a/docs/api/configurations/data_types.md +++ b/docs/api/configurations/data_types.md @@ -345,6 +345,24 @@ the public id object. - `ValueError`: if the string in input is not well formatted. + +#### try`_`from`_`str + +```python + | @classmethod + | try_from_str(cls, public_id_string: str) -> Optional["PublicId"] +``` + +Safely try to get public id from string. + +**Arguments**: + +- `public_id_string`: the public id in string format. + +**Returns**: + +the public id object or None + #### from`_`uri`_`path @@ -559,7 +577,7 @@ Get the package identifier without the version. | from_uri_path(cls, package_id_uri_path: str) -> "PackageId" ``` -Initialize the public id from the string. +Initialize the package id from the string. >>> str(PackageId.from_uri_path("skill/author/package_name/0.1.0")) '(skill, author/package_name:0.1.0)' @@ -572,11 +590,11 @@ ValueError: Input 'very/bad/formatted:input' is not well formatted. **Arguments**: -- `public_id_uri_path`: the public id in uri path string format. +- `package_id_uri_path`: the package id in uri path string format. **Returns**: -the public id object. +the package id object. **Raises**: diff --git a/docs/api/context/base.md b/docs/api/context/base.md index 52a9c6a70e..a6cacc6b9f 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: MultiplexerStatus, outbox: OutBox, decision_maker_message_queue: Queue, decision_maker_handler_context: SimpleNamespace, task_manager: TaskManager, default_ledger_id: str, currency_denominations: Dict[str, str], default_connection: Optional[PublicId], default_routing: Dict[PublicId, PublicId], search_service_address: Address, decision_maker_address: Address, data_dir: str, storage_callable: Callable[[], Optional[Storage]] = lambda: None, **kwargs: Any) -> None + | __init__(identity: Identity, connection_status: MultiplexerStatus, outbox: OutBox, decision_maker_message_queue: Queue, decision_maker_handler_context: SimpleNamespace, task_manager: TaskManager, default_ledger_id: str, currency_denominations: Dict[str, str], default_connection: Optional[PublicId], default_routing: Dict[PublicId, PublicId], search_service_address: Address, decision_maker_address: Address, data_dir: str, storage_callable: Callable[[], Optional[Storage]] = lambda: None, send_to_skill: Optional[Callable] = None, **kwargs: Any) -> None ``` Initialize an agent context. @@ -39,6 +39,24 @@ Initialize an agent context. - `storage_callable`: function that returns optional storage attached to agent. - `kwargs`: keyword arguments to be attached in the agent context namespace. + +#### send`_`to`_`skill + +```python + | send_to_skill(message_or_envelope: Union[Message, Envelope], context: Optional[EnvelopeContext] = None) -> None +``` + +Send message or envelope to another skill. + +**Arguments**: + +- `message_or_envelope`: envelope to send to another skill. +if message passed it will be wrapped into envelope with optional envelope context. + +**Returns**: + +None + #### storage diff --git a/docs/api/crypto/plugin.md b/docs/api/crypto/plugin.md new file mode 100644 index 0000000000..77b7813c63 --- /dev/null +++ b/docs/api/crypto/plugin.md @@ -0,0 +1,77 @@ + +# aea.crypto.plugin + +Implementation of plug-in mechanism for cryptos. + + +## Plugin Objects + +```python +class Plugin() +``` + +Class that implements an AEA plugin. + + +#### `__`init`__` + +```python + | __init__(group: str, entry_point: EntryPoint) +``` + +Initialize the plugin. + +**Arguments**: + +- `group`: the group the plugin belongs to. +- `entry_point`: the entrypoint. + + +#### name + +```python + | @property + | name() -> str +``` + +Get the plugin identifier. + + +#### group + +```python + | @property + | group() -> str +``` + +Get the group. + + +#### attr + +```python + | @property + | attr() -> str +``` + +Get the class name. + + +#### entry`_`point`_`path + +```python + | @property + | entry_point_path() -> str +``` + +Get the entry point path. + + +#### load`_`all`_`plugins + +```python +load_all_plugins() -> None +``` + +Load all plugins. + diff --git a/docs/api/error_handler/base.md b/docs/api/error_handler/base.md index 9a7dbf20c9..2c632ae730 100644 --- a/docs/api/error_handler/base.md +++ b/docs/api/error_handler/base.md @@ -38,7 +38,7 @@ None ```python | @classmethod | @abstractmethod - | send_decoding_error(cls, envelope: Envelope, logger: Logger) -> None + | send_decoding_error(cls, envelope: Envelope, exception: Exception, logger: Logger) -> None ``` Handle a decoding error. @@ -46,25 +46,29 @@ Handle a decoding error. **Arguments**: - `envelope`: the envelope +- `exception`: the exception raised during decoding +- `logger`: the logger **Returns**: None - -#### send`_`unsupported`_`skill + +#### send`_`no`_`active`_`handler ```python | @classmethod | @abstractmethod - | send_unsupported_skill(cls, envelope: Envelope, logger: Logger) -> None + | send_no_active_handler(cls, envelope: Envelope, reason: str, logger: Logger) -> None ``` -Handle the received envelope in case the skill is not supported. +Handle the received envelope in case the handler is not supported. **Arguments**: - `envelope`: the envelope +- `reason`: the reason for the failure +- `logger`: the logger **Returns**: diff --git a/docs/api/error_handler/default.md b/docs/api/error_handler/default.md index 7599ec681b..4d2f424e5b 100644 --- a/docs/api/error_handler/default.md +++ b/docs/api/error_handler/default.md @@ -35,7 +35,7 @@ None ```python | @classmethod - | send_decoding_error(cls, envelope: Envelope, logger: Logger) -> None + | send_decoding_error(cls, envelope: Envelope, exception: Exception, logger: Logger) -> None ``` Handle a decoding error. @@ -43,24 +43,27 @@ Handle a decoding error. **Arguments**: - `envelope`: the envelope +- `exception`: the exception raised during decoding +- `logger`: the logger **Returns**: None - -#### send`_`unsupported`_`skill + +#### send`_`no`_`active`_`handler ```python | @classmethod - | send_unsupported_skill(cls, envelope: Envelope, logger: Logger) -> None + | send_no_active_handler(cls, envelope: Envelope, reason: str, logger: Logger) -> None ``` -Handle the received envelope in case the skill is not supported. +Handle the received envelope in case the handler is not supported. **Arguments**: - `envelope`: the envelope +- `reason`: the reason for the failure **Returns**: diff --git a/docs/api/exceptions.md b/docs/api/exceptions.md index 0f7b0afa73..3562e2334c 100644 --- a/docs/api/exceptions.md +++ b/docs/api/exceptions.md @@ -66,6 +66,15 @@ class AEAInstantiationException(AEAException) Class for exceptions that are raised for instantiation errors of AEA packages. + +## AEAPluginError Objects + +```python +class AEAPluginError(AEAException) +``` + +Class for exceptions that are raised for wrong plugin setup of the working set. + ## AEAEnforceError Objects diff --git a/docs/api/helpers/preference_representations/base.md b/docs/api/helpers/preference_representations/base.md index f4553eb198..4242269593 100644 --- a/docs/api/helpers/preference_representations/base.md +++ b/docs/api/helpers/preference_representations/base.md @@ -7,7 +7,7 @@ Preference representation helpers. #### logarithmic`_`utility ```python -logarithmic_utility(utility_params_by_good_id: Dict[str, float], quantities_by_good_id: Dict[str, int], quantity_shift: int = 1) -> float +logarithmic_utility(utility_params_by_good_id: Dict[str, float], quantities_by_good_id: Dict[str, int], quantity_shift: int = 100) -> float ``` Compute agent's utility given her utility function params and a good bundle. @@ -16,7 +16,8 @@ Compute agent's utility given her utility function params and a good bundle. - `utility_params_by_good_id`: utility params by good identifier - `quantities_by_good_id`: quantities by good identifier -- `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) +- `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) **Returns**: diff --git a/docs/api/helpers/search/models.md b/docs/api/helpers/search/models.md index 2a6767c377..1e44f8396a 100644 --- a/docs/api/helpers/search/models.md +++ b/docs/api/helpers/search/models.md @@ -208,6 +208,16 @@ Initialize a data model. - `name`: the name of the data model. - `attributes`: the attributes of the data model. + +#### attributes`_`by`_`name + +```python + | @property + | attributes_by_name() -> Dict[str, Attribute] +``` + +Get the attributes by name. + #### `__`eq`__` diff --git a/docs/api/mail/base.md b/docs/api/mail/base.md index 57fffc4eb3..cea15e7958 100644 --- a/docs/api/mail/base.md +++ b/docs/api/mail/base.md @@ -3,24 +3,6 @@ Mail module abstract base classes. - -## AEAConnectionError Objects - -```python -class AEAConnectionError(Exception) -``` - -Exception class for connection errors. - - -## Empty Objects - -```python -class Empty(Exception) -``` - -Exception for when the inbox is empty. - ## URI Objects @@ -174,13 +156,13 @@ Compare with another object. class EnvelopeContext() ``` -Extra information for the handling of an envelope. +Contains context information of an envelope. #### `__`init`__` ```python - | __init__(connection_id: Optional[PublicId] = None, skill_id: Optional[PublicId] = None, uri: Optional[URI] = None) -> None + | __init__(connection_id: Optional[PublicId] = None, uri: Optional[URI] = None) -> None ``` Initialize the envelope context. @@ -188,38 +170,37 @@ 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 + +#### uri ```python | @property - | connection_id() -> Optional[PublicId] + | uri() -> Optional[URI] ``` -Get the connection id. +Get the URI. - -#### skill`_`id + +#### connection`_`id ```python | @property - | skill_id() -> Optional[PublicId] + | connection_id() -> Optional[PublicId] ``` -Get the skill id. +Get the connection id to route the envelope. - -#### uri`_`raw + +#### connection`_`id ```python - | @property - | uri_raw() -> str + | @connection_id.setter + | connection_id(connection_id: PublicId) -> None ``` -Get uri in string format. +Set the 'via' connection id. #### `__`str`__` @@ -239,6 +220,24 @@ Get the string representation. Compare with another object. + +## AEAConnectionError Objects + +```python +class AEAConnectionError(Exception) +``` + +Exception class for connection errors. + + +## Empty Objects + +```python +class Empty(Exception) +``` + +Exception for when the inbox is empty. + ## EnvelopeSerializer Objects @@ -440,38 +439,20 @@ Get the protocol-specific message. ```python | @property - | context() -> EnvelopeContext + | context() -> Optional[EnvelopeContext] ``` Get the envelope context. - -#### skill`_`id - -```python - | @property - | skill_id() -> Optional[PublicId] -``` - -Get the skill id from an envelope context, if set. - -**Returns**: - -skill id - - -#### connection`_`id + +#### to`_`as`_`public`_`id ```python | @property - | connection_id() -> Optional[PublicId] + | to_as_public_id() -> Optional[PublicId] ``` -Get the connection id from an envelope context, if set. - -**Returns**: - -connection id +Get to as public id. #### is`_`sender`_`public`_`id @@ -493,6 +474,16 @@ Check if sender is a public id. Check if to is a public id. + +#### is`_`component`_`to`_`component`_`message + +```python + | @property + | is_component_to_component_message() -> bool +``` + +Whether or not the message contained is component to component. + #### `__`eq`__` diff --git a/docs/api/manager/manager.md b/docs/api/manager/manager.md index 9878607d78..f64f644e0f 100644 --- a/docs/api/manager/manager.md +++ b/docs/api/manager/manager.md @@ -3,6 +3,33 @@ This module contains the implementation of AEA agents manager. + +## ProjectNotFoundError Objects + +```python +class ProjectNotFoundError(ValueError) +``` + +Project not found exception. + + +## ProjectCheckError Objects + +```python +class ProjectCheckError(ValueError) +``` + +Project check error exception. + + +#### `__`init`__` + +```python + | __init__(msg: str, source_exception: Exception) +``` + +Init exception. + ## AgentRunAsyncTask Objects @@ -134,7 +161,7 @@ Multi agents manager. #### `__`init`__` ```python - | __init__(working_dir: str, mode: str = "async", registry_path: str = DEFAULT_REGISTRY_NAME) -> None + | __init__(working_dir: str, mode: str = "async", registry_path: str = DEFAULT_REGISTRY_NAME, auto_add_remove_project: bool = False) -> None ``` Initialize manager. @@ -142,6 +169,13 @@ Initialize manager. **Arguments**: - `working_dir`: directory to store base agents. +- `mode`: str. async or threaded +- `registry_path`: str. path to the local packages registry +- `auto_add_remove_project`: bool. add/remove project on the first agent add/last agent remove + +**Returns**: + +None #### data`_`dir @@ -213,6 +247,18 @@ registry, and then from remote registry in case of failure). the MultiAgentManager instance. + +#### last`_`start`_`status + +```python + | @property + | last_start_status() -> Tuple[ + | bool, Dict[PublicId, List[Dict]], List[Tuple[PublicId, List[Dict], Exception]], + | ] +``` + +Get status of the last agents start loading state. + #### stop`_`manager @@ -249,6 +295,7 @@ registry, and then from remote registry in case of failure). **Arguments**: - `public_id`: the public if of the agent project. + - `local`: whether or not to fetch from local registry. - `remote`: whether or not to fetch from remote registry. - `restore`: bool flag for restoring already fetched agent. @@ -279,7 +326,7 @@ list of public ids of projects #### add`_`agent ```python - | add_agent(public_id: PublicId, agent_name: Optional[str] = None, agent_overrides: Optional[dict] = None, component_overrides: Optional[List[dict]] = None) -> "MultiAgentManager" + | add_agent(public_id: PublicId, agent_name: Optional[str] = None, agent_overrides: Optional[dict] = None, component_overrides: Optional[List[dict]] = None, local: bool = False, remote: bool = False, restore: bool = False) -> "MultiAgentManager" ``` Create new agent configuration based on project with config overrides applied. @@ -294,6 +341,10 @@ Alias is stored in memory only! - `component_overrides`: overrides for component section. - `config`: agent config (used for agent re-creation). +- `local`: whether or not to fetch from local registry. +- `remote`: whether or not to fetch from remote registry. +- `restore`: bool flag for restoring already fetched agent. + **Returns**: manager @@ -389,7 +440,7 @@ list of agents names #### remove`_`agent ```python - | remove_agent(agent_name: str) -> "MultiAgentManager" + | remove_agent(agent_name: str, skip_project_auto_remove: bool = False) -> "MultiAgentManager" ``` Remove agent alias definition from registry. @@ -397,6 +448,7 @@ Remove agent alias definition from registry. **Arguments**: - `agent_name`: agent name to remove +- `skip_project_auto_remove`: disable auto project remove on last agent removed. **Returns**: diff --git a/docs/api/manager/project.md b/docs/api/manager/project.md index 7b8d444305..4d8a8748f2 100644 --- a/docs/api/manager/project.md +++ b/docs/api/manager/project.md @@ -100,6 +100,15 @@ Remove project, do cleanup. Get builder instance. + +#### check + +```python + | check() -> None +``` + +Check we can still construct an AEA from the project with builder.build. + ## AgentAlias Objects diff --git a/docs/api/crypto/cosmos.md b/docs/api/plugins/aea_ledger_cosmos/cosmos.md similarity index 72% rename from docs/api/crypto/cosmos.md rename to docs/api/plugins/aea_ledger_cosmos/cosmos.md index 08a68ef106..a5a92cbb18 100644 --- a/docs/api/crypto/cosmos.md +++ b/docs/api/plugins/aea_ledger_cosmos/cosmos.md @@ -1,9 +1,9 @@ - -# aea.crypto.cosmos + +# plugins.aea-ledger-cosmos.aea`_`ledger`_`cosmos.cosmos Cosmos module wrapping the public and private key cryptography and ledger api. - + ## CosmosHelper Objects ```python @@ -12,7 +12,7 @@ class CosmosHelper(Helper) Helper class usable as Mixin for CosmosApi or as standalone class. - + #### is`_`transaction`_`settled ```python @@ -30,7 +30,7 @@ Check whether a transaction is settled or not. True if the transaction has been settled, False o/w. - + #### get`_`code`_`id ```python @@ -48,7 +48,7 @@ Retrieve the `code_id` from a transaction receipt. the code id, if present - + #### get`_`contract`_`address ```python @@ -66,7 +66,7 @@ Retrieve the `contract_address` from a transaction receipt. the contract address, if present - + #### is`_`transaction`_`valid ```python @@ -88,7 +88,7 @@ Check whether a transaction is valid or not. True if the random_message is equals to tx['input'] - + #### generate`_`tx`_`nonce ```python @@ -107,7 +107,7 @@ Generate a unique hash to distinguish txs with the same terms. return the hash in hex. - + #### get`_`address`_`from`_`public`_`key ```python @@ -125,7 +125,7 @@ Get the address from the public key. str - + #### recover`_`message ```python @@ -145,7 +145,7 @@ Recover the addresses from the hash. the recovered addresses - + #### recover`_`public`_`keys`_`from`_`message ```python @@ -165,7 +165,7 @@ Get the public key used to produce the `signature` of the `message` the recovered public keys - + #### get`_`hash ```python @@ -183,7 +183,7 @@ Get the hash of a message. the hash of the message. - + #### is`_`valid`_`address ```python @@ -197,7 +197,7 @@ Check if the address is valid. - `address`: the address to validate - + #### load`_`contract`_`interface ```python @@ -215,7 +215,7 @@ Load contract interface. the interface - + ## CosmosCrypto Objects ```python @@ -224,7 +224,7 @@ class CosmosCrypto(Crypto[SigningKey]) Class wrapping the Account Generation from Ethereum ledger. - + #### `__`init`__` ```python @@ -237,7 +237,7 @@ Instantiate an ethereum crypto object. - `private_key_path`: the private key path of the agent - + #### private`_`key ```python @@ -251,7 +251,7 @@ Return a private key. a private key string - + #### public`_`key ```python @@ -265,7 +265,7 @@ Return a public key in hex format. a public key string in hex format - + #### address ```python @@ -279,7 +279,7 @@ Return the address for the key pair. a display_address str - + #### load`_`private`_`key`_`from`_`path ```python @@ -297,7 +297,7 @@ Load a private key in hex format from a file. the Entity. - + #### sign`_`message ```python @@ -315,7 +315,7 @@ Sign a message in bytes string form. signature of the message in string form - + #### sign`_`transaction ```python @@ -332,7 +332,7 @@ Sign a transaction in bytes string form. signed transaction - + #### generate`_`private`_`key ```python @@ -342,7 +342,7 @@ signed transaction Generate a key pair for cosmos network. - + #### dump ```python @@ -359,7 +359,7 @@ Serialize crypto object as binary stream to `fp` (a `.write()`-supporting file-l None - + ## `_`CosmosApi Objects ```python @@ -368,7 +368,7 @@ class _CosmosApi(LedgerApi) Class to interact with the Cosmos SDK via a HTTP APIs. - + #### `__`init`__` ```python @@ -377,7 +377,7 @@ Class to interact with the Cosmos SDK via a HTTP APIs. Initialize the Cosmos ledger APIs. - + #### api ```python @@ -387,7 +387,7 @@ Initialize the Cosmos ledger APIs. Get the underlying API object. - + #### get`_`balance ```python @@ -396,7 +396,7 @@ Get the underlying API object. Get the balance of a given account. - + #### get`_`state ```python @@ -410,7 +410,7 @@ API specification, which takes a path (strings separated by '/'). The convention here is to define the root of the path (txs, blocks, etc.) as the callable_name and the rest of the path as args. - + #### get`_`deploy`_`transaction ```python @@ -427,7 +427,7 @@ Dispatches to _get_storage_transaction and _get_init_transaction based on kwargs - `deployer_address`: The address that will deploy the contract. :returns tx: the transaction dictionary. - + #### get`_`handle`_`transaction ```python @@ -441,6 +441,9 @@ Create a CosmWasm HandleMsg transaction. - `sender_address`: the sender address of the message initiator. - `contract_address`: the address of the smart contract. - `handle_msg`: HandleMsg in JSON format. +- `amount`: Funds amount sent with transaction. +- `tx_fee`: the tx fee accepted. +- `denom`: the name of the denomination of the contract funds - `gas`: Maximum amount of gas to be used on executing command. - `memo`: any string comment. - `chain_id`: the Chain ID of the CosmWasm transaction. Default is 1 (i.e. mainnet). @@ -449,7 +452,7 @@ Create a CosmWasm HandleMsg transaction. the unsigned CosmWasm HandleMsg - + #### execute`_`contract`_`query ```python @@ -467,7 +470,7 @@ Execute a CosmWasm QueryMsg. QueryMsg doesn't require signing. the message receipt - + #### get`_`transfer`_`transaction ```python @@ -492,7 +495,7 @@ Submit a transfer transaction to the ledger. the transfer transaction - + #### send`_`signed`_`transaction ```python @@ -509,17 +512,16 @@ Send a signed transaction and wait for confirmation. tx_digest, if present - + #### is`_`cosmwasm`_`transaction ```python - | @staticmethod | is_cosmwasm_transaction(tx_signed: JSONLike) -> bool ``` Check whether it is a cosmwasm tx. - + #### is`_`transfer`_`transaction ```python @@ -529,7 +531,7 @@ Check whether it is a cosmwasm tx. Check whether it is a transfer tx. - + #### get`_`transaction`_`receipt ```python @@ -546,7 +548,7 @@ Get the transaction receipt for a transaction digest. the tx receipt, if present - + #### get`_`transaction ```python @@ -563,7 +565,7 @@ Get the transaction for a transaction digest. the tx, if present - + #### get`_`contract`_`instance ```python @@ -581,7 +583,7 @@ Get the instance of a contract. the contract instance - + #### get`_`last`_`code`_`id ```python @@ -594,7 +596,7 @@ Get ID of latest deployed .wasm bytecode. code id of last deployed .wasm bytecode - + #### get`_`last`_`contract`_`address ```python @@ -611,7 +613,7 @@ Get contract address of latest initialised contract by its ID. contract address of last initialised contract - + #### update`_`with`_`gas`_`estimate ```python @@ -628,7 +630,7 @@ Attempts to update the transaction with a gas estimate the updated transaction - + ## CosmosApi Objects ```python @@ -637,7 +639,7 @@ class CosmosApi(_CosmosApi, CosmosHelper) Class to interact with the Cosmos SDK via a HTTP APIs. - + ## CosmosFaucetApi Objects ```python @@ -646,7 +648,7 @@ class CosmosFaucetApi(FaucetApi) Cosmos testnet faucet API. - + #### `__`init`__` ```python @@ -655,7 +657,7 @@ Cosmos testnet faucet API. Initialize CosmosFaucetApi. - + #### get`_`wealth ```python diff --git a/docs/api/crypto/ethereum.md b/docs/api/plugins/aea_ledger_ethereum/ethereum.md similarity index 67% rename from docs/api/crypto/ethereum.md rename to docs/api/plugins/aea_ledger_ethereum/ethereum.md index 02d4ae585b..16f6db449a 100644 --- a/docs/api/crypto/ethereum.md +++ b/docs/api/plugins/aea_ledger_ethereum/ethereum.md @@ -1,9 +1,9 @@ - -# aea.crypto.ethereum + +# plugins.aea-ledger-ethereum.aea`_`ledger`_`ethereum.ethereum Ethereum module wrapping the public and private key cryptography and ledger api. - + ## SignedTransactionTranslator Objects ```python @@ -12,7 +12,7 @@ class SignedTransactionTranslator() Translator for SignedTransaction. - + #### to`_`dict ```python @@ -22,7 +22,7 @@ Translator for SignedTransaction. Write SignedTransaction to dict. - + #### from`_`dict ```python @@ -32,7 +32,7 @@ Write SignedTransaction to dict. Get SignedTransaction from dict. - + ## AttributeDictTranslator Objects ```python @@ -41,7 +41,7 @@ class AttributeDictTranslator() Translator for AttributeDict. - + #### to`_`dict ```python @@ -51,7 +51,7 @@ Translator for AttributeDict. Simplify to dict. - + #### from`_`dict ```python @@ -61,7 +61,7 @@ Simplify to dict. Get back attribute dict. - + ## EthereumCrypto Objects ```python @@ -70,7 +70,7 @@ class EthereumCrypto(Crypto[Account]) Class wrapping the Account Generation from Ethereum ledger. - + #### `__`init`__` ```python @@ -83,7 +83,7 @@ Instantiate an ethereum crypto object. - `private_key_path`: the private key path of the agent - + #### private`_`key ```python @@ -97,7 +97,7 @@ Return a private key. a private key string - + #### public`_`key ```python @@ -111,7 +111,7 @@ Return a public key in hex format. a public key string in hex format - + #### address ```python @@ -125,7 +125,7 @@ Return the address for the key pair. a display_address str - + #### load`_`private`_`key`_`from`_`path ```python @@ -143,7 +143,7 @@ Load a private key in hex format from a file. the Entity. - + #### sign`_`message ```python @@ -161,7 +161,7 @@ Sign a message in bytes string form. signature of the message in string form - + #### sign`_`transaction ```python @@ -178,7 +178,7 @@ Sign a transaction in bytes string form. signed transaction - + #### generate`_`private`_`key ```python @@ -188,7 +188,7 @@ signed transaction Generate a key pair for ethereum network. - + #### dump ```python @@ -205,7 +205,7 @@ Serialize crypto object as binary stream to `fp` (a `.write()`-supporting file-l None - + ## EthereumHelper Objects ```python @@ -214,7 +214,7 @@ class EthereumHelper(Helper) Helper class usable as Mixin for EthereumApi or as standalone class. - + #### is`_`transaction`_`settled ```python @@ -232,7 +232,7 @@ Check whether a transaction is settled or not. True if the transaction has been settled, False o/w. - + #### is`_`transaction`_`valid ```python @@ -254,7 +254,7 @@ Check whether a transaction is valid or not. True if the random_message is equals to tx['input'] - + #### generate`_`tx`_`nonce ```python @@ -273,7 +273,7 @@ Generate a unique hash to distinguish txs with the same terms. return the hash in hex. - + #### get`_`address`_`from`_`public`_`key ```python @@ -291,7 +291,7 @@ Get the address from the public key. str - + #### recover`_`message ```python @@ -311,7 +311,7 @@ Recover the addresses from the hash. the recovered addresses - + #### recover`_`public`_`keys`_`from`_`message ```python @@ -331,7 +331,7 @@ Get the public key used to produce the `signature` of the `message` the recovered public keys - + #### get`_`hash ```python @@ -349,7 +349,7 @@ Get the hash of a message. the hash of the message. - + #### load`_`contract`_`interface ```python @@ -367,7 +367,7 @@ Load contract interface. the interface - + ## EthereumApi Objects ```python @@ -376,7 +376,7 @@ class EthereumApi(LedgerApi, EthereumHelper) Class to interact with the Ethereum Web3 APIs. - + #### `__`init`__` ```python @@ -389,7 +389,7 @@ Initialize the Ethereum ledger APIs. - `address`: the endpoint for Web3 APIs. - + #### api ```python @@ -399,7 +399,7 @@ Initialize the Ethereum ledger APIs. Get the underlying API object. - + #### get`_`balance ```python @@ -408,7 +408,7 @@ Get the underlying API object. Get the balance of a given account. - + #### get`_`state ```python @@ -417,7 +417,7 @@ Get the balance of a given account. Call a specified function on the ledger API. - + #### get`_`transfer`_`transaction ```python @@ -440,7 +440,7 @@ Submit a transfer transaction to the ledger. the transfer transaction - + #### update`_`with`_`gas`_`estimate ```python @@ -457,7 +457,7 @@ Attempts to update the transaction with a gas estimate the updated transaction - + #### send`_`signed`_`transaction ```python @@ -474,7 +474,7 @@ Send a signed transaction and wait for confirmation. tx_digest, if present - + #### get`_`transaction`_`receipt ```python @@ -491,7 +491,7 @@ Get the transaction receipt for a transaction digest. the tx receipt, if present - + #### get`_`transaction ```python @@ -508,7 +508,7 @@ Get the transaction for a transaction digest. the tx, if present - + #### get`_`contract`_`instance ```python @@ -526,7 +526,7 @@ Get the instance of a contract. the contract instance - + #### get`_`deploy`_`transaction ```python @@ -543,7 +543,7 @@ Get the transaction to deploy the smart contract. - `gas`: the gas to be used :returns tx: the transaction dictionary. - + #### is`_`valid`_`address ```python @@ -557,7 +557,7 @@ Check if the address is valid. - `address`: the address to validate - + ## EthereumFaucetApi Objects ```python @@ -566,7 +566,7 @@ class EthereumFaucetApi(FaucetApi) Ethereum testnet faucet API. - + #### get`_`wealth ```python @@ -584,7 +584,7 @@ Get wealth from the faucet for the provided address. None - + ## LruLockWrapper Objects ```python @@ -593,7 +593,7 @@ class LruLockWrapper() Wrapper for LRU with threading.Lock. - + #### `__`init`__` ```python @@ -602,7 +602,7 @@ Wrapper for LRU with threading.Lock. Init wrapper. - + #### `__`getitem`__` ```python @@ -611,7 +611,7 @@ Init wrapper. Get item - + #### `__`setitem`__` ```python @@ -620,7 +620,7 @@ Get item Set item. - + #### `__`contains`__` ```python @@ -629,7 +629,7 @@ Set item. Contain item. - + #### `__`delitem`__` ```python @@ -638,7 +638,7 @@ Contain item. Del item. - + #### set`_`wrapper`_`for`_`web3py`_`session`_`cache ```python diff --git a/docs/api/plugins/aea_ledger_fetchai/_cosmos.md b/docs/api/plugins/aea_ledger_fetchai/_cosmos.md new file mode 100644 index 0000000000..d849086144 --- /dev/null +++ b/docs/api/plugins/aea_ledger_fetchai/_cosmos.md @@ -0,0 +1,678 @@ + +# plugins.aea-ledger-fetchai.aea`_`ledger`_`fetchai.`_`cosmos + +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: JSONLike) -> bool +``` + +Check whether a transaction is settled or not. + +**Arguments**: + +- `tx_receipt`: the receipt of the transaction. + +**Returns**: + +True if the transaction has been settled, False o/w. + + +#### get`_`code`_`id + +```python + | @staticmethod + | get_code_id(tx_receipt: JSONLike) -> Optional[int] +``` + +Retrieve the `code_id` from a transaction receipt. + +**Arguments**: + +- `tx_receipt`: the receipt of the transaction. + +**Returns**: + +the code id, if present + + +#### get`_`contract`_`address + +```python + | @staticmethod + | get_contract_address(tx_receipt: JSONLike) -> Optional[str] +``` + +Retrieve the `contract_address` from a transaction receipt. + +**Arguments**: + +- `tx_receipt`: the receipt of the transaction. + +**Returns**: + +the contract address, if present + + +#### is`_`transaction`_`valid + +```python + | @staticmethod + | is_transaction_valid(tx: JSONLike, 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 + + +#### recover`_`public`_`keys`_`from`_`message + +```python + | @classmethod + | recover_public_keys_from_message(cls, message: bytes, signature: str, is_deprecated_mode: bool = False) -> Tuple[str, ...] +``` + +Get the public key used to produce the `signature` of the `message` + +**Arguments**: + +- `message`: raw bytes used to produce signature +- `signature`: signature of the message +- `is_deprecated_mode`: if the deprecated signing was used + +**Returns**: + +the recovered public keys + + +#### 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. + + +#### is`_`valid`_`address + +```python + | @classmethod + | is_valid_address(cls, address: Address) -> bool +``` + +Check if the address is valid. + +**Arguments**: + +- `address`: the address to validate + + +#### load`_`contract`_`interface + +```python + | @classmethod + | load_contract_interface(cls, file_path: Path) -> Dict[str, str] +``` + +Load contract interface. + +**Arguments**: + +- `file_path`: the file path to the interface + +**Returns**: + +the interface + + +## CosmosCrypto Objects + +```python +class CosmosCrypto(Crypto[SigningKey]) +``` + +Class wrapping the Account Generation from Ethereum ledger. + + +#### `__`init`__` + +```python + | __init__(private_key_path: Optional[str] = None) -> None +``` + +Instantiate an ethereum 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) -> SigningKey +``` + +Load a private key in hex format from a file. + +**Arguments**: + +- `file_name`: the path to the hex file. + +**Returns**: + +the Entity. + + +#### sign`_`message + +```python + | sign_message(message: bytes, is_deprecated_mode: bool = False) -> str +``` + +Sign a message in bytes string form. + +**Arguments**: + +- `message`: the message to be signed +- `is_deprecated_mode`: if the deprecated signing is used + +**Returns**: + +signature of the message in string form + + +#### sign`_`transaction + +```python + | sign_transaction(transaction: JSONLike) -> JSONLike +``` + +Sign a transaction in bytes string form. + +**Arguments**: + +- `transaction`: the transaction to be signed + +**Returns**: + +signed transaction + + +#### generate`_`private`_`key + +```python + | @classmethod + | generate_private_key(cls) -> SigningKey +``` + +Generate a key pair for cosmos network. + + +#### 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 + + +## `_`CosmosApi Objects + +```python +class _CosmosApi(LedgerApi) +``` + +Class to interact with the Cosmos SDK via a HTTP APIs. + + +#### `__`init`__` + +```python + | __init__(**kwargs: Any) -> None +``` + +Initialize the Cosmos ledger APIs. + + +#### api + +```python + | @property + | api() -> Any +``` + +Get the underlying API object. + + +#### get`_`balance + +```python + | get_balance(address: Address) -> Optional[int] +``` + +Get the balance of a given account. + + +#### get`_`state + +```python + | get_state(callable_name: str, *args: Any, **kwargs: Any) -> Optional[JSONLike] +``` + +Call a specified function on the ledger API. + +Based on the cosmos REST +API specification, which takes a path (strings separated by '/'). The +convention here is to define the root of the path (txs, blocks, etc.) +as the callable_name and the rest of the path as args. + + +#### get`_`deploy`_`transaction + +```python + | get_deploy_transaction(contract_interface: Dict[str, str], deployer_address: Address, **kwargs: Any, ,) -> Optional[JSONLike] +``` + +Get the transaction to deploy the smart contract. + +Dispatches to _get_storage_transaction and _get_init_transaction based on kwargs. + +**Arguments**: + +- `contract_interface`: the contract interface. +- `deployer_address`: The address that will deploy the contract. +:returns tx: the transaction dictionary. + + +#### get`_`handle`_`transaction + +```python + | get_handle_transaction(sender_address: Address, contract_address: Address, handle_msg: Any, amount: int, tx_fee: int, denom: Optional[str] = None, gas: int = 0, memo: str = "", chain_id: Optional[str] = None) -> Optional[JSONLike] +``` + +Create a CosmWasm HandleMsg transaction. + +**Arguments**: + +- `sender_address`: the sender address of the message initiator. +- `contract_address`: the address of the smart contract. +- `handle_msg`: HandleMsg in JSON format. +- `amount`: Funds amount sent with transaction. +- `tx_fee`: the tx fee accepted. +- `denom`: the name of the denomination of the contract funds +- `gas`: Maximum amount of gas to be used on executing command. +- `memo`: any string comment. +- `chain_id`: the Chain ID of the CosmWasm transaction. Default is 1 (i.e. mainnet). + +**Returns**: + +the unsigned CosmWasm HandleMsg + + +#### execute`_`contract`_`query + +```python + | execute_contract_query(contract_address: Address, query_msg: JSONLike) -> Optional[JSONLike] +``` + +Execute a CosmWasm QueryMsg. QueryMsg doesn't require signing. + +**Arguments**: + +- `contract_address`: the address of the smart contract. +- `query_msg`: QueryMsg in JSON format. + +**Returns**: + +the message receipt + + +#### get`_`transfer`_`transaction + +```python + | get_transfer_transaction(sender_address: Address, destination_address: Address, amount: int, tx_fee: int, tx_nonce: str, denom: Optional[str] = None, gas: int = 80000, memo: str = "", chain_id: Optional[str] = None, **kwargs: Any, ,) -> Optional[JSONLike] +``` + +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 +- `denom`: the denomination of tx fee and amount +- `gas`: the gas used. +- `memo`: memo to include in tx. +- `chain_id`: the chain ID of the transaction. + +**Returns**: + +the transfer transaction + + +#### send`_`signed`_`transaction + +```python + | send_signed_transaction(tx_signed: JSONLike) -> Optional[str] +``` + +Send a signed transaction and wait for confirmation. + +**Arguments**: + +- `tx_signed`: the signed transaction + +**Returns**: + +tx_digest, if present + + +#### is`_`cosmwasm`_`transaction + +```python + | is_cosmwasm_transaction(tx_signed: JSONLike) -> bool +``` + +Check whether it is a cosmwasm tx. + + +#### is`_`transfer`_`transaction + +```python + | @staticmethod + | is_transfer_transaction(tx_signed: JSONLike) -> bool +``` + +Check whether it is a transfer tx. + + +#### get`_`transaction`_`receipt + +```python + | get_transaction_receipt(tx_digest: str) -> Optional[JSONLike] +``` + +Get the transaction receipt for a transaction digest. + +**Arguments**: + +- `tx_digest`: the digest associated to the transaction. + +**Returns**: + +the tx receipt, if present + + +#### get`_`transaction + +```python + | get_transaction(tx_digest: str) -> Optional[JSONLike] +``` + +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`_`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`_`last`_`contract`_`address + +```python + | get_last_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 + + +#### update`_`with`_`gas`_`estimate + +```python + | update_with_gas_estimate(transaction: JSONLike) -> JSONLike +``` + +Attempts to update the transaction with a gas estimate + +**Arguments**: + +- `transaction`: the transaction + +**Returns**: + +the updated transaction + + +## CosmosApi Objects + +```python +class CosmosApi(_CosmosApi, CosmosHelper) +``` + +Class to interact with the Cosmos SDK via a HTTP APIs. + + +## CosmosFaucetApi Objects + +```python +class CosmosFaucetApi(FaucetApi) +``` + +Cosmos testnet faucet API. + + +#### `__`init`__` + +```python + | __init__(poll_interval: Optional[float] = None) +``` + +Initialize CosmosFaucetApi. + + +#### get`_`wealth + +```python + | get_wealth(address: Address, url: Optional[str] = None) -> None +``` + +Get wealth from the faucet for the provided address. + +**Arguments**: + +- `address`: the address. +- `url`: the url + +**Returns**: + +None +:raises: RuntimeError of explicit faucet failures + diff --git a/docs/api/crypto/fetchai.md b/docs/api/plugins/aea_ledger_fetchai/fetchai.md similarity index 56% rename from docs/api/crypto/fetchai.md rename to docs/api/plugins/aea_ledger_fetchai/fetchai.md index d1d44776e4..8bff79a609 100644 --- a/docs/api/crypto/fetchai.md +++ b/docs/api/plugins/aea_ledger_fetchai/fetchai.md @@ -1,9 +1,9 @@ - -# aea.crypto.fetchai + +# plugins.aea-ledger-fetchai.aea`_`ledger`_`fetchai.fetchai Fetchai module wrapping the public and private key cryptography and ledger api. - + ## FetchAIHelper Objects ```python @@ -12,7 +12,7 @@ class FetchAIHelper(CosmosHelper) Helper class usable as Mixin for FetchAIApi or as standalone class. - + ## FetchAICrypto Objects ```python @@ -21,7 +21,7 @@ class FetchAICrypto(CosmosCrypto) Class wrapping the Entity Generation from Fetch.AI ledger. - + ## FetchAIApi Objects ```python @@ -30,7 +30,7 @@ class FetchAIApi(_CosmosApi, FetchAIHelper) Class to interact with the Fetch ledger APIs. - + #### `__`init`__` ```python @@ -39,7 +39,7 @@ Class to interact with the Fetch ledger APIs. Initialize the Fetch.ai ledger APIs. - + ## FetchAIFaucetApi Objects ```python diff --git a/docs/api/protocols/dialogue/base.md b/docs/api/protocols/dialogue/base.md index df79d91235..68d5cad605 100644 --- a/docs/api/protocols/dialogue/base.md +++ b/docs/api/protocols/dialogue/base.md @@ -169,8 +169,7 @@ class _DialogueMeta(type) Metaclass for Dialogue. -Adds slot support forevery subclass -Creates classlevvel Rules instance +Creates class level Rules instance to share among instances #### `__`new`__` @@ -422,7 +421,7 @@ The incomplete dialogue label | dialogue_labels() -> Set[DialogueLabel] ``` -Get the dialogue labels (incomplete and complete, if it exists) +Get the dialogue labels (incomplete and complete, if it exists). **Returns**: diff --git a/docs/api/skills/base.md b/docs/api/skills/base.md index 8501e4ff08..24ea005277 100644 --- a/docs/api/skills/base.md +++ b/docs/api/skills/base.md @@ -302,6 +302,24 @@ Get the agent context namespace. Get attribute. + +#### send`_`to`_`skill + +```python + | send_to_skill(message_or_envelope: Union[Message, Envelope], context: Optional[EnvelopeContext] = None) -> None +``` + +Send message or envelope to another skill. + +**Arguments**: + +- `message_or_envelope`: envelope to send to another skill. +if message passed it will be wrapped into envelope with optional envelope context. + +**Returns**: + +None + ## SkillComponent Objects @@ -619,7 +637,7 @@ Tear the class down. | parse_module(cls, path: str, model_configs: Dict[str, SkillComponentConfiguration], skill_context: SkillContext) -> Dict[str, "Model"] ``` -Parse the tasks module. +Parse the model module. **Arguments**: @@ -758,3 +776,48 @@ Load the skill from configuration. the skill. + +## `_`SkillComponentLoadingItem Objects + +```python +class _SkillComponentLoadingItem() +``` + +Class to represent a triple (component name, component configuration, component class). + + +#### `__`init`__` + +```python + | __init__(name: str, config: SkillComponentConfiguration, class_: Type[SkillComponent], type_: _SKILL_COMPONENT_TYPES) +``` + +Initialize the item. + + +## `_`SkillComponentLoader Objects + +```python +class _SkillComponentLoader() +``` + +This class implements the loading policy for skill components. + + +#### `__`init`__` + +```python + | __init__(configuration: SkillConfig, skill_context: SkillContext, **kwargs: Any) +``` + +Initialize the helper class. + + +#### load`_`skill + +```python + | load_skill() -> Skill +``` + +Load the skill. + diff --git a/docs/api/test_tools/test_skill.md b/docs/api/test_tools/test_skill.md index ec44907fb3..766294b1f8 100644 --- a/docs/api/test_tools/test_skill.md +++ b/docs/api/test_tools/test_skill.md @@ -118,7 +118,7 @@ boolean result of the evaluation and accompanied message #### build`_`incoming`_`message ```python - | build_incoming_message(message_type: Type[Message], performative: Message.Performative, dialogue_reference: Optional[Tuple[str, str]] = None, message_id: Optional[int] = None, target: Optional[int] = None, to: Optional[Address] = None, sender: Address = COUNTERPARTY_ADDRESS, **kwargs: Any, ,) -> Message + | build_incoming_message(message_type: Type[Message], performative: Message.Performative, dialogue_reference: Optional[Tuple[str, str]] = None, message_id: Optional[int] = None, target: Optional[int] = None, to: Optional[Address] = None, sender: Optional[Address] = None, is_agent_to_agent_messages: Optional[bool] = None, **kwargs: Any, ,) -> Message ``` Quickly create an incoming message with the provided attributes. @@ -134,6 +134,7 @@ For any attribute not provided, the corresponding default value in message is us - `performative`: the performative - `to`: the 'to' address - `sender`: the 'sender' address +- `is_agent_to_agent_messages`: whether the dialogue is between agents or components - `kwargs`: other attributes **Returns**: @@ -176,7 +177,7 @@ the created incoming message #### prepare`_`skill`_`dialogue ```python - | prepare_skill_dialogue(dialogues: Dialogues, messages: Tuple[DialogueMessage, ...], counterparty: Address = COUNTERPARTY_ADDRESS) -> Dialogue + | prepare_skill_dialogue(dialogues: Dialogues, messages: Tuple[DialogueMessage, ...], counterparty: Optional[Address] = None, is_agent_to_agent_messages: Optional[bool] = None) -> Dialogue ``` Quickly create a dialogue. @@ -193,6 +194,7 @@ for any other message, it is the index of the message before it in the tuple of - `dialogues`: a dialogues class - `counterparty`: the message_id - `messages`: the dialogue_reference +- `is_agent_to_agent_messages`: whether the dialogue is between agents or components **Returns**: diff --git a/docs/aries-cloud-agent-demo.md b/docs/aries-cloud-agent-demo.md index 8197c033e1..01351a58ca 100644 --- a/docs/aries-cloud-agent-demo.md +++ b/docs/aries-cloud-agent-demo.md @@ -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.21.0 +aea fetch fetchai/aries_alice:0.22.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.16.0 -aea add connection fetchai/soef:0.17.0 -aea add connection fetchai/http_client:0.17.0 -aea add connection fetchai/webhook:0.13.0 -aea add skill fetchai/aries_alice:0.16.0 +aea add connection fetchai/p2p_libp2p:0.17.0 +aea add connection fetchai/soef:0.18.0 +aea add connection fetchai/http_client:0.18.0 +aea add connection fetchai/webhook:0.14.0 +aea add skill fetchai/aries_alice:0.17.0 ```

@@ -257,14 +257,14 @@ Finally run **Alice_AEA**: aea run ``` -Once you see a message of the form `To join its network use multiaddr 'SOME_ADDRESS'` take note of the address. (Alternatively, use `aea get-multiaddress fetchai -c -i fetchai/p2p_libp2p:0.16.0 -u public_uri` to retrieve the address.) We will refer to this as **Alice_AEA's P2P address**. +Once you see a message of the form `To join its network use multiaddr 'SOME_ADDRESS'` take note of the address. (Alternatively, use `aea get-multiaddress fetchai -c -i fetchai/p2p_libp2p:0.17.0 -u public_uri` to retrieve the address.) We will refer to this as **Alice_AEA's P2P address**. ### Faber_AEA In the fourth terminal, fetch **Faber_AEA** and move into its project folder: ``` bash -aea fetch fetchai/aries_faber:0.21.0 +aea fetch fetchai/aries_faber:0.22.0 cd aries_faber ``` @@ -275,11 +275,11 @@ The following steps create **Faber_AEA** from scratch: ``` bash aea create aries_faber cd aries_faber -aea add connection fetchai/p2p_libp2p:0.16.0 -aea add connection fetchai/soef:0.17.0 -aea add connection fetchai/http_client:0.17.0 -aea add connection fetchai/webhook:0.13.0 -aea add skill fetchai/aries_faber:0.15.0 +aea add connection fetchai/p2p_libp2p:0.17.0 +aea add connection fetchai/soef:0.18.0 +aea add connection fetchai/http_client:0.18.0 +aea add connection fetchai/webhook:0.14.0 +aea add skill fetchai/aries_faber:0.16.0 ```

diff --git a/docs/assets/cli_gui01_clean.png b/docs/assets/cli_gui01_clean.png deleted file mode 100644 index 85b985a332..0000000000 Binary files a/docs/assets/cli_gui01_clean.png and /dev/null differ diff --git a/docs/assets/cli_gui02_sequence_01.png b/docs/assets/cli_gui02_sequence_01.png deleted file mode 100644 index 8d5bdb083b..0000000000 Binary files a/docs/assets/cli_gui02_sequence_01.png and /dev/null differ diff --git a/docs/assets/cli_gui02_sequence_02.png b/docs/assets/cli_gui02_sequence_02.png deleted file mode 100644 index 85437ba335..0000000000 Binary files a/docs/assets/cli_gui02_sequence_02.png and /dev/null differ diff --git a/docs/assets/cli_gui03_oef_node.png b/docs/assets/cli_gui03_oef_node.png deleted file mode 100644 index 9113921cf5..0000000000 Binary files a/docs/assets/cli_gui03_oef_node.png and /dev/null differ diff --git a/docs/assets/cli_gui04_new_agent.png b/docs/assets/cli_gui04_new_agent.png deleted file mode 100644 index 87dc151533..0000000000 Binary files a/docs/assets/cli_gui04_new_agent.png and /dev/null differ diff --git a/docs/assets/cli_gui05_full_running_agent.png b/docs/assets/cli_gui05_full_running_agent.png deleted file mode 100644 index b2ca910b60..0000000000 Binary files a/docs/assets/cli_gui05_full_running_agent.png and /dev/null differ diff --git a/docs/build-aea-programmatically.md b/docs/build-aea-programmatically.md index ab2a58a92c..e1f88f59c6 100644 --- a/docs/build-aea-programmatically.md +++ b/docs/build-aea-programmatically.md @@ -9,6 +9,11 @@ Get the packages directory from the AEA repository: svn export https://github.com/fetchai/agents-aea.git/trunk/packages ``` +Also, install `aea-ledger-fetchai` plug-in: +```bash +pip install aea-ledger-fetchai +``` + ## Imports First, import the necessary common Python libraries and classes. @@ -22,9 +27,10 @@ from threading import Thread Then, import the application specific libraries. ``` python +from aea_ledger_fetchai import FetchAICrypto + from aea.aea_builder import AEABuilder from aea.configurations.base import SkillConfig -from aea.crypto.fetchai import FetchAICrypto from aea.crypto.helpers import PRIVATE_KEY_PATH_SCHEMA, create_private_key from aea.helpers.file_io import write_with_lock from aea.skills.base import Skill @@ -56,7 +62,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.12.0`, `fetchai/state_update:0.10.0` and `fetchai/signing:0.10.0` protocols. +We use the `AEABuilder` to readily build an AEA. By default, the `AEABuilder` adds the `fetchai/default:0.13.0`, `fetchai/state_update:0.11.0` and `fetchai/signing:0.11.0` protocols. ``` python # Instantiate the builder and build the AEA # By default, the default protocol, error skill and stub connection are added @@ -162,8 +168,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.12.0,\x12\x10\x08\x01\x12\x011*\t*\x07\n\x05hello, - output message: other_agent,my_aea,fetchai/default:0.12.0,...\x05hello + input message: my_aea,other_agent,fetchai/default:0.13.0,\x12\x10\x08\x01\x12\x011*\t*\x07\n\x05hello, + output message: other_agent,my_aea,fetchai/default:0.13.0,...\x05hello ## Entire code listing @@ -177,9 +183,10 @@ import os import time from threading import Thread +from aea_ledger_fetchai import FetchAICrypto + from aea.aea_builder import AEABuilder from aea.configurations.base import SkillConfig -from aea.crypto.fetchai import FetchAICrypto from aea.crypto.helpers import PRIVATE_KEY_PATH_SCHEMA, create_private_key from aea.helpers.file_io import write_with_lock from aea.skills.base import Skill diff --git a/docs/car-park-skills.md b/docs/car-park-skills.md index 4a56afe2d7..89348530fd 100644 --- a/docs/car-park-skills.md +++ b/docs/car-park-skills.md @@ -55,7 +55,7 @@ Follow the Preliminaries and =0.1.0"} +}' +aea config set agent.default_connection fetchai/p2p_libp2p:0.17.0 aea config set --type dict agent.default_routing \ '{ - "fetchai/ledger_api:0.10.0": "fetchai/ledger:0.13.0", - "fetchai/oef_search:0.13.0": "fetchai/soef:0.17.0" + "fetchai/ledger_api:0.11.0": "fetchai/ledger:0.14.0", + "fetchai/oef_search:0.14.0": "fetchai/soef:0.18.0" }' +aea install +aea build ```

@@ -89,7 +93,7 @@ aea config set --type dict agent.default_routing \ Then, fetch the car data client AEA: ``` bash -aea fetch fetchai/car_data_buyer:0.23.0 +aea fetch fetchai/car_data_buyer:0.24.0 cd car_data_buyer aea install aea build @@ -102,18 +106,22 @@ 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.16.0 -aea add connection fetchai/soef:0.17.0 -aea add connection fetchai/ledger:0.13.0 -aea add skill fetchai/carpark_client:0.20.0 -aea install -aea build -aea config set agent.default_connection fetchai/p2p_libp2p:0.16.0 +aea add connection fetchai/p2p_libp2p:0.17.0 +aea add connection fetchai/soef:0.18.0 +aea add connection fetchai/ledger:0.14.0 +aea add skill fetchai/carpark_client:0.21.0 +aea config set --type dict agent.dependencies \ +'{ + "aea-ledger-fetchai": {"version": "<0.2.0,>=0.1.0"} +}' +aea config set agent.default_connection fetchai/p2p_libp2p:0.17.0 aea config set --type dict agent.default_routing \ '{ - "fetchai/ledger_api:0.10.0": "fetchai/ledger:0.13.0", - "fetchai/oef_search:0.13.0": "fetchai/soef:0.17.0" + "fetchai/ledger_api:0.11.0": "fetchai/ledger:0.14.0", + "fetchai/oef_search:0.14.0": "fetchai/soef:0.18.0" }' +aea install +aea build ``` @@ -175,14 +183,14 @@ First, run the car data seller AEA: aea run ``` -Once you see a message of the form `To join its network use multiaddr 'SOME_ADDRESS'` take note of the address. (Alternatively, use `aea get-multiaddress fetchai -c -i fetchai/p2p_libp2p:0.16.0 -u public_uri` to retrieve the address.) +Once you see a message of the form `To join its network use multiaddr 'SOME_ADDRESS'` take note of the address. (Alternatively, use `aea get-multiaddress fetchai -c -i fetchai/p2p_libp2p:0.17.0 -u public_uri` to retrieve the address.) This is the entry peer address for the local
agent communication network created by the car data seller. -Then, in the buyer, run this command (replace `SOME_ADDRESS` with the correct value as described above): +Then, configure the buyer to connect to this same local ACN by running the following command in the buyer terminal, replacing `SOME_ADDRESS` with the value you noted above: ``` bash aea config set --type dict vendor.fetchai.connections.p2p_libp2p.config \ '{ @@ -278,9 +284,9 @@ aea config set --type dict vendor.fetchai.connections.p2p_libp2p.config \ "public_uri": "127.0.0.1:9001" }' ``` -This allows the buyer to connect to the same local agent communication network as the seller. Then run the buyer AEA: + ``` bash aea run ``` @@ -288,7 +294,9 @@ aea run You will see that the AEAs negotiate and then transact using the Fetch.ai testnet. ## Delete the AEAs -When you're done, go up a level and delete the AEAs. + +When you're done, stop the agents (`CTRL+C`), go up a level and delete the AEAs. + ``` bash cd .. aea delete my_seller_aea diff --git a/docs/gym-skill.md b/docs/gym-skill.md index 9d5faf5e0e..370f6ae44e 100644 --- a/docs/gym-skill.md +++ b/docs/gym-skill.md @@ -19,7 +19,7 @@ Follow the Preliminaries and It MUST implement protocols according to their specification (see here for details). -
  • It SHOULD implement the fetchai/default:0.12.0 protocol which satisfies the following protobuf schema: +
  • It SHOULD implement the fetchai/default:0.13.0 protocol which satisfies the following protobuf schema: ``` proto syntax = "proto3"; @@ -121,7 +121,7 @@ message DefaultMessage{
  • 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 elliptic curve must be of type SECP256k1).
  • -
  • It SHOULD implement handling of errors using the fetchai/default:0.12.0 protocol. The protobuf schema is given above. +
  • It SHOULD implement handling of errors using the fetchai/default:0.13.0 protocol. The protobuf schema is given above.
  • It MUST implement the following principles when handling messages: