diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 6908645e84..ce21001aaf 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -54,6 +54,7 @@ _Put an `x` in the boxes that apply._ - [ ] I built the documentation and updated it with the latest changes - [ ] I've added an item in `HISTORY.rst` for this release - [ ] I bumped the version number in the `aea/__version__.py` file. +- [ ] I bumped the version number in the `docs/version.md` file - [ ] I bumped the version number in every Docker image of the repo and published it. Also, I built and published them with tag `latest` (check the READMEs of [`aea-develop`](../develop-image/README.md#publish) and [`aea-deploy`](../deploy-image/README.md#publish)) diff --git a/HISTORY.rst b/HISTORY.rst index 372a8f902e..ec7e260c3b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -125,3 +125,22 @@ Release History - Adds support to run multiple connections from CLI - Updates the docs and adds uml diagrams - Multiple additional minor fixes and changes + +0.1.15 (2019-12-19) +------------------- + +- Moves non-default packages from aea to packages directory +- Supports get & set on package configs +- Changes skill configuration resource types from lists to dictionaries +- Adds additional features to decision maker +- Refactors most protocols and improves their API +- Removes multiple unintended side-effects of the CLI +- Improves dependency referencing in config files +- Adds push and publish functionality to CLI +- Introduces simple and composite behaviours and applies them in skills +- Adds URI to envelopes +- Adds guide for programmatic assembly of an AEA +- Adds guide on agent-oriented development +- Multiple minor doc updates +- Adds additional tests +- Multiple additional minor fixes and changes diff --git a/Pipfile b/Pipfile index ba1f844bc6..a69ac0c620 100644 --- a/Pipfile +++ b/Pipfile @@ -25,6 +25,7 @@ pygments = "*" pytest-asyncio = "*" gym = "*" numpy = "*" +tensorflow = "*" scikit-image = "*" mkdocs-mermaid-plugin = {editable = true,git = "https://github.com/pugong/mkdocs-mermaid-plugin.git"} @@ -45,6 +46,7 @@ python-dotenv = "*" fetchai-ledger-api = "==0.8.1" web3 = "==5.2.2" eth-account = "==0.4.0" +fetch-p2p-api = {index = "https://test.pypi.org/simple/",version = "==0.0.1"} [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 89bc838347..4fd1cc29ee 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "54ca472b4d79c59a127c1f10ef626afad67fbfb834d3876a7c03fadac3b2e421" + "sha256": "7669ae15154b83d5eb07fb9f09e9a8237e770d5a0a5d5af333de44fae47565c0" }, "pipfile-spec": 6, "requires": { @@ -242,6 +242,13 @@ ], "version": "==1.8.1" }, + "fetch-p2p-api": { + "hashes": [ + "sha256:330bb327016dece905187a8b261b68db12d3689c4d0f0aa299abbc81aedf79d5", + "sha256:4a36bdc7adf7ba4d96f231e473b5b5ee97a037f66afc48cb6b7b990e8d0e77fa" + ], + "version": "==0.0.1" + }, "fetchai-ledger-api": { "hashes": [ "sha256:097272a2032d9db93422cf690808e063e70786ab896ef2910fb281ebee29ef9f", @@ -281,11 +288,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", - "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" + "sha256:b044f07694ef14a6683b097ba56bd081dbc7cdc7c7fe46011e499dfecc082f21", + "sha256:e6ac600a142cf2db707b1998382cc7fc3b02befb7273876e01b8ad10b9652742" ], "markers": "python_version < '3.8'", - "version": "==0.23" + "version": "==1.1.0" }, "inflection": { "hashes": [ @@ -363,10 +370,10 @@ }, "more-itertools": { "hashes": [ - "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", - "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" + "sha256:53ff73f186307d9c8ef17a9600309154a6ae27f25579e80af4db8f047ba14bc2", + "sha256:a0ea684c39bc4315ba7aae406596ef191fd84f873d2d2751f84d64e81a7a2d45" ], - "version": "==7.2.0" + "version": "==8.0.0" }, "msgpack": { "hashes": [ @@ -440,27 +447,27 @@ }, "protobuf": { "hashes": [ - "sha256:0ba5d7626dbc4ce78971c3e62ec37f84c8139ea7008c008660d3312cf11e0db8", - "sha256:189b706f72e8b7ddc965168a79ff296ca5b7bdd95b5b05208afb9818a681c712", - "sha256:3017454b4b3ce4e4b2dc07f1e21e1bd6a41ad11b638997c2d7d493a656b4fd2c", - "sha256:340965444aafc7aac7e3586e930f5b3f8347ca9b350afab60bac84dcc0b94437", - "sha256:44fbc7b1786ab975ec9eba9da765398d58ec705d1c8e856b0523b8f9c1c53cf7", - "sha256:48d96b559fab3063feaebd316352e3418424629d59b77dbcb96ecc4c594d7f5f", - "sha256:5e32923c7896c49b1d3a327fe25a76363d200acdfa97844f5647f1bf9f298da8", - "sha256:6662442fbf22796dbd942bb15b664d70dcc25ae28d371b7e4ca6261e9bc495b7", - "sha256:6bb5d999faceee281bc4a2fc77866c61af7be4b7e5efadc930c42f234a99cafd", - "sha256:83b38b7b61b7c60af0fa03a71c27c4232117453a62ccf69a511284793a400751", - "sha256:90c22f4fd4e01279efc4e4911dafe308f35fcc4310bcb89bcee4d3ca20210d20", - "sha256:97b08853b9bb71512ed52381f05cf2d4179f4234825b505d8f8d2bb9d9429939", - "sha256:aef47082114428b47db73876ecb7751802548830ce5c95dba7ebe24d5e196d7c", - "sha256:b89ed3ba88ea5ec8b2c704a5ae747c9038ee1faff277fcddac75f850e645f7e1", - "sha256:be5afc2e1f5c320bd4a38e73d8b02c67d72dbee370a004732c923c7c8a472f72", - "sha256:ce8e1070dcff0c1149207ab2ee8c88738a5118e96fbf0fa4691659c83f5bb81f", - "sha256:d1c18853c7ad3c8e34edfafc6488fc24f4221c15b516c14796032cc53f8cde94", - "sha256:f4370d0e3d6e1ac2f80911651691ac540901f661b372036ea72637546ba98202" + "sha256:0265379852b9e1f76af6d3d3fe4b3c383a595cc937594bda8565cf69a96baabd", + "sha256:0fcf6c6e004f956df0b261a9ccd1619959dfe90da2d99acb1108a7cad7d3f408", + "sha256:29bd1ed46b2536ad8959401a2f02d2d7b5a309f8e97518e4f92ca6c5ba74dbed", + "sha256:3175d45698edb9a07c1a78a1a4850e674ce8988f20596580158b1d0921d0f057", + "sha256:34a7270940f86da7a28be466ac541c89b6dbf144a6348b9cf7ac6f56b71006ce", + "sha256:38cbc830a4a5ba9956763b0f37090bfd14dd74e72762be6225de2ceac55f4d03", + "sha256:665194f5ad386511ac8d8a0bd57b9ab37b8dd2cd71969458777318e774b9cd46", + "sha256:839bad7d115c77cdff29b488fae6a3ab503ce9a4192bd4c42302a6ea8e5d0f33", + "sha256:934a9869a7f3b0d84eca460e386fba1f7ba2a0c1a120a2648bc41fadf50efd1c", + "sha256:aecdf12ef6dc7fd91713a6da93a86c2f2a8fe54840a3b1670853a2b7402e77c9", + "sha256:b16c9458e34db53c3f6d3903ed3c33d9413e96d975208428edfcb8cbb4c53a8d", + "sha256:c4e90bc27c0691c76e09b5dc506133451e52caee1472b8b3c741b7c912ce43ef", + "sha256:c65d135ea2d85d40309e268106dab02d3bea723db2db21c23ecad4163ced210b", + "sha256:c98dea04a1ff41a70aff2489610f280004831798cb36a068013eed04c698903d", + "sha256:d9049aa194378a426f0b2c784e2054565bf6f754d20fcafdee7102a6250556e8", + "sha256:e028fee51c96de4e81924484c77111dfdea14010ecfc906ea5b252209b0c4de6", + "sha256:e84ad26fb50091b1ea676403c0dd2bd47663099454aa6d88000b1dafecab0941", + "sha256:e88a924b591b06d0191620e9c8aa75297b3111066bb09d49a24bae1054a10c13" ], "index": "pypi", - "version": "==3.11.0" + "version": "==3.11.1" }, "pycparser": { "hashes": [ @@ -521,22 +528,20 @@ }, "pyyaml": { "hashes": [ - "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", - "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", - "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", - "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", - "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", - "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", - "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", - "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", - "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", - "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", - "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", - "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", - "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" + "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", + "sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", + "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", + "sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", + "sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", + "sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", + "sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", + "sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", + "sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", + "sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", + "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4" ], "index": "pypi", - "version": "==5.1.2" + "version": "==5.2" }, "requests": { "hashes": [ @@ -547,10 +552,10 @@ }, "rlp": { "hashes": [ - "sha256:0505fd53278cb4a3ea6baf1b658357ac209bdcdd1b316ac90050c40f669ceacc", - "sha256:ebe80a03c50e3d6aac47f44ddd45048bb99e411203cd764f5da1330e6d83821c" + "sha256:27273fc2dbc3513c1e05ea6b8af28aac8745fb09c164e39e2ed2807bf7e1b342", + "sha256:97b7e770f16442772311b33e6bc28b45318e7c8def69b9df16452304e224e9df" ], - "version": "==1.1.0" + "version": "==1.2.0" }, "six": { "hashes": [ @@ -642,6 +647,21 @@ } }, "develop": { + "absl-py": { + "hashes": [ + "sha256:d9129186431e150d7fe455f1cb1ecbb92bb5dba9da9bc3ef7b012d98c4db2526" + ], + "version": "==0.8.1" + }, + "astor": { + "hashes": [ + "sha256:0e41295809baf43ae8303350e031aff81ae52189b6f881f36d623fa8b2f1960e", + "sha256:2dd9f19bf101f08652cdd773d28d8aa3162197db14749b198caaebbb8acfea99", + "sha256:37a6eed8b371f1228db08234ed7f6cfdc7817a3ed3824797e20cbb11dc2a7862", + "sha256:cd4a8408070226b373cf92796d53948c533a36f9c4d0ab1406b9254fef4c5b12" + ], + "version": "==0.8.0" + }, "attrs": { "hashes": [ "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", @@ -665,6 +685,13 @@ "index": "pypi", "version": "==0.0.1" }, + "cachetools": { + "hashes": [ + "sha256:428266a1c0d36dc5aca63a2d7c5942e88c2c898d72139fca0e97fdd2380517ae", + "sha256:8ea2d3ce97850f31e4a08b0e2b5e6c34997d7216a9d2c98e0f3978630d4da69a" + ], + "version": "==3.1.1" + }, "certifi": { "hashes": [ "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", @@ -672,6 +699,13 @@ ], "version": "==2019.11.28" }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, "click": { "hashes": [ "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", @@ -775,6 +809,87 @@ ], "version": "==0.18.2" }, + "gast": { + "hashes": [ + "sha256:fe939df4583692f0512161ec1c880e0a10e71e6a232da045ab8edd3756fbadf0" + ], + "version": "==0.2.2" + }, + "google-auth": { + "hashes": [ + "sha256:84105be98837fb8436e9d0bcb7a279fd85fa1d97bb35a077e70ba2fb95bcc983", + "sha256:baf1b3f8b29a5f96f66753ad848473699322b63f4d68964e510554b12d002443" + ], + "version": "==1.7.1" + }, + "google-auth-oauthlib": { + "hashes": [ + "sha256:88d2cd115e3391eb85e1243ac6902e76e77c5fe438b7276b297fbe68015458dd", + "sha256:a92a0f6f41a0fb6138454fbc02674e64f89d82a244ea32f98471733c8ef0e0e1" + ], + "version": "==0.4.1" + }, + "google-pasta": { + "hashes": [ + "sha256:41bbe63bab92408452585ff2e1673ec1e35b88e163371cbed2a18510be4e8bc5", + "sha256:644dcf3784cf7147ab01de5dc22e60a638d219d4e4a3a7464eb98997ae2fe66f", + "sha256:713813a9f7d6589e5defdaf21e80e4392eb124662f8bd829acd51a4f8735c0cb" + ], + "version": "==0.1.8" + }, + "grpcio": { + "hashes": [ + "sha256:0419ae5a45f49c7c40d9ae77ae4de9442431b7822851dfbbe56ee0eacb5e5654", + "sha256:1e8631eeee0fb0b4230aeb135e4890035f6ef9159c2a3555fa184468e325691a", + "sha256:24db2fa5438f3815a4edb7a189035051760ca6aa2b0b70a6a948b28bfc63c76b", + "sha256:2adb1cdb7d33e91069517b41249622710a94a1faece1fed31cd36904e4201cde", + "sha256:2cd51f35692b551aeb1fdeb7a256c7c558f6d78fcddff00640942d42f7aeba5f", + "sha256:3247834d24964589f8c2b121b40cd61319b3c2e8d744a6a82008643ef8a378b1", + "sha256:3433cb848b4209717722b62392e575a77a52a34d67c6730138102abc0a441685", + "sha256:39671b7ff77a962bd745746d9d2292c8ed227c5748f16598d16d8631d17dd7e5", + "sha256:40a0b8b2e6f6dd630f8b267eede2f40a848963d0f3c40b1b1f453a4a870f679e", + "sha256:40f9a74c7aa210b3e76eb1c9d56aa8d08722b73426a77626967019df9bbac287", + "sha256:423f76aa504c84cb94594fb88b8a24027c887f1c488cf58f2173f22f4fbd046c", + "sha256:43bd04cec72281a96eb361e1b0232f0f542b46da50bcfe72ef7e5a1b41d00cb3", + "sha256:43e38762635c09e24885d15e3a8e374b72d105d4178ee2cc9491855a8da9c380", + "sha256:4413b11c2385180d7de03add6c8845dd66692b148d36e27ec8c9ef537b2553a1", + "sha256:4450352a87094fd58daf468b04c65a9fa19ad11a0ac8ac7b7ff17d46f873cbc1", + "sha256:49ffda04a6e44de028b3b786278ac9a70043e7905c3eea29eed88b6524d53a29", + "sha256:4a38c4dde4c9120deef43aaabaa44f19186c98659ce554c29788c4071ab2f0a4", + "sha256:50b1febdfd21e2144b56a9aa226829e93a79c354ef22a4e5b013d9965e1ec0ed", + "sha256:559b1a3a8be7395ded2943ea6c2135d096f8cc7039d6d12127110b6496f251fe", + "sha256:5de86c182667ec68cf84019aa0d8ceccf01d352cdca19bf9e373725204bdbf50", + "sha256:5fc069bb481fe3fad0ba24d3baaf69e22dfa6cc1b63290e6dfeaf4ac1e996fb7", + "sha256:6a19d654da49516296515d6f65de4bbcbd734bc57913b21a610cfc45e6df3ff1", + "sha256:7535b3e52f498270e7877dde1c8944d6b7720e93e2e66b89c82a11447b5818f5", + "sha256:7c4e495bcabc308198b8962e60ca12f53b27eb8f03a21ac1d2d711d6dd9ecfca", + "sha256:8a8fc4a0220367cb8370cedac02272d574079ccc32bffbb34d53aaf9e38b5060", + "sha256:8b008515e067232838daca020d1af628bf6520c8cc338bf383284efe6d8bd083", + "sha256:8d1684258e1385e459418f3429e107eec5fb3d75e1f5a8c52e5946b3f329d6ea", + "sha256:8eb5d54b87fb561dc2e00a5c5226c33ffe8dbc13f2e4033a412bafb7b37b194d", + "sha256:94cdef0c61bd014bb7af495e21a1c3a369dd0399c3cd1965b1502043f5c88d94", + "sha256:9d9f3be69c7a5e84c3549a8c4403fa9ac7672da456863d21e390b2bbf45ccad1", + "sha256:9fb6fb5975a448169756da2d124a1beb38c0924ff6c0306d883b6848a9980f38", + "sha256:a5eaae8700b87144d7dfb475aa4675e500ff707292caba3deff41609ddc5b845", + "sha256:aaeac2d552772b76d24eaff67a5d2325bc5205c74c0d4f9fbe71685d4a971db2", + "sha256:bb611e447559b3b5665e12a7da5160c0de6876097f62bf1d23ba66911564868e", + "sha256:bc0d41f4eb07da8b8d3ea85e50b62f6491ab313834db86ae2345be07536a4e5a", + "sha256:bf51051c129b847d1bb63a9b0826346b5f52fb821b15fe5e0d5ef86f268510f5", + "sha256:c948c034d8997526011960db54f512756fb0b4be1b81140a15b4ef094c6594a4", + "sha256:d435a01334157c3b126b4ee5141401d44bdc8440993b18b05e2f267a6647f92d", + "sha256:d46c1f95672b73288e08cdca181e14e84c6229b5879561b7b8cfd48374e09287", + "sha256:d5d58309b42064228b16b0311ff715d6c6e20230e81b35e8d0c8cfa1bbdecad8", + "sha256:dc6e2e91365a1dd6314d615d80291159c7981928b88a4c65654e3fefac83a836", + "sha256:e0dfb5f7a39029a6cbec23affa923b22a2c02207960fd66f109e01d6f632c1eb", + "sha256:eb4bf58d381b1373bd21d50837a53953d625d1693f1b58fed12743c75d3dd321", + "sha256:ebb211a85248dbc396b29320273c1ffde484b898852432613e8df0164c091006", + "sha256:ec759ece4786ae993a5b7dc3b3dead6e9375d89a6c65dfd6860076d2eb2abe7b", + "sha256:f55108397a8fa164268238c3e69cc134e945d1f693572a2f05a028b8d0d2b837", + "sha256:f6c706866d424ff285b85a02de7bbe5ed0ace227766b2c42cbe12f3d9ea5a8aa", + "sha256:f8370ad332b36fbad117440faf0dd4b910e80b9c49db5648afd337abdde9a1b6" + ], + "version": "==1.25.0" + }, "gym": { "hashes": [ "sha256:3b930cbe1c76bbd30455b5e82ba723dea94159a5f988e927f443324bf7cc7ddd" @@ -782,12 +897,51 @@ "index": "pypi", "version": "==0.15.4" }, + "h5py": { + "hashes": [ + "sha256:063947eaed5f271679ed4ffa36bb96f57bc14f44dd4336a827d9a02702e6ce6b", + "sha256:13c87efa24768a5e24e360a40e0bc4c49bcb7ce1bb13a3a7f9902cec302ccd36", + "sha256:16ead3c57141101e3296ebeed79c9c143c32bdd0e82a61a2fc67e8e6d493e9d1", + "sha256:3dad1730b6470fad853ef56d755d06bb916ee68a3d8272b3bab0c1ddf83bb99e", + "sha256:51ae56894c6c93159086ffa2c94b5b3388c0400548ab26555c143e7cfa05b8e5", + "sha256:54817b696e87eb9e403e42643305f142cd8b940fe9b3b490bbf98c3b8a894cf4", + "sha256:549ad124df27c056b2e255ea1c44d30fb7a17d17676d03096ad5cd85edb32dc1", + "sha256:6998be619c695910cb0effe5eb15d3a511d3d1a5d217d4bd0bebad1151ec2262", + "sha256:6ef7ab1089e3ef53ca099038f3c0a94d03e3560e6aff0e9d6c64c55fb13fc681", + "sha256:769e141512b54dee14ec76ed354fcacfc7d97fea5a7646b709f7400cf1838630", + "sha256:79b23f47c6524d61f899254f5cd5e486e19868f1823298bc0c29d345c2447172", + "sha256:7be5754a159236e95bd196419485343e2b5875e806fe68919e087b6351f40a70", + "sha256:84412798925dc870ffd7107f045d7659e60f5d46d1c70c700375248bf6bf512d", + "sha256:86868dc07b9cc8cb7627372a2e6636cdc7a53b7e2854ad020c9e9d8a4d3fd0f5", + "sha256:8bb1d2de101f39743f91512a9750fb6c351c032e5cd3204b4487383e34da7f75", + "sha256:a5f82cd4938ff8761d9760af3274acf55afc3c91c649c50ab18fcff5510a14a5", + "sha256:aac4b57097ac29089f179bbc2a6e14102dd210618e94d77ee4831c65f82f17c0", + "sha256:bffbc48331b4a801d2f4b7dac8a72609f0b10e6e516e5c480a3e3241e091c878", + "sha256:c0d4b04bbf96c47b6d360cd06939e72def512b20a18a8547fa4af810258355d5", + "sha256:c54a2c0dd4957776ace7f95879d81582298c5daf89e77fb8bee7378f132951de", + "sha256:cbf28ae4b5af0f05aa6e7551cee304f1d317dbed1eb7ac1d827cee2f1ef97a99", + "sha256:d3c59549f90a891691991c17f8e58c8544060fdf3ccdea267100fa5f561ff62f", + "sha256:d7ae7a0576b06cb8e8a1c265a8bc4b73d05fdee6429bffc9a26a6eb531e79d72", + "sha256:ecf4d0b56ee394a0984de15bceeb97cbe1fe485f1ac205121293fc44dcf3f31f", + "sha256:f0e25bb91e7a02efccb50aba6591d3fe2c725479e34769802fcdd4076abfa917", + "sha256:f23951a53d18398ef1344c186fb04b26163ca6ce449ebd23404b153fd111ded9", + "sha256:ff7d241f866b718e4584fa95f520cb19405220c501bd3a53ee11871ba5166ea2" + ], + "version": "==2.10.0" + }, "htmlmin": { "hashes": [ "sha256:50c1ef4630374a5d723900096a961cff426dff46b48f34d194a81bbe14eca178" ], "version": "==0.1.12" }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, "imageio": { "hashes": [ "sha256:c9763e5c187ecf74091c845626b0bdcc6130a20a0de7a86ae0108e2b5335ed3f", @@ -797,11 +951,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", - "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" + "sha256:b044f07694ef14a6683b097ba56bd081dbc7cdc7c7fe46011e499dfecc082f21", + "sha256:e6ac600a142cf2db707b1998382cc7fc3b02befb7273876e01b8ad10b9652742" ], "markers": "python_version < '3.8'", - "version": "==0.23" + "version": "==1.1.0" }, "jinja2": { "hashes": [ @@ -816,6 +970,20 @@ ], "version": "==2.2.2" }, + "keras-applications": { + "hashes": [ + "sha256:5579f9a12bcde9748f4a12233925a59b93b73ae6947409ff34aa2ba258189fe5", + "sha256:df4323692b8c1174af821bf906f1e442e63fa7589bf0f1230a0b6bdc5a810c95" + ], + "version": "==1.0.8" + }, + "keras-preprocessing": { + "hashes": [ + "sha256:44aee5f2c4d80c3b29f208359fcb336df80f293a0bb6b1c738da43ca206656fb", + "sha256:5a8debe01d840de93d49e05ccf1c9b81ae30e210d34dacbcc47aeb3049b528e5" + ], + "version": "==1.1.0" + }, "kiwisolver": { "hashes": [ "sha256:05b5b061e09f60f56244adc885c4a7867da25ca387376b02c1efc29cc16bcd0f", @@ -940,11 +1108,11 @@ }, "mkdocs-material": { "hashes": [ - "sha256:41e8041e1dba6b6fa6624e21d73c1c700f53e60400429a4c7ee10b769b5136b0", - "sha256:e0ccb48f21e671acc2f25e37ff1ea7aba0bcfe799393a7c687a97e07f9cb8528" + "sha256:307d76239ad7aa126ec7b2f6aa751f22bcabafdbe7679451b08ffe0bc005e22c", + "sha256:e11d89d236b2ea62e0a645ff3f55f64d66e9d4c3426771ffaeb24600521166b1" ], "index": "pypi", - "version": "==4.5.0" + "version": "==4.5.1" }, "mkdocs-mermaid-plugin": { "editable": true, @@ -960,30 +1128,30 @@ }, "more-itertools": { "hashes": [ - "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", - "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" + "sha256:53ff73f186307d9c8ef17a9600309154a6ae27f25579e80af4db8f047ba14bc2", + "sha256:a0ea684c39bc4315ba7aae406596ef191fd84f873d2d2751f84d64e81a7a2d45" ], - "version": "==7.2.0" + "version": "==8.0.0" }, "mypy": { "hashes": [ - "sha256:1521c186a3d200c399bd5573c828ea2db1362af7209b2adb1bb8532cea2fb36f", - "sha256:31a046ab040a84a0fc38bc93694876398e62bc9f35eca8ccbf6418b7297f4c00", - "sha256:3b1a411909c84b2ae9b8283b58b48541654b918e8513c20a400bb946aa9111ae", - "sha256:48c8bc99380575deb39f5d3400ebb6a8a1cb5cc669bbba4d3bb30f904e0a0e7d", - "sha256:540c9caa57a22d0d5d3c69047cc9dd0094d49782603eb03069821b41f9e970e9", - "sha256:672e418425d957e276c291930a3921b4a6413204f53fe7c37cad7bc57b9a3391", - "sha256:6ed3b9b3fdc7193ea7aca6f3c20549b377a56f28769783a8f27191903a54170f", - "sha256:9371290aa2cad5ad133e4cdc43892778efd13293406f7340b9ffe99d5ec7c1d9", - "sha256:ace6ac1d0f87d4072f05b5468a084a45b4eda970e4d26704f201e06d47ab2990", - "sha256:b428f883d2b3fe1d052c630642cc6afddd07d5cd7873da948644508be3b9d4a7", - "sha256:d5bf0e6ec8ba346a2cf35cb55bf4adfddbc6b6576fcc9e10863daa523e418dbb", - "sha256:d7574e283f83c08501607586b3167728c58e8442947e027d2d4c7dcd6d82f453", - "sha256:dc889c84241a857c263a2b1cd1121507db7d5b5f5e87e77147097230f374d10b", - "sha256:f4748697b349f373002656bf32fede706a0e713d67bfdcf04edf39b1f61d46eb" + "sha256:02d9bdd3398b636723ecb6c5cfe9773025a9ab7f34612c1cde5c7f2292e2d768", + "sha256:088f758a50af31cf8b42688118077292370c90c89232c783ba7979f39ea16646", + "sha256:28e9fbc96d13397a7ddb7fad7b14f373f91b5cff538e0772e77c270468df083c", + "sha256:30e123b24931f02c5d99307406658ac8f9cd6746f0d45a3dcac2fe5fbdd60939", + "sha256:3294821b5840d51a3cd7a2bb63b40fc3f901f6a3cfb3c6046570749c4c7ef279", + "sha256:41696a7d912ce16fdc7c141d87e8db5144d4be664a0c699a2b417d393994b0c2", + "sha256:4f42675fa278f3913340bb8c3371d191319704437758d7c4a8440346c293ecb2", + "sha256:54d205ccce6ed930a8a2ccf48404896d456e8b87812e491cb907a355b1a9c640", + "sha256:6992133c95a2847d309b4b0c899d7054adc60481df6f6b52bb7dee3d5fd157f7", + "sha256:6ecbd0e8e371333027abca0922b0c2c632a5b4739a0c61ffbd0733391e39144c", + "sha256:83fa87f556e60782c0fc3df1b37b7b4a840314ba1ac27f3e1a1e10cb37c89c17", + "sha256:c87ac7233c629f305602f563db07f5221950fe34fe30af072ac838fa85395f78", + "sha256:de9ec8dba773b78c49e7bec9a35c9b6fc5235682ad1fc2105752ae7c22f4b931", + "sha256:f385a0accf353ca1bca4bbf473b9d83ed18d923fdb809d3a70a385da23e25b6a" ], "index": "pypi", - "version": "==0.740" + "version": "==0.750" }, "mypy-extensions": { "hashes": [ @@ -1026,6 +1194,13 @@ "index": "pypi", "version": "==1.17.4" }, + "oauthlib": { + "hashes": [ + "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", + "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" + ], + "version": "==3.1.0" + }, "opencv-python": { "hashes": [ "sha256:04bec0a6d3a00360a7fb769b755ff4489a4ac8291821b785151f63e6d8bb59ea", @@ -1057,6 +1232,12 @@ ], "version": "==4.1.2.30" }, + "opt-einsum": { + "hashes": [ + "sha256:edfada4b1d0b3b782ace8bc14e80618ff629abf53143e1e6bbf9bd00b11ece77" + ], + "version": "==3.1.0" + }, "packaging": { "hashes": [ "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", @@ -1121,6 +1302,30 @@ ], "version": "==0.13.1" }, + "protobuf": { + "hashes": [ + "sha256:0265379852b9e1f76af6d3d3fe4b3c383a595cc937594bda8565cf69a96baabd", + "sha256:0fcf6c6e004f956df0b261a9ccd1619959dfe90da2d99acb1108a7cad7d3f408", + "sha256:29bd1ed46b2536ad8959401a2f02d2d7b5a309f8e97518e4f92ca6c5ba74dbed", + "sha256:3175d45698edb9a07c1a78a1a4850e674ce8988f20596580158b1d0921d0f057", + "sha256:34a7270940f86da7a28be466ac541c89b6dbf144a6348b9cf7ac6f56b71006ce", + "sha256:38cbc830a4a5ba9956763b0f37090bfd14dd74e72762be6225de2ceac55f4d03", + "sha256:665194f5ad386511ac8d8a0bd57b9ab37b8dd2cd71969458777318e774b9cd46", + "sha256:839bad7d115c77cdff29b488fae6a3ab503ce9a4192bd4c42302a6ea8e5d0f33", + "sha256:934a9869a7f3b0d84eca460e386fba1f7ba2a0c1a120a2648bc41fadf50efd1c", + "sha256:aecdf12ef6dc7fd91713a6da93a86c2f2a8fe54840a3b1670853a2b7402e77c9", + "sha256:b16c9458e34db53c3f6d3903ed3c33d9413e96d975208428edfcb8cbb4c53a8d", + "sha256:c4e90bc27c0691c76e09b5dc506133451e52caee1472b8b3c741b7c912ce43ef", + "sha256:c65d135ea2d85d40309e268106dab02d3bea723db2db21c23ecad4163ced210b", + "sha256:c98dea04a1ff41a70aff2489610f280004831798cb36a068013eed04c698903d", + "sha256:d9049aa194378a426f0b2c784e2054565bf6f754d20fcafdee7102a6250556e8", + "sha256:e028fee51c96de4e81924484c77111dfdea14010ecfc906ea5b252209b0c4de6", + "sha256:e84ad26fb50091b1ea676403c0dd2bd47663099454aa6d88000b1dafecab0941", + "sha256:e88a924b591b06d0191620e9c8aa75297b3111066bb09d49a24bae1054a10c13" + ], + "index": "pypi", + "version": "==3.11.1" + }, "py": { "hashes": [ "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", @@ -1128,6 +1333,20 @@ ], "version": "==1.8.0" }, + "pyasn1": { + "hashes": [ + "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", + "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba" + ], + "version": "==0.4.8" + }, + "pyasn1-modules": { + "hashes": [ + "sha256:0c35a52e00b672f832e5846826f1fb7507907f7d52fba6faa9e3c4cbe874fe4b", + "sha256:b6ada4f840fe51abf5a6bd545b45bf537bea62221fa0dde2e8a553ed9f06a4e3" + ], + "version": "==0.2.7" + }, "pycodestyle": { "hashes": [ "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", @@ -1242,22 +1461,41 @@ }, "pyyaml": { "hashes": [ - "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", - "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", - "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", - "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", - "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", - "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", - "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", - "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", - "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", - "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", - "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", - "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", - "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" + "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", + "sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", + "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", + "sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", + "sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", + "sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", + "sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", + "sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", + "sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", + "sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", + "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4" ], "index": "pypi", - "version": "==5.1.2" + "version": "==5.2" + }, + "requests": { + "hashes": [ + "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", + "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + ], + "version": "==2.22.0" + }, + "requests-oauthlib": { + "hashes": [ + "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", + "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a" + ], + "version": "==1.3.0" + }, + "rsa": { + "hashes": [ + "sha256:14ba45700ff1ec9eeb206a2ce76b32814958a98e372006c8fb76ba820211be66", + "sha256:1a836406405730121ae9823e19c6e806c62bbad73f890574fff50efa4122c487" + ], + "version": "==4.0" }, "scikit-image": { "hashes": [ @@ -1329,6 +1567,42 @@ ], "version": "==1.9.5" }, + "tensorboard": { + "hashes": [ + "sha256:32d9dec38d053d7d75796eb7c2e0d77285af35f69ee1a6796ab5ecc896679fb3", + "sha256:ccae56f01acc78a138474081b631af52017c2075ffe1c453d58c49d5046ef081" + ], + "version": "==2.0.2" + }, + "tensorflow": { + "hashes": [ + "sha256:1351cc812880b083acf243602d05036bb27711ca66ee08c99a2d16da70371906", + "sha256:1d94692b1c2b71a9a3e5f2fbc5aba4a5126661ad4ba192d849b9626cc32648cf", + "sha256:32556d286068a6ec7ab65295be8b482cd22cccbf276af49837aed1d59c06995e", + "sha256:73e9bf5a3f850fd8f630ca17c13b79dfbe1283a08b6f15b9c956697e98fb53f3", + "sha256:8a4b09e67ae2fbd92802a4df9a7d24685950ff33b57b88439b9f1f027a54a513", + "sha256:c5f85f9270d4cdaea7c0dea0b3e6187f6844455bb03a51032145a2cde35fcabb", + "sha256:d2f99b19e161f7044929c004e98a72789267c7971ca667da6a31afc81a887932", + "sha256:e7f1c217852facad332e59025d4f872df0ba8fef0322249af4db98ffe09e9f41", + "sha256:f132935755472b77c1bf6d638f32c3101e5d6c04c5d6725dff8b6e27c5f9e15a", + "sha256:f31357637ae6dcdf135b4fd108f325bd6399d250cc2eefefd65140d5313fdd3a", + "sha256:fca6a935bb96f947c28a4bebba7ca1d8b9686fb4c02a06d24b11a2f66ca20b81" + ], + "index": "pypi", + "version": "==2.0.0" + }, + "tensorflow-estimator": { + "hashes": [ + "sha256:aa8deab25d09a9730dfbae8ec58f4eb00ec2a90b5ca3dcbd8fa0717103d3bbb3" + ], + "version": "==2.0.1" + }, + "termcolor": { + "hashes": [ + "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b" + ], + "version": "==1.1.0" + }, "toml": { "hashes": [ "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", @@ -1397,6 +1671,13 @@ ], "version": "==3.7.4.1" }, + "urllib3": { + "hashes": [ + "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", + "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745" + ], + "version": "==1.25.7" + }, "virtualenv": { "hashes": [ "sha256:116655188441670978117d0ebb6451eb6a7526f9ae0796cc0dee6bd7356909b0", @@ -1418,6 +1699,27 @@ ], "version": "==0.1.7" }, + "werkzeug": { + "hashes": [ + "sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7", + "sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4" + ], + "version": "==0.16.0" + }, + "wheel": { + "hashes": [ + "sha256:10c9da68765315ed98850f8e048347c3eb06dd81822dc2ab1d4fde9dc9702646", + "sha256:f4da1763d3becf2e2cd92a14a7c920f0f00eca30fdde9ea992c836685b9faf28" + ], + "markers": "python_version >= '3'", + "version": "==0.33.6" + }, + "wrapt": { + "hashes": [ + "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" + ], + "version": "==1.11.2" + }, "zipp": { "hashes": [ "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", diff --git a/aea/__version__.py b/aea/__version__.py index 2bb5df68ee..b3c12b4954 100644 --- a/aea/__version__.py +++ b/aea/__version__.py @@ -23,7 +23,7 @@ __title__ = 'aea' __description__ = 'Autonomous Economic Agent framework' __url__ = 'https://github.com/fetchai/agents-aea.git' -__version__ = '0.1.14' +__version__ = '0.1.15' __author__ = 'Fetch.AI Limited' __license__ = 'Apache 2.0' __copyright__ = '2019 Fetch.AI Limited' diff --git a/aea/aea.py b/aea/aea.py index 881ba84d58..d6155dca96 100644 --- a/aea/aea.py +++ b/aea/aea.py @@ -20,6 +20,7 @@ """This module contains the implementation of an Autonomous Economic Agent.""" import logging from asyncio import AbstractEventLoop +from concurrent.futures import Executor from typing import Optional, cast, List from aea.agent import Agent @@ -31,6 +32,7 @@ from aea.mail.base import Envelope from aea.registries.base import Filter, Resources from aea.skills.error.handlers import ErrorHandler +from aea.skills.tasks import TaskManager logger = logging.getLogger(__name__) @@ -46,7 +48,8 @@ def __init__(self, name: str, loop: Optional[AbstractEventLoop] = None, timeout: float = 0.0, debug: bool = False, - max_reactions: int = 20) -> None: + max_reactions: int = 20, + executor: Optional[Executor] = None) -> None: """ Instantiate the agent. @@ -59,12 +62,14 @@ def __init__(self, name: str, :param timeout: the time in (fractions of) seconds to time out an agent between act and react :param debug: if True, run the agent in debug mode. :param max_reactions: the processing rate of messages per iteration. + :param executor: executor for asynchronous execution of tasks. :return: None """ super().__init__(name=name, wallet=wallet, connections=connections, loop=loop, timeout=timeout, debug=debug) self.max_reactions = max_reactions + self._task_manager = TaskManager(executor) self._decision_maker = DecisionMaker(self.name, self.max_reactions, self.outbox, @@ -79,7 +84,8 @@ def __init__(self, name: str, self.decision_maker.message_in_queue, self.decision_maker.ownership_state, self.decision_maker.preferences, - self.decision_maker.goal_pursuit_readiness) + self.decision_maker.goal_pursuit_readiness, + self.task_manager.task_queue) self._resources = resources self._filter = Filter(self.resources, self.decision_maker.message_out_queue) @@ -108,6 +114,11 @@ def filter(self) -> Filter: """Get filter.""" return self._filter + @property + def task_manager(self) -> TaskManager: + """Get the task manager.""" + return self._task_manager + def setup(self) -> None: """ Set up the agent. @@ -116,6 +127,7 @@ def setup(self) -> None: """ self.resources.load(self.context) self.resources.setup() + self.task_manager.start() def act(self) -> None: """ @@ -124,7 +136,7 @@ def act(self) -> None: :return: None """ for behaviour in self.filter.get_active_behaviours(): - behaviour.act() + behaviour.act_wrapper() def react(self) -> None: """ @@ -159,8 +171,10 @@ def _handle(self, envelope: Envelope) -> None: try: msg = protocol.serializer.decode(envelope.message) - except Exception: + msg.counterparty = envelope.sender + except Exception as e: error_handler.send_decoding_error(envelope) + logger.warning("Decoding error. Exception: {}".format(str(e))) return if not protocol.check(msg): # pragma: no cover @@ -174,7 +188,7 @@ def _handle(self, envelope: Envelope) -> None: return for handler in handlers: - handler.handle(msg, envelope.sender) + handler.handle(msg) def update(self) -> None: """ @@ -182,6 +196,7 @@ def update(self) -> None: :return None """ + # TODO: task should be submitted by the behaviours and handlers for task in self.filter.get_active_tasks(): task.execute() self.decision_maker.execute() @@ -193,5 +208,6 @@ def teardown(self) -> None: :return: None """ + self.task_manager.stop() if self._resources is not None: self._resources.teardown() diff --git a/aea/cli/add.py b/aea/cli/add.py index b8ea94531c..423c78010e 100644 --- a/aea/cli/add.py +++ b/aea/cli/add.py @@ -31,9 +31,9 @@ from aea import AEA_DIR from aea.cli.common import Context, pass_ctx, logger, _try_to_load_agent_config +from aea.cli.registry.utils import fetch_package, split_public_id from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE, DEFAULT_CONNECTION_CONFIG_FILE, DEFAULT_SKILL_CONFIG_FILE, \ DEFAULT_PROTOCOL_CONFIG_FILE -from aea.cli.registry.utils import fetch_package, split_public_id @click.group() @@ -46,7 +46,7 @@ def add(ctx: Context, registry): _try_to_load_agent_config(ctx) -def _find_connection_locally(ctx, connection_name): +def _find_connection_locally(ctx, connection_name, click_context): # check that the provided path points to a proper connection directory -> look for connection.yaml file. # first check in aea dir registry_path = ctx.agent_config.registry_path @@ -62,7 +62,8 @@ def _find_connection_locally(ctx, connection_name): # try to load the connection configuration file try: connection_configuration = ctx.connection_loader.load(open(str(connection_configuration_filepath))) - logger.info("Connection '{}' supports the following protocols: {}".format(connection_name, connection_configuration.restricted_to_protocols)) + if connection_configuration.restricted_to_protocols != []: + logger.info("Connection '{}' is restricted to the following protocols: {}".format(connection_name, connection_configuration.restricted_to_protocols)) except ValidationError as e: logger.error("Connection configuration file not valid: {}".format(str(e))) sys.exit(1) @@ -77,6 +78,12 @@ def _find_connection_locally(ctx, connection_name): logger.error(str(e)) sys.exit(1) + # check for protocol dependencies not yet added, and add it. + for protocol_name in connection_configuration.protocols: + if protocol_name not in ctx.agent_config.protocols: + logger.debug("Adding protocol '{}' to the agent...".format(protocol_name)) + click_context.invoke(protocol, protocol_name=protocol_name) + @add.command() @click.argument( @@ -106,7 +113,7 @@ def connection(click_context, connection_name): # fetch from Registry fetch_package('connection', public_id=public_id, cwd=ctx.cwd) else: - _find_connection_locally(ctx, connection_name) + _find_connection_locally(ctx, connection_name, click_context) # make the 'connections' folder a Python package. connections_init_module = os.path.join(ctx.cwd, "connections", "__init__.py") @@ -221,7 +228,7 @@ def _find_skill_locally(ctx, skill_name, click_context): logger.error(str(e)) sys.exit(1) - # check for not supported protocol, and add it. + # check for protocol dependencies not yet added, and add it. for protocol_name in skill_configuration.protocols: if protocol_name not in ctx.agent_config.protocols: logger.debug("Adding protocol '{}' to the agent...".format(protocol_name)) diff --git a/aea/cli/common.py b/aea/cli/common.py index 942dba211c..3143d147c1 100644 --- a/aea/cli/common.py +++ b/aea/cli/common.py @@ -33,13 +33,15 @@ from aea.cli.loggers import default_logging_config from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE, AgentConfig, SkillConfig, ConnectionConfig, ProtocolConfig, \ - DEFAULT_PROTOCOL_CONFIG_FILE, DEFAULT_CONNECTION_CONFIG_FILE, DEFAULT_SKILL_CONFIG_FILE + DEFAULT_PROTOCOL_CONFIG_FILE, DEFAULT_CONNECTION_CONFIG_FILE, DEFAULT_SKILL_CONFIG_FILE, Dependencies from aea.configurations.loader import ConfigLoader logger = logging.getLogger("aea") logger = default_logging_config(logger) -DEFAULT_REGISTRY_PATH = "../packages" +DEFAULT_REGISTRY_PATH = str(Path("..", "packages")) +DEFAULT_CONNECTION = "stub" +DEFAULT_SKILL = "error" class Context(object): @@ -67,31 +69,31 @@ def set_config(self, key, value) -> None: self.config[key] = value logger.debug(' config[%s] = %s' % (key, value)) - def get_dependencies(self) -> List[str]: + def get_dependencies(self) -> Dependencies: """Aggregate the dependencies from every component. :return a list of dependency version specification. e.g. ["gym >= 1.0.0"] """ - dependencies = [] # type: List[str] + dependencies = {} # type: Dependencies for protocol_id in self.agent_config.protocols: path = str(Path("protocols", protocol_id, DEFAULT_PROTOCOL_CONFIG_FILE)) protocol_config = self.protocol_loader.load(open(path)) - deps = cast(List[str], protocol_config.dependencies) - dependencies.extend(deps) + deps = cast(Dependencies, protocol_config.dependencies) + dependencies.update(deps) for connection_id in self.agent_config.connections: path = str(Path("connections", connection_id, DEFAULT_CONNECTION_CONFIG_FILE)) connection_config = self.connection_loader.load(open(path)) - deps = cast(List[str], connection_config.dependencies) - dependencies.extend(deps) + deps = cast(Dependencies, connection_config.dependencies) + dependencies.update(deps) for skill_id in self.agent_config.skills: path = str(Path("skills", skill_id, DEFAULT_SKILL_CONFIG_FILE)) skill_config = self.skill_loader.load(open(path)) - deps = cast(List[str], skill_config.dependencies) - dependencies.extend(deps) + deps = cast(Dependencies, skill_config.dependencies) + dependencies.update(deps) - return sorted(set(dependencies)) + return dependencies pass_ctx = click.make_pass_decorator(Context) @@ -146,11 +148,14 @@ def format_items(items): for item in items: list_str += ( '{line}\n' + 'Public ID: {public_id}\n' 'Name: {name}\n' 'Description: {description}\n' 'Version: {version}\n' '{line}\n'.format( name=item['name'], + # TODO: switch to unsafe get public_id when every obj has it + public_id=item.get('public_id'), description=item['description'], version=item['version'], line='-' * 30 @@ -164,12 +169,15 @@ def format_skills(items): for item in items: list_str += ( '{line}\n' + 'Public ID: {public_id}\n' 'Name: {name}\n' 'Description: {description}\n' 'Protocols: {protocols}\n' 'Version: {version}\n' '{line}\n'.format( name=item['name'], + # TODO: switch to unsafe get public_id when every obj has it + public_id=item.get('public_id'), description=item['description'], version=item['version'], protocols=''.join( @@ -212,5 +220,5 @@ def arg_strip(s): connection_names = set(arg_strip(s) for s in value.split(",") if arg_strip(s) != "") return list(connection_names) - except Exception: + except Exception: # pragma: no cover raise click.BadParameter(value) diff --git a/aea/cli/config.py b/aea/cli/config.py new file mode 100644 index 0000000000..5aa7e42e0c --- /dev/null +++ b/aea/cli/config.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Implementation of the 'aea list' subcommand.""" +import sys +from pathlib import Path +from typing import Dict, List, cast + +import click +import yaml + +from aea.cli.common import Context, pass_ctx, _try_to_load_agent_config, logger +from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE, DEFAULT_SKILL_CONFIG_FILE, DEFAULT_PROTOCOL_CONFIG_FILE, \ + DEFAULT_CONNECTION_CONFIG_FILE +from aea.configurations.loader import ConfigLoader, ConfigurationType + +ALLOWED_PATH_ROOTS = ["agent", "skills", "protocols", "connections"] +RESOURCE_TYPE_TO_CONFIG_FILE = { + "skills": DEFAULT_SKILL_CONFIG_FILE, + "protocols": DEFAULT_PROTOCOL_CONFIG_FILE, + "connections": DEFAULT_CONNECTION_CONFIG_FILE +} # type: Dict[str, str] + + +class AEAJsonPathType(click.ParamType): + """This class implements the JSON-path parameter type for the AEA CLI tool.""" + + name = "json-path" + + def convert(self, value, param, ctx): + """Separate the path between path to resource and json path to attribute. + + Allowed values: + 'agent.an_attribute_name' + 'protocols.my_protocol.an_attribute_name' + 'connections.my_connection.an_attribute_name' + 'skills.my_skill.an_attribute_name' + """ + parts = value.split(".") + + root = parts[0] + if root not in ALLOWED_PATH_ROOTS: + self.fail("The root of the dotted path must be one of: {}".format(ALLOWED_PATH_ROOTS)) + + if len(parts) < 1 or parts[0] == "agent" and len(parts) < 2 or parts[0] != "agent" and len(parts) < 3: + self.fail("The path is too short. Please specify a path up to an attribute name.") + + # if the root is 'agent', stop. + if root == "agent": + config_loader = ConfigLoader.from_configuration_type(ConfigurationType.AGENT) + path_to_resource_configuration = DEFAULT_AEA_CONFIG_FILE + ctx.obj.set_config("configuration_file_path", path_to_resource_configuration) + ctx.obj.set_config("configuration_loader", config_loader) + return parts[1:] + else: + # navigate the resources of the agent to reach the target configuration file. + resource_type = root + resource_name = parts[1] + path_to_resource_directory = (Path(".") / resource_type / resource_name) + if not path_to_resource_directory.exists(): + self.fail("Resource {}/{} does not exist.".format(resource_type, resource_name)) + path_to_resource_configuration = path_to_resource_directory / RESOURCE_TYPE_TO_CONFIG_FILE[resource_type] + config_loader = ConfigLoader.from_configuration_type(ConfigurationType(root[:-1])) + ctx.obj.set_config("configuration_file_path", path_to_resource_configuration) + ctx.obj.set_config("configuration_loader", config_loader) + return parts[2:] + + +def _get_parent_object(obj: dict, dotted_path: List[str]): + """ + Given a nested dictionary, return the object denoted by the dotted path (if any). + + In particular if dotted_path = [], it returns the same object. + + :param obj: the dictionary. + :param dotted_path: the path to the object. + :return: the target dictionary + :raise ValueError: if the path is not valid. + """ + index = 0 + current_object = obj + while index < len(dotted_path): + current_attribute_name = dotted_path[index] + current_object = current_object.get(current_attribute_name, None) + # if the dictionary does not have the key we want, fail. + if current_object is None: + raise ValueError("Cannot get attribute '{}'".format(current_attribute_name)) + index += 1 + # if we are not at the last step and the attribute value is not a dictionary, fail. + if isinstance(current_object, dict): + return current_object + else: + raise ValueError("The target object is not a dictionary.") + + +@click.group() +@pass_ctx +def config(ctx: Context): + """Read or modify a configuration.""" + _try_to_load_agent_config(ctx) + + +@config.command() +@click.argument("JSON_PATH", required=True, type=AEAJsonPathType()) +@pass_ctx +def get(ctx: Context, json_path: List[str]): + """Get a field.""" + config_loader = cast(ConfigLoader, ctx.config.get("configuration_loader")) + configuration_file_path = cast(str, ctx.config.get("configuration_file_path")) + + configuration_object = yaml.safe_load(open(configuration_file_path)) + config_loader.validator.validate(instance=configuration_object) + + parent_object_path = json_path[:-1] + attribute_name = json_path[-1] + try: + parent_object = _get_parent_object(configuration_object, parent_object_path) + except ValueError as e: + logger.error(str(e)) + sys.exit(1) + + if attribute_name not in parent_object: + logger.error("Attribute '{}' not found.".format(attribute_name)) + sys.exit(1) + if not isinstance(parent_object.get(attribute_name), (str, int, bool, float)): + logger.error("Attribute '{}' is not of primitive type.".format(attribute_name)) + sys.exit(1) + + attribute_value = parent_object.get(attribute_name) + print(attribute_value) + + +@config.command() +@click.argument("JSON_PATH", required=True, type=AEAJsonPathType()) +@click.argument("VALUE", required=True, type=str) +@pass_ctx +def set(ctx: Context, json_path: List[str], value): + """Set a field.""" + config_loader = cast(ConfigLoader, ctx.config.get("configuration_loader")) + configuration_file_path = cast(str, ctx.config.get("configuration_file_path")) + + configuration_dict = yaml.safe_load(open(configuration_file_path)) + config_loader.validator.validate(instance=configuration_dict) + + parent_object_path = json_path[:-1] + attribute_name = json_path[-1] + try: + parent_object = _get_parent_object(configuration_dict, parent_object_path) + except ValueError as e: + logger.error(str(e)) + sys.exit(1) + + if attribute_name not in parent_object: + logger.error("Attribute '{}' not found.".format(attribute_name)) + sys.exit(1) + if not isinstance(parent_object.get(attribute_name), (str, int, bool, float)): + logger.error("Attribute '{}' is not of primitive type.".format(attribute_name)) + sys.exit(1) + + parent_object[attribute_name] = value + + try: + configuration_obj = config_loader.configuration_type.from_json(configuration_dict) + config_loader.validator.validate(instance=configuration_obj.json) + config_loader.dump(configuration_obj, open(configuration_file_path, "w")) + except Exception: + logger.error("Attribute or value not valid.") + sys.exit(1) diff --git a/aea/cli/core.py b/aea/cli/core.py index 4e78f3f1c5..99eb77aa4a 100644 --- a/aea/cli/core.py +++ b/aea/cli/core.py @@ -24,33 +24,32 @@ import shutil import sys from pathlib import Path -from typing import cast import click -from click import pass_context -from jsonschema import ValidationError import aea from aea.cli.add import add -from aea.cli.add import connection, skill -from aea.cli.common import Context, pass_ctx, logger, _try_to_load_agent_config, DEFAULT_REGISTRY_PATH +from aea.cli.common import Context, pass_ctx, logger, _try_to_load_agent_config +from aea.cli.config import config +from aea.cli.create import create +from aea.cli.fetch import fetch from aea.cli.install import install from aea.cli.list import list as _list from aea.cli.loggers import simple_verbosity_option +from aea.cli.login import login +from aea.cli.push import push +from aea.cli.publish import publish from aea.cli.remove import remove from aea.cli.run import run from aea.cli.scaffold import scaffold from aea.cli.search import search -from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE, AgentConfig, PrivateKeyPathConfig +from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE, PrivateKeyPathConfig from aea.crypto.default import DefaultCrypto from aea.crypto.ethereum import EthereumCrypto from aea.crypto.fetchai import FetchAICrypto from aea.crypto.helpers import DEFAULT_PRIVATE_KEY_FILE, FETCHAI_PRIVATE_KEY_FILE, ETHEREUM_PRIVATE_KEY_FILE, \ _validate_private_key_path -DEFAULT_CONNECTION = "oef" -DEFAULT_SKILL = "error" - @click.group(name="aea") @click.version_option(aea.__version__, prog_name="aea") @@ -61,50 +60,6 @@ def cli(ctx) -> None: ctx.obj = Context(cwd=".") -@cli.command() -@click.argument('agent_name', type=str, required=True) -@pass_context -def create(click_context, agent_name): - """Create an agent.""" - ctx = cast(Context, click_context.obj) - path = Path(agent_name) - logger.info("Initializing AEA project '{}'".format(agent_name)) - logger.info("Creating project directory '/{}'".format(agent_name)) - - # create the agent's directory - try: - path.mkdir(exist_ok=False) - - # create a config file inside it - logger.info("Creating config file {}".format(DEFAULT_AEA_CONFIG_FILE)) - config_file = open(os.path.join(agent_name, DEFAULT_AEA_CONFIG_FILE), "w") - agent_config = AgentConfig(agent_name=agent_name, aea_version=aea.__version__, authors="", version="v1", license="", url="", registry_path=DEFAULT_REGISTRY_PATH, description="") - agent_config.default_connection = DEFAULT_CONNECTION - ctx.agent_loader.dump(agent_config, config_file) - - # next commands must be done from the agent's directory -> overwrite ctx.cwd - ctx.agent_config = agent_config - ctx.cwd = agent_config.agent_name - - logger.info("Default connections:") - click_context.invoke(connection, connection_name=DEFAULT_CONNECTION) - - logger.info("Default skills:") - click_context.invoke(skill, skill_name=DEFAULT_SKILL) - - except OSError: - logger.error("Directory already exist. Aborting...") - sys.exit(1) - except ValidationError as e: - logger.error(str(e)) - shutil.rmtree(agent_name, ignore_errors=True) - sys.exit(1) - except Exception as e: - logger.exception(e) - shutil.rmtree(agent_name, ignore_errors=True) - sys.exit(1) - - @cli.command() @click.argument('agent_name', type=click.Path(exists=True, file_okay=False, dir_okay=True), required=True) @pass_ctx @@ -140,8 +95,8 @@ def delete(ctx: Context, agent_name): def freeze(ctx: Context): """Get the dependencies.""" _try_to_load_agent_config(ctx) - for d in ctx.get_dependencies(): - print(d) + for dependency_name, dependency_data in sorted(ctx.get_dependencies().items(), key=lambda x: x[0]): + print(dependency_name + dependency_data.get("version", "")) @cli.command() @@ -163,12 +118,23 @@ def gui(ctx: Context, port): @pass_ctx def generate_key(ctx: Context, type_): """Generate private keys.""" + def _can_write(path) -> bool: + if Path(path).exists(): + value = click.confirm('The file {} already exists. Do you want to overwrite it?' + .format(path), default=False) + return value + else: + return True + if type_ == DefaultCrypto.identifier or type_ == "all": - DefaultCrypto().dump(open(DEFAULT_PRIVATE_KEY_FILE, "wb")) + if _can_write(DEFAULT_PRIVATE_KEY_FILE): + DefaultCrypto().dump(open(DEFAULT_PRIVATE_KEY_FILE, "wb")) if type_ == FetchAICrypto.identifier or type_ == "all": - FetchAICrypto().dump(open(FETCHAI_PRIVATE_KEY_FILE, "wb")) + if _can_write(FETCHAI_PRIVATE_KEY_FILE): + FetchAICrypto().dump(open(FETCHAI_PRIVATE_KEY_FILE, "wb")) if type_ == EthereumCrypto.identifier or type_ == "all": - EthereumCrypto().dump(open(ETHEREUM_PRIVATE_KEY_FILE, "wb")) + if _can_write(ETHEREUM_PRIVATE_KEY_FILE): + EthereumCrypto().dump(open(ETHEREUM_PRIVATE_KEY_FILE, "wb")) @cli.command() @@ -191,10 +157,16 @@ def add_key(ctx: Context, type_, file): ctx.agent_loader.dump(ctx.agent_config, open(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w")) +cli.add_command(create) cli.add_command(add) cli.add_command(_list) +cli.add_command(login) cli.add_command(search) +cli.add_command(config) cli.add_command(scaffold) cli.add_command(remove) cli.add_command(install) cli.add_command(run) +cli.add_command(push) +cli.add_command(publish) +cli.add_command(fetch) diff --git a/aea/cli/create.py b/aea/cli/create.py new file mode 100644 index 0000000000..569810d240 --- /dev/null +++ b/aea/cli/create.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Implementation of the 'aea create' subcommand.""" +import os +import shutil +import sys +from pathlib import Path +from typing import cast + +import click +from click import pass_context +from jsonschema import ValidationError + +import aea +from aea.cli.add import connection, skill +from aea.cli.common import Context, logger, DEFAULT_REGISTRY_PATH, DEFAULT_CONNECTION, DEFAULT_SKILL +from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE, AgentConfig + + +def _check_is_parent_folders_are_aea_projects_recursively() -> None: + """Look for 'aea-config.yaml' in parent folders recursively up to the user home directory. + + :return: None + :raise ValueError: if a parent folder has a file named 'aea-config.yaml'. + """ + current = Path(".").resolve() + root = Path("/") + home = current.home() + while current != home and current != root: + files = set(map(lambda x: x.name, current.iterdir())) + if DEFAULT_AEA_CONFIG_FILE in files: + raise Exception("Folder {} has file named {}".format(current, DEFAULT_AEA_CONFIG_FILE)) + current = current.parent.resolve() + return + + +@click.command() +@click.argument('agent_name', type=str, required=True) +@pass_context +def create(click_context, agent_name): + """Create an agent.""" + try: + _check_is_parent_folders_are_aea_projects_recursively() + except Exception: + logger.error("The current folder is already an AEA project. Please move to the parent folder.") + sys.exit(1) + + ctx = cast(Context, click_context.obj) + path = Path(agent_name) + + logger.info("Initializing AEA project '{}'".format(agent_name)) + logger.info("Creating project directory '/{}'".format(agent_name)) + + # create the agent's directory + try: + path.mkdir(exist_ok=False) + + # create a config file inside it + logger.info("Creating config file {}".format(DEFAULT_AEA_CONFIG_FILE)) + config_file = open(os.path.join(agent_name, DEFAULT_AEA_CONFIG_FILE), "w") + agent_config = AgentConfig(agent_name=agent_name, aea_version=aea.__version__, author="", version="v1", license="", fingerprint="", url="", registry_path=DEFAULT_REGISTRY_PATH, description="") + agent_config.default_connection = DEFAULT_CONNECTION + ctx.agent_loader.dump(agent_config, config_file) + + # next commands must be done from the agent's directory -> overwrite ctx.cwd + ctx.agent_config = agent_config + ctx.cwd = agent_config.agent_name + + logger.info("Default connections:") + click_context.invoke(connection, connection_name=DEFAULT_CONNECTION) + + logger.info("Default skills:") + click_context.invoke(skill, skill_name=DEFAULT_SKILL) + + except OSError: + logger.error("Directory already exist. Aborting...") + sys.exit(1) + except ValidationError as e: + logger.error(str(e)) + shutil.rmtree(agent_name, ignore_errors=True) + sys.exit(1) + except Exception as e: + logger.exception(e) + shutil.rmtree(agent_name, ignore_errors=True) + sys.exit(1) diff --git a/aea/cli/fetch.py b/aea/cli/fetch.py new file mode 100644 index 0000000000..5d31842bc2 --- /dev/null +++ b/aea/cli/fetch.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Implementation of the 'aea fetch' subcommand.""" +import click + +from aea.cli.registry.fetch import fetch_agent + + +@click.command(name='fetch') +@click.argument('public-id', type=str, required=True) +def fetch(public_id): + """Fetch Agent from Registry.""" + fetch_agent(public_id) diff --git a/aea/cli/install.py b/aea/cli/install.py index 01b789db44..2cd0f8fc70 100644 --- a/aea/cli/install.py +++ b/aea/cli/install.py @@ -18,7 +18,7 @@ # ------------------------------------------------------------------------------ """Implementation of the 'aea install' subcommand.""" - +import pprint import subprocess import sys from typing import Optional @@ -26,6 +26,40 @@ import click from aea.cli.common import Context, pass_ctx, logger, _try_to_load_agent_config +from aea.configurations.base import Dependency + + +def _install_dependency(dependency_name: str, dependency: Dependency): + logger.info("Installing {}...".format(pprint.pformat(dependency_name))) + try: + index = dependency.get("index", None) + git_url = dependency.get("git", None) + revision = dependency.get("ref", "") + version_constraint = dependency.get("version", "") + command = [sys.executable, "-m", "pip", "install"] + if git_url is not None: + command += ["-i", index] if index is not None else [] + command += ["git+" + git_url + "@" + revision + "#egg=" + dependency_name] + else: + command += ["-i", index] if index is not None else [] + command += [dependency_name + version_constraint] + logger.debug("Calling '{}'".format(" ".join(command))) + subp = subprocess.Popen(command) + subp.wait(30.0) + assert subp.returncode == 0 + except Exception as e: + logger.error("An error occurred while installing {}: {}".format(dependency, str(e))) + sys.exit(1) + + +def _install_from_requirement(file: str): + try: + subp = subprocess.Popen([sys.executable, "-m", "pip", "install", "-r", file]) + subp.wait(30.0) + assert subp.returncode == 0 + except Exception: + logger.error("An error occurred while installing requirement file {}. Stopping...".format(file)) + sys.exit(1) @click.command() @@ -38,17 +72,9 @@ def install(ctx: Context, requirement: Optional[str]): if requirement: logger.debug("Installing the dependencies in '{}'...".format(requirement)) - dependencies = list(map(lambda x: x.strip(), open(requirement).readlines())) + _install_from_requirement(requirement) else: logger.debug("Installing all the dependencies...") dependencies = ctx.get_dependencies() - - for d in dependencies: - logger.info("Installing {}...".format(d)) - try: - subp = subprocess.Popen([sys.executable, "-m", "pip", "install", d]) - subp.wait(30.0) - assert subp.returncode == 0 - except Exception: - logger.error("An error occurred while installing {}. Stopping...".format(d)) - sys.exit(1) + for name, d in dependencies.items(): + _install_dependency(name, d) diff --git a/aea/cli/login.py b/aea/cli/login.py new file mode 100644 index 0000000000..b04e4a3551 --- /dev/null +++ b/aea/cli/login.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Implementation of the 'aea login' subcommand.""" +import click + +from aea.cli.registry.utils import registry_login, write_cli_config +from aea.cli.registry.settings import AUTH_TOKEN_KEY + + +@click.command(name='login', help='Login to Registry account') +@click.argument('username', type=str, required=True) +@click.argument('password', type=str, required=True) +def login(username, password): + """Login to Registry account.""" + click.echo('Signing in as {}...'.format(username)) + token = registry_login(username, password) + write_cli_config({AUTH_TOKEN_KEY: token}) + click.echo('Successfully signed in: {}.'.format(username)) diff --git a/aea/cli/publish.py b/aea/cli/publish.py new file mode 100644 index 0000000000..d6bd67ef72 --- /dev/null +++ b/aea/cli/publish.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Implementation of the 'aea publish' subcommand.""" +import click + +from aea.cli.registry.publish import publish_agent + + +@click.command(name='publish') +def publish(): + """Publish Agent to Registry.""" + publish_agent() diff --git a/aea/cli/push.py b/aea/cli/push.py new file mode 100644 index 0000000000..b2151bc4bb --- /dev/null +++ b/aea/cli/push.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Implementation of the 'aea push' subcommand.""" +import click + +from aea.cli.registry.push import push_item + + +@click.group() +def push(): + """Push item to Registry.""" + # no action needed as just a group of command defined here + pass + + +@push.command(name='connection') +@click.argument('connection-name', type=str, required=True) +def connection(connection_name): + """Push connection to Registry.""" + push_item('connection', connection_name) + + +@push.command(name='protocol') +@click.argument('protocol-name', type=str, required=True) +def protocol(protocol_name): + """Push protocol to Registry.""" + push_item('protocol', protocol_name) + + +@push.command(name='skill') +@click.argument('skill-name', type=str, required=True) +def skill(skill_name): + """Push skill to Registry.""" + push_item('skill', skill_name) diff --git a/aea/cli/registry/fetch.py b/aea/cli/registry/fetch.py new file mode 100644 index 0000000000..85ffcc5ced --- /dev/null +++ b/aea/cli/registry/fetch.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""Methods for CLI fetch functionality.""" + +import click +import os + +from aea.cli.registry.utils import ( + request_api, download_file, extract, split_public_id +) + + +def fetch_agent(public_id: str) -> None: + """ + Fetch Agent from Registry. + + :param public_id: str public ID of desirable Agent. + + :return: None + """ + owner, name, version = split_public_id(public_id) + api_path = '/agents/{}/{}/{}'.format(owner, name, version) + resp = request_api('GET', api_path) + file_url = resp['file'] + + cwd = os.getcwd() + filepath = download_file(file_url, cwd) + target_folder = os.path.join(cwd, name) + extract(filepath, target_folder) + click.echo( + 'Agent {} successfully fetched to {}.' + .format(name, target_folder) + ) diff --git a/aea/cli/registry/publish.py b/aea/cli/registry/publish.py new file mode 100644 index 0000000000..7ad3006a1f --- /dev/null +++ b/aea/cli/registry/publish.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""Methods for CLI publish functionality.""" + +import os +import click +import tarfile + +from aea.cli.common import logger +from aea.cli.registry.utils import clean_tarfiles, load_yaml, request_api +from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE + + +def _compress(output_filename: str, *filepaths): + """Compare the output file.""" + with tarfile.open(output_filename, "w:gz") as f: + for filepath in filepaths: + f.add(filepath, arcname=os.path.basename(filepath)) + + +@clean_tarfiles +def publish_agent(): + """Publish an agent.""" + cwd = os.getcwd() + agent_config_path = os.path.join(cwd, DEFAULT_AEA_CONFIG_FILE) + if not os.path.exists(agent_config_path): + raise click.ClickException( + 'Agent config not found in {}. Make sure you run push command ' + 'from a correct folder.'.format(cwd) + ) + agent_config = load_yaml(agent_config_path) + name = agent_config['agent_name'] + output_tar = os.path.join(cwd, '{}.tar.gz'.format(name)) + _compress(output_tar, agent_config_path) + + data = { + 'name': name, + 'description': agent_config['description'], + 'version': agent_config['version'] + } + path = '/agents/create' + logger.debug('Publishing agent {} to Registry ...'.format(name)) + resp = request_api( + 'POST', path, data=data, auth=True, filepath=output_tar + ) + click.echo( + 'Successfully published agent {} to the Registry. Public ID: {}'.format( + name, resp['public_id'] + ) + ) diff --git a/aea/cli/registry/push.py b/aea/cli/registry/push.py new file mode 100644 index 0000000000..24753ab899 --- /dev/null +++ b/aea/cli/registry/push.py @@ -0,0 +1,95 @@ +# -*- 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. +# +# ------------------------------------------------------------------------------ +"""Methods for CLI push functionality.""" + +import click +import os +import tarfile +import shutil + +from aea.cli.common import logger +from aea.cli.registry.utils import request_api, load_yaml, clean_tarfiles + + +def _remove_pycache(source_dir: str): + pycache_path = os.path.join(source_dir, '__pycache__') + if os.path.exists(pycache_path): + shutil.rmtree(pycache_path) + + +def _compress_dir(output_filename: str, source_dir: str): + _remove_pycache(source_dir) + with tarfile.open(output_filename, "w:gz") as f: + f.add(source_dir, arcname=os.path.basename(source_dir)) + + +@clean_tarfiles +def push_item(item_type: str, item_name: str) -> None: + """ + Push item to the Registry. + + :param item_type: str type of item (connection/protocol/skill). + :param item_name: str item name. + + :return: None + """ + item_type_plural = item_type + 's' + cwd = os.getcwd() + + items_folder = os.path.join(cwd, item_type_plural) + item_path = os.path.join(items_folder, item_name) + logger.debug( + 'Searching for {} {} in {} ...' + .format(item_name, item_type, items_folder) + ) + if not os.path.exists(item_path): + raise click.ClickException( + '{} "{}" not found in {}. Make sure you run push command ' + 'from a correct folder.'.format( + item_type.title(), item_name, items_folder + ) + ) + + output_filename = '{}.tar.gz'.format(item_name) + logger.debug( + 'Compressing {} {} to {} ...' + .format(item_name, item_type, output_filename) + ) + _compress_dir(output_filename, item_path) + output_filepath = os.path.join(cwd, output_filename) + + item_config_filepath = os.path.join(item_path, '{}.yaml'.format(item_type)) + logger.debug('Reading {} {} config ...'.format(item_name, item_type)) + item_config = load_yaml(item_config_filepath) + + data = { + 'name': item_name, + 'description': item_config['description'], + 'version': item_config['version'] + } + path = '/{}/create'.format(item_type_plural) + logger.debug('Pushing {} {} to Registry ...'.format(item_name, item_type)) + resp = request_api( + 'POST', path, data=data, auth=True, filepath=output_filepath + ) + click.echo( + 'Successfully pushed {} {} to the Registry. Public ID: {}'.format( + item_type, item_name, resp['public_id'] + ) + ) diff --git a/aea/cli/registry/settings.py b/aea/cli/registry/settings.py index ed917cfb85..35d6f1818b 100644 --- a/aea/cli/registry/settings.py +++ b/aea/cli/registry/settings.py @@ -17,6 +17,13 @@ # # ------------------------------------------------------------------------------ """Settings for operating Registry with CLI.""" +import os REGISTRY_API_URL = 'http://localhost:8000' +CLI_CONFIG_PATH = os.path.join( + os.path.expanduser('~'), + '.aea', + 'cli_config.yaml' +) +AUTH_TOKEN_KEY = 'auth_token' diff --git a/aea/cli/registry/utils.py b/aea/cli/registry/utils.py index 1df3d5f205..5ea2d16b5b 100644 --- a/aea/cli/registry/utils.py +++ b/aea/cli/registry/utils.py @@ -23,29 +23,76 @@ import os import requests import tarfile +import yaml from typing import List, Dict -from aea.cli.registry.settings import REGISTRY_API_URL +from aea.cli.common import logger +from aea.cli.registry.settings import ( + REGISTRY_API_URL, + CLI_CONFIG_PATH, + AUTH_TOKEN_KEY +) -def request_api(method: str, path: str, params=None) -> Dict: - """Request Registry API.""" +def request_api( + method: str, path: str, params=None, data=None, auth=False, filepath=None +) -> Dict: + """ + Request Registry API. + + :param method: str request method ('GET, 'POST', 'PUT', etc.). + :param path: str URL path. + :param params: dict GET params. + :param data: dict POST data. + :param auth: bool is auth requied (default False). + :param filepath: str path to file to upload (default None). + + :return: dict response from Registry API + """ + headers = {} + if auth: + token = read_cli_config()[AUTH_TOKEN_KEY] + headers.update({ + 'Authorization': 'Token {}'.format(token) + }) + + files = None + if filepath: + files = {'file': open(filepath, 'rb')} + resp = requests.request( method=method, url='{}{}'.format(REGISTRY_API_URL, path), - params=params + params=params, + files=files, + data=data, + headers=headers, ) + resp_json = resp.json() + if resp.status_code == 200: - return resp.json() + pass + elif resp.status_code == 201: + logger.debug('Successfully created!') elif resp.status_code == 403: - raise click.ClickException('You are not authenticated.') + raise click.ClickException( + 'You are not authenticated. ' + 'Please sign in with "aea login" command.' + ) elif resp.status_code == 404: raise click.ClickException('Not found in Registry.') + elif resp.status_code == 409: + raise click.ClickException( + 'Conflict in Registry. {}'.format(resp_json['detail']) + ) + elif resp.status_code == 400: + raise click.ClickException(resp.json()) else: raise click.ClickException( 'Wrong server response. Status code: {}'.format(resp.status_code) ) + return resp_json def split_public_id(public_id: str) -> List[str]: @@ -60,7 +107,7 @@ def split_public_id(public_id: str) -> List[str]: return public_id.split('/') -def _download_file(url: str, cwd: str) -> str: +def download_file(url: str, cwd: str) -> str: """ Download file from URL and save it in CWD (current working directory). @@ -83,7 +130,7 @@ def _download_file(url: str, cwd: str) -> str: return filepath -def _extract(source: str, target: str) -> None: +def extract(source: str, target: str) -> None: """ Extract tarball and remove source file. @@ -128,15 +175,106 @@ def fetch_package(obj_type: str, public_id: str, cwd: str) -> None: public_id=public_id, obj_type=obj_type )) - filepath = _download_file(file_url, cwd) + filepath = download_file(file_url, cwd) target_folder = os.path.join(cwd, plural_obj_type) click.echo('Extracting {obj_type} {public_id}...'.format( public_id=public_id, obj_type=obj_type )) - _extract(filepath, target_folder) + extract(filepath, target_folder) click.echo('Successfully fetched {obj_type}: {public_id}.'.format( public_id=public_id, obj_type=obj_type )) + + +def registry_login(username: str, password: str) -> str: + """ + Login into Registry account. + + :param username: str username. + :param password: str password. + + :return: str token + """ + resp = request_api( + 'POST', '/rest-auth/login/', + data={'username': username, 'password': password} + ) + return resp['key'] + + +def _init_config_folder() -> None: + """ + Create config folder if not exists. + + :return: None + """ + conf_dir = os.path.dirname(CLI_CONFIG_PATH) + if not os.path.exists(conf_dir): + os.makedirs(conf_dir) + + +def write_cli_config(dict_conf: Dict) -> None: + """ + Write CLI config into yaml file. + + :param dict_conf: dict config to write. + + :return: None + """ + _init_config_folder() + with open(CLI_CONFIG_PATH, 'w') as f: + yaml.dump(dict_conf, f, default_flow_style=False) + + +def load_yaml(filepath: str) -> Dict: + """ + Read content from yaml file. + + :param filepath: str path to yaml file. + + :return: dict YAML content + """ + with open(filepath, 'r') as f: + try: + return yaml.safe_load(f) + except yaml.YAMLError as e: + raise click.ClickException( + 'Loading yaml config from {} failed: {}'.format( + filepath, e + ) + ) + + +def read_cli_config() -> Dict: + """ + Read CLI config from yaml file. + + :return: dict CLI config. + """ + return load_yaml(CLI_CONFIG_PATH) + + +def _rm_tarfiles(): + cwd = os.getcwd() + for filename in os.listdir(cwd): + filepath = os.path.join(cwd, filename) + if filepath.endswith('.tar.gz'): + os.remove(filepath) + + +def clean_tarfiles(func): + """Decorate func to clean tarfiles after executing.""" + def wrapper(*args, **kwargs): + try: + result = func(*args, **kwargs) + except Exception as e: + _rm_tarfiles() + raise e + else: + _rm_tarfiles() + return result + + return wrapper diff --git a/aea/cli/run.py b/aea/cli/run.py index f8ce2e5e1d..28f808372a 100644 --- a/aea/cli/run.py +++ b/aea/cli/run.py @@ -134,13 +134,13 @@ def _verify_ledger_apis_access() -> None: _try_to_instantiate_ethereum_ledger_api(ethereum_ledger_config.addr, ethereum_ledger_config.port) -def _setup_connection(connection_name: str, public_key: str, ctx: Context) -> Connection: +def _setup_connection(connection_name: str, address: str, ctx: Context) -> Connection: """ Set up a connection. :param connection_name: the name of the connection. :param ctx: the CLI context object. - :param public_key: the path of the public key. + :param address: the address. :return: a Connection object. :raises AEAConfigException: if the connection name provided as argument is not declared in the configuration file, | or if the connection type is not supported by the framework. @@ -170,7 +170,7 @@ def _setup_connection(connection_name: str, public_key: str, ctx: Context) -> Co if connection_class is None: raise AEAConfigException("Connection class '{}' not found.".format(connection_class_name)) - connection = connection_class.from_config(public_key, connection_config) + connection = connection_class.from_config(address, connection_config) return connection @@ -202,7 +202,7 @@ def run(click_context, connection_names: List[str], env_file: str, install_deps: _try_to_load_protocols(ctx) try: for connection_name in connection_names: - connection = _setup_connection(connection_name, wallet.public_keys[FETCHAI], ctx) + connection = _setup_connection(connection_name, wallet.addresses[FETCHAI], ctx) connections.append(connection) except AEAConfigException as e: logger.error(str(e)) diff --git a/aea/cli/search.py b/aea/cli/search.py index 501e8d6754..194c23e7c0 100644 --- a/aea/cli/search.py +++ b/aea/cli/search.py @@ -18,15 +18,18 @@ # ------------------------------------------------------------------------------ """Implementation of the 'aea search' subcommand.""" +import os from pathlib import Path from typing import cast, List, Dict + import click -import os from aea import AEA_DIR -from aea.cli.common import Context, pass_ctx, DEFAULT_REGISTRY_PATH, logger, retrieve_details, ConfigLoader, format_items, format_skills -from aea.configurations.base import DEFAULT_CONNECTION_CONFIG_FILE, DEFAULT_SKILL_CONFIG_FILE, DEFAULT_PROTOCOL_CONFIG_FILE +from aea.cli.common import Context, pass_ctx, DEFAULT_REGISTRY_PATH, logger, retrieve_details, ConfigLoader, \ + format_items, format_skills from aea.cli.registry.utils import request_api +from aea.configurations.base import DEFAULT_CONNECTION_CONFIG_FILE, DEFAULT_SKILL_CONFIG_FILE, \ + DEFAULT_PROTOCOL_CONFIG_FILE @click.group() @@ -76,7 +79,7 @@ def connections(ctx: Context, query): 'GET', '/connections', params={'search': query} ) if not len(resp): - click.echo('No connections found.') + click.echo('No connections found.') # pragma: no cover else: click.echo('Connections found:\n') click.echo(format_items(resp)) @@ -103,7 +106,7 @@ def protocols(ctx: Context, query): 'GET', '/protocols', params={'search': query} ) if not len(resp): - click.echo('No protocols found.') + click.echo('No protocols found.') # pragma: no cover else: click.echo('Protocols found:\n') click.echo(format_items(resp)) @@ -130,7 +133,7 @@ def skills(ctx: Context, query): 'GET', '/skills', params={'search': query} ) if not len(resp): - click.echo('No skills found.') + click.echo('No skills found.') # pragma: no cover else: click.echo('Skills found:\n') click.echo(format_skills(resp)) diff --git a/aea/cli_gui/__init__.py b/aea/cli_gui/__init__.py index 32de53c428..bc672ee167 100644 --- a/aea/cli_gui/__init__.py +++ b/aea/cli_gui/__init__.py @@ -18,7 +18,7 @@ # ------------------------------------------------------------------------------ """Key pieces of functionality for CLI GUI.""" - +import sys from enum import Enum import glob import io @@ -132,21 +132,23 @@ def _sync_extract_items_from_tty(pid: subprocess.Popen): def get_registered_items(item_type: str): """Create a new AEA project.""" # need to place ourselves one directory down so the searcher can find the packages - pid = _call_aea_async(["aea", "search", item_type + "s"], os.path.join(app_context.module_dir, "aea")) + pid = _call_aea_async(["aea", "search", item_type + "s"], os.path.join(app_context.agents_dir, "aea")) return _sync_extract_items_from_tty(pid) def search_registered_items(item_type: str, search_term: str): """Create a new AEA project.""" # need to place ourselves one directory down so the searcher can find the packages - pid = _call_aea_async(["aea", "search", item_type + "s", "--query", search_term], os.path.join(app_context.module_dir, "aea")) + pid = _call_aea_async(["aea", "search", item_type + "s", "--query", search_term], os.path.join(app_context.agents_dir, "aea")) ret = _sync_extract_items_from_tty(pid) - return ret[0], item_type, search_term, ret[1] + search_result, status = ret + response = {"search_result": search_result, "item_type": item_type, "search_term": search_term} + return response, status def create_agent(agent_id: str): """Create a new AEA project.""" - if _call_aea(["aea", "create", agent_id], app_context.agents_dir) == 0: + if _call_aea([sys.executable, "-m", "aea.cli", "create", agent_id], app_context.agents_dir) == 0: return agent_id, 201 # 201 (Created) else: return {"detail": "Failed to create Agent {} - a folder of this name may exist already".format(agent_id)}, 400 # 400 Bad request @@ -154,7 +156,7 @@ def create_agent(agent_id: str): def delete_agent(agent_id: str): """Delete an existing AEA project.""" - if _call_aea(["aea", "delete", agent_id], app_context.agents_dir) == 0: + if _call_aea([sys.executable, "-m", "aea.cli", "delete", agent_id], app_context.agents_dir) == 0: return 'Agent {} deleted'.format(agent_id), 200 # 200 (OK) else: return {"detail": "Failed to delete Agent {} - it may not exist".format(agent_id)}, 400 # 400 Bad request @@ -172,7 +174,7 @@ def add_item(agent_id: str, item_type: str, item_id: str): def remove_local_item(agent_id: str, item_type: str, item_id: str): """Remove a protocol, skill or connection from a local agent.""" agent_dir = os.path.join(app_context.agents_dir, agent_id) - if _call_aea(["aea", "remove", item_type, item_id], agent_dir) == 0: + if _call_aea([sys.executable, "-m", "aea.cli", "remove", item_type, item_id], agent_dir) == 0: return agent_id, 201 # 200 (OK) else: return {"detail": "Failed to remove {} {} from agent {}".format(item_type, item_id, agent_id)}, 400 # 400 Bad request @@ -184,14 +186,14 @@ def get_local_items(agent_id: str, item_type: str): return [], 200 # 200 (Success) # need to place ourselves one directory down so the searcher can find the packages - pid = _call_aea_async(["aea", "list", item_type + "s"], os.path.join(app_context.agents_dir, agent_id)) + pid = _call_aea_async([sys.executable, "-m", "aea.cli", "list", item_type + "s"], os.path.join(app_context.agents_dir, agent_id)) return _sync_extract_items_from_tty(pid) def scaffold_item(agent_id: str, item_type: str, item_id: str): """Scaffold a moslty empty item on an agent (either protocol, skill or connection).""" agent_dir = os.path.join(app_context.agents_dir, agent_id) - if _call_aea(["aea", "scaffold", item_type, item_id], agent_dir) == 0: + if _call_aea([sys.executable, "-m", "aea.cli", "scaffold", item_type, item_id], agent_dir) == 0: return agent_id, 201 # 200 (OK) else: return {"detail": "Failed to scaffold a new {} in to agent {}".format(item_type, agent_id)}, 400 # 400 Bad request @@ -202,12 +204,13 @@ def _call_aea(param_list: List[str], dir_arg: str) -> int: old_cwd = os.getcwd() os.chdir(dir_arg) ret = subprocess.call(param_list) + subprocess.Popen(param_list, stderr=subprocess.PIPE, stdout=subprocess.PIPE).communicate() os.chdir(old_cwd) return ret def _call_aea_async(param_list: List[str], dir_arg: str) -> subprocess.Popen: - # Should lock here to prevet multiple calls coming in at once and changing the current working directory weirdly + # Should lock here to prevent multiple calls coming in at once and changing the current working directory weirdly with lock: old_cwd = os.getcwd() @@ -224,7 +227,7 @@ def start_oef_node(): _kill_running_oef_nodes() param_list = [ - "python", + sys.executable, "./scripts/oef/launch.py", "--disable_stdin", "--name", @@ -303,11 +306,11 @@ def start_agent(agent_id: str, connection_id: str): if element["id"] == connection_id: has_named_connection = True if has_named_connection: - agent_process = _call_aea_async(["aea", "run", "--connections", connection_id], agent_dir) + agent_process = _call_aea_async([sys.executable, "-m", "aea.cli", "run", "--connections", 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(["aea", "run"], agent_dir) + agent_process = _call_aea_async([sys.executable, "-m", "aea.cli", "run"], agent_dir) if agent_process is None: return {"detail": "Failed to run agent {}".format(agent_id)}, 400 # 400 Bad request @@ -446,12 +449,12 @@ def favicon(): return app -def run(port: int): +def run(port: int, host: str = "127.0.0.1"): """Run the GUI.""" _kill_running_oef_nodes() app = create_app() - app.run(host='127.0.0.1', port=port, debug=False) + app.run(host=host, port=port, debug=False) return app diff --git a/aea/cli_gui/__main__.py b/aea/cli_gui/__main__.py index e4352e4ca3..672f0da4c3 100644 --- a/aea/cli_gui/__main__.py +++ b/aea/cli_gui/__main__.py @@ -30,8 +30,14 @@ type=int, default=8080) +parser.add_argument('-H', + '--host', + help='host that the web server serves from', + type=str, + default="127.0.0.1") + args, unknown = parser.parse_known_args() # If we're running in stand alone mode, run the application if __name__ == '__main__': - aea.cli_gui.run(args.port) # pragma: no cover + aea.cli_gui.run(args.port, args.host) # pragma: no cover diff --git a/aea/cli_gui/aea_cli_rest.yaml b/aea/cli_gui/aea_cli_rest.yaml index 51f68a1269..36511b96e2 100644 --- a/aea/cli_gui/aea_cli_rest.yaml +++ b/aea/cli_gui/aea_cli_rest.yaml @@ -243,9 +243,7 @@ paths: 200: description: Successfully read item list operation schema: - type: array - items: - type: string + type: object /oef: post: diff --git a/aea/cli_gui/templates/home.js b/aea/cli_gui/templates/home.js index a832eb5f1c..46dd140743 100644 --- a/aea/cli_gui/templates/home.js +++ b/aea/cli_gui/templates/home.js @@ -517,8 +517,8 @@ class Controller{ }); this.$event_pump.on('model_searchReadSuccess', function(e, data) { - self.view.setSearchType(data[1]) - self.view.build_table(data[0], 'searchItemsTable'); + self.view.setSearchType(data["item_type"]) + self.view.build_table(data["search_result"], 'searchItemsTable'); self.handleButtonStates() }); diff --git a/aea/configurations/base.py b/aea/configurations/base.py index 540b447ca8..461edc829a 100644 --- a/aea/configurations/base.py +++ b/aea/configurations/base.py @@ -21,6 +21,7 @@ from abc import ABC, abstractmethod from typing import TypeVar, Generic, Optional, List, Tuple, Dict, Set, cast +# from aea.helpers.base import generate_fingerprint DEFAULT_AEA_CONFIG_FILE = "aea-config.yaml" DEFAULT_SKILL_CONFIG_FILE = "skill.yaml" @@ -29,9 +30,27 @@ DEFAULT_PRIVATE_KEY_PATHS = {"default": "", "fetchai": "", "ethereum": ""} T = TypeVar('T') -Address = str ProtocolId = str SkillId = str +""" +A dependency is a dictionary with the following (optional) keys: + - version: a version specifier(s) (e.g. '==0.1.0'). + - index: the PyPI index where to download the package from (default: https://pypi.org) + - git: the URL to the Git repository (e.g. https://github.com/fetchai/agents-aea.git) + - ref: either the branch name, the tag, the commit number or a Git reference (default: 'master'.) +If the 'git' field is set, the 'version' field will be ignored. +They are supposed to be forwarded to the 'pip' command. +""" +Dependency = dict +""" +A dictionary from package name to dependency data structure (see above). +The package name must satisfy the constraints on Python packages names. +For details, see https://www.python.org/dev/peps/pep-0426/#name. + +The main advantage of having a dictionary is that we implicitly filter out dependency duplicates. +We cannot have two items with the same package name since the keys of a YAML object form a set. +""" +Dependencies = Dict[str, Dependency] class JSONSerializable(ABC): @@ -164,24 +183,29 @@ class ConnectionConfig(Configuration): def __init__(self, name: str = "", - authors: str = "", + author: str = "", version: str = "", license: str = "", url: str = "", class_name: str = "", + protocols: List[str] = None, restricted_to_protocols: Optional[Set[str]] = None, - dependencies: Optional[List[str]] = None, + excluded_protocols: Optional[Set[str]] = None, + dependencies: Optional[Dependencies] = None, description: str = "", **config): """Initialize a connection configuration object.""" self.name = name - self.authors = authors + self.author = author self.version = version self.license = license + self.fingerprint = "" self.url = url self.class_name = class_name + self.protocols = protocols self.restricted_to_protocols = restricted_to_protocols if restricted_to_protocols is not None else set() - self.dependencies = dependencies if dependencies is not None else [] + self.excluded_protocols = excluded_protocols if excluded_protocols is not None else set() + self.dependencies = dependencies if dependencies is not None else {} self.description = description self.config = config @@ -190,12 +214,15 @@ def json(self) -> Dict: """Return the JSON representation.""" return { "name": self.name, - "authors": self.authors, + "author": self.author, "version": self.version, "license": self.license, + "fingerprint": self.fingerprint, "url": self.url, "class_name": self.class_name, + "protocols": self.protocols, "restricted_to_protocols": self.restricted_to_protocols, + "excluded_protocols": self.excluded_protocols, "dependencies": self.dependencies, "description": self.description, "config": self.config @@ -206,17 +233,22 @@ def from_json(cls, obj: Dict): """Initialize from a JSON object.""" restricted_to_protocols = obj.get("restricted_to_protocols") restricted_to_protocols = restricted_to_protocols if restricted_to_protocols is not None else set() - dependencies = cast(List[str], obj.get("dependencies", [])) + excluded_protocols = obj.get("excluded_protocols") + excluded_protocols = excluded_protocols if excluded_protocols is not None else set() + dependencies = cast(Dependencies, obj.get("dependencies", {})) + protocols = cast(List[str], obj.get("protocols", [])) return ConnectionConfig( name=cast(str, obj.get("name")), - authors=cast(str, obj.get("authors")), + author=cast(str, obj.get("author")), version=cast(str, obj.get("version")), license=cast(str, obj.get("license")), url=cast(str, obj.get("url")), class_name=cast(str, obj.get("class_name")), + protocols=protocols, restricted_to_protocols=cast(Set[str], restricted_to_protocols), + excluded_protocols=cast(Set[str], excluded_protocols), dependencies=dependencies, - description=cast(str, obj.get("description")), + description=cast(str, obj.get("description", "")), **cast(dict, obj.get("config")) ) @@ -226,19 +258,20 @@ class ProtocolConfig(Configuration): def __init__(self, name: str = "", - authors: str = "", + author: str = "", version: str = "", license: str = "", url: str = "", - dependencies: Optional[List[str]] = None, + dependencies: Optional[Dependencies] = None, description: str = ""): """Initialize a connection configuration object.""" self.name = name - self.authors = authors + self.author = author self.version = version self.license = license + self.fingerprint = "" self.url = url - self.dependencies = dependencies + self.dependencies = dependencies if dependencies is not None else {} self.description = description @property @@ -246,9 +279,10 @@ def json(self) -> Dict: """Return the JSON representation.""" return { "name": self.name, - "authors": self.authors, + "author": self.author, "version": self.version, "license": self.license, + "fingerprint": self.fingerprint, "url": self.url, "dependencies": self.dependencies, "description": self.description @@ -257,15 +291,15 @@ def json(self) -> Dict: @classmethod def from_json(cls, obj: Dict): """Initialize from a JSON object.""" - dependencies = cast(List[str], obj.get("dependencies", [])) + dependencies = cast(Dependencies, obj.get("dependencies", {})) return ProtocolConfig( name=cast(str, obj.get("name")), - authors=cast(str, obj.get("authors")), + author=cast(str, obj.get("author")), version=cast(str, obj.get("version")), license=cast(str, obj.get("license")), url=cast(str, obj.get("url")), dependencies=dependencies, - description=cast(str, obj.get("description")), + description=cast(str, obj.get("description", "")), ) @@ -378,21 +412,22 @@ class SkillConfig(Configuration): def __init__(self, name: str = "", - authors: str = "", + author: str = "", version: str = "", license: str = "", url: str = "", protocols: List[str] = None, - dependencies: Optional[List[str]] = None, + dependencies: Optional[Dependencies] = None, description: str = ""): """Initialize a skill configuration.""" self.name = name - self.authors = authors + self.author = author self.version = version self.license = license + self.fingerprint = "" self.url = url self.protocols = protocols if protocols is not None else [] # type: List[str] - self.dependencies = dependencies + self.dependencies = dependencies if dependencies is not None else {} self.description = description self.handlers = CRUDCollection[HandlerConfig]() self.behaviours = CRUDCollection[BehaviourConfig]() @@ -404,16 +439,17 @@ def json(self) -> Dict: """Return the JSON representation.""" return { "name": self.name, - "authors": self.authors, + "author": self.author, "version": self.version, "license": self.license, + "fingerprint": self.fingerprint, "url": self.url, "protocols": self.protocols, "dependencies": self.dependencies, - "handlers": [{"handler": h.json} for _, h in self.handlers.read_all()], - "behaviours": [{"behaviour": b.json} for _, b in self.behaviours.read_all()], - "tasks": [{"task": t.json} for _, t in self.tasks.read_all()], - "shared_classes": [{"shared_class": s.json} for _, s in self.shared_classes.read_all()], + "handlers": {key: h.json for key, h in self.handlers.read_all()}, + "behaviours": {key: b.json for key, b in self.behaviours.read_all()}, + "tasks": {key: t.json for key, t in self.tasks.read_all()}, + "shared_classes": {key: s.json for key, s in self.shared_classes.read_all()}, "description": self.description } @@ -421,16 +457,16 @@ def json(self) -> Dict: def from_json(cls, obj: Dict): """Initialize from a JSON object.""" name = cast(str, obj.get("name")) - authors = cast(str, obj.get("authors")) + author = cast(str, obj.get("author")) version = cast(str, obj.get("version")) license = cast(str, obj.get("license")) url = cast(str, obj.get("url")) protocols = cast(List[str], obj.get("protocols", [])) - dependencies = cast(List[str], obj.get("dependencies", [])) - description = cast(str, obj.get("description")) + dependencies = cast(Dependencies, obj.get("dependencies", {})) + description = cast(str, obj.get("description", "")) skill_config = SkillConfig( name=name, - authors=authors, + author=author, version=version, license=license, url=url, @@ -439,21 +475,21 @@ def from_json(cls, obj: Dict): description=description ) - for b in obj.get("behaviours", []): # type: ignore - behaviour_config = BehaviourConfig.from_json(b["behaviour"]) - skill_config.behaviours.create(behaviour_config.class_name, behaviour_config) + for behaviour_id, behaviour_data in obj.get("behaviours", {}).items(): # type: ignore + behaviour_config = BehaviourConfig.from_json(behaviour_data) + skill_config.behaviours.create(behaviour_id, behaviour_config) - for t in obj.get("tasks", []): # type: ignore - task_config = TaskConfig.from_json(t["task"]) - skill_config.tasks.create(task_config.class_name, task_config) + for task_id, task_data in obj.get("tasks", {}).items(): # type: ignore + task_config = TaskConfig.from_json(task_data) + skill_config.tasks.create(task_id, task_config) - for h in obj.get("handlers", []): # type: ignore - handler_config = HandlerConfig.from_json(h["handler"]) - skill_config.handlers.create(handler_config.class_name, handler_config) + for handler_id, handler_data in obj.get("handlers", {}).items(): # type: ignore + handler_config = HandlerConfig.from_json(handler_data) + skill_config.handlers.create(handler_id, handler_config) - for s in obj.get("shared_classes", []): # type: ignore - shared_class_config = SharedClassConfig.from_json(s["shared_class"]) - skill_config.shared_classes.create(shared_class_config.class_name, shared_class_config) + for shared_class_id, shared_class_data in obj.get("shared_classes", {}).items(): # type: ignore + shared_class_config = SharedClassConfig.from_json(shared_class_data) + skill_config.shared_classes.create(shared_class_id, shared_class_config) return skill_config @@ -464,9 +500,10 @@ class AgentConfig(Configuration): def __init__(self, agent_name: str = "", aea_version: str = "", - authors: str = "", + author: str = "", version: str = "", license: str = "", + fingerprint: str = "", url: str = "", registry_path: str = "", description: str = "", @@ -476,9 +513,10 @@ def __init__(self, """Instantiate the agent configuration object.""" self.agent_name = agent_name self.aea_version = aea_version - self.authors = authors + self.author = author self.version = version self.license = license + self.fingerprint = fingerprint self.url = url self.registry_path = registry_path self.description = description @@ -525,9 +563,10 @@ def json(self) -> Dict: return { "agent_name": self.agent_name, "aea_version": self.aea_version, - "authors": self.authors, + "author": self.author, "version": self.version, "license": self.license, + "fingerprint": self.fingerprint, "url": self.url, "registry_path": self.registry_path, "description": self.description, @@ -556,12 +595,12 @@ def from_json(cls, obj: Dict): agent_config = AgentConfig( agent_name=cast(str, obj.get("agent_name")), aea_version=cast(str, obj.get("aea_version")), - authors=cast(str, obj.get("authors")), + author=cast(str, obj.get("author")), version=cast(str, obj.get("version")), license=cast(str, obj.get("license")), url=cast(str, obj.get("url")), registry_path=cast(str, obj.get("registry_path")), - description=cast(str, obj.get("description")), + description=cast(str, obj.get("description", "")), logging_config=cast(Dict, obj.get("logging_config", {})), private_key_paths=cast(Dict, private_key_paths), ledger_apis=cast(Dict, ledger_apis) diff --git a/aea/configurations/loader.py b/aea/configurations/loader.py index a9c7b2ff2d..e79313e3b5 100644 --- a/aea/configurations/loader.py +++ b/aea/configurations/loader.py @@ -23,6 +23,7 @@ import json import os import re +from enum import Enum from pathlib import Path from typing import TextIO, Type, TypeVar, Generic @@ -39,11 +40,25 @@ T = TypeVar('T', AgentConfig, SkillConfig, ConnectionConfig, ProtocolConfig) +class ConfigurationType(Enum): + """Configuration types.""" + + AGENT = "agent" + PROTOCOL = "protocol" + CONNECTION = "connection" + SKILL = "skill" + + class ConfigLoader(Generic[T]): """This class implement parsing, serialization and validation functionalities for the 'aea' configuration files.""" def __init__(self, schema_filename: str, configuration_type: Type[T]): - """Initialize the parser for configuration files.""" + """ + Initialize the parser for configuration files. + + :param schema_filename: the path to the JSON-schema file in 'aea/configurations/schemas'. + :param configuration_type: + """ self.schema = json.load(open(os.path.join(_SCHEMAS_DIR, schema_filename))) root_path = "file://{}{}".format(Path(_SCHEMAS_DIR).absolute(), os.path.sep) self.resolver = jsonschema.RefResolver(root_path, self.schema) @@ -73,6 +88,20 @@ def dump(self, configuration: T, fp: TextIO) -> None: self.validator.validate(instance=result) yaml.safe_dump(result, fp) + @classmethod + def from_configuration_type(cls, configuration_type: ConfigurationType) -> 'ConfigLoader': + """Get the configuration loader from the type.""" + if configuration_type == ConfigurationType.AGENT: + return ConfigLoader("aea-config_schema.json", AgentConfig) + elif configuration_type == ConfigurationType.PROTOCOL: + return ConfigLoader("protocol-config_schema.json", ProtocolConfig) + elif configuration_type == ConfigurationType.CONNECTION: + return ConfigLoader("connection-config_schema.json", ConnectionConfig) + elif configuration_type == ConfigurationType.SKILL: + return ConfigLoader("skill-config_schema.json", SkillConfig) + else: + raise ValueError("Invalid configuration type.") + def _config_loader(): envvar_matcher = re.compile(r'\${([^}^{]+)\}') diff --git a/aea/configurations/schemas/aea-config_schema.json b/aea/configurations/schemas/aea-config_schema.json index 2ff0395234..102afb186a 100644 --- a/aea/configurations/schemas/aea-config_schema.json +++ b/aea/configurations/schemas/aea-config_schema.json @@ -6,7 +6,7 @@ "required": [ "aea_version", "agent_name", - "authors", + "author", "version", "license", "url", @@ -23,7 +23,7 @@ "agent_name": { "type": "string" }, - "authors": { + "author": { "type": "string" }, "version": { @@ -31,6 +31,9 @@ }, "license": { "type": "string" + }, + "fingerprint": { + "type": "string" }, "url": { "type": "string" @@ -60,9 +63,11 @@ "uniqueItems": true, "items": { "type": "object", - "required": ["ledger_api"], + "required": [ + "ledger_api" + ], "properties": { - "ledger_api": { + "ledger_api": { "$ref": "#/definitions/ledger_api" } } @@ -136,10 +141,6 @@ } } }, - "requirement": { - "type": "string", - "pattern": "([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])( *(~=|==|>=|<=|!=|<|>) *v?(?:(?:(?P[0-9]+)!)?(?P[0-9]+(?:\\.[0-9]+)*)(?P
[-_\\.]?(?P(a|b|c|rc|alpha|beta|pre|preview))[-_\\.]?(?P[0-9]+)?)?(?P(?:-(?P[0-9]+))|(?:[-_\\.]?(?Ppost|rev|r)[-_\\.]?(?P[0-9]+)?))?(?P[-_\\.]?(?Pdev)[-_\\.]?(?P[0-9]+)?)?)(?:\\+(?P[a-z0-9]+(?:[-_\\.][a-z0-9]+)*))?)?$"
-    },
     "resource_name": {
       "type": "string",
       "pattern": "[a-zA-Z_][a-zA-Z0-9_]*"
diff --git a/aea/configurations/schemas/connection-config_schema.json b/aea/configurations/schemas/connection-config_schema.json
index 10569b7832..d79823dcab 100644
--- a/aea/configurations/schemas/connection-config_schema.json
+++ b/aea/configurations/schemas/connection-config_schema.json
@@ -5,18 +5,19 @@
   "type": "object",
   "required": [
     "name",
-    "authors",
+    "author",
     "version",
     "license",
     "url",
     "class_name",
-    "config"
+    "config",
+    "protocols"
   ],
   "properties": {
     "name": {
       "$ref": "#/definitions/resource_name"
     },
-    "authors": {
+    "author": {
       "type": "string"
     },
     "version": {
@@ -25,37 +26,75 @@
     "license": {
       "type": "string"
     },
+    "fingerprint": {
+      "type": "string"
+    },
     "url": {
       "type": "string"
     },
     "class_name": {
       "type": "string"
     },
-    "restricted_to_protocols": {
+    "protocols": {
       "type": "array",
+      "additionalProperties": false,
       "uniqueItems": true,
       "items": {
         "type": "string"
       }
     },
-    "config": {
-      "type": "object"
+    "restricted_to_protocols": {
+      "type": "array",
+      "uniqueItems": true,
+      "items": {
+        "type": "string"
+      }
     },
-    "dependencies": {
+    "excluded_protocols": {
       "type": "array",
       "uniqueItems": true,
       "items": {
-        "$ref": "#/definitions/requirement"
+        "type": "string"
       }
     },
+    "config": {
+      "type": "object"
+    },
+    "dependencies": {
+      "$ref": "#/definitions/dependencies"
+    },
     "description": {
       "type": "string"
     }
   },
   "definitions": {
-    "requirement": {
-      "type": "string",
-      "pattern": "([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])( *(~=|==|>=|<=|!=|<|>) *v?(?:(?:(?P[0-9]+)!)?(?P[0-9]+(?:\\.[0-9]+)*)(?P
[-_\\.]?(?P(a|b|c|rc|alpha|beta|pre|preview))[-_\\.]?(?P[0-9]+)?)?(?P(?:-(?P[0-9]+))|(?:[-_\\.]?(?Ppost|rev|r)[-_\\.]?(?P[0-9]+)?))?(?P[-_\\.]?(?Pdev)[-_\\.]?(?P[0-9]+)?)?)(?:\\+(?P[a-z0-9]+(?:[-_\\.][a-z0-9]+)*))?)?$"
+    "dependencies": {
+      "type": "object",
+      "additionalProperties": false,
+      "patternProperties": {
+        "^([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9._-]*[A-Za-z0-9])$": {
+          "type": "object",
+          "additionalProperties": false,
+          "properties": {
+            "index": {
+              "type": "string",
+              "pattern": "^http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+$"
+            },
+            "git": {
+              "type": "string",
+              "pattern": "^http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+$"
+            },
+            "ref": {
+              "type": "string",
+              "pattern": "^[A-Za-z0-9/\\.\\-_]+$"
+            },
+            "version": {
+              "type": "string",
+              "pattern": "^(( *(~=|==|>=|<=|!=|<|>) *)([1-9][0-9]*!)?(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\\.post(0|[1-9][0-9]*))?(\\.dev(0|[1-9][0-9]*))?)?$"
+            }
+          }
+        }
+      }
     },
     "resource_name": {
       "type": "string",
diff --git a/aea/configurations/schemas/definitions.json b/aea/configurations/schemas/definitions.json
index ed884b9cca..1fd5503436 100644
--- a/aea/configurations/schemas/definitions.json
+++ b/aea/configurations/schemas/definitions.json
@@ -3,9 +3,33 @@
   "type": "object",
   "additionalProperties": false,
   "definitions": {
-    "requirement": {
-      "type": "string",
-      "pattern": "([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])( *(~=|==|>=|<=|!=|<|>) *v?(?:(?:(?P[0-9]+)!)?(?P[0-9]+(?:\\.[0-9]+)*)(?P
[-_\\.]?(?P(a|b|c|rc|alpha|beta|pre|preview))[-_\\.]?(?P[0-9]+)?)?(?P(?:-(?P[0-9]+))|(?:[-_\\.]?(?Ppost|rev|r)[-_\\.]?(?P[0-9]+)?))?(?P[-_\\.]?(?Pdev)[-_\\.]?(?P[0-9]+)?)?)(?:\\+(?P[a-z0-9]+(?:[-_\\.][a-z0-9]+)*))?)?$"
+    "dependencies": {
+      "type": "object",
+      "additionalProperties": false,
+      "patternProperties": {
+        "^([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9._-]*[A-Za-z0-9])$": {
+          "type": "object",
+          "additionalProperties": false,
+          "properties": {
+            "index": {
+              "type": "string",
+              "pattern": "^http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+$"
+            },
+            "git": {
+              "type": "string",
+              "pattern": "^http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+$"
+            },
+            "ref": {
+              "type": "string",
+              "pattern": "^[A-Za-z0-9/\\.\\-_]+$"
+            },
+            "version": {
+              "type": "string",
+              "pattern": "^(( *(~=|==|>=|<=|!=|<|>) *)([1-9][0-9]*!)?(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\\.post(0|[1-9][0-9]*))?(\\.dev(0|[1-9][0-9]*))?)?$"
+            }
+          }
+        }
+      }
     },
     "resource_name": {
       "type": "string",
diff --git a/aea/configurations/schemas/protocol-config_schema.json b/aea/configurations/schemas/protocol-config_schema.json
index bded7ce8bf..bc39a791df 100644
--- a/aea/configurations/schemas/protocol-config_schema.json
+++ b/aea/configurations/schemas/protocol-config_schema.json
@@ -5,7 +5,7 @@
   "type": "object",
   "required": [
     "name",
-    "authors",
+    "author",
     "version",
     "license",
     "url"
@@ -14,7 +14,7 @@
     "name": {
       "$ref": "#/definitions/resource_name"
     },
-    "authors": {
+    "author": {
       "type": "string"
     },
     "version": {
@@ -23,24 +23,47 @@
     "license": {
       "type": "string"
     },
+    "fingerprint": {
+      "type": "string"
+    },
     "url": {
       "type": "string"
     },
     "dependencies": {
-      "type": "array",
-      "uniqueItems": true,
-      "items": {
-        "$ref": "#/definitions/requirement"
-      }
+      "$ref": "#/definitions/dependencies"
     },
     "description": {
       "type": "string"
     }
   },
   "definitions": {
-    "requirement": {
-      "type": "string",
-      "pattern": "([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])( *(~=|==|>=|<=|!=|<|>) *v?(?:(?:(?P[0-9]+)!)?(?P[0-9]+(?:\\.[0-9]+)*)(?P
[-_\\.]?(?P(a|b|c|rc|alpha|beta|pre|preview))[-_\\.]?(?P[0-9]+)?)?(?P(?:-(?P[0-9]+))|(?:[-_\\.]?(?Ppost|rev|r)[-_\\.]?(?P[0-9]+)?))?(?P[-_\\.]?(?Pdev)[-_\\.]?(?P[0-9]+)?)?)(?:\\+(?P[a-z0-9]+(?:[-_\\.][a-z0-9]+)*))?)?$"
+    "dependencies": {
+      "type": "object",
+      "additionalProperties": false,
+      "patternProperties": {
+        "^([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9._-]*[A-Za-z0-9])$": {
+          "type": "object",
+          "additionalProperties": false,
+          "properties": {
+            "index": {
+              "type": "string",
+              "pattern": "^http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+$"
+            },
+            "git": {
+              "type": "string",
+              "pattern": "^http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+$"
+            },
+            "ref": {
+              "type": "string",
+              "pattern": "^[A-Za-z0-9/\\.\\-_]+$"
+            },
+            "version": {
+              "type": "string",
+              "pattern": "^(( *(~=|==|>=|<=|!=|<|>) *)([1-9][0-9]*!)?(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\\.post(0|[1-9][0-9]*))?(\\.dev(0|[1-9][0-9]*))?)?$"
+            }
+          }
+        }
+      }
     },
     "resource_name": {
       "type": "string",
diff --git a/aea/configurations/schemas/skill-config_schema.json b/aea/configurations/schemas/skill-config_schema.json
index 05a1257441..73f06eda4c 100644
--- a/aea/configurations/schemas/skill-config_schema.json
+++ b/aea/configurations/schemas/skill-config_schema.json
@@ -5,7 +5,7 @@
   "type": "object",
   "required": [
     "name",
-    "authors",
+    "author",
     "version",
     "license",
     "url",
@@ -15,7 +15,7 @@
     "name": {
       "$ref": "#/definitions/resource_name"
     },
-    "authors": {
+    "author": {
       "type": "string"
     },
     "version": {
@@ -24,6 +24,9 @@
     "license": {
       "type": "string"
     },
+    "fingerprint": {
+      "type": "string"
+    },
     "url": {
       "type": "string"
     },
@@ -36,62 +39,39 @@
       }
     },
     "handlers": {
-      "type": "array",
+      "type": "object",
       "additionalProperties": false,
       "uniqueItems": true,
-      "items": {
-        "type": "object",
-        "required": [
-          "handler"
-        ],
-        "properties": {
-          "handler": {
-            "$ref": "#/definitions/handler"
-          }
+      "patternProperties": {
+        "^[^\\d\\W]\\w*\\Z": {
+          "$ref": "#/definitions/handler"
         }
       }
     },
     "behaviours": {
-      "type": "array",
+      "type": "object",
       "uniqueItems": true,
-      "items": {
-        "type": "object",
-        "additionalProperties": false,
-        "required": [
-          "behaviour"
-        ],
-        "properties": {
-          "behaviour": {
-            "$ref": "#/definitions/behaviour"
-          }
+      "patternProperties": {
+        "^[^\\d\\W]\\w*\\Z": {
+          "$ref": "#/definitions/behaviour"
         }
       }
     },
     "tasks": {
-      "type": "array",
+      "type": "object",
       "uniqueItems": true,
-      "items": {
-        "type": "object",
-        "additionalProperties": false,
-        "required": [
-          "task"
-        ],
-        "properties": {
-          "task": {
-            "$ref": "#/definitions/task"
-          }
+      "patternProperties": {
+        "^[^\\d\\W]\\w*\\Z": {
+          "$ref": "#/definitions/task"
         }
       }
     },
     "shared_classes": {
-      "type": "array",
+      "type": "object",
       "uniqueItems": true,
-      "items": {
-        "type": "object",
-        "properties": {
-          "task": {
-            "$ref": "#/definitions/shared_class"
-          }
+      "patternProperties": {
+        "^[^\\d\\W]\\w*\\Z": {
+          "$ref": "#/definitions/shared_class"
         }
       }
     },
@@ -103,11 +83,7 @@
       }
     },
     "dependencies": {
-      "type": "array",
-      "uniqueItems": true,
-      "items": {
-        "$ref": "#/definitions/requirement"
-      }
+      "$ref": "#/definitions/dependencies"
     },
     "description": {
       "type": "string"
@@ -174,9 +150,33 @@
         }
       }
     },
-    "requirement": {
-      "type": "string",
-      "pattern": "([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])( *(~=|==|>=|<=|!=|<|>) *v?(?:(?:(?P[0-9]+)!)?(?P[0-9]+(?:\\.[0-9]+)*)(?P
[-_\\.]?(?P(a|b|c|rc|alpha|beta|pre|preview))[-_\\.]?(?P[0-9]+)?)?(?P(?:-(?P[0-9]+))|(?:[-_\\.]?(?Ppost|rev|r)[-_\\.]?(?P[0-9]+)?))?(?P[-_\\.]?(?Pdev)[-_\\.]?(?P[0-9]+)?)?)(?:\\+(?P[a-z0-9]+(?:[-_\\.][a-z0-9]+)*))?)?$"
+    "dependencies": {
+      "type": "object",
+      "additionalProperties": false,
+      "patternProperties": {
+        "^([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9._-]*[A-Za-z0-9])$": {
+          "type": "object",
+          "additionalProperties": false,
+          "properties": {
+            "index": {
+              "type": "string",
+              "pattern": "^http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+$"
+            },
+            "git": {
+              "type": "string",
+              "pattern": "^http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+$"
+            },
+            "ref": {
+              "type": "string",
+              "pattern": "^[A-Za-z0-9/\\.\\-_]+$"
+            },
+            "version": {
+              "type": "string",
+              "pattern": "^(( *(~=|==|>=|<=|!=|<|>) *)([1-9][0-9]*!)?(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\\.post(0|[1-9][0-9]*))?(\\.dev(0|[1-9][0-9]*))?)?$"
+            }
+          }
+        }
+      }
     },
     "resource_name": {
       "type": "string",
diff --git a/aea/connections/__init__.py b/aea/connections/__init__.py
index 4dbfe938a7..23b5f84725 100644
--- a/aea/connections/__init__.py
+++ b/aea/connections/__init__.py
@@ -19,8 +19,5 @@
 
 """This module contains the channel modules."""
 from typing import List
-import aea.protocols
 
-local_dependencies = [*aea.protocols.oef_dependencies]  # type: List[str]
-oef_dependencies = ["colorlog", "oef", *aea.protocols.oef_dependencies]  # type: List[str]
 stub_dependencies = ["watchdog"]  # type: List[str]
diff --git a/aea/connections/base.py b/aea/connections/base.py
index 239f989e74..672ff97bf7 100644
--- a/aea/connections/base.py
+++ b/aea/connections/base.py
@@ -27,7 +27,7 @@
 from aea.configurations.base import ConnectionConfig
 
 if TYPE_CHECKING:
-    from aea.mail.base import Envelope  # pragma: no cover
+    from aea.mail.base import Envelope, Address  # pragma: no cover
 
 
 logger = logging.getLogger(__name__)
@@ -44,15 +44,18 @@ def __init__(self):
 class Connection(ABC):
     """Abstract definition of a connection."""
 
-    def __init__(self, connection_id: str, restricted_to_protocols: Optional[Set[str]] = None):
+    def __init__(self, connection_id: str, restricted_to_protocols: Optional[Set[str]] = None,
+                 excluded_protocols: Optional[Set[str]] = None):
         """
         Initialize the connection.
 
         :param connection_id: the connection identifier.
         :param restricted_to_protocols: the set of protocols ids of the only supported protocols for this connection.
+        :param excluded_protocols: the set of protocols ids that we want to exclude for this connection.
         """
         self._connection_id = connection_id
         self._restricted_to_protocols = self._get_restricted_to_protocols(restricted_to_protocols)
+        self._excluded_protocols = self._get_excluded_protocols(excluded_protocols)
 
         self._loop = None  # type: Optional[AbstractEventLoop]
         self._connection_status = ConnectionStatus()
@@ -65,6 +68,14 @@ def _get_restricted_to_protocols(self, restricted_to_protocols: Optional[Set[str
         else:
             return set()
 
+    def _get_excluded_protocols(self, excluded_protocols: Optional[Set[str]] = None) -> Set[str]:
+        if excluded_protocols is not None:
+            return excluded_protocols
+        elif hasattr(type(self), "excluded_protocols") and isinstance(getattr(type(self), "excluded_protocols"), set):
+            return getattr(type(self), "excluded_protocols")
+        else:
+            return set()
+
     @property
     def loop(self) -> Optional[AbstractEventLoop]:
         """Get the event loop."""
@@ -91,6 +102,11 @@ def restricted_to_protocols(self) -> Set[str]:
         """Get the restricted to protocols.."""
         return self._restricted_to_protocols
 
+    @property
+    def excluded_protocols(self) -> Set[str]:
+        """Get the restricted to protocols.."""
+        return self._excluded_protocols
+
     @property
     def connection_status(self) -> ConnectionStatus:
         """Get the connection status."""
@@ -123,11 +139,11 @@ async def receive(self, *args, **kwargs) -> Optional['Envelope']:
 
     @classmethod
     @abstractmethod
-    def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection':
+    def from_config(cls, address: 'Address', connection_configuration: ConnectionConfig) -> 'Connection':
         """
         Initialize a connection instance from a configuration.
 
-        :param public_key: the public key of the agent.
+        :param address: the address of the agent.
         :param connection_configuration: the connection configuration.
         :return: an instance of the concrete connection class.
         """
diff --git a/aea/connections/p2p/README.md b/aea/connections/p2p/README.md
deleted file mode 100644
index 7f649b750b..0000000000
--- a/aea/connections/p2p/README.md
+++ /dev/null
@@ -1,36 +0,0 @@
-## Get started
-
-- Follow the instructions here to install the fetch.ai ledger:
-    
-      https://github.com/fetchai/ledger
-  
-  After you have the ledger setup you can run the following command to start the messenger server:
-      
-      ./libs/messenger/examples/example-messenger-messegner-server 
-        
-
-- Install the fetch-p2p-api to the aea framework:
-
-      pip install git+https://github.com/fetchai/peer-to-peer-api.git
-
-
-##Create the agent
-
-- Create an aea agent:
-
-      aea create my_agent
-
-- Add the peer to peer connection:
-     
-      cd my_agent 
-      aea add connection p2p
-
-
-- Run the agent:
-
-      aea run my_agent --connection p2p
-
-
-In order to see that everything is running, you have to create an envelope and send it. 
-
-
diff --git a/aea/connections/p2p/connection.py b/aea/connections/p2p/connection.py
deleted file mode 100644
index fbd80c08ea..0000000000
--- a/aea/connections/p2p/connection.py
+++ /dev/null
@@ -1,193 +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.
-#
-# ------------------------------------------------------------------------------
-
-"""Peer to Peer connection and channel."""
-
-# TODO: reintroduce p2p and add api
-# import logging
-# from asyncio import CancelledError
-# from queue import Queue
-# from typing import Optional, cast, Dict, List, Any, Set
-
-# from fetch.p2p.api.http_calls import HTTPCalls
-
-# from aea.configurations.base import ConnectionConfig
-# from aea.connections.base import Connection
-# from aea.mail.base import Envelope
-
-# logger = logging.getLogger(__name__)
-
-
-# class PeerToPeerChannel:
-#     """A wrapper for an SDK or API."""
-
-#     def __init__(self, public_key: str, provider_addr: str, provider_port: int):
-#         """
-#         Initialize a channel.
-
-#         :param public_key: the public key
-#         """
-#         self.public_key = public_key
-#         self.provider_addr = provider_addr
-#         self.provider_port = provider_port
-#         self.in_queue = Queue()  # type: Queue
-#         self._httpCall = None  # type: HTTPCalls
-#         logger.info("Initialised the peer to peer channel")
-
-#     def connect(self) -> Optional[Queue]:
-#         """
-#         Connect.
-
-#         :return: an asynchronous queue, that constitutes the communication channel.
-#         """
-#         self._httpCall = HTTPCalls(server_address=self.provider_addr, port=self.provider_port)
-#         logger.info("Connected")
-#         self.try_register()
-#         return self.in_queue
-
-#     def try_register(self) -> bool:
-#         """Try to register to the provider."""
-#         try:
-#             logger.info(self.public_key)
-#             query = self._httpCall.register(sender_address=self.public_key, mailbox=True)
-#             return query['status'] == "OK"
-#         except Exception:
-#             logger.warning("Could not register to the provider.")
-#             return False
-
-#     def send(self, envelope: Envelope) -> None:
-#         """
-#         Process the envelopes.
-
-#         :param envelope: the envelope
-#         :return: None
-#         """
-#         self._httpCall.send_message(sender_address=envelope.sender,
-#                                     receiver_address=envelope.to,
-#                                     protocol=envelope.protocol_id,
-#                                     context=b"None",
-#                                     payload=envelope.message)
-
-#     def receive(self) -> None:
-#         """Receive the messages from the provider."""
-#         messages = self._httpCall.get_messages(sender_address=self.public_key)  # type: List[Dict[str, Any]]
-#         for message in messages:
-#             logger.info(message)
-#             envelope = Envelope(to=message['TO']['RECEIVER_ADDRESS'],
-#                                 sender=message['FROM']['SENDER_ADDRESS'],
-#                                 protocol_id=message['PROTOCOL'],
-#                                 message=message['PAYLOAD'])
-#             self.in_queue.put(envelope)
-
-#     def disconnect(self) -> None:
-#         """
-#         Disconnect.
-
-#         :return: None
-#         """
-#         self._httpCall.unregister(self.public_key)
-#         self._httpCall.disconnect()
-
-
-# class PeerToPeerConnection(Connection):
-#     """Proxy to the functionality of the SDK or API."""
-
-#     restricted_to_protocols = set()  # type: Set[str]
-
-#     def __init__(self, public_key: str, provider_addr: str, provider_port: int = 8000, connection_id: str = "p2p",
-#                  restricted_to_protocols: Optional[Set[str]] = None):
-#         """
-#         Initialize a connection to an SDK or API.
-
-#         :param public_key: the public key used in the protocols.
-#         """
-#         super().__init__(connection_id=connection_id, restricted_to_protocols=restricted_to_protocols)
-#         self.channel = PeerToPeerChannel(public_key, provider_addr, provider_port)
-#         self.public_key = public_key
-#         self._connection = None  # type: Optional[Queue]
-
-#     def connect(self) -> None:
-#         """
-#         Connect to the gym.
-
-#         :return: None
-#         """
-#         if not self.connection_status.is_connected:
-#             self.connection_status.is_connected = True
-#             self._connection = self.channel.connect()
-
-#     def disconnect(self) -> None:
-#         """
-#         Disconnect from the gym.
-
-#         :return: None
-#         """
-#         if self.connection_status.is_connected:
-#             self.connection_status.is_connected = False
-#             self.channel.disconnect()
-#             self.stop()
-
-#     async def send(self, envelope: 'Envelope') -> None:
-#         """
-#         Send an envelope.
-
-#         :param envelope: the envelop
-#         :return: None
-#         """
-#         if not self.connection_status.is_connected:
-#             raise ConnectionError("Connection not established yet. Please use 'connect()'.")
-#         self.channel.send(envelope)
-
-#     async def receive(self, *args, **kwargs) -> Optional['Envelope']:
-#         """
-#         Receive an envelope.
-
-#         :return: the envelope received, or None.
-#         """
-#         if not self.connection_status.is_connected:
-#             raise ConnectionError("Connection not established yet. Please use 'connect()'.")
-#         try:
-#             assert self.loop is not None
-#             envelope = await self.loop.run_in_executor(None, self.channel.in_queue.get(block=True))
-#             return envelope
-#         except CancelledError:
-#             return None
-
-#     def stop(self) -> None:
-#         """
-#         Tear down the connection.
-
-#         :return: None
-#         """
-#         self._connection = None
-
-#     @classmethod
-#     def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection':
-#         """
-#         Get the Gym connection from the connection configuration.
-
-#         :param public_key: the public key of the agent.
-#         :param connection_configuration: the connection configuration object.
-#         :return: the connection object
-#         """
-#         addr = cast(str, connection_configuration.config.get("addr"))
-#         port = cast(int, connection_configuration.config.get("port"))
-#         return PeerToPeerConnection(public_key, addr, port,
-#                                     restricted_to_protocols=set(connection_configuration.restricted_to_protocols))
diff --git a/aea/connections/scaffold/connection.py b/aea/connections/scaffold/connection.py
index 726fbf8b26..cc15570ea2 100644
--- a/aea/connections/scaffold/connection.py
+++ b/aea/connections/scaffold/connection.py
@@ -25,7 +25,7 @@
 
 from aea.configurations.base import ConnectionConfig
 from aea.connections.base import Connection
-from aea.mail.base import Envelope
+from aea.mail.base import Envelope, Address
 
 logger = logging.getLogger(__name__)
 
@@ -34,16 +34,17 @@ class MyScaffoldConnection(Connection):
     """Proxy to the functionality of the SDK or API."""
 
     restricted_to_protocols = set()  # type: Set[str]
+    excluded_protocols = set()  # type: Set[str]
 
-    def __init__(self, connection_id: str, public_key: str, *args, **kwargs):
+    def __init__(self, connection_id: str, address: Address, *args, **kwargs):
         """
         Initialize a connection to an SDK or API.
 
         :param connection_id: the identifier of the connection object.
-        :param public_key: the public key used in the protocols.
+        :param address: the address used in the protocols.
         """
         super().__init__(connection_id, *args, **kwargs)
-        self.public_key = public_key
+        self.address = address
 
     async def connect(self) -> None:
         """
@@ -79,11 +80,11 @@ async def receive(self, *args, **kwargs) -> Optional['Envelope']:
         raise NotImplementedError  # pragma: no cover
 
     @classmethod
-    def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection':
+    def from_config(cls, address: Address, connection_configuration: ConnectionConfig) -> 'Connection':
         """
         Get the Gym connection from the connection configuration.
 
-        :param public_key: the public key of the agent.
+        :param address: the address of the agent.
         :param connection_configuration: the connection configuration object.
         :return: the connection object
         """
diff --git a/aea/connections/scaffold/connection.yaml b/aea/connections/scaffold/connection.yaml
index a4b556c74d..467a983cb5 100644
--- a/aea/connections/scaffold/connection.yaml
+++ b/aea/connections/scaffold/connection.yaml
@@ -1,10 +1,13 @@
-name: scaffold_connection
-authors: Fetch.AI Limited
+name: scaffold
+author: fetchai
 version: 0.1.0
 license: Apache 2.0
+fingerprint: ""
 url: ""
 description: "The scaffold connection provides a scaffold for a connection to be implemented by the developer."
 class_name: MyScaffoldConnection
+protocols: []
 restricted_to_protocols: []
+excluded_protocols: []
 config:
   foo: bar
diff --git a/aea/connections/stub/connection.py b/aea/connections/stub/connection.py
index 8f3768cbca..7247e35ab7 100644
--- a/aea/connections/stub/connection.py
+++ b/aea/connections/stub/connection.py
@@ -29,7 +29,7 @@
 
 from aea.configurations.base import ConnectionConfig
 from aea.connections.base import Connection
-from aea.mail.base import Envelope
+from aea.mail.base import Envelope, Address
 
 logger = logging.getLogger(__name__)
 
@@ -138,7 +138,11 @@ def read_envelopes(self) -> None:
         line = self.input_file.readline()
         logger.debug("read line: {!r}".format(line))
         while len(line) > 0:
-            self._process_line(line[:-1])
+            # If the line is the last line of the file, then it doesn't have a \n on the end
+            if line[-1:] == b"\n":
+                self._process_line(line[:-1])
+            else:
+                self._process_line(line)  # pragma: no cover
             line = self.input_file.readline()
 
     def _process_line(self, line) -> None:
@@ -215,11 +219,11 @@ async def send(self, envelope: Envelope):
         self.output_file.flush()
 
     @classmethod
-    def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection':
+    def from_config(cls, address: Address, connection_configuration: ConnectionConfig) -> 'Connection':
         """
         Get the OEF connection from the connection configuration.
 
-        :param public_key: the public key of the agent.
+        :param address: the address of the agent.
         :param connection_configuration: the connection configuration object.
         :return: the connection object
         """
diff --git a/aea/connections/stub/connection.yaml b/aea/connections/stub/connection.yaml
index 03acb772e7..f29f1f5981 100644
--- a/aea/connections/stub/connection.yaml
+++ b/aea/connections/stub/connection.yaml
@@ -1,13 +1,16 @@
 name: stub
-authors: Fetch.AI Limited
+author: fetchai
 version: 0.1.0
 license: Apache 2.0
+fingerprint: ""
 url: ""
 description: "The stub connection implements a connection stub which reads/writes messages from/to file."
 class_name: StubConnection
+protocols: []
 restricted_to_protocols: []
+excluded_protocols: []
 config:
   input_file: "./input_file"
   output_file: "./output_file"
 dependencies:
-  - watchdog
\ No newline at end of file
+  watchdog: {}
diff --git a/aea/context/base.py b/aea/context/base.py
index 6f0768f266..e41b33cb67 100644
--- a/aea/context/base.py
+++ b/aea/context/base.py
@@ -20,7 +20,7 @@
 """This module contains the agent context class."""
 
 from queue import Queue
-from typing import Dict
+from typing import Any, Dict
 
 from aea.connections.base import ConnectionStatus
 from aea.decision_maker.base import OwnershipState, Preferences, GoalPursuitReadiness
@@ -42,7 +42,8 @@ def __init__(self, agent_name: str,
                  decision_maker_message_queue: Queue,
                  ownership_state: OwnershipState,
                  preferences: Preferences,
-                 goal_pursuit_readiness: GoalPursuitReadiness):
+                 goal_pursuit_readiness: GoalPursuitReadiness,
+                 task_queue: Queue):
         """
         Initialize an agent context.
 
@@ -55,7 +56,9 @@ def __init__(self, agent_name: str,
         :param ownership_state: the ownership state of the agent
         :param preferences: the preferences of the agent
         :param goal_pursuit_readiness: ready to pursuit its goals
+        :param task_queue: the queue for the task to be processed enqueued by the agent.
         """
+        self._shared_state = {}  # type: Dict[str, Any]
         self._agent_name = agent_name
         self._public_keys = public_keys
         self._addresses = addresses
@@ -66,6 +69,12 @@ def __init__(self, agent_name: str,
         self._ownership_state = ownership_state
         self._preferences = preferences
         self._goal_pursuit_readiness = goal_pursuit_readiness
+        self._task_queue = task_queue
+
+    @property
+    def shared_state(self) -> Dict[str, Any]:
+        """Get the shared state dictionary."""
+        return self._shared_state
 
     @property
     def agent_name(self) -> str:
@@ -126,3 +135,8 @@ def goal_pursuit_readiness(self) -> GoalPursuitReadiness:
     def ledger_apis(self) -> LedgerApis:
         """Get the ledger APIs."""
         return self._ledger_apis
+
+    @property
+    def task_queue(self) -> Queue:
+        """Get the task queue."""
+        return self._task_queue
diff --git a/aea/crypto/base.py b/aea/crypto/base.py
index 0c64c382aa..790514997b 100644
--- a/aea/crypto/base.py
+++ b/aea/crypto/base.py
@@ -32,9 +32,9 @@ class Crypto(ABC):
     @abstractmethod
     def entity(self) -> Any:
         """
-        Return a public key.
+        Return an entity object.
 
-        :return: a public key string
+        :return: an entity object
         """
 
     @property
diff --git a/aea/crypto/ethereum.py b/aea/crypto/ethereum.py
index bf5ff60e31..cd9afb9044 100644
--- a/aea/crypto/ethereum.py
+++ b/aea/crypto/ethereum.py
@@ -20,7 +20,6 @@
 
 """Ethereum module wrapping the public and private key cryptography and ledger api."""
 
-from eth_account.messages import encode_defunct
 from web3 import Web3
 from eth_account import Account
 from eth_keys import keys
@@ -92,15 +91,14 @@ def _load_private_key_from_path(self, file_name) -> Account:
         except IOError as e:        # pragma: no cover
             logger.exception(str(e))
 
-    def sign_message(self, message: str) -> bytes:
+    def sign_transaction(self, tx_hash: bytes) -> bytes:
         """
-        Sing a transaction to send it to the ledger.
+        Sing a transaction hash.
 
-        :param message:
+        :param tx_hash: the transaction hash
         :return: Signed message in bytes
         """
-        m_message = encode_defunct(text=message)
-        signature = self._account.sign_message(m_message)
+        signature = self.entity.signHash(tx_hash)
         return signature
 
     def _generate_private_key(self) -> Account:
diff --git a/aea/crypto/fetchai.py b/aea/crypto/fetchai.py
index 7e459ba206..c005617792 100644
--- a/aea/crypto/fetchai.py
+++ b/aea/crypto/fetchai.py
@@ -96,14 +96,14 @@ def _generate_private_key(self) -> Entity:
         entity = Entity()
         return entity
 
-    def sign_transaction(self, message: bytes) -> bytes:
+    def sign_transaction(self, tx_hash: bytes) -> bytes:
         """
-        Sing a transaction to send it to the ledger.
+        Sing a transaction hash.
 
-        :param message:
+        :param tx_hash: the transaction hash
         :return: Signed message in bytes
         """
-        signature = self._entity.sign(message)
+        signature = self.entity.sign(tx_hash)
         return signature
 
     @classmethod
diff --git a/aea/crypto/ledger_apis.py b/aea/crypto/ledger_apis.py
index bfc137ddc8..4bfd977b35 100644
--- a/aea/crypto/ledger_apis.py
+++ b/aea/crypto/ledger_apis.py
@@ -131,11 +131,10 @@ def token_balance(self, identifier: str, address: str) -> int:
             raise Exception("Ledger id is not known")
         return balance
 
-    def transfer(self, identifier: str, crypto_object: Crypto, destination_address: str, amount: int, tx_fee: int) -> Optional[str]:
+    def transfer(self, crypto_object: Crypto, destination_address: str, amount: int, tx_fee: int) -> Optional[str]:
         """
         Transfer from self to destination.
 
-        :param identifier: the crypto code
         :param crypto_object: the crypto object that contains the fucntions for signing transactions.
         :param destination_address: the address of the receive
         :param amount: the amount
@@ -143,24 +142,24 @@ def transfer(self, identifier: str, crypto_object: Crypto, destination_address:
 
         :return: tx digest if successful, otherwise None
         """
-        assert identifier in self.apis.keys(), "Unsupported ledger identifier."
-        api = self.apis[identifier]
+        assert crypto_object.identifier in self.apis.keys(), "Unsupported ledger identifier."
+        api = self.apis[crypto_object.identifier]
         logger.info("Waiting for the validation of the transaction ...")
-        if identifier == FETCHAI:
+        if crypto_object.identifier == FETCHAI:
             try:
                 tx_digest = api.tokens.transfer(crypto_object.entity, destination_address, amount, tx_fee)
                 api.sync(tx_digest)
                 logger.info("Transaction validated ...")
-                self._last_tx_statuses[identifier] = OK
+                self._last_tx_statuses[crypto_object.identifier] = OK
             except Exception:
                 logger.warning("An error occurred while attempting the transfer.")
                 tx_digest = None
-                self._last_tx_statuses[identifier] = ERROR
-        elif identifier == ETHEREUM:
+                self._last_tx_statuses[crypto_object.identifier] = ERROR
+        elif crypto_object.identifier == ETHEREUM:
             try:
                 nonce = api.eth.getTransactionCount(api.toChecksumAddress(crypto_object.address))
                 # TODO : handle misconfiguration
-                chain_id = self.configs.get(identifier)[1]  # type: ignore
+                chain_id = self.configs.get(crypto_object.identifier)[1]  # type: ignore
                 transaction = {
                     'nonce': nonce,
                     'chainId': chain_id,
@@ -177,17 +176,17 @@ def transfer(self, identifier: str, crypto_object: Crypto, destination_address:
                         api.eth.getTransactionReceipt(hex_value)
                         logger.info("transaction validated - exiting")
                         tx_digest = hex_value.hex()
-                        self._last_tx_statuses[identifier] = OK
+                        self._last_tx_statuses[crypto_object.identifier] = OK
                         break
                     except web3.exceptions.TransactionNotFound:     # pragma: no cover
                         logger.info("transaction not found - sleeping for 3.0 seconds")
-                        self._last_tx_statuses[identifier] = ERROR
+                        self._last_tx_statuses[crypto_object.identifier] = ERROR
                         time.sleep(3.0)
                 return tx_digest
             except Exception:
                 logger.warning("An error occurred while attempting the transfer.")
                 tx_digest = None
-                self._last_tx_statuses[identifier] = ERROR
+                self._last_tx_statuses[crypto_object.identifier] = ERROR
         else:  # pragma: no cover
             raise Exception("Ledger id is not known")
         return tx_digest
diff --git a/aea/decision_maker/base.py b/aea/decision_maker/base.py
index a5a25e1279..34385e6907 100644
--- a/aea/decision_maker/base.py
+++ b/aea/decision_maker/base.py
@@ -27,8 +27,8 @@
 from typing import Dict, List, Optional, cast
 
 from aea.crypto.wallet import Wallet
-from aea.crypto.ledger_apis import LedgerApis
-from aea.decision_maker.messages.transaction import TransactionMessage
+from aea.crypto.ledger_apis import LedgerApis, SUPPORTED_LEDGER_APIS
+from aea.decision_maker.messages.transaction import TransactionMessage, OFF_CHAIN
 from aea.decision_maker.messages.state_update import StateUpdateMessage
 from aea.helpers.preference_representations.base import logarithmic_utility, linear_utility
 from aea.mail.base import OutBox  # , Envelope
@@ -42,6 +42,7 @@
 SENDER_TX_SHARE = 0.5
 QUANTITY_SHIFT = 100
 INTERNAL_PROTOCOL_ID = 'internal'
+OFF_CHAIN_SETTLEMENT_DIGEST = cast(Optional[str], 'off_chain_settlement')
 
 logger = logging.getLogger(__name__)
 
@@ -74,81 +75,86 @@ class OwnershipState:
 
     def __init__(self):
         """Instantiate an ownership state object."""
-        self._amount_by_currency = None  # type: CurrencyHoldings
-        self._quantities_by_good_pbk = None  # type: GoodHoldings
+        self._amount_by_currency_id = None  # type: CurrencyHoldings
+        self._quantities_by_good_id = None  # type: GoodHoldings
 
-    def init(self, amount_by_currency: CurrencyHoldings, quantities_by_good_pbk: GoodHoldings, agent_name: str = ''):
+    def init(self, amount_by_currency_id: CurrencyHoldings, quantities_by_good_id: GoodHoldings, agent_name: str = ''):
         """
         Instantiate an ownership state object.
 
-        :param amount_by_currency: the currency endowment of the agent in this state.
-        :param quantities_by_good_pbk: the good endowment of the agent in this state.
+        :param amount_by_currency_id: the currency endowment of the agent in this state.
+        :param quantities_by_good_id: the good endowment of the agent in this state.
         :param agent_name: the agent name
         """
         logger.warning("[{}]: Careful! OwnershipState are being initialized!".format(agent_name))
-        self._amount_by_currency = copy.copy(amount_by_currency)
-        self._quantities_by_good_pbk = copy.copy(quantities_by_good_pbk)
+        self._amount_by_currency_id = copy.copy(amount_by_currency_id)
+        self._quantities_by_good_id = copy.copy(quantities_by_good_id)
 
     @property
     def is_initialized(self) -> bool:
         """Get the initialization status."""
-        return self._amount_by_currency is not None and self._quantities_by_good_pbk is not None
+        return self._amount_by_currency_id is not None and self._quantities_by_good_id is not None
 
     @property
-    def amount_by_currency(self) -> CurrencyHoldings:
+    def amount_by_currency_id(self) -> CurrencyHoldings:
         """Get currency holdings in this state."""
-        assert self._amount_by_currency is not None, "CurrencyHoldings not set!"
-        return copy.copy(self._amount_by_currency)
+        assert self._amount_by_currency_id is not None, "CurrencyHoldings not set!"
+        return copy.copy(self._amount_by_currency_id)
 
     @property
-    def quantities_by_good_pbk(self) -> GoodHoldings:
+    def quantities_by_good_id(self) -> GoodHoldings:
         """Get good holdings in this state."""
-        assert self._quantities_by_good_pbk is not None, "GoodHoldings not set!"
-        return copy.copy(self._quantities_by_good_pbk)
+        assert self._quantities_by_good_id is not None, "GoodHoldings not set!"
+        return copy.copy(self._quantities_by_good_id)
 
     def check_transaction_is_consistent(self, tx_message: TransactionMessage) -> bool:
         """
         Check if the transaction is consistent.
 
-        E.g. check that the agent state has enough money if it is a buyer
-        or enough holdings if it is a seller.
+        E.g. check that the agent state has enough money if it is a buyer or enough holdings if it is a seller.
+        Note, the agent is the sender of the transaction message by design.
         :return: True if the transaction is legal wrt the current state, false otherwise.
         """
-        currency_pbk = cast(str, tx_message.get("currency_pbk"))
-        if tx_message.get("is_sender_buyer"):
-            # check if we have the money to cover amount and tx fee.
-            result = self.amount_by_currency[currency_pbk] >= cast(int, tx_message.get("amount")) + cast(int, tx_message.get("sender_tx_fee"))
-        else:
-            # check if we have the goods.
-            result = True
-            quantities_by_good_pbk = cast(Dict[str, int], tx_message.get("quantities_by_good_pbk"))
-            for good_pbk, quantity in quantities_by_good_pbk.items():
-                result = result and (self.quantities_by_good_pbk[good_pbk] >= quantity)
-            # check if we have the money to cover tx fee.
-            result = self.amount_by_currency[currency_pbk] + cast(int, tx_message.get("amount")) >= cast(int, tx_message.get("sender_tx_fee"))
+        result = len(tx_message.tx_amount_by_currency_id) == 1
+        for currency_id, amount in tx_message.tx_amount_by_currency_id.items():
+            if amount == 0 and all(quantity == 0 for quantity in tx_message.tx_quantities_by_good_id.values()):
+                # reject the transaction when there is no wealth exchange
+                result = False
+            elif amount <= 0 and all(quantity >= 0 for quantity in tx_message.tx_quantities_by_good_id.values()):
+                # check if the agent has the money to cover amount and tx fee (the agent is the buyer).
+                transfer_amount = -amount - tx_message.tx_counterparty_fee
+                result = result and (transfer_amount >= 0)
+                max_tx_fee = tx_message.tx_sender_fee + tx_message.tx_counterparty_fee
+                max_payable = transfer_amount + max_tx_fee
+                result = result and (self.amount_by_currency_id[currency_id] >= max_payable)
+            elif amount >= 0 and all(quantity <= 0 for quantity in tx_message.tx_quantities_by_good_id.values()):
+                # check if the agent has the goods (the agent is the seller).
+                result = result and all(self.quantities_by_good_id[good_id] >= -quantity for good_id, quantity in tx_message.tx_quantities_by_good_id.items())
+            else:
+                result = False
         return result
 
-    def apply_state_update(self, amount_deltas_by_currency: Dict[str, int], quantity_deltas_by_good_pbk: Dict[str, int]) -> 'OwnershipState':
+    def apply_state_update(self, amount_by_currency_id: Dict[str, int], quantities_by_good_id: Dict[str, int]) -> 'OwnershipState':
         """
-        Apply a list of transactions to the current state.
+        Apply a state update to the current state.
 
-        :param amount_deltas_by_currency: the delta in the currency amounts
-        :param quantity_deltas_by_good_pbk: the delta in the quantities by good
+        :param amount_by_currency_id: the delta in the currency amounts
+        :param quantities_by_good_id: the delta in the quantities by good
         :return: the final state.
         """
         new_state = copy.copy(self)
 
-        for currency, amount_delta in amount_deltas_by_currency.items():
-            new_state._amount_by_currency[currency] += amount_delta
+        for currency, amount_delta in amount_by_currency_id.items():
+            new_state._amount_by_currency_id[currency] += amount_delta
 
-        for good_pbk, quantity_delta in quantity_deltas_by_good_pbk.items():
-            new_state._quantities_by_good_pbk[good_pbk] += quantity_delta
+        for good_id, quantity_delta in quantities_by_good_id.items():
+            new_state._quantities_by_good_id[good_id] += quantity_delta
 
         return new_state
 
-    def apply(self, transactions: List[TransactionMessage]) -> 'OwnershipState':
+    def apply_transactions(self, transactions: List[TransactionMessage]) -> 'OwnershipState':
         """
-        Apply a list of transactions to the current state.
+        Apply a list of transactions to (a copy of) the current state.
 
         :param transactions: the sequence of transaction messages.
         :return: the final state.
@@ -166,25 +172,21 @@ def update(self, tx_message: TransactionMessage) -> None:
         :param tx_message:
         :return: None
         """
-        currency_pbk = cast(str, tx_message.get("currency_pbk"))
-        if tx_message.get("is_sender_buyer"):
-            diff = cast(int, tx_message.get("amount")) + cast(int, tx_message.get("sender_tx_fee"))
-            self._amount_by_currency[currency_pbk] -= diff
-        else:
-            diff = cast(int, tx_message.get("amount")) - cast(int, tx_message.get("sender_tx_fee"))
-            self._amount_by_currency[currency_pbk] += diff
+        assert self.check_transaction_is_consistent(tx_message), "Inconsistent transaction."
+        for currency_id, amount in tx_message.tx_amount_by_currency_id.items():
+            # we apply the worst case where all the tx_sender_fee is used up in the transaction
+            diff = amount - tx_message.tx_sender_fee
+            self._amount_by_currency_id[currency_id] += diff
 
-        quantities_by_good_pbk = cast(Dict[str, int], tx_message.get("quantities_by_good_pbk"))
-        for good_pbk, quantity in quantities_by_good_pbk.items():
-            quantity_delta = quantity if tx_message.get("is_sender_buyer") else -quantity
-            self._quantities_by_good_pbk[good_pbk] += quantity_delta
+            for good_id, quantity_delta in tx_message.tx_quantities_by_good_id.items():
+                self._quantities_by_good_id[good_id] += quantity_delta
 
     def __copy__(self):
         """Copy the object."""
         state = OwnershipState()
-        if self.amount_by_currency is not None and self.quantities_by_good_pbk is not None:
-            state._amount_by_currency = self.amount_by_currency
-            state._quantities_by_good_pbk = self.quantities_by_good_pbk
+        if self.amount_by_currency_id is not None and self.quantities_by_good_id is not None:
+            state._amount_by_currency_id = self.amount_by_currency_id
+            state._quantities_by_good_id = self.quantities_by_good_id
         return state
 
 
@@ -193,42 +195,42 @@ class Preferences:
 
     def __init__(self):
         """Instantiate an agent preference object."""
-        self._exchange_params_by_currency = None  # type: ExchangeParams
-        self._utility_params_by_good_pbk = None  # type: UtilityParams
+        self._exchange_params_by_currency_id = None  # type: ExchangeParams
+        self._utility_params_by_good_id = None  # type: UtilityParams
         self._transaction_fees = None  # type: Dict[str, int]
         self._quantity_shift = QUANTITY_SHIFT
 
-    def init(self, exchange_params_by_currency: ExchangeParams, utility_params_by_good_pbk: UtilityParams, tx_fee: int, agent_name: str = ''):
+    def init(self, exchange_params_by_currency_id: ExchangeParams, utility_params_by_good_id: UtilityParams, tx_fee: int, agent_name: str = ''):
         """
         Instantiate an agent preference object.
 
-        :param exchange_params_by_currency: the exchange params.
-        :param utility_params_by_good_pbk: the utility params for every asset.
+        :param exchange_params_by_currency_id: the exchange params.
+        :param utility_params_by_good_id: the utility params for every asset.
         :param agent_name: the agent name
         """
         logger.warning("[{}]: Careful! Preferences are being initialized!".format(agent_name))
-        self._exchange_params_by_currency = exchange_params_by_currency
-        self._utility_params_by_good_pbk = utility_params_by_good_pbk
+        self._exchange_params_by_currency_id = exchange_params_by_currency_id
+        self._utility_params_by_good_id = utility_params_by_good_id
         self._transaction_fees = self._split_tx_fees(tx_fee)
 
     @property
     def is_initialized(self) -> bool:
         """Get the initialization status."""
-        return (self._exchange_params_by_currency is not None) and \
-            (self._utility_params_by_good_pbk is not None) and \
+        return (self._exchange_params_by_currency_id is not None) and \
+            (self._utility_params_by_good_id is not None) and \
             (self._transaction_fees is not None)
 
     @property
-    def exchange_params_by_currency(self) -> ExchangeParams:
+    def exchange_params_by_currency_id(self) -> ExchangeParams:
         """Get exchange parameter for each currency."""
-        assert self._exchange_params_by_currency is not None, "ExchangeParams not set!"
-        return self._exchange_params_by_currency
+        assert self._exchange_params_by_currency_id is not None, "ExchangeParams not set!"
+        return self._exchange_params_by_currency_id
 
     @property
-    def utility_params_by_good_pbk(self) -> UtilityParams:
+    def utility_params_by_good_id(self) -> UtilityParams:
         """Get utility parameter for each good."""
-        assert self._utility_params_by_good_pbk is not None, "UtilityParams not set!"
-        return self._utility_params_by_good_pbk
+        assert self._utility_params_by_good_id is not None, "UtilityParams not set!"
+        return self._utility_params_by_good_id
 
     @property
     def transaction_fees(self) -> Dict[str, int]:
@@ -236,58 +238,58 @@ def transaction_fees(self) -> Dict[str, int]:
         assert self._transaction_fees is not None, "Transaction fee not set!"
         return self._transaction_fees
 
-    def logarithmic_utility(self, quantities_by_good_pbk: GoodHoldings) -> float:
+    def logarithmic_utility(self, quantities_by_good_id: GoodHoldings) -> float:
         """
         Compute agent's utility given her utility function params and a good bundle.
 
-        :param quantities_by_good_pbk: the good holdings (dictionary) with the identifier (key) and quantity (value) for each good
+        :param quantities_by_good_id: the good holdings (dictionary) with the identifier (key) and quantity (value) for each good
         :return: utility value
         """
-        result = logarithmic_utility(self.utility_params_by_good_pbk, quantities_by_good_pbk, self._quantity_shift)
+        result = logarithmic_utility(self.utility_params_by_good_id, quantities_by_good_id, self._quantity_shift)
         return result
 
-    def linear_utility(self, amount_by_currency: CurrencyHoldings) -> float:
+    def linear_utility(self, amount_by_currency_id: CurrencyHoldings) -> float:
         """
         Compute agent's utility given her utility function params and a currency bundle.
 
-        :param amount_by_currency: the currency holdings (dictionary) with the identifier (key) and quantity (value) for each currency
+        :param amount_by_currency_id: the currency holdings (dictionary) with the identifier (key) and quantity (value) for each currency
         :return: utility value
         """
-        result = linear_utility(self.exchange_params_by_currency, amount_by_currency)
+        result = linear_utility(self.exchange_params_by_currency_id, amount_by_currency_id)
         return result
 
-    def get_score(self, quantities_by_good_pbk: GoodHoldings, amount_by_currency: CurrencyHoldings) -> float:
+    def get_score(self, quantities_by_good_id: GoodHoldings, amount_by_currency_id: CurrencyHoldings) -> float:
         """
         Compute the score given the good and currency holdings.
 
-        :param quantities_by_good_pbk: the good holdings
-        :param amount_by_currency: the currency holdings
+        :param quantities_by_good_id: the good holdings
+        :param amount_by_currency_id: the currency holdings
         :return: the score.
         """
-        goods_score = self.logarithmic_utility(quantities_by_good_pbk)
-        currency_score = self.linear_utility(amount_by_currency)
+        goods_score = self.logarithmic_utility(quantities_by_good_id)
+        currency_score = self.linear_utility(amount_by_currency_id)
         score = goods_score + currency_score
         return score
 
-    def marginal_utility(self, ownership_state: OwnershipState, delta_good_holdings: Optional[GoodHoldings] = None, delta_currency_holdings: Optional[CurrencyHoldings] = None) -> float:
+    def marginal_utility(self, ownership_state: OwnershipState, delta_quantities_by_good_id: Optional[GoodHoldings] = None, delta_amount_by_currency_id: Optional[CurrencyHoldings] = None) -> float:
         """
         Compute the marginal utility.
 
         :param ownership_state: the current ownership state
-        :param delta_good_holdings: the change in good holdings
-        :param delta_currency_holdings: the change in money holdings
+        :param delta_quantities_by_good_id: the change in good holdings
+        :param delta_amount_by_currency_id: the change in money holdings
         :return: the marginal utility score
         """
-        current_goods_score = self.logarithmic_utility(ownership_state.quantities_by_good_pbk)
-        current_currency_score = self.linear_utility(ownership_state.amount_by_currency)
+        current_goods_score = self.logarithmic_utility(ownership_state.quantities_by_good_id)
+        current_currency_score = self.linear_utility(ownership_state.amount_by_currency_id)
         new_goods_score = current_goods_score
         new_currency_score = current_currency_score
-        if delta_good_holdings is not None:
-            new_quantities_by_good_pbk = {good_pbk: quantity + delta_good_holdings[good_pbk] for good_pbk, quantity in ownership_state.quantities_by_good_pbk.items()}
-            new_goods_score = self.logarithmic_utility(new_quantities_by_good_pbk)
-        if delta_currency_holdings is not None:
-            new_amount_by_currency = {currency: amount + delta_currency_holdings[currency] for currency, amount in ownership_state.amount_by_currency.items()}
-            new_currency_score = self.linear_utility(new_amount_by_currency)
+        if delta_quantities_by_good_id is not None:
+            new_quantities_by_good_id = {good_id: quantity + delta_quantities_by_good_id[good_id] for good_id, quantity in ownership_state.quantities_by_good_id.items()}
+            new_goods_score = self.logarithmic_utility(new_quantities_by_good_id)
+        if delta_amount_by_currency_id is not None:
+            new_amount_by_currency_id = {currency: amount + delta_amount_by_currency_id[currency] for currency, amount in ownership_state.amount_by_currency_id.items()}
+            new_currency_score = self.linear_utility(new_amount_by_currency_id)
         return new_goods_score + new_currency_score - current_goods_score - current_currency_score
 
     def get_score_diff_from_transaction(self, ownership_state: OwnershipState, tx_message: TransactionMessage) -> float:
@@ -297,11 +299,11 @@ def get_score_diff_from_transaction(self, ownership_state: OwnershipState, tx_me
         :param tx_message: a transaction object.
         :return: the score.
         """
-        current_score = self.get_score(quantities_by_good_pbk=ownership_state.quantities_by_good_pbk,
-                                       amount_by_currency=ownership_state.amount_by_currency)
-        new_ownership_state = ownership_state.apply([tx_message])
-        new_score = self.get_score(quantities_by_good_pbk=new_ownership_state.quantities_by_good_pbk,
-                                   amount_by_currency=new_ownership_state.amount_by_currency)
+        current_score = self.get_score(quantities_by_good_id=ownership_state.quantities_by_good_id,
+                                       amount_by_currency_id=ownership_state.amount_by_currency_id)
+        new_ownership_state = ownership_state.apply_transactions([tx_message])
+        new_score = self.get_score(quantities_by_good_id=new_ownership_state.quantities_by_good_id,
+                                   amount_by_currency_id=new_ownership_state.amount_by_currency_id)
         return new_score - current_score
 
     def _split_tx_fees(self, tx_fee: int) -> Dict[str, int]:
@@ -410,50 +412,107 @@ def _handle_tx_message(self, tx_message: TransactionMessage) -> None:
         :param tx_message: the transaction message
         :return: None
         """
-        # if not self.goal_pursuit_readiness.is_ready:
-        #     logger.warning("[{}]: Preferences and ownership state not initialized. Refusing to process transaction!".format(self._agent_name))
-        #     return
-        # TODO: reintroduce above check
+        if tx_message.ledger_id not in SUPPORTED_LEDGER_APIS:
+            logger.error("[{}]: ledger_id={} is not supported".format(self._agent_name, tx_message.ledger_id))
+            return
+
+        if not self.goal_pursuit_readiness.is_ready:
+            logger.debug("[{}]: Preferences and ownership state not initialized!".format(self._agent_name))
 
         # check if the transaction is acceptable and process it accordingly
-        if self._is_acceptable_tx(tx_message):
+        if tx_message.performative == TransactionMessage.Performative.PROPOSE_FOR_SETTLEMENT:
+            self._handle_tx_message_for_settlement(tx_message)
+        elif tx_message.performative == TransactionMessage.Performative.PROPOSE_FOR_SIGNING:
+            self._handle_tx_message_for_signing(tx_message)
+        else:
+            logger.error("[{}]: Unexpected transaction message performative".format(self._agent_name))
+
+    def _handle_tx_message_for_settlement(self, tx_message) -> None:
+        """
+        Handle a transaction message for settlement.
+
+        :param tx_message: the transaction message
+        :return: None
+        """
+        if self._is_acceptable_for_settlement(tx_message):
             tx_digest = self._settle_tx(tx_message)
             if tx_digest is not None:
-                tx_message_response = TransactionMessage.respond_with(tx_message,
-                                                                      performative=TransactionMessage.Performative.ACCEPT,
-                                                                      transaction_digest=tx_digest)
+                tx_message_response = TransactionMessage.respond_settlement(tx_message,
+                                                                            performative=TransactionMessage.Performative.SUCCESSFUL_SETTLEMENT,
+                                                                            tx_digest=tx_digest)
             else:
-                tx_message_response = TransactionMessage.respond_with(tx_message,
-                                                                      performative=TransactionMessage.Performative.REJECT)
+                tx_message_response = TransactionMessage.respond_settlement(tx_message,
+                                                                            performative=TransactionMessage.Performative.FAILED_SETTLEMENT)
         else:
-            tx_message_response = TransactionMessage.respond_with(tx_message,
-                                                                  performative=TransactionMessage.Performative.REJECT)
+            tx_message_response = TransactionMessage.respond_settlement(tx_message,
+                                                                        performative=TransactionMessage.Performative.REJECTED_SETTLEMENT)
         self.message_out_queue.put(tx_message_response)
 
-    def _is_acceptable_tx(self, tx_message: TransactionMessage) -> bool:
+    def _is_acceptable_for_settlement(self, tx_message: TransactionMessage) -> bool:
         """
         Check if the tx is acceptable.
 
         :param tx_message: the transaction message
         :return: whether the transaction is acceptable or not
         """
-        if tx_message.get("ledger_id") is not None:
-            amount = cast(int, tx_message.get("amount"))
-            counterparty_tx_fee = cast(int, tx_message.get("counterparty_tx_fee"))
-            sender_tx_fee = cast(int, tx_message.get("sender_tx_fee"))
-            # adjust payment amount to reflect transaction fee split
-            amount -= counterparty_tx_fee
-            tx_fee = counterparty_tx_fee + sender_tx_fee
-            payable = amount + tx_fee
-            is_correct_format = isinstance(payable, int)
-            crypto_object = self._wallet.crypto_objects.get(tx_message.get("ledger_id"))
-            balance = self.ledger_apis.token_balance(crypto_object.identifier, cast(str, crypto_object.address))
-            is_affordable = payable <= balance
-            # TODO check against preferences and other constraints
-            is_acceptable = is_correct_format and is_affordable
+        is_valid_tx_amount = self._is_valid_tx_amount(tx_message)
+        is_utility_enhancing = self._is_utility_enhancing(tx_message)
+        is_affordable = self._is_affordable(tx_message)
+        return is_valid_tx_amount and is_utility_enhancing and is_affordable
+
+    def _is_valid_tx_amount(self, tx_message: TransactionMessage) -> bool:
+        """
+        Check if the transaction amount is negative (agent is buyer).
+
+        If the transaction amount is positive, then the agent is the seller, so abort.
+        """
+        result = len(tx_message.tx_amount_by_currency_id) == 1
+        for currency_id, amount in tx_message.tx_amount_by_currency_id.items():
+            result = result and amount <= 0
+        return result
+
+    def _is_utility_enhancing(self, tx_message: TransactionMessage) -> bool:
+        """
+        Check if the tx is utility enhancing.
+
+        :param tx_message: the transaction message
+        :return: whether the transaction is utility enhancing or not
+        """
+        if self.preferences.is_initialized and self.ownership_state.is_initialized:
+            is_utility_enhancing = self.preferences.get_score_diff_from_transaction(self.ownership_state, tx_message) >= 0.0
+        else:
+            logger.warning("[{}]: Cannot verify whether transaction improves utility. Assuming it does!".format(self._agent_name))
+            is_utility_enhancing = True
+        return is_utility_enhancing
+
+    def _is_affordable(self, tx_message: TransactionMessage) -> bool:
+        """
+        Check if the tx is affordable.
+
+        :param tx_message: the transaction message
+        :return: whether the transaction is affordable or not
+        """
+        if tx_message.get("ledger_id") == 'off_chain':
+            logger.warning("[{}]: Cannot verify whether transaction is affordable. Assuming it is!".format(self._agent_name))
+            is_affordable = True
         else:
-            is_acceptable = self.preferences.get_score_diff_from_transaction(self.ownership_state, tx_message) >= 0.0
-        return is_acceptable
+            # TODO check currency
+            assert len(tx_message.tx_amount_by_currency_id) == 1
+            for currency_id, amount in tx_message.tx_amount_by_currency_id.items():
+                if amount <= 0 and all(quantity >= 0 for quantity in tx_message.tx_quantities_by_good_id.values()):
+                    # check if the agent has the money to cover amount and tx fee (the agent is the buyer).
+                    transfer_amount = -amount - tx_message.tx_counterparty_fee
+                    max_tx_fee = tx_message.tx_sender_fee + tx_message.tx_counterparty_fee
+                    max_payable = transfer_amount + max_tx_fee
+                    crypto_object = self._wallet.crypto_objects.get(tx_message.ledger_id)
+                    balance = self.ledger_apis.token_balance(crypto_object.identifier, crypto_object.address)
+                    is_affordable = (max_payable <= balance) and (transfer_amount >= 0)
+                elif amount >= 0 and all(quantity <= 0 for quantity in tx_message.tx_quantities_by_good_id.values()):
+                    # check if the agent has the goods to cover the transaction
+                    is_affordable = all(self.ownership_state.quantities_by_good_id[good_id] >= -quantity for good_id, quantity in tx_message.tx_quantities_by_good_id.items())
+                else:
+                    is_affordable = False
+        return is_affordable
 
     def _settle_tx(self, tx_message: TransactionMessage) -> Optional[str]:
         """
@@ -462,21 +521,87 @@ def _settle_tx(self, tx_message: TransactionMessage) -> Optional[str]:
         :param tx_message: the transaction message
         :return: the transaction digest
         """
-        logger.info("[{}]: Settling transaction!".format(self._agent_name))
-        if tx_message.get("ledger_id") is not None:
-            amount = cast(int, tx_message.get("amount"))
-            counterparty_tx_fee = cast(int, tx_message.get("counterparty_tx_fee"))
-            sender_tx_fee = cast(int, tx_message.get("sender_tx_fee"))
-            counterparty_address = cast(str, tx_message.get("counterparty"))
-            # adjust payment amount to reflect transaction fee split
-            amount -= counterparty_tx_fee
-            tx_fee = counterparty_tx_fee + sender_tx_fee
-            crypto_object = self._wallet.crypto_objects.get(tx_message.get("ledger_id"))
-            tx_digest = self.ledger_apis.transfer(crypto_object.identifier, crypto_object, counterparty_address, amount, tx_fee)
+        if tx_message.ledger_id == OFF_CHAIN:
+            logger.info("[{}]: Cannot settle transaction, settlement happens off chain!".format(self._agent_name))
+            tx_digest = OFF_CHAIN_SETTLEMENT_DIGEST
         else:
-            tx_digest = cast(str, tx_message.get("transaction_id"))
+            logger.info("[{}]: Settling transaction on chain!".format(self._agent_name))
+            assert len(tx_message.tx_amount_by_currency_id) == 1
+            for currency_id, amount in tx_message.tx_amount_by_currency_id.items():
+                # adjust payment amount to reflect transaction fee split
+                payable = -amount - tx_message.tx_counterparty_fee
+                max_tx_fee = tx_message.tx_counterparty_fee + tx_message.tx_sender_fee
+                crypto_object = self._wallet.crypto_objects.get(tx_message.ledger_id)
+                tx_digest = self.ledger_apis.transfer(crypto_object, tx_message.tx_counterparty_addr, payable, max_tx_fee)
         return tx_digest
 
+    def _handle_tx_message_for_signing(self, tx_message: TransactionMessage) -> None:
+        """
+        Handle a transaction message for signing.
+
+        :param tx_message: the transaction message
+        :return: None
+        """
+        if self._is_acceptable_for_signing(tx_message):
+            tx_signature = self._sign_tx(tx_message)
+            tx_message_response = TransactionMessage.respond_signing(tx_message,
+                                                                     performative=TransactionMessage.Performative.SUCCESSFUL_SIGNING,
+                                                                     tx_signature=tx_signature)
+        else:
+            tx_message_response = TransactionMessage.respond_signing(tx_message,
+                                                                     performative=TransactionMessage.Performative.REJECTED_SIGNING)
+        self.message_out_queue.put(tx_message_response)
+
+    def _is_acceptable_for_signing(self, tx_message: TransactionMessage) -> bool:
+        """
+        Check if the tx is acceptable.
+
+        :param tx_message: the transaction message
+        :return: whether the transaction is acceptable or not
+        """
+        is_valid_ledger_id = self._is_valid_ledger_id(tx_message)
+        is_valid_tx_hash = self._is_valid_tx_hash(tx_message)
+        is_utility_enhancing = self._is_utility_enhancing(tx_message)
+        is_affordable = self._is_affordable(tx_message)
+        return is_valid_ledger_id and is_valid_tx_hash and is_utility_enhancing and is_affordable
+
+    def _is_valid_ledger_id(self, tx_message: TransactionMessage) -> bool:
+        """
+        Check if the ledger id is valid for signing.
+
+        :param tx_message: the transaction message
+        :return: whether the transaction has a valid ledger id
+        """
+        valid = True
+        if tx_message.ledger_id == OFF_CHAIN:
+            logger.error("[{}]: ledger_id='off_chain' is not valid for signing!")
+            valid = False
+        return valid
+
+    def _is_valid_tx_hash(self, tx_message: TransactionMessage) -> bool:
+        """
+        Check if the tx hash is present and matches the terms.
+
+        :param tx_message: the transaction message
+        :return: whether the transaction hash is valid
+        """
+        # TODO check the hash matches the terms of the transaction, this means dm requires knowledge of how the hash is composed
+        tx_hash = tx_message.signing_payload.get('tx_hash')
+        valid = isinstance(tx_hash, bytes)
+        return valid
+
+    def _sign_tx(self, tx_message: TransactionMessage) -> str:
+        """
+        Sign the tx.
+
+        :param tx_message: the transaction message
+        :return: the signature of the signing payload
+        """
+        crypto_object = self._wallet.crypto_objects.get(tx_message.ledger_id)
+        tx_hash = tx_message.signing_payload.get('tx_hash')
+        tx_signature = crypto_object.sign_transaction(tx_hash)
+        return tx_signature
+
     def _handle_state_update_message(self, state_update_message: StateUpdateMessage) -> None:
         """
         Handle a state update message.
@@ -484,18 +609,12 @@ def _handle_state_update_message(self, state_update_message: StateUpdateMessage)
         :param state_update_message: the state update message
         :return: None
         """
-        performative = state_update_message.get("performative")
-        if performative == StateUpdateMessage.Performative.INITIALIZE:
-            amount_by_currency = cast(Dict[str, int], state_update_message.get("amount_by_currency"))
-            quantities_by_good_pbk = cast(Dict[str, int], state_update_message.get("quantities_by_good_pbk"))
-            self.ownership_state.init(amount_by_currency=amount_by_currency, quantities_by_good_pbk=quantities_by_good_pbk, agent_name=self._agent_name)
-            exchange_params_by_currency = cast(Dict[str, float], state_update_message.get("exchange_params_by_currency"))
-            utility_params_by_good_pbk = cast(Dict[str, float], state_update_message.get("utility_params_by_good_pbk"))
-            tx_fee = cast(int, state_update_message.get("tx_fee"))
-            self.preferences.init(exchange_params_by_currency=exchange_params_by_currency, utility_params_by_good_pbk=utility_params_by_good_pbk, tx_fee=tx_fee, agent_name=self._agent_name)
+        if state_update_message.performative == StateUpdateMessage.Performative.INITIALIZE:
+            logger.info("[{}]: Applying state initialization!".format(self._agent_name))
+            self.ownership_state.init(amount_by_currency_id=state_update_message.amount_by_currency_id, quantities_by_good_id=state_update_message.quantities_by_good_id, agent_name=self._agent_name)
+            self.preferences.init(exchange_params_by_currency_id=state_update_message.exchange_params_by_currency_id, utility_params_by_good_id=state_update_message.utility_params_by_good_id, tx_fee=state_update_message.tx_fee, agent_name=self._agent_name)
             self.goal_pursuit_readiness.update(GoalPursuitReadiness.Status.READY)
-        elif performative == StateUpdateMessage.Performative.APPLY:
-            amount_by_currency = cast(Dict[str, int], state_update_message.get("amount_by_currency"))
-            quantities_by_good_pbk = cast(Dict[str, int], state_update_message.get("quantities_by_good_pbk"))
-            new_ownership_state = self.ownership_state.apply_state_update(amount_deltas_by_currency=amount_by_currency, quantity_deltas_by_good_pbk=quantities_by_good_pbk)
+        elif state_update_message.performative == StateUpdateMessage.Performative.APPLY:
+            logger.info("[{}]: Applying state update!".format(self._agent_name))
+            new_ownership_state = self.ownership_state.apply_state_update(amount_by_currency_id=state_update_message.amount_by_currency_id, quantities_by_good_id=state_update_message.quantities_by_good_id)
             self._ownership_state = new_ownership_state
diff --git a/aea/decision_maker/messages/base.py b/aea/decision_maker/messages/base.py
new file mode 100644
index 0000000000..3671ac3d43
--- /dev/null
+++ b/aea/decision_maker/messages/base.py
@@ -0,0 +1,96 @@
+# -*- coding: utf-8 -*-
+
+# ------------------------------------------------------------------------------
+#
+#   Copyright 2018-2019 Fetch.AI Limited
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+#
+# ------------------------------------------------------------------------------
+
+"""This module contains the base message and serialization definition."""
+
+from copy import copy
+from typing import Any, Dict, Optional
+
+
+class InternalMessage:
+    """This class implements a message."""
+
+    protocol_id = "internal"
+
+    def __init__(self, body: Optional[Dict] = None,
+                 **kwargs):
+        """
+        Initialize a Message object.
+
+        :param body: the dictionary of values to hold.
+        :param kwargs: any additional value to add to the body. It will overwrite the body values.
+        """
+        self._body = copy(body) if body else {}  # type: Dict[str, Any]
+        self._body.update(kwargs)
+
+    @property
+    def body(self) -> Dict:
+        """
+        Get the body of the message (in dictionary form).
+
+        :return: the body
+        """
+        return self._body
+
+    @body.setter
+    def body(self, body: Dict) -> None:
+        """
+        Set the body of hte message.
+
+        :param body: the body.
+        :return: None
+        """
+        self._body = body
+
+    def set(self, key: str, value: Any) -> None:
+        """
+        Set key and value pair.
+
+        :param key: the key.
+        :param value: the value.
+        :return: None
+        """
+        self._body[key] = value
+
+    def get(self, key: str) -> Optional[Any]:
+        """Get value for key."""
+        return self._body.get(key, None)
+
+    def unset(self, key: str) -> None:
+        """Unset valye for key."""
+        self._body.pop(key, None)
+
+    def is_set(self, key: str) -> bool:
+        """Check value is set for key."""
+        return key in self._body
+
+    def check_consistency(self) -> bool:
+        """Check that the data is consistent."""
+        return True
+
+    def __eq__(self, other):
+        """Compare with another object."""
+        return isinstance(other, InternalMessage) \
+            and self.body == other.body \
+
+
+    def __str__(self):
+        """Get the string representation of the message."""
+        return "InternalMessage(" + " ".join(map(lambda key_value: str(key_value[0]) + "=" + str(key_value[1]), self.body.items())) + ")"
diff --git a/aea/decision_maker/messages/state_update.py b/aea/decision_maker/messages/state_update.py
index ee4082b7f4..d63352d4c8 100644
--- a/aea/decision_maker/messages/state_update.py
+++ b/aea/decision_maker/messages/state_update.py
@@ -21,12 +21,11 @@
 """The state update message module."""
 
 from enum import Enum
-from typing import cast, Dict, Optional, Union
+from typing import cast, Dict
 
-from aea.protocols.base import Message
+from aea.decision_maker.messages.base import InternalMessage
 
 TransactionId = str
-Address = str
 
 Currencies = Dict[str, int]  # a map from identifier to quantity
 Goods = Dict[str, int]   # a map from identifier to quantity
@@ -34,43 +33,68 @@
 ExchangeParams = Dict[str, float]   # a map from identifier to quantity
 
 
-class StateUpdateMessage(Message):
+class StateUpdateMessage(InternalMessage):
     """The state update message class."""
 
-    protocol_id = "internal"
-
     class Performative(Enum):
         """State update performative."""
 
         INITIALIZE = "initialize"
         APPLY = "apply"
 
-    def __init__(self, performative: Union[str, Performative],
-                 amount_by_currency: Currencies,
-                 quantities_by_good_pbk: Goods,
-                 exchange_params_by_currency: Optional[ExchangeParams] = None,
-                 utility_params_by_good_pbk: Optional[UtilityParams] = None,
-                 tx_fee: Optional[int] = None,
+    def __init__(self, performative: Performative,
+                 amount_by_currency_id: Currencies,
+                 quantities_by_good_id: Goods,
                  **kwargs):
         """
         Instantiate transaction message.
 
         :param performative: the performative
-        :param amount_by_currency: the amounts of currencies.
-        :param quantities_by_good_pbk: the quantities of goods.
-        :param exchange_params_by_currency: the exchange params.
-        :param utility_params_by_good_pbk: the utility params.
-        :param tx_fee: the tx fee.
+        :param amount_by_currency_id: the amounts of currencies.
+        :param quantities_by_good_id: the quantities of goods.
         """
         super().__init__(performative=performative,
-                         amount_by_currency=amount_by_currency,
-                         quantities_by_good_pbk=quantities_by_good_pbk,
-                         exchange_params_by_currency=exchange_params_by_currency,
-                         utility_params_by_good_pbk=utility_params_by_good_pbk,
-                         tx_fee=tx_fee,
+                         amount_by_currency_id=amount_by_currency_id,
+                         quantities_by_good_id=quantities_by_good_id,
                          **kwargs)
         assert self.check_consistency(), "StateUpdateMessage initialization inconsistent."
 
+    @property
+    def performative(self) -> Performative:  # noqa: F821
+        """Get the performative of the message."""
+        assert self.is_set("performative"), "Performative is not set."
+        return StateUpdateMessage.Performative(self.get('performative'))
+
+    @property
+    def amount_by_currency_id(self) -> Currencies:
+        """Get the amount by currency."""
+        assert self.is_set("amount_by_currency_id")
+        return cast(Currencies, self.get("amount_by_currency_id"))
+
+    @property
+    def quantities_by_good_id(self) -> Goods:
+        """Get the quantities by good id."""
+        assert self.is_set("quantities_by_good_id"), "quantities_by_good_id is not set."
+        return cast(Goods, self.get("quantities_by_good_id"))
+
+    @property
+    def exchange_params_by_currency_id(self) -> ExchangeParams:
+        """Get the exchange parameters by currency from the message."""
+        assert self.is_set("exchange_params_by_currency_id")
+        return cast(ExchangeParams, self.get("exchange_params_by_currency_id"))
+
+    @property
+    def utility_params_by_good_id(self) -> UtilityParams:
+        """Get the utility parameters by good id."""
+        assert self.is_set("utility_params_by_good_id")
+        return cast(UtilityParams, self.get("utility_params_by_good_id"))
+
+    @property
+    def tx_fee(self) -> int:
+        """Get the transaction fee."""
+        assert self.is_set("tx_fee")
+        return cast(int, self.get("tx_fee"))
+
     def check_consistency(self) -> bool:
         """
         Check that the data is consistent.
@@ -78,28 +102,33 @@ def check_consistency(self) -> bool:
         :return: bool
         """
         try:
-            assert self.is_set("performative")
-            performative = self.get("performative")
-            assert self.is_set("amount_by_currency")
-            amount_by_currency = self.get("amount_by_currency")
-            amount_by_currency = cast(Currencies, amount_by_currency)
-            assert self.is_set("quantities_by_good_pbk")
-            quantities_by_good_pbk = self.get("quantities_by_good_pbk")
-            quantities_by_good_pbk = cast(Goods, quantities_by_good_pbk)
-            if performative == self.Performative.INITIALIZE:
-                assert self.is_set("exchange_params_by_currency")
-                exchange_params_by_currency = self.get("exchange_params_by_currency")
-                exchange_params_by_currency = cast(ExchangeParams, exchange_params_by_currency)
-                assert amount_by_currency.keys() == exchange_params_by_currency.keys()
-                assert self.is_set("utility_params_by_good_pbk")
-                utility_params_by_good_pbk = self.get("utility_params_by_good_pbk")
-                utility_params_by_good_pbk = cast(UtilityParams, utility_params_by_good_pbk)
-                assert quantities_by_good_pbk.keys() == utility_params_by_good_pbk.keys()
-                assert self.is_set("tx_fee")
-            elif performative == self.Performative.APPLY:
-                assert self.get("exchange_params_by_currency") is None
-                assert self.get("utility_params_by_good_pbk") is None
-                assert self.get("tx_fee") is None
+            assert isinstance(self.performative, StateUpdateMessage.Performative)
+            assert isinstance(self.amount_by_currency_id, dict)
+            for key, int_value in self.amount_by_currency_id.items():
+                assert isinstance(key, str)
+                assert isinstance(int_value, int)
+            assert isinstance(self.quantities_by_good_id, dict)
+            for key, int_value in self.quantities_by_good_id.items():
+                assert isinstance(key, str)
+                assert isinstance(int_value, int)
+            if self.performative == self.Performative.INITIALIZE:
+                assert isinstance(self.exchange_params_by_currency_id, dict)
+                for key, float_value in self.exchange_params_by_currency_id.items():
+                    assert isinstance(key, str)
+                    assert isinstance(float_value, float)
+                assert self.amount_by_currency_id.keys() == self.exchange_params_by_currency_id.keys()
+                assert isinstance(self.utility_params_by_good_id, dict)
+                for key, float_value in self.utility_params_by_good_id.items():
+                    assert isinstance(key, str)
+                    assert isinstance(float_value, float)
+                assert self.quantities_by_good_id.keys() == self.utility_params_by_good_id.keys()
+                assert isinstance(self.tx_fee, int)
+                assert len(self.body) == 6
+            elif self.performative == self.Performative.APPLY:
+                assert len(self.body) == 3
+            else:  # pragma: no cover
+                raise ValueError("Performative not recognized.")
+
         except (AssertionError, KeyError):
             return False
         return True
diff --git a/aea/decision_maker/messages/transaction.py b/aea/decision_maker/messages/transaction.py
index ceb6d864ac..bcec3ee28d 100644
--- a/aea/decision_maker/messages/transaction.py
+++ b/aea/decision_maker/messages/transaction.py
@@ -21,75 +21,157 @@
 """The transaction message module."""
 
 from enum import Enum
-from typing import Dict, Optional, Union, cast
+from typing import Any, Dict, List, Optional, cast
 
-from aea.protocols.base import Message
+from aea.crypto.ledger_apis import SUPPORTED_LEDGER_APIS
+from aea.decision_maker.messages.base import InternalMessage
+from aea.mail.base import Address
 
 TransactionId = str
-Address = str
+LedgerId = str
+OFF_CHAIN = 'off_chain'
+SUPPORTED_LEDGER_IDS = SUPPORTED_LEDGER_APIS + [OFF_CHAIN]
 
 
-class TransactionMessage(Message):
+class TransactionMessage(InternalMessage):
     """The transaction message class."""
 
-    protocol_id = "internal"
-
     class Performative(Enum):
         """Transaction performative."""
 
-        PROPOSE = "propose"
-        ACCEPT = "accept"
-        REJECT = "reject"
-
-    def __init__(self, performative: Union[str, Performative],
-                 skill_id: str,
-                 transaction_id: TransactionId,
-                 sender: Address,
-                 counterparty: Address,
-                 is_sender_buyer: bool,
-                 currency_pbk: str,
-                 amount: int,
-                 sender_tx_fee: int,
-                 counterparty_tx_fee: int,
-                 quantities_by_good_pbk: Dict[str, int],
-                 transaction_digest: Optional[str] = None,
-                 dialogue_label: Optional[Dict[str, str]] = None,
-                 ledger_id: Optional[str] = None,
+        PROPOSE_FOR_SETTLEMENT = "propose_for_settlement"
+        SUCCESSFUL_SETTLEMENT = "successful_settlement"
+        FAILED_SETTLEMENT = "failed_settlement"
+        REJECTED_SETTLEMENT = "rejected_settlement"
+        PROPOSE_FOR_SIGNING = "propose_for_signing"
+        SUCCESSFUL_SIGNING = "successful_signing"
+        REJECTED_SIGNING = "rejected_signing"
+
+    def __init__(self, performative: Performative,
+                 skill_callback_ids: List[str],
+                 tx_id: TransactionId,
+                 tx_sender_addr: Address,
+                 tx_counterparty_addr: Address,
+                 tx_amount_by_currency_id: Dict[str, int],
+                 tx_sender_fee: int,
+                 tx_counterparty_fee: int,
+                 tx_quantities_by_good_id: Dict[str, int],
+                 ledger_id: LedgerId,
+                 info: Dict[str, Any],
                  **kwargs):
         """
         Instantiate transaction message.
 
         :param performative: the performative
-        :param skill_id: the skill sending the transaction message
-        :param transaction_id: the id of the transaction.
-        :param sender: the sender of the transaction.
-        :param counterparty: the counterparty of the transaction.
-        :param is_sender_buyer: whether the transaction is sent by a buyer.
-        :param currency_pbk: the currency of the transaction.
-        :param sender_tx_fee: the part of the tx fee paid by the sender
-        :param counterparty_tx_fee: the part of the tx fee paid by the counterparty
-        :param amount: the amount of money involved.
-        :param quantities_by_good_pbk: a map from good pbk to the quantity of that good involved in the transaction.
-        :param transaction_digest: the transaction digest
-        :param dialog_label: the dialog label
+        :param skill_callback_ids: the skills to receive the transaction message response
+        :param tx_id: the id of the transaction.
+        :param tx_sender: the sender of the transaction.
+        :param tx_counterparty: the counterparty of the transaction.
+        :param tx_amount_by_currency_id: the amount by the currency of the transaction.
+        :param tx_sender_fee: the part of the tx fee paid by the sender
+        :param tx_counterparty_fee: the part of the tx fee paid by the counterparty
+        :param tx_quantities_by_good_id: a map from good id to the quantity of that good involved in the transaction.
+        :param ledger_id: the ledger id
+        :param info: a dictionary for arbitrary information
         """
         super().__init__(performative=performative,
-                         skill_id=skill_id,
-                         transaction_id=transaction_id,
-                         sender=sender,
-                         counterparty=counterparty,
-                         is_sender_buyer=is_sender_buyer,
-                         currency_pbk=currency_pbk,
-                         sender_tx_fee=sender_tx_fee,
-                         counterparty_tx_fee=counterparty_tx_fee,
-                         amount=amount,
-                         quantities_by_good_pbk=quantities_by_good_pbk,
-                         transaction_digest=transaction_digest,
-                         dialogue_label=dialogue_label,
+                         skill_callback_ids=skill_callback_ids,
+                         tx_id=tx_id,
+                         tx_sender_addr=tx_sender_addr,
+                         tx_counterparty_addr=tx_counterparty_addr,
+                         tx_amount_by_currency_id=tx_amount_by_currency_id,
+                         tx_sender_fee=tx_sender_fee,
+                         tx_counterparty_fee=tx_counterparty_fee,
+                         tx_quantities_by_good_id=tx_quantities_by_good_id,
                          ledger_id=ledger_id,
+                         info=info,
                          **kwargs)
         assert self.check_consistency(), "Transaction message initialization inconsistent."
 
+    @property
+    def performative(self) -> Performative:  # noqa: F821
+        """Get the performative of the message."""
+        assert self.is_set("performative"), "Performative is not set."
+        return TransactionMessage.Performative(self.get('performative'))
+
+    @property
+    def skill_callback_ids(self) -> List[str]:
+        """Get the list of skill_callback_ids from the message."""
+        assert self.is_set("skill_callback_ids"), "Skill_callback_ids is not set."
+        return cast(List[str], self.get("skill_callback_ids"))
+
+    @property
+    def tx_id(self) -> str:
+        """Get the transaction id."""
+        assert self.is_set("tx_id"), "Transaction_id is not set."
+        return cast(str, self.get("tx_id"))
+
+    @property
+    def tx_sender_addr(self) -> Address:
+        """Get the address of the sender."""
+        assert self.is_set("tx_sender_addr"), "Tx_sender_addr is not set."
+        return cast(Address, self.get("tx_sender_addr"))
+
+    @property
+    def tx_counterparty_addr(self) -> Address:
+        """Get the counterparty of the message."""
+        assert self.is_set("tx_counterparty_addr"), "Counterparty is not set."
+        return cast(Address, self.get("tx_counterparty_addr"))
+
+    @property
+    def tx_amount_by_currency_id(self) -> Dict[str, int]:
+        """Get the currency id."""
+        assert self.is_set("tx_amount_by_currency_id"), "Tx_amount_by_currency_id is not set."
+        return cast(Dict[str, int], self.get("tx_amount_by_currency_id"))
+
+    @property
+    def tx_sender_fee(self) -> int:
+        """Get the fee for the sender from the messgae."""
+        assert self.is_set("tx_sender_fee"), "Tx_sender_fee is not set."
+        return cast(int, self.get("tx_sender_fee"))
+
+    @property
+    def tx_counterparty_fee(self) -> int:
+        """Get the fee for the counterparty from the messgae."""
+        assert self.is_set("tx_counterparty_fee"), "Tx_counterparty_fee is not set."
+        return cast(int, self.get("tx_counterparty_fee"))
+
+    @property
+    def tx_quantities_by_good_id(self) -> Dict[str, int]:
+        """Get the quantities by good ids."""
+        assert self.is_set("tx_quantities_by_good_id"), "Tx_quantities_by_good_id is not set."
+        return cast(Dict[str, int], self.get("tx_quantities_by_good_id"))
+
+    @property
+    def ledger_id(self) -> LedgerId:
+        """Get the ledger_id."""
+        assert self.is_set("ledger_id"), "Ledger_id is not set."
+        return cast(str, self.get("ledger_id"))
+
+    @property
+    def info(self) -> Dict[str, Any]:
+        """Get the infos from the message."""
+        assert self.is_set("info"), "Info is not set."
+        return cast(Dict[str, Any], self.get("info"))
+
+    @property
+    def tx_digest(self) -> str:
+        """Get the transaction digest."""
+        assert self.is_set("tx_digest"), "Tx_digest is not set."
+        return cast(str, self.get("tx_digest"))
+
+    @property
+    def signing_payload(self) -> Dict[str, Any]:
+        """Get the signing payload."""
+        assert self.is_set("signing_payload"), "signing_payload is not set."
+        return cast(Dict[str, Any], self.get("signing_payload"))
+
+    @property
+    def tx_signature(self) -> str:
+        """Get the transaction signature."""
+        assert self.is_set("tx_signature"), "Tx_signature is not set."
+        return cast(str, self.get("tx_signature"))
+
     def check_consistency(self) -> bool:
         """
         Check that the data is consistent.
@@ -97,108 +179,114 @@ def check_consistency(self) -> bool:
         :return: bool
         """
         try:
-            assert self.is_set("performative")
-            assert self.is_set("skill_id")
-            assert self.is_set("transaction_id")
-            assert self.is_set("sender")
-            assert self.is_set("counterparty")
-            sender = self.get("sender")
-            counterparty = self.get("counterparty")
-            assert sender != counterparty
-            assert self.is_set("is_sender_buyer")
-            assert self.is_set("currency_pbk")
-            assert self.is_set("amount")
-            amount = self.get("amount")
-            amount = cast(int, amount)
-            assert amount >= 0
-            assert self.is_set("sender_tx_fee")
-            sender_tx_fee = self.get("sender_tx_fee")
-            sender_tx_fee = cast(int, sender_tx_fee)
-            assert sender_tx_fee >= 0
-            assert self.is_set("counterparty_tx_fee")
-            counterparty_tx_fee = self.get("counterparty_tx_fee")
-            counterparty_tx_fee = cast(int, counterparty_tx_fee)
-            assert counterparty_tx_fee >= 0
-            assert self.is_set("quantities_by_good_pbk")
-            quantities_by_good_pbk = self.get("quantities_by_good_pbk")
-            quantities_by_good_pbk = cast(Dict[str, int], quantities_by_good_pbk)
-            assert len(quantities_by_good_pbk.keys()) == len(set(quantities_by_good_pbk.keys()))
-            assert all(quantity >= 0 for quantity in quantities_by_good_pbk.values())
-            assert self.is_set("transaction_digest")
-            assert self.is_set("dialogue_label")
-            assert self.is_set("ledger_id")
+            assert isinstance(self.performative, TransactionMessage.Performative), "Performative is not of correct type."
+            assert isinstance(self.skill_callback_ids, list) and all(isinstance(s, str) for s in self.skill_callback_ids), "Skill_callback_ids must be of type List[str]."
+            assert isinstance(self.tx_id, str), "Tx_id must of type str."
+            assert isinstance(self.tx_sender_addr, Address), "Tx_sender_addr must be of type Address."
+            assert isinstance(self.tx_counterparty_addr, Address), "Tx_counterparty_addr must be of type Address."
+            assert self.tx_sender_addr != self.tx_counterparty_addr, "Tx_sender_addr must be different of tx_counterparty_addr."
+            assert isinstance(self.tx_amount_by_currency_id, dict) and all((isinstance(key, str) and isinstance(value, int)) for key, value in self.tx_amount_by_currency_id.items()), "Tx_amount_by_currency_id must be of type Dict[str, int]."
+            assert len(self.tx_amount_by_currency_id) == 1, "Cannot reference more than one currency."
+            assert isinstance(self.tx_sender_fee, int), "Tx_sender_fee must be of type int."
+            assert self.tx_sender_fee >= 0, "Tx_sender_fee must be greater or equal to zero."
+            assert isinstance(self.tx_counterparty_fee, int), "Tx_counterparty_fee must be of type int."
+            assert self.tx_counterparty_fee >= 0, "Tx_counterparty_fee must be greater or equal to zero."
+            assert isinstance(self.tx_quantities_by_good_id, dict) and all((isinstance(key, str) and isinstance(value, int)) for key, value in self.tx_quantities_by_good_id.items()), "Tx_quantities_by_good_id must be of type Dict[str, int]."
+            assert len(self.tx_quantities_by_good_id.keys()) == len(set(self.tx_quantities_by_good_id.keys())), "Good ids must be unique in tx_quantities_by_good_id."
+            assert isinstance(self.ledger_id, str) and self.ledger_id in SUPPORTED_LEDGER_IDS, "Ledger_id must be str and " \
+                                                                                               "must in the supported ledger ids."
+            assert isinstance(self.info, dict) and all(isinstance(key, str) for key in self.info.keys()), "Info must be of type Dict[str, Any]."
+            if self.performative in {self.Performative.PROPOSE_FOR_SETTLEMENT, self.Performative.REJECTED_SETTLEMENT, self.Performative.FAILED_SETTLEMENT}:
+                assert len(self.body) == 11
+            elif self.performative == self.Performative.SUCCESSFUL_SETTLEMENT:
+                assert isinstance(self.tx_digest, str), "Tx_digest must be of type str."
+                assert len(self.body) == 12
+            elif self.performative in {self.Performative.PROPOSE_FOR_SIGNING, self.Performative.REJECTED_SIGNING}:
+                assert isinstance(self.signing_payload, dict) and all(isinstance(key, str) for key in self.signing_payload.keys()), "Signing_payload must be of type Dict[str, Any]"
+                assert len(self.body) == 12
+            elif self.performative == self.Performative.SUCCESSFUL_SIGNING:
+                assert isinstance(self.signing_payload, dict) and all(isinstance(key, str) for key in self.signing_payload.keys()), "Signing_payload must be of type Dict[str, Any]"
+                assert isinstance(self.tx_signature, str), "Tx_signature must be of type str"
+                assert len(self.body) == 13
+            else:  # pragma: no cover
+                raise ValueError("Performative not recognized.")
+
         except (AssertionError, KeyError):
             return False
         return True
 
-    def matches(self, other: 'TransactionMessage') -> bool:
-        """
-        Check if the transaction matches with another (mirroring) transaction.
-
-        :param other: the other transaction to match.
-        :return: True if the two
-        """
-        return isinstance(other, TransactionMessage) \
-            and self.get("performative") == other.get("performative") \
-            and self.get("skill_id") == other.get("skill_id") \
-            and self.get("transaction_id") == other.get("transaction_id") \
-            and self.get("sender") == other.get("counterparty") \
-            and self.get("counterparty") == other.get("sender") \
-            and self.get("is_sender_buyer") != other.get("is_sender_buyer") \
-            and self.get("currency") == other.get("currency") \
-            and self.get("amount") == other.get("amount") \
-            and self.get("sender_tx_fee") == other.get("counterparty_tx_fee") \
-            and self.get("counterparty_tx_fee") == other.get("sender_tx_fee") \
-            and self.get("quantities_by_good_pbk") == other.get("quantities_by_good_pbk") \
-            and self.get("transaction_digest") == other.get("transaction_digest") \
-            and self.get("dialogue_label") == other.get("dialogue_label") \
-            and self.get("ledger_id") == other.get("ledger_id")
-
     @classmethod
-    def respond_with(cls, other: 'TransactionMessage', performative: Performative, transaction_digest: Optional[str] = None) -> 'TransactionMessage':
+    def respond_settlement(cls, other: 'TransactionMessage', performative: Performative, tx_digest: Optional[str] = None) -> 'TransactionMessage':
         """
         Create response message.
 
         :param other: TransactionMessage
         :param performative: the performative
-        :param transaction_digest: the transaction digest
+        :param tx_digest: the transaction digest
         :return: a transaction message object
         """
-        tx_msg = TransactionMessage(performative=performative,
-                                    skill_id=cast(str, other.get("skill_id")),
-                                    transaction_id=cast(str, other.get("transaction_id")),
-                                    sender=cast(Address, other.get("sender")),
-                                    counterparty=cast(Address, other.get("counterparty")),
-                                    is_sender_buyer=cast(bool, other.get("is_sender_buyer")),
-                                    currency_pbk=cast(str, other.get("currency_pbk")),
-                                    sender_tx_fee=cast(int, other.get("sender_tx_fee")),
-                                    counterparty_tx_fee=cast(int, other.get("counterparty_tx_fee")),
-                                    amount=cast(int, other.get("amount")),
-                                    quantities_by_good_pbk=cast(Dict[str, int], other.get("quantities_by_good_pbk")),
-                                    transaction_digest=transaction_digest,
-                                    dialogue_label=cast(Dict, other.get("dialogue_label")),
-                                    ledger_id=other.get("ledger_id"))
+        if tx_digest is None:
+            tx_msg = TransactionMessage(performative=performative,
+                                        skill_callback_ids=other.skill_callback_ids,
+                                        tx_id=other.tx_id,
+                                        tx_sender_addr=other.tx_sender_addr,
+                                        tx_counterparty_addr=other.tx_counterparty_addr,
+                                        tx_amount_by_currency_id=other.tx_amount_by_currency_id,
+                                        tx_sender_fee=other.tx_sender_fee,
+                                        tx_counterparty_fee=other.tx_counterparty_fee,
+                                        tx_quantities_by_good_id=other.tx_quantities_by_good_id,
+                                        ledger_id=other.ledger_id,
+                                        info=other.info)
+        else:
+            tx_msg = TransactionMessage(performative=performative,
+                                        skill_callback_ids=other.skill_callback_ids,
+                                        tx_id=other.tx_id,
+                                        tx_sender_addr=other.tx_sender_addr,
+                                        tx_counterparty_addr=other.tx_counterparty_addr,
+                                        tx_amount_by_currency_id=other.tx_amount_by_currency_id,
+                                        tx_sender_fee=other.tx_sender_fee,
+                                        tx_counterparty_fee=other.tx_counterparty_fee,
+                                        tx_quantities_by_good_id=other.tx_quantities_by_good_id,
+                                        ledger_id=other.ledger_id,
+                                        info=other.info,
+                                        tx_digest=tx_digest)
         return tx_msg
 
-    def __eq__(self, other: object) -> bool:
+    @classmethod
+    def respond_signing(cls, other: 'TransactionMessage', performative: Performative, tx_signature: Optional[str] = None) -> 'TransactionMessage':
         """
-        Compare to another object.
+        Create response message.
 
-        :param other: the other transaction to match.
-        :return: True if the two
+        :param other: TransactionMessage
+        :param performative: the performative
+        :param tx_digest: the transaction digest
+        :return: a transaction message object
         """
-        return isinstance(other, TransactionMessage) \
-            and self.get("performative") == other.get("performative") \
-            and self.get("skill_id") == other.get("skill_id") \
-            and self.get("transaction_id") == other.get("transaction_id") \
-            and self.get("sender") == other.get("sender") \
-            and self.get("counterparty") == other.get("counterparty") \
-            and self.get("is_sender_buyer") == other.get("is_sender_buyer") \
-            and self.get("currency") == other.get("currency") \
-            and self.get("amount") == other.get("amount") \
-            and self.get("sender_tx_fee") == other.get("sender_tx_fee") \
-            and self.get("counterparty_tx_fee") == other.get("counterparty_tx_fee") \
-            and self.get("quantities_by_good_pbk") == other.get("quantities_by_good_pbk") \
-            and self.get("transaction_digest") == other.get("transaction_digest") \
-            and self.get("dialogue_label") == other.get("dialogue_label")\
-            and self.get("ledger_id") == other.get("ledger_id")
+        if tx_signature is None:
+            tx_msg = TransactionMessage(performative=performative,
+                                        skill_callback_ids=other.skill_callback_ids,
+                                        tx_id=other.tx_id,
+                                        tx_sender_addr=other.tx_sender_addr,
+                                        tx_counterparty_addr=other.tx_counterparty_addr,
+                                        tx_amount_by_currency_id=other.tx_amount_by_currency_id,
+                                        tx_sender_fee=other.tx_sender_fee,
+                                        tx_counterparty_fee=other.tx_counterparty_fee,
+                                        tx_quantities_by_good_id=other.tx_quantities_by_good_id,
+                                        ledger_id=other.ledger_id,
+                                        info=other.info,
+                                        signing_payload=other.signing_payload)
+        else:
+            tx_msg = TransactionMessage(performative=performative,
+                                        skill_callback_ids=other.skill_callback_ids,
+                                        tx_id=other.tx_id,
+                                        tx_sender_addr=other.tx_sender_addr,
+                                        tx_counterparty_addr=other.tx_counterparty_addr,
+                                        tx_amount_by_currency_id=other.tx_amount_by_currency_id,
+                                        tx_sender_fee=other.tx_sender_fee,
+                                        tx_counterparty_fee=other.tx_counterparty_fee,
+                                        tx_quantities_by_good_id=other.tx_quantities_by_good_id,
+                                        ledger_id=other.ledger_id,
+                                        info=other.info,
+                                        signing_payload=other.signing_payload,
+                                        tx_signature=tx_signature)
+        return tx_msg
diff --git a/aea/helpers/base.py b/aea/helpers/base.py
index c67a0d5f39..6e0b3e511c 100644
--- a/aea/helpers/base.py
+++ b/aea/helpers/base.py
@@ -20,6 +20,8 @@
 """Miscellaneous helpers."""
 
 import builtins
+from typing import Optional
+
 import importlib.util
 import logging
 import os
@@ -68,3 +70,23 @@ def locate(path):
         except AttributeError:
             return None
     return object
+
+
+def generate_fingerprint(author: str, package_name: str, version: str, nonce: Optional[int] = None) -> str:
+    """Generate a unique id for the package.
+
+    :param author: The author of the package.
+    :param package_name: The name of the package
+    :param version: The version of the package.
+    :param nonce: Enable the developer to generate two different fingerprints for the same package.
+           (Can be used with different configuration)
+    """
+    import hashlib
+    if nonce is not None:
+        string_for_hash = "".join([author, package_name, version, str(nonce)])
+    else:
+        string_for_hash = "".join([author, package_name, version])
+    m_hash = hashlib.sha3_256()
+    m_hash.update(string_for_hash.encode())
+    encoded_str = m_hash.digest().hex()
+    return encoded_str
diff --git a/aea/helpers/dialogue/base.py b/aea/helpers/dialogue/base.py
index 5e6ab2bab9..d484c454fe 100644
--- a/aea/helpers/dialogue/base.py
+++ b/aea/helpers/dialogue/base.py
@@ -29,25 +29,26 @@
 from abc import abstractmethod
 from typing import Dict, List, Optional, Tuple, cast
 
+from aea.mail.base import Address
 from aea.protocols.base import Message
 
 
 class DialogueLabel:
     """The dialogue label class acts as an identifier for dialogues."""
 
-    def __init__(self, dialogue_reference: Tuple[str, str], dialogue_opponent_pbk: str, dialogue_starter_pbk: str) -> None:
+    def __init__(self, dialogue_reference: Tuple[str, str], dialogue_opponent_addr: Address, dialogue_starter_addr: Address) -> None:
         """
         Initialize a dialogue label.
 
         :param dialogue_reference: the reference of the dialogue.
-        :param dialogue_opponent_pbk: the pbk of the agent with which the dialogue is kept.
-        :param dialogue_starter_pbk: the pbk of the agent which started the dialogue.
+        :param dialogue_opponent_addr: the addr of the agent with which the dialogue is kept.
+        :param dialogue_starter_addr: the addr of the agent which started the dialogue.
 
         :return: None
         """
         self._dialogue_reference = dialogue_reference
-        self._dialogue_opponent_pbk = dialogue_opponent_pbk
-        self._dialogue_starter_pbk = dialogue_starter_pbk
+        self._dialogue_opponent_addr = dialogue_opponent_addr
+        self._dialogue_starter_addr = dialogue_starter_addr
 
     @property
     def dialogue_reference(self) -> Tuple[str, str]:
@@ -65,25 +66,25 @@ def dialogue_responder_reference(self) -> str:
         return self._dialogue_reference[1]
 
     @property
-    def dialogue_opponent_pbk(self) -> str:
-        """Get the public key of the dialogue opponent."""
-        return self._dialogue_opponent_pbk
+    def dialogue_opponent_addr(self) -> str:
+        """Get the address of the dialogue opponent."""
+        return self._dialogue_opponent_addr
 
     @property
-    def dialogue_starter_pbk(self) -> str:
-        """Get the public key of the dialogue starter."""
-        return self._dialogue_starter_pbk
+    def dialogue_starter_addr(self) -> str:
+        """Get the address of the dialogue starter."""
+        return self._dialogue_starter_addr
 
     def __eq__(self, other) -> bool:
         """Check for equality between two DialogueLabel objects."""
         if type(other) == DialogueLabel:
-            return self.dialogue_reference == other.dialogue_reference and self._dialogue_starter_pbk == other.dialogue_starter_pbk and self._dialogue_opponent_pbk == other.dialogue_opponent_pbk
+            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
         else:
             return False
 
     def __hash__(self) -> int:
         """Turn object into hash."""
-        return hash((self.dialogue_reference, self.dialogue_opponent_pbk, self.dialogue_starter_pbk))
+        return hash((self.dialogue_reference, self.dialogue_opponent_addr, self.dialogue_starter_addr))
 
     @property
     def json(self) -> Dict:
@@ -91,8 +92,8 @@ def json(self) -> Dict:
         return {
             "dialogue_starter_reference": self.dialogue_starter_reference,
             "dialogue_responder_reference": self.dialogue_responder_reference,
-            "dialogue_opponent_pbk": self.dialogue_opponent_pbk,
-            "dialogue_starter_pbk": self.dialogue_starter_pbk
+            "dialogue_opponent_addr": self.dialogue_opponent_addr,
+            "dialogue_starter_addr": self.dialogue_starter_addr
         }
 
     @classmethod
@@ -100,14 +101,15 @@ def from_json(cls, obj: Dict[str, str]) -> 'DialogueLabel':
         """Get dialogue label from json."""
         dialogue_label = DialogueLabel(
             (cast(str, obj.get('dialogue_starter_reference')), cast(str, obj.get('dialogue_responder_reference'))),
-            cast(str, obj.get('dialogue_opponent_pbk')),
-            cast(str, obj.get('dialogue_starter_pbk'))
+            cast(str, obj.get('dialogue_opponent_addr')),
+            cast(str, obj.get('dialogue_starter_addr'))
         )
         return dialogue_label
 
     def __str__(self):
         """Get the string representation."""
-        return "{}_{}_{}_{}".format(self.dialogue_starter_reference, self.dialogue_responder_reference, self.dialogue_opponent_pbk, self.dialogue_starter_pbk)
+        return "{}_{}_{}_{}".format(self.dialogue_starter_reference, self.dialogue_responder_reference,
+                                    self.dialogue_opponent_addr, self.dialogue_starter_addr)
 
 
 class Dialogue:
@@ -122,7 +124,7 @@ def __init__(self, dialogue_label: DialogueLabel) -> None:
         :return: None
         """
         self._dialogue_label = dialogue_label
-        self._is_self_initiated = dialogue_label.dialogue_opponent_pbk is not dialogue_label.dialogue_starter_pbk
+        self._is_self_initiated = dialogue_label.dialogue_opponent_addr is not dialogue_label.dialogue_starter_addr
         self._outgoing_messages = []  # type: List[Message]
         self._incoming_messages = []  # type: List[Message]
 
@@ -183,36 +185,33 @@ def dialogues(self) -> Dict[DialogueLabel, Dialogue]:
         return self._dialogues
 
     @abstractmethod
-    def is_permitted_for_new_dialogue(self, msg: Message, sender: str) -> bool:
+    def is_permitted_for_new_dialogue(self, msg: Message) -> bool:
         """
         Check whether an agent message is permitted for a new dialogue.
 
         :param msg: the agent message
-        :param sender: the address of the sender
 
         :return: a boolean indicating whether the message is permitted for a new dialogue
         """
 
     @abstractmethod
-    def is_belonging_to_registered_dialogue(self, msg: Message, sender: str, agent_pbk: str) -> bool:
+    def is_belonging_to_registered_dialogue(self, msg: Message, agent_addr: Address) -> bool:
         """
         Check whether an agent message is part of a registered dialogue.
 
         :param msg: the agent message
-        :param sender: the address of the sender
-        :param agent_pbk: the public key of the agent
+        :param agent_addr: the address of the agent
 
         :return: boolean indicating whether the message belongs to a registered dialogue
         """
 
     @abstractmethod
-    def get_dialogue(self, msg: Message, sender: str, agent_pbk: str) -> Dialogue:
+    def get_dialogue(self, msg: Message, agent_addr: Address) -> Dialogue:
         """
         Retrieve dialogue.
 
         :param msg: the agent message
-        :param sender: the address of the sender
-        :param agent_pbk: the public key of the agent
+        :param agent_addr: the address of the agent
 
         :return: the dialogue
         """
diff --git a/aea/helpers/preference_representations/base.py b/aea/helpers/preference_representations/base.py
index 87e9b96cef..4d20d48267 100644
--- a/aea/helpers/preference_representations/base.py
+++ b/aea/helpers/preference_representations/base.py
@@ -23,28 +23,28 @@
 from typing import Dict
 
 
-def logarithmic_utility(utility_params_by_good_pbk: Dict[str, float], quantities_by_good_pbk: Dict[str, int], quantity_shift: int = 1) -> float:
+def logarithmic_utility(utility_params_by_good_id: Dict[str, float], quantities_by_good_id: Dict[str, int], quantity_shift: int = 1) -> float:
     """
     Compute agent's utility given her utility function params and a good bundle.
 
-    :param utility_params_by_good_pbk: utility params by good identifier
-    :param quantities_by_good_pbk: quantities by good identifier
+    :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)
     :return: utility value
     """
     assert quantity_shift >= 0, "The quantity_shift argument must be a non-negative integer."
-    goodwise_utility = [utility_params_by_good_pbk[good_pbk] * math.log(quantity + quantity_shift) if quantity + quantity_shift > 0 else -10000
-                        for good_pbk, quantity in quantities_by_good_pbk.items()]
+    goodwise_utility = [utility_params_by_good_id[good_id] * math.log(quantity + quantity_shift) if quantity + quantity_shift > 0 else -10000
+                        for good_id, quantity in quantities_by_good_id.items()]
     return sum(goodwise_utility)
 
 
-def linear_utility(exchange_params_by_currency: Dict[str, float], balance_by_currency: Dict[str, int]) -> float:
+def linear_utility(exchange_params_by_currency_id: Dict[str, float], balance_by_currency_id: Dict[str, int]) -> float:
     """
     Compute agent's utility given her utility function params and a good bundle.
 
-    :param exchange_params_by_currency: exchange params by currency
-    :param balance_by_currency: balance by currency
+    :param exchange_params_by_currency_id: exchange params by currency
+    :param balance_by_currency_id: balance by currency
     :return: utility value
     """
-    money_utility = [exchange_params_by_currency[currency] * balance for currency, balance in balance_by_currency.items()]
+    money_utility = [exchange_params_by_currency_id[currency_id] * balance for currency_id, balance in balance_by_currency_id.items()]
     return sum(money_utility)
diff --git a/aea/helpers/search/__init__.py b/aea/helpers/search/__init__.py
new file mode 100644
index 0000000000..6436b560e0
--- /dev/null
+++ b/aea/helpers/search/__init__.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# ------------------------------------------------------------------------------
+#
+#   Copyright 2018-2019 Fetch.AI Limited
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+#
+# ------------------------------------------------------------------------------
+
+"""This module contains search related classes."""
diff --git a/aea/protocols/oef/models.py b/aea/helpers/search/models.py
similarity index 96%
rename from aea/protocols/oef/models.py
rename to aea/helpers/search/models.py
index 0816c0da67..c4d5f8302f 100644
--- a/aea/protocols/oef/models.py
+++ b/aea/helpers/search/models.py
@@ -18,7 +18,7 @@
 #
 # ------------------------------------------------------------------------------
 
-"""Useful classes for the OEF protocol."""
+"""Useful classes for the OEF search."""
 from abc import ABC, abstractmethod
 from copy import deepcopy
 from enum import Enum
@@ -27,28 +27,6 @@
 ATTRIBUTE_TYPES = Union[float, str, bool, int]
 
 
-class JSONSerializable(ABC):
-    """Interface for JSON-serializable objects."""
-
-    @abstractmethod
-    def to_json(self) -> Dict:
-        """
-        Return the JSON representation of the object.
-
-        :return: the JSON object.
-        """
-
-    @classmethod
-    @abstractmethod
-    def from_json(cls, d: Dict) -> Any:
-        """
-        Parse the JSON representation of the object.
-
-        :param d: the JSON object.
-        :return: the equivalent Python object.
-        """
-
-
 class Attribute:
     """Implements an attribute for an OEF data model."""
 
diff --git a/aea/mail/base.py b/aea/mail/base.py
index c457503e80..9360744ffe 100644
--- a/aea/mail/base.py
+++ b/aea/mail/base.py
@@ -26,8 +26,9 @@
 from concurrent.futures import Future
 from threading import Thread, Lock
 from typing import Optional, TYPE_CHECKING, List, Tuple, Dict, cast
+from urllib.parse import urlparse
 
-from aea.configurations.base import Address, ProtocolId
+from aea.configurations.base import ProtocolId
 from aea.connections.base import ConnectionStatus
 from aea.mail import base_pb2
 
@@ -37,6 +38,9 @@
 logger = logging.getLogger(__name__)
 
 
+Address = str
+
+
 class AEAConnectionError(Exception):
     """Exception class for connection errors."""
 
@@ -45,17 +49,116 @@ class Empty(Exception):
     """Exception for when the inbox is empty."""
 
 
+class URI:
+    """URI following RFC3986."""
+
+    def __init__(self, uri_raw: str):
+        """
+        Initialize the URI.
+
+        :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
+
+    @property
+    def scheme(self) -> str:
+        """Get the scheme."""
+        return self._scheme
+
+    @property
+    def netloc(self) -> str:
+        """Get the netloc."""
+        return self._netloc
+
+    @property
+    def path(self) -> str:
+        """Get the path."""
+        return self._path
+
+    @property
+    def params(self) -> str:
+        """Get the params."""
+        return self._params
+
+    @property
+    def query(self) -> str:
+        """Get the query."""
+        return self._query
+
+    @property
+    def fragment(self) -> str:
+        """Get the fragment."""
+        return self._fragment
+
+    @property
+    def username(self) -> Optional[str]:
+        """Get the username."""
+        return self._username
+
+    @property
+    def password(self) -> Optional[str]:
+        """Get the password."""
+        return self._password
+
+    @property
+    def host(self) -> Optional[str]:
+        """Get the host."""
+        return self._host
+
+    @property
+    def port(self) -> Optional[int]:
+        """Get the port."""
+        return self._port
+
+    def __str__(self):
+        """Get string representation."""
+        return self.uri_raw
+
+    def __eq__(self, other):
+        """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
+
+
 class EnvelopeContext:
     """Extra information for the handling of an envelope."""
 
-    def __init__(self, connection_id: Optional[str] = None):
+    def __init__(self, connection_id: Optional[str] = None, uri: Optional[URI] = None):
         """Initialize the envelope context."""
         self.connection_id = connection_id
+        self.uri = uri  # must follow: https://tools.ietf.org/html/rfc3986.html
+
+    @property
+    def uri_raw(self) -> str:
+        """Get uri in string format."""
+        return str(self.uri)
 
     def __eq__(self, other):
         """Compare with another object."""
         return isinstance(other, EnvelopeContext) \
-            and self.connection_id == other.connection_id
+            and self.connection_id == other.connection_id \
+            and self.uri == other.uri
 
 
 class EnvelopeSerializer(ABC):
@@ -115,8 +218,8 @@ def __init__(self, to: Address,
         """
         Initialize a Message object.
 
-        :param to: the public key of the receiver.
-        :param sender: the public key of the sender.
+        :param to: the address of the receiver.
+        :param sender: the address of the sender.
         :param protocol_id: the protocol id.
         :param message: the protocol-specific message
         """
@@ -128,22 +231,22 @@ def __init__(self, to: Address,
 
     @property
     def to(self) -> Address:
-        """Get public key of receiver."""
+        """Get address of receiver."""
         return self._to
 
     @to.setter
     def to(self, to: Address) -> None:
-        """Set public key of receiver."""
+        """Set address of receiver."""
         self._to = to
 
     @property
     def sender(self) -> Address:
-        """Get public key of sender."""
+        """Get address of sender."""
         return self._sender
 
     @sender.setter
     def sender(self, sender: Address) -> None:
-        """Set public key of sender."""
+        """Set address of sender."""
         self._sender = sender
 
     @property
@@ -481,7 +584,7 @@ async def _send(self, envelope: Envelope) -> None:
 
         try:
             await connection.send(envelope)
-        except Exception as e:
+        except Exception as e:  # pragma: no cover
             raise e
 
     def get(self, block: bool = False, timeout: Optional[float] = None) -> Optional[Envelope]:
@@ -508,7 +611,7 @@ def put(self, envelope: Envelope) -> None:
         :return: None
         """
         fut = asyncio.run_coroutine_threadsafe(self.out_queue.put(envelope), self._loop)
-        return fut.result()
+        fut.result()
 
 
 class InBox(object):
diff --git a/aea/protocols/__init__.py b/aea/protocols/__init__.py
index 669a6fc3c6..339744401a 100644
--- a/aea/protocols/__init__.py
+++ b/aea/protocols/__init__.py
@@ -21,5 +21,3 @@
 from typing import List
 
 default_dependencies = []  # type: List[str]
-fipa_dependencies = ["protobuf"]  # type: List[str]
-oef_dependencies = ["colorlog", "oef"]  # type: List[str]
diff --git a/aea/protocols/base.py b/aea/protocols/base.py
index 3ea0a42269..3b7787ba0f 100644
--- a/aea/protocols/base.py
+++ b/aea/protocols/base.py
@@ -28,6 +28,7 @@
 from google.protobuf.struct_pb2 import Struct
 
 from aea.configurations.base import ProtocolConfig
+from aea.mail.base import Address
 
 
 class Message:
@@ -43,9 +44,25 @@ def __init__(self, body: Optional[Dict] = None,
         :param body: the dictionary of values to hold.
         :param kwargs: any additional value to add to the body. It will overwrite the body values.
         """
+        self._counterparty = None  # type: Optional[Address]
         self._body = copy(body) if body else {}  # type: Dict[str, Any]
         self._body.update(kwargs)
 
+    @property
+    def counterparty(self) -> Address:
+        """
+        Get the counterparty of the message in Address form.
+
+        :return the address
+        """
+        assert self._counterparty is not None, "Counterparty must not be None."
+        return self._counterparty
+
+    @counterparty.setter
+    def counterparty(self, counterparty: Address) -> None:
+        """Set the counterparty of the message."""
+        self._counterparty = counterparty
+
     @property
     def body(self) -> Dict:
         """
@@ -94,7 +111,8 @@ def check_consistency(self) -> bool:
     def __eq__(self, other):
         """Compare with another object."""
         return isinstance(other, Message) \
-            and self.body == other.body
+            and self.body == other.body \
+            and self._counterparty == other._counterparty
 
     def __str__(self):
         """Get the string representation of the message."""
diff --git a/aea/protocols/default/message.py b/aea/protocols/default/message.py
index 8baa2f2d19..885e1b61c6 100644
--- a/aea/protocols/default/message.py
+++ b/aea/protocols/default/message.py
@@ -20,7 +20,7 @@
 
 """This module contains the default message definition."""
 from enum import Enum
-from typing import Optional
+from typing import cast, Dict, Any
 
 from aea.protocols.base import Message
 
@@ -49,7 +49,7 @@ class ErrorCode(Enum):
         UNSUPPORTED_SKILL = -10004
         INVALID_DIALOGUE = -10005
 
-    def __init__(self, type: Optional[Type] = None,
+    def __init__(self, type: Type,
                  **kwargs):
         """
         Initialize.
@@ -59,20 +59,49 @@ def __init__(self, type: Optional[Type] = None,
         super().__init__(type=type, **kwargs)
         assert self.check_consistency(), "DefaultMessage initialization inconsistent."
 
+    @property
+    def type(self) -> Type:  # noqa: F821
+        """Get the type of the message."""
+        assert self.is_set("type"), "type is not set"
+        return DefaultMessage.Type(self.get("type"))
+
+    @property
+    def content(self) -> bytes:
+        """Get the content of the message."""
+        assert self.is_set("content"), "content is not set!"
+        return cast(bytes, self.get("content"))
+
+    @property
+    def error_code(self) -> ErrorCode:  # noqa: F821
+        """Get the error_code of the message."""
+        assert self.is_set("error_code"), "error_code is not set"
+        return DefaultMessage.ErrorCode(self.get("error_code"))
+
+    @property
+    def error_msg(self) -> str:
+        """Get the error message."""
+        assert self.is_set("error_msg"), "error_msg is not set"
+        return cast(str, self.get("error_msg"))
+
+    @property
+    def error_data(self) -> Dict[str, Any]:
+        """Get the data of the error message."""
+        assert self.is_set("error_data"), "error_data is not set."
+        return cast(Dict[str, Any], self.get("error_data"))
+
     def check_consistency(self) -> bool:
         """Check that the data is consistent."""
         try:
-            ttype = DefaultMessage.Type(self.get("type"))
-            if ttype == DefaultMessage.Type.BYTES:
-                assert self.is_set("content")
-                content = self.get("content")
-                assert isinstance(content, bytes)
-            elif ttype == DefaultMessage.Type.ERROR:
-                assert self.is_set("error_code")
-                error_code = DefaultMessage.ErrorCode(self.get("error_code"))
-                assert error_code in DefaultMessage.ErrorCode
-                assert self.is_set("error_msg")
-                assert self.is_set("error_data")
+            assert isinstance(self.type, DefaultMessage.Type)
+            if self.type == DefaultMessage.Type.BYTES:
+                assert isinstance(self.content, bytes), "Expect the content to be bytes"
+                assert len(self.body) == 2
+            elif self.type == DefaultMessage.Type.ERROR:
+                assert self.error_code in DefaultMessage.ErrorCode, "ErrorCode is not valid"
+                assert isinstance(self.error_code, DefaultMessage.ErrorCode), "error_code has wrong type."
+                assert isinstance(self.error_msg, str), "error_msg should be str"
+                assert isinstance(self.error_data, dict), "error_data should be dict"
+                assert len(self.body) == 4
             else:
                 raise ValueError("Type not recognized.")
 
diff --git a/aea/protocols/default/protocol.yaml b/aea/protocols/default/protocol.yaml
index b07cec7c60..15940ced51 100644
--- a/aea/protocols/default/protocol.yaml
+++ b/aea/protocols/default/protocol.yaml
@@ -1,7 +1,8 @@
 name: 'default'
-authors: Fetch.AI Limited
+author: fetchai
 version: 0.1.0
 license: Apache 2.0
+fingerprint: ""
 url: ""
 description: "The default protocol allows for any bytes message."
-dependencies: []
\ No newline at end of file
+dependencies: {}
diff --git a/aea/protocols/default/serialization.py b/aea/protocols/default/serialization.py
index 080b8f386b..c2da39819d 100644
--- a/aea/protocols/default/serialization.py
+++ b/aea/protocols/default/serialization.py
@@ -33,18 +33,16 @@ class DefaultSerializer(Serializer):
 
     def encode(self, msg: Message) -> bytes:
         """Encode a 'default' message into bytes."""
+        msg = cast(DefaultMessage, msg)
         body = {}  # Dict[str, Any]
+        body["type"] = msg.type.value
 
-        msg_type = DefaultMessage.Type(msg.get("type"))
-        body["type"] = str(msg_type.value)
-
-        if msg_type == DefaultMessage.Type.BYTES:
-            content = cast(bytes, msg.get("content"))
-            body["content"] = base64.b64encode(content).decode("utf-8")
-        elif msg_type == DefaultMessage.Type.ERROR:
-            body["error_code"] = cast(str, msg.get("error_code"))
-            body["error_msg"] = cast(str, msg.get("error_msg"))
-            body["error_data"] = cast(str, msg.get("error_data"))
+        if msg.type == DefaultMessage.Type.BYTES:
+            body["content"] = base64.b64encode(msg.content).decode("utf-8")
+        elif msg.type == DefaultMessage.Type.ERROR:
+            body["error_code"] = msg.error_code.value
+            body["error_msg"] = msg.error_msg
+            body["error_data"] = msg.error_data
         else:
             raise ValueError("Type not recognized.")
 
diff --git a/aea/protocols/fipa/README.md b/aea/protocols/fipa/README.md
deleted file mode 100644
index 70b80ac297..0000000000
--- a/aea/protocols/fipa/README.md
+++ /dev/null
@@ -1,6 +0,0 @@
-# fipa protocol
-
-## To update the `fipa_pb2.py` file
-
-	  cd aea/protocols/fipa
-	  protoc --python_out=. fipa.proto
\ No newline at end of file
diff --git a/aea/protocols/fipa/message.py b/aea/protocols/fipa/message.py
deleted file mode 100644
index 7941becb82..0000000000
--- a/aea/protocols/fipa/message.py
+++ /dev/null
@@ -1,127 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# ------------------------------------------------------------------------------
-#
-#   Copyright 2018-2019 Fetch.AI Limited
-#
-#   Licensed under the Apache License, Version 2.0 (the "License");
-#   you may not use this file except in compliance with the License.
-#   You may obtain a copy of the License at
-#
-#       http://www.apache.org/licenses/LICENSE-2.0
-#
-#   Unless required by applicable law or agreed to in writing, software
-#   distributed under the License is distributed on an "AS IS" BASIS,
-#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#   See the License for the specific language governing permissions and
-#   limitations under the License.
-#
-# ------------------------------------------------------------------------------
-
-"""This module contains the FIPA message definition."""
-from enum import Enum
-from typing import Dict, List, Optional, Tuple, Union, cast
-
-from aea.protocols.base import Message
-from aea.protocols.oef.models import Description, Query
-
-
-class FIPAMessage(Message):
-    """The FIPA message class."""
-
-    protocol_id = "fipa"
-
-    STARTING_MESSAGE_ID = 1
-    STARTING_TARGET = 0
-
-    class Performative(Enum):
-        """FIPA performatives."""
-
-        CFP = "cfp"
-        PROPOSE = "propose"
-        ACCEPT = "accept"
-        MATCH_ACCEPT = "match_accept"
-        DECLINE = "decline"
-        INFORM = "inform"
-        ACCEPT_W_ADDRESS = "accept_w_address"
-        MATCH_ACCEPT_W_ADDRESS = "match_accept_w_address"
-
-        def __str__(self):
-            """Get string representation."""
-            return self.value
-
-    def __init__(self, dialogue_reference: Tuple[str, str] = None,
-                 message_id: Optional[int] = None,
-                 target: Optional[int] = None,
-                 performative: Optional[Union[str, Performative]] = None,
-                 **kwargs):
-        """
-        Initialize.
-
-        :param message_id: the message id.
-        :param dialogue_reference: the dialogue reference.
-        :param target: the message target.
-        :param performative: the message performative.
-        """
-        super().__init__(message_id=message_id,
-                         dialogue_reference=dialogue_reference,
-                         target=target,
-                         performative=FIPAMessage.Performative(performative),
-                         **kwargs)
-        assert self.check_consistency(), "FIPAMessage initialization inconsistent."
-
-    def check_consistency(self) -> bool:
-        """Check that the data is consistent."""
-        try:
-            assert self.is_set("dialogue_reference")
-            dialogue_reference = self.get("dialogue_reference")
-            assert type(dialogue_reference) == tuple
-            dialogue_reference = cast(Tuple, dialogue_reference)
-            assert type(dialogue_reference[0]) == str and type(dialogue_reference[0]) == str
-            assert self.is_set("message_id")
-            assert type(self.get("message_id")) == int
-            assert self.is_set("target")
-            assert type(self.get("target")) == int
-            performative = FIPAMessage.Performative(self.get("performative"))
-            if performative == FIPAMessage.Performative.CFP:
-                assert self.is_set("query")
-                query = self.get("query")
-                assert isinstance(query, Query) or isinstance(query, bytes) or query is None
-                assert len(self.body) == 5
-            elif performative == FIPAMessage.Performative.PROPOSE:
-                assert self.is_set("proposal")
-                proposal = self.get("proposal")
-                assert type(proposal) == list and all(isinstance(d, Description) or type(d) == bytes for d in proposal)  # type: ignore
-                assert len(self.body) == 5
-            elif performative == FIPAMessage.Performative.ACCEPT \
-                    or performative == FIPAMessage.Performative.MATCH_ACCEPT \
-                    or performative == FIPAMessage.Performative.DECLINE:
-                assert len(self.body) == 4
-            elif performative == FIPAMessage.Performative.ACCEPT_W_ADDRESS\
-                    or performative == FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS:
-                assert self.is_set("address")
-                assert len(self.body) == 5
-            elif performative == FIPAMessage.Performative.INFORM:
-                assert self.is_set("json_data")
-                json_data = self.get("json_data")
-                assert isinstance(json_data, dict)
-                assert len(self.body) == 5
-            else:
-                raise ValueError("Performative not recognized.")
-
-        except (AssertionError, ValueError, KeyError):
-            return False
-
-        return True
-
-
-VALID_PREVIOUS_PERFORMATIVES = {
-    FIPAMessage.Performative.CFP: [None],
-    FIPAMessage.Performative.PROPOSE: [FIPAMessage.Performative.CFP],
-    FIPAMessage.Performative.ACCEPT: [FIPAMessage.Performative.PROPOSE],
-    FIPAMessage.Performative.ACCEPT_W_ADDRESS: [FIPAMessage.Performative.PROPOSE],
-    FIPAMessage.Performative.MATCH_ACCEPT: [FIPAMessage.Performative.ACCEPT, FIPAMessage.Performative.ACCEPT_W_ADDRESS],
-    FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS: [FIPAMessage.Performative.ACCEPT, FIPAMessage.Performative.ACCEPT_W_ADDRESS],
-    FIPAMessage.Performative.INFORM: [FIPAMessage.Performative.MATCH_ACCEPT, FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS, FIPAMessage.Performative.INFORM],
-    FIPAMessage.Performative.DECLINE: [FIPAMessage.Performative.CFP, FIPAMessage.Performative.PROPOSE, FIPAMessage.Performative.ACCEPT, FIPAMessage.Performative.ACCEPT_W_ADDRESS]
-}  # type: Dict[FIPAMessage.Performative, List[Union[None, FIPAMessage.Performative]]]
diff --git a/aea/protocols/oef/message.py b/aea/protocols/oef/message.py
deleted file mode 100644
index 64cab90c54..0000000000
--- a/aea/protocols/oef/message.py
+++ /dev/null
@@ -1,145 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# ------------------------------------------------------------------------------
-#
-#   Copyright 2018-2019 Fetch.AI Limited
-#
-#   Licensed under the Apache License, Version 2.0 (the "License");
-#   you may not use this file except in compliance with the License.
-#   You may obtain a copy of the License at
-#
-#       http://www.apache.org/licenses/LICENSE-2.0
-#
-#   Unless required by applicable law or agreed to in writing, software
-#   distributed under the License is distributed on an "AS IS" BASIS,
-#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#   See the License for the specific language governing permissions and
-#   limitations under the License.
-#
-# ------------------------------------------------------------------------------
-
-"""This module contains the default message definition."""
-from enum import Enum
-from typing import Optional, List, cast
-
-from aea.protocols.base import Message
-from aea.protocols.oef.models import Description, Query
-
-
-class OEFMessage(Message):
-    """The OEF message class."""
-
-    protocol_id = "oef"
-
-    class Type(Enum):
-        """OEF Message types."""
-
-        REGISTER_SERVICE = "register_service"
-        REGISTER_AGENT = "register_agent"
-        UNREGISTER_SERVICE = "unregister_service"
-        UNREGISTER_AGENT = "unregister_agent"
-        SEARCH_SERVICES = "search_services"
-        SEARCH_AGENTS = "search_agents"
-        OEF_ERROR = "oef_error"
-        DIALOGUE_ERROR = "dialogue_error"
-        SEARCH_RESULT = "search_result"
-
-        def __str__(self):
-            """Get string representation."""
-            return self.value
-
-    class OEFErrorOperation(Enum):
-        """Operation code for the OEF. It is returned in the OEF Error messages."""
-
-        REGISTER_SERVICE = 0
-        UNREGISTER_SERVICE = 1
-        SEARCH_SERVICES = 2
-        SEARCH_SERVICES_WIDE = 3
-        SEARCH_AGENTS = 4
-        SEND_MESSAGE = 5
-        REGISTER_AGENT = 6
-        UNREGISTER_AGENT = 7
-
-        OTHER = 10000
-
-        def __str__(self):
-            """Get string representation."""
-            return str(self.value)
-
-    def __init__(self, oef_type: Optional[Type] = None,
-                 **kwargs):
-        """
-        Initialize.
-
-        :param oef_type: the type of OEF message.
-        """
-        super().__init__(type=oef_type, **kwargs)
-        assert self.check_consistency(), "OEFMessage initialization inconsistent."
-
-    def check_consistency(self) -> bool:
-        """Check that the data is consistent."""
-        try:
-            assert self.is_set("type")
-            oef_type = OEFMessage.Type(self.get("type"))
-            if oef_type == OEFMessage.Type.REGISTER_SERVICE:
-                assert self.is_set("id")
-                assert self.is_set("service_description")
-                assert self.is_set("service_id")
-                service_description = self.get("service_description")
-                service_id = self.get("service_id")
-                assert isinstance(service_description, Description)
-                assert isinstance(service_id, str)
-            elif oef_type == OEFMessage.Type.REGISTER_AGENT:
-                assert self.is_set("id")
-                assert self.is_set("agent_description")
-                assert self.is_set("agent_id")
-                agent_description = self.get("agent_description")
-                agent_id = self.get("agent_id")
-                assert isinstance(agent_description, Description)
-                assert isinstance(agent_id, str)
-            elif oef_type == OEFMessage.Type.UNREGISTER_SERVICE:
-                assert self.is_set("id")
-                assert self.is_set("service_description")
-                assert self.is_set("service_id")
-                service_description = self.get("service_description")
-                service_id = self.get("service_id")
-                assert isinstance(service_description, Description)
-                assert isinstance(service_id, str)
-            elif oef_type == OEFMessage.Type.UNREGISTER_AGENT:
-                assert self.is_set("id")
-                assert self.is_set("agent_description")
-                assert self.is_set("agent_id")
-                agent_description = self.get("agent_description")
-                agent_id = self.get("agent_id")
-                assert isinstance(agent_description, Description)
-                assert isinstance(agent_id, str)
-            elif oef_type == OEFMessage.Type.SEARCH_SERVICES:
-                assert self.is_set("id")
-                assert self.is_set("query")
-                query = self.get("query")
-                assert isinstance(query, Query)
-            elif oef_type == OEFMessage.Type.SEARCH_AGENTS:
-                assert self.is_set("id")
-                assert self.is_set("query")
-                query = self.get("query")
-                assert isinstance(query, Query)
-            elif oef_type == OEFMessage.Type.SEARCH_RESULT:
-                assert self.is_set("id")
-                assert self.is_set("agents")
-                agents = cast(List[str], self.get("agents"))
-                assert type(agents) == list and all(type(a) == str for a in agents)
-            elif oef_type == OEFMessage.Type.OEF_ERROR:
-                assert self.is_set("id")
-                assert self.is_set("operation")
-                operation = self.get("operation")
-                assert operation in set(OEFMessage.OEFErrorOperation)
-            elif oef_type == OEFMessage.Type.DIALOGUE_ERROR:
-                assert self.is_set("id")
-                assert self.is_set("dialogue_id")
-                assert self.is_set("origin")
-            else:
-                raise ValueError("Type not recognized.")
-        except (AssertionError, ValueError):
-            return False
-
-        return True
diff --git a/aea/protocols/scaffold/message.py b/aea/protocols/scaffold/message.py
index d05bf86bb6..8015553520 100644
--- a/aea/protocols/scaffold/message.py
+++ b/aea/protocols/scaffold/message.py
@@ -21,7 +21,6 @@
 """This module contains the scaffold message definition."""
 
 from enum import Enum
-from typing import Optional
 
 from aea.protocols.base import Message
 
@@ -31,20 +30,21 @@ class MyScaffoldMessage(Message):
 
     protocol_id = "my_scaffold_protocol"
 
-    class Type(Enum):
+    class Performative(Enum):
         """Scaffold Message types."""
 
         def __str__(self):
             """Get string representation."""
             return self.value   # pragma: no cover
 
-    def __init__(self, oef_type: Optional[Type] = None, **kwargs):
+    def __init__(self, performative: Performative,
+                 **kwargs):
         """
         Initialize.
 
-        :param oef_type: the type of message.
+        :param performative: the type of message.
         """
-        super().__init__(type=oef_type, **kwargs)
+        super().__init__(performative=performative, **kwargs)
         assert self.check_consistency(), "MyScaffoldMessage initialization inconsistent."
 
     def check_consistency(self) -> bool:
diff --git a/aea/protocols/scaffold/protocol.yaml b/aea/protocols/scaffold/protocol.yaml
index 62dc31060d..949f1b3690 100644
--- a/aea/protocols/scaffold/protocol.yaml
+++ b/aea/protocols/scaffold/protocol.yaml
@@ -1,6 +1,7 @@
-name: 'scaffold_protocol'
-authors: Fetch.AI Limited
+name: 'scaffold'
+author: fetchai
 version: 0.1.0
 license: Apache 2.0
+fingerprint: ""
 url: ""
 description: "The scaffold protocol scaffolds a protocol to be implemented by the developer."
diff --git a/aea/registries/base.py b/aea/registries/base.py
index 8b3d28cca8..e7e60608d9 100644
--- a/aea/registries/base.py
+++ b/aea/registries/base.py
@@ -33,7 +33,7 @@
 from aea.configurations.base import ProtocolId, SkillId, ProtocolConfig, DEFAULT_PROTOCOL_CONFIG_FILE
 from aea.configurations.loader import ConfigLoader
 from aea.decision_maker.messages.transaction import TransactionMessage
-from aea.protocols.base import Protocol
+from aea.protocols.base import Protocol, Message
 from aea.skills.base import Handler, Behaviour, Task, Skill, AgentContext
 
 logger = logging.getLogger(__name__)
@@ -509,11 +509,11 @@ def add_skill(self, skill: Skill):
         skill_id = skill.config.name
         self._skills[skill_id] = skill
         if skill.handlers is not None:
-            self.handler_registry.register((None, skill_id), cast(List[Handler], skill.handlers))
+            self.handler_registry.register((None, skill_id), cast(List[Handler], list(skill.handlers.values())))
         if skill.behaviours is not None:
-            self.behaviour_registry.register((None, skill_id), cast(List[Behaviour], skill.behaviours))
+            self.behaviour_registry.register((None, skill_id), cast(List[Behaviour], list(skill.behaviours.values())))
         if skill.tasks is not None:
-            self.task_registry.register((None, skill_id), cast(List[Task], skill.tasks))
+            self.task_registry.register((None, skill_id), cast(List[Task], list(skill.tasks.values())))
 
     def get_skill(self, skill_id: SkillId) -> Optional[Skill]:
         """Get the skill."""
@@ -599,8 +599,7 @@ def get_active_behaviours(self) -> List[Behaviour]:
         :return: the list of behaviours currently active
         """
         behaviours = self.resources.behaviour_registry.fetch_all()
-        # TODO: add filtering, remove inactive behaviours
-        return behaviours
+        return [b for b in behaviours if not b.done()]
 
     def handle_internal_messages(self) -> None:
         """
@@ -611,7 +610,11 @@ def handle_internal_messages(self) -> None:
         while not self.decision_maker_out_queue.empty():
             tx_message = self.decision_maker_out_queue.get_nowait()  # type: Optional[TransactionMessage]
             if tx_message is not None:
-                skill_id = cast(str, tx_message.get("skill_id"))
-                handler = self.resources.handler_registry.fetch_internal_handler(skill_id)
-                if handler is not None:
-                    handler.handle(tx_message, DECISION_MAKER)
+                skill_callback_ids = tx_message.skill_callback_ids
+                for skill_id in skill_callback_ids:
+                    handler = self.resources.handler_registry.fetch_internal_handler(skill_id)
+                    if handler is not None:
+                        logger.debug("Calling handler {} of skill {}".format(type(handler), skill_id))
+                        handler.handle(cast(Message, tx_message))
+                    else:
+                        logger.warning("No internal handler fetched for skill_id={}".format(skill_id))
diff --git a/aea/skills/base.py b/aea/skills/base.py
index 7495843001..214c3cf7b4 100644
--- a/aea/skills/base.py
+++ b/aea/skills/base.py
@@ -26,12 +26,13 @@
 from abc import ABC, abstractmethod
 from pathlib import Path
 from queue import Queue
-from typing import Optional, List, Dict, Any, cast
+from types import SimpleNamespace
+from typing import Optional, Dict, Any, cast
 
-from aea.connections.base import ConnectionStatus
 from aea.configurations.base import BehaviourConfig, HandlerConfig, TaskConfig, SharedClassConfig, SkillConfig, \
     ProtocolId, DEFAULT_SKILL_CONFIG_FILE
 from aea.configurations.loader import ConfigLoader
+from aea.connections.base import ConnectionStatus
 from aea.context.base import AgentContext
 from aea.crypto.ledger_apis import LedgerApis
 from aea.decision_maker.base import OwnershipState, Preferences, GoalPursuitReadiness
@@ -54,6 +55,11 @@ def __init__(self, agent_context: AgentContext):
         self._in_queue = Queue()  # type: Queue
         self._skill = None  # type: Optional[Skill]
 
+    @property
+    def shared_state(self) -> Dict[str, Any]:
+        """Get the shared state dictionary."""
+        return self._agent_context.shared_state
+
     @property
     def agent_name(self) -> str:
         """Get agent name."""
@@ -120,22 +126,29 @@ def ledger_apis(self) -> LedgerApis:
         return self._agent_context.ledger_apis
 
     @property
-    def handlers(self) -> Optional[List['Handler']]:
+    def task_queue(self) -> Queue:
+        """Get the task queue."""
+        # TODO this is potentially dangerous - it exposes the task queue to other skills
+        #      such that other skills can modify it.
+        return self._agent_context.task_queue
+
+    @property
+    def handlers(self) -> SimpleNamespace:
         """Get handlers of the skill."""
         assert self._skill is not None, "Skill not initialized."
-        return self._skill.handlers
+        return SimpleNamespace(**self._skill.handlers)
 
     @property
-    def behaviours(self) -> Optional[List['Behaviour']]:
+    def behaviours(self) -> SimpleNamespace:
         """Get behaviours of the skill."""
         assert self._skill is not None, "Skill not initialized."
-        return self._skill.behaviours
+        return SimpleNamespace(**self._skill.behaviours)
 
     @property
-    def tasks(self) -> Optional[List['Task']]:
+    def tasks(self) -> SimpleNamespace:
         """Get tasks of the skill."""
         assert self._skill is not None, "Skill not initialized."
-        return self._skill.tasks
+        return SimpleNamespace(**self._skill.tasks)
 
     def __getattr__(self, item) -> Any:
         """Get attribute."""
@@ -189,8 +202,16 @@ def teardown(self) -> None:
         :return: None
         """
 
+    def done(self) -> bool:
+        """Return True if the behaviour is terminated, False otherwise."""
+        return False
+
+    def act_wrapper(self) -> None:
+        """Wrap the call of the action. This method must be called only by the framework."""
+        self.act()
+
     @classmethod
-    def parse_module(cls, path: str, behaviours_configs: List[BehaviourConfig], skill_context: SkillContext) -> List['Behaviour']:
+    def parse_module(cls, path: str, behaviours_configs: Dict[str, BehaviourConfig], skill_context: SkillContext) -> Dict[str, 'Behaviour']:
         """
         Parse the behaviours module.
 
@@ -199,7 +220,7 @@ def parse_module(cls, path: str, behaviours_configs: List[BehaviourConfig], skil
         :param skill_context: the skill context
         :return: a list of Behaviour.
         """
-        behaviours = []
+        behaviours = {}
         behaviours_spec = importlib.util.spec_from_file_location("behaviours", location=path)
         behaviour_module = importlib.util.module_from_spec(behaviours_spec)
         behaviours_spec.loader.exec_module(behaviour_module)  # type: ignore
@@ -207,9 +228,10 @@ def parse_module(cls, path: str, behaviours_configs: List[BehaviourConfig], skil
         behaviours_classes = list(filter(lambda x: re.match("\\w+Behaviour", x[0]), classes))
 
         name_to_class = dict(behaviours_classes)
-        for behaviour_config in behaviours_configs:
+        for behaviour_id, behaviour_config in behaviours_configs.items():
             behaviour_class_name = cast(str, behaviour_config.class_name)
             logger.debug("Processing behaviour {}".format(behaviour_class_name))
+            assert behaviour_id.isidentifier(), "'{}' is not a valid identifier.".format(behaviour_id)
             behaviour_class = name_to_class.get(behaviour_class_name, None)
             if behaviour_class is None:
                 logger.warning("Behaviour '{}' cannot be found.".format(behaviour_class_name))
@@ -218,7 +240,7 @@ def parse_module(cls, path: str, behaviours_configs: List[BehaviourConfig], skil
                 assert 'skill_context' not in args.keys(), "'skill_context' is a reserved key. Please rename your arguments!"
                 args['skill_context'] = skill_context
                 behaviour = behaviour_class(**args)
-                behaviours.append(behaviour)
+                behaviours[behaviour_id] = behaviour
 
         return behaviours
 
@@ -249,12 +271,11 @@ def config(self) -> Dict[Any, Any]:
         return self._config
 
     @abstractmethod
-    def handle(self, message: Message, sender: str) -> None:
+    def handle(self, message: Message) -> None:
         """
         Implement the reaction to a message.
 
         :param message: the message
-        :param sender: the sender
         :return: None
         """
 
@@ -275,7 +296,7 @@ def teardown(self) -> None:
         """
 
     @classmethod
-    def parse_module(cls, path: str, handler_configs: List[HandlerConfig], skill_context: SkillContext) -> List['Handler']:
+    def parse_module(cls, path: str, handler_configs: Dict[str, HandlerConfig], skill_context: SkillContext) -> Dict[str, 'Handler']:
         """
         Parse the handler module.
 
@@ -284,7 +305,7 @@ def parse_module(cls, path: str, handler_configs: List[HandlerConfig], skill_con
         :param skill_context: the skill context
         :return: an handler, or None if the parsing fails.
         """
-        handlers = []
+        handlers = {}
         handler_spec = importlib.util.spec_from_file_location("handlers", location=path)
         handler_module = importlib.util.module_from_spec(handler_spec)
         handler_spec.loader.exec_module(handler_module)  # type: ignore
@@ -292,9 +313,10 @@ def parse_module(cls, path: str, handler_configs: List[HandlerConfig], skill_con
         handler_classes = list(filter(lambda x: re.match("\\w+Handler", x[0]), classes))
 
         name_to_class = dict(handler_classes)
-        for handler_config in handler_configs:
+        for handler_id, handler_config in handler_configs.items():
             handler_class_name = cast(str, handler_config.class_name)
             logger.debug("Processing handler {}".format(handler_class_name))
+            assert handler_id.isidentifier(), "'{}' is not a valid identifier.".format(handler_id)
             handler_class = name_to_class.get(handler_class_name, None)
             if handler_class is None:
                 logger.warning("Handler '{}' cannot be found.".format(handler_class_name))
@@ -303,7 +325,7 @@ def parse_module(cls, path: str, handler_configs: List[HandlerConfig], skill_con
                 assert 'skill_context' not in args.keys(), "'skill_context' is a reserved key. Please rename your arguments!"
                 args['skill_context'] = skill_context
                 handler = handler_class(**args)
-                handlers.append(handler)
+                handlers[handler_id] = handler
 
         return handlers
 
@@ -320,6 +342,7 @@ def __init__(self, *args, **kwargs):
         """
         self._context = kwargs.pop('skill_context')  # type: SkillContext
         self._config = kwargs
+        self.completed = False
 
     @property
     def context(self) -> SkillContext:
@@ -356,7 +379,7 @@ def teardown(self) -> None:
         """
 
     @classmethod
-    def parse_module(cls, path: str, tasks_configs: List[TaskConfig], skill_context: SkillContext) -> List['Task']:
+    def parse_module(cls, path: str, tasks_configs: Dict[str, TaskConfig], skill_context: SkillContext) -> Dict[str, 'Task']:
         """
         Parse the tasks module.
 
@@ -365,7 +388,7 @@ def parse_module(cls, path: str, tasks_configs: List[TaskConfig], skill_context:
         :param skill_context: the skill context
         :return: a list of Tasks.
         """
-        tasks = []
+        tasks = {}
         tasks_spec = importlib.util.spec_from_file_location("tasks", location=path)
         task_module = importlib.util.module_from_spec(tasks_spec)
         tasks_spec.loader.exec_module(task_module)  # type: ignore
@@ -373,9 +396,10 @@ def parse_module(cls, path: str, tasks_configs: List[TaskConfig], skill_context:
         tasks_classes = list(filter(lambda x: re.match("\\w+Task", x[0]), classes))
 
         name_to_class = dict(tasks_classes)
-        for task_config in tasks_configs:
+        for task_id, task_config in tasks_configs.items():
             task_class_name = task_config.class_name
             logger.debug("Processing task {}".format(task_class_name))
+            assert task_id.isidentifier(), "'{}' is not a valid identifier.".format(task_id)
             task_class = name_to_class.get(task_class_name, None)
             if task_class is None:
                 logger.warning("Task '{}' cannot be found.".format(task_class_name))
@@ -384,7 +408,7 @@ def parse_module(cls, path: str, tasks_configs: List[TaskConfig], skill_context:
                 assert 'skill_context' not in args.keys(), "'skill_context' is a reserved key. Please rename your arguments!"
                 args['skill_context'] = skill_context
                 task = task_class(**args)
-                tasks.append(task)
+                tasks[task_id] = task
 
         return tasks
 
@@ -413,7 +437,7 @@ def config(self) -> Dict[Any, Any]:
         return self._config
 
     @classmethod
-    def parse_module(cls, path: str, shared_classes_configs: List[SharedClassConfig], skill_context: SkillContext) -> List['SharedClass']:
+    def parse_module(cls, path: str, shared_classes_configs: Dict[str, SharedClassConfig], skill_context: SkillContext) -> Dict[str, 'SharedClass']:
         """
         Parse the tasks module.
 
@@ -422,10 +446,10 @@ def parse_module(cls, path: str, shared_classes_configs: List[SharedClassConfig]
         :param skill_context: the skill context
         :return: a list of SharedClass.
         """
-        instances = []
+        instances = {}
         shared_classes = []
 
-        shared_classes_names = set(config.class_name for config in shared_classes_configs)
+        shared_classes_names = set(config.class_name for _, config in shared_classes_configs.items())
 
         # get all Python modules except the standard ones
         ignore_regex = "|".join(["handlers.py", "behaviours.py", "tasks.py", "__.*"])
@@ -448,9 +472,10 @@ def parse_module(cls, path: str, shared_classes_configs: List[SharedClassConfig]
             shared_classes.extend(filtered_classes)
 
         name_to_class = dict(shared_classes)
-        for shared_class_config in shared_classes_configs:
+        for shared_class_id, shared_class_config in shared_classes_configs.items():
             shared_class_name = shared_class_config.class_name
-            logger.debug("Processing shared class {}".format(shared_class_name))
+            logger.debug("Processing shared class id={}, class={}".format(shared_class_id, shared_class_name))
+            assert shared_class_id.isidentifier(), "'{}' is not a valid identifier.".format(shared_class_id)
             shared_class = name_to_class.get(shared_class_name, None)
             if shared_class is None:
                 logger.warning("Shared class '{}' cannot be found.".format(shared_class_name))
@@ -459,8 +484,8 @@ def parse_module(cls, path: str, shared_classes_configs: List[SharedClassConfig]
                 assert 'skill_context' not in args.keys(), "'skill_context' is a reserved key. Please rename your arguments!"
                 args['skill_context'] = skill_context
                 shared_class_instance = shared_class(**args)
-                instances.append(shared_class_instance)
-                setattr(skill_context, shared_class_name.lower(), shared_class_instance)
+                instances[shared_class_id] = shared_class_instance
+                setattr(skill_context, shared_class_id, shared_class_instance)
         return instances
 
 
@@ -469,10 +494,10 @@ class Skill:
 
     def __init__(self, config: SkillConfig,
                  skill_context: SkillContext,
-                 handlers: Optional[List[Handler]],
-                 behaviours: Optional[List[Behaviour]],
-                 tasks: Optional[List[Task]],
-                 shared_classes: Optional[List[SharedClass]]):
+                 handlers: Optional[Dict[str, Handler]],
+                 behaviours: Optional[Dict[str, Behaviour]],
+                 tasks: Optional[Dict[str, Task]],
+                 shared_classes: Optional[Dict[str, SharedClass]]):
         """
         Initialize a skill.
 
@@ -484,10 +509,10 @@ def __init__(self, config: SkillConfig,
         """
         self.config = config
         self.skill_context = skill_context
-        self.handlers = handlers
-        self.behaviours = behaviours
-        self.tasks = tasks
-        self.shared_classes = shared_classes
+        self.handlers = handlers if handlers is not None else {}
+        self.behaviours = behaviours if behaviours is not None else {}
+        self.tasks = tasks if tasks is not None else {}
+        self.shared_classes = shared_classes if shared_classes is not None else {}
 
     @classmethod
     def from_dir(cls, directory: str, agent_context: AgentContext) -> 'Skill':
@@ -511,33 +536,29 @@ def from_dir(cls, directory: str, agent_context: AgentContext) -> 'Skill':
 
         skill_context = SkillContext(agent_context)
 
-        handlers_by_id = skill_config.handlers.read_all()
+        handlers_by_id = dict(skill_config.handlers.read_all())
         if len(handlers_by_id) > 0:
-            handlers_configurations = list(dict(handlers_by_id).values())
-            handlers = Handler.parse_module(os.path.join(directory, "handlers.py"), handlers_configurations, skill_context)
+            handlers = Handler.parse_module(os.path.join(directory, "handlers.py"), handlers_by_id, skill_context)
         else:
-            handlers = []
+            handlers = {}
 
-        behaviours_by_id = skill_config.behaviours.read_all()
+        behaviours_by_id = dict(skill_config.behaviours.read_all())
         if len(behaviours_by_id) > 0:
-            behaviours_configurations = list(dict(behaviours_by_id).values())
-            behaviours = Behaviour.parse_module(os.path.join(directory, "behaviours.py"), behaviours_configurations, skill_context)
+            behaviours = Behaviour.parse_module(os.path.join(directory, "behaviours.py"), behaviours_by_id, skill_context)
         else:
-            behaviours = []
+            behaviours = {}
 
-        tasks_by_id = skill_config.tasks.read_all()
+        tasks_by_id = dict(skill_config.tasks.read_all())
         if len(tasks_by_id) > 0:
-            tasks_configurations = list(dict(tasks_by_id).values())
-            tasks = Task.parse_module(os.path.join(directory, "tasks.py"), tasks_configurations, skill_context)
+            tasks = Task.parse_module(os.path.join(directory, "tasks.py"), tasks_by_id, skill_context)
         else:
-            tasks = []
+            tasks = {}
 
-        shared_classes_by_id = skill_config.shared_classes.read_all()
+        shared_classes_by_id = dict(skill_config.shared_classes.read_all())
         if len(shared_classes_by_id) > 0:
-            shared_classes_configurations = list(dict(shared_classes_by_id).values())
-            shared_classes_instances = SharedClass.parse_module(directory, shared_classes_configurations, skill_context)
+            shared_classes_instances = SharedClass.parse_module(directory, shared_classes_by_id, skill_context)
         else:
-            shared_classes_instances = []
+            shared_classes_instances = {}
 
         skill = Skill(skill_config, skill_context, handlers, behaviours, tasks, shared_classes_instances)
         skill_context._skill = skill
diff --git a/aea/skills/behaviours.py b/aea/skills/behaviours.py
new file mode 100644
index 0000000000..e8b35a644c
--- /dev/null
+++ b/aea/skills/behaviours.py
@@ -0,0 +1,259 @@
+# ------------------------------------------------------------------------------
+#
+#   Copyright 2018-2019 Fetch.AI Limited
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+#
+# ------------------------------------------------------------------------------
+
+"""This module contains the classes for specific behaviours."""
+import datetime
+from abc import ABC
+from typing import Optional, List, Dict
+
+from aea.skills.base import Behaviour
+
+
+class SimpleBehaviour(Behaviour, ABC):
+    """This class implements a simple behaviour."""
+
+
+class CompositeBehaviour(Behaviour, ABC):
+    """This class implements a composite behaviour."""
+
+
+class CyclicBehaviour(SimpleBehaviour, ABC):
+    """This behaviour is executed until the agent is stopped."""
+
+    def __init__(self, **kwargs):
+        """Initialize the cyclic behaviour."""
+        super().__init__(**kwargs)
+        self._number_of_executions = 0
+
+    def act_wrapper(self) -> None:
+        """Wrap the call of the action. This method must be called only by the framework."""
+        if not self.done():
+            self.act()
+            self._number_of_executions += 1
+
+    def done(self) -> bool:
+        """
+        Return True if the behaviour is terminated, False otherwise.
+
+        The user should implement it properly to determine the stopping condition.
+        """
+        return False
+
+
+class OneShotBehaviour(SimpleBehaviour, ABC):
+    """This behaviour is executed only once."""
+
+    def __init__(self, **kwargs):
+        """Initialize the cyclic behaviour."""
+        super().__init__(**kwargs)
+        self._already_executed = False  # type
+
+    def done(self) -> bool:
+        """Return True if the behaviour is terminated, False otherwise."""
+        return self._already_executed
+
+    def act_wrapper(self) -> None:
+        """Wrap the call of the action. This method must be called only by the framework."""
+        if not self._already_executed:
+            self.act()
+            self._already_executed = True
+
+
+class TickerBehaviour(SimpleBehaviour, ABC):
+    """This behaviour is executed periodically with an interval."""
+
+    def __init__(self, tick_interval: float = 1.0, start_at: Optional[datetime.datetime] = None, **kwargs):
+        """
+        Initialize the ticker behaviour.
+
+        :param tick_interval: interval of the behaviour in seconds.
+        :param start_at: whether to start the behaviour with an offset.
+        """
+        super().__init__(**kwargs)
+
+        self._tick_interval = tick_interval
+        self._start_at = start_at if start_at is not None else datetime.datetime.now()  # type: datetime.datetime
+        # note, we set _last_act_time to be in the past so the ticker starts immediately
+        self._last_act_time = datetime.datetime.now() - datetime.timedelta(seconds=tick_interval)
+
+    @property
+    def tick_interval(self) -> float:
+        """Get the tick_interval in seconds."""
+        return self._tick_interval
+
+    @property
+    def start_at(self) -> datetime.datetime:
+        """Get the start time."""
+        return self._start_at
+
+    @property
+    def last_act_time(self) -> datetime.datetime:
+        """Get the last time the act method has been called."""
+        return self._start_at
+
+    def act_wrapper(self) -> None:
+        """Wrap the call of the action. This method must be called only by the framework."""
+        if not self.done() and self.is_time_to_act():
+            self._last_act_time = datetime.datetime.now()
+            self.act()
+
+    def is_time_to_act(self) -> bool:
+        """
+        Check whether it is time to act, according to the tick_interval constraint and the 'start at' constraint.
+
+        :return: True if it is time to act, false otherwise.
+        """
+        now = datetime.datetime.now()
+        return now > self._start_at and (now - self._last_act_time).total_seconds() > self.tick_interval
+
+
+class SequenceBehaviour(CompositeBehaviour, ABC):
+    """This behaviour executes sub-behaviour serially."""
+
+    def __init__(self, behaviour_sequence: List[Behaviour], **kwargs):
+        """
+        Initialize the sequence behaviour.
+
+        :param behaviour_sequence: the sequence of behaviour.
+        :param kwargs:
+        """
+        super().__init__(**kwargs)
+
+        self._behaviour_sequence = behaviour_sequence
+        assert len(self._behaviour_sequence) > 0, "at least one behaviour."
+        self._index = 0
+
+    @property
+    def current_behaviour(self) -> Optional[Behaviour]:
+        """
+        Get the current behaviour.
+
+        If None, the sequence behaviour can be considered done.
+        """
+        return None if self._index >= len(self._behaviour_sequence) else self._behaviour_sequence[self._index]
+
+    def _increase_index_if_possible(self):
+        if self._index < len(self._behaviour_sequence):
+            self._index += 1
+
+    def act(self) -> None:
+        """Implement the behaviour."""
+        while not self.done() and self.current_behaviour is not None and self.current_behaviour.done():
+            self._increase_index_if_possible()
+
+        if not self.done() and self.current_behaviour is not None and not self.current_behaviour.done():
+            self.current_behaviour.act_wrapper()
+
+    def done(self) -> bool:
+        """Return True if the behaviour is terminated, False otherwise."""
+        return self._index >= len(self._behaviour_sequence)
+
+
+class State(OneShotBehaviour, ABC):
+    """A state of a FSMBehaviour is a OneShotBehaviour."""
+
+    def __init__(self, **kwargs):
+        """Initialize a state of the state machine."""
+        super().__init__(**kwargs)
+        self._next_state = None
+
+    @property
+    def next_state(self) -> Optional[str]:
+        """Get the next state name. If None, the current state is supposed to be final."""
+        return self._next_state
+
+    @next_state.setter
+    def next_state(self, state_name):
+        """
+        Set the state to transition to when this state is finished.
+
+        The argument 'state_name' must be a valid state and the transition must be registered.
+        If the setter is not called then current state is a final state.
+
+        :param: state_name: the name of the state to transition to
+        """
+        self._next_state = state_name
+
+
+class FSMBehaviour(CompositeBehaviour):
+    """This class implements a finite-state machine behaviour."""
+
+    def __init__(self, **kwargs):
+        """Initialize the finite-state machine behaviour."""
+        super().__init__(**kwargs)
+
+        self.name_to_state = {}  # type: Dict[str, State]
+        self._initial_state = None  # type: Optional[str]
+        self.current = None  # type: Optional[str]
+
+        self.transitions = {}  # type: Dict[str, Dict[str, str]]
+
+    @property
+    def is_started(self) -> bool:
+        """Check if the behaviour is started."""
+        return self._initial_state is not None
+
+    def register_state(self, name: str, state: State, initial: bool = False) -> None:
+        """
+        Register a state.
+
+        :param name: the name of the state.
+        :param state: the behaviour in that state.
+        :return: None
+        """
+        self.name_to_state[name] = state
+        if initial:
+            self._initial_state = name
+            self.current = self._initial_state
+
+    @property
+    def initial_state(self) -> Optional[str]:
+        """Get the initial state name."""
+        return self._initial_state
+
+    @initial_state.setter
+    def initial_state(self, name: str):
+        """Set the initial state."""
+        if name not in self.name_to_state:
+            raise ValueError("Name is not registered as state.")
+        self._initial_state = name
+
+    def get_state(self, name) -> Optional[State]:
+        """Get a state from its name."""
+        return self.name_to_state.get(name, None)
+
+    def reset(self):
+        """Reset the behaviour to its initial conditions."""
+        self.current = None
+
+    def act(self):
+        """Implement the behaviour."""
+        if self.current is None:
+            return
+
+        current_state = self.get_state(self.current)
+        if current_state is None:
+            return
+        current_state.act_wrapper()
+
+        if current_state.done():
+            self.current = current_state.next_state
+
+    def done(self) -> bool:
+        """Return True if the behaviour is terminated, False otherwise."""
+        return self.current is None
diff --git a/aea/skills/error/handlers.py b/aea/skills/error/handlers.py
index 2ece1c7f38..4ad94de5d7 100644
--- a/aea/skills/error/handlers.py
+++ b/aea/skills/error/handlers.py
@@ -44,12 +44,11 @@ def setup(self) -> None:
         :return: None
         """
 
-    def handle(self, message: Message, sender: str) -> None:
+    def handle(self, message: Message) -> None:
         """
         Implement the reaction to an envelope.
 
         :param message: the message
-        :param sender: the sender
         """
 
     def teardown(self) -> None:
@@ -68,10 +67,10 @@ def send_unsupported_protocol(self, envelope: Envelope) -> None:
         """
         logger.warning("Unsupported protocol: {}".format(envelope.protocol_id))
         reply = DefaultMessage(type=DefaultMessage.Type.ERROR,
-                               error_code=DefaultMessage.ErrorCode.UNSUPPORTED_PROTOCOL.value,
+                               error_code=DefaultMessage.ErrorCode.UNSUPPORTED_PROTOCOL,
                                error_msg="Unsupported protocol.",
                                error_data={"protocol_id": envelope.protocol_id})
-        self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key,
+        self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_address,
                                         protocol_id=DefaultMessage.protocol_id,
                                         message=DefaultSerializer().encode(reply))
 
@@ -85,10 +84,10 @@ def send_decoding_error(self, envelope: Envelope) -> None:
         logger.warning("Decoding error: {}.".format(envelope))
         encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8")
         reply = DefaultMessage(type=DefaultMessage.Type.ERROR,
-                               error_code=DefaultMessage.ErrorCode.DECODING_ERROR.value,
+                               error_code=DefaultMessage.ErrorCode.DECODING_ERROR,
                                error_msg="Decoding error.",
                                error_data={"envelope": encoded_envelope})
-        self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key,
+        self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_address,
                                         protocol_id=DefaultMessage.protocol_id,
                                         message=DefaultSerializer().encode(reply))
 
@@ -102,10 +101,10 @@ def send_invalid_message(self, envelope: Envelope) -> None:
         logger.warning("Invalid message wrt protocol: {}.".format(envelope.protocol_id))
         encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8")
         reply = DefaultMessage(type=DefaultMessage.Type.ERROR,
-                               error_code=DefaultMessage.ErrorCode.INVALID_MESSAGE.value,
+                               error_code=DefaultMessage.ErrorCode.INVALID_MESSAGE,
                                error_msg="Invalid message.",
                                error_data={"envelope": encoded_envelope})
-        self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key,
+        self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_address,
                                         protocol_id=DefaultMessage.protocol_id,
                                         message=DefaultSerializer().encode(reply))
 
@@ -119,9 +118,9 @@ def send_unsupported_skill(self, envelope: Envelope) -> None:
         logger.warning("Cannot handle envelope: no handler registered for the protocol '{}'.".format(envelope.protocol_id))
         encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8")
         reply = DefaultMessage(type=DefaultMessage.Type.ERROR,
-                               error_code=DefaultMessage.ErrorCode.UNSUPPORTED_SKILL.value,
+                               error_code=DefaultMessage.ErrorCode.UNSUPPORTED_SKILL,
                                error_msg="Unsupported skill.",
                                error_data={"envelope": encoded_envelope})
-        self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key,
+        self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_address,
                                         protocol_id=DefaultMessage.protocol_id,
                                         message=DefaultSerializer().encode(reply))
diff --git a/aea/skills/error/skill.yaml b/aea/skills/error/skill.yaml
index 33806a3345..5e546e789c 100644
--- a/aea/skills/error/skill.yaml
+++ b/aea/skills/error/skill.yaml
@@ -1,15 +1,16 @@
 name: error
-authors: Fetch.AI Limited
+author: fetchai
 version: 0.1.0
 license: Apache 2.0
+fingerprint: ""
 url: ""
 description: "The error skill implements basic error handling required by all AEAs."
-behaviours: []
+behaviours: {}
 handlers:
-  - handler:
+  error_handler:
       class_name: ErrorHandler
       args:
         foo: bar
-tasks: []
-shared_classes: []
+tasks: {}
+shared_classes: {}
 protocols: ['default']
diff --git a/aea/skills/scaffold/handlers.py b/aea/skills/scaffold/handlers.py
index 8cefccd000..b7fa596e61 100644
--- a/aea/skills/scaffold/handlers.py
+++ b/aea/skills/scaffold/handlers.py
@@ -39,12 +39,11 @@ def setup(self) -> None:
         """
         raise NotImplementedError  # pragma: no cover
 
-    def handle(self, message: Message, sender: str) -> None:
+    def handle(self, message: Message) -> None:
         """
         Implement the reaction to an envelope.
 
         :param message: the message
-        :param sender: the sender
         :return: None
         """
         raise NotImplementedError  # pragma: no cover
diff --git a/aea/skills/scaffold/skill.yaml b/aea/skills/scaffold/skill.yaml
index 8854ac25e8..d7adcac7a6 100644
--- a/aea/skills/scaffold/skill.yaml
+++ b/aea/skills/scaffold/skill.yaml
@@ -1,27 +1,28 @@
-name: scaffold_skill
-authors: Fetch.AI Limited
+name: scaffold
+author: fetchai
 version: 0.1.0
 license: Apache 2.0
+fingerprint: ""
 url: ""
 description: "The scaffold skill is a scaffold for your own skill implementation."
 behaviours:
-  - behaviour:
-      class_name: MyScaffoldBehaviour
-      args:
-        foo: bar
+  scaffold:
+    class_name: MyScaffoldBehaviour
+    args:
+      foo: bar
 handlers:
-  - handler:
-      class_name: MyScaffoldHandler
-      args:
-        foo: bar
+  scaffold:
+    class_name: MyScaffoldHandler
+    args:
+      foo: bar
 tasks:
-  - task:
-      class_name: MyScaffoldTask
-      args:
-        foo: bar
+  scaffold:
+    class_name: MyScaffoldTask
+    args:
+      foo: bar
 shared_classes:
-  - shared_class:
-      class_name: MySharedClass
-      args:
-        foo: bar
+  scaffold:
+    class_name: MySharedClass
+    args:
+      foo: bar
 protocols: []
diff --git a/aea/skills/tasks.py b/aea/skills/tasks.py
new file mode 100644
index 0000000000..a766a1fbe7
--- /dev/null
+++ b/aea/skills/tasks.py
@@ -0,0 +1,86 @@
+# ------------------------------------------------------------------------------
+#
+#   Copyright 2018-2019 Fetch.AI Limited
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+#
+# ------------------------------------------------------------------------------
+
+"""This module contains the classes for tasks."""
+import logging
+import threading
+from collections import deque
+from concurrent.futures import Executor, ThreadPoolExecutor
+from queue import Queue
+from threading import Thread
+from typing import Optional
+
+from aea.skills.base import Task
+
+logger = logging.getLogger(__name__)
+
+
+class TaskManager:
+    """A Task manager."""
+
+    def __init__(self, executor: Optional[Executor] = None):
+        """
+        Initialize the task manager.
+
+        :param executor: the executor
+        """
+        self._executor = executor if executor is not None else ThreadPoolExecutor()
+
+        self.task_queue = Queue()  # type: Queue
+        self.futures = deque([])  # type: deque
+
+        self.stopped = True
+        self.thread = Thread(target=self.dispatch)
+        self.lock = threading.Lock()
+
+    def enqueue_task(self, task: Task) -> None:
+        """
+        Enqueue a task with the executor.
+
+        :param task: the task instance to be enqueued
+        """
+        self.task_queue.put(task)
+
+    def dispatch(self) -> None:
+        """Dispatch a task."""
+        while not self.stopped:
+            for future in self.futures:
+                if future.done():
+                    future.result()
+
+            next_task = self.task_queue.get(block=True)  # type: Optional[Task]
+            if next_task is None:
+                return
+
+            future = self._executor.submit(next_task.execute)
+            self.futures.append(future)
+
+    def start(self) -> None:
+        """Start the task manager."""
+        with self.lock:
+            logger.debug("Start the task manager.")
+            self.stopped = False
+            self.thread.start()
+
+    def stop(self) -> None:
+        """Stop the task manager."""
+        with self.lock:
+            logger.debug("Start the task manager.")
+            self.stopped = True
+            self.task_queue.put(None)
+            self.thread.join()
diff --git a/deploy-image/docker-env.sh b/deploy-image/docker-env.sh
index 0bc1ee07c4..fc7d559d5f 100755
--- a/deploy-image/docker-env.sh
+++ b/deploy-image/docker-env.sh
@@ -1,7 +1,7 @@
 #!/bin/bash
 
 # Swap the following lines if you want to work with 'latest'
-DOCKER_IMAGE_TAG=aea-deploy:0.1.14
+DOCKER_IMAGE_TAG=aea-deploy:0.1.15
 # DOCKER_IMAGE_TAG=aea-deploy:latest
 
 DOCKER_BUILD_CONTEXT_DIR=..
diff --git a/develop-image/Dockerfile b/develop-image/Dockerfile
index 738e3b057d..2abdc49d54 100644
--- a/develop-image/Dockerfile
+++ b/develop-image/Dockerfile
@@ -52,8 +52,7 @@ RUN add-apt-repository ppa:deadsnakes/ppa
 RUN apt-get update
 RUN apt-get install -y      \
     python3.8               \
-    python3.8-dev           \
-    python3.8-distutils
+    python3.8-dev           
 RUN python3.8 -m pip install Cython
 RUN python3.8 -m pip install git+https://github.com/pytoolz/cytoolz.git#egg=cytoolz==0.10.1.dev0
 
@@ -69,6 +68,6 @@ RUN sudo make clean
 
 RUN pipenv --python python3.7
 RUN pipenv install --dev
-RUN pipenv run pip3 install .
+RUN pipenv run pip3 install .[all]
 
 CMD ["/bin/bash"]
diff --git a/develop-image/docker-env.sh b/develop-image/docker-env.sh
index d80c881018..6735e07fee 100755
--- a/develop-image/docker-env.sh
+++ b/develop-image/docker-env.sh
@@ -1,7 +1,7 @@
 #!/bin/bash
 
 # Swap the following lines if you want to work with 'latest'
-DOCKER_IMAGE_TAG=aea-develop:0.1.14
+DOCKER_IMAGE_TAG=aea-develop:0.1.15
 # DOCKER_IMAGE_TAG=aea-develop:latest
 
 DOCKER_BUILD_CONTEXT_DIR=..
diff --git a/docs/agent-oriented-development.md b/docs/agent-oriented-development.md
new file mode 100644
index 0000000000..fab6406a2e
--- /dev/null
+++ b/docs/agent-oriented-development.md
@@ -0,0 +1,56 @@
+# Agent-oriented development
+In this section, we highlight some of the most fundamental characteristics of the agent-oriented approach to solution development, which might be different from some of the existing paradigms and methodologies you may be used to. We hope that with this, we can guide you towards having the right mindset when you are designing your own agent-based solutions to real world problems. 
+
+## Decentralisation
+Multi-Agent Systems (**MAS**) are inherently decentralised. The vision is, an environment in which every agent is able to directly connect with everyone else and interact with them without having to rely on third-parties to facilitate this. This is in direct contrast to centralised systems in which a single entity is the central point of authority, through which all interactions happen. For example systems based on the client-server architecture, in which clients interact with one another, regarding a specific service (e.g. communication, trade), only through the server.
+
+Note, this is not to say that facilitators and middlemen have no place in a multi-agent system; rather it is the 'commanding reliance on middlemen' that MAS disagrees with.
+
+**Division of responsibilities:** In a decentralised system, every agent is equally privileged, and (in principle) should be able to interact with any other agent. The idea is very much aligned with the peer-to-peer paradigm, in which it is the voluntary participation and contribution of peers that creates the infrastructure. As such, in a decentralised system, there is no central 'enforcer'. This means all the work that would typically fall under the responsibilities of a central entity must be performed by individual parties in a decentralised system. Blockchain-based cryptocurrencies are a good example of this. People who are getting into cryptocurrencies are often reminded that, due to the lack of a central trusted entity (e.g. a bank), most security precautions related to the handling of digital assets and the execution of transactions fall on individuals themselves. 
+
+
+
+**Decentralisation vs distribution:** It is important to emphasise that by decentralisation we do not mean distribution; although multi-agent systems typically do tend to also be distributed. A distributed system is one whose components are physically located in different places and connected over a network. A fully centralised system, owned and operated by a single entity, may in fact be highly distributed. Google's infrastructure is an example of this, where all of the components are distributed across the globe, yet designed to work together highly efficiently and function in unison. Decentralisation on the other hand refers to a system whose components may be owned, operated, and managed by different stakeholders, each with their own personal objectives, interests, and preferences, which may not necessarily be aligned with one another or that of the system itself. Therefore, distribution refers to the physical placement of a system's components, whereas decentralisation refers to **a)** the diversity of ownership and control over a system's constituents, and **b)** the absence of a central point of authority between them.
+
+**Example:** To better illustrate the distinction between centralised and decentralised systems, consider another example: search and discoverability in a commerce environment. In a centralised system (say amazon.com), there is a single search service -- provided, owned and run by the commerce company itself -- which takes care of all search related functionality for every product within their domain. So to be discoverable in this system, all sellers must register their products with this particular service. However in a decentralised system, there may not necessarily be a single search service provider. There may be multiple such services, run by different, perhaps competing entities. Each seller has the freedom to register with (i.e. make themselves known to) one or a handful of services. On the buyers side, the more services they contact and query, the higher their chances of finding the product they are looking for. 
+
+## Conflicting Environment
+
+As discussed above, the notion of decentralisation extends as far as ownership and control. Therefore, the different components that make up a decentralised system may each be owned by a different entity, designed according to very different principles and standards, with heterogeneous software and hardware, and each with internal objectives that may be fundamentally inconsistent, worst yet contradictory, with those of others. 
+
+As such, a distinctive characteristic of a multi-agent environment, is that it is inhabited by more than one agent (as the name suggests), where each agent may be owned potentially by a different stakeholder (individual, company, government). Since by design, each agent represents and looks after the interests of its owner(s), and because different stakeholders may have unaligned, conflicting, or contradictory interests, it is very common to have multi-agent systems in which the agents' objectives, values and preferences are unaligned, conflicting, or contradictory.
+
+**In practice:** There are practical implications that follow from the above when it comes to designing an agent. For example, it is not rational for an agent to automatically rely on the information it receives from other agents. The information could be:
+
+* Incomplete: what is unrevealed may have been deemed private for strategic reasons. 
+* Uncertain: it may be the result of an inaccurate prediction. 
+* Incorrect: it could be an outright lie, due to the adversarial nature of the environment.
+
+Therefore one can argue, that there is a degree of uncertainty attached to almost all information an agent receives or infers in a multi-agent system. It wouldn't then be illogical for an agent to take a sceptical approach: treating everything as uncertain, unless proved otherwise.
+
+## Asynchronisation
+
+The conflicting nature of multi-agent systems, consisting of self-interested autonomous agents, points to _asynchronisation_ as the preferred method of designing and managing processes and interactions.
+
+**Synchronisation vs asynchronisation:** In general, asynchronisation refers to the decoupling of events that do interact with one another but do not occur at predetermined intervals, not necessarily relying on each other's existence to function. This is in contrast with _synchronous_ systems in which processes are aware of one another, where one's execution depends in some way on the other.
+
+**Asynchronisation in MAS:** In the context of multi-agent systems, the decentralised and potentially conflicting nature of the environment creates uncertainty over the behaviour of the whole system, in particular of other agents. For example, suppose an agent _i_ sends a message requesting some resources from an agent _j_. Since MAS often tends to be distributed, there is the usual uncertainties with communication over a network: _j_ may never receive _i_'s request, or may receive it after a long delay. Furthermore, _j_ could receive the request in time and respond immediately, but as mentioned in the last section, its answer might be incomplete (gives only some of the requested resources), uncertain (promises to give the resources, but cannot be fully trusted), or incorrect (sends a wrong resource). In addition, since agents are self-interested, _j_ may _decide_ to reply much later, to the point that the resource is no longer useful to agent _i_, or _j_ may simply decide not to respond at all. There might be a myriad of reasons why it may choose to do that; it could be because _j_ assigns a low priority to answering _i_ over its other tasks. But that's beside the point. The take away is that agents' autonomy strongly influences what can be expected of them, and of an environment inhabited by them. As such, developing for a system whose constituents are autonomous, e.g. agents in a multi-agent system, is fundamentally different from one whose constituents aren't, e.g. objects in an object-oriented system.
+
+**Objects vs agents:** In object-oriented systems, objects are entities that encapsulate state and perform actions, i.e. call methods, on this state. In object-oriented languages, like C++ and Java, it is common practice to declare methods as public, so they can be invoked by other objects in the system whenever they wish. This implies that an object does not control its own behaviour. If an object’s method is public, the object has no control over whether or not that method is executed.  
+
+We cannot take for granted that an agent _j_ will execute an action (the equivalent of a method in object-oriented systems) just because another agent _i_ wants it to; this action may not be in the best interests of agent _i_. So we do not think of agents as invoking methods on one another, rather as _requesting_ actions. If _i_ requests _j_ to perform an action, then _j_ may or may not perform the action. It may choose to do it later or do it in exchange for something. The locus of control is therefore different in object-oriented and agent-oriented systems. In the former, the decision lies with the object invoking the method, whereas in the latter, the decision lies with the agent receiving the request. This distinction could be summarised by the following slogan (from An Introduction to MultiAgent Systems by Michael Wooldridge):
+>objects do it for free; agents do it because they want to.
+
+All of this makes asynchronisation the preferred method for designing agent processes and interactions. An agent's interactions should be independent of each other, as much as possible, and of the agent's decision making processes and actions. This means the success or failure of, or delay in any single interaction does not block the agent's other tasks. 
+
+## Complex, Incomplete, Inconsistent and Uncertain
+
+The forth characteristic(s) relate to the environment in which agents are expected to operate in, and these have been mentioned a number of times in the previous sections. 
+
+The environment agents are suited for typically tend to be complex, to the point that it is usually impossible for any single agent to perceive the whole of the environment on its own. This means that at any point in time, any agent has a limited knowledge about the state of the environment. In other words, the agents;' information tend to be incomplete due to the complexity and sophistication of the world in which they reside. 
+
+Consider an agent which represents a driverless vehicle. The complexity of the problem of driving on the road makes it impossible for a single vehicle to have an accurate and up-to-date knowledge of the overall state of the world . This means that an agent's model of the world is at best uncertain. For instance, the vehicle, through its sensor may detect green light at a junction, and by being aware of what it means, it may infer that it is safe to cross a junction. However, that simply may not be true as another car in the opposite direction may still cross the junction violating their red light. Therefore, there is uncertainty associated with the knowledge "it is safe to cross the road because the light is green", and the agent must recognise that. 
+
+Furthermore, the often conflicting nature of the environment means information obtained from multiple sources (agents) may be inconsistent. Again, this must be taken into consideration when designing an agent which is expected to operate successfully in a potentially conflicting environment. 
+
+
diff --git a/docs/assets/cli_gui02_sequence.png b/docs/assets/cli_gui02_sequence.png deleted file mode 100644 index de97be515c..0000000000 Binary files a/docs/assets/cli_gui02_sequence.png and /dev/null differ diff --git a/docs/car-park-alt.md b/docs/car-park-alt.md deleted file mode 100644 index b5698f34bc..0000000000 --- a/docs/car-park-alt.md +++ /dev/null @@ -1,213 +0,0 @@ -!!! Warning - Work in progress. - - - -# Car Park Agent Application - -The Fetch.ai car park agent application is made up of three components: - -* A Raspberry Pi hardware device with camera module viewing a car park. -* A car park agent GUI running on the Raspberry Pi that collects data on free spaces and serves it up for purchase on the Fetch.ai network. -* A remote client agent GUI that connects to the Raspberry Pi and buys data. - - -| Hardware | Car Park Agent GUI | Client Agent GUI | -| ------------- |:-------------:|:-----:| -| | | | - - - - -## Raspberry Pi hardware set up - -The hardware set up is the most time consuming part of these instructions. - -We assume the developer has some familiarity with Raspberry Pi and refer them to the manufacturer's instructions. However, we do list any problems we encountered and their solutions below. - -!!! Note - We have used the Raspberry Pi 4. You are welcome to use earlier versions but the set up may change slightly. - -Follow the manufacturer's instructions to set up the Raspberry Pi 4 hardware device: https://projects.raspberrypi.org/en/projects/raspberry-pi-setting-up/2. - - -### Install and update the OS - -Install the Raspbian OS and update it. - -``` bash -sudo apt update -y -sudo apt-get update -sudo apt-get dist-upgrade -sudo reboot -``` - -### Enable camera, ssh, and VNC - -Click on the Raspberry symbol in the top left of the screen. - -Select Preferences then Raspberry Pi Configuration. - -
- -
-
- -Enable the Camera, SSH, and VNC options. -
- -
- - -### Set up camera module - -Follow the manufacturer's instructions to set up the Raspberry Pi Camera module: https://projects.raspberrypi.org/en/projects/getting-started-with-picamera. - -Configure and test the camera software: https://www.raspberrypi.org/documentation/configuration/camera.md. - -Set up your Pi to physically view the car park. We'll leave that to you. - - -### Potential issues - -1. Make sure you use the first port, `HDMI 0` on the Pi for the initial set up monitor. -2. If you install the Pi with a used SD card, you will need to reformat the card with NOOBS: https://www.raspberrypi.org/downloads/noobs/. -3. Fix the screen resolution issues by editing the configuration. - -``` bash -sudo raspi-config -``` -Select the `1920X1080` resolution option - number 31. - -Then update the configuration file as follows. Open it. - -``` bash -sudo nano /boot/config.txt -``` -And make sure the following three lines are commented out. - -``` bash -# Enable DRM VC4 V3D driver on top of the dispmanx display stack -# dtoverlay=vc4-fkms-v3d -# max_framebuffers=2 -``` - - - -## Agent server application installation - -Now we are ready to install the car park agent GUI server application on the Raspberry Pi. - -### Get the code - -``` bash -cd ~/Desktop -git clone https://github.com/fetchai/carpark_agent.git -cd carpark_agent -``` - -### Download datafile - -This is required for the machine learning algorithms. - -``` bash -./car_detection/weights/download_weights.sh -``` -Install the required libraries. - -``` bash -sudo apt-get install gcc htop vim mc python3-dev ffmpeg virtualenv libatlas-base-dev libsm6 libxext6 clang libblas3 liblapack3 liblapack-dev libblas-dev cython gfortran build-essential libgdal-dev libopenblas-dev liblapack3 liblapacke liblapacke-dev liblcms2-utils liblcms2-2 libwebpdemux2 python3-scipy python3-numpy python3-matplotlib libjasper-dev libqtgui4 libqt4-test protobuf-compiler python3-opencv gpsd gpsd-clients -``` - -### Activate a virtual environment. - -``` bash -pip3 install virtualenv -./run_scripts/create_venv.sh -source venv/bin/activate -``` - -### Install the software - -``` bash -python setup.py develop -``` - -!!! Note - We recommend that using `develop` as this creates a link to the code and so any changes you make will take immediate effect when you run the code. - - -### Run it - -``` bash -./run_scripts/run_carpark_agent.sh -``` -You should now see the agent running. - -### Ensure agent start on boot (RPi4 only) - -Ensure the startup script runs whenever we the Raspberry Pi turns on. - -``` bash -crontab -e -``` -Pick an editor which will open a text file. Scroll to the bottom and add the following line. - -``` bash -@reboot /home/pi/Desktop/carpark_agent/run_scripts/run_carpark_agent.sh -``` - -Save and reboot. The agent should now start automatically on reboot. - - -### Get the Pi's ip address - -We will need the ip address of the Raspberry Pi to connect remotely. - -``` bash -ifconfig -``` -Returns something like: -``` bash -... -wlan0: flags=4163 mtu 1500 - inet 192.168.11.9 netmask 255.255.255.0 broadcast 192.168.11.255 -... -``` -The `inet` value is the Raspberry Pi's ip address. - - - - -### Connect to the app remotely - -Download and install the VNC viewer onto your remote laptop: https://www.realvnc.com/en/connect/download/viewer/. - -Add the Pi's ip address. You will be prompted for the Raspberry Pi password. The Raspberry Pi's desktop should appear. - - -### Get your remote desktop ip - -Follow the instructions to get your remote ip address. - -### Connect to the Raspberry Pi - -Start up the agent, if it is not running - but it should be. - -``` bash -cd Desktop/carpark_agent -./run_scripts/run_carpark_agent.sh -``` - -# STOPPED HERE - Monday 28th October -# The Pi server app instructions come next - -From "When it starts up and you see the output from the camera, you can move your camera around so it is looking at the area... \ No newline at end of file diff --git a/docs/car-park.md b/docs/car-park.md index f24da01b6c..d83c54fc40 100644 --- a/docs/car-park.md +++ b/docs/car-park.md @@ -7,17 +7,19 @@ The Fetch.ai car park agent demo is documented in its own repo [here](https://gi First, create the carpark detection agent: -``` +``` bash aea create car_detector cd car_detector +aea add connection oef aea add skill carpark_detection aea install ``` Then, create the carpark client agent: -``` +``` bash aea create car_data_buyer cd car_data_buyer +aea add connection oef aea add skill carpark_client aea install aea generate-key fetchai @@ -25,7 +27,7 @@ aea add-key fetchai fet_private_key.txt ``` Add the ledger info to both aea configs: -``` +``` bash ledger_apis: - ledger_api: ledger: fetchai @@ -34,7 +36,7 @@ ledger_apis: ``` Fund the carpark client agent: -``` +``` bash cd .. python scripts/fetchai_wealth_generation.py --private-key car_data_buyer/fet_private_key.txt --amount 10000000000 --addr alpha.fetch-ai.com --port 80 ``` @@ -46,7 +48,7 @@ Then, in the carpark detection skill settings (`car_detector/skills/carpark_dete ``` Then, launch an OEF node instance: -``` +``` bash python scripts/oef/launch.py -c ./scripts/oef/launch_config.json ``` @@ -55,7 +57,7 @@ Finally, run both agents with `aea run`. You can see that the agents find each other, negotiate and eventually trade. When you're finished, delete your agents: -``` +``` bash cd .. aea delete car_detector aea delete car_data_buyer diff --git a/docs/cli-commands.md b/docs/cli-commands.md index d91d0b9d18..7c8d7fcb81 100644 --- a/docs/cli-commands.md +++ b/docs/cli-commands.md @@ -1,11 +1,31 @@ # CLI commands - +| Command | Description | +| ------------------------------------------- | ---------------------------------------------------------------------------- | +| `add connection/protocol/skill [name]` | Add connection, protocol, or skill, called `[name]`, to the agent. | +| `add-key default/fetchai/ethereum file` | Add a private key from a file. | +| `create NAME` | Create a new aea project called `[name]`. | +| `delete NAME` | Delete an aea project. See below for disabling a resource. | +| `fetch NAME` | Fetch an aea project called `[name]`. | +| `freeze` | Get all the dependencies needed for the aea project and its components. | +| `gui` | Run the GUI. | +| `generate-key default/fetchai/ethereum/all` | Generate private keys. | +| `install [-r ]` | Install the dependencies. (With `--install-deps` to install dependencies.) | +| `list protocols/connections/skills` | List the installed resources. | +| `remove connection/protocol/skill [name]` | Remove connection, protocol, or skill, called `[name]`, from agent. | +| `run {using [connection, ...]}` | Run the agent on the Fetch.ai network with default or specified connections. | +| `search protocols/connections/skills` | Search for components in the registry. | +| `scaffold connection/protocol/skill [name]` | Scaffold a new connection, protocol, or skill called `[name]`. | +| `-v DEBUG run` | Run with debugging. | + + -!!! Tip - You can also disable a resource without deleting it by removing the entry from the configuration but leaving the package in the skills namespace. - - +
+

Tip

+

You can also disable a resource without deleting it by removing the entry from the configuration but leaving the package in the skills namespace.

+
-
\ No newline at end of file +
diff --git a/docs/cli-gui.md b/docs/cli-gui.md index d690ff0b6e..f0d0c1db62 100644 --- a/docs/cli-gui.md +++ b/docs/cli-gui.md @@ -1,6 +1,6 @@ -You can invoke the AEA Command Line Interface (CLI) from a Graphical User Interface (GUI) accessed from a web browser. +You can invoke the AEA Command Line Interface (CLI) from a Graphical User Interface (GUI) accessed from a web browser. -These instructions will take you through building an agent, starting an OEF Node, and running the agent - all from the GUI. +These instructions will take you through building an agent, starting an OEF Node, and running the agent - all from the GUI. ## Preliminaries @@ -14,7 +14,7 @@ pip install aea[cli_gui] ## Starting the GUI -Go to the directory in which you will create new agents. If you followed the quick start guide, this will be `my_aea`. +Go to the directory in which you will create new agents. If you followed the quick start guide, this will be `my_aea`. Start the local web-server. ``` bash @@ -26,7 +26,7 @@ You should see the following page.
![new gui screen](assets/cli_gui01_clean.png)
-On the left-hand side we can see any agents you have created and beneath that the protocols, connections and skills they have. Initially this will be empty - unless you have run the quick start previously and not deleted those agents. +On the left-hand side we can see any agents you have created and beneath that the protocols, connections and skills they have. Initially this will be empty - unless you have followed the quick start guide previously and not deleted those agents. On the right-hand side is a search interface to the Registry which gives you access to protocols, connections, and skills which are available to add to your agent. @@ -38,20 +38,21 @@ To create a new agent and run it, follow these steps. 3. Click in the search input box and type "echo" 4. Click the [Search] button - this will list all the skills with echo in their name or description. Note that at present this search functionality is not working and it will list all the skills -
![gui sequence](assets/cli_gui02_sequence_02.png)
+
![gui sequence](assets/cli_gui02_sequence_02.png)
5. Find the Echo skill and click on it - this will select it. 6. Click on the [Add skill] button - which should now say "Add echo skill to my_new_agent agent". 7. Start an OEF Node by clicking on the [Start OEF Node] button. Wait for the text saying "A thing of beauty is a joy forever..." to appear. When you see that, the node has started successfully. -
![start node](assets/cli_gui03_oef_node.png)
+
![start node](assets/cli_gui03_oef_node.png)
8. Start the agent running by clicking on the [start agent] button. You should see the output from the echo agent appearing on the screen. -
![start agent](assets/cli_gui04_new_agent.png)
+
![start agent](assets/cli_gui04_new_agent.png)
- This is how your whole page should look if you followed the instructions correctly. +This is how your whole page should look if you followed the instructions correctly. -
![whole screen running](assets/cli_gui05_full_running_agent.png)
- -
+
![whole screen running](assets/cli_gui05_full_running_agent.png)
+ + +
diff --git a/docs/connection.md b/docs/connection.md index 245c27a65d..bbb7ddecf0 100644 --- a/docs/connection.md +++ b/docs/connection.md @@ -79,14 +79,14 @@ def send(self, envelope: Envelope): self.channel.send(envelope) ``` --> -### `from_config(cls, public_key: str, connection_configuration: ConnectionConfig)` +### `from_config(cls, address: Address, connection_configuration: ConnectionConfig)` diff --git a/docs/core-components.md b/docs/core-components.md index 1847e5cf78..3d6f591b56 100644 --- a/docs/core-components.md +++ b/docs/core-components.md @@ -26,7 +26,7 @@ The `Message` class in the `protocols/base.py` module provides an abstract class A number of protocols come packaged up with the AEA framework. * `default`: this protocol provides a bare bones implementation for an AEA protocol which includes a `DefaultMessage` class and a `DefaultSerialization` class with functions for managing serialisation. Use this protocol as a starting point for building custom protocols. -* `oef`: this protocol provides the AEA protocol implementation for communication with the OEF including an `OEFMessage` class for hooking up to OEF services and search agents. Utility classes are available in the `models.py` module which provides OEF specific requirements such as classes needed to perform querying on the OEF such as `Description`, `Query`, and `Constraint`, to name a few. +* `oef`: this protocol provides the AEA protocol implementation for communication with the OEF including an `OEFMessage` class for hooking up to OEF services and search agents. Utility classes are available in the `models.py` module which provides OEF specific requirements, such as classes, needed to perform querying on the OEF, such as `Description`, `Query`, and `Constraint`, to name a few. * `fipa`: this protocol provides classes and functions necessary for communication between AEAs via the [FIPA](http://www.fipa.org/repository/aclspecs.html) Agent Communication Language. For example, the `FIPAMessage` class provides negotiation terms such as `cfp`, `propose`, `decline`, `accept` and `match_accept`. diff --git a/docs/css/my-styles.css b/docs/css/my-styles.css index 452bfd1af9..0799b02f96 100644 --- a/docs/css/my-styles.css +++ b/docs/css/my-styles.css @@ -1,9 +1,9 @@ pre { - background-color: #f8f8f7; + background-color: #f8f8f7; } code { - background-color: #0083fb; + background-color: #0083fb; } /* this doesn't work now @@ -14,16 +14,20 @@ code { */ /* Katharine's css additions */ -.md-header, .md-tabs, .md-footer-meta, .md-footer-nav, .md-footer-nav__inner { - background-color: #172b6e; +.md-header, +.md-tabs, +.md-footer-meta, +.md-footer-nav, +.md-footer-nav__inner { + background-color: #172b6e; } .md-nav__title { - color: #172b6e; + color: #172b6e; } .md-icon { - ./assets/images/favicon.ico + ./assets/images/favicon.ico; } /* Needed so that Mermaid UML diagrams don't end up being massively tall */ diff --git a/docs/design-principles.md b/docs/design-principles.md index dd3ac85c5c..944c50a93f 100644 --- a/docs/design-principles.md +++ b/docs/design-principles.md @@ -1,12 +1,12 @@ Eight principles guide AEA framework development: * **Accessibility**: ease of use. -* **Modularity**: encourages module creation and sharing and reuse. -* **Openness**: easily extensible with third party libraries. +* **Modularity**: encourages module creation, sharing and reuse. +* **Openness**: easily extensible with third-party libraries. * **Conciseness**: conceptually simple. * **Value-driven**: drives immediate value. * **Low entry barriers**: leverages existing programming languages and web protocols. * **Safety**: safe for the user (economically speaking). -* **Goal alignment**: seamless facilitation of users' preferences and goals. +* **Goal-alignment**: seamless facilitation of users' preferences and goals.
\ No newline at end of file diff --git a/docs/diagram.md b/docs/diagram.md index e50e88e6c7..4cc67b5bb5 100644 --- a/docs/diagram.md +++ b/docs/diagram.md @@ -1,21 +1,21 @@ -!!! Note - Work in progress. - +
+

Note

+

Work in progress.

+
The framework has two distinctive parts. -* A **core** that is developed by the Fetch.ai team as well as external contributors. -* **extensions** (also known as **packages**) developed by any developer which promotes a modular and scalable framework. +- A **core** that is developed by the Fetch.ai team as well as external contributors. +- **Extensions** (also known as **packages**) developed by any developer which promotes a modular and scalable framework. -Currently, the framework supports three types of packages which can be added to the core as modules. +Currently, the framework supports three types of packages which can be added to the core as modules: -* Skills -* Protocols -* Connections +- Skills +- Protocols +- Connections -The following figure illustrates the framework's architecture. +The following figure illustrates the framework's architecture:
![The AEA Framework Architecture](assets/framework-architecture.png)
- -
\ No newline at end of file +
diff --git a/docs/fipa-skill.md b/docs/fipa-skill.md deleted file mode 100644 index bce93c43ba..0000000000 --- a/docs/fipa-skill.md +++ /dev/null @@ -1,131 +0,0 @@ -!!! Note - Work in progress. - -The AEA FIPA skill demonstrates how FIPA negotiation strategies may be embedded into an Autonomous Economic Agent. - -## Configuration - -The FIPA skill `skill.yaml` configuration file looks like this. - -``` yaml -name: 'fipa_negotiation' -authors: Fetch.ai Limited -version: 0.1.0 -license: Apache 2.0 -url: "" -behaviours: - - behaviour: - class_name: GoodsRegisterAndSearchBehaviour - args: - services_interval: 5 -handlers: - - handler: - class_name: FIPANegotiationHandler - args: {} -tasks: - - task: - class_name: TransactionCleanUpTask - args: {} -shared_classes: - - shared_class: - class_name: Search - args: {} - - shared_class: - class_name: Strategy - args: {} - - shared_class: - class_name: Dialogues - args: {} - - shared_class: - class_name: Transactions - args: - pending_transaction_timeout: 30 -protocols: ['oef', 'fipa'] -``` - -Above, you can see the registered `Behaviour` class name `GoodsRegisterAndSearchBehaviour` which implements register and search behaviour of an AEA for the FIPA skill. - -The `FIPANegotiationHandler` deals with receiving `FIPAMessage` types containing FIPA negotiation terms, such as `cfp`, `propose`, `decline`, `accept` and `match_accept`. - -The `TransactionCleanUpTask` takes care of removing potential transaction of different degrees of commitment from the potential transactions list if they are unlikely to be settled. - -## Shared classes - -The `shared_classes` element in the configuration `yaml` lists a number of important classes for agents communicating via the FIPA skill. - -### Search - -This class abstracts the logic required by agents performing searches for other buying/selling agents according to strategy (see below). - -### Strategy - -This class defines the strategy behind an agent's activities. - -The class is instantiated with the agent's goals, for example whether the agent intends to buy/sell something, and is therefore looking for other sellers, buyers, or both. - -It also provides methods for defining what goods agents are looking for and what goods they may have to sell, for generating proposal queries, and checking whether a proposal is profitable or not. - -### Dialogue - -`Dialogues` abstract the negotiations that take place between agents including all negotiation end states, such as accepted, declined, etc. and all the negotiation states in between. - -### Transactions - -This class deals with representing potential transactions between agents. - - -## Demo instructions - -!!! Warn - FIPA negotiation skill is not fully developed. - - -Follow the Preliminaries and Installation instructions here. - - -Then, download the examples and packages directory. -``` bash -svn export https://github.com/fetchai/agents-aea.git/trunk/packages -``` - -### Create the agent -In the root directory, create the FIPA agent. -``` bash -aea create my_fipa_agent -``` - - -### Add the FIPA skill -``` bash -cd my_fipa_agent -aea add skill fipa_negotiation -``` - -### Add the local connection -``` bash -aea add connection local -``` - -### Run the agent with the default connection - -``` bash -aea run --connection local -``` - - - -### Delete the agent - -When you're done, go up a level and delete the agent. - -``` bash -cd .. -aea delete my_fipa_agent -``` - - -
\ No newline at end of file diff --git a/docs/gym-skill.md b/docs/gym-skill.md index 0076f3d73a..e1a20b9f67 100644 --- a/docs/gym-skill.md +++ b/docs/gym-skill.md @@ -13,6 +13,7 @@ In the root directory, create the gym agent. aea create my_gym_agent ``` + ### Add the gym skill ``` bash cd my_gym_agent @@ -50,7 +51,7 @@ aea install ### Run the agent with the gym connection ``` bash -aea run --connection gym +aea run --connections gym ``` You will see the gym training logs. diff --git a/docs/hacking-an-agent.md b/docs/hacking-an-agent.md index 2ae6c6edd6..bc871f59cf 100644 --- a/docs/hacking-an-agent.md +++ b/docs/hacking-an-agent.md @@ -1,97 +1,222 @@ ## Preliminaries -These instructions detail the Python code you need for running an AEA outside the `cli` tool. +These instructions detail the Python code you need for running an AEA outside the `cli` tool, using the code interface. -!!! Note - You have already coded up your agent. See the build your own skill guide for a reminder. + +This guide assumes you have already followed the Preliminaries and Installation section in the [quick start](quickstart.md) guide and so have the framework installed and the packages and scripts directory downloaded into the directory you are working in. +## Create private key +Before we make our agent we make a private key to be used in the wallet. Open a terminal and type: + +``` bash +python scripts/generate_private_key.py my_key.txt +``` + ## Imports +Now we can start to write our python script. + First, import the necessary common Python libraries and classes. ``` python import os import time from threading import Thread +import yaml ``` Then, import the application specific libraries. ``` python -from aea.aea import AEA +from aea.aea import AEA +from aea import AEA_DIR +from aea.skills.base import Skill from aea.connections.stub.connection import StubConnection from aea.crypto.ledger_apis import LedgerApis from aea.crypto.wallet import Wallet -from aea.mail.base import Envelope -from aea.protocols.default.message import DefaultMessage from aea.protocols.default.serialization import DefaultSerializer from aea.registries.base import Resources +from aea.configurations.base import ProtocolConfig +from aea.protocols.base import Protocol ``` +Set up a variable pointing to where the packages directory is located - this should be our current directory. +``` python +root_dir = "./" +``` +## Clearing the input and output files +We will use the stub connection to pass envelopes in and out of the agent. Ensure that any input and output text files are removed before we start. +``` python +# Ensure the input and output files do not exist initially +input_filename = "input.txt" +output_filename = "output.txt" +if os.path.isfile(input_filename): + os.remove(input_filename) +if os.path.isfile(output_filename): + os.remove(output_filename) +``` ## Initialise the agent +We use the private key file we created to initialise a wallet, we also create the stub connection, tell it what Ledger APIs we are going to use (none in this example) and create a resources object containing skills, protocols and connections (this is initially empty). -Create a private key. -``` bash -python scripts/generate_private_key.py my_key.txt +Then we pass all of this into the AEA constructor to create our agent. +``` python +# Set up the wallet, stub connection, ledger and (empty) resources +wallet = Wallet({'default': 'my_key.txt'}) +stub_connection = StubConnection(input_file_path=input_filename, output_file_path=output_filename) +ledger_apis = LedgerApis({}) +resources = Resources('') + +# Create our AEA +my_agent = AEA("my_agent", [stub_connection], wallet, ledger_apis, resources) ``` -Create a wallet object with a private key. +Create the default protocol and add it to the agent ``` python -wallet = Wallet({'default': 'my_key.txt'}) +# Add the default protocol +default_protocol_configuration = ProtocolConfig.from_json( + yaml.safe_load(open(os.path.join(AEA_DIR, "protocols", "default", "protocol.yaml")))) +default_protocol = Protocol("default", DefaultSerializer(), default_protocol_configuration) +resources.protocol_registry.register(("default", None), default_protocol) ``` -Create a `Connection`. +Create the error skill (needed by all agents) and the echo skill which will bounce our messages back to us ``` python -stub_connection = StubConnection(input_file_path='input.txt', output_file_path='output.txt') +# Add the error skill and the echo skill +echo_skill = Skill.from_dir(os.path.join(root_dir, "packages", "skills", "echo"), my_agent.context) +resources.add_skill(echo_skill) +error_skill = Skill.from_dir(os.path.join(AEA_DIR, "skills", "error"), my_agent.context) +resources.add_skill(error_skill) ``` -For ledger APIs, we simply feed the agent an empty dictionary (meaning we do not require any). +## Start the agent +We run the agent from a different thread so that we can still use the main thread to pass it messages. ``` python -ledger_apis = LedgerApis({}) +# Set the agent running in a different thread +t = Thread(target=my_agent.start) +t.start() + +# Wait for everything to start up +time.sleep(4) ``` -Create the resources pointing to the working directory. +## Send and receive an envelope +We use the input and output text files to send an envelope to our agent and receive a response (from the echo skill) ``` python -resources = Resources('') +# Create a message inside an envelope and get the stub connection to pass it on to the echo skill +message_text = 'my_agent,other_agent,default,{"type": "bytes", "content": "aGVsbG8="}' +with open(input_filename, 'w') as f: + f.write(message_text) + print("input message: " + message_text) + +# Wait for the envelope to get processed +time.sleep(4) + +# Read the output envelope generated by the echo skill +with open(output_filename, 'r') as f: + print("output message: " + f.readline()) ``` -Now we have everything we need for initialisation. +## Shutdown +Finally stop our agent and wait for it to finish ``` python -my_agent = AEA("my_agent", stub_connection, wallet, ledger_apis, resources) +# Shut down the agent +my_agent.stop() +t.join() ``` -## Add skills and protocols +## Running the agent +If you now run this python script file, you should see this output: -We can add the echo skill as follows... + No protocol found. + No skill found. + input message: my_agent,other_agent,default,{"type": "bytes", "content": "aGVsbG8="} + output message: other_agent,my_agent,default,{"type": "bytes", "content": "aGVsbG8="} -!!! Note - Work in progress. -## Run the agent +## Entire code listing +If you just want to copy and past the entire script in you can find it here: -Create a thread and add the agent to it. +
Click here to see full listing +

-``` python -t = Thread(target=my_agent.start) -``` +```python +import os +import time +from threading import Thread +import yaml -Start the agent. +from aea.aea import AEA +from aea import AEA_DIR +from aea.skills.base import Skill +from aea.connections.stub.connection import StubConnection +from aea.crypto.ledger_apis import LedgerApis +from aea.crypto.wallet import Wallet +from aea.protocols.default.serialization import DefaultSerializer +from aea.registries.base import Resources +from aea.configurations.base import ProtocolConfig +from aea.protocols.base import Protocol -``` python +root_dir = "./" + +# Ensure the input and output files do not exist initially +input_filename = "input.txt" +output_filename = "output.txt" +if os.path.isfile(input_filename): + os.remove(input_filename) +if os.path.isfile(output_filename): + os.remove(output_filename) + +# set up the Wallet, stub connection, ledger and (empty) resources +wallet = Wallet({'default': 'my_key.txt'}) +stub_connection = StubConnection(input_file_path=input_filename, output_file_path=output_filename) +ledger_apis = LedgerApis({}) +resources = Resources('') + +# Create our AEA +my_agent = AEA("my_agent", [stub_connection], wallet, ledger_apis, resources) + +# Add the default protocol +default_protocol_configuration = ProtocolConfig.from_json( + yaml.safe_load(open(os.path.join(AEA_DIR, "protocols", "default", "protocol.yaml")))) +default_protocol = Protocol("default", DefaultSerializer(), default_protocol_configuration) +resources.protocol_registry.register(("default", None), default_protocol) + +# Add the error skill and the echo skill +echo_skill = Skill.from_dir(os.path.join(root_dir, "packages/", "skills", "echo"), my_agent.context) +resources.add_skill(echo_skill) +error_skill = Skill.from_dir(os.path.join(AEA_DIR, "skills", "error"), my_agent.context) +resources.add_skill(error_skill) + +# Set the agent running in a different thread +t = Thread(target=my_agent.start) t.start() -``` -## Terminate the agent +# Wait for everything to start up +time.sleep(4) -Finalise the agent thread. +# Create a message inside an envelope and get the stub connection to pass it on to the echo skill +message_text = 'my_agent,other_agent,default,{"type": "bytes", "content": "aGVsbG8="}' +with open(input_filename, 'w') as f: + f.write(message_text) + print("input message: " + message_text) -``` python +# Wait for the envelope to get processed +time.sleep(4) + +# Read the output envelope generated by the echo skill +with open(output_filename, 'r') as f: + print("output message: " + f.readline()) + +# Shut down the agent my_agent.stop() t.join() t = None + ``` +

+

\ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 45674a67fd..b4cdc322fd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,33 +1,18 @@ -## Software to work for you +## Autonomous Economic Agent Framework -Do you want to create software to work for you and enrich your life? +The AEA framework provides the tools for creating Autonomous Economic Agents (AEA). AEAs work continuously for your benefit without you having to do anything more than writing them and starting them up. -Autonomous Economic Agents (AEAs) work continuously for your benefit without you having to do anything more than write them and start them up. +AEAs act independently of constant input and autonomously develop new capabilities by acquiring new skills. Their goal is to create economic value for you, their owner. AEAs have a wide range of application areas. Check out the Demos section from the left menu for some examples. -AEAs act independently of constant input and autonomously develop new capabilities. Their goal is to create economic gain for you, their owner. +The AEA framework is a Python-based development suite which equips you with an efficient and easy to understand set of tools for building autonomous economic agents. The framework is truly modular, easily extensible, and highly composable. This framework attempts to make agent development as straightforward an experience as possible, similar to web development using popular web frameworks. -AEAs have a wide range of application areas. Check out the demo section for examples. +To start learning about some of the distinctive characteristics of agent-oriented development, check out the next page. -Bridging Web 2.0 to Web 3.0, AEAs are the future, now. - - -## More specifically - -The AEA framework provides the tools for creating autonomous economic agents. - -It is a Python-based development suite which equips you with an efficient and easy to understand set of tools for building autonomous economic agents. - -The framework is super modular, easily extensible, and highly composable. - -The AEA framework attempts to make agent development as straightforward as web development using popular web frameworks. - -AEAs achieve their goals with the help of the Fetch.ai OEF - a search and discovery platform for agents - and the Fetch.ai blockchain. Third party systems, such as Ethereum, may also allow AEA integration. - - -!!! Note - This developer documentation is a work in progress. If you spot any errors please open an issue [here](https://github.com/fetchai/agents-aea). +AEAs achieve their goals with the help of the OEF - a search and discovery platform for agents by Fetch.ai - and using Fetch.ai's blockchain as a financial system. Third-party systems, such as Ethereum, may also allow AEA integration. +
+

Note

+

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

+

- - diff --git a/docs/integration.md b/docs/integration.md index 5a596e9620..89b9cafacd 100644 --- a/docs/integration.md +++ b/docs/integration.md @@ -1,16 +1,17 @@ -In this section, we show you how to integrate the AEA with the Fetch.ai and third party ledgers. - +In this section, we show you how to integrate the AEA with the Fetch.ai and third-party ledgers. ## Fetch.ai Ledger -!!! Note - Coming soon. - +
+

Note

+

Coming soon.

+
## Ethereum Ledger -!!! Note - Coming soon. - +
+

Note

+

Coming soon.

+
-
\ No newline at end of file +
diff --git a/docs/ml-skills.md b/docs/ml-skills.md new file mode 100644 index 0000000000..1c0d6133fd --- /dev/null +++ b/docs/ml-skills.md @@ -0,0 +1,159 @@ +The AEA ml skills demonstrate an interaction between two AEAs trading data. + +There are two types of agents: + +* The ml data provider which sells training data. +* The ml model trainer which trains a model + +### Dependencies + +Follow the Preliminaries and Installation sections from the AEA quick start. + + +## Launch an OEF node +In a separate terminal, launch a local OEF node (for search and discovery). +``` bash +python scripts/oef/launch.py -c ./scripts/oef/launch_config.json +``` + +Keep it running for all the following demos. + +## Demo 1 - no ledger: + + +### Create the data provider AEA +In the root directory, create the data provider AEA. +``` bash +aea create ml_data_provider +``` + +### Add the `ml_data_provider` skill +``` bash +cd ml_data_provider +aea add connection oef +aea add skill ml_data_provider +``` + +### Install the dependencies +The ml data provider uses `tensorflow` and `numpy`. +``` bash +aea install +``` + +### Run the data provider AEA +``` bash +aea run --connections oef +``` + +### Create the model trainer AEA +In a separate terminal, in the root directory, create the model trainer AEA. +``` bash +aea create ml_model_trainer +``` + +### Add the `ml_train` skill to the model trainer AEA +``` bash +cd ml_model_trainer +aea add connection oef +aea add skill ml_train +``` + +### Install the dependencies +The ml data provider uses `tensorflow` and `numpy`. +``` bash +aea install +``` + +### Run the model trainer AEA +``` bash +aea run --connections oef +``` + +After some time, you should see the AEAs transact and the model trainer train its model. + + +## Demo 2 - ledger: + + +We will now run the same demo but with a real ledger transaction on test net. + +### Update the AEA configs + +Both in `ml_model_trainer/aea-config.yaml` and +`ml_data_provider/aea-config.yaml`, replace `ledger_apis: []` with the following. + +``` yaml +ledger_apis: + - ledger_api: + ledger: fetchai + addr: alpha.fetch-ai.com + port: 80 +``` + +### Fund the ml model trainer AEA + +Create some wealth for your ml model trainer on the Fetch.ai `testnet`. (It takes a while). +``` bash +cd .. +python scripts/fetchai_wealth_generation.py --private-key ml_model_trainer/fet_private_key.txt --amount 10000000 --addr alpha.fetch-ai.com --port 80 +cd ml_model_trainer +``` + +### Update the ml model trainer AEA skills config + +We tell the ml model trainer skill to use the ledger, by updating the following field in `ml_model_trainer/skills/ml_train/skill.yaml`: +``` bash +is_ledger_tx: True +``` + +### Run both AEAs + +From their respective directories, run both AEAs +``` bash +aea run --connections oef +``` + + +### Clean up +``` bash +cd .. +aea delete ml_data_provider +aea delete ml_model_trainer +``` + + +### Communication +This diagram shows the communication between the two agents. + +
+ sequenceDiagram + participant ml_model_trainer + participant ml_data_provider + participant Search + participant Ledger + + activate ml_model_trainer + activate ml_data_provider + activate Search + activate Ledger + + ml_data_provider->>Search: register_service + ml_model_trainer->>Search: search + Search-->>ml_model_trainer: list_of_agents + ml_model_trainer->>ml_data_provider: call_for_terms + ml_data_provider->>ml_model_trainer: terms + ml_model_trainer->>Ledger: request_transaction + ml_model_trainer->>ml_data_provider: accept (incl transaction_hash) + ml_data_provider->>Ledger: check_transaction_status + ml_data_provider->>ml_model_trainer: data + loop train + ml_model_trainer->>ml_model_trainer: tran_model + end + + deactivate ml_model_trainer + deactivate ml_data_provider + deactivate Search + deactivate Ledger + +
+ diff --git a/docs/protocol.md b/docs/protocol.md index 4a6ed45cc2..58831e880c 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -2,7 +2,6 @@ A `Protocol` manages message representation, encoding, and serialisation. It als An agent can have one or more protocols. The AEA framework supplies three: `oef`, `fipa`, and a `default` protocol. - ## Custom protocol For a custom protocol, the developer must code methods from two classes. @@ -12,45 +11,47 @@ For a custom protocol, the developer must code methods from two classes. This method checks the message data for consistency and raises an error if necessary. !!! TODO - For example. +For example. ### `Serializer.encode(self, msg: Message)` This method encodes a message object into bytes for passing around. !!! TODO - For example. +For example. ### `Serializer.decode(self, obj: bytes)` This method decodes the byte representation of a message object. !!! TODO - For example. +For example. Outside of these, the developer is free to implement the agent protocols in any way they see fit. ### `rules.py` -!!! Note - Coming soon. - - +
+

Note

+

Coming soon.

+
## `oef` protocol -The `oef` helps agents to search for and find other agents and (for now) talk to them via different protocols. +The `oef` helps agents to search for and find other agents and (for now) talk to them via different protocols. -!!! Note - In future, the framework will support peer to peer communications. +
+

Note

+

In future, the framework will support peer to peer communications.

+
The `oef` protocol definition includes an `OEFMessage` class which gets a `protocol_id` of `oef`. It defines OEF agent delegation by way of a `MessageType` Enum. -``` python +```python class Type(Enum): - + """OEF Message types.""" REGISTER_SERVICE = "register_service" UNREGISTER_SERVICE = "unregister_service" @@ -64,11 +65,12 @@ class Type(Enum): """Get string representation.""" return self.value ``` + It also provides error codes. -``` python +```python class OEFErrorOperation(Enum): - + """Operation code for the OEF. It is returned in the OEF Error messages.""" REGISTER_SERVICE = 0 UNREGISTER_SERVICE = 1 @@ -79,10 +81,8 @@ class OEFErrorOperation(Enum): OTHER = 10000 ``` -A `models.py` module is provided by the `oef` protocol which includes classes and methods commonly required by OEF agents. These includes a class for serialising json and classes for implementing the OEF query language such as `Attribute`, `Query`, etc. - - +A `models.py` module is provided by the `oef` protocol which includes classes and methods commonly required by OEF agents. These includes a class for serialising json and classes for implementing the OEF query language such as `Attribute`, `Query`, etc. ## `fipa` protocol @@ -90,9 +90,9 @@ The `fipa` protocol definition includes a `FIPAMessage` class which gets a `prot It defines FIPA negotiating terms by way of a `Performative(Enum)`. -``` python +```python class Performative(Enum): - + """FIPA performatives.""" CFP = "cfp" PROPOSE = "propose" @@ -107,13 +107,14 @@ class Performative(Enum): `FIPAMessages` are constructed with a `message_id`, a `dialogue_id`, a `target` and `peformative`. -``` python -super().__init__(id=message_id, dialogue_id=dialogue_id, target=target, +```python +super().__init__(id=message_id, dialogue_id=dialogue_id, target=target, performative=FIPAMessage.Performative(performative), **kwargs) ``` + The `fipa.proto` file then further qualifies the performatives for `protobuf` messaging. -``` java +```java syntax = "proto3"; package fetch.aea.fipa; @@ -148,7 +149,6 @@ message FIPAMessage{ } ``` - ## `default` protocol The `default` protocol has a `DefaultMessage` class which gets a `protocol_id` of `default`. @@ -157,12 +157,4 @@ It has two message types: `BYTES` and `ERROR`, and provides error messages for t The serialisation methods `encode` and `decode` implement transformations from `Message` type to bytes and back. - - - - - - - -
diff --git a/docs/quickstart.md b/docs/quickstart.md index e4abf7435a..1caa300832 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -9,7 +9,7 @@ cd my_aea/ We highly recommend using a virtual environment to ensure consistency across dependencies. -Check you have [`pipenv`](https://github.com/pypa/pipenv). +Check that you have [`pipenv`](https://github.com/pypa/pipenv). ``` bash which pipenv @@ -53,14 +53,14 @@ pip install aea ``` --> -The following installs the entire AEA package which includes the cli too. +The following installs the entire AEA package which also includes a command-line interface (CLI). ``` bash pip install aea[all] ``` -However, you can run this demo by installing the base AEA, including the CLI (Command Line Interface) extension, alone. +However, you can run this demo by installing the base AEA, including the CLI extension, alone. ``` bash pip install aea[cli] @@ -70,7 +70,7 @@ pip install aea[cli] ### Known issues -If the installation steps fail, it might a dependency issue. +If the installation steps fail, it might be a dependency issue. The following hints can help: @@ -100,19 +100,17 @@ cd my_first_agent aea add skill echo ``` -This copies the echo application code for the behaviours, handlers, and tasks into the skill, ready to run. +This copies the echo application code containing the "behaviours", "handlers", and "tasks" into the skill, ready to run. ### Add a stub connection -AEAs use messages for communication. We will add a stub connection to send messages to and receive messages from the AEA. +AEAs use messages for communication. We use a stub connection to send messages to and receive messages from the AEA. -``` bash -aea add connection stub -``` +The stub conection is already added to the agent by default. -A stub connection provides an I/O reader/writer. +A stub connection provides an I/O reader and writer. -It uses two files for communication: one for the incoming messages and the other for the outgoing messages. Each line contains an encoded envelope. +It uses two files for communication: one for incoming messages and the other for outgoing messages. Each line contains an encoded envelope. The AEA waits for new messages posted to the file `my_first_agent/input_file`, and adds a response to the file `my_first_agent/output_file`. @@ -133,7 +131,7 @@ recipient_agent,sender_agent,default,{"type": "bytes", "content": "aGVsbG8="} Run the agent with the `stub` connection. ``` bash -aea run --connection stub +aea run ``` You will see the echo task running in the terminal window. @@ -173,7 +171,7 @@ Stop the agent by pressing `CTRL C` ### Delete the agent -Delete the agent from the parent directory via `cd ..`. +Delete the agent from the parent directory (`cd ..` to go to the parent directory). ``` bash aea delete my_first_agent diff --git a/docs/skill-guide.md b/docs/skill-guide.md index 4153d5828d..9471cf2aac 100644 --- a/docs/skill-guide.md +++ b/docs/skill-guide.md @@ -1,8 +1,9 @@ The scaffolding tool allows you to create the folder structure required for a skill. -!!! Note - Before developing your first skill, please read the skill guide. - +
+

Note

+

Before developing your first skill, please read the skill guide.

+
### Dependencies @@ -12,21 +13,20 @@ Follow the Preliminaries and diff --git a/docs/skill.md b/docs/skill.md index b1b3b7522b..a484a186ff 100644 --- a/docs/skill.md +++ b/docs/skill.md @@ -12,7 +12,7 @@ This means it is possible to, at any point, grab the `context` and have access t For example, in the `ErrorHandler(Handler)` class, the code often grabs a reference to its context and by doing so can access initialised and running framework objects such as an `OutBox` for putting messages into. ``` python -self.context.outbox.put_message(to=recipient, sender=self.context.agent_public_key,protocol_id=DefaultMessage.protocol_id, message=DefaultSerializer().encode(reply)) +self.context.outbox.put_message(to=recipient, sender=self.context.agent_address, protocol_id=DefaultMessage.protocol_id, message=DefaultSerializer().encode(reply)) ``` Importantly, however, a skill does not have access to the context of another skill or protected AEA components like the `DecisionMaker`. diff --git a/docs/tac-skills.md b/docs/tac-skills.md index 477829616d..f4c7bd0981 100644 --- a/docs/tac-skills.md +++ b/docs/tac-skills.md @@ -30,7 +30,9 @@ aea create tac_controller ### Add the tac control skill ``` bash cd tac_controller +aea add connection oef aea add skill tac_control +aea install ``` ### Update the game parameters @@ -40,7 +42,7 @@ You must set the start time to a point in the future `start_time: Nov 10 2019 1 ### Run the TAC controller AEA ``` bash -aea run +aea run --connections oef ``` ### Create the TAC participants AEA @@ -53,20 +55,24 @@ aea create tac_participant_two ### Add the tac participation skill to participant one ``` bash cd tac_participant_one +aea add connection oef aea add skill tac_participation aea add skill tac_negotiation +aea install ``` ### Add the tac participation skill to participant two ``` bash cd tac_participant_two +aea add connection oef aea add skill tac_participation aea add skill tac_negotiation +aea install ``` ### Run both the TAC participant AEAs ``` bash -aea run +aea run --connections oef ``` !!! Note @@ -108,31 +114,130 @@ This diagram shows the communication between the two agents and the controller.
sequenceDiagram - participant Searching_Agent + participant Buyer_Agent participant Seller_Agent + participant Search participant Controller - activate Searching_Agent + activate Buyer_Agent activate Seller_Agent + activate Search activate Controller - Searching_Agent->>Controller: search - Controller-->>Searching_Agent: list_of_agents - Searching_Agent->>Seller_Agent: call_for_proposal - Seller_Agent->>Searching_Agent: proposal - Searching_Agent->>Seller_Agent: accept - Searching_Agent->>Controller: request_transaction - Seller_Agent->>Searching_Agent: match_accept - Seller_Agent->>Controller: request_transaction - Controller->>Controller: transfer_funds + Seller_Agent->>Search: register_service + Buyer_Agent->>Search: search + Search-->>Buyer_Agent: list_of_agents + Buyer_Agent->>Seller_Agent: call_for_proposal + Seller_Agent->>Buyer_Agent: proposal + Buyer_Agent->>Seller_Agent: accept + Seller_Agent->>Buyer_Agent: match_accept + Seller_Agent->>Controller: transaction + Controller->>Controller: transaction_execution + Controller->>Seller_Agent: confirm_transaction + Controller->>Buyer_Agent: confirm_transaction - deactivate Searching_Agent + deactivate Buyer_Agent deactivate Seller_Agent + deactivate Search deactivate Controller
-In the above case, the proposal received contains a set of good which the seller wishes to sell and a cost of them. The Searching Agent needs to determine if this is a good deal for them and if so, it accepts. +In the above case, the proposal received contains a set of good which the seller wishes to sell and a cost of them. The buyer agent needs to determine if this is a good deal for them and if so, it accepts. + +There is an equivalent diagram for seller agents set up to search for buyers and their interaction with agents which are registered as buyers. In that scenario, the proposal will instead, be a list of goods that the buyer wishes to buy and the price it is willing to pay for them. + + +## Negotiation skill - deep dive + +The AEA `tac_negotiation` skill demonstrates how negotiation strategies may be embedded into an Autonomous Economic Agent. + +The `tac_negotiation` skill `skill.yaml` configuration file looks like this. + +```yaml +name: 'tac_negotiation' +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +description: "The tac negotiation skill implements the logic for an AEA to do fipa negotiation in the TAC." +url: "" +behaviours: + - behaviour: + class_name: GoodsRegisterAndSearchBehaviour + args: + services_interval: 5 +handlers: + - handler: + class_name: FIPANegotiationHandler + args: {} + - handler: + class_name: TransactionHandler + args: {} + - handler: + class_name: OEFSearchHandler + args: {} +tasks: + - task: + class_name: TransactionCleanUpTask + args: {} +shared_classes: + - shared_class: + class_name: Search + args: + search_interval: 5 + - shared_class: + class_name: Registration + args: + update_interval: 5 + - shared_class: + class_name: Strategy + args: + register_as: both + search_for: both + - shared_class: + class_name: Dialogues + args: {} + - shared_class: + class_name: Transactions + args: + pending_transaction_timeout: 30 +protocols: ['oef', 'fipa'] +``` + +Above, you can see the registered `Behaviour` class name `GoodsRegisterAndSearchBehaviour` which implements register and search behaviour of an AEA for the `tac_negotiation` skill. + +The `FIPANegotiationHandler` deals with receiving `FIPAMessage` types containing FIPA negotiation terms, such as `cfp`, `propose`, `decline`, `accept` and `match_accept`. + +The `TransactionHandler` deals with `TransactionMessage`s received from the decision maker component. The decision maker component is responsible for cryptoeconomic security. + +The `OEFSearchHandler` deals with `OEFMessage` types returned from the OEF search nodes. + +The `TransactionCleanUpTask` is responsible for cleaning up transactions which are no longer likely to being settled with the controller agent. + +## Shared classes + +The `shared_classes` element in the configuration `yaml` lists a number of important classes which are shared between the handlers, behaviours and tasks. + +### Search + +This class abstracts the logic required by agents performing searches for other buying/selling agents according to strategy (see below). + +### Registration + +This class abstracts the logic required by agents performing service registrations on the OEF. + +### Strategy + +This class defines the strategy behind an agent's activities. + +The class is instantiated with the agent's goals, for example whether the agent intends to buy/sell something, and is therefore looking for other sellers, buyers, or both. + +It also provides methods for defining what goods agents are looking for and what goods they may have to sell, for generating proposal queries, and checking whether a proposal is profitable or not. + +### Dialogue + +`Dialogues` abstract the negotiations that take place between agents including all negotiation end states, such as accepted, declined, etc. and all the negotiation states in between. -There is an equivilent diagram for agents set up to search for buyers and their interaction with agents which are registered as buyers. In that scenario, the proposal will instead, be a list of goods that the buyer wishes to buy and the price it is willing to pay for them. +### Transactions +This class deals with representing potential transactions between agents. diff --git a/docs/trust.md b/docs/trust.md index 29fc277e04..da7e62a1c1 100644 --- a/docs/trust.md +++ b/docs/trust.md @@ -2,12 +2,11 @@ AEA applications operate within different orders of trustlessness. For example, using the AEA weather skills demo without a ledger means that clients must trust that any data the weather station sends is sufficient, including no data at all. Similarly, the weather station must trust the weather clients to send payment via some mechanism. -A step up, if you run the weather skills demo with a ledger (Fetch.ai or Ethereum) then the clients must again trust the weather station to send sufficient data. However, all payment transactions are executed via the public ledger. This means the weather station need no longer trust the weather clients as it can observe the transactions taking place on the public ledger. +A step up, if you run the weather skills demo with a ledger (Fetch.ai or Ethereum) then the clients must again trust the weather station to send sufficient data. However, all payment transactions are executed via the public ledger. This means the weather station no longer needs to trust the weather clients as it can observe the transactions taking place on the public ledger. -We can expand trustlessness even further by incorporating a third party as an arbitrator or some escrow contract. However, in the weather skills demo there are limits to trustlessness as the station ultimately offers unverifiable data. +We can expand trustlessness even further by incorporating a third-party as an arbitrator or some escrow contract. However, in the weather skills demo there are limits to trustlessness as the station ultimately offers unverifiable data. Finally, in the case of (non-fungible) token transactions where there is an atomic swap, full trustlessness is apparent. This is demonstrated in the TAC. -
diff --git a/docs/version.md b/docs/version.md index 66847a0be1..b1eb074e75 100644 --- a/docs/version.md +++ b/docs/version.md @@ -1,7 +1,7 @@ -The current version of the Autonomous Economic Agent framework is `0.1.x`. The framework is under rapid development with frequent breaking changes. +The current version of the Autonomous Economic Agent framework is `0.1.15`. The framework is under rapid development with frequent breaking changes. To check which version you have installed locally, run -` +```bash aea --version -` \ No newline at end of file +``` \ No newline at end of file diff --git a/docs/weather-skills.md b/docs/weather-skills.md index 53b849ea8e..9d1a617b4d 100644 --- a/docs/weather-skills.md +++ b/docs/weather-skills.md @@ -28,16 +28,18 @@ aea create my_weather_station ``` -### Add the weather station skill +### Add the oef connection and the weather station skill ``` bash cd my_weather_station +aea add connection oef aea add skill weather_station +aea install ``` ### Run the weather station AEA ``` bash -aea run +aea run --connections oef ``` @@ -48,16 +50,18 @@ aea create my_weather_client ``` -### Add the weather client skill +### Add the oef connection and the weather client skill ``` bash cd my_weather_client +aea add connection oef aea add skill weather_client +aea install ``` ### Run the weather client AEA ``` bash -aea run +aea run --connections oef ``` @@ -94,7 +98,7 @@ This diagram shows the communication between the various entities as data is suc Weather_AEA->>Search: register_service Client_AEA->>Search: search - Search->>Client_AEA: list_of_agents + Search-->>Client_AEA: list_of_agents Client_AEA->>Weather_AEA: call_for_proposal Weather_AEA->>Client_AEA: propose Client_AEA->>Weather_AEA: accept @@ -120,7 +124,9 @@ Create the AEA that will provide weather measurements. ``` bash aea create my_weather_station cd my_weather_station +aea add connection oef aea add skill weather_station_ledger +aea install ``` ### Create the weather client (ledger version) @@ -130,7 +136,9 @@ In another terminal, create the AEA that will query the weather station. ``` bash aea create my_weather_client cd my_weather_client +aea add connection oef aea add skill weather_client_ledger +aea install ``` Additionally, create the private key for the weather client AEA. @@ -165,7 +173,7 @@ cd my_weather_client Run both AEAs from their respective terminals. ``` bash -aea run +aea run --connections oef ``` You will see that the AEAs negotiate and then transact using the Fetch.ai `testnet`. @@ -191,7 +199,9 @@ Create the AEA that will provide weather measurements. ``` bash aea create my_weather_station cd my_weather_station +aea add connection oef aea add skill weather_station_ledger +aea install ``` ### Create the weather client (ledger version) @@ -201,7 +211,9 @@ In another terminal, create the AEA that will query the weather station. ``` bash aea create my_weather_client cd my_weather_client +aea add connection oef aea add skill weather_client_ledger +aea install ``` Additionally, create the private key for the weather client AEA. @@ -225,9 +237,9 @@ ledger_apis: ### Update the skill configs -In the weather station skill config (`my_weather_station/skills/weather_station_ledger/skill.yaml`) under strategy, amend the `currency_pbk` and `ledger_id` as follows. +In the weather station skill config (`my_weather_station/skills/weather_station_ledger/skill.yaml`) under strategy, amend the `currency_id` and `ledger_id` as follows. ``` bash -currency_pbk: 'ETH' +currency_id: 'ETH' ledger_id: 'ethereum' ``` Amend `ledgers` to the following. @@ -235,10 +247,10 @@ Amend `ledgers` to the following. ledgers: ['ethereum'] ``` -In the weather client skill config (`my_weather_client/skills/weather_client_ledger/skill.yaml`) under strategy change the `currency_pbk` and `ledger_id`. +In the weather client skill config (`my_weather_client/skills/weather_client_ledger/skill.yaml`) under strategy change the `currency_id` and `ledger_id`. ``` bash max_buyer_tx_fee: 20000 -currency_pbk: 'ETH' +currency_id: 'ETH' ledger_id: 'ethereum' ``` Amend `ledgers` to the following. @@ -256,7 +268,7 @@ Go to the
MetaMask Faucet None: :return: None """ wallet = Wallet({DEFAULT: None}) - super().__init__(name, [GymConnection(wallet.public_keys.get(DEFAULT), gym_env)], wallet, timeout=0) + super().__init__(name, [GymConnection(wallet.addresses.get(DEFAULT), gym_env)], wallet, timeout=0) self.proxy_env_queue = proxy_env_queue def setup(self) -> None: diff --git a/examples/gym_ex/proxy/env.py b/examples/gym_ex/proxy/env.py index 283b6edc82..8c71ae2770 100755 --- a/examples/gym_ex/proxy/env.py +++ b/examples/gym_ex/proxy/env.py @@ -66,7 +66,7 @@ def __init__(self, gym_env: gym.Env) -> None: self._action_counter = 0 self._agent = ProxyAgent(name="proxy", gym_env=gym_env, proxy_env_queue=self._queue) crypto_object = self._agent.wallet.crypto_objects.get(DEFAULT) - self._agent_public_key = crypto_object.public_key + self._agent_address = crypto_object.address self._agent_thread = Thread(target=self._agent.start) def step(self, action: Action) -> Feedback: @@ -120,7 +120,7 @@ def reset(self) -> None: self._connect() gym_msg = GymMessage(performative=GymMessage.Performative.RESET) gym_bytes = GymSerializer().encode(gym_msg) - envelope = Envelope(to=DEFAULT_GYM, sender=self._agent_public_key, protocol_id=GymMessage.protocol_id, + envelope = Envelope(to=DEFAULT_GYM, sender=self._agent_address, protocol_id=GymMessage.protocol_id, message=gym_bytes) self._agent.outbox.put(envelope) @@ -132,7 +132,7 @@ def close(self) -> None: """ gym_msg = GymMessage(performative=GymMessage.Performative.CLOSE) gym_bytes = GymSerializer().encode(gym_msg) - envelope = Envelope(to=DEFAULT_GYM, sender=self._agent_public_key, protocol_id=GymMessage.protocol_id, + envelope = Envelope(to=DEFAULT_GYM, sender=self._agent_address, protocol_id=GymMessage.protocol_id, message=gym_bytes) self._agent.outbox.put(envelope) self._disconnect() @@ -168,7 +168,7 @@ def _encode_action(self, action: Action, step_id: int) -> Envelope: """ gym_msg = GymMessage(performative=GymMessage.Performative.ACT, action=action, step_id=step_id) gym_bytes = GymSerializer().encode(gym_msg) - envelope = Envelope(to=DEFAULT_GYM, sender=self._agent_public_key, protocol_id=GymMessage.protocol_id, + envelope = Envelope(to=DEFAULT_GYM, sender=self._agent_address, protocol_id=GymMessage.protocol_id, message=gym_bytes) return envelope diff --git a/examples/ml_ex/model.json b/examples/ml_ex/model.json new file mode 100644 index 0000000000..d34a0871db --- /dev/null +++ b/examples/ml_ex/model.json @@ -0,0 +1 @@ +{"name": "my_model", "layers": [{"class_name": "InputLayer", "config": {"batch_input_shape": [null, 28, 28], "dtype": "float32", "sparse": false, "name": "pictures"}, "name": "pictures", "inbound_nodes": []}, {"class_name": "Dense", "config": {"name": "dense_1", "trainable": true, "dtype": "float32", "units": 128, "activation": "relu", "use_bias": true, "kernel_initializer": {"class_name": "GlorotUniform", "config": {"seed": null}}, "bias_initializer": {"class_name": "Zeros", "config": {}}, "kernel_regularizer": null, "bias_regularizer": null, "activity_regularizer": null, "kernel_constraint": null, "bias_constraint": null}, "name": "dense_1", "inbound_nodes": [[["pictures", 0, 0, {}]]]}, {"class_name": "Dense", "config": {"name": "activations", "trainable": true, "dtype": "float32", "units": 10, "activation": "softmax", "use_bias": true, "kernel_initializer": {"class_name": "GlorotUniform", "config": {"seed": null}}, "bias_initializer": {"class_name": "Zeros", "config": {}}, "kernel_regularizer": null, "bias_regularizer": null, "activity_regularizer": null, "kernel_constraint": null, "bias_constraint": null}, "name": "activations", "inbound_nodes": [[["dense_1", 0, 0, {}]]]}], "input_layers": [["pictures", 0, 0]], "output_layers": [["activations", 0, 0]]} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 0fc4b5953d..64da3c45d3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -19,7 +19,9 @@ nav: - Autonomous Economic Agent Framework: - Welcome: 'index.md' + - Agent-oriented development: 'agent-oriented-development.md' - AEA quick start: 'quickstart.md' + - Version: 'version.md' - The AEA universe: - "Vision": 'vision.md' - "Application areas": 'app-areas.md' @@ -27,13 +29,13 @@ nav: - "Trust issues": 'trust.md' # - "Two-layered machine learning": 'two-layer.md' - Demos: + - "Car park agent": 'car-park.md' - "Gym demo": 'gym-plugin.md' - "Gym skill": 'gym-skill.md' - - "Weather skills": 'weather-skills.md' + - "ML skills": 'ml-skills.md' - "TAC skills": 'tac-skills.md' - "TAC external app": 'tac.md' - # - "FIPA skill": 'fipa-skill.md' - - "Car park agent": 'car-park.md' + - "Weather skills": 'weather-skills.md' - Architecture: - "Design principles": 'design-principles.md' - "Architectural diagram": 'diagram.md' @@ -51,7 +53,7 @@ nav: - "CLI tool": 'cli-how-to.md' - "Commands": 'cli-commands.md' - "GUI": 'cli-gui.md' - - "Code an agent directly": 'hacking-an-agent.md' + - "Build an AEA using code API": 'hacking-an-agent.md' - "Integrate with third parties": 'integration.md' diff --git a/packages/connections/gym/connection.py b/packages/connections/gym/connection.py index 1211053c3c..96f79ca5bf 100644 --- a/packages/connections/gym/connection.py +++ b/packages/connections/gym/connection.py @@ -31,7 +31,7 @@ from aea.configurations.base import ConnectionConfig from aea.connections.base import Connection from aea.helpers.base import locate -from aea.mail.base import Envelope +from aea.mail.base import Envelope, Address if TYPE_CHECKING or "pytest" in sys.modules: from packages.protocols.gym.message import GymMessage @@ -50,9 +50,9 @@ class GymChannel: """A wrapper of the gym environment.""" - def __init__(self, public_key: str, gym_env: gym.Env): + def __init__(self, address: Address, gym_env: gym.Env): """Initialize a gym channel.""" - self.public_key = public_key + self.address = address self.gym_env = gym_env self._lock = threading.Lock() @@ -60,16 +60,16 @@ def __init__(self, public_key: str, gym_env: gym.Env): def connect(self) -> Optional[asyncio.Queue]: """ - Connect a public key to the gym. + Connect an address to the gym. :return: an asynchronous queue, that constitutes the communication channel. """ - if self.public_key in self._queues: + if self.address in self._queues: return None - assert len(self._queues.keys()) == 0, "Only one public key can register to a gym." + assert len(self._queues.keys()) == 0, "Only one address can register to a gym." q = asyncio.Queue() # type: asyncio.Queue - self._queues[self.public_key] = q + self._queues[self.address] = q return q def send(self, envelope: Envelope) -> None: @@ -132,7 +132,7 @@ def disconnect(self) -> None: :return: None """ with self._lock: - self._queues.pop(self.public_key, None) + self._queues.pop(self.address, None) class GymConnection(Connection): @@ -140,17 +140,17 @@ class GymConnection(Connection): restricted_to_protocols = {"gym"} - def __init__(self, public_key: str, gym_env: gym.Env, connection_id: str = "gym", **kwargs): + def __init__(self, address: Address, gym_env: gym.Env, connection_id: str = "gym", **kwargs): """ Initialize a connection to a local gym environment. - :param public_key: the public key used in the protocols. + :param address: the address used in the protocols. :param gym_env: the gym environment. :param connection_id: the connection id. """ super().__init__(connection_id=connection_id, **kwargs) - self.public_key = public_key - self.channel = GymChannel(public_key, gym_env) + self.address = address + self.channel = GymChannel(address, gym_env) self._connection = None # type: Optional[asyncio.Queue] @@ -211,16 +211,16 @@ def stop(self) -> None: self._connection = None @classmethod - def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection': + def from_config(cls, address: Address, connection_configuration: ConnectionConfig) -> 'Connection': """ Get the Gym connection from the connection configuration. - :param public_key: the public key of the agent. + :param address: the address of the agent. :param connection_configuration: the connection configuration object. :return: the connection object """ gym_env_package = cast(str, connection_configuration.config.get('env')) gym_env = locate(gym_env_package) - return GymConnection(public_key, gym_env(), + return GymConnection(address, gym_env(), connection_id=connection_configuration.name, restricted_to_protocols=set(connection_configuration.restricted_to_protocols)) diff --git a/packages/connections/gym/connection.yaml b/packages/connections/gym/connection.yaml index da9fdb0a88..48a7cafadc 100644 --- a/packages/connections/gym/connection.yaml +++ b/packages/connections/gym/connection.yaml @@ -1,12 +1,15 @@ name: gym -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" description: "The gym connection wraps an OpenAI gym." url: "" class_name: GymConnection +protocols: ["gym"] restricted_to_protocols: ["gym"] +excluded_protocols: [] config: env: '' # put here the dotted path to your Gym Environment class. dependencies: - - gym + gym: {} diff --git a/aea/connections/local/__init__.py b/packages/connections/local/__init__.py similarity index 100% rename from aea/connections/local/__init__.py rename to packages/connections/local/__init__.py diff --git a/aea/connections/local/connection.py b/packages/connections/local/connection.py similarity index 67% rename from aea/connections/local/connection.py rename to packages/connections/local/connection.py index dbefefc833..d14ec4d613 100644 --- a/aea/connections/local/connection.py +++ b/packages/connections/local/connection.py @@ -23,15 +23,21 @@ import logging from asyncio import Queue, AbstractEventLoop from collections import defaultdict +import sys from threading import Thread -from typing import Dict, List, Optional, cast, Set +from typing import Dict, List, Optional, Set, cast, TYPE_CHECKING from aea.configurations.base import ConnectionConfig from aea.connections.base import Connection -from aea.mail.base import Envelope, AEAConnectionError -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.models import Description, Query -from aea.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF +from aea.helpers.search.models import Description, Query +from aea.mail.base import Envelope, AEAConnectionError, Address + +if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.oef.message import OEFMessage + from packages.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF +else: + from oef_protocol.message import OEFMessage + from oef_protocol.serialization import OEFSerializer, DEFAULT_OEF logger = logging.getLogger(__name__) @@ -78,20 +84,20 @@ def _run_loop(self): self._loop.run_forever() logger.debug("Asyncio loop has been stopped.") - async def connect(self, public_key: str, writer: asyncio.Queue) -> Optional[asyncio.Queue]: + async def connect(self, address: Address, writer: asyncio.Queue) -> Optional[asyncio.Queue]: """ - Connect a public key to the node. + Connect an address to the node. - :param public_key: the public key of the agent. + :param address: the address of the agent. :param writer: the queue where the client is listening. :return: an asynchronous queue, that constitutes the communication channel. """ - if public_key in self._out_queues.keys(): + if address in self._out_queues.keys(): return None assert self._in_queue is not None q = self._in_queue # type: asyncio.Queue - self._out_queues[public_key] = writer + self._out_queues[address] = writer return q @@ -140,21 +146,22 @@ async def _handle_oef_message(self, envelope: Envelope) -> None: :return: None """ oef_message = OEFSerializer().decode(envelope.message) + oef_message = cast(OEFMessage, oef_message) sender = envelope.sender - request_id = cast(int, oef_message.get("id")) - oef_type = OEFMessage.Type(oef_message.get("type")) + request_id = oef_message.id + oef_type = oef_message.type if oef_type == OEFMessage.Type.REGISTER_SERVICE: - await self._register_service(sender, cast(Description, oef_message.get("service_description"))) + await self._register_service(sender, oef_message.service_description) elif oef_type == OEFMessage.Type.REGISTER_AGENT: - await self._register_agent(sender, cast(Description, oef_message.get("agent_description"))) + await self._register_agent(sender, oef_message.agent_description) elif oef_type == OEFMessage.Type.UNREGISTER_SERVICE: - await self._unregister_service(sender, request_id, cast(Description, oef_message.get("service_description"))) + await self._unregister_service(sender, request_id, oef_message.service_description) elif oef_type == OEFMessage.Type.UNREGISTER_AGENT: - await self._unregister_agent(sender, request_id, cast(Description, oef_message.get("agent_description"))) + await self._unregister_agent(sender, request_id, oef_message.agent_description) elif oef_type == OEFMessage.Type.SEARCH_AGENTS: - await self._search_agents(sender, request_id, cast(Query, oef_message.get("query"))) + await self._search_agents(sender, request_id, oef_message.query) elif oef_type == OEFMessage.Type.SEARCH_SERVICES: - await self._search_services(sender, request_id, cast(Query, oef_message.get("query"))) + await self._search_services(sender, request_id, oef_message.query) else: # request not recognized pass @@ -169,7 +176,7 @@ async def _handle_agent_message(self, envelope: Envelope) -> None: destination = envelope.to if destination not in self._out_queues.keys(): - msg = OEFMessage(oef_type=OEFMessage.Type.DIALOGUE_ERROR, id=STUB_DIALOGUE_ID, dialogue_id=STUB_DIALOGUE_ID, origin=destination) + msg = OEFMessage(type=OEFMessage.Type.DIALOGUE_ERROR, id=STUB_DIALOGUE_ID, dialogue_id=STUB_DIALOGUE_ID, origin=destination) msg_bytes = OEFSerializer().encode(msg) error_envelope = Envelope(to=envelope.sender, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) await self._send(error_envelope) @@ -177,80 +184,80 @@ async def _handle_agent_message(self, envelope: Envelope) -> None: else: await self._send(envelope) - async def _register_service(self, public_key: str, service_description: Description): + async def _register_service(self, address: Address, service_description: Description): """ Register a service agent in the service directory of the node. - :param public_key: the public key of the service agent to be registered. + :param address: the address of the service agent to be registered. :param service_description: the description of the service agent to be registered. :return: None """ async with self._lock: - self.services[public_key].append(service_description) + self.services[address].append(service_description) - async def _register_agent(self, public_key: str, agent_description: Description): + async def _register_agent(self, address: Address, agent_description: Description): """ Register a service agent in the service directory of the node. - :param public_key: the public key of the service agent to be registered. + :param address: the address of the service agent to be registered. :param agent_description: the description of the service agent to be registered. :return: None """ async with self._lock: - self.agents[public_key].append(agent_description) + self.agents[address].append(agent_description) - async def _register_service_wide(self, public_key: str, service_description: Description): + async def _register_service_wide(self, address: Address, service_description: Description): """Register service wide.""" raise NotImplementedError # pragma: no cover - async def _unregister_service(self, public_key: str, msg_id: int, service_description: Description) -> None: + async def _unregister_service(self, address: Address, msg_id: int, service_description: Description) -> None: """ Unregister a service agent. - :param public_key: the public key of the service agent to be unregistered. + :param address: the address of the service agent to be unregistered. :param msg_id: the message id of the request. :param service_description: the description of the service agent to be unregistered. :return: None """ async with self._lock: - if public_key not in self.services: - msg = OEFMessage(oef_type=OEFMessage.Type.OEF_ERROR, id=msg_id, operation=OEFMessage.OEFErrorOperation.UNREGISTER_SERVICE) + if address not in self.services: + msg = OEFMessage(type=OEFMessage.Type.OEF_ERROR, id=msg_id, operation=OEFMessage.OEFErrorOperation.UNREGISTER_SERVICE) msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=address, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) await self._send(envelope) else: - self.services[public_key].remove(service_description) - if len(self.services[public_key]) == 0: - self.services.pop(public_key) + self.services[address].remove(service_description) + if len(self.services[address]) == 0: + self.services.pop(address) - async def _unregister_agent(self, public_key: str, msg_id: int, agent_description: Description) -> None: + async def _unregister_agent(self, address: Address, msg_id: int, agent_description: Description) -> None: """ Unregister an agent. :param agent_description: - :param public_key: the public key of the service agent to be unregistered. + :param address: the address of the service agent to be unregistered. :param msg_id: the message id of the request. :return: None """ async with self._lock: - if public_key not in self.agents: - msg = OEFMessage(oef_type=OEFMessage.Type.OEF_ERROR, id=msg_id, operation=OEFMessage.OEFErrorOperation.UNREGISTER_AGENT) + if address not in self.agents: + msg = OEFMessage(type=OEFMessage.Type.OEF_ERROR, id=msg_id, operation=OEFMessage.OEFErrorOperation.UNREGISTER_AGENT) msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=address, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) await self._send(envelope) else: - self.agents[public_key].remove(agent_description) - if len(self.agents[public_key]) == 0: - self.agents.pop(public_key) + self.agents[address].remove(agent_description) + if len(self.agents[address]) == 0: + self.agents.pop(address) - async def _search_agents(self, public_key: str, search_id: int, query: Query) -> None: + async def _search_agents(self, address: Address, search_id: int, query: Query) -> None: """ Search the agents in the local Agent Directory, and send back the result. This is actually a dummy search, it will return all the registered agents with the specified data model. If the data model is not specified, it will return all the agents. - :param public_key: the source of the search request. + :param address: the source of the search request. :param search_id: the search identifier associated with the search request. :param query: the query that constitutes the search. :return: None @@ -259,24 +266,24 @@ async def _search_agents(self, public_key: str, search_id: int, query: Query) -> if query.model is None: result = list(set(self.services.keys())) else: - for agent_public_key, descriptions in self.agents.items(): + for agent_address, descriptions in self.agents.items(): for description in descriptions: if query.model == description.data_model: - result.append(agent_public_key) + result.append(agent_address) - msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_RESULT, id=search_id, agents=sorted(set(result))) + msg = OEFMessage(type=OEFMessage.Type.SEARCH_RESULT, id=search_id, agents=sorted(set(result))) msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=address, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) await self._send(envelope) - async def _search_services(self, public_key: str, search_id: int, query: Query) -> None: + async def _search_services(self, address: Address, search_id: int, query: Query) -> None: """ Search the agents in the local Service Directory, and send back the result. This is actually a dummy search, it will return all the registered agents with the specified data model. If the data model is not specified, it will return all the agents. - :param public_key: the source of the search request. + :param address: the source of the search request. :param search_id: the search identifier associated with the search request. :param query: the query that constitutes the search. :return: None @@ -285,14 +292,14 @@ async def _search_services(self, public_key: str, search_id: int, query: Query) if query.model is None: result = list(set(self.services.keys())) else: - for agent_public_key, descriptions in self.services.items(): + for agent_address, descriptions in self.services.items(): for description in descriptions: if description.data_model == query.model: - result.append(agent_public_key) + result.append(agent_address) - msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_RESULT, id=search_id, agents=sorted(set(result))) + msg = OEFMessage(type=OEFMessage.Type.SEARCH_RESULT, id=search_id, agents=sorted(set(result))) msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=address, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) await self._send(envelope) async def _send(self, envelope: Envelope): @@ -302,17 +309,17 @@ async def _send(self, envelope: Envelope): destination_queue._loop.call_soon_threadsafe(destination_queue.put_nowait, envelope) # type: ignore logger.debug("Send envelope {}".format(envelope)) - async def disconnect(self, public_key: str) -> None: + async def disconnect(self, address: Address) -> None: """ Disconnect. - :param public_key: the public key + :param address: the address of the agent :return: None """ async with self._lock: - self._out_queues.pop(public_key, None) - self.services.pop(public_key, None) - self.agents.pop(public_key, None) + self._out_queues.pop(address, None) + self.services.pop(address, None) + self.agents.pop(address, None) class OEFLocalConnection(Connection): @@ -323,38 +330,39 @@ class OEFLocalConnection(Connection): It is useful for local testing. """ - def __init__(self, public_key: str, local_node: LocalNode, connection_id: str = "local", - restricted_to_protocols: Optional[Set[str]] = None): + def __init__(self, address: Address, local_node: LocalNode, connection_id: str = "local", + restricted_to_protocols: Optional[Set[str]] = None, excluded_protocols: Optional[Set[str]] = None): """ Initialize a OEF proxy for a local OEF Node (that is, :class:`~oef.proxy.OEFLocalProxy.LocalNode`. - :param public_key: the public key used in the protocols. + :param address: the address used in the protocols. :param local_node: the Local OEF Node object. This reference must be the same across the agents of interest. """ - super().__init__(connection_id=connection_id, restricted_to_protocols=restricted_to_protocols) - self._public_key = public_key + super().__init__(connection_id=connection_id, restricted_to_protocols=restricted_to_protocols, + excluded_protocols=excluded_protocols) + self._address = address self._local_node = local_node self._reader = None # type: Optional[Queue] self._writer = None # type: Optional[Queue] @property - def public_key(self) -> str: - """Get the public key.""" - return self._public_key + def address(self) -> str: + """Get the address.""" + return self._address async def connect(self) -> None: """Connect to the local OEF Node.""" if not self.connection_status.is_connected: self._reader = Queue() - self._writer = await self._local_node.connect(self._public_key, self._reader) + self._writer = await self._local_node.connect(self._address, self._reader) self.connection_status.is_connected = True async def disconnect(self) -> None: """Disconnect from the local OEF Node.""" if self.connection_status.is_connected: assert self._reader is not None - await self._local_node.disconnect(self.public_key) + await self._local_node.disconnect(self.address) await self._reader.put(None) self._reader, self._writer = None, None self.connection_status.is_connected = False @@ -385,14 +393,15 @@ async def receive(self, *args, **kwargs) -> Optional['Envelope']: return None @classmethod - def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection': + def from_config(cls, address: Address, connection_configuration: ConnectionConfig) -> 'Connection': """Get the Local OEF connection from the connection configuration. - :param public_key: the public key of the agent. + :param address: the address of the agent. :param connection_configuration: the connection configuration object. :return: the connection object """ local_node = LocalNode() - return OEFLocalConnection(public_key, local_node, + return OEFLocalConnection(address, local_node, connection_id=connection_configuration.name, - restricted_to_protocols=set(connection_configuration.restricted_to_protocols)) + restricted_to_protocols=set(connection_configuration.restricted_to_protocols), + excluded_protocols=set(connection_configuration.excluded_protocols)) diff --git a/aea/connections/local/connection.yaml b/packages/connections/local/connection.yaml similarity index 72% rename from aea/connections/local/connection.yaml rename to packages/connections/local/connection.yaml index 6697ab36e2..c0b41f5d1f 100644 --- a/aea/connections/local/connection.yaml +++ b/packages/connections/local/connection.yaml @@ -1,9 +1,12 @@ name: local -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" description: "The local connection provides a stub for an OEF node." class_name: OEFLocalConnection +protocols: ["oef"] restricted_to_protocols: [] +excluded_protocols: [] config: {} diff --git a/aea/connections/oef/__init__.py b/packages/connections/oef/__init__.py similarity index 100% rename from aea/connections/oef/__init__.py rename to packages/connections/oef/__init__.py diff --git a/aea/connections/oef/connection.py b/packages/connections/oef/connection.py similarity index 82% rename from aea/connections/oef/connection.py rename to packages/connections/oef/connection.py index e683fa84f7..14f2ed1f76 100644 --- a/aea/connections/oef/connection.py +++ b/packages/connections/oef/connection.py @@ -24,8 +24,9 @@ import pickle import time from asyncio import AbstractEventLoop, CancelledError +import sys from threading import Thread -from typing import List, Optional, cast, Set +from typing import List, Optional, cast, Set, TYPE_CHECKING import oef from oef.agents import OEFAgent @@ -43,17 +44,23 @@ from aea.configurations.base import ConnectionConfig from aea.connections.base import Connection -from aea.mail.base import Envelope -from aea.protocols.fipa.message import FIPAMessage -from aea.protocols.fipa.serialization import FIPASerializer -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.models import Description, Attribute, DataModel, Query, ConstraintExpr, And, Or, Not, Constraint, \ +from aea.helpers.search.models import Description, Attribute, DataModel, Query, ConstraintExpr, And, Or, Not, Constraint, \ ConstraintType, ConstraintTypes -from aea.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF +from aea.mail.base import Envelope, Address + +if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.fipa.message import FIPAMessage + from packages.protocols.fipa.serialization import FIPASerializer + from packages.protocols.oef.message import OEFMessage + from packages.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF +else: + from fipa_protocol.message import FIPAMessage + from fipa_protocol.serialization import FIPASerializer + from oef_protocol.message import OEFMessage + from oef_protocol.serialization import OEFSerializer, DEFAULT_OEF logger = logging.getLogger(__name__) - STUB_MESSSAGE_ID = 0 STUB_DIALOGUE_ID = 0 @@ -192,26 +199,29 @@ def from_oef_constraint_type(cls, constraint_type: OEFConstraintType) -> Constra class OEFChannel(OEFAgent): """The OEFChannel connects the OEF Agent with the connection.""" - def __init__(self, public_key: str, oef_addr: str, oef_port: int, core: AsyncioCore): + def __init__(self, address: Address, oef_addr: str, oef_port: int, core: AsyncioCore, + excluded_protocols: Optional[List[str]] = None): """ Initialize. - :param public_key: the public key of the agent. + :param address: the address of the agent. :param oef_addr: the OEF IP address. :param oef_port: the OEF port. """ - super().__init__(public_key, oef_addr=oef_addr, oef_port=oef_port, core=core, + super().__init__(address, oef_addr=oef_addr, oef_port=oef_port, core=core, logger=lambda *x: None, logger_debug=lambda *x: None) + self.address = address self.in_queue = None # type: Optional[asyncio.Queue] self.loop = None # type: Optional[AbstractEventLoop] + self.excluded_protocols = excluded_protocols - def on_message(self, msg_id: int, dialogue_id: int, origin: str, content: bytes) -> None: + def on_message(self, msg_id: int, dialogue_id: int, origin: Address, content: bytes) -> None: """ On message event handler. :param msg_id: the message id. :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. + :param origin: the address of the sender. :param content: the bytes content. :return: None """ @@ -222,13 +232,13 @@ def on_message(self, msg_id: int, dialogue_id: int, origin: str, content: bytes) envelope = Envelope.decode(content) asyncio.run_coroutine_threadsafe(self.in_queue.put(envelope), self.loop).result() - def on_cfp(self, msg_id: int, dialogue_id: int, origin: str, target: int, query: CFP_TYPES) -> None: + def on_cfp(self, msg_id: int, dialogue_id: int, origin: Address, target: int, query: CFP_TYPES) -> None: """ On cfp event handler. :param msg_id: the message id. :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. + :param origin: the address of the sender. :param target: the message target. :param query: the query. :return: None @@ -246,16 +256,16 @@ def on_cfp(self, msg_id: int, dialogue_id: int, origin: str, target: int, query: performative=FIPAMessage.Performative.CFP, query=query if query != b"" else None) msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=self.address, sender=origin, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) asyncio.run_coroutine_threadsafe(self.in_queue.put(envelope), self.loop).result() - def on_propose(self, msg_id: int, dialogue_id: int, origin: str, target: int, b_proposals: PROPOSE_TYPES) -> None: + def on_propose(self, msg_id: int, dialogue_id: int, origin: Address, target: int, b_proposals: PROPOSE_TYPES) -> None: """ On propose event handler. :param msg_id: the message id. :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. + :param origin: the address of the sender. :param target: the message target. :param b_proposals: the proposals. :return: None @@ -264,13 +274,13 @@ def on_propose(self, msg_id: int, dialogue_id: int, origin: str, target: int, b_ assert self.loop is not None logger.warning('Dropping incompatible on_propose: msg_id={}, dialogue_id={}, origin={}, target={}'.format(msg_id, dialogue_id, origin, target)) - def on_accept(self, msg_id: int, dialogue_id: int, origin: str, target: int) -> None: + def on_accept(self, msg_id: int, dialogue_id: int, origin: Address, target: int) -> None: """ On accept event handler. :param msg_id: the message id. :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. + :param origin: the address of the sender. :param target: the message target. :return: None """ @@ -278,13 +288,13 @@ def on_accept(self, msg_id: int, dialogue_id: int, origin: str, target: int) -> assert self.loop is not None logger.warning('Dropping incompatible on_accept: msg_id={}, dialogue_id={}, origin={}, target={}'.format(msg_id, dialogue_id, origin, target)) - def on_decline(self, msg_id: int, dialogue_id: int, origin: str, target: int) -> None: + def on_decline(self, msg_id: int, dialogue_id: int, origin: Address, target: int) -> None: """ On decline event handler. :param msg_id: the message id. :param dialogue_id: the dialogue id. - :param origin: the public key of the sender. + :param origin: the address of the sender. :param target: the message target. :return: None """ @@ -292,7 +302,7 @@ def on_decline(self, msg_id: int, dialogue_id: int, origin: str, target: int) -> assert self.loop is not None logger.warning('Dropping incompatible on_decline: msg_id={}, dialogue_id={}, origin={}, target={}'.format(msg_id, dialogue_id, origin, target)) - def on_search_result(self, search_id: int, agents: List[str]) -> None: + def on_search_result(self, search_id: int, agents: List[Address]) -> None: """ On accept event handler. @@ -302,9 +312,9 @@ def on_search_result(self, search_id: int, agents: List[str]) -> None: """ assert self.in_queue is not None assert self.loop is not None - msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_RESULT, id=search_id, agents=agents) + msg = OEFMessage(type=OEFMessage.Type.SEARCH_RESULT, id=search_id, agents=agents) msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=self.address, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) asyncio.run_coroutine_threadsafe(self.in_queue.put(envelope), self.loop).result() def on_oef_error(self, answer_id: int, operation: oef.messages.OEFErrorOperation) -> None: @@ -322,12 +332,12 @@ def on_oef_error(self, answer_id: int, operation: oef.messages.OEFErrorOperation except ValueError: operation = OEFMessage.OEFErrorOperation.OTHER - msg = OEFMessage(oef_type=OEFMessage.Type.OEF_ERROR, id=answer_id, operation=operation) + msg = OEFMessage(type=OEFMessage.Type.OEF_ERROR, id=answer_id, operation=operation) msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=self.address, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) asyncio.run_coroutine_threadsafe(self.in_queue.put(envelope), self.loop).result() - def on_dialogue_error(self, answer_id: int, dialogue_id: int, origin: str) -> None: + def on_dialogue_error(self, answer_id: int, dialogue_id: int, origin: Address) -> None: """ On dialogue error event handler. @@ -338,12 +348,12 @@ def on_dialogue_error(self, answer_id: int, dialogue_id: int, origin: str) -> No """ assert self.in_queue is not None assert self.loop is not None - msg = OEFMessage(oef_type=OEFMessage.Type.DIALOGUE_ERROR, + msg = OEFMessage(type=OEFMessage.Type.DIALOGUE_ERROR, id=answer_id, dialogue_id=dialogue_id, origin=origin) msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=self.address, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) asyncio.run_coroutine_threadsafe(self.in_queue.put(envelope), self.loop).result() def send(self, envelope: Envelope) -> None: @@ -353,10 +363,11 @@ def send(self, envelope: Envelope) -> None: :param envelope: the message. :return: None """ - if envelope.protocol_id == "gym": - logger.error("This envelope cannot be sent with the oef connection: protocol_id={}".format(envelope.protocol_id)) - raise ValueError("Cannot send message.") - elif envelope.protocol_id == "oef": + if self.excluded_protocols is not None: + if envelope.protocol_id in self.excluded_protocols: + logger.error("This envelope cannot be sent with the oef connection: protocol_id={}".format(envelope.protocol_id)) + raise ValueError("Cannot send message.") + if envelope.protocol_id == "oef": self.send_oef_message(envelope) else: self.send_default_message(envelope) @@ -373,24 +384,25 @@ def send_oef_message(self, envelope: Envelope) -> None: :return: None """ oef_message = OEFSerializer().decode(envelope.message) - oef_type = OEFMessage.Type(oef_message.get("type")) - oef_msg_id = cast(int, oef_message.get("id")) + oef_message = cast(OEFMessage, oef_message) + oef_type = oef_message.type + oef_msg_id = oef_message.id if oef_type == OEFMessage.Type.REGISTER_SERVICE: - service_description = cast(Description, oef_message.get("service_description")) - service_id = cast(int, oef_message.get("service_id")) + service_description = oef_message.service_description + service_id = oef_message.service_id oef_service_description = OEFObjectTranslator.to_oef_description(service_description) self.register_service(oef_msg_id, oef_service_description, service_id) elif oef_type == OEFMessage.Type.UNREGISTER_SERVICE: - service_description = cast(Description, oef_message.get("service_description")) - service_id = cast(int, oef_message.get("service_id")) + service_description = oef_message.service_description + service_id = oef_message.service_id oef_service_description = OEFObjectTranslator.to_oef_description(service_description) self.unregister_service(oef_msg_id, oef_service_description, service_id) elif oef_type == OEFMessage.Type.SEARCH_AGENTS: - query = cast(Query, oef_message.get("query")) + query = oef_message.query oef_query = OEFObjectTranslator.to_oef_query(query) self.search_agents(oef_msg_id, oef_query) elif oef_type == OEFMessage.Type.SEARCH_SERVICES: - query = cast(Query, oef_message.get("query")) + query = oef_message.query oef_query = OEFObjectTranslator.to_oef_query(query) self.search_services(oef_msg_id, oef_query) else: @@ -401,22 +413,27 @@ class OEFConnection(Connection): """The OEFConnection connects the to the mailbox.""" restricted_to_protocols = set() # type: Set[str] + excluded_protocols = set() # type: Set[str] - def __init__(self, public_key: str, oef_addr: str, oef_port: int = 10000, connection_id: str = "oef", - restricted_to_protocols: Optional[Set[str]] = None): + def __init__(self, address: Address, oef_addr: str, oef_port: int = 10000, connection_id: str = "oef", + restricted_to_protocols: Optional[Set[str]] = None, + excluded_protocols: Optional[Set[str]] = None): """ Initialize. - :param public_key: the public key of the agent. + :param address: the address of the agent. :param oef_addr: the OEF IP address. :param oef_port: the OEF port. :param connection_id: the identifier of the connection object. :param restricted_to_protocols: the only supported protocols for this connection. + :param excluded_protocols: the excluded protocols for this conenction. """ - super().__init__(connection_id=connection_id, restricted_to_protocols=restricted_to_protocols) + super().__init__(connection_id=connection_id, restricted_to_protocols=restricted_to_protocols, + excluded_protocols=excluded_protocols) self._core = AsyncioCore(logger=logger) # type: AsyncioCore self.in_queue = None # type: Optional[asyncio.Queue] - self.channel = OEFChannel(public_key, oef_addr, oef_port, core=self._core) + self.channel = OEFChannel(address, oef_addr, oef_port, core=self._core, + excluded_protocols=excluded_protocols) # type: ignore self._connection_check_thread = None # type: Optional[Thread] @@ -485,6 +502,7 @@ async def disconnect(self) -> None: """ assert self._connection_check_thread is not None, "Call connect before disconnect." assert self.in_queue is not None + # import pdb; pdb.set_trace() self.connection_status.is_connected = False self._connection_check_thread.join() self._connection_check_thread = None @@ -524,16 +542,17 @@ async def send(self, envelope: 'Envelope') -> None: self.channel.send(envelope) @classmethod - def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection': + def from_config(cls, address: Address, connection_configuration: ConnectionConfig) -> 'Connection': """ Get the OEF connection from the connection configuration. - :param public_key: the public key of the agent. + :param address: the address of the agent. :param connection_configuration: the connection configuration object. :return: the connection object """ oef_addr = cast(str, connection_configuration.config.get("addr")) oef_port = cast(int, connection_configuration.config.get("port")) - return OEFConnection(public_key, oef_addr, oef_port, + return OEFConnection(address, oef_addr, oef_port, connection_id=connection_configuration.name, - restricted_to_protocols=set(connection_configuration.restricted_to_protocols)) + restricted_to_protocols=set(connection_configuration.restricted_to_protocols), + excluded_protocols=set(connection_configuration.excluded_protocols)) diff --git a/aea/connections/oef/connection.yaml b/packages/connections/oef/connection.yaml similarity index 66% rename from aea/connections/oef/connection.yaml rename to packages/connections/oef/connection.yaml index 3e698ec3de..7354f93f0b 100644 --- a/aea/connections/oef/connection.yaml +++ b/packages/connections/oef/connection.yaml @@ -1,14 +1,18 @@ name: oef -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" description: "The oef connection provides a wrapper around the OEF sdk." class_name: OEFConnection +protocols: ["oef", "fipa"] restricted_to_protocols: [] +excluded_protocols: ["gym"] config: addr: ${OEF_ADDR:127.0.0.1} port: ${OEF_PORT:10000} dependencies: - - colorlog - - oef==0.8.1 + colorlog: {} + oef: + version: ==0.8.1 diff --git a/aea/connections/p2p/__init__.py b/packages/connections/p2p/__init__.py similarity index 100% rename from aea/connections/p2p/__init__.py rename to packages/connections/p2p/__init__.py diff --git a/packages/connections/p2p/connection.py b/packages/connections/p2p/connection.py new file mode 100644 index 0000000000..c193c395db --- /dev/null +++ b/packages/connections/p2p/connection.py @@ -0,0 +1,219 @@ +# -*- 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. +# +# ------------------------------------------------------------------------------ + +"""Peer to Peer connection and channel.""" + +import asyncio +import logging +import threading +import time +from asyncio import CancelledError +from threading import Thread +from typing import Optional, cast, Dict, List, Any, Set + +from fetch.p2p.api.http_calls import HTTPCalls + +from aea.configurations.base import ConnectionConfig +from aea.connections.base import Connection +from aea.mail.base import Envelope, AEAConnectionError, Address + +logger = logging.getLogger(__name__) + + +class PeerToPeerChannel: + """A wrapper for an SDK or API.""" + + def __init__(self, address: Address, provider_addr: str, provider_port: int, + excluded_protocols: Optional[List[str]] = None): + """ + Initialize a channel. + + :param address: the address + """ + self.address = address + self.provider_addr = provider_addr + self.provider_port = provider_port + self.in_queue = None # type: Optional[asyncio.Queue] + self.loop = None # type: Optional[asyncio.AbstractEventLoop] + self._httpCall = None # type: Optional[HTTPCalls] + self.excluded_protocols = excluded_protocols + self.thread = Thread(target=self.receiving_loop) + self.lock = threading.Lock() + self.stopped = True + logger.info("Initialised the peer to peer channel") + + def connect(self): + """ + Connect. + + :return: an asynchronous queue, that constitutes the communication channel. + """ + with self.lock: + if self.stopped: + self._httpCall = HTTPCalls(server_address=self.provider_addr, port=self.provider_port) + self.stopped = False + self.thread.start() + logger.debug("P2P Channel is connected.") + self.try_register() + + def try_register(self) -> bool: + """Try to register to the provider.""" + try: + assert self._httpCall is not None + logger.info(self.address) + query = self._httpCall.register(sender_address=self.address, mailbox=True) + return query['status'] == "OK" + except Exception: # pragma: no cover + logger.warning("Could not register to the provider.") + raise AEAConnectionError() + + def send(self, envelope: Envelope) -> None: + """ + Process the envelopes. + + :param envelope: the envelope + :return: None + """ + assert self._httpCall is not None + + if self.excluded_protocols is not None: + if envelope.protocol_id in self.excluded_protocols: + logger.error( + "This envelope cannot be sent with the oef connection: protocol_id={}".format(envelope.protocol_id)) + raise ValueError("Cannot send message.") + + self._httpCall.send_message(sender_address=envelope.sender, + receiver_address=envelope.to, + protocol=envelope.protocol_id, + context=b"None", + payload=envelope.message) + + def receiving_loop(self) -> None: + """Receive the messages from the provider.""" + assert self._httpCall is not None + assert self.in_queue is not None + assert self.loop is not None + while not self.stopped: + messages = self._httpCall.get_messages(sender_address=self.address) # type: List[Dict[str, Any]] + for message in messages: + logger.debug("Received message: {}".format(message)) + envelope = Envelope(to=message['TO']['RECEIVER_ADDRESS'], + sender=message['FROM']['SENDER_ADDRESS'], + protocol_id=message['PROTOCOL'], + message=message['PAYLOAD']) + self.loop.call_soon_threadsafe(self.in_queue.put_nowait, envelope) + time.sleep(0.5) + logger.debug("Receiving loop stopped.") + + def disconnect(self) -> None: + """ + Disconnect. + + :return: None + """ + assert self._httpCall is not None + with self.lock: + if not self.stopped: + self._httpCall.unregister(self.address) + # self._httpCall.disconnect() + self.stopped = True + self.thread.join() + + +class PeerToPeerConnection(Connection): + """Proxy to the functionality of the SDK or API.""" + + restricted_to_protocols = set() # type: Set[str] + + def __init__(self, address: Address, provider_addr: str, provider_port: int = 8000, connection_id: str = "p2p", + restricted_to_protocols: Optional[Set[str]] = None, excluded_protocols: Optional[Set[str]] = None): + """ + Initialize a connection to an SDK or API. + + :param address: the address used in the protocols. + """ + super().__init__(connection_id=connection_id, restricted_to_protocols=restricted_to_protocols) + self.channel = PeerToPeerChannel(address, provider_addr, provider_port, excluded_protocols=excluded_protocols) # type: ignore + self.address = address + + async def connect(self) -> None: + """ + Connect to the gym. + + :return: None + """ + if not self.connection_status.is_connected: + self.connection_status.is_connected = True + self.channel.in_queue = asyncio.Queue() + self.channel.loop = self.loop + self.channel.connect() + + async def disconnect(self) -> None: + """ + Disconnect from P2P. + + :return: None + """ + if self.connection_status.is_connected: + self.connection_status.is_connected = False + self.channel.disconnect() + + async def send(self, envelope: 'Envelope') -> None: + """ + Send an envelope. + + :param envelope: the envelop + :return: None + """ + if not self.connection_status.is_connected: + raise ConnectionError("Connection not established yet. Please use 'connect()'.") # pragma: no cover + self.channel.send(envelope) + + async def receive(self, *args, **kwargs) -> Optional['Envelope']: + """ + Receive an envelope. + + :return: the envelope received, or None. + """ + if not self.connection_status.is_connected: + raise ConnectionError("Connection not established yet. Please use 'connect()'.") # pragma: no cover + assert self.channel.in_queue is not None + try: + envelope = await self.channel.in_queue.get() + if envelope is None: + return None # pragma: no cover + + return envelope + except CancelledError: # pragma: no cover + return None + + @classmethod + def from_config(cls, address: Address, connection_configuration: ConnectionConfig) -> 'Connection': + """ + Get the P2P connection from the connection configuration. + + :param address: the address of the agent. + :param connection_configuration: the connection configuration object. + :return: the connection object + """ + addr = cast(str, connection_configuration.config.get("addr")) + port = cast(int, connection_configuration.config.get("port")) + return PeerToPeerConnection(address, addr, port, + restricted_to_protocols=set(connection_configuration.restricted_to_protocols)) diff --git a/aea/connections/p2p/connection.yaml b/packages/connections/p2p/connection.yaml similarity index 62% rename from aea/connections/p2p/connection.yaml rename to packages/connections/p2p/connection.yaml index 68a9415450..c8ad845007 100644 --- a/aea/connections/p2p/connection.yaml +++ b/packages/connections/p2p/connection.yaml @@ -1,12 +1,17 @@ name: p2p -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" description: "The p2p connection provides a connection with the fetch.ai mail provider." class_name: PeerToPeerConnection +protocols: [] restricted_to_protocols: [] +excluded_protocols: ["gym"] config: addr: ${addr:127.0.0.1} port: ${port:8000} - +dependencies: + fetch: + git: "https://github.com/fetchai/peer-to-peer-api.git" diff --git a/aea/connections/tcp/__init__.py b/packages/connections/tcp/__init__.py similarity index 100% rename from aea/connections/tcp/__init__.py rename to packages/connections/tcp/__init__.py diff --git a/aea/connections/tcp/base.py b/packages/connections/tcp/base.py similarity index 94% rename from aea/connections/tcp/base.py rename to packages/connections/tcp/base.py index 22a59bc216..c689ae178b 100644 --- a/aea/connections/tcp/base.py +++ b/packages/connections/tcp/base.py @@ -25,7 +25,7 @@ from typing import Optional, Set from aea.connections.base import Connection -from aea.mail.base import Envelope +from aea.mail.base import Envelope, Address logger = logging.getLogger(__name__) @@ -36,7 +36,7 @@ class TCPConnection(Connection, ABC): restricted_to_protocols = set() # type: Set[str] def __init__(self, - public_key: str, + address: Address, host: str, port: int, connection_id: str, @@ -44,14 +44,14 @@ def __init__(self, """ Initialize the TCP connection. - :param public_key: the public key used for identification. + :param address: the address used for identification. :param host: the host to connect to. :param port: the port to connect to. :param connection_id: the identifier of the connection object. :param restricted_to_protocols: the only supported protocols for this connection. """ super().__init__(connection_id=connection_id, restricted_to_protocols=restricted_to_protocols) - self.public_key = public_key + self.address = address self.host = host self.port = port @@ -118,7 +118,7 @@ async def _recv(self, reader: StreamReader) -> Optional[bytes]: return data async def _send(self, writer, data): - logger.debug("[{}] Send a message".format(self.public_key)) + logger.debug("[{}] Send a message".format(self.address)) nbytes = struct.pack("I", len(data)) logger.debug("#bytes: {!r}".format(nbytes)) try: @@ -140,4 +140,4 @@ async def send(self, envelope: Envelope) -> None: data = envelope.encode() await self._send(writer, data) else: - logger.error("[{}]: Cannot send envelope {}".format(self.public_key, envelope)) + logger.error("[{}]: Cannot send envelope {}".format(self.address, envelope)) diff --git a/aea/connections/tcp/connection.py b/packages/connections/tcp/connection.py similarity index 100% rename from aea/connections/tcp/connection.py rename to packages/connections/tcp/connection.py diff --git a/aea/connections/tcp/connection.yaml b/packages/connections/tcp/connection.yaml similarity index 71% rename from aea/connections/tcp/connection.yaml rename to packages/connections/tcp/connection.yaml index 0ff340ced8..ddab01f442 100644 --- a/aea/connections/tcp/connection.yaml +++ b/packages/connections/tcp/connection.yaml @@ -1,10 +1,13 @@ name: tcp -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" class_name: TCPClientConnection # this can be eitehr TCPClientConnection or TCPServerConnection +protocols: [] restricted_to_protocols: [] +excluded_protocols: [] config: address: 127.0.0.1 - port: 8082 \ No newline at end of file + port: 8082 diff --git a/aea/connections/tcp/tcp_client.py b/packages/connections/tcp/tcp_client.py similarity index 77% rename from aea/connections/tcp/tcp_client.py rename to packages/connections/tcp/tcp_client.py index c07c0c568f..3beb34e3dc 100644 --- a/aea/connections/tcp/tcp_client.py +++ b/packages/connections/tcp/tcp_client.py @@ -21,13 +21,18 @@ import asyncio import logging import struct +import sys from asyncio import StreamWriter, StreamReader, CancelledError -from typing import Optional, cast +from typing import Optional, cast, TYPE_CHECKING from aea.configurations.base import ConnectionConfig from aea.connections.base import Connection -from aea.connections.tcp.base import TCPConnection -from aea.mail.base import Envelope +from aea.mail.base import Envelope, Address + +if TYPE_CHECKING or "pytest" in sys.modules: + from packages.connections.tcp.base import TCPConnection +else: + from tcp_connection.base import TCPConnection logger = logging.getLogger(__name__) @@ -38,7 +43,7 @@ class TCPClientConnection(TCPConnection): """This class implements a TCP client.""" def __init__(self, - public_key: str, + address: Address, host: str, port: int, connection_id: str = "tcp_client", @@ -46,20 +51,20 @@ def __init__(self, """ Initialize a TCP channel. - :param public_key: public key. + :param address: address. :param host: the socket bind address. :param port: the socket bind port. :param connection_id: the identifier for the connection object. """ - super().__init__(public_key, host, port, connection_id, **kwargs) + super().__init__(address, host, port, connection_id, **kwargs) self._reader, self._writer = (None, None) # type: Optional[StreamReader], Optional[StreamWriter] async def setup(self): """Set the connection up.""" self._reader, self._writer = await asyncio.open_connection(self.host, self.port) - public_key_bytes = self.public_key.encode("utf-8") - await self._send(self._writer, public_key_bytes) + address_bytes = self.address.encode("utf-8") + await self._send(self._writer, address_bytes) async def teardown(self): """Tear the connection down.""" @@ -77,14 +82,14 @@ async def receive(self, *args, **kwargs) -> Optional['Envelope']: assert self._reader is not None data = await self._recv(self._reader) if data is None: - logger.debug("[{}] No data received.".format(self.public_key)) + logger.debug("[{}] No data received.".format(self.address)) return None - logger.debug("[{}] Message received: {!r}".format(self.public_key, data)) + logger.debug("[{}] Message received: {!r}".format(self.address, data)) envelope = Envelope.decode(data) # TODO handle decoding error - logger.debug("[{}] Decoded envelope: {}".format(self.public_key, envelope)) + logger.debug("[{}] Decoded envelope: {}".format(self.address, envelope)) return envelope except CancelledError: - logger.debug("[{}] Read cancelled.".format(self.public_key)) + logger.debug("[{}] Read cancelled.".format(self.address)) return None except struct.error as e: logger.debug("Struct error: {}".format(str(e))) @@ -98,15 +103,15 @@ def select_writer_from_envelope(self, envelope: Envelope) -> Optional[StreamWrit return self._writer @classmethod - def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection': + def from_config(cls, address: Address, connection_configuration: ConnectionConfig) -> 'Connection': """Get the TCP server connection from the connection configuration. - :param public_key: the public key of the agent. + :param address: the address of the agent. :param connection_configuration: the connection configuration object. :return: the connection object """ - address = cast(str, connection_configuration.config.get("address")) - port = cast(int, connection_configuration.config.get("port")) - return TCPClientConnection(public_key, address, port, + server_address = cast(str, connection_configuration.config.get("address")) + server_port = cast(int, connection_configuration.config.get("port")) + return TCPClientConnection(address, server_address, server_port, connection_id=connection_configuration.name, restricted_to_protocols=set(connection_configuration.restricted_to_protocols)) diff --git a/aea/connections/tcp/tcp_server.py b/packages/connections/tcp/tcp_server.py similarity index 73% rename from aea/connections/tcp/tcp_server.py rename to packages/connections/tcp/tcp_server.py index b5820230c7..f03023d672 100644 --- a/aea/connections/tcp/tcp_server.py +++ b/packages/connections/tcp/tcp_server.py @@ -21,13 +21,18 @@ """Implementation of the TCP server.""" import asyncio import logging +import sys from asyncio import StreamReader, StreamWriter, AbstractServer, Future -from typing import Dict, Optional, Tuple, cast +from typing import Dict, Optional, Tuple, cast, TYPE_CHECKING from aea.configurations.base import ConnectionConfig from aea.connections.base import Connection -from aea.connections.tcp.base import TCPConnection -from aea.mail.base import Envelope +from aea.mail.base import Envelope, Address + +if TYPE_CHECKING or "pytest" in sys.modules: + from packages.connections.tcp.base import TCPConnection +else: + from tcp_connection.base import TCPConnection logger = logging.getLogger(__name__) @@ -38,7 +43,7 @@ class TCPServerConnection(TCPConnection): """This class implements a TCP server.""" def __init__(self, - public_key: str, + address: Address, host: str, port: int, connection_id: str = "tcp_server", @@ -46,15 +51,15 @@ def __init__(self, """ Initialize a TCP channel. - :param public_key: public key. + :param address: address. :param host: the socket bind address. """ - super().__init__(public_key, host, port, connection_id, **kwargs) + super().__init__(address, host, port, connection_id, **kwargs) self._server = None # type: Optional[AbstractServer] self.connections = {} # type: Dict[str, Tuple[StreamReader, StreamWriter]] - self._read_tasks_to_public_key = dict() # type: Dict[Future, str] + self._read_tasks_to_address = dict() # type: Dict[Future, Address] async def handle(self, reader: StreamReader, writer: StreamWriter) -> None: """ @@ -64,15 +69,15 @@ async def handle(self, reader: StreamReader, writer: StreamWriter) -> None: :param writer: the stream writer. :return: None """ - logger.debug("Waiting for client public key...") - public_key_bytes = await self._recv(reader) - if public_key_bytes: - public_key_bytes = cast(bytes, public_key_bytes) - public_key = public_key_bytes.decode("utf-8") - logger.debug("Public key of the client: {}".format(public_key)) - self.connections[public_key] = (reader, writer) + logger.debug("Waiting for client address...") + address_bytes = await self._recv(reader) + if address_bytes: + address_bytes = cast(bytes, address_bytes) + address = address_bytes.decode("utf-8") + logger.debug("Public key of the client: {}".format(address)) + self.connections[address] = (reader, writer) read_task = asyncio.ensure_future(self._recv(reader), loop=self._loop) - self._read_tasks_to_public_key[read_task] = public_key + self._read_tasks_to_address[read_task] = address async def receive(self, *args, **kwargs) -> Optional['Envelope']: """ @@ -80,13 +85,13 @@ async def receive(self, *args, **kwargs) -> Optional['Envelope']: :return: the received envelope, or None if an error occurred. """ - if len(self._read_tasks_to_public_key) == 0: + if len(self._read_tasks_to_address) == 0: logger.warning("Tried to read from the TCP server. However, there is no open connection to read from.") return None try: logger.debug("Waiting for incoming messages...") - done, pending = await asyncio.wait(self._read_tasks_to_public_key.keys(), return_when=asyncio.FIRST_COMPLETED) # type: ignore + done, pending = await asyncio.wait(self._read_tasks_to_address.keys(), return_when=asyncio.FIRST_COMPLETED) # type: ignore # take the first task = next(iter(done)) @@ -95,10 +100,10 @@ async def receive(self, *args, **kwargs) -> Optional['Envelope']: logger.debug("[{}]: No data received.") return None envelope = Envelope.decode(envelope_bytes) - public_key = self._read_tasks_to_public_key.pop(task) - reader = self.connections[public_key][0] + address = self._read_tasks_to_address.pop(task) + reader = self.connections[address][0] new_task = asyncio.ensure_future(self._recv(reader), loop=self._loop) - self._read_tasks_to_public_key[new_task] = public_key + self._read_tasks_to_address[new_task] = address return envelope except asyncio.CancelledError: logger.debug("Receiving loop cancelled.") @@ -117,7 +122,7 @@ async def teardown(self): for pbk, (reader, _) in self.connections.items(): reader.feed_eof() - for t in self._read_tasks_to_public_key: + for t in self._read_tasks_to_address: t.cancel() self._server.close() @@ -131,15 +136,15 @@ def select_writer_from_envelope(self, envelope: Envelope): return writer @classmethod - def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection': + def from_config(cls, address: Address, connection_configuration: ConnectionConfig) -> 'Connection': """Get the TCP server connection from the connection configuration. - :param public_key: the public key of the agent. + :param address: the address of the agent. :param connection_configuration: the connection configuration object. :return: the connection object """ - address = cast(str, connection_configuration.config.get("address")) + server_address = cast(str, connection_configuration.config.get("address")) port = cast(int, connection_configuration.config.get("port")) - return TCPServerConnection(public_key, address, port, + return TCPServerConnection(address, server_address, port, connection_id=connection_configuration.name, restricted_to_protocols=set(connection_configuration.restricted_to_protocols)) diff --git a/aea/protocols/fipa/__init__.py b/packages/protocols/fipa/__init__.py similarity index 100% rename from aea/protocols/fipa/__init__.py rename to packages/protocols/fipa/__init__.py diff --git a/aea/protocols/fipa/dialogues.py b/packages/protocols/fipa/dialogues.py similarity index 81% rename from aea/protocols/fipa/dialogues.py rename to packages/protocols/fipa/dialogues.py index 2968a7aafc..abb8d903fa 100644 --- a/aea/protocols/fipa/dialogues.py +++ b/packages/protocols/fipa/dialogues.py @@ -27,12 +27,17 @@ """ from enum import Enum -from typing import Dict, Tuple, cast +import sys +from typing import Dict, Tuple, cast, TYPE_CHECKING from aea.helpers.dialogue.base import DialogueLabel, Dialogue, Dialogues from aea.mail.base import Address from aea.protocols.base import Message -from aea.protocols.fipa.message import FIPAMessage, VALID_PREVIOUS_PERFORMATIVES + +if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.fipa.message import FIPAMessage, VALID_PREVIOUS_PERFORMATIVES +else: + from fipa_protocol.message import FIPAMessage, VALID_PREVIOUS_PERFORMATIVES # pragma: no cover class FIPADialogue(Dialogue): @@ -75,24 +80,25 @@ def role(self) -> 'FIPADialogue.AgentRole': """Get role of agent in dialogue.""" return self._role - def is_valid_next_message(self, fipa_msg: FIPAMessage) -> bool: + def is_valid_next_message(self, fipa_msg: Message) -> bool: """ Check whether this is a valid next message in the dialogue. :return: True if yes, False otherwise. """ - this_message_id = fipa_msg.get("message_id") - this_target = fipa_msg.get("target") - this_performative = cast(FIPAMessage.Performative, fipa_msg.get("performative")) - last_outgoing_message = self.last_outgoing_message + fipa_msg = cast(FIPAMessage, fipa_msg) + this_message_id = fipa_msg.message_id + this_target = fipa_msg.target + this_performative = fipa_msg.performative + last_outgoing_message = cast(FIPAMessage, self.last_outgoing_message) if last_outgoing_message is None: result = this_message_id == FIPAMessage.STARTING_MESSAGE_ID and \ this_target == FIPAMessage.STARTING_TARGET and \ this_performative == FIPAMessage.Performative.CFP else: - last_message_id = cast(int, last_outgoing_message.get("message_id")) - last_target = cast(int, last_outgoing_message.get("target")) - last_performative = cast(FIPAMessage.Performative, last_outgoing_message.get("performative")) + last_message_id = last_outgoing_message.message_id + last_target = last_outgoing_message.target + last_performative = last_outgoing_message.performative result = this_message_id == last_message_id + 1 and \ this_target == last_target + 1 and \ last_performative in VALID_PREVIOUS_PERFORMATIVES[this_performative] @@ -108,8 +114,8 @@ def assign_final_dialogue_label(self, final_dialogue_label: DialogueLabel) -> No assert self.dialogue_label.dialogue_starter_reference == final_dialogue_label.dialogue_starter_reference assert self.dialogue_label.dialogue_responder_reference == '' assert final_dialogue_label.dialogue_responder_reference != '' - assert self.dialogue_label.dialogue_opponent_pbk == final_dialogue_label.dialogue_opponent_pbk - assert self.dialogue_label.dialogue_starter_pbk == final_dialogue_label.dialogue_starter_pbk + assert self.dialogue_label.dialogue_opponent_addr == final_dialogue_label.dialogue_opponent_addr + assert self.dialogue_label.dialogue_starter_addr == final_dialogue_label.dialogue_starter_addr self._dialogue_label = final_dialogue_label @@ -182,45 +188,44 @@ def dialogue_stats(self) -> FIPADialogueStats: """Get the dialogue statistics.""" return self._dialogue_stats - def is_permitted_for_new_dialogue(self, fipa_msg: Message, sender: Address) -> bool: + def is_permitted_for_new_dialogue(self, fipa_msg: Message) -> bool: """ Check whether a fipa message is permitted for a new dialogue. That is, the message has to - be a CFP, and - - have the correct msg id and message target. + - have the correct msg id and message target + - have msg counterparty set. :param message: the fipa message - :param sender: the sender :return: a boolean indicating whether the message is permitted for a new dialogue """ fipa_msg = cast(FIPAMessage, fipa_msg) - this_message_id = fipa_msg.get("message_id") - this_target = fipa_msg.get("target") - this_performative = fipa_msg.get("performative") + this_message_id = fipa_msg.message_id + this_target = fipa_msg.target + this_performative = fipa_msg.performative result = this_message_id == FIPAMessage.STARTING_MESSAGE_ID and \ this_target == FIPAMessage.STARTING_TARGET and \ this_performative == FIPAMessage.Performative.CFP return result - def is_belonging_to_registered_dialogue(self, fipa_msg: Message, sender: Address, agent_pbk: Address) -> bool: + def is_belonging_to_registered_dialogue(self, fipa_msg: Message, agent_addr: Address) -> bool: """ Check whether an agent message is part of a registered dialogue. :param fipa_msg: the fipa message - :param sender: the sender - :param agent_pbk: the public key of the agent + :param agent_addr: the address of the agent :return: boolean indicating whether the message belongs to a registered dialogue """ fipa_msg = cast(FIPAMessage, fipa_msg) - dialogue_reference = cast(Tuple[str, str], fipa_msg.get("dialogue_reference")) + dialogue_reference = fipa_msg.dialogue_reference alt_dialogue_reference = (dialogue_reference[0], '') - self_initiated_dialogue_label = DialogueLabel(dialogue_reference, sender, agent_pbk) - alt_self_initiated_dialogue_label = DialogueLabel(alt_dialogue_reference, sender, agent_pbk) - other_initiated_dialogue_label = DialogueLabel(dialogue_reference, sender, sender) + self_initiated_dialogue_label = DialogueLabel(dialogue_reference, fipa_msg.counterparty, agent_addr) + alt_self_initiated_dialogue_label = DialogueLabel(alt_dialogue_reference, fipa_msg.counterparty, agent_addr) + other_initiated_dialogue_label = DialogueLabel(dialogue_reference, fipa_msg.counterparty, fipa_msg.counterparty) result = False if other_initiated_dialogue_label in self.dialogues: other_initiated_dialogue = cast(FIPADialogue, self.dialogues[other_initiated_dialogue_label]) @@ -233,25 +238,24 @@ def is_belonging_to_registered_dialogue(self, fipa_msg: Message, sender: Address result = self_initiated_dialogue.is_valid_next_message(fipa_msg) if result: self._initiated_dialogues.pop(alt_self_initiated_dialogue_label) - final_dialogue_label = DialogueLabel(dialogue_reference, alt_self_initiated_dialogue_label.dialogue_opponent_pbk, alt_self_initiated_dialogue_label.dialogue_starter_pbk) + final_dialogue_label = DialogueLabel(dialogue_reference, alt_self_initiated_dialogue_label.dialogue_opponent_addr, alt_self_initiated_dialogue_label.dialogue_starter_addr) self_initiated_dialogue.assign_final_dialogue_label(final_dialogue_label) self._add(self_initiated_dialogue) return result - def get_dialogue(self, fipa_msg: Message, sender: Address, agent_pbk: Address) -> Dialogue: + def get_dialogue(self, fipa_msg: Message, agent_addr: Address) -> Dialogue: """ Retrieve dialogue. :param fipa_msg: the fipa message - :param sender_pbk: the sender public key - :param agent_pbk: the public key of the agent + :param agent_addr: the address of the agent :return: the dialogue """ fipa_msg = cast(FIPAMessage, fipa_msg) - dialogue_reference = cast(Tuple[str, str], fipa_msg.get("dialogue_reference")) - self_initiated_dialogue_label = DialogueLabel(dialogue_reference, sender, agent_pbk) - other_initiated_dialogue_label = DialogueLabel(dialogue_reference, sender, sender) + dialogue_reference = fipa_msg.dialogue_reference + self_initiated_dialogue_label = DialogueLabel(dialogue_reference, fipa_msg.counterparty, agent_addr) + other_initiated_dialogue_label = DialogueLabel(dialogue_reference, fipa_msg.counterparty, fipa_msg.counterparty) if other_initiated_dialogue_label in self.dialogues: other_initiated_dialogue = cast(FIPADialogue, self.dialogues[other_initiated_dialogue_label]) if other_initiated_dialogue.is_valid_next_message(fipa_msg): @@ -264,35 +268,35 @@ def get_dialogue(self, fipa_msg: Message, sender: Address, agent_pbk: Address) - raise ValueError('Should have found dialogue.') return result - def create_self_initiated(self, dialogue_opponent_pbk: Address, dialogue_starter_pbk: Address, is_seller: bool) -> Dialogue: + def create_self_initiated(self, dialogue_opponent_addr: Address, dialogue_starter_addr: Address, is_seller: bool) -> Dialogue: """ Create a self initiated dialogue. - :param dialogue_opponent_pbk: the pbk of the agent with which the dialogue is kept. - :param dialogue_starter_pbk: the pbk of the agent which started the dialogue + :param dialogue_opponent_addr: the pbk of the agent with which the dialogue is kept. + :param dialogue_starter_addr: the pbk of the agent which started the dialogue :param is_seller: boolean indicating the agent role :return: the created dialogue. """ dialogue_reference = (str(self._next_dialogue_nonce()), '') - dialogue_label = DialogueLabel(dialogue_reference, dialogue_opponent_pbk, dialogue_starter_pbk) + dialogue_label = DialogueLabel(dialogue_reference, dialogue_opponent_addr, dialogue_starter_addr) dialogue = FIPADialogue(dialogue_label, is_seller) self._initiated_dialogues.update({dialogue_label: dialogue}) return dialogue - def create_opponent_initiated(self, dialogue_opponent_pbk: Address, dialogue_reference: Tuple[str, str], is_seller: bool) -> Dialogue: + def create_opponent_initiated(self, dialogue_opponent_addr: Address, dialogue_reference: Tuple[str, str], + is_seller: bool) -> Dialogue: """ Save an opponent initiated dialogue. - :param dialogue_opponent_pbk: the pbk of the agent with which the dialogue is kept. - :param dialogue_id: the id of the dialogue - :param sender: the pbk of the sender - + :param dialogue_opponent_addr: the address of the agent with which the dialogue is kept. + :param dialogue_reference: the reference of the dialogue. + :param is_seller: keeps track if the counterparty is a seller. :return: the created dialogue """ assert dialogue_reference[0] != '' and dialogue_reference[1] == '', "Cannot initiate dialogue with preassigned dialogue_responder_reference!" new_dialogue_reference = (dialogue_reference[0], str(self._next_dialogue_nonce())) - dialogue_label = DialogueLabel(new_dialogue_reference, dialogue_opponent_pbk, dialogue_opponent_pbk) + dialogue_label = DialogueLabel(new_dialogue_reference, dialogue_opponent_addr, dialogue_opponent_addr) result = self._create(dialogue_label, is_seller) return result diff --git a/aea/protocols/fipa/fipa.proto b/packages/protocols/fipa/fipa.proto similarity index 69% rename from aea/protocols/fipa/fipa.proto rename to packages/protocols/fipa/fipa.proto index 65ab5f6ca7..310a5aab2b 100644 --- a/aea/protocols/fipa/fipa.proto +++ b/packages/protocols/fipa/fipa.proto @@ -20,24 +20,18 @@ message FIPAMessage{ message MatchAccept{} - message Accept_W_Address{ - string address = 1; - } - - message MatchAccept_W_Address{ - string address = 1; - } message Decline{} + message Inform{ bytes bytes = 1; } - message AcceptWAddress{ - string address = 1; + message AcceptWInform{ + bytes bytes = 1; } - message MatchAcceptWAddress{ - string address = 1; + message MatchAcceptWInform{ + bytes bytes = 1; } int32 message_id = 1; @@ -51,7 +45,7 @@ message FIPAMessage{ MatchAccept match_accept = 8; Decline decline = 9; Inform inform = 10; - AcceptWAddress accept_w_address = 11; - MatchAcceptWAddress match_accept_w_address = 12; + AcceptWInform accept_w_inform = 11; + MatchAcceptWInform match_accept_w_inform = 12; } } diff --git a/aea/protocols/fipa/fipa_pb2.py b/packages/protocols/fipa/fipa_pb2.py similarity index 73% rename from aea/protocols/fipa/fipa_pb2.py rename to packages/protocols/fipa/fipa_pb2.py index 9407ded226..04675f843f 100644 --- a/aea/protocols/fipa/fipa_pb2.py +++ b/packages/protocols/fipa/fipa_pb2.py @@ -20,7 +20,7 @@ package='fetch.aea.fipa', syntax='proto3', serialized_options=None, - serialized_pb=_b('\n\nfipa.proto\x12\x0e\x66\x65tch.aea.fipa\"\xe6\x07\n\x0b\x46IPAMessage\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.\n\x03\x63\x66p\x18\x05 \x01(\x0b\x32\x1f.fetch.aea.fipa.FIPAMessage.CFPH\x00\x12\x36\n\x07propose\x18\x06 \x01(\x0b\x32#.fetch.aea.fipa.FIPAMessage.ProposeH\x00\x12\x34\n\x06\x61\x63\x63\x65pt\x18\x07 \x01(\x0b\x32\".fetch.aea.fipa.FIPAMessage.AcceptH\x00\x12?\n\x0cmatch_accept\x18\x08 \x01(\x0b\x32\'.fetch.aea.fipa.FIPAMessage.MatchAcceptH\x00\x12\x36\n\x07\x64\x65\x63line\x18\t \x01(\x0b\x32#.fetch.aea.fipa.FIPAMessage.DeclineH\x00\x12\x34\n\x06inform\x18\n \x01(\x0b\x32\".fetch.aea.fipa.FIPAMessage.InformH\x00\x12\x46\n\x10\x61\x63\x63\x65pt_w_address\x18\x0b \x01(\x0b\x32*.fetch.aea.fipa.FIPAMessage.AcceptWAddressH\x00\x12Q\n\x16match_accept_w_address\x18\x0c \x01(\x0b\x32/.fetch.aea.fipa.FIPAMessage.MatchAcceptWAddressH\x00\x1a}\n\x03\x43\x46P\x12\x0f\n\x05\x62ytes\x18\x01 \x01(\x0cH\x00\x12:\n\x07nothing\x18\x02 \x01(\x0b\x32\'.fetch.aea.fipa.FIPAMessage.CFP.NothingH\x00\x12\x15\n\x0bquery_bytes\x18\x03 \x01(\x0cH\x00\x1a\t\n\x07NothingB\x07\n\x05query\x1a\x1b\n\x07Propose\x12\x10\n\x08proposal\x18\x01 \x03(\x0c\x1a\x08\n\x06\x41\x63\x63\x65pt\x1a\r\n\x0bMatchAccept\x1a#\n\x10\x41\x63\x63\x65pt_W_Address\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x1a(\n\x15MatchAccept_W_Address\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x1a\t\n\x07\x44\x65\x63line\x1a\x17\n\x06Inform\x12\r\n\x05\x62ytes\x18\x01 \x01(\x0c\x1a!\n\x0e\x41\x63\x63\x65ptWAddress\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x1a&\n\x13MatchAcceptWAddress\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\tB\x0e\n\x0cperformativeb\x06proto3') + serialized_pb=_b('\n\nfipa.proto\x12\x0e\x66\x65tch.aea.fipa\"\x8d\x07\n\x0b\x46IPAMessage\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.\n\x03\x63\x66p\x18\x05 \x01(\x0b\x32\x1f.fetch.aea.fipa.FIPAMessage.CFPH\x00\x12\x36\n\x07propose\x18\x06 \x01(\x0b\x32#.fetch.aea.fipa.FIPAMessage.ProposeH\x00\x12\x34\n\x06\x61\x63\x63\x65pt\x18\x07 \x01(\x0b\x32\".fetch.aea.fipa.FIPAMessage.AcceptH\x00\x12?\n\x0cmatch_accept\x18\x08 \x01(\x0b\x32\'.fetch.aea.fipa.FIPAMessage.MatchAcceptH\x00\x12\x36\n\x07\x64\x65\x63line\x18\t \x01(\x0b\x32#.fetch.aea.fipa.FIPAMessage.DeclineH\x00\x12\x34\n\x06inform\x18\n \x01(\x0b\x32\".fetch.aea.fipa.FIPAMessage.InformH\x00\x12\x44\n\x0f\x61\x63\x63\x65pt_w_inform\x18\x0b \x01(\x0b\x32).fetch.aea.fipa.FIPAMessage.AcceptWInformH\x00\x12O\n\x15match_accept_w_inform\x18\x0c \x01(\x0b\x32..fetch.aea.fipa.FIPAMessage.MatchAcceptWInformH\x00\x1a}\n\x03\x43\x46P\x12\x0f\n\x05\x62ytes\x18\x01 \x01(\x0cH\x00\x12:\n\x07nothing\x18\x02 \x01(\x0b\x32\'.fetch.aea.fipa.FIPAMessage.CFP.NothingH\x00\x12\x15\n\x0bquery_bytes\x18\x03 \x01(\x0cH\x00\x1a\t\n\x07NothingB\x07\n\x05query\x1a\x1b\n\x07Propose\x12\x10\n\x08proposal\x18\x01 \x03(\x0c\x1a\x08\n\x06\x41\x63\x63\x65pt\x1a\r\n\x0bMatchAccept\x1a\t\n\x07\x44\x65\x63line\x1a\x17\n\x06Inform\x12\r\n\x05\x62ytes\x18\x01 \x01(\x0c\x1a\x1e\n\rAcceptWInform\x12\r\n\x05\x62ytes\x18\x01 \x01(\x0c\x1a#\n\x12MatchAcceptWInform\x12\r\n\x05\x62ytes\x18\x01 \x01(\x0c\x42\x0e\n\x0cperformativeb\x06proto3') ) @@ -45,8 +45,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=751, - serialized_end=760, + serialized_start=747, + serialized_end=756, ) _FIPAMESSAGE_CFP = _descriptor.Descriptor( @@ -92,8 +92,8 @@ name='query', full_name='fetch.aea.fipa.FIPAMessage.CFP.query', index=0, containing_type=None, fields=[]), ], - serialized_start=644, - serialized_end=769, + serialized_start=640, + serialized_end=765, ) _FIPAMESSAGE_PROPOSE = _descriptor.Descriptor( @@ -122,8 +122,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=771, - serialized_end=798, + serialized_start=767, + serialized_end=794, ) _FIPAMESSAGE_ACCEPT = _descriptor.Descriptor( @@ -145,8 +145,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=800, - serialized_end=808, + serialized_start=796, + serialized_end=804, ) _FIPAMESSAGE_MATCHACCEPT = _descriptor.Descriptor( @@ -168,68 +168,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=810, - serialized_end=823, -) - -_FIPAMESSAGE_ACCEPT_W_ADDRESS = _descriptor.Descriptor( - name='Accept_W_Address', - full_name='fetch.aea.fipa.FIPAMessage.Accept_W_Address', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='address', full_name='fetch.aea.fipa.FIPAMessage.Accept_W_Address.address', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=825, - serialized_end=860, -) - -_FIPAMESSAGE_MATCHACCEPT_W_ADDRESS = _descriptor.Descriptor( - name='MatchAccept_W_Address', - full_name='fetch.aea.fipa.FIPAMessage.MatchAccept_W_Address', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='address', full_name='fetch.aea.fipa.FIPAMessage.MatchAccept_W_Address.address', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=862, - serialized_end=902, + serialized_start=806, + serialized_end=819, ) _FIPAMESSAGE_DECLINE = _descriptor.Descriptor( @@ -251,8 +191,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=904, - serialized_end=913, + serialized_start=821, + serialized_end=830, ) _FIPAMESSAGE_INFORM = _descriptor.Descriptor( @@ -281,21 +221,21 @@ extension_ranges=[], oneofs=[ ], - serialized_start=915, - serialized_end=938, + serialized_start=832, + serialized_end=855, ) -_FIPAMESSAGE_ACCEPTWADDRESS = _descriptor.Descriptor( - name='AcceptWAddress', - full_name='fetch.aea.fipa.FIPAMessage.AcceptWAddress', +_FIPAMESSAGE_ACCEPTWINFORM = _descriptor.Descriptor( + name='AcceptWInform', + full_name='fetch.aea.fipa.FIPAMessage.AcceptWInform', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='address', full_name='fetch.aea.fipa.FIPAMessage.AcceptWAddress.address', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), + name='bytes', full_name='fetch.aea.fipa.FIPAMessage.AcceptWInform.bytes', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), @@ -311,21 +251,21 @@ extension_ranges=[], oneofs=[ ], - serialized_start=940, - serialized_end=973, + serialized_start=857, + serialized_end=887, ) -_FIPAMESSAGE_MATCHACCEPTWADDRESS = _descriptor.Descriptor( - name='MatchAcceptWAddress', - full_name='fetch.aea.fipa.FIPAMessage.MatchAcceptWAddress', +_FIPAMESSAGE_MATCHACCEPTWINFORM = _descriptor.Descriptor( + name='MatchAcceptWInform', + full_name='fetch.aea.fipa.FIPAMessage.MatchAcceptWInform', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='address', full_name='fetch.aea.fipa.FIPAMessage.MatchAcceptWAddress.address', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), + name='bytes', full_name='fetch.aea.fipa.FIPAMessage.MatchAcceptWInform.bytes', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), @@ -341,8 +281,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=975, - serialized_end=1013, + serialized_start=889, + serialized_end=924, ) _FIPAMESSAGE = _descriptor.Descriptor( @@ -423,14 +363,14 @@ is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='accept_w_address', full_name='fetch.aea.fipa.FIPAMessage.accept_w_address', index=10, + name='accept_w_inform', full_name='fetch.aea.fipa.FIPAMessage.accept_w_inform', index=10, number=11, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='match_accept_w_address', full_name='fetch.aea.fipa.FIPAMessage.match_accept_w_address', index=11, + name='match_accept_w_inform', full_name='fetch.aea.fipa.FIPAMessage.match_accept_w_inform', index=11, number=12, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, @@ -439,7 +379,7 @@ ], extensions=[ ], - nested_types=[_FIPAMESSAGE_CFP, _FIPAMESSAGE_PROPOSE, _FIPAMESSAGE_ACCEPT, _FIPAMESSAGE_MATCHACCEPT, _FIPAMESSAGE_ACCEPT_W_ADDRESS, _FIPAMESSAGE_MATCHACCEPT_W_ADDRESS, _FIPAMESSAGE_DECLINE, _FIPAMESSAGE_INFORM, _FIPAMESSAGE_ACCEPTWADDRESS, _FIPAMESSAGE_MATCHACCEPTWADDRESS, ], + nested_types=[_FIPAMESSAGE_CFP, _FIPAMESSAGE_PROPOSE, _FIPAMESSAGE_ACCEPT, _FIPAMESSAGE_MATCHACCEPT, _FIPAMESSAGE_DECLINE, _FIPAMESSAGE_INFORM, _FIPAMESSAGE_ACCEPTWINFORM, _FIPAMESSAGE_MATCHACCEPTWINFORM, ], enum_types=[ ], serialized_options=None, @@ -452,7 +392,7 @@ index=0, containing_type=None, fields=[]), ], serialized_start=31, - serialized_end=1029, + serialized_end=940, ) _FIPAMESSAGE_CFP_NOTHING.containing_type = _FIPAMESSAGE_CFP @@ -470,20 +410,18 @@ _FIPAMESSAGE_PROPOSE.containing_type = _FIPAMESSAGE _FIPAMESSAGE_ACCEPT.containing_type = _FIPAMESSAGE _FIPAMESSAGE_MATCHACCEPT.containing_type = _FIPAMESSAGE -_FIPAMESSAGE_ACCEPT_W_ADDRESS.containing_type = _FIPAMESSAGE -_FIPAMESSAGE_MATCHACCEPT_W_ADDRESS.containing_type = _FIPAMESSAGE _FIPAMESSAGE_DECLINE.containing_type = _FIPAMESSAGE _FIPAMESSAGE_INFORM.containing_type = _FIPAMESSAGE -_FIPAMESSAGE_ACCEPTWADDRESS.containing_type = _FIPAMESSAGE -_FIPAMESSAGE_MATCHACCEPTWADDRESS.containing_type = _FIPAMESSAGE +_FIPAMESSAGE_ACCEPTWINFORM.containing_type = _FIPAMESSAGE +_FIPAMESSAGE_MATCHACCEPTWINFORM.containing_type = _FIPAMESSAGE _FIPAMESSAGE.fields_by_name['cfp'].message_type = _FIPAMESSAGE_CFP _FIPAMESSAGE.fields_by_name['propose'].message_type = _FIPAMESSAGE_PROPOSE _FIPAMESSAGE.fields_by_name['accept'].message_type = _FIPAMESSAGE_ACCEPT _FIPAMESSAGE.fields_by_name['match_accept'].message_type = _FIPAMESSAGE_MATCHACCEPT _FIPAMESSAGE.fields_by_name['decline'].message_type = _FIPAMESSAGE_DECLINE _FIPAMESSAGE.fields_by_name['inform'].message_type = _FIPAMESSAGE_INFORM -_FIPAMESSAGE.fields_by_name['accept_w_address'].message_type = _FIPAMESSAGE_ACCEPTWADDRESS -_FIPAMESSAGE.fields_by_name['match_accept_w_address'].message_type = _FIPAMESSAGE_MATCHACCEPTWADDRESS +_FIPAMESSAGE.fields_by_name['accept_w_inform'].message_type = _FIPAMESSAGE_ACCEPTWINFORM +_FIPAMESSAGE.fields_by_name['match_accept_w_inform'].message_type = _FIPAMESSAGE_MATCHACCEPTWINFORM _FIPAMESSAGE.oneofs_by_name['performative'].fields.append( _FIPAMESSAGE.fields_by_name['cfp']) _FIPAMESSAGE.fields_by_name['cfp'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] @@ -503,11 +441,11 @@ _FIPAMESSAGE.fields_by_name['inform']) _FIPAMESSAGE.fields_by_name['inform'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] _FIPAMESSAGE.oneofs_by_name['performative'].fields.append( - _FIPAMESSAGE.fields_by_name['accept_w_address']) -_FIPAMESSAGE.fields_by_name['accept_w_address'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] + _FIPAMESSAGE.fields_by_name['accept_w_inform']) +_FIPAMESSAGE.fields_by_name['accept_w_inform'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] _FIPAMESSAGE.oneofs_by_name['performative'].fields.append( - _FIPAMESSAGE.fields_by_name['match_accept_w_address']) -_FIPAMESSAGE.fields_by_name['match_accept_w_address'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] + _FIPAMESSAGE.fields_by_name['match_accept_w_inform']) +_FIPAMESSAGE.fields_by_name['match_accept_w_inform'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] DESCRIPTOR.message_types_by_name['FIPAMessage'] = _FIPAMESSAGE _sym_db.RegisterFileDescriptor(DESCRIPTOR) @@ -548,20 +486,6 @@ )) , - Accept_W_Address = _reflection.GeneratedProtocolMessageType('Accept_W_Address', (_message.Message,), dict( - DESCRIPTOR = _FIPAMESSAGE_ACCEPT_W_ADDRESS, - __module__ = 'fipa_pb2' - # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.Accept_W_Address) - )) - , - - MatchAccept_W_Address = _reflection.GeneratedProtocolMessageType('MatchAccept_W_Address', (_message.Message,), dict( - DESCRIPTOR = _FIPAMESSAGE_MATCHACCEPT_W_ADDRESS, - __module__ = 'fipa_pb2' - # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.MatchAccept_W_Address) - )) - , - Decline = _reflection.GeneratedProtocolMessageType('Decline', (_message.Message,), dict( DESCRIPTOR = _FIPAMESSAGE_DECLINE, __module__ = 'fipa_pb2' @@ -576,17 +500,17 @@ )) , - AcceptWAddress = _reflection.GeneratedProtocolMessageType('AcceptWAddress', (_message.Message,), dict( - DESCRIPTOR = _FIPAMESSAGE_ACCEPTWADDRESS, + AcceptWInform = _reflection.GeneratedProtocolMessageType('AcceptWInform', (_message.Message,), dict( + DESCRIPTOR = _FIPAMESSAGE_ACCEPTWINFORM, __module__ = 'fipa_pb2' - # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.AcceptWAddress) + # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.AcceptWInform) )) , - MatchAcceptWAddress = _reflection.GeneratedProtocolMessageType('MatchAcceptWAddress', (_message.Message,), dict( - DESCRIPTOR = _FIPAMESSAGE_MATCHACCEPTWADDRESS, + MatchAcceptWInform = _reflection.GeneratedProtocolMessageType('MatchAcceptWInform', (_message.Message,), dict( + DESCRIPTOR = _FIPAMESSAGE_MATCHACCEPTWINFORM, __module__ = 'fipa_pb2' - # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.MatchAcceptWAddress) + # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.MatchAcceptWInform) )) , DESCRIPTOR = _FIPAMESSAGE, @@ -599,12 +523,10 @@ _sym_db.RegisterMessage(FIPAMessage.Propose) _sym_db.RegisterMessage(FIPAMessage.Accept) _sym_db.RegisterMessage(FIPAMessage.MatchAccept) -_sym_db.RegisterMessage(FIPAMessage.Accept_W_Address) -_sym_db.RegisterMessage(FIPAMessage.MatchAccept_W_Address) _sym_db.RegisterMessage(FIPAMessage.Decline) _sym_db.RegisterMessage(FIPAMessage.Inform) -_sym_db.RegisterMessage(FIPAMessage.AcceptWAddress) -_sym_db.RegisterMessage(FIPAMessage.MatchAcceptWAddress) +_sym_db.RegisterMessage(FIPAMessage.AcceptWInform) +_sym_db.RegisterMessage(FIPAMessage.MatchAcceptWInform) # @@protoc_insertion_point(module_scope) diff --git a/packages/protocols/fipa/message.py b/packages/protocols/fipa/message.py new file mode 100644 index 0000000000..6bcb368490 --- /dev/null +++ b/packages/protocols/fipa/message.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the FIPA message definition.""" +from enum import Enum +from typing import Any, Dict, List, Tuple, Union, cast + +from aea.helpers.search.models import Description, Query +from aea.protocols.base import Message + + +class FIPAMessage(Message): + """The FIPA message class.""" + + protocol_id = "fipa" + + STARTING_MESSAGE_ID = 1 + STARTING_TARGET = 0 + + class Performative(Enum): + """FIPA performatives.""" + + CFP = "cfp" + PROPOSE = "propose" + ACCEPT = "accept" + MATCH_ACCEPT = "match_accept" + DECLINE = "decline" + INFORM = "inform" + ACCEPT_W_INFORM = "accept_w_inform" + MATCH_ACCEPT_W_INFORM = "match_accept_w_inform" + + def __str__(self): + """Get string representation.""" + return self.value + + def __init__(self, dialogue_reference: Tuple[str, str], + message_id: int, + target: int, + performative: Performative, + **kwargs): + """ + Initialize. + + :param message_id: the message id. + :param dialogue_reference: the dialogue reference. + :param target: the message target. + :param performative: the message performative. + """ + super().__init__(message_id=message_id, + dialogue_reference=dialogue_reference, + target=target, + performative=FIPAMessage.Performative(performative), + **kwargs) + assert self.check_consistency(), "FIPAMessage initialization inconsistent." + + @property + def dialogue_reference(self) -> Tuple[str, str]: + """Get the dialogue_reference of the message.""" + assert self.is_set("dialogue_reference"), " dialogue_reference is not set" + return cast(Tuple[str, str], self.get("dialogue_reference")) + + @property + def message_id(self) -> int: + """Get the message_id of the message.""" + assert self.is_set("message_id"), "message_id is not set" + return cast(int, self.get("message_id")) + + @property + def target(self) -> int: + """Get the target of the message.""" + assert self.is_set("target"), "target is not set." + return cast(int, self.get("target")) + + @property + def performative(self) -> Performative: # noqa: F821 + """Get the performative of the message.""" + return FIPAMessage.Performative(self.get("performative")) + + @property + def query(self) -> Union[Query, bytes, None]: + """Get the query of the message.""" + assert self.is_set("query"), "query is not set." + return cast(Union[Query, bytes, None], self.get("query")) + + @property + def proposal(self) -> List[Description]: + """Get the proposal list from the message.""" + assert self.is_set("proposal"), "proposal is not set." + return cast(List[Description], self.get("proposal")) + + @property + def info(self) -> Dict[str, Any]: + """Get hte info from the message.""" + assert self.is_set("info"), "info is not set." + return cast(Dict[str, Any], self.get("info")) + + def check_consistency(self) -> bool: + """Check that the data is consistent.""" + try: + assert isinstance(self.performative, FIPAMessage.Performative) + assert isinstance(self.dialogue_reference, tuple) + assert isinstance(self.dialogue_reference[0], str) and isinstance(self.dialogue_reference[1], str) + assert isinstance(self.message_id, int) + assert isinstance(self.target, int) + if self.performative == FIPAMessage.Performative.CFP: + assert isinstance(self.query, Query) or isinstance(self.query, bytes) or self.query is None + assert len(self.body) == 5 + elif self.performative == FIPAMessage.Performative.PROPOSE: + assert isinstance(self.proposal, list) and all(isinstance(d, Description) for d in self.proposal) + assert len(self.body) == 5 + elif self.performative == FIPAMessage.Performative.ACCEPT \ + or self.performative == FIPAMessage.Performative.MATCH_ACCEPT \ + or self.performative == FIPAMessage.Performative.DECLINE: + assert len(self.body) == 4 + elif self.performative == FIPAMessage.Performative.ACCEPT_W_INFORM\ + or self.performative == FIPAMessage.Performative.MATCH_ACCEPT_W_INFORM\ + or self.performative == FIPAMessage.Performative.INFORM: + assert isinstance(self.info, dict) + assert len(self.body) == 5 + else: + raise ValueError("Performative not recognized.") + + except (AssertionError, ValueError, KeyError): + return False + + return True + + +VALID_PREVIOUS_PERFORMATIVES = { + FIPAMessage.Performative.CFP: [None], + FIPAMessage.Performative.PROPOSE: [FIPAMessage.Performative.CFP], + FIPAMessage.Performative.ACCEPT: [FIPAMessage.Performative.PROPOSE], + FIPAMessage.Performative.ACCEPT_W_INFORM: [FIPAMessage.Performative.PROPOSE], + FIPAMessage.Performative.MATCH_ACCEPT: [FIPAMessage.Performative.ACCEPT, FIPAMessage.Performative.ACCEPT_W_INFORM], + FIPAMessage.Performative.MATCH_ACCEPT_W_INFORM: [FIPAMessage.Performative.ACCEPT, FIPAMessage.Performative.ACCEPT_W_INFORM], + FIPAMessage.Performative.INFORM: [FIPAMessage.Performative.MATCH_ACCEPT, FIPAMessage.Performative.MATCH_ACCEPT_W_INFORM, FIPAMessage.Performative.INFORM], + FIPAMessage.Performative.DECLINE: [FIPAMessage.Performative.CFP, FIPAMessage.Performative.PROPOSE, FIPAMessage.Performative.ACCEPT, FIPAMessage.Performative.ACCEPT_W_INFORM] +} # type: Dict[FIPAMessage.Performative, List[Union[None, FIPAMessage.Performative]]] diff --git a/aea/protocols/fipa/protocol.yaml b/packages/protocols/fipa/protocol.yaml similarity index 73% rename from aea/protocols/fipa/protocol.yaml rename to packages/protocols/fipa/protocol.yaml index f9197cdcb6..640086227e 100644 --- a/aea/protocols/fipa/protocol.yaml +++ b/packages/protocols/fipa/protocol.yaml @@ -1,8 +1,9 @@ name: 'fipa' -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" dependencies: - - protobuf + protobuf: {} description: "The fipa protocol implements the FIPA ACL." diff --git a/aea/protocols/fipa/serialization.py b/packages/protocols/fipa/serialization.py similarity index 77% rename from aea/protocols/fipa/serialization.py rename to packages/protocols/fipa/serialization.py index 959e889aaa..702b720e20 100644 --- a/aea/protocols/fipa/serialization.py +++ b/packages/protocols/fipa/serialization.py @@ -21,13 +21,19 @@ """Serialization for the FIPA protocol.""" import json import pickle -from typing import Tuple, cast +import sys +from typing import cast, TYPE_CHECKING +from aea.helpers.search.models import Description, Query from aea.protocols.base import Message from aea.protocols.base import Serializer -from aea.protocols.fipa import fipa_pb2 -from aea.protocols.fipa.message import FIPAMessage -from aea.protocols.oef.models import Description, Query + +if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.fipa import fipa_pb2 + from packages.protocols.fipa.message import FIPAMessage +else: + import fipa_protocol.fipa_pb2 as fipa_pb2 # pragma: no cover + from fipa_protocol.message import FIPAMessage # pragma: no cover class FIPASerializer(Serializer): @@ -35,17 +41,18 @@ class FIPASerializer(Serializer): def encode(self, msg: Message) -> bytes: """Encode a FIPA message into bytes.""" + msg = cast(FIPAMessage, msg) fipa_msg = fipa_pb2.FIPAMessage() - fipa_msg.message_id = msg.get("message_id") - dialogue_reference = cast(Tuple[str, str], msg.get("dialogue_reference")) + fipa_msg.message_id = msg.message_id + dialogue_reference = msg.dialogue_reference fipa_msg.dialogue_starter_reference = dialogue_reference[0] fipa_msg.dialogue_responder_reference = dialogue_reference[1] - fipa_msg.target = msg.get("target") + fipa_msg.target = msg.target - performative_id = FIPAMessage.Performative(msg.get("performative")) + performative_id = msg.performative if performative_id == FIPAMessage.Performative.CFP: performative = fipa_pb2.FIPAMessage.CFP() # type: ignore - query = msg.get("query") + query = msg.query if query is None or query == b"": nothing = fipa_pb2.FIPAMessage.CFP.Nothing() # type: ignore performative.nothing.CopyFrom(nothing) @@ -59,7 +66,7 @@ def encode(self, msg: Message) -> bytes: fipa_msg.cfp.CopyFrom(performative) elif performative_id == FIPAMessage.Performative.PROPOSE: performative = fipa_pb2.FIPAMessage.Propose() # type: ignore - proposal = cast(Description, msg.get("proposal")) + proposal = msg.proposal p_array_bytes = [pickle.dumps(p) for p in proposal] performative.proposal.extend(p_array_bytes) fipa_msg.propose.CopyFrom(performative) @@ -69,24 +76,24 @@ def encode(self, msg: Message) -> bytes: elif performative_id == FIPAMessage.Performative.MATCH_ACCEPT: performative = fipa_pb2.FIPAMessage.MatchAccept() # type: ignore fipa_msg.match_accept.CopyFrom(performative) - elif performative_id == FIPAMessage.Performative.ACCEPT_W_ADDRESS: - performative = fipa_pb2.FIPAMessage.AcceptWAddress() # type: ignore - address = msg.get("address") - if type(address) == str: - performative.address = address - fipa_msg.accept_w_address.CopyFrom(performative) - elif performative_id == FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS: - performative = fipa_pb2.FIPAMessage.MatchAcceptWAddress() # type: ignore - address = msg.get("address") - if type(address) == str: - performative.address = address - fipa_msg.match_accept_w_address.CopyFrom(performative) + elif performative_id == FIPAMessage.Performative.ACCEPT_W_INFORM: + performative = fipa_pb2.FIPAMessage.AcceptWInform() # type: ignore + data = msg.info + data_bytes = json.dumps(data).encode("utf-8") + performative.bytes = data_bytes + fipa_msg.accept_w_inform.CopyFrom(performative) + elif performative_id == FIPAMessage.Performative.MATCH_ACCEPT_W_INFORM: + performative = fipa_pb2.FIPAMessage.MatchAcceptWInform() # type: ignore + data = msg.info + data_bytes = json.dumps(data).encode("utf-8") + performative.bytes = data_bytes + fipa_msg.match_accept_w_inform.CopyFrom(performative) elif performative_id == FIPAMessage.Performative.DECLINE: performative = fipa_pb2.FIPAMessage.Decline() # type: ignore fipa_msg.decline.CopyFrom(performative) elif performative_id == FIPAMessage.Performative.INFORM: performative = fipa_pb2.FIPAMessage.Inform() # type: ignore - data = msg.get("json_data") + data = msg.info data_bytes = json.dumps(data).encode("utf-8") performative.bytes = data_bytes fipa_msg.inform.CopyFrom(performative) @@ -128,17 +135,17 @@ def decode(self, obj: bytes) -> Message: pass elif performative_id == FIPAMessage.Performative.MATCH_ACCEPT: pass - elif performative_id == FIPAMessage.Performative.ACCEPT_W_ADDRESS: - address = fipa_pb.accept_w_address.address - performative_content['address'] = address - elif performative_id == FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS: - address = fipa_pb.match_accept_w_address.address - performative_content['address'] = address + elif performative_id == FIPAMessage.Performative.ACCEPT_W_INFORM: + info = json.loads(fipa_pb.accept_w_inform.bytes) + performative_content['info'] = info + elif performative_id == FIPAMessage.Performative.MATCH_ACCEPT_W_INFORM: + info = json.loads(fipa_pb.match_accept_w_inform.bytes) + performative_content['info'] = info elif performative_id == FIPAMessage.Performative.DECLINE: pass elif performative_id == FIPAMessage.Performative.INFORM: - data = json.loads(fipa_pb.inform.bytes) - performative_content["json_data"] = data + info = json.loads(fipa_pb.inform.bytes) + performative_content["info"] = info else: raise ValueError("Performative not valid: {}.".format(performative)) diff --git a/packages/protocols/gym/message.py b/packages/protocols/gym/message.py index 1a1a318d7f..82b5342832 100644 --- a/packages/protocols/gym/message.py +++ b/packages/protocols/gym/message.py @@ -20,7 +20,7 @@ """This module contains the FIPA message definition.""" from enum import Enum -from typing import Optional, Union +from typing import cast, Dict, Any from aea.protocols.base import Message @@ -42,37 +42,73 @@ def __str__(self): """Get string representation.""" return self.value - def __init__(self, performative: Optional[Union[str, Performative]] = None, **kwargs): + def __init__(self, performative: Performative, **kwargs): """ Initialize. - :param type: the type. + :param performative: the performative. """ - super().__init__(performative=GymMessage.Performative(performative), **kwargs) + super().__init__(performative=performative, **kwargs) assert self.check_consistency(), "GymMessage initialization inconsistent." + @property + def performative(self) -> Performative: # noqa: F821 + """Get the performative of the message.""" + assert self.is_set("performative"), "Performative is not set." + return GymMessage.Performative(self.get('performative')) + + @property + def action(self) -> Any: + """Get the action from the message.""" + assert self.is_set("action"), "Action is not set." + return cast(Any, self.get("action")) + + @property + def step_id(self) -> int: + """Get the step id from the message.""" + assert self.is_set("step_id"), "Step_id is not set." + return cast(int, self.get("step_id")) + + @property + def observation(self) -> Any: + """Get the observation from the message.""" + assert self.is_set("observation"), "Observation is not set." + return cast(Any, self.get("observation")) + + @property + def reward(self) -> float: + """Get the reward from the message.""" + assert self.is_set("reward"), "Reward is not set." + return cast(float, self.get("reward")) + + @property + def done(self) -> bool: + """Get the value of the done variable from the message.""" + assert self.is_set("done"), "Done is not set." + return cast(bool, self.get("done")) + + @property + def info(self) -> Dict[str, Any]: + """Get the info from the message.""" + assert self.is_set("info"), "Info is not set." + return cast(Dict[str, Any], self.get("info")) + def check_consistency(self) -> bool: """Check that the data is consistent.""" try: - assert self.is_set("performative") - performative = GymMessage.Performative(self.get("performative")) - if performative == GymMessage.Performative.ACT: - assert self.is_set("action") - assert self.is_set("step_id") - assert type(self.get("step_id")) == int + assert isinstance(self.performative, GymMessage.Performative) + if self.performative == GymMessage.Performative.ACT: + assert self.is_set("action"), "Action is not set." + assert isinstance(self.step_id, int) assert len(self.body) == 3 - elif performative == GymMessage.Performative.PERCEPT: - assert self.is_set("observation") - assert self.is_set("reward") - assert type(self.get("reward")) == float - assert self.is_set("done") - assert type(self.get("done")) == bool - assert self.is_set("info") - assert type(self.get("info")) == dict - assert self.is_set("step_id") - assert type(self.get("step_id")) == int + elif self.performative == GymMessage.Performative.PERCEPT: + assert self.is_set("observation"), "Observation is not set." + assert isinstance(self.reward, float) + assert isinstance(self.done, bool) + assert isinstance(self.info, dict) + assert isinstance(self.step_id, int) assert len(self.body) == 6 - elif performative == GymMessage.Performative.RESET or performative == GymMessage.Performative.CLOSE: + elif self.performative == GymMessage.Performative.RESET or self.performative == GymMessage.Performative.CLOSE: assert len(self.body) == 1 else: raise ValueError("Performative not recognized.") diff --git a/packages/protocols/gym/protocol.yaml b/packages/protocols/gym/protocol.yaml index 7b10d5429b..8d4bbfe5e5 100644 --- a/packages/protocols/gym/protocol.yaml +++ b/packages/protocols/gym/protocol.yaml @@ -1,6 +1,7 @@ name: gym -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" -description: "The gym protocol implements the messages an agent needs to engage with a gym connection." \ No newline at end of file +description: "The gym protocol implements the messages an agent needs to engage with a gym connection." diff --git a/packages/protocols/gym/serialization.py b/packages/protocols/gym/serialization.py index ffcf362856..6226e43383 100644 --- a/packages/protocols/gym/serialization.py +++ b/packages/protocols/gym/serialization.py @@ -24,7 +24,7 @@ import json import pickle import sys -from typing import Any, TYPE_CHECKING +from typing import Any, TYPE_CHECKING, cast from aea.protocols.base import Message from aea.protocols.base import Serializer @@ -45,27 +45,27 @@ def encode(self, msg: Message) -> bytes: :param msg: the message object :return: the bytes """ - performative = GymMessage.Performative(msg.get("performative")) + msg = cast(GymMessage, msg) new_body = copy.copy(msg.body) - new_body["performative"] = performative.value + new_body["performative"] = msg.performative.value - if performative == GymMessage.Performative.ACT: - action = msg.body["action"] # type: Any + if msg.performative == GymMessage.Performative.ACT: + action = msg.action # type: Any action_bytes = base64.b64encode(pickle.dumps(action)).decode("utf-8") new_body["action"] = action_bytes - new_body["step_id"] = msg.body["step_id"] - elif performative == GymMessage.Performative.PERCEPT: + new_body["step_id"] = msg.step_id + elif msg.performative == GymMessage.Performative.PERCEPT: # observation, reward and info are gym implementation specific, done is boolean - observation = msg.body["observation"] # type: Any + observation = msg.observation observation_bytes = base64.b64encode(pickle.dumps(observation)).decode("utf-8") new_body["observation"] = observation_bytes - reward = msg.body["reward"] # type: Any + reward = msg.reward reward_bytes = base64.b64encode(pickle.dumps(reward)).decode("utf-8") new_body["reward"] = reward_bytes - info = msg.body["info"] # type: Any + info = msg.info info_bytes = base64.b64encode(pickle.dumps(info)).decode("utf-8") new_body["info"] = info_bytes - new_body["step_id"] = msg.body["step_id"] + new_body["step_id"] = msg.step_id gym_message_bytes = json.dumps(new_body).encode("utf-8") return gym_message_bytes diff --git a/packages/protocols/ml_trade/__init__.py b/packages/protocols/ml_trade/__init__.py new file mode 100644 index 0000000000..2ff6f15da1 --- /dev/null +++ b/packages/protocols/ml_trade/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the support resources for the ML trade protocol.""" diff --git a/packages/protocols/ml_trade/message.py b/packages/protocols/ml_trade/message.py new file mode 100644 index 0000000000..eb82eb67a7 --- /dev/null +++ b/packages/protocols/ml_trade/message.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the FIPA message definition.""" +from enum import Enum +from typing import cast, Tuple +import numpy as np + +from aea.helpers.search.models import Description, Query +from aea.protocols.base import Message + + +class MLTradeMessage(Message): + """The ML trade message class.""" + + protocol_id = "ml_trade" + + class Performative(Enum): + """ML trade performatives.""" + + CFT = 'cft' + TERMS = 'terms' + ACCEPT = 'accept' + DATA = 'data' + + def __str__(self): + """Get string representation.""" + return self.value + + def __init__(self, performative: Performative, **kwargs): + """ + Initialize. + + :param type: the type. + """ + super().__init__(performative=performative, **kwargs) + assert self.check_consistency(), "MLTradeMessage initialization inconsistent." + + @property + def performative(self) -> Performative: # noqa: F821 + """Get the performative of the message.""" + assert self.is_set("performative"), "performative is not set." + return MLTradeMessage.Performative(self.get("performative")) + + @property + def query(self) -> Query: + """Get the query of the message.""" + assert self.is_set("query"), "query is not set." + return cast(Query, self.get("query")) + + @property + def terms(self) -> Description: + """Get the terms of the message.""" + assert self.is_set("terms"), "Terms are not set." + return cast(Description, self.get("terms")) + + @property + def tx_digest(self) -> str: + """Get the transaction digest from the message.""" + assert self.is_set("tx_digest"), "tx_digest is not set." + return cast(str, self.get("tx_digest")) + + @property + def data(self) -> Tuple[np.ndarray, np.ndarray]: + """Get the data from the message.""" + assert self.is_set("data"), "Data is not set." + return cast(Tuple[np.ndarray, np.ndarray], self.get("data")) + + def check_consistency(self) -> bool: + """Check that the data is consistent.""" + try: + assert isinstance(self.performative, MLTradeMessage.Performative), "Performative is invalid type." + if self.performative == MLTradeMessage.Performative.CFT: + assert isinstance(self.query, Query) + assert len(self.body) == 2 + elif self.performative == MLTradeMessage.Performative.TERMS: + assert isinstance(self.terms, Description) + assert len(self.body) == 2 + elif self.performative == MLTradeMessage.Performative.ACCEPT: + assert isinstance(self.terms, Description) + assert isinstance(self.tx_digest, str) + assert len(self.body) == 3 + elif self.performative == MLTradeMessage.Performative.DATA: + assert isinstance(self.terms, Description) + assert isinstance(self.data, tuple) + assert len(self.data) == 2 + assert isinstance(self.data[0], np.ndarray) + assert isinstance(self.data[1], np.ndarray) + assert self.data[0].shape[0] == self.data[1].shape[0] + assert len(self.body) == 3 + else: + raise ValueError("Performative not recognized.") + + except (AssertionError, ValueError, KeyError): + return False + + return True diff --git a/packages/protocols/ml_trade/protocol.yaml b/packages/protocols/ml_trade/protocol.yaml new file mode 100644 index 0000000000..382e85647b --- /dev/null +++ b/packages/protocols/ml_trade/protocol.yaml @@ -0,0 +1,8 @@ +name: ml_trade +author: fetchai +version: 0.1.0 +license: Apache 2.0 +fingerprint: "" +url: "" +description: The ml trade protocol implements the messages an agent needs to engage + in trading data for training and prediction. diff --git a/packages/protocols/ml_trade/serialization.py b/packages/protocols/ml_trade/serialization.py new file mode 100644 index 0000000000..a81c82201d --- /dev/null +++ b/packages/protocols/ml_trade/serialization.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Serialization for the TAC protocol.""" +import base64 +import json +import pickle +import sys +from typing import TYPE_CHECKING, cast + +from aea.protocols.base import Message +from aea.protocols.base import Serializer + +if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.ml_trade.message import MLTradeMessage +else: + from ml_trade_protocol.message import MLTradeMessage # pragma: no cover + + +class MLTradeSerializer(Serializer): + """Serialization for the ML Trade protocol.""" + + def encode(self, msg: Message) -> bytes: + """Encode a 'ml_trade' message into bytes.""" + body = {} # Dict[str, Any] + msg = cast(MLTradeMessage, msg) + body["performative"] = msg.performative.value + + if msg.performative == MLTradeMessage.Performative.CFT: + query = msg.query + query_bytes = base64.b64encode(pickle.dumps(query)).decode("utf-8") + body["query"] = query_bytes + elif msg.performative == MLTradeMessage.Performative.TERMS: + terms = msg.terms + terms_bytes = base64.b64encode(pickle.dumps(terms)).decode("utf-8") + body["terms"] = terms_bytes + elif msg.performative == MLTradeMessage.Performative.ACCEPT: + # encoding terms + terms = msg.terms + terms_bytes = base64.b64encode(pickle.dumps(terms)).decode("utf-8") + body["terms"] = terms_bytes + # encoding tx_digest + body["tx_digest"] = msg.tx_digest + elif msg.performative == MLTradeMessage.Performative.DATA: + # encoding terms + terms = msg.terms + terms_bytes = base64.b64encode(pickle.dumps(terms)).decode("utf-8") + body["terms"] = terms_bytes + # encoding data + data = msg.data + data_bytes = base64.b64encode(pickle.dumps(data)).decode("utf-8") + body["data"] = data_bytes + else: # pragma: no cover + raise ValueError("Type not recognized.") + + bytes_msg = json.dumps(body).encode("utf-8") + return bytes_msg + + def decode(self, obj: bytes) -> Message: + """Decode bytes into a 'ml_trade' message.""" + json_body = json.loads(obj.decode("utf-8")) + body = {} + + msg_type = MLTradeMessage.Performative(json_body["performative"]) + body["performative"] = msg_type + if msg_type == MLTradeMessage.Performative.CFT: + query_bytes = base64.b64decode(json_body["query"]) + query = pickle.loads(query_bytes) + body["query"] = query + elif msg_type == MLTradeMessage.Performative.TERMS: + terms_bytes = base64.b64decode(json_body["terms"]) + terms = pickle.loads(terms_bytes) + body["terms"] = terms + elif msg_type == MLTradeMessage.Performative.ACCEPT: + terms_bytes = base64.b64decode(json_body["terms"]) + terms = pickle.loads(terms_bytes) + body["terms"] = terms + body["tx_digest"] = json_body["tx_digest"] + elif msg_type == MLTradeMessage.Performative.DATA: + # encoding terms + terms_bytes = base64.b64decode(json_body["terms"]) + terms = pickle.loads(terms_bytes) + body["terms"] = terms + # encoding data + data_bytes = base64.b64decode(json_body["data"]) + data = pickle.loads(data_bytes) + body["data"] = data + else: # pragma: no cover + raise ValueError("Type not recognized.") + + return MLTradeMessage(performative=msg_type, body=body) diff --git a/aea/protocols/oef/__init__.py b/packages/protocols/oef/__init__.py similarity index 100% rename from aea/protocols/oef/__init__.py rename to packages/protocols/oef/__init__.py diff --git a/packages/protocols/oef/message.py b/packages/protocols/oef/message.py new file mode 100644 index 0000000000..ade4974be4 --- /dev/null +++ b/packages/protocols/oef/message.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the default message definition.""" +from enum import Enum +from typing import List, cast + +from aea.helpers.search.models import Description, Query +from aea.protocols.base import Message + + +class OEFMessage(Message): + """The OEF message class.""" + + protocol_id = "oef" + + class Type(Enum): + """OEF Message types.""" + + REGISTER_SERVICE = "register_service" + REGISTER_AGENT = "register_agent" + UNREGISTER_SERVICE = "unregister_service" + UNREGISTER_AGENT = "unregister_agent" + SEARCH_SERVICES = "search_services" + SEARCH_AGENTS = "search_agents" + OEF_ERROR = "oef_error" + DIALOGUE_ERROR = "dialogue_error" + SEARCH_RESULT = "search_result" + + def __str__(self): + """Get string representation.""" + return self.value + + class OEFErrorOperation(Enum): + """Operation code for the OEF. It is returned in the OEF Error messages.""" + + REGISTER_SERVICE = 0 + UNREGISTER_SERVICE = 1 + SEARCH_SERVICES = 2 + SEARCH_SERVICES_WIDE = 3 + SEARCH_AGENTS = 4 + SEND_MESSAGE = 5 + REGISTER_AGENT = 6 + UNREGISTER_AGENT = 7 + + OTHER = 10000 + + def __str__(self): + """Get string representation.""" + return str(self.value) + + def __init__(self, type: Type, + id: int, + **kwargs): + """ + Initialize. + + :param type: the type of OEF message. + :param id: the message id. + """ + super().__init__(type=type, id=id, **kwargs) + assert self.check_consistency(), "OEFMessage initialization inconsistent." + + @property + def type(self) -> Type: # noqa: F821 + """Get the type of the oef_message.""" + assert self.is_set("type"), "type is not set." + return OEFMessage.Type(self.get("type")) + + @property + def id(self) -> int: + """Get the id of the oef_message.""" + assert self.is_set("id"), "id is not set." + return cast(int, self.get('id')) + + @property + def service_description(self) -> Description: + """Get the service_description from the message.""" + assert self.is_set("service_description"), "service_description is not set" + return cast(Description, self.get('service_description')) + + @property + def service_id(self) -> str: + """Get the service_id from the message.""" + assert self.is_set("service_id"), "service_id is not set." + return cast(str, self.get("service_id")) + + @property + def agent_description(self) -> Description: + """Get the agent_description from the message.""" + assert self.is_set("agent_description"), "agent_description is not set." + return cast(Description, self.get("agent_description")) + + @property + def agent_id(self) -> str: + """Get the agent_id from the message.""" + assert self.is_set("agent_id"), "agent_id is not set." + return cast(str, self.get("agent_id")) + + @property + def query(self) -> Query: + """Get the query from the message.""" + assert self.is_set("query"), "query is not set." + return cast(Query, self.get("query")) + + @property + def agents(self) -> List[str]: + """Get the agents from the message.""" + assert self.is_set("agents"), "list of agents is not set." + return cast(List[str], self.get("agents")) + + @property + def operation(self) -> OEFErrorOperation: # noqa: F821 + """Get the error_operation code from the message.""" + assert self.is_set("operation"), "operation is not set." + return OEFMessage.OEFErrorOperation(self.get("operation")) + + @property + def dialogue_id(self) -> int: + """Get the dialogue_id from the message.""" + assert self.is_set("dialogue_id"), "dialogue_id is not set." + return cast(int, self.get("dialogue_id")) + + @property + def origin(self) -> str: + """Get the origin from the message.""" + assert self.is_set("origin"), "origin is not set." + return cast(str, self.get("origin")) + + def check_consistency(self) -> bool: + """Check that the data is consistent.""" + try: + assert isinstance(self.type, OEFMessage.Type), "type not of correct type." + assert isinstance(self.id, int), "id must be int." + if self.type == OEFMessage.Type.REGISTER_SERVICE: + assert isinstance(self.service_description, Description), \ + "service_description must be of type Description." + assert isinstance(self.service_id, str), "service_id must be of type str." + assert len(self.body) == 4 + elif self.type == OEFMessage.Type.REGISTER_AGENT: + assert isinstance(self.agent_description, Description), "agent_description must be of type Description." + assert isinstance(self.agent_id, str), "agent_id must be of type str." + assert len(self.body) == 4 + elif self.type == OEFMessage.Type.UNREGISTER_SERVICE: + assert isinstance(self.service_description, Description), \ + "service_description must be of type Description." + assert isinstance(self.service_id, str), "service_id must be of type str." + assert len(self.body) == 4 + elif self.type == OEFMessage.Type.UNREGISTER_AGENT: + assert isinstance(self.agent_description, Description), "agent_description must be of type Description." + assert isinstance(self.agent_id, str), "agent_id must be of type str." + assert len(self.body) == 4 + elif self.type == OEFMessage.Type.SEARCH_SERVICES or self.type == OEFMessage.Type.SEARCH_AGENTS: + assert isinstance(self.query, Query), "query must be of type Query." + assert len(self.body) == 3 + elif self.type == OEFMessage.Type.SEARCH_RESULT: + assert type(self.agents) == list and all(type(a) == str for a in self.agents) + assert len(self.body) == 3 + elif self.type == OEFMessage.Type.OEF_ERROR: + assert isinstance(self.operation, OEFMessage.OEFErrorOperation), \ + "operation must be type of OEFErrorOperation" + assert len(self.body) == 3 + elif self.type == OEFMessage.Type.DIALOGUE_ERROR: + assert isinstance(self.dialogue_id, int), "dialogue_id must be of type int." + assert isinstance(self.origin, str), "origin must be of type str." + assert len(self.body) == 4 + else: + raise ValueError("Type not recognized.") + except (AssertionError, ValueError): + return False + + return True diff --git a/aea/protocols/oef/protocol.yaml b/packages/protocols/oef/protocol.yaml similarity index 70% rename from aea/protocols/oef/protocol.yaml rename to packages/protocols/oef/protocol.yaml index 83130abea0..afb2d2a60b 100644 --- a/aea/protocols/oef/protocol.yaml +++ b/packages/protocols/oef/protocol.yaml @@ -1,9 +1,10 @@ name: 'oef' -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" dependencies: - - colorlog - - oef + colorlog: {} + oef: {} description: "The oef protocol implements the OEF specific messages." diff --git a/aea/protocols/oef/serialization.py b/packages/protocols/oef/serialization.py similarity index 78% rename from aea/protocols/oef/serialization.py rename to packages/protocols/oef/serialization.py index ba2bc4754d..6698042ad0 100644 --- a/aea/protocols/oef/serialization.py +++ b/packages/protocols/oef/serialization.py @@ -19,15 +19,21 @@ # ------------------------------------------------------------------------------ """Serialization for the FIPA protocol.""" +from typing import cast, TYPE_CHECKING + import base64 import copy import json import pickle +import sys from aea.protocols.base import Message from aea.protocols.base import Serializer -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.models import Description, Query + +if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.oef.message import OEFMessage +else: + from oef_protocol.message import OEFMessage # pragma: no cover """default 'to' field for OEF envelopes.""" DEFAULT_OEF = "oef" @@ -43,29 +49,29 @@ def encode(self, msg: Message) -> bytes: :param msg: the message object :return: the bytes """ - oef_type = OEFMessage.Type(msg.get("type")) + msg = cast(OEFMessage, msg) new_body = copy.copy(msg.body) - new_body["type"] = oef_type.value + new_body["type"] = msg.type.value + new_body['id'] = msg.id - if oef_type in {OEFMessage.Type.REGISTER_SERVICE, OEFMessage.Type.UNREGISTER_SERVICE}: - service_description = msg.body["service_description"] # type: Description + if msg.type in {OEFMessage.Type.REGISTER_SERVICE, OEFMessage.Type.UNREGISTER_SERVICE}: + service_description = msg.service_description service_description_bytes = base64.b64encode(pickle.dumps(service_description)).decode("utf-8") new_body["service_description"] = service_description_bytes - elif oef_type in {OEFMessage.Type.REGISTER_AGENT, OEFMessage.Type.UNREGISTER_AGENT}: - agent_description = msg.body["agent_description"] # type: Description + elif msg.type in {OEFMessage.Type.REGISTER_AGENT, OEFMessage.Type.UNREGISTER_AGENT}: + agent_description = msg.agent_description agent_description_bytes = base64.b64encode(pickle.dumps(agent_description)).decode("utf-8") new_body["agent_description"] = agent_description_bytes - elif oef_type in {OEFMessage.Type.SEARCH_SERVICES, OEFMessage.Type.SEARCH_AGENTS}: - query = msg.body["query"] # type: Query + elif msg.type in {OEFMessage.Type.SEARCH_SERVICES, OEFMessage.Type.SEARCH_AGENTS}: + query = msg.query query_bytes = base64.b64encode(pickle.dumps(query)).decode("utf-8") new_body["query"] = query_bytes - elif oef_type in {OEFMessage.Type.SEARCH_RESULT}: + elif msg.type in {OEFMessage.Type.SEARCH_RESULT}: # we need this cast because the "agents" field might contains # the Protobuf type "RepeatedScalarContainer", which is not JSON serializable. - new_body["agents"] = list(msg.body["agents"]) - elif oef_type in {OEFMessage.Type.OEF_ERROR}: - operation = msg.body["operation"] - new_body["operation"] = str(operation) + new_body["agents"] = msg.agents + elif msg.type in {OEFMessage.Type.OEF_ERROR}: + new_body["operation"] = msg.operation.value oef_message_bytes = json.dumps(new_body).encode("utf-8") return oef_message_bytes @@ -79,8 +85,8 @@ def decode(self, obj: bytes) -> Message: """ json_msg = json.loads(obj.decode("utf-8")) oef_type = OEFMessage.Type(json_msg["type"]) + oef_id = json_msg['id'] new_body = copy.copy(json_msg) - new_body["type"] = oef_type if oef_type in {OEFMessage.Type.REGISTER_SERVICE, OEFMessage.Type.UNREGISTER_SERVICE}: service_description_bytes = base64.b64decode(json_msg["service_description"]) @@ -100,5 +106,5 @@ def decode(self, obj: bytes) -> Message: operation = json_msg["operation"] new_body["operation"] = OEFMessage.OEFErrorOperation(int(operation)) - oef_message = OEFMessage(oef_type=oef_type, body=new_body) + oef_message = OEFMessage(type=oef_type, id=oef_id, body=new_body) return oef_message diff --git a/packages/protocols/tac/message.py b/packages/protocols/tac/message.py index 9ba49fb457..7d79c0bad8 100644 --- a/packages/protocols/tac/message.py +++ b/packages/protocols/tac/message.py @@ -20,9 +20,10 @@ """This module contains the default message definition.""" from enum import Enum -from typing import Dict, Optional, cast +from typing import Dict, cast, Any from collections import defaultdict +from aea.mail.base import Address from aea.protocols.base import Message @@ -53,7 +54,7 @@ class ErrorCode(Enum): GENERIC_ERROR = 0 REQUEST_NOT_VALID = 1 - AGENT_PBK_ALREADY_REGISTERED = 2 + AGENT_ADDR_ALREADY_REGISTERED = 2 AGENT_NAME_ALREADY_REGISTERED = 3 AGENT_NOT_REGISTERED = 4 TRANSACTION_NOT_VALID = 5 @@ -65,7 +66,7 @@ class ErrorCode(Enum): _from_ec_to_msg = { ErrorCode.GENERIC_ERROR: "Unexpected error.", ErrorCode.REQUEST_NOT_VALID: "Request not recognized", - ErrorCode.AGENT_PBK_ALREADY_REGISTERED: "Agent pbk already registered.", + ErrorCode.AGENT_ADDR_ALREADY_REGISTERED: "Agent addr already registered.", ErrorCode.AGENT_NAME_ALREADY_REGISTERED: "Agent name already registered.", ErrorCode.AGENT_NOT_REGISTERED: "Agent not registered.", ErrorCode.TRANSACTION_NOT_VALID: "Error in checking transaction", @@ -75,107 +76,214 @@ class ErrorCode(Enum): ErrorCode.DIALOGUE_INCONSISTENT: "The message is inconsistent with the dialogue." } # type: Dict[ErrorCode, str] - def __init__(self, tac_type: Optional[Type] = None, + def __init__(self, type: Type, **kwargs): """ Initialize. :param tac_type: the type of TAC message. """ - super().__init__(type=tac_type, **kwargs) + super().__init__(type=type, **kwargs) assert self.check_consistency(), "TACMessage initialization inconsistent." + @property + def type(self) -> Type: # noqa: F821 + """Get the type for the message.""" + assert self.is_set("type"), "type is not set" + return TACMessage.Type(self.get("type")) + + @property + def agent_name(self) -> str: + """Get the agent name from the message.""" + assert self.is_set("agent_name"), "Agent name is not set." + return cast(str, self.get("agent_name")) + + @property + def tx_id(self) -> str: + """Get the transaction id from the message.""" + assert self.is_set("tx_id"), "Transaction id is not set." + return cast(str, self.get("tx_id")) + + @property + def tx_sender_addr(self) -> str: + """Get the sender of the transaction.""" + assert self.is_set("tx_sender_addr"), "Tx_sender_addr is not set." + return cast(str, self.get("tx_sender_addr")) + + @property + def tx_counterparty_addr(self) -> str: + """Get the counterparty of the transaction.""" + assert self.is_set("tx_counterparty_addr"), "Tx_counterparty_addr is not set." + return cast(str, self.get("tx_counterparty_addr")) + + @property + def amount_by_currency_id(self) -> Dict[str, int]: + """Get the amount for each currency.""" + assert self.is_set("amount_by_currency_id"), "Amount by currency is not set." + return cast(Dict[str, int], self.get('amount_by_currency_id')) + + @property + def tx_sender_fee(self) -> int: + """Get the transaction fee for the sender.""" + assert self.is_set("tx_sender_fee"), "Tx_sender_fee is not set." + return cast(int, self.get("tx_sender_fee")) + + @property + def tx_counterparty_fee(self) -> int: + """Get the transaction fee for the counterparty.""" + assert self.is_set("tx_counterparty_fee"), "Tx_counterparty_fee is not set." + return cast(int, self.get("tx_counterparty_fee")) + + @property + def quantities_by_good_id(self) -> Dict[str, int]: + """Get the quantities of the good ids from the message.""" + assert self.is_set('quantities_by_good_id') + return cast(Dict[str, int], self.get("quantities_by_good_id")) + + @property + def exchange_params_by_currency_id(self) -> Dict[str, float]: + """Get the exchange parameters for each currency.""" + assert self.is_set("exchange_params_by_currency_id"), "exchange_params_by_currency_id is not set." + return cast(Dict[str, float], self.get("exchange_params_by_currency_id")) + + @property + def utility_params_by_good_id(self) -> Dict[str, float]: + """Get the utility parameters for each good.""" + assert self.is_set("utility_params_by_good_id"), "utility_params_by_good_id is not set." + return cast(Dict[str, float], self.get("utility_params_by_good_id")) + + @property + def tx_fee(self) -> int: + """Get the transaction fee.""" + assert self.is_set("tx_fee"), "tx_fee is not set." + return cast(int, self.get("tx_fee")) + + @property + def agent_addr_to_name(self) -> Dict[Address, str]: + """Get the mapping from agent address to name.""" + assert self.is_set("agent_addr_to_name"), "agent_id_to_name is not set." + return cast(Dict[Address, str], self.get("agent_addr_to_name")) + + @property + def good_id_to_name(self) -> Dict[str, str]: + """Get mapping from good id to name.""" + assert self.is_set("good_id_to_name"), "good_id_to_name is not set." + return cast(Dict[str, str], self.get("good_id_to_name")) + + @property + def version_id(self) -> str: + """Get the version id.""" + assert self.is_set("version_id"), "version_id is not set." + return cast(str, self.get("version_id")) + + @property + def error_code(self) -> ErrorCode: # noqa: F821 + """Get the error code.""" + assert self.is_set("error_code"), "error_code is not set." + return TACMessage.ErrorCode(self.get("error_code")) + + @property + def info(self) -> Dict[str, Any]: + """Get the info dictionary.""" + assert self.is_set("info"), "info is not set." + return cast(Dict[str, Any], self.get("info")) + + @property + def tx_nonce(self) -> int: + """Get the nonce of the transaction.""" + assert self.is_set("tx_nonce"), "Tx_nonce is not set." + return cast(int, self.get("tx_nonce")) + + @property + def tx_sender_signature(self) -> bytes: + """Get the transaction signature for the sender.""" + assert self.is_set("tx_sender_signature"), "Tx_sender_signature is not set." + return cast(bytes, self.get("tx_sender_signature")) + + @property + def tx_counterparty_signature(self) -> bytes: + """Get the transaction signature for the counterparty.""" + assert self.is_set("tx_counterparty_signature"), "Tx_counterparty_fee is not set." + return cast(bytes, self.get("tx_counterparty_signature")) + def check_consistency(self) -> bool: """Check that the data is consistent.""" try: - assert self.is_set("type") - tac_type = TACMessage.Type(self.get("type")) - if tac_type == TACMessage.Type.REGISTER: - assert self.is_set("agent_name") + assert isinstance(self.type, TACMessage.Type), "Type is not valid type." + if self.type == TACMessage.Type.REGISTER: + assert isinstance(self.agent_name, str) assert len(self.body) == 2 - elif tac_type == TACMessage.Type.UNREGISTER: + elif self.type == TACMessage.Type.UNREGISTER: assert len(self.body) == 1 - elif tac_type == TACMessage.Type.TRANSACTION: - assert self.is_set("transaction_id") - assert type(self.get("transaction_id")) == str - assert self.is_set("counterparty") - assert type(self.get("counterparty")) == str - assert self.is_set("amount_by_currency") - amount_by_currency = cast(Dict, self.get("amount_by_currency")) - for key, value in amount_by_currency.items(): - assert type(key) == str and type(value) == int - assert len(amount_by_currency.keys()) == len(set(amount_by_currency.keys())) - assert self.is_set("sender_tx_fee") - sender_tx_fee = self.get("sender_tx_fee") - assert type(sender_tx_fee) == int - assert cast(int, sender_tx_fee) >= 0 - assert self.is_set("counterparty_tx_fee") - counterparty_tx_fee = self.get("counterparty_tx_fee") - assert type(counterparty_tx_fee) == int - assert cast(int, counterparty_tx_fee) >= 0 - assert self.is_set("quantities_by_good_pbk") - quantities_by_good_pbk = cast(Dict, self.get("quantities_by_good_pbk")) - for key, value in quantities_by_good_pbk.items(): - assert type(key) == str and type(value) == int - assert len(quantities_by_good_pbk.keys()) == len(set(quantities_by_good_pbk.keys())) - assert len(self.body) == 7 - elif tac_type == TACMessage.Type.GET_STATE_UPDATE: + elif self.type == TACMessage.Type.TRANSACTION: + assert isinstance(self.tx_id, str) + assert isinstance(self.tx_sender_addr, str) + assert isinstance(self.tx_counterparty_addr, str) + assert isinstance(self.amount_by_currency_id, dict) + for key, int_value in self.amount_by_currency_id.items(): + assert type(key) == str and type(int_value) == int + assert len(self.amount_by_currency_id.keys()) == len(set(self.amount_by_currency_id.keys())) + assert isinstance(self.tx_sender_fee, int) + assert self.tx_sender_fee >= 0 + assert isinstance(self.tx_counterparty_fee, int) + assert self.tx_counterparty_fee >= 0 + assert isinstance(self.quantities_by_good_id, dict) + for key, int_value in self.quantities_by_good_id.items(): + assert type(key) == str and type(int_value) == int + assert len(self.quantities_by_good_id.keys()) == len(set(self.quantities_by_good_id.keys())) + assert isinstance(self.tx_nonce, int) + assert isinstance(self.tx_sender_signature, bytes) + assert isinstance(self.tx_counterparty_signature, bytes) + assert len(self.body) == 11 + elif self.type == TACMessage.Type.GET_STATE_UPDATE: assert len(self.body) == 1 - elif tac_type == TACMessage.Type.CANCELLED: + elif self.type == TACMessage.Type.CANCELLED: assert len(self.body) == 1 - elif tac_type == TACMessage.Type.GAME_DATA: - assert self.is_set("amount_by_currency") - amount_by_currency = cast(Dict, self.get("amount_by_currency")) - for key, value in amount_by_currency.items(): - assert type(key) == str and type(value) == int - assert self.is_set("exchange_params_by_currency") - exchange_params_by_currency = cast(Dict, self.get("exchange_params_by_currency")) - for key, value in exchange_params_by_currency.items(): - assert type(key) == str and type(value) == float - assert amount_by_currency.keys() == exchange_params_by_currency.keys() - assert self.is_set("quantities_by_good_pbk") - quantities_by_good_pbk = cast(Dict, self.get("quantities_by_good_pbk")) - for key, value in quantities_by_good_pbk.items(): - assert type(key) == str and type(value) == int - assert self.is_set("utility_params_by_good_pbk") - utility_params_by_good_pbk = cast(Dict, self.get("utility_params_by_good_pbk")) - for key, value in utility_params_by_good_pbk.items(): - assert type(key) == str and type(value) == float - assert quantities_by_good_pbk.keys() == utility_params_by_good_pbk.keys() - assert self.is_set("tx_fee") - assert type(self.get("tx_fee")) == int - assert self.is_set("agent_pbk_to_name") - assert type(self.get("agent_pbk_to_name")) in [dict, defaultdict] - assert self.is_set("good_pbk_to_name") - assert type(self.get("good_pbk_to_name")) in [dict, defaultdict] - assert self.is_set("version_id") - assert type(self.get("version_id")) == str + elif self.type == TACMessage.Type.GAME_DATA: + assert isinstance(self.amount_by_currency_id, dict) + for key, int_value in self.amount_by_currency_id.items(): + assert type(key) == str and type(int_value) == int + assert isinstance(self.exchange_params_by_currency_id, dict) + for key, float_value in self.exchange_params_by_currency_id.items(): + assert type(key) == str and type(float_value) == float + assert self.amount_by_currency_id.keys() == self.exchange_params_by_currency_id.keys() + assert isinstance(self.quantities_by_good_id, dict) + for key, int_value in self.quantities_by_good_id.items(): + assert type(key) == str and type(int_value) == int + assert isinstance(self.utility_params_by_good_id, dict) + for key, float_value in self.utility_params_by_good_id.items(): + assert type(key) == str and type(float_value) == float + assert self.quantities_by_good_id.keys() == self.utility_params_by_good_id.keys() + assert isinstance(self.tx_fee, int) + assert type(self.agent_addr_to_name) in [dict, defaultdict] + assert type(self.good_id_to_name) in [dict, defaultdict] + assert isinstance(self.version_id, str) assert len(self.body) == 9 - elif tac_type == TACMessage.Type.TRANSACTION_CONFIRMATION: - assert self.is_set("transaction_id") - assert type(self.get("transaction_id")) == str - assert self.is_set("amount_by_currency") - amount_by_currency = cast(Dict, self.get("amount_by_currency")) - for key, value in amount_by_currency.items(): - assert type(key) == str and type(value) == int - assert len(amount_by_currency.keys()) == len(set(amount_by_currency.keys())) - assert self.is_set("quantities_by_good_pbk") - quantities_by_good_pbk = cast(Dict, self.get("quantities_by_good_pbk")) - for key, value in quantities_by_good_pbk.items(): - assert type(key) == str and type(value) == int - assert len(quantities_by_good_pbk.keys()) == len(set(quantities_by_good_pbk.keys())) + elif self.type == TACMessage.Type.TRANSACTION_CONFIRMATION: + assert isinstance(self.tx_id, str) + assert isinstance(self.amount_by_currency_id, dict) + for key, int_value in self.amount_by_currency_id.items(): + assert type(key) == str and type(int_value) == int + assert len(self.amount_by_currency_id.keys()) == len(set(self.amount_by_currency_id.keys())) + assert isinstance(self.quantities_by_good_id, dict) + for key, int_value in self.quantities_by_good_id.items(): + assert type(key) == str and type(int_value) == int + assert len(self.quantities_by_good_id.keys()) == len(set(self.quantities_by_good_id.keys())) assert len(self.body) == 4 # elif tac_type == TACMessage.Type.STATE_UPDATE: # assert self.is_set("game_data") # assert self.is_set("transactions") # assert len(self.body) == 3 - elif tac_type == TACMessage.Type.TAC_ERROR: - assert self.is_set("error_code") - error_code = self.get("error_code") - assert error_code in set(self.ErrorCode) - assert len(self.body) == 2 + elif self.type == TACMessage.Type.TAC_ERROR: + assert self.error_code in TACMessage.ErrorCode + if self.is_set("info"): + isinstance(self.info, dict) + assert len(self.body) == 3 + else: + assert len(self.body) == 2 else: raise ValueError("Type not recognized.") + except (AssertionError, ValueError): # pragma: no cover return False diff --git a/packages/protocols/tac/protocol.yaml b/packages/protocols/tac/protocol.yaml index 435e33b383..b4a07d56a2 100644 --- a/packages/protocols/tac/protocol.yaml +++ b/packages/protocols/tac/protocol.yaml @@ -1,6 +1,7 @@ name: tac -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" -description: "The tac protocol implements the messages an AEA needs to participate in the TAC." \ No newline at end of file +description: "The tac protocol implements the messages an AEA needs to participate in the TAC." diff --git a/packages/protocols/tac/serialization.py b/packages/protocols/tac/serialization.py index b2f3a271aa..666162f3c0 100644 --- a/packages/protocols/tac/serialization.py +++ b/packages/protocols/tac/serialization.py @@ -21,10 +21,9 @@ """Serialization for the TAC protocol.""" import sys -from typing import Any, Dict, TYPE_CHECKING +from typing import Any, Dict, TYPE_CHECKING, cast -from aea.protocols.base import Message -from aea.protocols.base import Serializer +from aea.protocols.base import Message, Serializer if TYPE_CHECKING or "pytest" in sys.modules: from packages.protocols.tac import tac_pb2 @@ -73,60 +72,64 @@ def encode(self, msg: Message) -> bytes: :param msg: the message object :return: the bytes """ - tac_type = TACMessage.Type(msg.get("type")) + msg = cast(TACMessage, msg) tac_container = tac_pb2.TACMessage() - if tac_type == TACMessage.Type.REGISTER: - agent_name = msg.get("agent_name") + if msg.type == TACMessage.Type.REGISTER: + agent_name = msg.agent_name tac_msg = tac_pb2.TACAgent.Register() # type: ignore tac_msg.agent_name = agent_name tac_container.register.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.UNREGISTER: + elif msg.type == TACMessage.Type.UNREGISTER: tac_msg = tac_pb2.TACAgent.Unregister() # type: ignore tac_container.unregister.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.TRANSACTION: + elif msg.type == TACMessage.Type.TRANSACTION: tac_msg = tac_pb2.TACAgent.Transaction() # type: ignore - tac_msg.transaction_id = msg.get("transaction_id") - tac_msg.counterparty = msg.get("counterparty") - tac_msg.amount_by_currency.extend(_from_dict_to_pairs(msg.get("amount_by_currency"))) - tac_msg.sender_tx_fee = msg.get("sender_tx_fee") - tac_msg.counterparty_tx_fee = msg.get("counterparty_tx_fee") - tac_msg.quantities_by_good_pbk.extend(_from_dict_to_pairs(msg.get("quantities_by_good_pbk"))) + tac_msg.tx_id = msg.tx_id + tac_msg.tx_sender_addr = msg.tx_sender_addr + tac_msg.tx_counterparty_addr = msg.tx_counterparty_addr + tac_msg.amount_by_currency_id.extend(_from_dict_to_pairs(msg.amount_by_currency_id)) + tac_msg.tx_sender_fee = msg.tx_sender_fee + tac_msg.tx_counterparty_fee = msg.tx_counterparty_fee + tac_msg.quantities_by_good_id.extend(_from_dict_to_pairs(msg.quantities_by_good_id)) + tac_msg.tx_nonce = msg.tx_nonce + tac_msg.tx_sender_signature = msg.tx_sender_signature + tac_msg.tx_counterparty_signature = msg.tx_counterparty_signature tac_container.transaction.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.GET_STATE_UPDATE: + elif msg.type == TACMessage.Type.GET_STATE_UPDATE: tac_msg = tac_pb2.TACAgent.GetStateUpdate() # type: ignore tac_container.get_state_update.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.CANCELLED: + elif msg.type == TACMessage.Type.CANCELLED: tac_msg = tac_pb2.TACController.Cancelled() # type: ignore tac_container.cancelled.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.GAME_DATA: + elif msg.type == TACMessage.Type.GAME_DATA: tac_msg = tac_pb2.TACController.GameData() # type: ignore - tac_msg.amount_by_currency.extend(_from_dict_to_pairs(msg.get("amount_by_currency"))) - tac_msg.exchange_params_by_currency.extend(_from_dict_to_pairs(msg.get("exchange_params_by_currency"))) - tac_msg.quantities_by_good_pbk.extend(_from_dict_to_pairs(msg.get("quantities_by_good_pbk"))) - tac_msg.utility_params_by_good_pbk.extend(_from_dict_to_pairs(msg.get("utility_params_by_good_pbk"))) - tac_msg.tx_fee = msg.get("tx_fee") - tac_msg.agent_pbk_to_name.extend(_from_dict_to_pairs(msg.get("agent_pbk_to_name"))) - tac_msg.good_pbk_to_name.extend(_from_dict_to_pairs(msg.get("good_pbk_to_name"))) - tac_msg.version_id = msg.get("version_id") + tac_msg.amount_by_currency_id.extend(_from_dict_to_pairs(msg.amount_by_currency_id)) + tac_msg.exchange_params_by_currency_id.extend(_from_dict_to_pairs(msg.exchange_params_by_currency_id)) + tac_msg.quantities_by_good_id.extend(_from_dict_to_pairs(msg.quantities_by_good_id)) + tac_msg.utility_params_by_good_id.extend(_from_dict_to_pairs(msg.utility_params_by_good_id)) + tac_msg.tx_fee = msg.tx_fee + tac_msg.agent_addr_to_name.extend(_from_dict_to_pairs(msg.agent_addr_to_name)) + tac_msg.good_id_to_name.extend(_from_dict_to_pairs(msg.good_id_to_name)) + tac_msg.version_id = msg.version_id tac_container.game_data.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.TRANSACTION_CONFIRMATION: + elif msg.type == TACMessage.Type.TRANSACTION_CONFIRMATION: tac_msg = tac_pb2.TACController.TransactionConfirmation() # type: ignore - tac_msg.transaction_id = msg.get("transaction_id") - tac_msg.amount_by_currency.extend(_from_dict_to_pairs(msg.get("amount_by_currency"))) - tac_msg.quantities_by_good_pbk.extend(_from_dict_to_pairs(msg.get("quantities_by_good_pbk"))) + tac_msg.tx_id = msg.tx_id + tac_msg.amount_by_currency_id.extend(_from_dict_to_pairs(msg.amount_by_currency_id)) + tac_msg.quantities_by_good_id.extend(_from_dict_to_pairs(msg.quantities_by_good_id)) tac_container.transaction_confirmation.CopyFrom(tac_msg) # elif tac_type == TACMessage.Type.STATE_UPDATE: # tac_msg = tac_pb2.TACController.StateUpdate() # type: ignore # game_data_json = msg.get("game_data") # game_data = tac_pb2.TACController.GameData() # type: ignore - # game_data.amount_by_currency.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["amount_by_currency"]))) # type: ignore - # game_data.exchange_params_by_currency.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["exchange_params_by_currency"]))) # type: ignore - # game_data.quantities_by_good_pbk.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["quantities_by_good_pbk"]))) # type: ignore - # game_data.utility_params_by_good_pbk.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["utility_params_by_good_pbk"]))) # type: ignore + # game_data.amount_by_currency_id.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["amount_by_currency_id"]))) # type: ignore + # game_data.exchange_params_by_currency_id.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["exchange_params_by_currency_id"]))) # type: ignore + # game_data.quantities_by_good_id.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["quantities_by_good_id"]))) # type: ignore + # game_data.utility_params_by_good_id.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["utility_params_by_good_id"]))) # type: ignore # game_data.tx_fee = game_data_json["tx_fee"] # type: ignore - # game_data.agent_pbk_to_name.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["agent_pbk_to_name"]))) # type: ignore - # game_data.good_pbk_to_name.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["good_pbk_to_name"]))) # type: ignore + # game_data.agent_addr_to_name.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["agent_addr_to_name"]))) # type: ignore + # game_data.good_id_to_name.extend(_from_dict_to_pairs(cast(Dict[str, str], game_data_json["good_id_to_name"]))) # type: ignore # tac_msg.initial_state.CopyFrom(game_data) @@ -136,24 +139,21 @@ def encode(self, msg: Message) -> bytes: # tx = tac_pb2.TACAgent.Transaction() # type: ignore # tx.transaction_id = t.get("transaction_id") # tx.counterparty = t.get("counterparty") - # tx.amount_by_currency.extend(_from_dict_to_pairs(t.get("amount_by_currency"))) + # tx.amount_by_currency_id.extend(_from_dict_to_pairs(t.get("amount_by_currency_id"))) # tx.sender_tx_fee = t.get("sender_tx_fee") # tx.counterparty_tx_fee = t.get("counterparty_tx_fee") - # tx.quantities_by_good_pbk.extend(_from_dict_to_pairs(t.get("quantities_by_good_pbk"))) + # tx.quantities_by_good_id.extend(_from_dict_to_pairs(t.get("quantities_by_good_id"))) # transactions.append(tx) # tac_msg.txs.extend(transactions) # tac_container.state_update.CopyFrom(tac_msg) - elif tac_type == TACMessage.Type.TAC_ERROR: + elif msg.type == TACMessage.Type.TAC_ERROR: tac_msg = tac_pb2.TACController.Error() # type: ignore - tac_msg.error_code = TACMessage.ErrorCode(msg.get("error_code")).value - if msg.is_set("error_msg"): - tac_msg.error_msg = msg.get("error_msg") - if msg.is_set("details"): - tac_msg.details.update(msg.get("details")) - + tac_msg.error_code = msg.error_code.value + if msg.is_set("info"): + tac_msg.info.extend(_from_dict_to_pairs(msg.info)) tac_container.error.CopyFrom(tac_msg) - else: - raise ValueError("Type not recognized: {}.".format(tac_type)) + else: # pragma: no cover + raise ValueError("Type not recognized: {}.".format(msg.type)) tac_message_bytes = tac_container.SerializeToString() return tac_message_bytes @@ -178,41 +178,45 @@ def decode(self, obj: bytes) -> Message: new_body["type"] = TACMessage.Type.UNREGISTER elif tac_type == "transaction": new_body["type"] = TACMessage.Type.TRANSACTION - new_body["transaction_id"] = tac_container.transaction.transaction_id - new_body["counterparty"] = tac_container.transaction.counterparty - new_body["amount_by_currency"] = _from_pairs_to_dict(tac_container.transaction.amount_by_currency) - new_body["sender_tx_fee"] = tac_container.transaction.sender_tx_fee - new_body["counterparty_tx_fee"] = tac_container.transaction.counterparty_tx_fee - new_body["quantities_by_good_pbk"] = _from_pairs_to_dict(tac_container.transaction.quantities_by_good_pbk) + new_body["tx_id"] = tac_container.transaction.tx_id + new_body["tx_sender_addr"] = tac_container.transaction.tx_sender_addr + new_body["tx_counterparty_addr"] = tac_container.transaction.tx_counterparty_addr + new_body["amount_by_currency_id"] = _from_pairs_to_dict(tac_container.transaction.amount_by_currency_id) + new_body["tx_sender_fee"] = tac_container.transaction.tx_sender_fee + new_body["tx_counterparty_fee"] = tac_container.transaction.tx_counterparty_fee + new_body["quantities_by_good_id"] = _from_pairs_to_dict(tac_container.transaction.quantities_by_good_id) + new_body["tx_nonce"] = tac_container.transaction.tx_nonce + new_body["tx_sender_signature"] = tac_container.transaction.tx_sender_signature + new_body["tx_counterparty_signature"] = tac_container.transaction.tx_counterparty_signature elif tac_type == "get_state_update": new_body["type"] = TACMessage.Type.GET_STATE_UPDATE elif tac_type == "cancelled": new_body["type"] = TACMessage.Type.CANCELLED elif tac_type == "game_data": new_body["type"] = TACMessage.Type.GAME_DATA - new_body["amount_by_currency"] = _from_pairs_to_dict(tac_container.game_data.amount_by_currency) - new_body["exchange_params_by_currency"] = _from_pairs_to_dict(tac_container.game_data.exchange_params_by_currency) - new_body["quantities_by_good_pbk"] = _from_pairs_to_dict(tac_container.game_data.quantities_by_good_pbk) - new_body["utility_params_by_good_pbk"] = _from_pairs_to_dict(tac_container.game_data.utility_params_by_good_pbk) + new_body["amount_by_currency_id"] = _from_pairs_to_dict(tac_container.game_data.amount_by_currency_id) + new_body["exchange_params_by_currency_id"] = _from_pairs_to_dict(tac_container.game_data.exchange_params_by_currency_id) + new_body["quantities_by_good_id"] = _from_pairs_to_dict(tac_container.game_data.quantities_by_good_id) + new_body["utility_params_by_good_id"] = _from_pairs_to_dict(tac_container.game_data.utility_params_by_good_id) new_body["tx_fee"] = tac_container.game_data.tx_fee - new_body["agent_pbk_to_name"] = _from_pairs_to_dict(tac_container.game_data.agent_pbk_to_name) - new_body["good_pbk_to_name"] = _from_pairs_to_dict(tac_container.game_data.good_pbk_to_name) + new_body["agent_addr_to_name"] = _from_pairs_to_dict(tac_container.game_data.agent_addr_to_name) + new_body["good_id_to_name"] = _from_pairs_to_dict(tac_container.game_data.good_id_to_name) new_body["version_id"] = tac_container.game_data.version_id elif tac_type == "transaction_confirmation": new_body["type"] = TACMessage.Type.TRANSACTION_CONFIRMATION - new_body["transaction_id"] = tac_container.transaction_confirmation.transaction_id - new_body["amount_by_currency"] = _from_pairs_to_dict(tac_container.transaction_confirmation.amount_by_currency) - new_body["quantities_by_good_pbk"] = _from_pairs_to_dict(tac_container.transaction_confirmation.quantities_by_good_pbk) + new_body["tx_id"] = tac_container.transaction_confirmation.tx_id + new_body["amount_by_currency_id"] = _from_pairs_to_dict(tac_container.transaction_confirmation.amount_by_currency_id) + new_body["quantities_by_good_id"] = _from_pairs_to_dict(tac_container.transaction_confirmation.quantities_by_good_id) # elif tac_type == "state_update": # new_body["type"] = TACMessage.Type.STATE_UPDATE # game_data = dict( - # amount_by_currency=_from_pairs_to_dict(tac_container.state_update.game_data.amount_by_currency), - # exchange_params_by_currency=_from_pairs_to_dict(tac_container.state_update.game_data.exchange_params_by_currency), - # quantities_by_good_pbk=_from_pairs_to_dict(tac_container.state_update.game_data.quantities_by_good_pbk), - # utility_params_by_good_pbk=_from_pairs_to_dict(tac_container.state_update.game_data.utility_params_by_good_pbk), + # amount_by_currency_id=_from_pairs_to_dict(tac_container.state_update.game_data.amount_by_currency_id), + # exchange_params_by_currency_id=_from_pairs_to_dict(tac_container.state_update.game_data.exchange_params_by_currency_id), + # quantities_by_good_id=_from_pairs_to_dict(tac_container.state_update.game_data.quantities_by_good_id), + # utility_params_by_good_id=_from_pairs_to_dict(tac_container.state_update.game_data.utility_params_by_good_id), # tx_fee=tac_container.state_update.game_data.tx_fee, - # agent_pbk_to_name=_from_pairs_to_dict(tac_container.state_update.game_data.agent_pbk_to_name), - # good_pbk_to_name=_from_pairs_to_dict(tac_container.state_update.game_data.good_pbk_to_name), + # agent_addr_to_name=_from_pairs_to_dict(tac_container.state_update.game_data.agent_addr_to_name), + # good_id_to_name=_from_pairs_to_dict(tac_container.state_update.game_data.good_id_to_name), # version_id=tac_container.state_update.game_data.version_id # ) # new_body["game_data"] = game_data @@ -221,24 +225,22 @@ def decode(self, obj: bytes) -> Message: # tx_json = dict( # transaction_id=transaction.transaction_id, # counterparty=transaction.counterparty, - # amount_by_currency=_from_pairs_to_dict(transaction.amount_by_currency), + # amount_by_currency_id=_from_pairs_to_dict(transaction.amount_by_currency_id), # sender_tx_fee=transaction.sender_tx_fee, # counterparty_tx_fee=transaction.counterparty_tx_fee, - # quantities_by_good_pbk=_from_pairs_to_dict(transaction.quantities_by_good_pbk), + # quantities_by_good_id=_from_pairs_to_dict(transaction.quantities_by_good_id), # ) # transactions.append(tx_json) # new_body["transactions"] = transactions elif tac_type == "error": new_body["type"] = TACMessage.Type.TAC_ERROR new_body["error_code"] = TACMessage.ErrorCode(tac_container.error.error_code) - if tac_container.error.error_msg: - new_body["error_msg"] = tac_container.error.error_msg - if tac_container.error.details: - new_body["details"] = dict(tac_container.error.details) - else: + if tac_container.error.info: + new_body["info"] = _from_pairs_to_dict(tac_container.error.info) + else: # pragma: no cover raise ValueError("Type not recognized.") tac_type = TACMessage.Type(new_body["type"]) new_body["type"] = tac_type - tac_message = TACMessage(tac_type=tac_type, body=new_body) + tac_message = TACMessage(type=tac_type, body=new_body) return tac_message diff --git a/packages/protocols/tac/tac.proto b/packages/protocols/tac/tac.proto index d8e6fc93dc..c6657f6377 100644 --- a/packages/protocols/tac/tac.proto +++ b/packages/protocols/tac/tac.proto @@ -2,8 +2,6 @@ syntax = "proto3"; package fetch.oef.pb; -import "google/protobuf/struct.proto"; - message StrIntPair { string first = 1; int32 second = 2; @@ -29,20 +27,20 @@ message TACController { } message GameData { - repeated StrIntPair amount_by_currency = 1; - repeated StrFloatPair exchange_params_by_currency = 2; - repeated StrIntPair quantities_by_good_pbk = 3; - repeated StrFloatPair utility_params_by_good_pbk = 4; + repeated StrIntPair amount_by_currency_id = 1; + repeated StrFloatPair exchange_params_by_currency_id = 2; + repeated StrIntPair quantities_by_good_id = 3; + repeated StrFloatPair utility_params_by_good_id = 4; int64 tx_fee = 5; - repeated StrStrPair agent_pbk_to_name = 6; - repeated StrStrPair good_pbk_to_name = 7; + repeated StrStrPair agent_addr_to_name = 6; + repeated StrStrPair good_id_to_name = 7; string version_id = 8; } message TransactionConfirmation { - string transaction_id = 1; - repeated StrIntPair amount_by_currency = 2; - repeated StrIntPair quantities_by_good_pbk = 3; + string tx_id = 1; + repeated StrIntPair amount_by_currency_id = 2; + repeated StrIntPair quantities_by_good_id = 3; } message StateUpdate { @@ -54,7 +52,7 @@ message TACController { enum ErrorCode { GENERIC_ERROR = 0; REQUEST_NOT_VALID = 1; - AGENT_PBK_ALREADY_REGISTERED = 2; + AGENT_ADDR_ALREADY_REGISTERED = 2; AGENT_NAME_ALREADY_REGISTERED = 3; AGENT_NOT_REGISTERED = 4; TRANSACTION_NOT_VALID = 5; @@ -65,8 +63,7 @@ message TACController { } ErrorCode error_code = 1; - string error_msg = 2; - google.protobuf.Struct details = 3; + repeated StrStrPair info = 2; } } @@ -80,12 +77,16 @@ message TACAgent { } message Transaction { - string transaction_id = 1; - string counterparty = 2; - repeated StrIntPair amount_by_currency = 3; - int64 sender_tx_fee = 4; - int64 counterparty_tx_fee = 5; - repeated StrIntPair quantities_by_good_pbk = 6; + string tx_id = 1; + string tx_sender_addr = 2; + string tx_counterparty_addr = 3; + repeated StrIntPair amount_by_currency_id = 4; + int64 tx_sender_fee = 5; + int64 tx_counterparty_fee = 6; + repeated StrIntPair quantities_by_good_id = 7; + int64 tx_nonce = 8; + bytes tx_sender_signature = 9; + bytes tx_counterparty_signature = 10; } message GetStateUpdate { diff --git a/packages/protocols/tac/tac_pb2.py b/packages/protocols/tac/tac_pb2.py index 302fa77adb..eca7673074 100644 --- a/packages/protocols/tac/tac_pb2.py +++ b/packages/protocols/tac/tac_pb2.py @@ -13,7 +13,6 @@ _sym_db = _symbol_database.Default() -from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 DESCRIPTOR = _descriptor.FileDescriptor( @@ -21,9 +20,8 @@ package='fetch.oef.pb', syntax='proto3', serialized_options=None, - serialized_pb=_b('\n\ttac.proto\x12\x0c\x66\x65tch.oef.pb\x1a\x1cgoogle/protobuf/struct.proto\"+\n\nStrIntPair\x12\r\n\x05\x66irst\x18\x01 \x01(\t\x12\x0e\n\x06second\x18\x02 \x01(\x05\"-\n\x0cStrFloatPair\x12\r\n\x05\x66irst\x18\x01 \x01(\t\x12\x0e\n\x06second\x18\x02 \x01(\x01\"+\n\nStrStrPair\x12\r\n\x05\x66irst\x18\x01 \x01(\t\x12\x0e\n\x06second\x18\x02 \x01(\t\"\x93\t\n\rTACController\x1a\x0c\n\nRegistered\x1a\x0e\n\x0cUnregistered\x1a\x0b\n\tCancelled\x1a\x88\x03\n\x08GameData\x12\x34\n\x12\x61mount_by_currency\x18\x01 \x03(\x0b\x32\x18.fetch.oef.pb.StrIntPair\x12?\n\x1b\x65xchange_params_by_currency\x18\x02 \x03(\x0b\x32\x1a.fetch.oef.pb.StrFloatPair\x12\x38\n\x16quantities_by_good_pbk\x18\x03 \x03(\x0b\x32\x18.fetch.oef.pb.StrIntPair\x12>\n\x1autility_params_by_good_pbk\x18\x04 \x03(\x0b\x32\x1a.fetch.oef.pb.StrFloatPair\x12\x0e\n\x06tx_fee\x18\x05 \x01(\x03\x12\x33\n\x11\x61gent_pbk_to_name\x18\x06 \x03(\x0b\x32\x18.fetch.oef.pb.StrStrPair\x12\x32\n\x10good_pbk_to_name\x18\x07 \x03(\x0b\x32\x18.fetch.oef.pb.StrStrPair\x12\x12\n\nversion_id\x18\x08 \x01(\t\x1a\xa1\x01\n\x17TransactionConfirmation\x12\x16\n\x0etransaction_id\x18\x01 \x01(\t\x12\x34\n\x12\x61mount_by_currency\x18\x02 \x03(\x0b\x32\x18.fetch.oef.pb.StrIntPair\x12\x38\n\x16quantities_by_good_pbk\x18\x03 \x03(\x0b\x32\x18.fetch.oef.pb.StrIntPair\x1aw\n\x0bStateUpdate\x12\x37\n\tgame_data\x18\x01 \x01(\x0b\x32$.fetch.oef.pb.TACController.GameData\x12/\n\x03txs\x18\x02 \x03(\x0b\x32\".fetch.oef.pb.TACAgent.Transaction\x1a\xae\x03\n\x05\x45rror\x12?\n\nerror_code\x18\x01 \x01(\x0e\x32+.fetch.oef.pb.TACController.Error.ErrorCode\x12\x11\n\terror_msg\x18\x02 \x01(\t\x12(\n\x07\x64\x65tails\x18\x03 \x01(\x0b\x32\x17.google.protobuf.Struct\"\xa6\x02\n\tErrorCode\x12\x11\n\rGENERIC_ERROR\x10\x00\x12\x15\n\x11REQUEST_NOT_VALID\x10\x01\x12 \n\x1c\x41GENT_PBK_ALREADY_REGISTERED\x10\x02\x12!\n\x1d\x41GENT_NAME_ALREADY_REGISTERED\x10\x03\x12\x18\n\x14\x41GENT_NOT_REGISTERED\x10\x04\x12\x19\n\x15TRANSACTION_NOT_VALID\x10\x05\x12\x1c\n\x18TRANSACTION_NOT_MATCHING\x10\x06\x12\x1f\n\x1b\x41GENT_NAME_NOT_IN_WHITELIST\x10\x07\x12\x1b\n\x17\x43OMPETITION_NOT_RUNNING\x10\x08\x12\x19\n\x15\x44IALOGUE_INCONSISTENT\x10\t\"\xac\x02\n\x08TACAgent\x1a\x1e\n\x08Register\x12\x12\n\nagent_name\x18\x01 \x01(\t\x1a\x0c\n\nUnregister\x1a\xdf\x01\n\x0bTransaction\x12\x16\n\x0etransaction_id\x18\x01 \x01(\t\x12\x14\n\x0c\x63ounterparty\x18\x02 \x01(\t\x12\x34\n\x12\x61mount_by_currency\x18\x03 \x03(\x0b\x32\x18.fetch.oef.pb.StrIntPair\x12\x15\n\rsender_tx_fee\x18\x04 \x01(\x03\x12\x1b\n\x13\x63ounterparty_tx_fee\x18\x05 \x01(\x03\x12\x38\n\x16quantities_by_good_pbk\x18\x06 \x03(\x0b\x32\x18.fetch.oef.pb.StrIntPair\x1a\x10\n\x0eGetStateUpdate\"\xc8\x05\n\nTACMessage\x12\x33\n\x08register\x18\x01 \x01(\x0b\x32\x1f.fetch.oef.pb.TACAgent.RegisterH\x00\x12\x37\n\nunregister\x18\x02 \x01(\x0b\x32!.fetch.oef.pb.TACAgent.UnregisterH\x00\x12\x39\n\x0btransaction\x18\x03 \x01(\x0b\x32\".fetch.oef.pb.TACAgent.TransactionH\x00\x12\x41\n\x10get_state_update\x18\x04 \x01(\x0b\x32%.fetch.oef.pb.TACAgent.GetStateUpdateH\x00\x12<\n\nregistered\x18\x05 \x01(\x0b\x32&.fetch.oef.pb.TACController.RegisteredH\x00\x12@\n\x0cunregistered\x18\x06 \x01(\x0b\x32(.fetch.oef.pb.TACController.UnregisteredH\x00\x12:\n\tcancelled\x18\x07 \x01(\x0b\x32%.fetch.oef.pb.TACController.CancelledH\x00\x12\x39\n\tgame_data\x18\x08 \x01(\x0b\x32$.fetch.oef.pb.TACController.GameDataH\x00\x12W\n\x18transaction_confirmation\x18\t \x01(\x0b\x32\x33.fetch.oef.pb.TACController.TransactionConfirmationH\x00\x12?\n\x0cstate_update\x18\n \x01(\x0b\x32\'.fetch.oef.pb.TACController.StateUpdateH\x00\x12\x32\n\x05\x65rror\x18\x0b \x01(\x0b\x32!.fetch.oef.pb.TACController.ErrorH\x00\x42\t\n\x07\x63ontentb\x06proto3') - , - dependencies=[google_dot_protobuf_dot_struct__pb2.DESCRIPTOR,]) + serialized_pb=_b('\n\ttac.proto\x12\x0c\x66\x65tch.oef.pb\"+\n\nStrIntPair\x12\r\n\x05\x66irst\x18\x01 \x01(\t\x12\x0e\n\x06second\x18\x02 \x01(\x05\"-\n\x0cStrFloatPair\x12\r\n\x05\x66irst\x18\x01 \x01(\t\x12\x0e\n\x06second\x18\x02 \x01(\x01\"+\n\nStrStrPair\x12\r\n\x05\x66irst\x18\x01 \x01(\t\x12\x0e\n\x06second\x18\x02 \x01(\t\"\xfc\x08\n\rTACController\x1a\x0c\n\nRegistered\x1a\x0e\n\x0cUnregistered\x1a\x0b\n\tCancelled\x1a\x8c\x03\n\x08GameData\x12\x37\n\x15\x61mount_by_currency_id\x18\x01 \x03(\x0b\x32\x18.fetch.oef.pb.StrIntPair\x12\x42\n\x1e\x65xchange_params_by_currency_id\x18\x02 \x03(\x0b\x32\x1a.fetch.oef.pb.StrFloatPair\x12\x37\n\x15quantities_by_good_id\x18\x03 \x03(\x0b\x32\x18.fetch.oef.pb.StrIntPair\x12=\n\x19utility_params_by_good_id\x18\x04 \x03(\x0b\x32\x1a.fetch.oef.pb.StrFloatPair\x12\x0e\n\x06tx_fee\x18\x05 \x01(\x03\x12\x34\n\x12\x61gent_addr_to_name\x18\x06 \x03(\x0b\x32\x18.fetch.oef.pb.StrStrPair\x12\x31\n\x0fgood_id_to_name\x18\x07 \x03(\x0b\x32\x18.fetch.oef.pb.StrStrPair\x12\x12\n\nversion_id\x18\x08 \x01(\t\x1a\x9a\x01\n\x17TransactionConfirmation\x12\r\n\x05tx_id\x18\x01 \x01(\t\x12\x37\n\x15\x61mount_by_currency_id\x18\x02 \x03(\x0b\x32\x18.fetch.oef.pb.StrIntPair\x12\x37\n\x15quantities_by_good_id\x18\x03 \x03(\x0b\x32\x18.fetch.oef.pb.StrIntPair\x1aw\n\x0bStateUpdate\x12\x37\n\tgame_data\x18\x01 \x01(\x0b\x32$.fetch.oef.pb.TACController.GameData\x12/\n\x03txs\x18\x02 \x03(\x0b\x32\".fetch.oef.pb.TACAgent.Transaction\x1a\x9a\x03\n\x05\x45rror\x12?\n\nerror_code\x18\x01 \x01(\x0e\x32+.fetch.oef.pb.TACController.Error.ErrorCode\x12&\n\x04info\x18\x02 \x03(\x0b\x32\x18.fetch.oef.pb.StrStrPair\"\xa7\x02\n\tErrorCode\x12\x11\n\rGENERIC_ERROR\x10\x00\x12\x15\n\x11REQUEST_NOT_VALID\x10\x01\x12!\n\x1d\x41GENT_ADDR_ALREADY_REGISTERED\x10\x02\x12!\n\x1d\x41GENT_NAME_ALREADY_REGISTERED\x10\x03\x12\x18\n\x14\x41GENT_NOT_REGISTERED\x10\x04\x12\x19\n\x15TRANSACTION_NOT_VALID\x10\x05\x12\x1c\n\x18TRANSACTION_NOT_MATCHING\x10\x06\x12\x1f\n\x1b\x41GENT_NAME_NOT_IN_WHITELIST\x10\x07\x12\x1b\n\x17\x43OMPETITION_NOT_RUNNING\x10\x08\x12\x19\n\x15\x44IALOGUE_INCONSISTENT\x10\t\"\x97\x03\n\x08TACAgent\x1a\x1e\n\x08Register\x12\x12\n\nagent_name\x18\x01 \x01(\t\x1a\x0c\n\nUnregister\x1a\xca\x02\n\x0bTransaction\x12\r\n\x05tx_id\x18\x01 \x01(\t\x12\x16\n\x0etx_sender_addr\x18\x02 \x01(\t\x12\x1c\n\x14tx_counterparty_addr\x18\x03 \x01(\t\x12\x37\n\x15\x61mount_by_currency_id\x18\x04 \x03(\x0b\x32\x18.fetch.oef.pb.StrIntPair\x12\x15\n\rtx_sender_fee\x18\x05 \x01(\x03\x12\x1b\n\x13tx_counterparty_fee\x18\x06 \x01(\x03\x12\x37\n\x15quantities_by_good_id\x18\x07 \x03(\x0b\x32\x18.fetch.oef.pb.StrIntPair\x12\x10\n\x08tx_nonce\x18\x08 \x01(\x03\x12\x1b\n\x13tx_sender_signature\x18\t \x01(\x0c\x12!\n\x19tx_counterparty_signature\x18\n \x01(\x0c\x1a\x10\n\x0eGetStateUpdate\"\xc8\x05\n\nTACMessage\x12\x33\n\x08register\x18\x01 \x01(\x0b\x32\x1f.fetch.oef.pb.TACAgent.RegisterH\x00\x12\x37\n\nunregister\x18\x02 \x01(\x0b\x32!.fetch.oef.pb.TACAgent.UnregisterH\x00\x12\x39\n\x0btransaction\x18\x03 \x01(\x0b\x32\".fetch.oef.pb.TACAgent.TransactionH\x00\x12\x41\n\x10get_state_update\x18\x04 \x01(\x0b\x32%.fetch.oef.pb.TACAgent.GetStateUpdateH\x00\x12<\n\nregistered\x18\x05 \x01(\x0b\x32&.fetch.oef.pb.TACController.RegisteredH\x00\x12@\n\x0cunregistered\x18\x06 \x01(\x0b\x32(.fetch.oef.pb.TACController.UnregisteredH\x00\x12:\n\tcancelled\x18\x07 \x01(\x0b\x32%.fetch.oef.pb.TACController.CancelledH\x00\x12\x39\n\tgame_data\x18\x08 \x01(\x0b\x32$.fetch.oef.pb.TACController.GameDataH\x00\x12W\n\x18transaction_confirmation\x18\t \x01(\x0b\x32\x33.fetch.oef.pb.TACController.TransactionConfirmationH\x00\x12?\n\x0cstate_update\x18\n \x01(\x0b\x32\'.fetch.oef.pb.TACController.StateUpdateH\x00\x12\x32\n\x05\x65rror\x18\x0b \x01(\x0b\x32!.fetch.oef.pb.TACController.ErrorH\x00\x42\t\n\x07\x63ontentb\x06proto3') +) @@ -42,7 +40,7 @@ serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='AGENT_PBK_ALREADY_REGISTERED', index=2, number=2, + name='AGENT_ADDR_ALREADY_REGISTERED', index=2, number=2, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( @@ -76,8 +74,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=1072, - serialized_end=1366, + serialized_start=1018, + serialized_end=1313, ) _sym_db.RegisterEnumDescriptor(_TACCONTROLLER_ERROR_ERRORCODE) @@ -115,8 +113,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=57, - serialized_end=100, + serialized_start=27, + serialized_end=70, ) @@ -153,8 +151,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=102, - serialized_end=147, + serialized_start=72, + serialized_end=117, ) @@ -191,8 +189,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=149, - serialized_end=192, + serialized_start=119, + serialized_end=162, ) @@ -215,8 +213,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=212, - serialized_end=224, + serialized_start=182, + serialized_end=194, ) _TACCONTROLLER_UNREGISTERED = _descriptor.Descriptor( @@ -238,8 +236,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=226, - serialized_end=240, + serialized_start=196, + serialized_end=210, ) _TACCONTROLLER_CANCELLED = _descriptor.Descriptor( @@ -261,8 +259,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=242, - serialized_end=253, + serialized_start=212, + serialized_end=223, ) _TACCONTROLLER_GAMEDATA = _descriptor.Descriptor( @@ -273,28 +271,28 @@ containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='amount_by_currency', full_name='fetch.oef.pb.TACController.GameData.amount_by_currency', index=0, + name='amount_by_currency_id', full_name='fetch.oef.pb.TACController.GameData.amount_by_currency_id', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='exchange_params_by_currency', full_name='fetch.oef.pb.TACController.GameData.exchange_params_by_currency', index=1, + name='exchange_params_by_currency_id', full_name='fetch.oef.pb.TACController.GameData.exchange_params_by_currency_id', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='quantities_by_good_pbk', full_name='fetch.oef.pb.TACController.GameData.quantities_by_good_pbk', index=2, + name='quantities_by_good_id', full_name='fetch.oef.pb.TACController.GameData.quantities_by_good_id', index=2, number=3, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='utility_params_by_good_pbk', full_name='fetch.oef.pb.TACController.GameData.utility_params_by_good_pbk', index=3, + name='utility_params_by_good_id', full_name='fetch.oef.pb.TACController.GameData.utility_params_by_good_id', index=3, number=4, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, @@ -308,14 +306,14 @@ is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='agent_pbk_to_name', full_name='fetch.oef.pb.TACController.GameData.agent_pbk_to_name', index=5, + name='agent_addr_to_name', full_name='fetch.oef.pb.TACController.GameData.agent_addr_to_name', index=5, number=6, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='good_pbk_to_name', full_name='fetch.oef.pb.TACController.GameData.good_pbk_to_name', index=6, + name='good_id_to_name', full_name='fetch.oef.pb.TACController.GameData.good_id_to_name', index=6, number=7, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, @@ -340,8 +338,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=256, - serialized_end=648, + serialized_start=226, + serialized_end=622, ) _TACCONTROLLER_TRANSACTIONCONFIRMATION = _descriptor.Descriptor( @@ -352,21 +350,21 @@ containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='transaction_id', full_name='fetch.oef.pb.TACController.TransactionConfirmation.transaction_id', index=0, + name='tx_id', full_name='fetch.oef.pb.TACController.TransactionConfirmation.tx_id', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='amount_by_currency', full_name='fetch.oef.pb.TACController.TransactionConfirmation.amount_by_currency', index=1, + name='amount_by_currency_id', full_name='fetch.oef.pb.TACController.TransactionConfirmation.amount_by_currency_id', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='quantities_by_good_pbk', full_name='fetch.oef.pb.TACController.TransactionConfirmation.quantities_by_good_pbk', index=2, + name='quantities_by_good_id', full_name='fetch.oef.pb.TACController.TransactionConfirmation.quantities_by_good_id', index=2, number=3, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, @@ -384,8 +382,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=651, - serialized_end=812, + serialized_start=625, + serialized_end=779, ) _TACCONTROLLER_STATEUPDATE = _descriptor.Descriptor( @@ -421,8 +419,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=814, - serialized_end=933, + serialized_start=781, + serialized_end=900, ) _TACCONTROLLER_ERROR = _descriptor.Descriptor( @@ -440,16 +438,9 @@ is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='error_msg', full_name='fetch.oef.pb.TACController.Error.error_msg', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='details', full_name='fetch.oef.pb.TACController.Error.details', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, + name='info', full_name='fetch.oef.pb.TACController.Error.info', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), @@ -466,8 +457,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=936, - serialized_end=1366, + serialized_start=903, + serialized_end=1313, ) _TACCONTROLLER = _descriptor.Descriptor( @@ -489,8 +480,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=195, - serialized_end=1366, + serialized_start=165, + serialized_end=1313, ) @@ -520,8 +511,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1381, - serialized_end=1411, + serialized_start=1328, + serialized_end=1358, ) _TACAGENT_UNREGISTER = _descriptor.Descriptor( @@ -543,8 +534,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1413, - serialized_end=1425, + serialized_start=1360, + serialized_end=1372, ) _TACAGENT_TRANSACTION = _descriptor.Descriptor( @@ -555,47 +546,75 @@ containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='transaction_id', full_name='fetch.oef.pb.TACAgent.Transaction.transaction_id', index=0, + name='tx_id', full_name='fetch.oef.pb.TACAgent.Transaction.tx_id', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='counterparty', full_name='fetch.oef.pb.TACAgent.Transaction.counterparty', index=1, + name='tx_sender_addr', full_name='fetch.oef.pb.TACAgent.Transaction.tx_sender_addr', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='amount_by_currency', full_name='fetch.oef.pb.TACAgent.Transaction.amount_by_currency', index=2, - number=3, type=11, cpp_type=10, label=3, + name='tx_counterparty_addr', full_name='fetch.oef.pb.TACAgent.Transaction.tx_counterparty_addr', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='amount_by_currency_id', full_name='fetch.oef.pb.TACAgent.Transaction.amount_by_currency_id', index=3, + number=4, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='sender_tx_fee', full_name='fetch.oef.pb.TACAgent.Transaction.sender_tx_fee', index=3, - number=4, type=3, cpp_type=2, label=1, + name='tx_sender_fee', full_name='fetch.oef.pb.TACAgent.Transaction.tx_sender_fee', index=4, + number=5, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='counterparty_tx_fee', full_name='fetch.oef.pb.TACAgent.Transaction.counterparty_tx_fee', index=4, - number=5, type=3, cpp_type=2, label=1, + name='tx_counterparty_fee', full_name='fetch.oef.pb.TACAgent.Transaction.tx_counterparty_fee', index=5, + number=6, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='quantities_by_good_pbk', full_name='fetch.oef.pb.TACAgent.Transaction.quantities_by_good_pbk', index=5, - number=6, type=11, cpp_type=10, label=3, + name='quantities_by_good_id', full_name='fetch.oef.pb.TACAgent.Transaction.quantities_by_good_id', index=6, + number=7, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='tx_nonce', full_name='fetch.oef.pb.TACAgent.Transaction.tx_nonce', index=7, + number=8, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='tx_sender_signature', full_name='fetch.oef.pb.TACAgent.Transaction.tx_sender_signature', index=8, + number=9, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='tx_counterparty_signature', full_name='fetch.oef.pb.TACAgent.Transaction.tx_counterparty_signature', index=9, + number=10, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -608,8 +627,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1428, - serialized_end=1651, + serialized_start=1375, + serialized_end=1705, ) _TACAGENT_GETSTATEUPDATE = _descriptor.Descriptor( @@ -631,8 +650,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1653, - serialized_end=1669, + serialized_start=1707, + serialized_end=1723, ) _TACAGENT = _descriptor.Descriptor( @@ -654,8 +673,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1369, - serialized_end=1669, + serialized_start=1316, + serialized_end=1723, ) @@ -758,34 +777,34 @@ name='content', full_name='fetch.oef.pb.TACMessage.content', index=0, containing_type=None, fields=[]), ], - serialized_start=1672, - serialized_end=2384, + serialized_start=1726, + serialized_end=2438, ) _TACCONTROLLER_REGISTERED.containing_type = _TACCONTROLLER _TACCONTROLLER_UNREGISTERED.containing_type = _TACCONTROLLER _TACCONTROLLER_CANCELLED.containing_type = _TACCONTROLLER -_TACCONTROLLER_GAMEDATA.fields_by_name['amount_by_currency'].message_type = _STRINTPAIR -_TACCONTROLLER_GAMEDATA.fields_by_name['exchange_params_by_currency'].message_type = _STRFLOATPAIR -_TACCONTROLLER_GAMEDATA.fields_by_name['quantities_by_good_pbk'].message_type = _STRINTPAIR -_TACCONTROLLER_GAMEDATA.fields_by_name['utility_params_by_good_pbk'].message_type = _STRFLOATPAIR -_TACCONTROLLER_GAMEDATA.fields_by_name['agent_pbk_to_name'].message_type = _STRSTRPAIR -_TACCONTROLLER_GAMEDATA.fields_by_name['good_pbk_to_name'].message_type = _STRSTRPAIR +_TACCONTROLLER_GAMEDATA.fields_by_name['amount_by_currency_id'].message_type = _STRINTPAIR +_TACCONTROLLER_GAMEDATA.fields_by_name['exchange_params_by_currency_id'].message_type = _STRFLOATPAIR +_TACCONTROLLER_GAMEDATA.fields_by_name['quantities_by_good_id'].message_type = _STRINTPAIR +_TACCONTROLLER_GAMEDATA.fields_by_name['utility_params_by_good_id'].message_type = _STRFLOATPAIR +_TACCONTROLLER_GAMEDATA.fields_by_name['agent_addr_to_name'].message_type = _STRSTRPAIR +_TACCONTROLLER_GAMEDATA.fields_by_name['good_id_to_name'].message_type = _STRSTRPAIR _TACCONTROLLER_GAMEDATA.containing_type = _TACCONTROLLER -_TACCONTROLLER_TRANSACTIONCONFIRMATION.fields_by_name['amount_by_currency'].message_type = _STRINTPAIR -_TACCONTROLLER_TRANSACTIONCONFIRMATION.fields_by_name['quantities_by_good_pbk'].message_type = _STRINTPAIR +_TACCONTROLLER_TRANSACTIONCONFIRMATION.fields_by_name['amount_by_currency_id'].message_type = _STRINTPAIR +_TACCONTROLLER_TRANSACTIONCONFIRMATION.fields_by_name['quantities_by_good_id'].message_type = _STRINTPAIR _TACCONTROLLER_TRANSACTIONCONFIRMATION.containing_type = _TACCONTROLLER _TACCONTROLLER_STATEUPDATE.fields_by_name['game_data'].message_type = _TACCONTROLLER_GAMEDATA _TACCONTROLLER_STATEUPDATE.fields_by_name['txs'].message_type = _TACAGENT_TRANSACTION _TACCONTROLLER_STATEUPDATE.containing_type = _TACCONTROLLER _TACCONTROLLER_ERROR.fields_by_name['error_code'].enum_type = _TACCONTROLLER_ERROR_ERRORCODE -_TACCONTROLLER_ERROR.fields_by_name['details'].message_type = google_dot_protobuf_dot_struct__pb2._STRUCT +_TACCONTROLLER_ERROR.fields_by_name['info'].message_type = _STRSTRPAIR _TACCONTROLLER_ERROR.containing_type = _TACCONTROLLER _TACCONTROLLER_ERROR_ERRORCODE.containing_type = _TACCONTROLLER_ERROR _TACAGENT_REGISTER.containing_type = _TACAGENT _TACAGENT_UNREGISTER.containing_type = _TACAGENT -_TACAGENT_TRANSACTION.fields_by_name['amount_by_currency'].message_type = _STRINTPAIR -_TACAGENT_TRANSACTION.fields_by_name['quantities_by_good_pbk'].message_type = _STRINTPAIR +_TACAGENT_TRANSACTION.fields_by_name['amount_by_currency_id'].message_type = _STRINTPAIR +_TACAGENT_TRANSACTION.fields_by_name['quantities_by_good_id'].message_type = _STRINTPAIR _TACAGENT_TRANSACTION.containing_type = _TACAGENT _TACAGENT_GETSTATEUPDATE.containing_type = _TACAGENT _TACMESSAGE.fields_by_name['register'].message_type = _TACAGENT_REGISTER diff --git a/packages/skills/carpark_client/behaviours.py b/packages/skills/carpark_client/behaviours.py index 4814e22f28..c5fe1390f0 100644 --- a/packages/skills/carpark_client/behaviours.py +++ b/packages/skills/carpark_client/behaviours.py @@ -22,19 +22,21 @@ import sys from typing import cast, TYPE_CHECKING -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.serialization import DEFAULT_OEF, OEFSerializer -from aea.skills.base import Behaviour +from aea.skills.behaviours import TickerBehaviour if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.oef.message import OEFMessage + from packages.protocols.oef.serialization import DEFAULT_OEF, OEFSerializer from packages.skills.carpark_client.strategy import Strategy else: + from oef_protocol.message import OEFMessage + from oef_protocol.serialization import DEFAULT_OEF, OEFSerializer from carpark_client_skill.strategy import Strategy logger = logging.getLogger("aea.carpark_client_skill") -class MySearchBehaviour(Behaviour): +class MySearchBehaviour(TickerBehaviour): """This class scaffolds a behaviour.""" def __init__(self, **kwargs): @@ -58,15 +60,15 @@ def act(self) -> None: :return: None """ strategy = cast(Strategy, self.context.strategy) - if strategy.is_searching and strategy.is_time_to_search(): + if strategy.is_searching: strategy.on_submit_search() self._search_id += 1 query = strategy.get_service_query() - search_request = OEFMessage(oef_type=OEFMessage.Type.SEARCH_SERVICES, + search_request = OEFMessage(type=OEFMessage.Type.SEARCH_SERVICES, id=self._search_id, query=query) self.context.outbox.put_message(to=DEFAULT_OEF, - sender=self.context.agent_public_key, + sender=self.context.agent_address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(search_request)) diff --git a/packages/skills/carpark_client/dialogues.py b/packages/skills/carpark_client/dialogues.py index 4f956bb08a..730e5945cc 100644 --- a/packages/skills/carpark_client/dialogues.py +++ b/packages/skills/carpark_client/dialogues.py @@ -25,13 +25,18 @@ - Dialogues: The dialogues class keeps track of all dialogues. """ -from typing import Optional +import sys +from typing import Optional, TYPE_CHECKING from aea.helpers.dialogue.base import DialogueLabel -from aea.protocols.fipa.dialogues import FIPADialogues, FIPADialogue -from aea.protocols.oef.models import Description +from aea.helpers.search.models import Description from aea.skills.base import SharedClass +if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.fipa.dialogues import FIPADialogues, FIPADialogue +else: + from fipa_protocol.dialogues import FIPADialogues, FIPADialogue + class Dialogue(FIPADialogue): """The dialogue class maintains state of a dialogue and manages it.""" diff --git a/packages/skills/carpark_client/handlers.py b/packages/skills/carpark_client/handlers.py index 09484fc67e..efc38116d2 100644 --- a/packages/skills/carpark_client/handlers.py +++ b/packages/skills/carpark_client/handlers.py @@ -24,21 +24,24 @@ from typing import Dict, List, Optional, cast, TYPE_CHECKING from aea.configurations.base import ProtocolId +from aea.decision_maker.messages.transaction import TransactionMessage from aea.helpers.dialogue.base import DialogueLabel +from aea.helpers.search.models import Description from aea.protocols.base import Message from aea.protocols.default.message import DefaultMessage from aea.protocols.default.serialization import DefaultSerializer -from aea.protocols.fipa.message import FIPAMessage -from aea.protocols.fipa.serialization import FIPASerializer -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.models import Description from aea.skills.base import Handler -from aea.decision_maker.messages.transaction import TransactionMessage if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.fipa.message import FIPAMessage + from packages.protocols.fipa.serialization import FIPASerializer + from packages.protocols.oef.message import OEFMessage from packages.skills.carpark_client.dialogues import Dialogue, Dialogues from packages.skills.carpark_client.strategy import Strategy else: + from fipa_protocol.message import FIPAMessage + from fipa_protocol.serialization import FIPASerializer + from oef_protocol.message import OEFMessage from carpark_client_skill.dialogues import Dialogue, Dialogues from carpark_client_skill.strategy import Strategy @@ -66,37 +69,35 @@ def setup(self) -> None: """ pass - def handle(self, message: Message, sender: str) -> None: + def handle(self, message: Message) -> None: """ Implement the reaction to a message. :param message: the message - :param sender: the sender :return: None """ # convenience representations fipa_msg = cast(FIPAMessage, message) msg_performative = FIPAMessage.Performative(message.get('performative')) - message_id = cast(int, message.get("message_id")) # recover dialogue dialogues = cast(Dialogues, self.context.dialogues) - if dialogues.is_belonging_to_registered_dialogue(fipa_msg, sender, self.context.agent_public_key): - dialogue = cast(Dialogue, dialogues.get_dialogue(fipa_msg, sender, self.context.agent_public_key)) + if dialogues.is_belonging_to_registered_dialogue(fipa_msg, self.context.agent_address): + dialogue = cast(Dialogue, dialogues.get_dialogue(fipa_msg, self.context.agent_address)) dialogue.incoming_extend(fipa_msg) else: - self._handle_unidentified_dialogue(fipa_msg, sender) + self._handle_unidentified_dialogue(fipa_msg) return # handle message if msg_performative == FIPAMessage.Performative.PROPOSE: - self._handle_propose(fipa_msg, sender, message_id, dialogue) + self._handle_propose(fipa_msg, dialogue) elif msg_performative == FIPAMessage.Performative.DECLINE: - self._handle_decline(fipa_msg, sender, message_id, dialogue) - elif msg_performative == FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS: - self._handle_match_accept(fipa_msg, sender, message_id, dialogue) + self._handle_decline(fipa_msg, dialogue) + elif msg_performative == FIPAMessage.Performative.MATCH_ACCEPT_W_INFORM: + self._handle_match_accept(fipa_msg, dialogue) elif msg_performative == FIPAMessage.Performative.INFORM: - self._handle_inform(fipa_msg, sender, message_id, dialogue) + self._handle_inform(fipa_msg, dialogue) def teardown(self) -> None: """ @@ -106,133 +107,123 @@ def teardown(self) -> None: """ pass - def _handle_unidentified_dialogue(self, msg: FIPAMessage, sender: str) -> None: + def _handle_unidentified_dialogue(self, msg: FIPAMessage) -> None: """ Handle an unidentified dialogue. - :param msg: the message - :param sender: the sender + :param msg: the message. """ logger.info("[{}]: unidentified dialogue.".format(self.context.agent_name)) default_msg = DefaultMessage(type=DefaultMessage.Type.ERROR, error_code=DefaultMessage.ErrorCode.INVALID_DIALOGUE.value, error_msg="Invalid dialogue.", error_data="fipa_message") # FIPASerializer().encode(msg)) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=DefaultMessage.protocol_id, message=DefaultSerializer().encode(default_msg)) - def _handle_propose(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: Dialogue) -> None: + def _handle_propose(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the propose. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ - new_message_id = message_id + 1 - new_target_id = message_id - proposals = cast(List[Description], msg.get("proposal")) + new_message_id = msg.message_id + 1 + new_target = msg.message_id + proposals = msg.proposal if proposals is not []: # only take the first proposal proposal = proposals[0] logger.info("[{}]: received proposal={} from sender={}".format(self.context.agent_name, proposal.values, - sender[-5:])) + msg.counterparty[-5:])) strategy = cast(Strategy, self.context.strategy) acceptable = strategy.is_acceptable_proposal(proposal) affordable = self.context.ledger_apis.token_balance('fetchai', cast(str, self.context.agent_addresses.get('fetchai'))) >= cast(int, proposal.values.get('price')) if acceptable and affordable: logger.info("[{}]: accepting the proposal from sender={}".format(self.context.agent_name, - sender[-5:])) + msg.counterparty[-5:])) dialogue.proposal = proposal accept_msg = FIPAMessage(message_id=new_message_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, - target=new_target_id, + target=new_target, performative=FIPAMessage.Performative.ACCEPT) dialogue.outgoing_extend(accept_msg) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(accept_msg)) else: logger.info("[{}]: declining the proposal from sender={}".format(self.context.agent_name, - sender[-5:])) + msg.counterparty[-5:])) decline_msg = FIPAMessage(message_id=new_message_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, - target=new_target_id, + target=new_target, performative=FIPAMessage.Performative.DECLINE) dialogue.outgoing_extend(decline_msg) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(decline_msg)) - def _handle_decline(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: Dialogue) -> None: + def _handle_decline(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the decline. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ - logger.info("[{}]: received DECLINE from sender={}".format(self.context.agent_name, sender[-5:])) + logger.info("[{}]: received DECLINE from sender={}".format(self.context.agent_name, msg.counterparty[-5:])) - def _handle_match_accept(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: Dialogue) -> None: + def _handle_match_accept(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the match accept. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ - logger.info("[{}]: received MATCH_ACCEPT_W_ADDRESS from sender={}".format(self.context.agent_name, sender[-5:])) - address = cast(str, msg.get("address")) + logger.info("[{}]: received MATCH_ACCEPT_W_INFORM from sender={}".format(self.context.agent_name, + msg.counterparty[-5:])) + info = msg.info + address = cast(str, info.get("address")) proposal = cast(Description, dialogue.proposal) - tx_msg = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE, - skill_id="carpark_client", - transaction_id="transaction0", - sender=self.context.agent_public_keys['fetchai'], - counterparty=address, - is_sender_buyer=True, - currency_pbk="FET", - amount=proposal.values['price'], - sender_tx_fee=0, - counterparty_tx_fee=0, - quantities_by_good_pbk={}, - dialogue_label=dialogue.dialogue_label.json, - ledger_id='fetchai') + tx_msg = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE_FOR_SETTLEMENT, + skill_callback_ids=["carpark_client"], + tx_id="transaction0", + tx_sender_addr=self.context.agent_addresses['fetchai'], + tx_counterparty_addr=address, + tx_amount_by_currency_id={proposal.values['currency_id']: - proposal.values['price']}, + tx_sender_fee=0, + tx_counterparty_fee=0, + tx_quantities_by_good_id={}, + ledger_id=proposal.values['ledger_id'], + info={'dialogue_label': dialogue.dialogue_label.json}) self.context.decision_maker_message_queue.put_nowait(tx_msg) logger.info("[{}]: proposing the transaction to the decision maker. Waiting for confirmation ...".format(self.context.agent_name)) - def _handle_inform(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: Dialogue) -> None: + def _handle_inform(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the match inform. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ - logger.info("[{}]: received INFORM from sender={}".format(self.context.agent_name, sender[-5:])) - json_data = cast(dict, msg.get("json_data")) - if 'message_type' in json_data and json_data['message_type'] == 'car_park_data': + logger.info("[{}]: received INFORM from sender={}".format(self.context.agent_name, msg.counterparty[-5:])) + if 'message_type' in msg.info and msg.info['message_type'] == 'car_park_data': logger.info("[{}]: received the following carpark data={}".format(self.context.agent_name, - pprint.pformat(json_data))) + pprint.pformat(msg.info))) # dialogues = cast(Dialogues, self.context.dialogues) # dialogues.dialogue_stats.add_dialogue_endstate(Dialogue.EndState.SUCCESSFUL) else: logger.info("[{}]: received no data from sender={}".format(self.context.agent_name, - sender[-5:])) + msg.counterparty[-5:])) class OEFHandler(Handler): @@ -248,21 +239,18 @@ def setup(self) -> None: """Call to setup the handler.""" pass - def handle(self, message: Message, sender: str) -> None: + def handle(self, message: Message) -> None: """ Implement the reaction to a message. :param message: the message - :param sender: the sender :return: None """ # convenience representations oef_msg = cast(OEFMessage, message) - oef_msg_type = OEFMessage.Type(oef_msg.get("type")) - if oef_msg_type is OEFMessage.Type.SEARCH_RESULT: - agents = cast(List[str], oef_msg.get("agents")) - self._handle_search(agents) + if oef_msg.type is OEFMessage.Type.SEARCH_RESULT: + self._handle_search(oef_msg.agents) def teardown(self) -> None: """ @@ -286,19 +274,19 @@ def _handle_search(self, agents: List[str]) -> None: logger.info("[{}]: found agents={}, stopping search.".format(self.context.agent_name, list(map(lambda x: x[-5:], agents)))) # pick first agent found - opponent_pbk = agents[0] + opponent_addr = agents[0] dialogues = cast(Dialogues, self.context.dialogues) - dialogue = dialogues.create_self_initiated(opponent_pbk, self.context.agent_public_key, is_seller=False) + dialogue = dialogues.create_self_initiated(opponent_addr, self.context.agent_address, is_seller=False) query = strategy.get_service_query() - logger.info("[{}]: sending CFP to agent={}".format(self.context.agent_name, opponent_pbk[-5:])) + logger.info("[{}]: sending CFP to agent={}".format(self.context.agent_name, opponent_addr[-5:])) cfp_msg = FIPAMessage(message_id=STARTING_MESSAGE_ID, dialogue_reference=dialogue.dialogue_label.dialogue_reference, performative=FIPAMessage.Performative.CFP, target=STARTING_TARGET_ID, query=query) dialogue.outgoing_extend(cfp_msg) - self.context.outbox.put_message(to=opponent_pbk, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=opponent_addr, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(cfp_msg)) else: @@ -315,38 +303,38 @@ def setup(self) -> None: """Implement the setup for the handler.""" pass - def handle(self, message: Message, sender: str) -> None: + def handle(self, message: Message) -> None: """ Implement the reaction to a message. :param message: the message - :param sender: the sender :return: None """ tx_msg_response = cast(TransactionMessage, message) if tx_msg_response is not None and \ - TransactionMessage.Performative(tx_msg_response.get("performative")) == TransactionMessage.Performative.ACCEPT: + tx_msg_response.performative == TransactionMessage.Performative.SUCCESSFUL_SETTLEMENT: logger.info("[{}]: transaction was successful.".format(self.context.agent_name)) - json_data = {'transaction_digest': tx_msg_response.get("transaction_digest")} - dialogue_label = DialogueLabel.from_json(cast(Dict[str, str], tx_msg_response.get("dialogue_label"))) + json_data = {'transaction_digest': tx_msg_response.tx_digest} + info = tx_msg_response.info + dialogue_label = DialogueLabel.from_json(cast(Dict[str, str], info.get("dialogue_label"))) dialogues = cast(Dialogues, self.context.dialogues) dialogue = dialogues.dialogues[dialogue_label] fipa_msg = cast(FIPAMessage, dialogue.last_incoming_message) - new_message_id = cast(int, fipa_msg.get("message_id")) + 1 - new_target_id = cast(int, fipa_msg.get("target")) + 1 - counterparty_pbk = dialogue.dialogue_label.dialogue_opponent_pbk + new_message_id = fipa_msg.message_id + 1 + new_target_id = fipa_msg.message_id + counterparty_id = dialogue.dialogue_label.dialogue_opponent_addr inform_msg = FIPAMessage(message_id=new_message_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, target=new_target_id, performative=FIPAMessage.Performative.INFORM, - json_data=json_data) + info=json_data) dialogue.outgoing_extend(inform_msg) - self.context.outbox.put_message(to=counterparty_pbk, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=counterparty_id, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(inform_msg)) - logger.info("[{}]: informing counterparty={} of transaction digest.".format(self.context.agent_name, counterparty_pbk[-5:])) + logger.info("[{}]: informing counterparty={} of transaction digest.".format(self.context.agent_name, counterparty_id[-5:])) self._received_tx_message = True else: logger.info("[{}]: transaction was not successful.".format(self.context.agent_name)) diff --git a/packages/skills/carpark_client/skill.yaml b/packages/skills/carpark_client/skill.yaml index 4020ab3aad..49927a4cc1 100644 --- a/packages/skills/carpark_client/skill.yaml +++ b/packages/skills/carpark_client/skill.yaml @@ -1,34 +1,36 @@ name: carpark_client -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" behaviours: - - behaviour: - class_name: MySearchBehaviour - args: {} + search: + class_name: MySearchBehaviour + args: + tick_interval: 120 handlers: - - handler: - class_name: FIPAHandler - args: {} - - handler: - class_name: OEFHandler - args: {} - - handler: - class_name: MyTransactionHandler - args: {} -tasks: [] + fipa: + class_name: FIPAHandler + args: {} + oef: + class_name: OEFHandler + args: {} + transaction: + class_name: MyTransactionHandler + args: {} +tasks: {} shared_classes: - - shared_class: - class_name: Strategy - args: - country: UK - search_interval: 120 - no_find_search_interval: 5 - max_price: 400000000 - max_detection_age: 36000000 - - shared_class: - class_name: Dialogues - args: {} + strategy: + class_name: Strategy + args: + country: UK + search_interval: 120 + no_find_search_interval: 5 + max_price: 400000000 + max_detection_age: 36000000 + dialogues: + class_name: Dialogues + args: {} protocols: ['fipa','default','oef'] ledgers: ['fetchai'] diff --git a/packages/skills/carpark_client/strategy.py b/packages/skills/carpark_client/strategy.py index 472c2223c0..a29d7ea5cb 100644 --- a/packages/skills/carpark_client/strategy.py +++ b/packages/skills/carpark_client/strategy.py @@ -19,11 +19,10 @@ """This module contains the strategy class.""" -import datetime import time from typing import cast -from aea.protocols.oef.models import Description, Query, Constraint, ConstraintType +from aea.helpers.search.models import Description, Query, Constraint, ConstraintType from aea.skills.base import SharedClass DEFAULT_COUNTRY = 'UK' @@ -49,8 +48,8 @@ def __init__(self, **kwargs) -> None: self._max_price = kwargs.pop('max_price') if 'max_price' in kwargs.keys() else DEFAULT_MAX_PRICE self._max_detection_age = kwargs.pop('max_detection_age') if 'max_detection_age' in kwargs.keys() else DEFAULT_MAX_DETECTION_AGE super().__init__(**kwargs) + self.is_searching = True - self.last_search_time = datetime.datetime.now() - datetime.timedelta(seconds=self._search_interval) def get_service_query(self) -> Query: """ @@ -67,25 +66,12 @@ def on_submit_search(self): def on_search_success(self): """Call when search returns succesfully.""" - self.last_search_time = datetime.datetime.now() self.is_searching = True def on_search_failed(self): """Call when search returns with no matches.""" - self.last_search_time = datetime.datetime.now() - datetime.timedelta(seconds=self._search_interval - self._no_find_search_interval) self.is_searching = True - def is_time_to_search(self) -> bool: - """ - Check whether it is time to search. - - :return: whether it is time to search - """ - now = datetime.datetime.now() - diff = now - self.last_search_time - result = diff.total_seconds() > self._search_interval - return result - def is_acceptable_proposal(self, proposal: Description) -> bool: """ Check whether it is an acceptable proposal. diff --git a/packages/skills/carpark_detection/behaviours.py b/packages/skills/carpark_detection/behaviours.py index 25b56614e9..3b03f9cea2 100755 --- a/packages/skills/carpark_detection/behaviours.py +++ b/packages/skills/carpark_detection/behaviours.py @@ -19,21 +19,23 @@ """This package contains a scaffold of a behaviour.""" -import datetime import logging import os import subprocess import sys from typing import Optional, cast, TYPE_CHECKING +from aea.helpers.search.models import Description from aea.skills.base import Behaviour -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.models import Description -from aea.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF +from aea.skills.behaviours import TickerBehaviour if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.oef.message import OEFMessage + from packages.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF from packages.skills.carpark_detection.strategy import Strategy else: + from oef_protocol.message import OEFMessage + from oef_protocol.serialization import OEFSerializer, DEFAULT_OEF from carpark_detection_skill.strategy import Strategy logger = logging.getLogger("aea.carpark_detection_skill") @@ -118,15 +120,13 @@ def teardown(self) -> None: self.process_id.wait() -class ServiceRegistrationBehaviour(Behaviour): +class ServiceRegistrationBehaviour(TickerBehaviour): """This class implements a behaviour.""" def __init__(self, **kwargs): """Initialise the behaviour.""" - self._services_interval = kwargs.pop('services_interval', 30) # type: int super().__init__(**kwargs) self._last_connection_status = self.context.connection_status.is_connected - self._last_update_time = datetime.datetime.now() # type: datetime.datetime self._registered_service_description = None # type: Optional[Description] self._oef_msf_id = 0 @@ -150,9 +150,8 @@ def act(self) -> None: :return: None """ self._update_connection_status() - if self._is_time_to_update_services(): - self._unregister_service() - self._register_service() + self._unregister_service() + self._register_service() def _register_service(self) -> None: """ @@ -165,12 +164,12 @@ def _register_service(self) -> None: desc = strategy.get_service_description() self._registered_service_description = desc self._oef_msf_id += 1 - msg = OEFMessage(oef_type=OEFMessage.Type.REGISTER_SERVICE, + msg = OEFMessage(type=OEFMessage.Type.REGISTER_SERVICE, id=self._oef_msf_id, service_description=desc, service_id=SERVICE_ID) self.context.outbox.put_message(to=DEFAULT_OEF, - sender=self.context.agent_public_key, + sender=self.context.agent_address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(msg)) logger.info("[{}]: updating car park detection services on OEF.".format(self.context.agent_name)) @@ -183,30 +182,17 @@ def _unregister_service(self) -> None: """ if self._registered_service_description is not None: self._oef_msf_id += 1 - msg = OEFMessage(oef_type=OEFMessage.Type.UNREGISTER_SERVICE, + msg = OEFMessage(type=OEFMessage.Type.UNREGISTER_SERVICE, id=self._oef_msf_id, service_description=self._registered_service_description, service_id=SERVICE_ID) self.context.outbox.put_message(to=DEFAULT_OEF, - sender=self.context.agent_public_key, + sender=self.context.agent_address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(msg)) logger.info("[{}]: unregistering car park detection services from OEF.".format(self.context.agent_name)) self._registered_service_description = None - def _is_time_to_update_services(self) -> bool: - """ - Check if the agent should update the service directory. - - :return: bool indicating the action - """ - now = datetime.datetime.now() - diff = now - self._last_update_time - result = diff.total_seconds() > self._services_interval - if result: - self._last_update_time = now - return result - def _update_connection_status(self) -> None: """ Update the connection status in the db. diff --git a/packages/skills/carpark_detection/carpark_detection_data_model.py b/packages/skills/carpark_detection/carpark_detection_data_model.py index dfdc8600b5..278e83237a 100644 --- a/packages/skills/carpark_detection/carpark_detection_data_model.py +++ b/packages/skills/carpark_detection/carpark_detection_data_model.py @@ -1,7 +1,7 @@ """This package contains the dataModel for the carpark detection agent.""" -from aea.protocols.oef.models import DataModel, Attribute +from aea.helpers.search.models import DataModel, Attribute class CarParkDataModel (DataModel): diff --git a/packages/skills/carpark_detection/dialogues.py b/packages/skills/carpark_detection/dialogues.py index 2d279d027c..455810825d 100644 --- a/packages/skills/carpark_detection/dialogues.py +++ b/packages/skills/carpark_detection/dialogues.py @@ -25,13 +25,18 @@ - Dialogues: The dialogues class keeps track of all dialogues. """ -from typing import Any, Dict, Optional +import sys +from typing import Any, Dict, Optional, TYPE_CHECKING from aea.helpers.dialogue.base import DialogueLabel -from aea.protocols.fipa.dialogues import FIPADialogues, FIPADialogue -from aea.protocols.oef.models import Description +from aea.helpers.search.models import Description from aea.skills.base import SharedClass +if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.fipa.dialogues import FIPADialogues, FIPADialogue +else: + from fipa_protocol.dialogues import FIPADialogues, FIPADialogue + class Dialogue(FIPADialogue): """The dialogue class maintains state of a dialogue and manages it.""" diff --git a/packages/skills/carpark_detection/handlers.py b/packages/skills/carpark_detection/handlers.py index 1a8a38d53b..f4504c8287 100644 --- a/packages/skills/carpark_detection/handlers.py +++ b/packages/skills/carpark_detection/handlers.py @@ -21,21 +21,23 @@ import logging import sys -from typing import Optional, Tuple, cast, TYPE_CHECKING +from typing import Optional, cast, TYPE_CHECKING from aea.configurations.base import ProtocolId +from aea.helpers.search.models import Description, Query from aea.protocols.base import Message from aea.protocols.default.message import DefaultMessage from aea.protocols.default.serialization import DefaultSerializer -from aea.protocols.fipa.message import FIPAMessage -from aea.protocols.fipa.serialization import FIPASerializer -from aea.protocols.oef.models import Description, Query from aea.skills.base import Handler if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.fipa.message import FIPAMessage + from packages.protocols.fipa.serialization import FIPASerializer from packages.skills.carpark_detection.dialogues import Dialogue, Dialogues from packages.skills.carpark_detection.strategy import Strategy else: + from fipa_protocol.message import FIPAMessage + from fipa_protocol.serialization import FIPASerializer from carpark_detection_skill.dialogues import Dialogue, Dialogues from carpark_detection_skill.strategy import Strategy @@ -51,41 +53,38 @@ def setup(self) -> None: """Implement the setup for the handler.""" pass - def handle(self, message: Message, sender: str) -> None: + def handle(self, message: Message) -> None: """ Implement the reaction to a message. :param message: the message - :param sender: the sender :return: None """ # convenience representations fipa_msg = cast(FIPAMessage, message) - msg_performative = FIPAMessage.Performative(fipa_msg.get('performative')) - message_id = cast(int, fipa_msg.get('message_id')) - dialogue_reference = cast(Tuple[str, str], fipa_msg.get('dialogue_reference')) + dialogue_reference = fipa_msg.dialogue_reference # recover dialogue dialogues = cast(Dialogues, self.context.dialogues) - if dialogues.is_belonging_to_registered_dialogue(fipa_msg, sender, self.context.agent_public_key): - dialogue = cast(Dialogue, dialogues.get_dialogue(fipa_msg, sender, self.context.agent_public_key)) + if dialogues.is_belonging_to_registered_dialogue(fipa_msg, self.context.agent_address): + dialogue = cast(Dialogue, dialogues.get_dialogue(fipa_msg, self.context.agent_address)) dialogue.incoming_extend(fipa_msg) - elif dialogues.is_permitted_for_new_dialogue(fipa_msg, sender): - dialogue = cast(Dialogue, dialogues.create_opponent_initiated(sender, dialogue_reference, is_seller=True)) + elif dialogues.is_permitted_for_new_dialogue(fipa_msg): + dialogue = cast(Dialogue, dialogues.create_opponent_initiated(fipa_msg.counterparty, dialogue_reference, is_seller=True)) dialogue.incoming_extend(fipa_msg) else: - self._handle_unidentified_dialogue(fipa_msg, sender) + self._handle_unidentified_dialogue(fipa_msg) return # handle message - if msg_performative == FIPAMessage.Performative.CFP: - self._handle_cfp(fipa_msg, sender, message_id, dialogue) - elif msg_performative == FIPAMessage.Performative.DECLINE: - self._handle_decline(fipa_msg, sender, message_id, dialogue) - elif msg_performative == FIPAMessage.Performative.ACCEPT: - self._handle_accept(fipa_msg, sender, message_id, dialogue) - elif msg_performative == FIPAMessage.Performative.INFORM: - self._handle_inform(fipa_msg, sender, message_id, dialogue) + if fipa_msg.performative == FIPAMessage.Performative.CFP: + self._handle_cfp(fipa_msg, dialogue) + elif fipa_msg.performative == FIPAMessage.Performative.DECLINE: + self._handle_decline(fipa_msg, dialogue) + elif fipa_msg.performative == FIPAMessage.Performative.ACCEPT: + self._handle_accept(fipa_msg, dialogue) + elif fipa_msg.performative == FIPAMessage.Performative.INFORM: + self._handle_inform(fipa_msg, dialogue) def teardown(self) -> None: """ @@ -95,14 +94,13 @@ def teardown(self) -> None: """ pass - def _handle_unidentified_dialogue(self, msg: FIPAMessage, sender: str) -> None: + def _handle_unidentified_dialogue(self, msg: FIPAMessage) -> None: """ Handle an unidentified dialogue. Respond to the sender with a default message containing the appropriate error information. :param msg: the message - :param sender: the sender :return: None """ logger.info("[{}]: unidentified dialogue.".format(self.context.agent_name)) @@ -110,28 +108,26 @@ def _handle_unidentified_dialogue(self, msg: FIPAMessage, sender: str) -> None: error_code=DefaultMessage.ErrorCode.INVALID_DIALOGUE.value, error_msg="Invalid dialogue.", error_data="fipa_message") # FIPASerializer().encode(msg) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=DefaultMessage.protocol_id, message=DefaultSerializer().encode(default_msg)) - def _handle_cfp(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: Dialogue) -> None: + def _handle_cfp(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the CFP. If the CFP matches the supplied services then send a PROPOSE, otherwise send a DECLINE. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ - new_message_id = message_id + 1 - new_target = message_id + new_message_id = msg.message_id + 1 + new_target = msg.message_id logger.info("[{}]: received CFP from sender={}".format(self.context.agent_name, - sender[-5:])) - query = cast(Query, msg.get("query")) + msg.counterparty[-5:])) + query = cast(Query, msg.query) strategy = cast(Strategy, self.context.strategy) if strategy.is_matching_supply(query) and strategy.has_data(): @@ -139,7 +135,7 @@ def _handle_cfp(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: dialogue.carpark_data = carpark_data dialogue.proposal = proposal logger.info("[{}]: sending sender={} a PROPOSE with proposal={}".format(self.context.agent_name, - sender[-5:], + msg.counterparty[-5:], proposal.values)) proposal_msg = FIPAMessage(message_id=new_message_id, target=new_target, @@ -147,79 +143,79 @@ def _handle_cfp(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: performative=FIPAMessage.Performative.PROPOSE, proposal=[proposal]) dialogue.outgoing_extend(proposal_msg) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(proposal_msg)) - strategy.db.set_dialogue_status(str(dialogue.dialogue_label), sender[-5:], "received_cfp", "send_proposal") + strategy.db.set_dialogue_status(str(dialogue.dialogue_label), msg.counterparty[-5:], + "received_cfp", "send_proposal") else: logger.info("[{}]: declined the CFP from sender={}".format(self.context.agent_name, - sender[-5:])) + msg.counterparty[-5:])) decline_msg = FIPAMessage(message_id=new_message_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, target=new_target, performative=FIPAMessage.Performative.DECLINE) dialogue.outgoing_extend(decline_msg) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(decline_msg)) - strategy.db.set_dialogue_status(str(dialogue.dialogue_label), sender[-5:], "received_cfp", "send_no_proposal") + strategy.db.set_dialogue_status(str(dialogue.dialogue_label), msg.counterparty[-5:], + "received_cfp", "send_no_proposal") - def _handle_decline(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: Dialogue) -> None: + def _handle_decline(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the DECLINE. Close the dialogue. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ logger.info("[{}]: received DECLINE from sender={}".format(self.context.agent_name, - sender[-5:])) + msg.counterparty[-5:])) strategy = cast(Strategy, self.context.strategy) - strategy.db.set_dialogue_status(str(dialogue.dialogue_label), sender[-5:], "received_decline", "[NONE]") + strategy.db.set_dialogue_status(str(dialogue.dialogue_label), msg.counterparty[-5:], + "received_decline", "[NONE]") # dialogues = cast(Dialogues, self.context.dialogues) # dialogues.dialogue_stats.add_dialogue_endstate(Dialogue.EndState.DECLINED_PROPOSE) - def _handle_accept(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: Dialogue) -> None: + def _handle_accept(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the ACCEPT. - Respond with a MATCH_ACCEPT_W_ADDRESS which contains the address to send the funds to. + Respond with a MATCH_ACCEPT_W_INFORM which contains the address to send the funds to. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ - new_message_id = message_id + 1 - new_target = message_id + new_message_id = msg.message_id + 1 + new_target = msg.message_id logger.info("[{}]: received ACCEPT from sender={}".format(self.context.agent_name, - sender[-5:])) - logger.info("[{}]: sending MATCH_ACCEPT_W_ADDRESS to sender={}".format(self.context.agent_name, - sender[-5:])) + msg.counterparty[-5:])) + logger.info("[{}]: sending MATCH_ACCEPT_W_INFORM to sender={}".format(self.context.agent_name, + msg.counterparty[-5:])) match_accept_msg = FIPAMessage(message_id=new_message_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, target=new_target, - performative=FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS, - address=self.context.agent_addresses['fetchai']) + performative=FIPAMessage.Performative.MATCH_ACCEPT_W_INFORM, + info={"address": self.context.agent_addresses['fetchai']}) dialogue.outgoing_extend(match_accept_msg) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(match_accept_msg)) strategy = cast(Strategy, self.context.strategy) - strategy.db.set_dialogue_status(str(dialogue.dialogue_label), sender[-5:], "received_accept", "send_match_accept") + strategy.db.set_dialogue_status(str(dialogue.dialogue_label), msg.counterparty[-5:], + "received_accept", "send_match_accept") - def _handle_inform(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: Dialogue) -> None: + def _handle_inform(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the INFORM. @@ -227,17 +223,15 @@ def _handle_inform(self, msg: FIPAMessage, sender: str, message_id: int, dialogu If the transaction is settled send the weather data, otherwise do nothing. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ - new_message_id = message_id + 1 - new_target = message_id + new_message_id = msg.message_id + 1 + new_target = msg.message_id logger.info("[{}]: received INFORM from sender={}".format(self.context.agent_name, - sender[-5:])) + msg.counterparty[-5:])) - json_data = cast(dict, msg.get("json_data")) + json_data = msg.info if "transaction_digest" in json_data.keys(): tx_digest = json_data['transaction_digest'] logger.info("[{}]: checking whether transaction={} has been received ...".format(self.context.agent_name, @@ -257,30 +251,30 @@ def _handle_inform(self, msg: FIPAMessage, sender: str, message_id: int, dialogu self.context.agent_name, tx_digest, token_balance, - sender[-5:])) + msg.counterparty[-5:])) inform_msg = FIPAMessage(message_id=new_message_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, target=new_target, performative=FIPAMessage.Performative.INFORM, - json_data=dialogue.carpark_data) + info=dialogue.carpark_data) dialogue.outgoing_extend(inform_msg) - # import pdb; pdb.set_trace() - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(inform_msg)) # dialogues = cast(Dialogues, self.context.dialogues) # dialogues.dialogue_stats.add_dialogue_endstate(Dialogue.EndState.SUCCESSFUL) strategy.db.add_in_progress_transaction( tx_digest, - sender[-5:], + msg.counterparty[-5:], self.context.agent_name, total_price) strategy.db.set_transaction_complete(tx_digest) - strategy.db.set_dialogue_status(str(dialogue.dialogue_label), sender[-5:], "transaction_complete", "send_request_data") + strategy.db.set_dialogue_status(str(dialogue.dialogue_label), msg.counterparty[-5:], + "transaction_complete", "send_request_data") else: logger.info("[{}]: transaction={} not settled, aborting".format(self.context.agent_name, tx_digest)) else: logger.info("[{}]: did not receive transaction digest from sender={}.".format(self.context.agent_name, - sender[-5:])) + msg.counterparty[-5:])) diff --git a/packages/skills/carpark_detection/skill.yaml b/packages/skills/carpark_detection/skill.yaml index b5c0d46825..7885bdde0b 100644 --- a/packages/skills/carpark_detection/skill.yaml +++ b/packages/skills/carpark_detection/skill.yaml @@ -1,36 +1,37 @@ name: carpark_detection -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" behaviours: - - behaviour: - class_name: ServiceRegistrationBehaviour - args: {} - - behaviour: - class_name: CarParkDetectionAndGUIBehaviour - args: - default_longitude: -73.967491 - default_latitude: 40.780343 - image_capture_interval: 120 - + service_registration: + class_name: ServiceRegistrationBehaviour + args: {} + car_park_detection: + class_name: CarParkDetectionAndGUIBehaviour + args: + default_longitude: -73.967491 + default_latitude: 40.780343 + image_capture_interval: 120 handlers: - - handler: - class_name: FIPAHandler - args: {} -tasks: [] + fipa: + class_name: FIPAHandler + args: {} +tasks: {} shared_classes: - - shared_class: - class_name: Strategy - args: - data_price_fet: 200000000 - db_is_rel_to_cwd: true - db_rel_dir: ../temp_files - - - shared_class: - class_name: Dialogues - args: {} + strategy: + class_name: Strategy + args: + data_price_fet: 200000000 + db_is_rel_to_cwd: true + db_rel_dir: ../temp_files + currency_id: 'FET' + ledger_id: 'fetchai' + dialogues: + class_name: Dialogues + args: {} protocols: ['fipa', 'oef', 'default'] ledgers: ['fetchai'] dependencies: - - scikit-image + scikit-image: {} diff --git a/packages/skills/carpark_detection/strategy.py b/packages/skills/carpark_detection/strategy.py index 8be5bb4de3..aa14993356 100644 --- a/packages/skills/carpark_detection/strategy.py +++ b/packages/skills/carpark_detection/strategy.py @@ -25,7 +25,7 @@ from typing import Any, Dict, List, Tuple, TYPE_CHECKING, cast import time -from aea.protocols.oef.models import Description, Query +from aea.helpers.search.models import Description, Query from aea.skills.base import SharedClass if TYPE_CHECKING or "pytest" in sys.modules: @@ -39,6 +39,8 @@ DEFAULT_PRICE = 2000 DEFAULT_DB_IS_REL_TO_CWD = False DEFAULT_DB_REL_DIR = "temp_files_placeholder" +DEFAULT_CURRENCY_ID = 'FET' +DEFAULT_LEDGER_ID = 'fetchai' logger = logging.getLogger("aea.carpark_detection_skill") @@ -64,6 +66,8 @@ def __init__(self, **kwargs) -> None: db_dir = os.path.join(os.path.dirname(__file__), DEFAULT_DB_REL_DIR) self.data_price_fet = kwargs.pop('data_price_fet') if 'data_price_fet' in kwargs.keys() else DEFAULT_PRICE + self.currency_id = kwargs.pop('currency_id') if 'currency_id' in kwargs.keys() else DEFAULT_CURRENCY_ID + self.ledger_id = kwargs.pop('ledger_id') if 'ledger_id' in kwargs.keys() else DEFAULT_LEDGER_ID super().__init__(**kwargs) self.db = DetectionDatabase(db_dir, False) @@ -105,7 +109,7 @@ def get_service_description(self) -> Description: { "latitude": lat, "longitude": lon, - "unique_id": self.context.agent_public_key + "unique_id": self.context.agent_address }, data_model=CarParkDataModel() ) @@ -151,6 +155,8 @@ def generate_proposal_and_data(self, query: Query) -> Tuple[Description, Dict[st "lat": data[0]["lat"], "lon": data[0]["lon"], "price": self.data_price_fet, + "currency_id": self.currency_id, + "ledger_id": self.ledger_id, "last_detection_time": last_detection_time, "max_spaces": max_spaces, }) diff --git a/packages/skills/echo/handlers.py b/packages/skills/echo/handlers.py index 06447d326b..ab070d7a0f 100644 --- a/packages/skills/echo/handlers.py +++ b/packages/skills/echo/handlers.py @@ -42,16 +42,15 @@ def setup(self) -> None: """Set up the handler.""" logger.info("Echo Handler: setup method called.") - def handle(self, message: Message, sender: str) -> None: + def handle(self, message: Message) -> None: """ Handle the message. :param message: the message. - :param sender: the sender. :return: None """ - logger.info("Echo Handler: message={}, sender={}".format(message, sender)) - self.context.outbox.put_message(to=sender, sender=self.context.agent_name, protocol_id="default", + logger.info("Echo Handler: message={}, sender={}".format(message, message.counterparty)) + self.context.outbox.put_message(to=message.counterparty, sender=self.context.agent_name, protocol_id="default", message=DefaultSerializer().encode(message)) def teardown(self) -> None: diff --git a/packages/skills/echo/skill.yaml b/packages/skills/echo/skill.yaml index 57b568ac77..093c906086 100644 --- a/packages/skills/echo/skill.yaml +++ b/packages/skills/echo/skill.yaml @@ -1,26 +1,27 @@ name: echo -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" description: "The echo skill implements simple echo functionality." url: "" behaviours: - - behaviour: - class_name: EchoBehaviour - args: - foo: bar + echo: + class_name: EchoBehaviour + args: + foo: bar handlers: - - handler: - class_name: EchoHandler - args: - foo: bar - bar: foo + echo: + class_name: EchoHandler + args: + foo: bar + bar: foo tasks: - - task: - class_name: EchoTask - args: - foo: bar - bar: foo -shared_classes: [] + echo: + class_name: EchoTask + args: + foo: bar + bar: foo +shared_classes: {} protocols: ["default"] -dependencies: [] +dependencies: {} diff --git a/packages/skills/gym/handlers.py b/packages/skills/gym/handlers.py index eb34b00900..0bacf41fd5 100644 --- a/packages/skills/gym/handlers.py +++ b/packages/skills/gym/handlers.py @@ -49,23 +49,20 @@ def setup(self) -> None: """Set up the handler.""" logger.info("Gym handler: setup method called.") - def handle(self, message: Message, sender: str) -> None: + def handle(self, message: Message) -> None: """ Handle messages. :param message: the message - :param sender: the sender :return: None """ gym_msg = cast(GymMessage, message) - gym_msg_performative = GymMessage.Performative(gym_msg.get("performative")) - if gym_msg_performative == GymMessage.Performative.PERCEPT: + if gym_msg.performative == GymMessage.Performative.PERCEPT: assert self.context.tasks is not None, "Incorrect initialization." - assert len(self.context.tasks) == 1, "Too many tasks loaded!" - gym_task = cast(GymTask, self.context.tasks[0]) + gym_task = cast(GymTask, self.context.tasks.gym) gym_task.proxy_env_queue.put(gym_msg) else: - raise ValueError("Unexpected performative or no step_id: {}".format(gym_msg_performative)) + raise ValueError("Unexpected performative or no step_id: {}".format(gym_msg.performative)) def teardown(self) -> None: """ diff --git a/packages/skills/gym/helpers.py b/packages/skills/gym/helpers.py index b4c5bcd10c..0eb3e6b981 100644 --- a/packages/skills/gym/helpers.py +++ b/packages/skills/gym/helpers.py @@ -99,11 +99,10 @@ def step(self, action: Action) -> Feedback: # Wait (blocking!) for the response envelope from the environment gym_msg = self._queue.get(block=True, timeout=None) # type: GymMessage - gym_msg_step_id = gym_msg.get("step_id") - if gym_msg_step_id == step_id: + if gym_msg.step_id == step_id: observation, reward, done, info = self._message_to_percept(gym_msg) else: - raise ValueError("Unexpected step id! expected={}, actual={}".format(step_id, gym_msg_step_id)) + raise ValueError("Unexpected step id! expected={}, actual={}".format(step_id, gym_msg.step_id)) return observation, reward, done, info @@ -125,7 +124,7 @@ def reset(self) -> None: self._is_rl_agent_trained = False gym_msg = GymMessage(performative=GymMessage.Performative.RESET) gym_bytes = GymSerializer().encode(gym_msg) - envelope = Envelope(to=DEFAULT_GYM, sender=self._skill_context.agent_public_key, protocol_id=GymMessage.protocol_id, + envelope = Envelope(to=DEFAULT_GYM, sender=self._skill_context.agent_address, protocol_id=GymMessage.protocol_id, message=gym_bytes) self._skill_context.outbox.put(envelope) @@ -138,7 +137,7 @@ def close(self) -> None: self._is_rl_agent_trained = True gym_msg = GymMessage(performative=GymMessage.Performative.CLOSE) gym_bytes = GymSerializer().encode(gym_msg) - envelope = Envelope(to=DEFAULT_GYM, sender=self._skill_context.agent_public_key, protocol_id=GymMessage.protocol_id, + envelope = Envelope(to=DEFAULT_GYM, sender=self._skill_context.agent_address, protocol_id=GymMessage.protocol_id, message=gym_bytes) self._skill_context.outbox.put(envelope) @@ -152,7 +151,7 @@ def _encode_action(self, action: Action, step_id: int) -> Envelope: """ gym_msg = GymMessage(performative=GymMessage.Performative.ACT, action=action, step_id=step_id) gym_bytes = GymSerializer().encode(gym_msg) - envelope = Envelope(to=DEFAULT_GYM, sender=self._skill_context.agent_public_key, protocol_id=GymMessage.protocol_id, + envelope = Envelope(to=DEFAULT_GYM, sender=self._skill_context.agent_address, protocol_id=GymMessage.protocol_id, message=gym_bytes) return envelope @@ -163,10 +162,11 @@ def _message_to_percept(self, message: Message) -> Feedback: :param: the message received as a response to the action performed in apply_action. :return: the standard feedback (observation, reward, done, info) of a gym environment. """ - observation = cast(Any, message.get("observation")) - reward = cast(float, message.get("reward")) - done = cast(bool, message.get("done")) - info = cast(dict, message.get("info")) + msg = cast(GymMessage, message) + observation = msg.observation + reward = msg.reward + done = msg.done + info = msg.info return observation, reward, done, info diff --git a/packages/skills/gym/skill.yaml b/packages/skills/gym/skill.yaml index 024da2a961..a1b678e616 100644 --- a/packages/skills/gym/skill.yaml +++ b/packages/skills/gym/skill.yaml @@ -1,21 +1,22 @@ name: gym -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" description: "The gym skill wraps an RL agent." url: "" -behaviours: [] +behaviours: {} handlers: - - handler: - class_name: GymHandler - args: - foo: bar + gym: + class_name: GymHandler + args: + foo: bar tasks: - - task: - class_name: GymTask - args: - nb_steps: 4000 -shared_classes: [] + gym: + class_name: GymTask + args: + nb_steps: 4000 +shared_classes: {} protocols: ["gym"] dependencies: - - gym + gym: {} diff --git a/packages/skills/ml_data_provider/__init__.py b/packages/skills/ml_data_provider/__init__.py new file mode 100644 index 0000000000..ed15c864a9 --- /dev/null +++ b/packages/skills/ml_data_provider/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the implementation of the ml train and predict skill.""" diff --git a/packages/skills/ml_data_provider/behaviours.py b/packages/skills/ml_data_provider/behaviours.py new file mode 100644 index 0000000000..1123eba831 --- /dev/null +++ b/packages/skills/ml_data_provider/behaviours.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains the behaviours.""" + +import logging +import sys +from typing import cast, Optional, TYPE_CHECKING + +from aea.crypto.ethereum import ETHEREUM +from aea.crypto.fetchai import FETCHAI +from aea.helpers.search.models import Description +from aea.skills.behaviours import TickerBehaviour + +if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.oef.message import OEFMessage + from packages.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF + from packages.skills.ml_data_provider.strategy import Strategy +else: + from oef_protocol.message import OEFMessage + from oef_protocol.serialization import OEFSerializer, DEFAULT_OEF + from ml_data_provider_skill.strategy import Strategy + +logger = logging.getLogger("aea.ml_data_provider") + +SERVICE_ID = '' +DEFAULT_SERVICES_INTERVAL = 30.0 + + +class ServiceRegistrationBehaviour(TickerBehaviour): + """This class implements a behaviour.""" + + def __init__(self, **kwargs): + """Initialise the behaviour.""" + services_interval = kwargs.pop('services_interval', DEFAULT_SERVICES_INTERVAL) # type: int + super().__init__(tick_interval=services_interval, **kwargs) + self._registered_service_description = None # type: Optional[Description] + + def setup(self) -> None: + """ + Implement the setup. + + :return: None + """ + if self.context.ledger_apis.has_fetchai: + fet_balance = self.context.ledger_apis.token_balance(FETCHAI, cast(str, self.context.agent_addresses.get(FETCHAI))) + if fet_balance > 0: + logger.info("[{}]: starting balance on fetchai ledger={}.".format(self.context.agent_name, fet_balance)) + else: + logger.warning("[{}]: you have no starting balance on fetchai ledger!".format(self.context.agent_name)) + + if self.context.ledger_apis.has_ethereum: + eth_balance = self.context.ledger_apis.token_balance(ETHEREUM, cast(str, self.context.agent_addresses.get(ETHEREUM))) + if eth_balance > 0: + logger.info("[{}]: starting balance on ethereum ledger={}.".format(self.context.agent_name, eth_balance)) + else: + logger.warning("[{}]: you have no starting balance on ethereum ledger!".format(self.context.agent_name)) + + self._register_service() + + def act(self) -> None: + """ + Implement the act. + + :return: None + """ + self._unregister_service() + self._register_service() + + def teardown(self) -> None: + """ + Implement the task teardown. + + :return: None + """ + if self.context.ledger_apis.has_fetchai: + balance = self.context.ledger_apis.token_balance(FETCHAI, cast(str, self.context.agent_addresses.get(FETCHAI))) + logger.info("[{}]: ending balance on fetchai ledger={}.".format(self.context.agent_name, balance)) + + if self.context.ledger_apis.has_ethereum: + balance = self.context.ledger_apis.token_balance(ETHEREUM, cast(str, self.context.agent_addresses.get(ETHEREUM))) + logger.info("[{}]: ending balance on ethereum ledger={}.".format(self.context.agent_name, balance)) + + self._unregister_service() + + def _register_service(self) -> None: + """ + Register to the OEF Service Directory. + + :return: None + """ + strategy = cast(Strategy, self.context.strategy) + desc = strategy.get_service_description() + self._registered_service_description = desc + oef_msg_id = strategy.get_next_oef_msg_id() + msg = OEFMessage(type=OEFMessage.Type.REGISTER_SERVICE, + id=oef_msg_id, + service_description=desc, + service_id=SERVICE_ID) + self.context.outbox.put_message(to=DEFAULT_OEF, + sender=self.context.agent_address, + protocol_id=OEFMessage.protocol_id, + message=OEFSerializer().encode(msg)) + logger.info("[{}]: updating ml data provider service on OEF.".format(self.context.agent_name)) + + def _unregister_service(self) -> None: + """ + Unregister service from OEF Service Directory. + + :return: None + """ + strategy = cast(Strategy, self.context.strategy) + oef_msg_id = strategy.get_next_oef_msg_id() + msg = OEFMessage(type=OEFMessage.Type.UNREGISTER_SERVICE, + id=oef_msg_id, + service_description=self._registered_service_description, + service_id=SERVICE_ID) + self.context.outbox.put_message(to=DEFAULT_OEF, + sender=self.context.agent_address, + protocol_id=OEFMessage.protocol_id, + message=OEFSerializer().encode(msg)) + logger.info("[{}]: unregistering ml data provider service from OEF.".format(self.context.agent_name)) + self._registered_service_description = None diff --git a/packages/skills/ml_data_provider/handlers.py b/packages/skills/ml_data_provider/handlers.py new file mode 100644 index 0000000000..4b5099b2a7 --- /dev/null +++ b/packages/skills/ml_data_provider/handlers.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the handler for the 'ml_data_provider' skill.""" +import logging +import sys +from typing import cast, TYPE_CHECKING + +from aea.protocols.base import Message +from aea.skills.base import Handler + +if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.ml_trade.message import MLTradeMessage + from packages.protocols.ml_trade.serialization import MLTradeSerializer + from packages.skills.ml_data_provider.strategy import Strategy +else: + from ml_trade_protocol.message import MLTradeMessage + from ml_trade_protocol.serialization import MLTradeSerializer + from ml_data_provider_skill.strategy import Strategy + +logger = logging.getLogger("aea.ml_data_provider") + + +class MLTradeHandler(Handler): + """ML trade handler.""" + + SUPPORTED_PROTOCOL = "ml_trade" + + def __init__(self, **kwargs): + """Initialize the handler.""" + super().__init__(**kwargs) + + def setup(self) -> None: + """Set up the handler.""" + logger.debug("MLTrade handler: setup method called.") + + def handle(self, message: Message) -> None: + """ + Handle messages. + + :param message: the message + :return: None + """ + ml_msg = cast(MLTradeMessage, message) + if ml_msg.performative == MLTradeMessage.Performative.CFT: + self._handle_cft(ml_msg) + elif ml_msg.performative == MLTradeMessage.Performative.ACCEPT: + self._handle_accept(ml_msg) + + def _handle_cft(self, ml_trade_msg: MLTradeMessage) -> None: + """ + Handle call for terms. + + :param ml_trade_msg: the ml trade message + :return: None + """ + query = ml_trade_msg.query + logger.info("Got a Call for Terms from {}: query={}".format(ml_trade_msg.counterparty[-5:], query)) + strategy = cast(Strategy, self.context.strategy) + if not strategy.is_matching_supply(query): + return + terms = strategy.generate_terms() + logger.info("[{}]: sending to the address={} a Terms message: {}" + .format(self.context.agent_name, ml_trade_msg.counterparty[-5:], terms.values)) + terms_msg = MLTradeMessage(performative=MLTradeMessage.Performative.TERMS, terms=terms) + self.context.outbox.put_message(to=ml_trade_msg.counterparty, + sender=self.context.agent_address, + protocol_id=MLTradeMessage.protocol_id, + message=MLTradeSerializer().encode(terms_msg)) + + def _handle_accept(self, ml_trade_msg: MLTradeMessage) -> None: + """ + Handle accept. + + :param ml_trade_msg: the ml trade message + :return: None + """ + terms = ml_trade_msg.terms + logger.info("Got an Accept from {}: {}".format(ml_trade_msg.counterparty[-5:], terms.values)) + strategy = cast(Strategy, self.context.strategy) + if not strategy.is_valid_terms(terms): + return + batch_size = terms.values["batch_size"] + data = strategy.sample_data(batch_size) + logger.info("[{}]: sending to address={} a Data message: shape={}" + .format(self.context.agent_name, ml_trade_msg.counterparty[-5:], data[0].shape)) + data_msg = MLTradeMessage(performative=MLTradeMessage.Performative.DATA, terms=terms, data=data) + self.context.outbox.put_message(to=ml_trade_msg.counterparty, + sender=self.context.agent_address, + protocol_id=MLTradeMessage.protocol_id, + message=MLTradeSerializer().encode(data_msg)) + + def teardown(self) -> None: + """ + Teardown the handler. + + :return: None + """ + logger.debug("MLTrade handler: teardown method called.") diff --git a/packages/skills/ml_data_provider/skill.yaml b/packages/skills/ml_data_provider/skill.yaml new file mode 100644 index 0000000000..d7fe08a678 --- /dev/null +++ b/packages/skills/ml_data_provider/skill.yaml @@ -0,0 +1,32 @@ +name: 'ml_data_provider' +author: fetchai +version: 0.1.0 +license: Apache 2.0 +fingerprint: "" +description: "The ml data provider skill implements a provider for Machine Learning datasets in order to monetize data." +url: "" +behaviours: + service_registration: + class_name: ServiceRegistrationBehaviour + args: + services_interval: 30 +handlers: + ml_trade: + class_name: MLTradeHandler + args: {} +tasks: {} +shared_classes: + strategy: + class_name: Strategy + args: + price_per_data_batch: 100 + batch_size: 2 + seller_tx_fee: 0 + buyer_tx_fee: 10 + dataset_id: 'fmnist' + currency_id: 'FET' + ledger_id: 'fetchai' +protocols: ['oef', 'ml_trade'] +dependencies: + tensorflow: {} + numpy: {} diff --git a/packages/skills/ml_data_provider/strategy.py b/packages/skills/ml_data_provider/strategy.py new file mode 100644 index 0000000000..16d79c8a11 --- /dev/null +++ b/packages/skills/ml_data_provider/strategy.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the strategy class.""" + +import numpy as np +from tensorflow import keras + +from aea.helpers.search.models import Attribute, DataModel, Description, Query +from aea.skills.base import SharedClass + +DEFAULT_PRICE_PER_DATA_BATCH = 10 +DEFAULT_DATASET_ID = "fmnist" +DEFAULT_BATCH_SIZE = 32 +DEFAULT_SELLER_TX_FEE = 0 +DEFAULT_BUYER_TX_FEE = 0 +DEFAULT_CURRENCY_PBK = 'FET' +DEFAULT_LEDGER_ID = 'fetchai' + + +class Strategy(SharedClass): + """This class defines a strategy for the agent.""" + + def __init__(self, **kwargs) -> None: + """Initialize the strategy of the agent.""" + self.price_per_data_batch = kwargs.pop('price_per_data_batch', DEFAULT_PRICE_PER_DATA_BATCH) + self.batch_size = kwargs.pop('batch_size', DEFAULT_BATCH_SIZE) + self.dataset_id = kwargs.pop('dataset_id', DEFAULT_DATASET_ID) + self.seller_tx_fee = kwargs.pop('seller_tx_fee', DEFAULT_SELLER_TX_FEE) + self.buyer_tx_fee = kwargs.pop('buyer_tx_fee', DEFAULT_BUYER_TX_FEE) + self.currency_id = kwargs.pop('currency_id', DEFAULT_CURRENCY_PBK) + self.ledger_id = kwargs.pop('ledger_id', DEFAULT_LEDGER_ID) + super().__init__(**kwargs) + self._oef_msg_id = 0 + + # loading ML dataset + # TODO this should be parametrized + (self.train_x, self.train_y), (self.test_x, self.test_y) = keras.datasets.fashion_mnist.load_data() + + def get_next_oef_msg_id(self) -> int: + """ + Get the next oef msg id. + + :return: the next oef msg id + """ + self._oef_msg_id += 1 + return self._oef_msg_id + + def get_service_description(self) -> Description: + """ + Get the service description. + + :return: a description of the offered services + """ + dm = DataModel("ml_datamodel", [Attribute("dataset_id", str, True)]) + desc = Description({'dataset_id': self.dataset_id}, data_model=dm) + return desc + + def sample_data(self, n: int): + """Sample N rows from data.""" + idx = np.arange(self.train_x.shape[0]) + mask = np.zeros_like(idx, dtype=bool) + + selected = np.random.choice(idx, n, replace=False) + mask[selected] = True + + x_sample = self.train_x[mask] + y_sample = self.train_y[mask] + return x_sample, y_sample + + def is_matching_supply(self, query: Query) -> bool: + """ + Check if the query matches the supply. + + :param query: the query + :return: bool indiciating whether matches or not + """ + service_desc = self.get_service_description() + return query.check(service_desc) + + def generate_terms(self) -> Description: + """ + Generate a proposal. + + :return: a tuple of proposal and the weather data + """ + address = self.context.agent_addresses[self.ledger_id] + proposal = Description({"batch_size": self.batch_size, + "price": self.price_per_data_batch, + "seller_tx_fee": self.seller_tx_fee, + "buyer_tx_fee": self.buyer_tx_fee, + "currency_id": self.currency_id, + "ledger_id": self.ledger_id, + "address": address}) + return proposal + + def is_valid_terms(self, terms: Description) -> bool: + """ + Check the terms are valid. + + :param terms: the terms + :return: boolean + """ + return terms == self.generate_terms() diff --git a/packages/skills/ml_train/__init__.py b/packages/skills/ml_train/__init__.py new file mode 100644 index 0000000000..ed15c864a9 --- /dev/null +++ b/packages/skills/ml_train/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the implementation of the ml train and predict skill.""" diff --git a/packages/skills/ml_train/behaviours.py b/packages/skills/ml_train/behaviours.py new file mode 100644 index 0000000000..bb39b0dfb0 --- /dev/null +++ b/packages/skills/ml_train/behaviours.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains a the behaviours.""" + +import logging +import sys +from typing import cast, TYPE_CHECKING + +from aea.crypto.ethereum import ETHEREUM +from aea.crypto.fetchai import FETCHAI +from aea.skills.behaviours import TickerBehaviour + +if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.oef.message import OEFMessage + from packages.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF + from packages.skills.ml_train.strategy import Strategy +else: + from oef_protocol.message import OEFMessage + from oef_protocol.serialization import OEFSerializer, DEFAULT_OEF + from ml_train_skill.strategy import Strategy + +logger = logging.getLogger("aea.ml_train_skill") + +SERVICE_ID = '' +DEFAULT_SEARCH_INTERVAL = 5.0 + + +class MySearchBehaviour(TickerBehaviour): + """This behaviour searches for data to buy.""" + + def __init__(self, **kwargs): + """Initialize the search behaviour.""" + search_interval = kwargs.pop('search_interval', DEFAULT_SEARCH_INTERVAL) + super().__init__(tick_interval=search_interval, **kwargs) + + def setup(self) -> None: + """ + Implement the setup for the behaviour. + + :return: None + """ + if self.context.ledger_apis.has_fetchai: + fet_balance = self.context.ledger_apis.token_balance(FETCHAI, cast(str, self.context.agent_addresses.get(FETCHAI))) + if fet_balance > 0: + logger.info("[{}]: starting balance on fetchai ledger={}.".format(self.context.agent_name, fet_balance)) + else: + logger.warning("[{}]: you have no starting balance on fetchai ledger!".format(self.context.agent_name)) + + if self.context.ledger_apis.has_ethereum: + eth_balance = self.context.ledger_apis.token_balance(ETHEREUM, cast(str, self.context.agent_addresses.get(ETHEREUM))) + if eth_balance > 0: + logger.info("[{}]: starting balance on ethereum ledger={}.".format(self.context.agent_name, eth_balance)) + else: + logger.warning("[{}]: you have no starting balance on ethereum ledger!".format(self.context.agent_name)) + + def act(self) -> None: + """ + Implement the act. + + :return: None + """ + strategy = cast(Strategy, self.context.strategy) + if strategy.is_searching: + query = strategy.get_service_query() + search_id = strategy.get_next_search_id() + oef_msg = OEFMessage(type=OEFMessage.Type.SEARCH_SERVICES, + id=search_id, + query=query) + self.context.outbox.put_message(to=DEFAULT_OEF, + sender=self.context.agent_address, + protocol_id=OEFMessage.protocol_id, + message=OEFSerializer().encode(oef_msg)) + + def teardown(self) -> None: + """ + Implement the task teardown. + + :return: None + """ + if self.context.ledger_apis.has_fetchai: + balance = self.context.ledger_apis.token_balance(FETCHAI, cast(str, self.context.agent_addresses.get(FETCHAI))) + logger.info("[{}]: ending balance on fetchai ledger={}.".format(self.context.agent_name, balance)) + + if self.context.ledger_apis.has_ethereum: + balance = self.context.ledger_apis.token_balance(ETHEREUM, cast(str, self.context.agent_addresses.get(ETHEREUM))) + logger.info("[{}]: ending balance on ethereum ledger={}.".format(self.context.agent_name, balance)) diff --git a/packages/skills/ml_train/handlers.py b/packages/skills/ml_train/handlers.py new file mode 100644 index 0000000000..885cf09533 --- /dev/null +++ b/packages/skills/ml_train/handlers.py @@ -0,0 +1,245 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the handler for the 'ml_train' skill.""" +import logging +import sys +from typing import cast, TYPE_CHECKING, Optional, List + +from aea.configurations.base import ProtocolId +from aea.decision_maker.messages.transaction import TransactionMessage +from aea.helpers.search.models import Description +from aea.protocols.base import Message +from aea.skills.base import Handler + +if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.oef.message import OEFMessage + from packages.protocols.ml_trade.message import MLTradeMessage + from packages.protocols.ml_trade.serialization import MLTradeSerializer + from packages.skills.ml_train.strategy import Strategy + from packages.skills.ml_train.tasks import MLTrainTask + # from packages.skills.ml_train.tasks import MLTask +else: + from oef_protocol.message import OEFMessage + from ml_trade_protocol.message import MLTradeMessage + from ml_trade_protocol.serialization import MLTradeSerializer + from ml_train_skill.strategy import Strategy + from ml_train_skill.tasks import MLTrainTask + +logger = logging.getLogger("aea.ml_train_skill") + +DUMMY_DIGEST = 'dummy_digest' + + +class TrainHandler(Handler): + """Train handler.""" + + SUPPORTED_PROTOCOL = "ml_trade" + + def __init__(self, **kwargs): + """Initialize the handler.""" + super().__init__(**kwargs) + + def setup(self) -> None: + """ + Set up the handler. + + :return: None + """ + logger.debug("Train handler: setup method called.") + + def handle(self, message: Message) -> None: + """ + Handle messages. + + :param message: the message + :param sender: the sender + :return: None + """ + ml_msg = cast(MLTradeMessage, message) + if ml_msg.performative == MLTradeMessage.Performative.TERMS: + self._handle_terms(ml_msg) + elif ml_msg.performative == MLTradeMessage.Performative.DATA: + self._handle_data(ml_msg) + + def _handle_terms(self, ml_trade_msg: MLTradeMessage) -> None: + """ + Handle the terms of the request. + + :param ml_trade_msg: the ml trade message + :return: None + """ + terms = ml_trade_msg.terms + logger.info("Received terms message from {}: terms={}".format(ml_trade_msg.counterparty[-5:], terms.values)) + + strategy = cast(Strategy, self.context.strategy) + acceptable = strategy.is_acceptable_terms(terms) + affordable = strategy.is_affordable_terms(terms) + if not acceptable and affordable: + logger.info("[{}]: rejecting, terms are not acceptable and/or affordable".format(self.context.agent_name)) + return + + if strategy.is_ledger_tx: + # propose the transaction to the decision maker for settlement on the ledger + tx_msg = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE_FOR_SETTLEMENT, + skill_callback_ids=['ml_train'], + tx_id=strategy.get_next_transition_id(), + tx_sender_addr=self.context.agent_addresses[terms.values["ledger_id"]], + tx_counterparty_addr=terms.values["address"], + tx_amount_by_currency_id={terms.values['currency_id']: - terms.values["price"]}, + tx_sender_fee=terms.values["buyer_tx_fee"], + tx_counterparty_fee=terms.values["seller_tx_fee"], + tx_quantities_by_good_id={}, + ledger_id=terms.values["ledger_id"], + info={'terms': terms, 'counterparty_addr': ml_trade_msg.counterparty}) # this is used to send the terms later - because the seller is stateless and must know what terms have been accepted + self.context.decision_maker_message_queue.put_nowait(tx_msg) + logger.info("[{}]: proposing the transaction to the decision maker. Waiting for confirmation ...".format(self.context.agent_name)) + else: + # accept directly with a dummy transaction digest, no settlement + ml_accept = MLTradeMessage(performative=MLTradeMessage.Performative.ACCEPT, + tx_digest=DUMMY_DIGEST, + terms=terms) + self.context.outbox.put_message(to=ml_trade_msg.counterparty, + sender=self.context.agent_address, + protocol_id=MLTradeMessage.protocol_id, + message=MLTradeSerializer().encode(ml_accept)) + logger.info("[{}]: sending dummy transaction digest ...".format(self.context.agent_name)) + + def _handle_data(self, ml_trade_msg: MLTradeMessage) -> None: + """ + Handle the data. + + :param ml_trade_msg: the ml trade message + :return: None + """ + terms = ml_trade_msg.terms + data = ml_trade_msg.data + if data is None: + logger.info("Received data message with no data from {}".format(ml_trade_msg.counterparty[-5:])) + else: + logger.info("Received data message from {}: data shape={}, terms={}".format(ml_trade_msg.counterparty[-5:], + data[0].shape, terms.values)) + training_task = MLTrainTask(data, skill_context=self.context) + self.context.task_queue.put(training_task) + + def teardown(self) -> None: + """ + Teardown the handler. + + :return: None + """ + logger.debug("Train handler: teardown method called.") + + +class OEFHandler(Handler): + """This class scaffolds a handler.""" + + SUPPORTED_PROTOCOL = OEFMessage.protocol_id # type: Optional[ProtocolId] + + def setup(self) -> None: + """Call to setup the handler.""" + pass + + def handle(self, message: Message) -> None: + """ + Implement the reaction to a message. + + :param message: the message + :return: None + """ + # convenience representations + oef_msg = cast(OEFMessage, message) + + if oef_msg.type is OEFMessage.Type.SEARCH_RESULT: + agents = oef_msg.agents + self._handle_search(agents) + + def teardown(self) -> None: + """ + Implement the handler teardown. + + :return: None + """ + pass + + def _handle_search(self, agents: List[str]) -> None: + """ + Handle the search response. + + :param agents: the agents returned by the search + :return: None + """ + if len(agents) == 0: + logger.info("[{}]: found no agents, continue searching.".format(self.context.agent_name)) + return + + logger.info("[{}]: found agents={}, stopping search.".format(self.context.agent_name, list(map(lambda x: x[-5:], agents)))) + strategy = cast(Strategy, self.context.strategy) + strategy.is_searching = False + query = strategy.get_service_query() + for opponent_address in agents: + logger.info("[{}]: sending CFT to agent={}".format(self.context.agent_name, opponent_address[-5:])) + cft_msg = MLTradeMessage(performative=MLTradeMessage.Performative.CFT, query=query) + self.context.outbox.put_message(to=opponent_address, + sender=self.context.agent_address, + protocol_id=MLTradeMessage.protocol_id, + message=MLTradeSerializer().encode(cft_msg)) + + +class MyTransactionHandler(Handler): + """Implement the transaction handler.""" + + SUPPORTED_PROTOCOL = TransactionMessage.protocol_id # type: Optional[ProtocolId] + + def setup(self) -> None: + """Implement the setup for the handler.""" + pass + + def handle(self, message: Message) -> None: + """ + Implement the reaction to a message. + + :param message: the message + :param sender: the sender + :return: None + """ + tx_msg_response = cast(TransactionMessage, message) + if tx_msg_response.performative == TransactionMessage.Performative.SUCCESSFUL_SETTLEMENT: + logger.info("[{}]: transaction was successful.".format(self.context.agent_name)) + info = tx_msg_response.info + terms = cast(Description, info.get("terms")) + ml_accept = MLTradeMessage(performative=MLTradeMessage.Performative.ACCEPT, + tx_digest=tx_msg_response.tx_digest, + terms=terms) + self.context.outbox.put_message(to=tx_msg_response.tx_counterparty_addr, + sender=self.context.agent_address, + protocol_id=MLTradeMessage.protocol_id, + message=MLTradeSerializer().encode(ml_accept)) + logger.info("[{}]: Sending accept to counterparty={} with transaction digest={} and terms={}." + .format(self.context.agent_name, tx_msg_response.tx_counterparty_addr[-5:], tx_msg_response.tx_digest, terms.values)) + else: + logger.info("[{}]: transaction was not successful.".format(self.context.agent_name)) + + def teardown(self) -> None: + """ + Implement the handler teardown. + + :return: None + """ + pass diff --git a/packages/skills/ml_train/model.json b/packages/skills/ml_train/model.json new file mode 100644 index 0000000000..63adc8822c --- /dev/null +++ b/packages/skills/ml_train/model.json @@ -0,0 +1,108 @@ +{ + "name": "my_model", + "layers": [ + { + "class_name": "InputLayer", + "config": { + "batch_input_shape": [ + null, + 28, + 28 + ], + "dtype": "float32", + "sparse": false, + "name": "pictures" + }, + "name": "pictures", + "inbound_nodes": [] + }, + { + "class_name": "Dense", + "config": { + "name": "dense_1", + "trainable": true, + "dtype": "float32", + "units": 128, + "activation": "relu", + "use_bias": true, + "kernel_initializer": { + "class_name": "GlorotUniform", + "config": { + "seed": null + } + }, + "bias_initializer": { + "class_name": "Zeros", + "config": {} + }, + "kernel_regularizer": null, + "bias_regularizer": null, + "activity_regularizer": null, + "kernel_constraint": null, + "bias_constraint": null + }, + "name": "dense_1", + "inbound_nodes": [ + [ + [ + "pictures", + 0, + 0, + {} + ] + ] + ] + }, + { + "class_name": "Dense", + "config": { + "name": "activations", + "trainable": true, + "dtype": "float32", + "units": 10, + "activation": "softmax", + "use_bias": true, + "kernel_initializer": { + "class_name": "GlorotUniform", + "config": { + "seed": null + } + }, + "bias_initializer": { + "class_name": "Zeros", + "config": {} + }, + "kernel_regularizer": null, + "bias_regularizer": null, + "activity_regularizer": null, + "kernel_constraint": null, + "bias_constraint": null + }, + "name": "activations", + "inbound_nodes": [ + [ + [ + "dense_1", + 0, + 0, + {} + ] + ] + ] + } + ], + "input_layers": [ + [ + "pictures", + 0, + 0 + ] + ], + "output_layers": [ + [ + "activations", + 0, + 0 + ] + ] +} \ No newline at end of file diff --git a/packages/skills/ml_train/model.py b/packages/skills/ml_train/model.py new file mode 100644 index 0000000000..0804056600 --- /dev/null +++ b/packages/skills/ml_train/model.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the strategy class.""" +import threading +from pathlib import Path + +from tensorflow import keras + +from aea.skills.base import SharedClass + +DEFAULT_MODEL_CONFIG_PATH = str(Path("..", "..", "model.config").resolve()) + + +class Model(SharedClass): + """This class defines a machine learning model.""" + + def __init__(self, **kwargs): + """Initialize the machine learning model.""" + self._model_config_path = kwargs.pop("model_config_path", DEFAULT_MODEL_CONFIG_PATH) + super().__init__(**kwargs) + + # TODO this at the momment does not work - need to compile the model according to the network configuration + # A better alternative is to save/load in HDF5 format, but that might require some system level dependencies + # https://keras.io/getting-started/faq/#how-can-i-install-hdf5-or-h5py-to-save-my-models-in-keras + # self._model = keras.Model.from_config(json.load(open(self._model_config_path))) + self._model = keras.Sequential([ + keras.layers.Flatten(input_shape=(28, 28)), + keras.layers.Dense(128, activation='relu'), + keras.layers.Dense(10, activation='softmax') + ]) + self._model.compile(optimizer='adam', + loss='sparse_categorical_crossentropy', + metrics=['accuracy']) + self._lock = threading.Lock() + + @property + def tf_model(self) -> keras.Model: + """Get the TensorFlow model.""" + with self._lock: + return self._model + + def save(self): + """Save the model weights.""" + # TODO to implement. diff --git a/packages/skills/ml_train/skill.yaml b/packages/skills/ml_train/skill.yaml new file mode 100644 index 0000000000..c041748780 --- /dev/null +++ b/packages/skills/ml_train/skill.yaml @@ -0,0 +1,41 @@ +name: 'ml_train' +author: fetchai +version: 0.1.0 +license: Apache 2.0 +fingerprint: "" +description: "The ml train and predict skill implements a simple skill which buys training data, trains a model and sells predictions." +url: "" +behaviours: + search: + class_name: MySearchBehaviour + args: + search_interval: 30 +handlers: + oef: + class_name: OEFHandler + args: {} + train: + class_name: TrainHandler + args: {} + transaction: + class_name: MyTransactionHandler + args: {} +tasks: {} +shared_classes: + strategy: + class_name: Strategy + args: + dataset_id: 'fmnist' + max_unit_price: 70 + max_buyer_tx_fee: 20 + currency_id: 'FET' + ledger_id: 'fetchai' + is_ledger_tx: False + model: + class_name: Model + args: + model_config_path: "./skills/ml_train/model.json" +protocols: ['oef', 'ml_trade'] +dependencies: + tensorflow: {} + numpy: {} diff --git a/packages/skills/ml_train/strategy.py b/packages/skills/ml_train/strategy.py new file mode 100644 index 0000000000..d63942da15 --- /dev/null +++ b/packages/skills/ml_train/strategy.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the strategy class.""" +import datetime +from typing import cast + +from aea.helpers.search.models import Attribute, DataModel, Query, Constraint, ConstraintType, Description +from aea.skills.base import SharedClass + +DEFAULT_DATASET_ID = 'UK' +DEFAULT_MAX_ROW_PRICE = 5 +DEFAULT_MAX_TX_FEE = 2 +DEFAULT_CURRENCY_ID = 'FET' +DEFAULT_LEDGER_ID = 'None' + + +class Strategy(SharedClass): + """This class defines a strategy for the agent.""" + + def __init__(self, **kwargs) -> None: + """Initialize the strategy of the agent.""" + self._dataset_id = kwargs.pop('dataset_id', DEFAULT_DATASET_ID) + self._max_unit_price = kwargs.pop('max_unit_price', DEFAULT_MAX_ROW_PRICE) + self._max_buyer_tx_fee = kwargs.pop('max_buyer_tx_fee', DEFAULT_MAX_TX_FEE) + self._currency_id = kwargs.pop('currency_id', DEFAULT_CURRENCY_ID) + self._ledger_id = kwargs.pop('ledger_id', DEFAULT_LEDGER_ID) + self.is_ledger_tx = kwargs.pop('is_ledger_tx', False) + super().__init__(**kwargs) + self._search_id = 0 + self.is_searching = True + self._last_search_time = datetime.datetime.now() + self._tx_id = 0 + + def get_next_search_id(self) -> int: + """ + Get the next search id and set the search time. + + :return: the next search id + """ + self._search_id += 1 + self._last_search_time = datetime.datetime.now() + return self._search_id + + def get_next_transition_id(self) -> str: + """ + Get the next transaction id. + + :return: The next transaction id + """ + self._tx_id += 1 + return "transaction_{}".format(self._tx_id) + + def get_service_query(self) -> Query: + """ + Get the service query of the agent. + + :return: the query + """ + dm = DataModel("ml_datamodel", [Attribute("dataset_id", str, True)]) + query = Query([Constraint("dataset_id", ConstraintType("==", self._dataset_id))], model=dm) + return query + + def is_acceptable_terms(self, terms: Description) -> bool: + """ + Check whether the terms are acceptable. + + :params terms: the terms + :return: boolean + """ + result = (terms.values['price'] - terms.values['seller_tx_fee'] > 0) and \ + (terms.values['price'] <= self._max_unit_price * terms.values['batch_size']) and \ + (terms.values['buyer_tx_fee'] <= self._max_buyer_tx_fee) and \ + (terms.values['currency_id'] == self._currency_id) and \ + (terms.values['ledger_id'] == self._ledger_id) + return result + + def is_affordable_terms(self, terms: Description) -> bool: + """ + Check whether the terms are affordable. + + :params terms: the terms + :return: whether it is affordable + """ + if self.is_ledger_tx: + payable = terms.values['price'] - terms.values['seller_tx_fee'] + terms.values['buyer_tx_fee'] + ledger_id = terms.values['ledger_id'] + address = cast(str, self.context.agent_addresses.get(ledger_id)) + balance = self.context.ledger_apis.token_balance(ledger_id, address) + result = balance >= payable + else: + result = True + return result diff --git a/packages/skills/ml_train/tasks.py b/packages/skills/ml_train/tasks.py new file mode 100644 index 0000000000..ccf7cc338a --- /dev/null +++ b/packages/skills/ml_train/tasks.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the tasks for the 'ml_train' skill.""" +import logging +from typing import Tuple + +import numpy as np +from tensorflow import keras +from aea.skills.base import Task + +logger = logging.getLogger("aea.gym_skill") + + +class MLTrainTask(Task): + """ML train task.""" + + def __init__(self, train_data: Tuple[np.ndarray, np.ndarray], *args, **kwargs): + """Initialize the task.""" + super().__init__(*args, **kwargs) + self.train_x, self.train_y = train_data + + self.model = self.context.model.tf_model # type: keras.Model + self.epochs_per_batch = kwargs.pop("epochs_per_batch", 10) + # TODO not sure it's relevant - MLTrainTask already trains over a single batch. + self.batch_size = kwargs.pop("batch_size", 32) + + def setup(self) -> None: + """Set up the task.""" + logger.info("ML Train task: setup method called.") + + def execute(self, *args, **kwargs) -> None: + """Execute the task.""" + logger.info("Start training with {} rows".format(self.train_x.shape[0])) + self.model.fit(self.train_x, self.train_y, epochs=self.epochs_per_batch) + loss, acc = self.model.evaluate(self.train_x, self.train_y, verbose=2) + logger.info("Loss: {}, Acc: {}".format(loss, acc)) + self.completed = True + + def teardown(self) -> None: + """Teardown the task.""" + logger.info("ML Train task: teardown method called.") diff --git a/packages/skills/tac_control/behaviours.py b/packages/skills/tac_control/behaviours.py index 0959c169fc..ddb29259df 100644 --- a/packages/skills/tac_control/behaviours.py +++ b/packages/skills/tac_control/behaviours.py @@ -24,17 +24,19 @@ import sys from typing import cast, Optional, TYPE_CHECKING -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.models import Description, DataModel, Attribute -from aea.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF +from aea.helpers.search.models import Description, DataModel, Attribute from aea.skills.base import Behaviour if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.oef.message import OEFMessage + from packages.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF from packages.protocols.tac.message import TACMessage from packages.protocols.tac.serialization import TACSerializer from packages.skills.tac_control.game import Game, Phase from packages.skills.tac_control.parameters import Parameters else: + from oef_protocol.message import OEFMessage + from oef_protocol.serialization import OEFSerializer, DEFAULT_OEF from tac_protocol.message import TACMessage from tac_protocol.serialization import TACSerializer from tac_control_skill.game import Game, Phase @@ -109,12 +111,12 @@ def _register_tac(self) -> None: self._oef_msg_id += 1 desc = Description({"version": self.context.parameters.version_id}, data_model=CONTROLLER_DATAMODEL) logger.info("[{}]: Registering TAC data model".format(self.context.agent_name)) - oef_msg = OEFMessage(oef_type=OEFMessage.Type.REGISTER_SERVICE, + oef_msg = OEFMessage(type=OEFMessage.Type.REGISTER_SERVICE, id=self._oef_msg_id, service_description=desc, service_id="") self.context.outbox.put_message(to=DEFAULT_OEF, - sender=self.context.agent_public_key, + sender=self.context.agent_address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(oef_msg)) self._registered_desc = desc @@ -127,12 +129,12 @@ def _unregister_tac(self) -> None: """ self._oef_msg_id += 1 logger.info("[{}]: Unregistering TAC data model".format(self.context.agent_name)) - oef_msg = OEFMessage(oef_type=OEFMessage.Type.UNREGISTER_SERVICE, + oef_msg = OEFMessage(type=OEFMessage.Type.UNREGISTER_SERVICE, id=self._oef_msg_id, service_description=self._registered_desc, service_id="") self.context.outbox.put_message(to=DEFAULT_OEF, - sender=self.context.agent_public_key, + sender=self.context.agent_address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(oef_msg)) self._registered_desc = None @@ -143,21 +145,21 @@ def _start_tac(self): game.create() logger.info("[{}]: Started competition:\n{}".format(self.context.agent_name, game.holdings_summary)) logger.info("[{}]: Computed equilibrium:\n{}".format(self.context.agent_name, game.equilibrium_summary)) - for agent_public_key in game.configuration.agent_pbks: - agent_state = game.current_agent_states[agent_public_key] - tac_msg = TACMessage(tac_type=TACMessage.Type.GAME_DATA, - amount_by_currency=agent_state.balance_by_currency, - exchange_params_by_currency=agent_state.exchange_params_by_currency, - quantities_by_good_pbk=agent_state.quantities_by_good_pbk, - utility_params_by_good_pbk=agent_state.utility_params_by_good_pbk, + for agent_address in game.configuration.agent_addresses: + agent_state = game.current_agent_states[agent_address] + tac_msg = TACMessage(type=TACMessage.Type.GAME_DATA, + amount_by_currency_id=agent_state.balance_by_currency_id, + exchange_params_by_currency_id=agent_state.exchange_params_by_currency_id, + quantities_by_good_id=agent_state.quantities_by_good_id, + utility_params_by_good_id=agent_state.utility_params_by_good_id, tx_fee=game.configuration.tx_fee, - agent_pbk_to_name=game.configuration.agent_pbk_to_name, - good_pbk_to_name=game.configuration.good_pbk_to_name, + agent_addr_to_name=game.configuration.agent_addr_to_name, + good_id_to_name=game.configuration.good_id_to_name, version_id=game.configuration.version_id) logger.debug("[{}]: sending game data to '{}': {}" - .format(self.context.agent_name, agent_public_key, str(tac_msg))) - self.context.outbox.put_message(to=agent_public_key, - sender=self.context.agent_public_key, + .format(self.context.agent_name, agent_address, str(tac_msg))) + self.context.outbox.put_message(to=agent_address, + sender=self.context.agent_address, protocol_id=TACMessage.protocol_id, message=TACSerializer().encode(tac_msg)) @@ -165,9 +167,11 @@ def _cancel_tac(self): """Notify agents that the TAC is cancelled.""" game = cast(Game, self.context.game) logger.info("[{}]: Notifying agents that TAC is cancelled.".format(self.context.agent_name)) - for agent_pbk in game.registration.agent_pbk_to_name.keys(): - tac_msg = TACMessage(tac_type=TACMessage.Type.CANCELLED) - self.context.outbox.put_message(to=agent_pbk, - sender=self.context.agent_public_key, + for agent_addr in game.registration.agent_addr_to_name.keys(): + tac_msg = TACMessage(type=TACMessage.Type.CANCELLED) + self.context.outbox.put_message(to=agent_addr, + sender=self.context.agent_address, protocol_id=TACMessage.protocol_id, message=TACSerializer().encode(tac_msg)) + logger.info("[{}]: Finished competition:\n{}".format(self.context.agent_name, game.holdings_summary)) + logger.info("[{}]: Computed equilibrium:\n{}".format(self.context.agent_name, game.equilibrium_summary)) diff --git a/packages/skills/tac_control/game.py b/packages/skills/tac_control/game.py index e383b78ac2..69fd167b1e 100644 --- a/packages/skills/tac_control/game.py +++ b/packages/skills/tac_control/game.py @@ -27,20 +27,20 @@ from typing import cast, Dict, List, Optional, TYPE_CHECKING from aea.helpers.preference_representations.base import logarithmic_utility, linear_utility +from aea.mail.base import Address from aea.skills.base import SharedClass if TYPE_CHECKING or "pytest" in sys.modules: from packages.protocols.tac.message import TACMessage - from packages.skills.tac_control.helpers import generate_good_pbk_to_name, determine_scaling_factor, \ + from packages.skills.tac_control.helpers import generate_good_id_to_name, determine_scaling_factor, \ generate_money_endowments, generate_good_endowments, generate_utility_params, generate_equilibrium_prices_and_holdings from packages.skills.tac_control.parameters import Parameters else: from tac_protocol.message import TACMessage - from tac_control_skill.helpers import generate_good_pbk_to_name, determine_scaling_factor, \ + from tac_control_skill.helpers import generate_good_id_to_name, determine_scaling_factor, \ generate_money_endowments, generate_good_endowments, generate_utility_params, generate_equilibrium_prices_and_holdings from tac_control_skill.parameters import Parameters -Address = str TransactionId = str Endowment = List[int] # an element e_j is the endowment of good j. UtilityParams = List[float] # an element u_j is the utility value of good j. @@ -67,8 +67,8 @@ def __init__(self, nb_agents: int, nb_goods: int, tx_fee: int, - agent_pbk_to_name: Dict[Address, str], - good_pbk_to_name: Dict[Address, str]): + agent_addr_to_name: Dict[Address, str], + good_id_to_name: Dict[str, str]): """ Instantiate a game configuration. @@ -76,15 +76,15 @@ def __init__(self, :param nb_agents: the number of agents. :param nb_goods: the number of goods. :param tx_fee: the fee for a transaction. - :param agent_pbk_to_name: a dictionary mapping agent public keys to agent names (as strings). - :param good_pbk_to_name: a dictionary mapping good public keys to good names (as strings). + :param agent_addr_to_name: a dictionary mapping agent addresses to agent names (as strings). + :param good_id_to_name: a dictionary mapping good ids to good names (as strings). """ self._version_id = version_id self._nb_agents = nb_agents self._nb_goods = nb_goods self._tx_fee = tx_fee - self._agent_pbk_to_name = agent_pbk_to_name - self._good_pbk_to_name = good_pbk_to_name + self._agent_addr_to_name = agent_addr_to_name + self._good_id_to_name = good_id_to_name self._check_consistency() @@ -109,34 +109,34 @@ def tx_fee(self) -> int: return self._tx_fee @property - def agent_pbk_to_name(self) -> Dict[Address, str]: - """Map agent public keys to names.""" - return self._agent_pbk_to_name + def agent_addr_to_name(self) -> Dict[Address, str]: + """Map agent addresses to names.""" + return self._agent_addr_to_name @property - def good_pbk_to_name(self) -> Dict[Address, str]: - """Map good public keys to names.""" - return self._good_pbk_to_name + def good_id_to_name(self) -> Dict[str, str]: + """Map good ids to names.""" + return self._good_id_to_name @property - def agent_pbks(self) -> List[Address]: - """List of agent public keys.""" - return list(self._agent_pbk_to_name.keys()) + def agent_addresses(self) -> List[Address]: + """List of agent addresses.""" + return list(self._agent_addr_to_name.keys()) @property def agent_names(self): """List of agent names.""" - return list(self._agent_pbk_to_name.values()) + return list(self._agent_addr_to_name.values()) @property - def good_pbks(self) -> List[Address]: - """List of good public keys.""" - return list(self._good_pbk_to_name.keys()) + def good_ids(self) -> List[Address]: + """List of good ids.""" + return list(self._good_id_to_name.keys()) @property def good_names(self) -> List[str]: """List of good names.""" - return list(self._good_pbk_to_name.values()) + return list(self._good_id_to_name.values()) def _check_consistency(self): """ @@ -149,9 +149,9 @@ def _check_consistency(self): assert self.tx_fee >= 0, "Tx fee must be non-negative." assert self.nb_agents > 1, "Must have at least two agents." assert self.nb_goods > 1, "Must have at least two goods." - assert len(self.agent_pbks) == self.nb_agents, "There must be one public key for each agent." + assert len(self.agent_addresses) == self.nb_agents, "There must be one address for each agent." assert len(set(self.agent_names)) == self.nb_agents, "Agents' names must be unique." - assert len(self.good_pbks) == self.nb_goods, "There must be one public key for each good." + assert len(self.good_ids) == self.nb_goods, "There must be one id for each good." assert len(set(self.good_names)) == self.nb_goods, "Goods' names must be unique." @@ -268,70 +268,79 @@ class Transaction: """Convenience representation of a transaction.""" def __init__(self, - transaction_id: TransactionId, - sender: Address, - counterparty: Address, - amount_by_currency: Dict[str, int], - sender_tx_fee: int, - counterparty_tx_fee: int, - quantities_by_good_pbk: Dict[str, int]) -> None: + id: TransactionId, + sender_addr: Address, + counterparty_addr: Address, + amount_by_currency_id: Dict[str, int], + sender_fee: int, + counterparty_fee: int, + quantities_by_good_id: Dict[str, int], + nonce: int, + sender_signature: bytes, + counterparty_signature: bytes) -> None: """ Instantiate transaction request. - :param transaction_id: the id of the transaction. - :param sender: the sender of the transaction. - :param counterparty: the counterparty of the transaction. - :param amount_by_currency: the currency used. - :param sender_tx_fee: the transaction fee covered by the sender. - :param counterparty_tx_fee: the transaction fee covered by the counterparty. - :param quantities_by_good_pbk: a map from good pbk to the quantity of that good involved in the transaction. + :param id: the id of the transaction. + :param sender_addr: the sender of the transaction. + :param tx_counterparty_addr: the counterparty of the transaction. + :param amount_by_currency_id: the currency used. + :param sender_fee: the transaction fee covered by the sender. + :param counterparty_fee: the transaction fee covered by the counterparty. + :param quantities_by_good_id: a map from good pbk to the quantity of that good involved in the transaction. + :param nonce: the nonce of the transaction + :param sender_signature: the signature of the transaction sender + :param counterparty_signature: the signature of the transaction counterparty :return: None """ - self.transaction_id = transaction_id - self.sender = sender - self.counterparty = counterparty - self.is_sender_buyer = any(value <= 0 for value in amount_by_currency.values()) - self.amount_by_currency = amount_by_currency - self.sender_tx_fee = sender_tx_fee - self.counterparty_tx_fee = counterparty_tx_fee - self.quantities_by_good_pbk = quantities_by_good_pbk + self.id = id + self.sender_addr = sender_addr + self.counterparty_addr = counterparty_addr + self.amount_by_currency_id = amount_by_currency_id + self.sender_fee = sender_fee + self.counterparty_fee = counterparty_fee + self.quantities_by_good_id = quantities_by_good_id + self.nonce = nonce + self.sender_signature = sender_signature + self.counterparty_signature = counterparty_signature self._check_consistency() + self.is_sender_buyer = all(value <= 0 for value in amount_by_currency_id.values()) @property - def buyer_pbk(self) -> Address: - """Get the public key of the buyer.""" - result = self.sender if self.is_sender_buyer else self.counterparty + def buyer_addr(self) -> Address: + """Get the address of the buyer.""" + result = self.sender_addr if self.is_sender_buyer else self.counterparty_addr return result @property - def seller_pbk(self) -> Address: - """Get the public key of the seller.""" - result = self.counterparty if self.is_sender_buyer else self.sender + def seller_addr(self) -> Address: + """Get the address of the seller.""" + result = self.counterparty_addr if self.is_sender_buyer else self.sender_addr return result @property def buyer_tx_fee(self) -> int: """Get the tx fee of the buyer.""" - result = self.sender_tx_fee if self.is_sender_buyer else self.counterparty_tx_fee + result = self.sender_fee if self.is_sender_buyer else self.counterparty_fee return result @property def seller_tx_fee(self) -> int: """Get the tx fee of the seller.""" - result = self.counterparty_tx_fee if self.is_sender_buyer else self.sender_tx_fee + result = self.counterparty_fee if self.is_sender_buyer else self.sender_fee return result @property def amount(self) -> int: """Get the amount.""" - assert len(self.amount_by_currency) == 1 - return list(self.amount_by_currency.values())[0] + assert len(self.amount_by_currency_id) == 1 + return list(self.amount_by_currency_id.values())[0] @property - def currency(self) -> str: + def currency_id(self) -> str: """Get the currency.""" - assert len(self.amount_by_currency) == 1 - return list(self.amount_by_currency.keys())[0] + assert len(self.amount_by_currency_id) == 1 + return list(self.amount_by_currency_id.keys())[0] def _check_consistency(self) -> None: """ @@ -340,105 +349,89 @@ def _check_consistency(self) -> None: :return: None :raises AssertionError if some constraint is not satisfied. """ - assert self.sender != self.counterparty - assert len(self.amount_by_currency.keys()) == 1 # For now we restrict to one currency per transaction. - assert self.sender_tx_fee >= 0 - assert self.counterparty_tx_fee >= 0 - assert len(self.quantities_by_good_pbk.keys()) == len(set(self.quantities_by_good_pbk.keys())) + assert self.sender_addr != self.counterparty_addr + assert len(self.amount_by_currency_id.keys()) == 1 # For now we restrict to one currency per transaction. + assert self.sender_fee >= 0 + assert self.counterparty_fee >= 0 + assert len(self.quantities_by_good_id.keys()) == len(set(self.quantities_by_good_id.keys())) + assert (all(amount >= 0 for amount in self.amount_by_currency_id.values()) and all(quantity <= 0 for quantity in self.quantities_by_good_id.values())) or \ + (all(amount <= 0 for amount in self.amount_by_currency_id.values()) and all(quantity >= 0 for quantity in self.quantities_by_good_id.values())) @classmethod - def from_message(cls, message: TACMessage, sender: Address) -> 'Transaction': + def from_message(cls, message: TACMessage) -> 'Transaction': """ Create a transaction from a proposal. :param message: the message :return: Transaction """ - assert message.get('type') == TACMessage.Type.TRANSACTION - return Transaction(cast(str, message.get("transaction_id")), - sender, - cast(str, message.get("counterparty")), - cast(Dict[str, int], message.get("amount_by_currency")), - cast(int, message.get("sender_tx_fee")), - cast(int, message.get("counterparty_tx_fee")), - cast(Dict[str, int], message.get("quantities_by_good_pbk"))) - - def matches(self, other: 'Transaction') -> bool: - """ - Check if the transaction matches with another (mirroring) transaction. - - Two transaction requests do match if: - - the transaction id is the same; - - one of them is from a buyer and the other one is from a seller - - the counterparty and the origin field are consistent. - - the amount and the quantities are equal. - - :param other: the other transaction to match. - :return: True if the two - """ - return self.transaction_id == other.transaction_id \ - and self.sender == other.counterparty \ - and self.counterparty == other.sender \ - and self.is_sender_buyer != other.is_sender_buyer \ - and self.amount_by_currency == {x: y * -1 for x, y in other.amount_by_currency.items()} \ - and self.sender_tx_fee == other.counterparty_tx_fee \ - and self.counterparty_tx_fee == other.sender_tx_fee \ - and self.quantities_by_good_pbk == {x: y * -1 for x, y in other.quantities_by_good_pbk.items()} + assert message.type == TACMessage.Type.TRANSACTION + return Transaction(message.tx_id, + message.tx_sender_addr, + message.tx_counterparty_addr, + message.amount_by_currency_id, + message.tx_sender_fee, + message.tx_counterparty_fee, + message.quantities_by_good_id, + message.tx_nonce, + message.tx_sender_signature, + message.tx_counterparty_signature) def __eq__(self, other): """Compare to another object.""" return isinstance(other, Transaction) \ - and self.transaction_id == other.transaction_id \ - and self.sender == other.sender \ - and self.counterparty == other.counterparty \ - and self.is_sender_buyer == other.is_sender_buyer \ - and self.amount_by_currency == other.amount_by_currency \ - and self.sender_tx_fee == other.sender_tx_fee \ - and self.counterparty_tx_fee == other.counterparty_tx_fee \ - and self.quantities_by_good_pbk == other.quantities_by_good_pbk + and self.id == other.id \ + and self.sender_addr == other.sender_addr \ + and self.counterparty_addr == other.counterparty_addr \ + and self.amount_by_currency_id == other.amount_by_currency_id \ + and self.sender_fee == other.sender_fee \ + and self.counterparty_fee == other.counterparty_fee \ + and self.quantities_by_good_id == other.quantities_by_good_id \ + and self.sender_signature == other.sender_signature \ + and self.counterparty_signature == other.counterparty_signature class AgentState: """Represent the state of an agent during the game.""" - def __init__(self, amount_by_currency: Dict[str, int], - exchange_params_by_currency: Dict[str, float], - quantities_by_good_pbk: Dict[str, int], - utility_params_by_good_pbk: Dict[str, float]): + def __init__(self, amount_by_currency_id: Dict[str, int], + exchange_params_by_currency_id: Dict[str, float], + quantities_by_good_id: Dict[str, int], + utility_params_by_good_id: Dict[str, float]): """ Instantiate an agent state object. - :param amount_by_currency: the amount for each currency - :param exchange_params_by_currency: the exchange parameters of the different currencies - :param quantities_by_good_pbk: the quantities for each good. - :param utility_params_by_good_pbk: the utility params for every good. + :param amount_by_currency_id: the amount for each currency + :param exchange_params_by_currency_id: the exchange parameters of the different currencies + :param quantities_by_good_id: the quantities for each good. + :param utility_params_by_good_id: the utility params for every good. """ - assert len(amount_by_currency.keys()) == len(exchange_params_by_currency.keys()) - assert len(quantities_by_good_pbk.keys()) == len(utility_params_by_good_pbk.keys()) - self._balance_by_currency = copy.copy(amount_by_currency) - self._exchange_params_by_currency = copy.copy(exchange_params_by_currency) - self._quantities_by_good_pbk = quantities_by_good_pbk - self._utility_params_by_good_pbk = copy.copy(utility_params_by_good_pbk) + assert len(amount_by_currency_id.keys()) == len(exchange_params_by_currency_id.keys()) + assert len(quantities_by_good_id.keys()) == len(utility_params_by_good_id.keys()) + self._balance_by_currency_id = copy.copy(amount_by_currency_id) + self._exchange_params_by_currency_id = copy.copy(exchange_params_by_currency_id) + self._quantities_by_good_id = quantities_by_good_id + self._utility_params_by_good_id = copy.copy(utility_params_by_good_id) @property - def balance_by_currency(self) -> Dict[str, int]: + def balance_by_currency_id(self) -> Dict[str, int]: """Get the balance for each currency.""" - return copy.copy(self._balance_by_currency) + return copy.copy(self._balance_by_currency_id) @property - def exchange_params_by_currency(self) -> Dict[str, float]: + def exchange_params_by_currency_id(self) -> Dict[str, float]: """Get the exchange parameters for each currency.""" - return copy.copy(self._exchange_params_by_currency) + return copy.copy(self._exchange_params_by_currency_id) @property - def quantities_by_good_pbk(self) -> Dict[str, int]: + def quantities_by_good_id(self) -> Dict[str, int]: """Get holding of each good.""" - return copy.copy(self._quantities_by_good_pbk) + return copy.copy(self._quantities_by_good_id) @property - def utility_params_by_good_pbk(self) -> Dict[str, float]: + def utility_params_by_good_id(self) -> Dict[str, float]: """Get utility parameter for each good.""" - return copy.copy(self._utility_params_by_good_pbk) + return copy.copy(self._utility_params_by_good_id) def get_score(self) -> float: """ @@ -448,8 +441,8 @@ def get_score(self) -> float: with positive quantity plus the money left. :return: the score. """ - goods_score = logarithmic_utility(self.utility_params_by_good_pbk, self.quantities_by_good_pbk) - money_score = linear_utility(self.exchange_params_by_currency, self.balance_by_currency) + goods_score = logarithmic_utility(self.utility_params_by_good_id, self.quantities_by_good_id) + money_score = linear_utility(self.exchange_params_by_currency_id, self.balance_by_currency_id) score = goods_score + money_score return score @@ -475,12 +468,12 @@ def check_transaction_is_consistent(self, tx: Transaction) -> bool: """ if tx.is_sender_buyer: # check if we have the money as the buyer. - result = self.balance_by_currency[tx.currency] >= tx.amount + tx.buyer_tx_fee + result = self.balance_by_currency_id[tx.currency_id] >= tx.amount + tx.buyer_tx_fee else: # check if we have the diff and the goods as the seller. - result = self.balance_by_currency[tx.currency] + tx.amount >= tx.seller_tx_fee - for good_pbk, quantity in tx.quantities_by_good_pbk.items(): - result = result and (self.quantities_by_good_pbk[good_pbk] >= quantity) + result = self.balance_by_currency_id[tx.currency_id] + tx.amount >= tx.seller_tx_fee + for good_id, quantity in tx.quantities_by_good_id.items(): + result = result and (self.quantities_by_good_id[good_id] >= quantity) return result def apply(self, transactions: List[Transaction]) -> 'AgentState': @@ -503,44 +496,44 @@ def update(self, tx: Transaction) -> None: :param tx: the transaction. :return: None """ - new_balance_by_currency = self.balance_by_currency + new_balance_by_currency_id = self.balance_by_currency_id if tx.is_sender_buyer: total = tx.amount + tx.buyer_tx_fee - new_balance_by_currency[tx.currency] -= total + new_balance_by_currency_id[tx.currency_id] -= total else: diff = tx.amount - tx.seller_tx_fee - new_balance_by_currency[tx.currency] += diff - self._balance_by_currency = new_balance_by_currency + new_balance_by_currency_id[tx.currency_id] += diff + self._balance_by_currency_id = new_balance_by_currency_id - new_quantities_by_good_pbk = self.quantities_by_good_pbk - for good_pbk, quantity in tx.quantities_by_good_pbk.items(): + new_quantities_by_good_id = self.quantities_by_good_id + for good_id, quantity in tx.quantities_by_good_id.items(): quantity_delta = quantity if tx.is_sender_buyer else -quantity - new_quantities_by_good_pbk[good_pbk] += quantity_delta - self._quantities_by_good_pbk = new_quantities_by_good_pbk + new_quantities_by_good_id[good_id] += quantity_delta + self._quantities_by_good_id = new_quantities_by_good_id def __copy__(self): """Copy the object.""" - return AgentState(self.balance_by_currency, - self.exchange_params_by_currency, - self.quantities_by_good_pbk, - self.utility_params_by_good_pbk) + return AgentState(self.balance_by_currency_id, + self.exchange_params_by_currency_id, + self.quantities_by_good_id, + self.utility_params_by_good_id) def __str__(self): """From object to string.""" return "AgentState{}".format(pprint.pformat({ - "balance_by_currency": self.balance_by_currency, - "exchange_params_by_currency": self.exchange_params_by_currency, - "quantities_by_good_pbk": self.quantities_by_good_pbk, - "utility_params_by_good_pbk": self.utility_params_by_good_pbk + "balance_by_currency_id": self.balance_by_currency_id, + "exchange_params_by_currency_id": self.exchange_params_by_currency_id, + "quantities_by_good_id": self.quantities_by_good_id, + "utility_params_by_good_id": self.utility_params_by_good_id })) def __eq__(self, other) -> bool: """Compare equality of two instances of the class.""" return isinstance(other, AgentState) and \ - self.balance_by_currency == other.balance_by_currency and \ - self.exchange_params_by_currency == other.exchange_params_by_currency and \ - self.quantities_by_good_pbk == other.quantities_by_good_pbk and \ - self.utility_params_by_good_pbk == other.utility_params_by_good_pbk + self.balance_by_currency_id == other.balance_by_currency_id and \ + self.exchange_params_by_currency_id == other.exchange_params_by_currency_id and \ + self.quantities_by_good_id == other.quantities_by_good_id and \ + self.utility_params_by_good_id == other.utility_params_by_good_id class Transactions: @@ -574,7 +567,7 @@ def add_pending(self, transaction: Transaction) -> None: :param transaction: the transaction :return: None """ - self._pending[transaction.transaction_id] = transaction + self._pending[transaction.id] = transaction def pop_pending(self, transaction_id: TransactionId) -> Transaction: """ @@ -593,8 +586,8 @@ def add_confirmed(self, transaction: Transaction) -> None: :return: None """ self._confirmed.append(transaction) - self._confirmed_per_agent[transaction.sender].append(transaction) - self._confirmed_per_agent[transaction.counterparty].append(transaction) + self._confirmed_per_agent[transaction.sender_addr].append(transaction) + self._confirmed_per_agent[transaction.counterparty_addr].append(transaction) class Registration: @@ -602,36 +595,36 @@ class Registration: def __init__(self): """Instantiate the registration class.""" - self._agent_pbk_to_name = defaultdict() # type: Dict[str, str] + self._agent_addr_to_name = defaultdict() # type: Dict[str, str] @property - def agent_pbk_to_name(self) -> Dict[str, str]: - """Get the registered agent public keys and their names.""" - return self._agent_pbk_to_name + def agent_addr_to_name(self) -> Dict[str, str]: + """Get the registered agent addresses and their names.""" + return self._agent_addr_to_name @property def nb_agents(self) -> int: """Get the number of registered agents.""" - return len(self._agent_pbk_to_name) + return len(self._agent_addr_to_name) - def register_agent(self, agent_pbk: str, agent_name: str) -> None: + def register_agent(self, agent_addr: Address, agent_name: str) -> None: """ Register an agent. - :param agent_pbk: the public key of the agent + :param agent_addr: the Address of the agent :param agent_name: the name of the agent :return: None """ - self._agent_pbk_to_name[agent_pbk] = agent_name + self._agent_addr_to_name[agent_addr] = agent_name - def unregister_agent(self, agent_pbk: str) -> None: + def unregister_agent(self, agent_addr: Address) -> None: """ Register an agent. - :param agent_pbk: the public key of the agent + :param agent_addr: the Address of the agent :return: None """ - self._agent_pbk_to_name.pop(agent_pbk) + self._agent_addr_to_name.pop(agent_addr) class Game(SharedClass): @@ -709,8 +702,8 @@ def _generate(self): """Generate a TAC game.""" parameters = cast(Parameters, self.context.parameters) - good_pbk_to_name = generate_good_pbk_to_name(parameters.nb_goods) - self._configuration = Configuration(parameters.version_id, self.registration.nb_agents, parameters.nb_goods, parameters.tx_fee, self.registration.agent_pbk_to_name, good_pbk_to_name) + good_id_to_name = generate_good_id_to_name(parameters.nb_goods) + self._configuration = Configuration(parameters.version_id, self.registration.nb_agents, parameters.nb_goods, parameters.tx_fee, self.registration.agent_addr_to_name, good_id_to_name) scaling_factor = determine_scaling_factor(parameters.money_endowment) money_endowments = generate_money_endowments(self.registration.nb_agents, parameters.money_endowment) @@ -720,29 +713,29 @@ def _generate(self): self._initialization = Initialization(money_endowments, good_endowments, utility_params, eq_prices, eq_good_holdings, eq_money_holdings) self._initial_agent_states = dict( - (agent_pbk, + (agent_addr, AgentState( {DEFAULT_CURRENCY: self.initialization.initial_money_amounts[i]}, {DEFAULT_CURRENCY: DEFAULT_CURRENCY_EXCHANGE_RATE}, - {good_pbk: endowment for good_pbk, endowment in zip(list(good_pbk_to_name.keys()), self.initialization.endowments[i])}, - {good_pbk: utility_param for good_pbk, utility_param in zip(list(good_pbk_to_name.keys()), self.initialization.utility_params[i])} + {good_id: endowment for good_id, endowment in zip(list(good_id_to_name.keys()), self.initialization.endowments[i])}, + {good_id: utility_param for good_id, utility_param in zip(list(good_id_to_name.keys()), self.initialization.utility_params[i])} )) - for agent_pbk, i in zip(self.configuration.agent_pbks, range(self.configuration.nb_agents))) + for agent_addr, i in zip(self.configuration.agent_addresses, range(self.configuration.nb_agents))) self._current_agent_states = dict( - (agent_pbk, + (agent_addr, AgentState( {DEFAULT_CURRENCY: self.initialization.initial_money_amounts[i]}, {DEFAULT_CURRENCY: DEFAULT_CURRENCY_EXCHANGE_RATE}, - {good_pbk: endowment for good_pbk, endowment in zip(list(good_pbk_to_name.keys()), self.initialization.endowments[i])}, - {good_pbk: utility_param for good_pbk, utility_param in zip(list(good_pbk_to_name.keys()), self.initialization.utility_params[i])} + {good_id: endowment for good_id, endowment in zip(list(good_id_to_name.keys()), self.initialization.endowments[i])}, + {good_id: utility_param for good_id, utility_param in zip(list(good_id_to_name.keys()), self.initialization.utility_params[i])} )) - for agent_pbk, i in zip(self.configuration.agent_pbks, range(self.configuration.nb_agents))) + for agent_addr, i in zip(self.configuration.agent_addresses, range(self.configuration.nb_agents))) self._current_good_states = dict( - (good_pbk, + (good_id, GoodState()) - for good_pbk in self.configuration.good_pbks) + for good_id in self.configuration.good_ids) def reset(self) -> None: """Reset the game.""" @@ -758,12 +751,12 @@ def reset(self) -> None: @property def initial_agent_scores(self) -> Dict[str, float]: """Get the initial scores for every agent.""" - return {agent_pbk: agent_state.get_score() for agent_pbk, agent_state in self.initial_agent_states.items()} + return {agent_addr: agent_state.get_score() for agent_addr, agent_state in self.initial_agent_states.items()} @property def current_agent_scores(self) -> Dict[str, float]: """Get the current scores for every agent.""" - return {agent_pbk: agent_state.get_score() for agent_pbk, agent_state in self.current_agent_states.items()} + return {agent_addr: agent_state.get_score() for agent_addr, agent_state in self.current_agent_states.items()} @property def holdings_matrix(self) -> List[Endowment]: @@ -772,13 +765,13 @@ def holdings_matrix(self) -> List[Endowment]: :return: the holdings matrix. """ - result = list(map(lambda state: list(state.quantities_by_good_pbk.values()), self.current_agent_states.values())) + result = list(map(lambda state: list(state.quantities_by_good_id.values()), self.current_agent_states.values())) return result @property def agent_balances(self) -> Dict[str, int]: """Get the current agent balances.""" - result = {agent_pbk: agent_state.balance_by_currency[DEFAULT_CURRENCY] for agent_pbk, agent_state in self.current_agent_states.items()} + result = {agent_addr: agent_state.balance_by_currency_id[DEFAULT_CURRENCY] for agent_addr, agent_state in self.current_agent_states.items()} return result @property @@ -790,17 +783,19 @@ def good_prices(self) -> List[float]: @property def holdings_summary(self) -> str: """Get holdings summary (a string representing the holdings for every agent).""" - result = "" - for agent_pbk, agent_state in self.current_agent_states.items(): - result = result + self.configuration.agent_pbk_to_name[agent_pbk] + " " + str(agent_state.quantities_by_good_pbk) + "\n" + result = "Current good allocation: \n" + for agent_addr, agent_state in self.current_agent_states.items(): + result = result + self.configuration.agent_addr_to_name[agent_addr] + ":" + "\n" + for good_id, quantity in agent_state.quantities_by_good_id.items(): + result += " " + good_id + ": " + str(quantity) + "\n" return result @property def equilibrium_summary(self) -> str: """Get equilibrium summary.""" result = "Equilibrium prices: \n" - for good_pbk, eq_price in zip(self.configuration.good_pbks, self.initialization.eq_prices): - result = result + good_pbk + " " + str(eq_price) + "\n" + for good_id, eq_price in zip(self.configuration.good_ids, self.initialization.eq_prices): + result = result + good_id + " " + str(eq_price) + "\n" result = result + "\n" result = result + "Equilibrium good allocation: \n" for agent_name, eq_allocations in zip(self.configuration.agent_names, self.initialization.eq_good_holdings): @@ -819,8 +814,8 @@ def is_transaction_valid(self, tx: Transaction) -> bool: :return: True if the transaction is valid, False otherwise. :raises: AssertionError: if the data in the transaction are not allowed (e.g. negative amount). """ - buyer_state = self.current_agent_states[tx.buyer_pbk] - seller_state = self.current_agent_states[tx.seller_pbk] + buyer_state = self.current_agent_states[tx.buyer_addr] + seller_state = self.current_agent_states[tx.seller_addr] result = buyer_state.check_transaction_is_consistent(tx) and \ seller_state.check_transaction_is_consistent(tx) return result @@ -836,11 +831,11 @@ def settle_transaction(self, tx: Transaction) -> None: # assert self.is_transaction_valid(tx) # self._transactions.append(tx) assert self._current_agent_states is not None, "Call create before calling current_agent_states." - buyer_state = self.current_agent_states[tx.buyer_pbk] - seller_state = self.current_agent_states[tx.seller_pbk] + buyer_state = self.current_agent_states[tx.buyer_addr] + seller_state = self.current_agent_states[tx.seller_addr] buyer_state.update(tx) seller_state.update(tx) - self._current_agent_states.update({tx.buyer_pbk: buyer_state}) - self._current_agent_states.update({tx.seller_pbk: seller_state}) + self._current_agent_states.update({tx.buyer_addr: buyer_state}) + self._current_agent_states.update({tx.seller_addr: seller_state}) diff --git a/packages/skills/tac_control/handlers.py b/packages/skills/tac_control/handlers.py index 579ef82f11..e6eaa342d5 100644 --- a/packages/skills/tac_control/handlers.py +++ b/packages/skills/tac_control/handlers.py @@ -24,23 +24,22 @@ from typing import cast, TYPE_CHECKING from aea.protocols.base import Message -from aea.protocols.oef.message import OEFMessage from aea.skills.base import Handler if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.oef.message import OEFMessage from packages.protocols.tac.message import TACMessage from packages.protocols.tac.serialization import TACSerializer from packages.skills.tac_control.game import Game, Phase, Transaction from packages.skills.tac_control.parameters import Parameters else: + from oef_protocol.message import OEFMessage from tac_protocol.message import TACMessage from tac_protocol.serialization import TACSerializer from tac_control_skill.game import Game, Phase, Transaction from tac_control_skill.parameters import Parameters -Address = str - logger = logging.getLogger("aea.tac_control_skill") @@ -57,118 +56,116 @@ def setup(self) -> None: """ pass - def handle(self, message: Message, sender: Address) -> None: + def handle(self, message: Message) -> None: """ Handle a register message. - If the public key is already registered, answer with an error message. + If the address is already registered, answer with an error message. :param message: the 'get agent state' TACMessage. - :param sender: the public key of the sender :return: None """ tac_message = cast(TACMessage, message) - tac_type = tac_message.get("type") + tac_type = tac_message.type game = cast(Game, self.context.game) logger.debug("[{}]: Handling TAC message. type={}".format(self.context.agent_name, tac_type)) if tac_type == TACMessage.Type.REGISTER and game.phase == Phase.GAME_REGISTRATION: - self._on_register(tac_message, sender) + self._on_register(tac_message) elif tac_type == TACMessage.Type.UNREGISTER and game.phase == Phase.GAME_REGISTRATION: - self._on_unregister(tac_message, sender) + self._on_unregister(tac_message) elif tac_type == TACMessage.Type.TRANSACTION and game.phase == Phase.GAME: - self._on_transaction(tac_message, sender) + self._on_transaction(tac_message) else: logger.warning("[{}]: TAC Message type not recognized or not permitted.".format(self.context.agent_name)) - def _on_register(self, message: TACMessage, sender: Address) -> None: + def _on_register(self, message: TACMessage) -> None: """ Handle a register message. - If the public key is not registered, answer with an error message. + If the address is not registered, answer with an error message. :param message: the 'get agent state' TACMessage. - :param sender: the public key of the sender :return: None """ parameters = cast(Parameters, self.context.parameters) - agent_name = cast(str, message.get("agent_name")) + agent_name = message.agent_name if len(parameters.whitelist) != 0 and agent_name not in parameters.whitelist: logger.error("[{}]: Agent name not in whitelist: '{}'".format(self.context.agent_name, agent_name)) - tac_msg = TACMessage(tac_type=TACMessage.Type.TAC_ERROR, + tac_msg = TACMessage(type=TACMessage.Type.TAC_ERROR, error_code=TACMessage.ErrorCode.AGENT_NAME_NOT_IN_WHITELIST) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=message.counterparty, + sender=self.context.agent_address, protocol_id=TACMessage.protocol_id, message=TACSerializer().encode(tac_msg)) return game = cast(Game, self.context.game) - if sender in game.registration.agent_pbk_to_name: - logger.error("[{}]: Agent already registered: '{}'".format(self.context.agent_name, game.registration.agent_pbk_to_name[sender])) - tac_msg = TACMessage(tac_type=TACMessage.Type.TAC_ERROR, - error_code=TACMessage.ErrorCode.AGENT_PBK_ALREADY_REGISTERED) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + if message.counterparty in game.registration.agent_addr_to_name: + logger.error("[{}]: Agent already registered: '{}'".format(self.context.agent_name, + game.registration.agent_addr_to_name[message.counterparty])) + tac_msg = TACMessage(type=TACMessage.Type.TAC_ERROR, + error_code=TACMessage.ErrorCode.AGENT_ADDR_ALREADY_REGISTERED) + self.context.outbox.put_message(to=message.counterparty, + sender=self.context.agent_address, protocol_id=TACMessage.protocol_id, message=TACSerializer().encode(tac_msg)) - if agent_name in game.registration.agent_pbk_to_name.values(): + if agent_name in game.registration.agent_addr_to_name.values(): logger.error("[{}]: Agent with this name already registered: '{}'".format(self.context.agent_name, agent_name)) - tac_msg = TACMessage(tac_type=TACMessage.Type.TAC_ERROR, + tac_msg = TACMessage(type=TACMessage.Type.TAC_ERROR, error_code=TACMessage.ErrorCode.AGENT_NAME_ALREADY_REGISTERED) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=message.counterparty, + sender=self.context.agent_address, protocol_id=TACMessage.protocol_id, message=TACSerializer().encode(tac_msg)) - game.registration.register_agent(sender, agent_name) + game.registration.register_agent(message.counterparty, agent_name) logger.info("[{}]: Agent registered: '{}'".format(self.context.agent_name, agent_name)) - def _on_unregister(self, message: TACMessage, sender: Address) -> None: + def _on_unregister(self, message: TACMessage) -> None: """ Handle a unregister message. - If the public key is not registered, answer with an error message. + If the address is not registered, answer with an error message. :param message: the 'get agent state' TACMessage. - :param sender: the public key of the sender :return: None """ game = cast(Game, self.context.game) - if sender not in game.registration.agent_pbk_to_name: - logger.error("[{}]: Agent not registered: '{}'".format(self.context.agent_name, sender)) - tac_msg = TACMessage(tac_type=TACMessage.Type.TAC_ERROR, + if message.counterparty not in game.registration.agent_addr_to_name: + logger.error("[{}]: Agent not registered: '{}'".format(self.context.agent_name, message.counterparty)) + tac_msg = TACMessage(type=TACMessage.Type.TAC_ERROR, error_code=TACMessage.ErrorCode.AGENT_NOT_REGISTERED) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=message.counterparty, + sender=self.context.agent_address, protocol_id=TACMessage.protocol_id, message=TACSerializer().encode(tac_msg)) else: - logger.debug("[{}]: Agent unregistered: '{}'".format(self.context.agent_name, game.configuration.agent_pbk_to_name[sender])) - game.registration.unregister_agent(sender) + logger.debug("[{}]: Agent unregistered: '{}'".format(self.context.agent_name, + game.configuration.agent_addr_to_name[message.counterparty])) + game.registration.unregister_agent(message.counterparty) - def _on_transaction(self, message: TACMessage, sender: Address) -> None: + def _on_transaction(self, message: TACMessage) -> None: """ Handle a transaction TACMessage message. If the transaction is invalid (e.g. because the state of the game are not consistent), reply with an error. :param message: the 'get agent state' TACMessage. - :param sender: the public key of the sender :return: None """ - transaction = Transaction.from_message(message, sender) + transaction = Transaction.from_message(message) logger.debug("[{}]: Handling transaction: {}".format(self.context.agent_name, transaction)) game = cast(Game, self.context.game) if game.is_transaction_valid(transaction): - self._handle_valid_transaction(message, sender, transaction) + self._handle_valid_transaction(message, transaction) else: - self._handle_invalid_transaction(message, sender) + self._handle_invalid_transaction(message) - def _handle_valid_transaction(self, message: TACMessage, sender: Address, transaction: Transaction) -> None: + def _handle_valid_transaction(self, message: TACMessage, transaction: Transaction) -> None: """ Handle a valid transaction. @@ -176,39 +173,45 @@ def _handle_valid_transaction(self, message: TACMessage, sender: Address, transa - update the game state - send a transaction confirmation both to the buyer and the seller. - :param tx: the transaction. + :param transaction: the transaction. :return: None """ game = cast(Game, self.context.game) - logger.debug("[{}]: Handling valid transaction: {}".format(self.context.agent_name, transaction.transaction_id)) + logger.info("[{}]: Handling valid transaction: {}".format(self.context.agent_name, transaction.id[-10:])) game.transactions.add_confirmed(transaction) game.settle_transaction(transaction) # send the transaction confirmation. - sender_tac_msg = TACMessage(tac_type=TACMessage.Type.TRANSACTION_CONFIRMATION, - transaction_id=transaction.transaction_id) - counterparty_tac_msg = TACMessage(tac_type=TACMessage.Type.TRANSACTION_CONFIRMATION, - transaction_id=transaction.transaction_id) - self.context.outbox.put_message(to=sender, - sender=self.context.public_key, + sender_tac_msg = TACMessage(type=TACMessage.Type.TRANSACTION_CONFIRMATION, + transaction_id=transaction.id, + amount_by_currency_id=transaction.amount_by_currency_id, + quantities_by_good_id=transaction.quantities_by_good_id) + counterparty_tac_msg = TACMessage(type=TACMessage.Type.TRANSACTION_CONFIRMATION, + transaction_id=transaction.id, + amount_by_currency_id=transaction.amount_by_currency_id, + quantities_by_good_id=transaction.quantities_by_good_id) + self.context.outbox.put_message(to=transaction.sender_addr, + sender=self.context.agent_address, protocol_id=TACMessage.protocol_id, message=TACSerializer().encode(sender_tac_msg)) - self.context.outbox.put_message(to=cast(str, message.get("counterparty")), - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=transaction.counterparty_addr, + sender=self.context.agent_address, protocol_id=TACMessage.protocol_id, message=TACSerializer().encode(counterparty_tac_msg)) # log messages - logger.debug("[{}]: Transaction '{}' settled successfully.".format(self.context.agent_name, transaction.transaction_id)) - logger.debug("[{}]: Current state:\n{}".format(self.context.agent_name, game.holdings_summary)) + logger.info("[{}]: Transaction '{}' settled successfully.".format(self.context.agent_name, transaction.id[-10:])) + logger.info("[{}]: Current state:\n{}".format(self.context.agent_name, game.holdings_summary)) - def _handle_invalid_transaction(self, message: TACMessage, sender: Address) -> None: + def _handle_invalid_transaction(self, message: TACMessage) -> None: """Handle an invalid transaction.""" - tac_msg = TACMessage(tac_type=TACMessage.Type.TAC_ERROR, + tx_id = message.tx_id[-10:] + logger.info("[{}]: Handling invalid transaction: {}".format(self.context.agent_name, tx_id)) + tac_msg = TACMessage(type=TACMessage.Type.TAC_ERROR, error_code=TACMessage.ErrorCode.TRANSACTION_NOT_VALID, - details={"transaction_id": message.get("transaction_id")}) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + info={"transaction_id": message.tx_id}) + self.context.outbox.put_message(to=message.counterparty, + sender=self.context.agent_address, protocol_id=TACMessage.protocol_id, message=TACSerializer().encode(tac_msg)) @@ -234,16 +237,15 @@ def setup(self) -> None: """ pass - def handle(self, message: Message, sender: str) -> None: + def handle(self, message: Message) -> None: """ Implement the reaction to a message. :param message: the message - :param sender: the sender :return: None """ oef_message = cast(OEFMessage, message) - oef_type = oef_message.get("type") + oef_type = oef_message.type logger.debug("[{}]: Handling OEF message. type={}".format(self.context.agent_name, oef_type)) if oef_type == OEFMessage.Type.OEF_ERROR: @@ -262,7 +264,7 @@ def _on_oef_error(self, oef_error: OEFMessage) -> None: :return: None """ logger.error("[{}]: Received OEF error: answer_id={}, operation={}" - .format(self.context.agent_name, oef_error.get("id"), oef_error.get("operation"))) + .format(self.context.agent_name, oef_error.id, oef_error.operation)) def _on_dialogue_error(self, dialogue_error: OEFMessage) -> None: """ @@ -273,7 +275,7 @@ def _on_dialogue_error(self, dialogue_error: OEFMessage) -> None: :return: None """ logger.error("[{}]: Received Dialogue error: answer_id={}, dialogue_id={}, origin={}" - .format(self.context.agent_name, dialogue_error.get("id"), dialogue_error.get("dialogue_id"), dialogue_error.get("origin"))) + .format(self.context.agent_name, dialogue_error.id, dialogue_error.dialogue_id, dialogue_error.origin)) def teardown(self) -> None: """ diff --git a/packages/skills/tac_control/helpers.py b/packages/skills/tac_control/helpers.py index 89fb88cd4a..274518279e 100644 --- a/packages/skills/tac_control/helpers.py +++ b/packages/skills/tac_control/helpers.py @@ -29,16 +29,16 @@ QUANTITY_SHIFT = 1 # Any non-negative integer is fine. -def generate_good_pbk_to_name(nb_goods: int) -> Dict[str, str]: +def generate_good_id_to_name(nb_goods: int) -> Dict[str, str]: """ - Generate public keys for things. + Generate ids for things. :param nb_goods: the number of things. - :return: a dictionary mapping goods' public keys to names. + :return: a dictionary mapping goods' ids to names. """ max_number_of_digits = math.ceil(math.log10(nb_goods)) string_format = 'tac_good_{:0' + str(max_number_of_digits) + '}' - return {string_format.format(i) + '_pbk': string_format.format(i) for i in range(nb_goods)} + return {string_format.format(i) + '_id': string_format.format(i) for i in range(nb_goods)} def determine_scaling_factor(money_endowment: int) -> float: diff --git a/packages/skills/tac_control/parameters.py b/packages/skills/tac_control/parameters.py index 3a3a9c521b..39c09e12c7 100644 --- a/packages/skills/tac_control/parameters.py +++ b/packages/skills/tac_control/parameters.py @@ -111,7 +111,7 @@ def inactivity_timeout(self): @property def whitelist(self) -> Set[str]: - """Whitelist of agent public keys allowed into the TAC instance.""" + """Whitelist of agent addresses allowed into the TAC instance.""" return self._whitelist @property diff --git a/packages/skills/tac_control/skill.yaml b/packages/skills/tac_control/skill.yaml index 49106d35b3..28b3f0ebd6 100644 --- a/packages/skills/tac_control/skill.yaml +++ b/packages/skills/tac_control/skill.yaml @@ -1,41 +1,42 @@ name: 'tac_control' -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" description: "The tac control skill implements the logic for an AEA to control an instance of the TAC." url: "" behaviours: - - behaviour: - class_name: TACBehaviour - args: {} + tac: + class_name: TACBehaviour + args: {} handlers: - - handler: - class_name: TACHandler - args: {} - - handler: - class_name: OEFRegistrationHandler - args: {} -tasks: [] + tac: + class_name: TACHandler + args: {} + oef: + class_name: OEFRegistrationHandler + args: {} +tasks: {} shared_classes: - - shared_class: - class_name: Parameters - args: - min_nb_agents: 2 - money_endowment: 2000000 - nb_goods: 10 - tx_fee: 1 - base_good_endowment: 2 - lower_bound_factor: 1 - upper_bound_factor: 1 - start_time: 12 11 2019 15:01 - registration_timeout: 60 - competition_timeout: 180 - inactivity_timeout: 60 - whitelist: [] - version_id: v1 - - shared_class: - class_name: Game - args: {} + parameters: + class_name: Parameters + args: + min_nb_agents: 2 + money_endowment: 2000000 + nb_goods: 10 + tx_fee: 1 + base_good_endowment: 2 + lower_bound_factor: 1 + upper_bound_factor: 1 + start_time: 12 11 2019 15:01 + registration_timeout: 60 + competition_timeout: 180 + inactivity_timeout: 60 + whitelist: [] + version_id: v1 + game: + class_name: Game + args: {} protocols: ['oef', 'tac'] dependencies: - - numpy + numpy: {} diff --git a/packages/skills/tac_negotiation/behaviours.py b/packages/skills/tac_negotiation/behaviours.py index 8cba6a3177..4b4a20644c 100644 --- a/packages/skills/tac_negotiation/behaviours.py +++ b/packages/skills/tac_negotiation/behaviours.py @@ -23,14 +23,16 @@ from typing import cast, TYPE_CHECKING from aea.skills.base import Behaviour -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.oef.message import OEFMessage + from packages.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF from packages.skills.tac_negotiation.registration import Registration from packages.skills.tac_negotiation.search import Search from packages.skills.tac_negotiation.strategy import Strategy else: + from oef_protocol.message import OEFMessage + from oef_protocol.serialization import OEFSerializer, DEFAULT_OEF from tac_negotiation_skill.registration import Registration from tac_negotiation_skill.search import Search from tac_negotiation_skill.strategy import Strategy @@ -83,23 +85,23 @@ def _unregister_service(self) -> None: registration = cast(Registration, self.context.registration) if registration.registered_goods_demanded_description is not None: - oef_msg = OEFMessage(oef_type=OEFMessage.Type.UNREGISTER_SERVICE, + oef_msg = OEFMessage(type=OEFMessage.Type.UNREGISTER_SERVICE, id=registration.get_next_id(), service_description=registration.registered_goods_demanded_description, service_id="") self.context.outbox.put_message(to=DEFAULT_OEF, - sender=self.context.agent_public_key, + sender=self.context.agent_address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(oef_msg)) registration.registered_goods_demanded_description = None if registration.registered_goods_supplied_description is not None: - oef_msg = OEFMessage(oef_type=OEFMessage.Type.UNREGISTER_SERVICE, + oef_msg = OEFMessage(type=OEFMessage.Type.UNREGISTER_SERVICE, id=registration.get_next_id(), service_description=registration.registered_goods_supplied_description, service_id="") self.context.outbox.put_message(to=DEFAULT_OEF, - sender=self.context.agent_public_key, + sender=self.context.agent_address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(oef_msg)) registration.registered_goods_supplied_description = None @@ -122,12 +124,12 @@ def _register_service(self) -> None: logger.debug("[{}]: Updating service directory as seller with goods supplied.".format(self.context.agent_name)) goods_supplied_description = strategy.get_own_service_description(is_supply=True) registration.registered_goods_supplied_description = goods_supplied_description - oef_msg = OEFMessage(oef_type=OEFMessage.Type.REGISTER_SERVICE, + oef_msg = OEFMessage(type=OEFMessage.Type.REGISTER_SERVICE, id=registration.get_next_id(), service_description=goods_supplied_description, service_id="") self.context.outbox.put_message(to=DEFAULT_OEF, - sender=self.context.agent_public_key, + sender=self.context.agent_address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(oef_msg)) @@ -135,12 +137,12 @@ def _register_service(self) -> None: logger.debug("[{}]: Updating service directory as buyer with goods demanded.".format(self.context.agent_name)) goods_demanded_description = strategy.get_own_service_description(is_supply=False) registration.registered_goods_demanded_description = goods_demanded_description - oef_msg = OEFMessage(oef_type=OEFMessage.Type.REGISTER_SERVICE, + oef_msg = OEFMessage(type=OEFMessage.Type.REGISTER_SERVICE, id=registration.get_next_id(), service_description=goods_demanded_description, service_id="") self.context.outbox.put_message(to=DEFAULT_OEF, - sender=self.context.agent_public_key, + sender=self.context.agent_address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(oef_msg)) @@ -166,11 +168,11 @@ def _search_services(self) -> None: else: search_id = search.get_next_id(is_searching_for_sellers=True) logger.info("[{}]: Searching for sellers which match the demand of the agent, search_id={}.".format(self.context.agent_name, search_id)) - oef_msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_SERVICES, + oef_msg = OEFMessage(type=OEFMessage.Type.SEARCH_SERVICES, id=search_id, query=query) self.context.outbox.put_message(to=DEFAULT_OEF, - sender=self.context.agent_public_key, + sender=self.context.agent_address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(oef_msg)) @@ -182,10 +184,10 @@ def _search_services(self) -> None: else: search_id = search.get_next_id(is_searching_for_sellers=False) logger.info("[{}]: Searching for buyers which match the supply of the agent, search_id={}.".format(self.context.agent_name, search_id)) - oef_msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_SERVICES, + oef_msg = OEFMessage(type=OEFMessage.Type.SEARCH_SERVICES, id=search_id, query=query) self.context.outbox.put_message(to=DEFAULT_OEF, - sender=self.context.agent_public_key, + sender=self.context.agent_address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(oef_msg)) diff --git a/packages/skills/tac_negotiation/dialogues.py b/packages/skills/tac_negotiation/dialogues.py index 1a72c83463..bdc4529e21 100644 --- a/packages/skills/tac_negotiation/dialogues.py +++ b/packages/skills/tac_negotiation/dialogues.py @@ -24,9 +24,16 @@ - Dialogues: The dialogues class keeps track of all dialogues. """ -from aea.protocols.fipa.dialogues import FIPADialogues +import sys +from typing import TYPE_CHECKING + from aea.skills.base import SharedClass +if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.fipa.dialogues import FIPADialogues +else: + from fipa_protocol.dialogues import FIPADialogues + class Dialogues(SharedClass, FIPADialogues): """The dialogues class keeps track of all dialogues.""" diff --git a/packages/skills/tac_negotiation/handlers.py b/packages/skills/tac_negotiation/handlers.py index ac53169ff2..455fb4c39f 100644 --- a/packages/skills/tac_negotiation/handlers.py +++ b/packages/skills/tac_negotiation/handlers.py @@ -22,30 +22,34 @@ import logging import pprint import sys -from typing import Dict, List, Optional, Tuple, cast, TYPE_CHECKING +from typing import Dict, List, Optional, cast, TYPE_CHECKING from aea.configurations.base import ProtocolId from aea.helpers.dialogue.base import DialogueLabel +from aea.helpers.search.models import Query from aea.skills.base import Handler from aea.protocols.base import Message from aea.protocols.default.message import DefaultMessage from aea.protocols.default.serialization import DefaultSerializer -from aea.protocols.fipa.dialogues import FIPADialogue as Dialogue -from aea.protocols.fipa.message import FIPAMessage -from aea.protocols.fipa.serialization import FIPASerializer -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.models import Query, Description from aea.decision_maker.messages.transaction import TransactionMessage if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.fipa.dialogues import FIPADialogue as Dialogue + from packages.protocols.fipa.message import FIPAMessage + from packages.protocols.fipa.serialization import FIPASerializer + from packages.protocols.oef.message import OEFMessage from packages.skills.tac_negotiation.dialogues import Dialogues - from packages.skills.tac_negotiation.helpers import generate_transaction_message, DEMAND_DATAMODEL_NAME + from packages.skills.tac_negotiation.helpers import DEMAND_DATAMODEL_NAME from packages.skills.tac_negotiation.search import Search from packages.skills.tac_negotiation.strategy import Strategy from packages.skills.tac_negotiation.transactions import Transactions else: + from fipa_protocol.dialogues import FIPADialogue as Dialogue + from fipa_protocol.message import FIPAMessage + from fipa_protocol.serialization import FIPASerializer + from oef_protocol.message import OEFMessage from tac_negotiation_skill.dialogues import Dialogues - from tac_negotiation_skill.helpers import generate_transaction_message, DEMAND_DATAMODEL_NAME + from tac_negotiation_skill.helpers import DEMAND_DATAMODEL_NAME from tac_negotiation_skill.search import Search from tac_negotiation_skill.strategy import Strategy from tac_negotiation_skill.transactions import Transactions @@ -66,39 +70,39 @@ def setup(self) -> None: """ pass - def handle(self, message: Message, sender: str) -> None: + def handle(self, message: Message) -> None: """ Dispatch message to relevant handler and respond. :param message: the message - :param sender: the sender :return: None """ fipa_msg = cast(FIPAMessage, message) logger.debug("[{}]: Identifying dialogue of FIPAMessage={}".format(self.context.agent_name, fipa_msg)) dialogues = cast(Dialogues, self.context.dialogues) - if dialogues.is_belonging_to_registered_dialogue(fipa_msg, sender, self.context.agent_public_key): - dialogue = cast(Dialogue, dialogues.get_dialogue(fipa_msg, sender, self.context.agent_public_key)) + if dialogues.is_belonging_to_registered_dialogue(fipa_msg, self.context.agent_address): + dialogue = cast(Dialogue, dialogues.get_dialogue(fipa_msg, self.context.agent_address)) dialogue.incoming_extend(fipa_msg) - elif dialogues.is_permitted_for_new_dialogue(fipa_msg, sender): - query = cast(Query, fipa_msg.get("query")) + elif dialogues.is_permitted_for_new_dialogue(fipa_msg): + query = cast(Query, fipa_msg.query) assert query.model is not None, "Query has no data model." is_seller = query.model.name == DEMAND_DATAMODEL_NAME - dialogue = cast(Dialogue, dialogues.create_opponent_initiated(sender, cast(Tuple[str, str], fipa_msg.get('dialogue_reference')), is_seller)) + dialogue = cast(Dialogue, dialogues.create_opponent_initiated(message.counterparty, fipa_msg.dialogue_reference, is_seller)) dialogue.incoming_extend(fipa_msg) else: logger.debug("[{}]: Unidentified dialogue.".format(self.context.agent_name)) default_msg = DefaultMessage(type=DefaultMessage.Type.BYTES, content=b'This message belongs to an unidentified dialogue.') - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=fipa_msg.counterparty, + sender=self.context.agent_address, protocol_id=DefaultMessage.protocol_id, message=DefaultSerializer().encode(default_msg)) return - fipa_msg_performative = fipa_msg.get("performative") - logger.debug("[{}]: Handling FIPAMessage of performative={}".format(self.context.agent_name, fipa_msg_performative)) + fipa_msg_performative = fipa_msg.performative + logger.debug("[{}]: Handling FIPAMessage of performative={}".format(self.context.agent_name, + fipa_msg_performative)) if fipa_msg_performative == FIPAMessage.Performative.CFP: self._on_cfp(fipa_msg, dialogue) elif fipa_msg_performative == FIPAMessage.Performative.PROPOSE: @@ -107,7 +111,7 @@ def handle(self, message: Message, sender: str) -> None: self._on_decline(fipa_msg, dialogue) elif fipa_msg_performative == FIPAMessage.Performative.ACCEPT: self._on_accept(fipa_msg, dialogue) - elif fipa_msg_performative == FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS: + elif fipa_msg_performative == FIPAMessage.Performative.MATCH_ACCEPT_W_INFORM: self._on_match_accept(fipa_msg, dialogue) def teardown(self) -> None: @@ -127,45 +131,45 @@ def _on_cfp(self, cfp: FIPAMessage, dialogue: Dialogue) -> None: :return: None """ - new_msg_id = cast(int, cfp.get("message_id")) + 1 - query = cast(Query, cfp.get("query")) + new_msg_id = cfp.message_id + 1 + query = cast(Query, cfp.query) strategy = cast(Strategy, self.context.strategy) proposal_description = strategy.get_proposal_for_query(query, dialogue.is_seller) if proposal_description is None: - logger.debug("[{}]: sending to {} a Decline{}".format(self.context.agent_name, dialogue.dialogue_label.dialogue_opponent_pbk, + logger.debug("[{}]: sending to {} a Decline{}".format(self.context.agent_name, dialogue.dialogue_label.dialogue_opponent_addr[-5:], pprint.pformat({ "msg_id": new_msg_id, - "dialogue_id": cfp.get("dialogue_id"), - "origin": dialogue.dialogue_label.dialogue_opponent_pbk, - "target": cfp.get("target") + "dialogue_reference": cfp.dialogue_reference, + "origin": dialogue.dialogue_label.dialogue_opponent_addr[-5:], + "target": cfp.target }))) fipa_msg = FIPAMessage(performative=FIPAMessage.Performative.DECLINE, message_id=new_msg_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, - target=cfp.get("message_id")) + target=cfp.message_id) dialogues = cast(Dialogues, self.context.dialogues) dialogues.dialogue_stats.add_dialogue_endstate(Dialogue.EndState.DECLINED_CFP, dialogue.is_self_initiated) else: - transaction_msg = generate_transaction_message(proposal_description, dialogue.dialogue_label, dialogue.is_seller, self.context.agent_public_key) transactions = cast(Transactions, self.context.transactions) + transaction_msg = transactions.generate_transaction_message(TransactionMessage.Performative.PROPOSE_FOR_SIGNING, proposal_description, dialogue.dialogue_label, dialogue.is_seller, self.context.agent_public_key) transactions.add_pending_proposal(dialogue.dialogue_label, new_msg_id, transaction_msg) - logger.info("[{}]: sending to {} a Propose{}".format(self.context.agent_name, dialogue.dialogue_label.dialogue_opponent_pbk, + logger.info("[{}]: sending to {} a Propose{}".format(self.context.agent_name, dialogue.dialogue_label.dialogue_opponent_addr[-5:], pprint.pformat({ "msg_id": new_msg_id, - "dialogue_id": cfp.get("dialogue_id"), - "origin": dialogue.dialogue_label.dialogue_opponent_pbk, - "target": cfp.get("message_id"), + "dialogue_reference": cfp.dialogue_reference, + "origin": dialogue.dialogue_label.dialogue_opponent_addr[-5:], + "target": cfp.message_id, "propose": proposal_description.values }))) fipa_msg = FIPAMessage(performative=FIPAMessage.Performative.PROPOSE, message_id=new_msg_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, - target=cfp.get("message_id"), + target=cfp.message_id, proposal=[proposal_description]) dialogue.outgoing_extend(fipa_msg) - self.context.outbox.put_message(to=dialogue.dialogue_label.dialogue_opponent_pbk, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=dialogue.dialogue_label.dialogue_opponent_addr, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(fipa_msg)) @@ -177,35 +181,35 @@ def _on_propose(self, propose: FIPAMessage, dialogue: Dialogue) -> None: :param dialogue: the dialogue :return: None """ - new_msg_id = cast(int, propose.get("message_id")) + 1 + new_msg_id = propose.message_id + 1 strategy = cast(Strategy, self.context.strategy) - proposals = cast(List[Description], propose.get("proposal")) + proposals = propose.proposal logger.debug("[{}]: on Propose as {}.".format(self.context.agent_name, dialogue.role)) for num, proposal_description in enumerate(proposals): if num > 0: continue # TODO: allow for dialogue branching with multiple proposals - transaction_msg = generate_transaction_message(proposal_description, dialogue.dialogue_label, dialogue.is_seller, self.context.agent_public_key) + transactions = cast(Transactions, self.context.transactions) + transaction_msg = transactions.generate_transaction_message(TransactionMessage.Performative.PROPOSE_FOR_SETTLEMENT, proposal_description, dialogue.dialogue_label, dialogue.is_seller, self.context.agent_public_key) if strategy.is_profitable_transaction(transaction_msg, is_seller=dialogue.is_seller): logger.info("[{}]: Accepting propose (as {}).".format(self.context.agent_name, dialogue.role)) - transactions = cast(Transactions, self.context.transactions) transactions.add_locked_tx(transaction_msg, as_seller=dialogue.is_seller) transactions.add_pending_initial_acceptance(dialogue.dialogue_label, new_msg_id, transaction_msg) fipa_msg = FIPAMessage(performative=FIPAMessage.Performative.ACCEPT, message_id=new_msg_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, - target=propose.get("message_id")) + target=propose.message_id) else: logger.info("[{}]: Declining propose (as {})".format(self.context.agent_name, dialogue.role)) fipa_msg = FIPAMessage(performative=FIPAMessage.Performative.DECLINE, message_id=new_msg_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, - target=propose.get("message_id")) + target=propose.message_id) dialogues = cast(Dialogues, self.context.dialogues) dialogues.dialogue_stats.add_dialogue_endstate(Dialogue.EndState.DECLINED_PROPOSE, dialogue.is_self_initiated) dialogue.outgoing_extend(fipa_msg) - self.context.outbox.put_message(to=dialogue.dialogue_label.dialogue_opponent_pbk, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=dialogue.dialogue_label.dialogue_opponent_addr, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(fipa_msg)) @@ -217,9 +221,9 @@ def _on_decline(self, decline: FIPAMessage, dialogue: Dialogue) -> None: :param dialogue: the dialogue :return: None """ - logger.debug("[{}]: on_decline: msg_id={}, dialogue_id={}, origin={}, target={}" - .format(self.context.agent_name, decline.get("message_id"), decline.get("dialogue_id"), dialogue.dialogue_label.dialogue_opponent_pbk, decline.get("target"))) - target = decline.get("target") + logger.debug("[{}]: on_decline: msg_id={}, dialogue_reference={}, origin={}, target={}" + .format(self.context.agent_name, decline.message_id, decline.dialogue_reference, dialogue.dialogue_label.dialogue_opponent_addr, decline.target)) + target = decline.target dialogues = cast(Dialogues, self.context.dialogues) if target == 1: @@ -242,11 +246,11 @@ def _on_accept(self, accept: FIPAMessage, dialogue: Dialogue) -> None: :param dialogue: the dialogue :return: None """ - logger.debug("[{}]: on_accept: msg_id={}, dialogue_id={}, origin={}, target={}" - .format(self.context.agent_name, accept.get("message_id"), accept.get("dialogue_id"), dialogue.dialogue_label.dialogue_opponent_pbk, accept.get("target"))) - new_msg_id = cast(int, accept.get("message_id")) + 1 + logger.debug("[{}]: on_accept: msg_id={}, dialogue_reference={}, origin={}, target={}" + .format(self.context.agent_name, accept.message_id, accept.dialogue_reference, dialogue.dialogue_label.dialogue_opponent_addr, accept.target)) + new_msg_id = accept.message_id + 1 transactions = cast(Transactions, self.context.transactions) - transaction_msg = transactions.pop_pending_proposal(dialogue.dialogue_label, cast(int, accept.get("target"))) + transaction_msg = transactions.pop_pending_proposal(dialogue.dialogue_label, accept.target) strategy = cast(Strategy, self.context.strategy) if strategy.is_profitable_transaction(transaction_msg, is_seller=dialogue.is_seller): @@ -258,12 +262,12 @@ def _on_accept(self, accept: FIPAMessage, dialogue: Dialogue) -> None: fipa_msg = FIPAMessage(performative=FIPAMessage.Performative.DECLINE, message_id=new_msg_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, - target=accept.get("message_id"), ) + target=accept.message_id) dialogue.outgoing_extend(fipa_msg) dialogues = cast(Dialogues, self.context.dialogues) dialogues.dialogue_stats.add_dialogue_endstate(Dialogue.EndState.DECLINED_ACCEPT, dialogue.is_self_initiated) - self.context.outbox.put_message(to=dialogue.dialogue_label.dialogue_opponent_pbk, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=dialogue.dialogue_label.dialogue_opponent_addr, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(fipa_msg)) @@ -275,12 +279,11 @@ def _on_match_accept(self, match_accept: FIPAMessage, dialogue: Dialogue) -> Non :param dialogue: the dialogue :return: None """ - logger.debug("[{}]: on_match_accept: msg_id={}, dialogue_id={}, origin={}, target={}" - .format(self.context.agent_name, match_accept.get("message_id"), match_accept.get("dialogue_id"), dialogue.dialogue_label.dialogue_opponent_pbk, match_accept.get("target"))) + logger.debug("[{}]: on_match_accept: msg_id={}, dialogue_reference={}, origin={}, target={}" + .format(self.context.agent_name, match_accept.message_id, match_accept.dialogue_reference, dialogue.dialogue_label.dialogue_opponent_addr, match_accept.target)) transactions = cast(Transactions, self.context.transactions) - transaction_msg = transactions.pop_pending_initial_acceptance(dialogue.dialogue_label, cast(int, match_accept.get("target"))) - # update skill id to route back to tac participation skill - transaction_msg.set('skill_id', 'tac_participation_skill') + transaction_msg = transactions.pop_pending_initial_acceptance(dialogue.dialogue_label, match_accept.target) + transaction_msg.info['tx_counterparty_signature'] = match_accept.info.get('tx_signature') self.context.decision_maker_message_queue.put(transaction_msg) @@ -297,34 +300,35 @@ def setup(self) -> None: """ pass - def handle(self, message: Message, sender: str) -> None: + def handle(self, message: Message) -> None: """ Dispatch message to relevant handler and respond. :param message: the message - :param sender: the sender :return: None """ tx_message = cast(TransactionMessage, message) - if TransactionMessage.Performative(tx_message.get("performative")) == TransactionMessage.Performative.ACCEPT: - logger.info("[{}]: transaction confirmed by decision maker, sending match accept.".format(self.context.agent_name)) - dialogue_label = DialogueLabel.from_json(cast(Dict[str, str], tx_message.get("dialogue_label"))) + if tx_message.performative == TransactionMessage.Performative.SUCCESSFUL_SIGNING: + logger.info("[{}]: transaction confirmed by decision maker".format(self.context.agent_name)) + info = tx_message.info + dialogue_label = DialogueLabel.from_json(cast(Dict[str, str], info.get("dialogue_label"))) dialogues = cast(Dialogues, self.context.dialogues) dialogue = dialogues.dialogues[dialogue_label] - tac_message = dialogue.last_incoming_message - if tac_message is not None and tac_message.get("performative") == FIPAMessage.Performative.ACCEPT: - fipa_msg = FIPAMessage(performative=FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS, - message_id=cast(int, tac_message.get("message_id")) + 1, + fipa_message = cast(FIPAMessage, dialogue.last_incoming_message) + if fipa_message is not None and fipa_message.performative == FIPAMessage.Performative.ACCEPT: + logger.info("[{}]: sending match accept to {}.".format(self.context.agent_name, dialogue.dialogue_label.dialogue_opponent_addr[-5:])) + fipa_msg = FIPAMessage(performative=FIPAMessage.Performative.MATCH_ACCEPT_W_INFORM, + message_id=fipa_message.message_id + 1, dialogue_reference=dialogue.dialogue_label.dialogue_reference, - target=tac_message.get("message_id"), - address=tx_message.get("transaction_digest")) + target=fipa_message.message_id, + info={"tx_signature": tx_message.tx_signature}) dialogue.outgoing_extend(fipa_msg) - self.context.outbox.put_message(to=dialogue.dialogue_label.dialogue_opponent_pbk, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=dialogue.dialogue_label.dialogue_opponent_addr, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(fipa_msg)) else: - logger.info("[{}]: last message should be of performative accept.".format(self.context.agent_name)) + logger.warning("[{}]: last message should be of performative accept.".format(self.context.agent_name)) else: logger.info("[{}]: transaction was not successful.".format(self.context.agent_name)) @@ -350,24 +354,22 @@ def setup(self) -> None: """ pass - def handle(self, message: Message, sender: str) -> None: + def handle(self, message: Message) -> None: """ Implement the reaction to a message. :param message: the message - :param sender: the sender :return: None """ # convenience representations oef_msg = cast(OEFMessage, message) - oef_msg_type = OEFMessage.Type(oef_msg.get("type")) - if oef_msg_type is OEFMessage.Type.SEARCH_RESULT: - agents = cast(List[str], oef_msg.get("agents")) - search_id = cast(int, oef_msg.get("id")) + if oef_msg.type is OEFMessage.Type.SEARCH_RESULT: + agents = oef_msg.agents + search_id = oef_msg.id search = cast(Search, self.context.search) - if self.context.agent_public_key in agents: - agents.remove(self.context.agent_public_key) + if self.context.agent_address in agents: + agents.remove(self.context.agent_address) if search_id in search.ids_for_sellers: self._handle_search(agents, search_id, is_searching_for_sellers=True) elif search_id in search.ids_for_buyers: @@ -396,17 +398,17 @@ def _handle_search(self, agents: List[str], search_id: int, is_searching_for_sel dialogues = cast(Dialogues, self.context.dialogues) query = strategy.get_own_services_query(is_searching_for_sellers) - for opponent_pbk in agents: - dialogue = dialogues.create_self_initiated(opponent_pbk, self.context.agent_public_key, not is_searching_for_sellers) - logger.info("[{}]: sending CFP to agent={}".format(self.context.agent_name, opponent_pbk[-5:])) + for opponent_addr in agents: + dialogue = dialogues.create_self_initiated(opponent_addr, self.context.agent_address, not is_searching_for_sellers) + logger.info("[{}]: sending CFP to agent={}".format(self.context.agent_name, opponent_addr[-5:])) fipa_msg = FIPAMessage(message_id=FIPAMessage.STARTING_MESSAGE_ID, dialogue_reference=dialogue.dialogue_label.dialogue_reference, performative=FIPAMessage.Performative.CFP, target=FIPAMessage.STARTING_TARGET, query=query) dialogue.outgoing_extend(fipa_msg) - self.context.outbox.put_message(to=opponent_pbk, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=opponent_addr, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(fipa_msg)) else: diff --git a/packages/skills/tac_negotiation/helpers.py b/packages/skills/tac_negotiation/helpers.py index 7a07278b29..2320118bb6 100644 --- a/packages/skills/tac_negotiation/helpers.py +++ b/packages/skills/tac_negotiation/helpers.py @@ -20,60 +20,58 @@ """This class contains the helpers for FIPA negotiation.""" -import copy +import collections from typing import Dict, List, Union, cast +from web3 import Web3 -from aea.decision_maker.messages.transaction import TransactionMessage -from aea.helpers.dialogue.base import DialogueLabel -from aea.protocols.oef.models import Attribute, DataModel, Description, Query, Constraint, ConstraintType, Or, \ +from aea.helpers.search.models import Attribute, DataModel, Description, Query, Constraint, ConstraintType, Or, \ ConstraintExpr - -Address = str -TransactionId = str +from aea.mail.base import Address SUPPLY_DATAMODEL_NAME = 'supply' DEMAND_DATAMODEL_NAME = 'demand' -def build_goods_datamodel(good_pbks: List[str], is_supply: bool) -> DataModel: +def _build_goods_datamodel(good_ids: List[str], is_supply: bool) -> DataModel: """ Build a data model for supply and demand of goods (i.e. for offered or requested goods). - :param good_pbks: a list of public keys (i.e. identifiers) of the relevant goods. + :param good_ids: a list of ids (i.e. identifiers) of the relevant goods. :param currency: the currency used for trading. :param is_supply: Boolean indicating whether it is a supply or demand data model :return: the data model. """ - good_quantities_attributes = [Attribute(good_pbk, int, True, "A good on offer.") for good_pbk in good_pbks] - currency_attribute = Attribute('currency', str, True, "The currency for pricing and transacting the goods.") + good_quantities_attributes = [Attribute(good_id, int, True, "A good on offer.") for good_id in good_ids] + currency_attribute = Attribute('currency_id', str, True, "The currency for pricing and transacting the goods.") price_attribute = Attribute('price', int, False, "The price of the goods in the currency.") seller_tx_fee_attribute = Attribute('seller_tx_fee', int, False, "The transaction fee payable by the seller in the currency.") buyer_tx_fee_attribute = Attribute('buyer_tx_fee', int, False, "The transaction fee payable by the buyer in the currency.") + tx_nonce_attribute = Attribute('tx_nonce', str, False, "The nonce to distinguish identical descriptions.") description = SUPPLY_DATAMODEL_NAME if is_supply else DEMAND_DATAMODEL_NAME - attributes = good_quantities_attributes + [currency_attribute, price_attribute, seller_tx_fee_attribute, buyer_tx_fee_attribute] + attributes = good_quantities_attributes + [currency_attribute, price_attribute, seller_tx_fee_attribute, buyer_tx_fee_attribute, tx_nonce_attribute] data_model = DataModel(description, attributes) return data_model -def build_goods_description(good_pbk_to_quantities: Dict[str, int], currency: str, is_supply: bool) -> Description: +def build_goods_description(good_id_to_quantities: Dict[str, int], currency_id: str, is_supply: bool) -> Description: """ Get the service description (good quantities supplied or demanded and their price). - :param good_pbk_to_quantities: a dictionary mapping the public keys of the goods to the quantities. - :param currency: the currency used for pricing and transacting. + :param good_id_to_quantities: a dictionary mapping the ids of the goods to the quantities. + :param currency_id: the currency used for pricing and transacting. :param is_supply: True if the description is indicating supply, False if it's indicating demand. :return: the description to advertise on the Service Directory. """ - data_model = build_goods_datamodel(good_pbks=list(good_pbk_to_quantities.keys()), is_supply=is_supply) - values = cast(Dict[str, Union[int, str]], good_pbk_to_quantities) - values.update({'currency': currency}) + data_model = _build_goods_datamodel(good_ids=list(good_id_to_quantities.keys()), is_supply=is_supply) + values = cast(Dict[str, Union[int, str]], good_id_to_quantities) + values.update({'currency_id': currency_id}) desc = Description(values, data_model=data_model) return desc -def build_goods_query(good_pbks: List[str], currency: str, is_searching_for_sellers: bool) -> Query: +def build_goods_query(good_ids: List[str], currency_id: str, is_searching_for_sellers: bool) -> Query: """ Build buyer or seller search query. @@ -81,89 +79,98 @@ def build_goods_query(good_pbks: List[str], currency: str, is_searching_for_sell - to look for sellers if the agent is a buyer, or - to look for buyers if the agent is a seller. - In particular, if the agent is a buyer and the demanded good public keys are {'tac_good_0', 'tac_good_2', 'tac_good_3'}, the resulting constraint expression is: + In particular, if the agent is a buyer and the demanded good ids are {'tac_good_0', 'tac_good_2', 'tac_good_3'}, the resulting constraint expression is: tac_good_0 >= 1 OR tac_good_2 >= 1 OR tac_good_3 >= 1 That is, the OEF will return all the sellers that have at least one of the good in the query (assuming that the sellers are registered with the data model specified). - :param good_pbks: the list of good public keys to put in the query - :param currency: the currency used for pricing and transacting. + :param good_ids: the list of good ids to put in the query + :param currency_id: the currency used for pricing and transacting. :param is_searching_for_sellers: Boolean indicating whether the query is for sellers (supply) or buyers (demand). :return: the query """ - data_model = build_goods_datamodel(good_pbks=good_pbks, is_supply=is_searching_for_sellers) - constraints = [Constraint(good_pbk, ConstraintType(">=", 1)) for good_pbk in good_pbks] - constraints.append(Constraint('currency', ConstraintType("==", currency))) + data_model = _build_goods_datamodel(good_ids=good_ids, is_supply=is_searching_for_sellers) + constraints = [Constraint(good_id, ConstraintType(">=", 1)) for good_id in good_ids] + constraints.append(Constraint('currency_id', ConstraintType("==", currency_id))) constraint_expr = cast(List[ConstraintExpr], constraints) - if len(good_pbks) > 1: + if len(good_ids) > 1: constraint_expr = [Or(constraint_expr)] query = Query(constraint_expr, model=data_model) return query -def generate_transaction_id(agent_pbk: Address, opponent_pbk: Address, dialogue_label: DialogueLabel, agent_is_seller: bool) -> TransactionId: - """ - Make a transaction id. - - :param agent_pbk: the pbk of the agent. - :param opponent_pbk: the public key of the opponent. - :param dialogue_label: the dialogue label - :param agent_is_seller: boolean indicating if the agent is a seller - :return: a transaction id +def _get_hash(tx_sender_addr: Address, + tx_counterparty_addr: Address, + good_ids: List[int], + sender_supplied_quantities: List[int], + counterparty_supplied_quantities: List[int], + tx_amount: int, + tx_nonce: int) -> bytes: """ - # the format is {buyer_pbk}_{seller_pbk}_{dialogue_id}_{dialogue_starter_pbk} - assert opponent_pbk == dialogue_label.dialogue_opponent_pbk - buyer_pbk, seller_pbk = (opponent_pbk, agent_pbk) if agent_is_seller else (agent_pbk, opponent_pbk) - transaction_id = "{}_{}_{}_{}_{}".format(buyer_pbk, seller_pbk, dialogue_label.dialogue_starter_reference, dialogue_label.dialogue_responder_reference, dialogue_label.dialogue_starter_pbk) - return transaction_id - - -def dialogue_label_from_transaction_id(agent_pbk: Address, transaction_id: TransactionId) -> DialogueLabel: + Generate a hash from transaction information. + + :param tx_sender_addr: the sender address + :param tx_counterparty_addr: the counterparty address + :param good_ids: the list of good ids + :param sender_supplied_quantities: the quantities supplied by the sender (must all be positive) + :param counterparty_supplied_quantities: the quantities supplied by the counterparty (must all be positive) + :param tx_amount: the amount of the transaction + :param tx_nonce: the nonce of the transaction + :return: the hash """ - Recover dialogue label from transaction id. - - :param agent_pbk: the pbk of the agent. - :param transaction_id: the transaction id - :return: a dialogue label - """ - buyer_pbk, seller_pbk, dialogue_starter_reference, dialogue_responder_reference, dialogue_starter_pbk = transaction_id.split('_') - if agent_pbk == buyer_pbk: - dialogue_opponent_pbk = seller_pbk - else: - dialogue_opponent_pbk = buyer_pbk - dialogue_label = DialogueLabel((dialogue_starter_reference, dialogue_responder_reference), dialogue_opponent_pbk, dialogue_starter_pbk) - return dialogue_label - - -def generate_transaction_message(proposal_description: Description, dialogue_label: DialogueLabel, is_seller: bool, agent_public_key: str) -> TransactionMessage: + aggregate_hash = Web3.keccak(b''.join( + [good_ids[0].to_bytes(32, 'big'), sender_supplied_quantities[0].to_bytes(32, 'big'), counterparty_supplied_quantities[0].to_bytes(32, 'big')])) + for i in range(len(good_ids)): + if not i == 0: + aggregate_hash = Web3.keccak(b''.join( + [aggregate_hash, good_ids[i].to_bytes(32, 'big'), sender_supplied_quantities[i].to_bytes(32, 'big'), + counterparty_supplied_quantities[i].to_bytes(32, 'big')])) + + m_list = [] # type: List[bytes] + m_list.append(tx_sender_addr.encode('utf-8')) + m_list.append(tx_counterparty_addr.encode('utf-8')) + m_list.append(aggregate_hash) + m_list.append(tx_amount.to_bytes(32, 'big')) + m_list.append(tx_nonce.to_bytes(32, 'big')) + return Web3.keccak(b''.join(m_list)) + + +def tx_hash_from_values(tx_sender_addr: str, + tx_counterparty_addr: str, + tx_quantities_by_good_id: Dict[str, int], + tx_amount_by_currency_id: Dict[str, int], + tx_nonce: int) -> bytes: """ - Generate the transaction message from the description and the dialogue. + Get the hash for a transaction based on the transaction message. - :param proposal_description: the description of the proposal - :param dialogue_label: the dialogue label - :param is_seller: the agent is a seller - :param agent_public_key: the public key of the agent - :return: a transaction message + :param tx_message: the transaction message + :return: the hash """ - transaction_id = generate_transaction_id(agent_public_key, dialogue_label.dialogue_opponent_pbk, dialogue_label, is_seller) - sender_tx_fee = proposal_description.values['seller_tx_fee'] if is_seller else proposal_description.values['buyer_tx_fee'] - counterparty_tx_fee = proposal_description.values['buyer_tx_fee'] if is_seller else proposal_description.values['seller_tx_fee'] - goods_component = copy.copy(proposal_description.values) - [goods_component.pop(key) for key in ['seller_tx_fee', 'buyer_tx_fee', 'price', 'currency']] - transaction_msg = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE, - skill_id="tac_negotiation_skill", - transaction_id=transaction_id, - sender=agent_public_key, - counterparty=dialogue_label.dialogue_opponent_pbk, - currency_pbk=proposal_description.values['currency'], - amount=proposal_description.values['price'], - is_sender_buyer=not is_seller, - sender_tx_fee=sender_tx_fee, - counterparty_tx_fee=counterparty_tx_fee, - quantities_by_good_pbk=goods_component) - return transaction_msg + converted = {int(good_id): quantity for good_id, quantity in tx_quantities_by_good_id.items()} + ordered = collections.OrderedDict(sorted(converted.items())) + good_ids = [] # type: List[int] + sender_supplied_quantities = [] # type: List[int] + counterparty_supplied_quantities = [] # type: List[int] + for good_id, quantity in ordered.items(): + good_ids.append(good_id) + if quantity >= 0: + sender_supplied_quantities.append(quantity) + counterparty_supplied_quantities.append(0) + else: + sender_supplied_quantities.append(0) + counterparty_supplied_quantities.append(quantity) + assert len(tx_amount_by_currency_id) == 1 + tx_amount = list(tx_amount_by_currency_id.values())[0] + tx_hash = _get_hash(tx_sender_addr=tx_sender_addr, + tx_counterparty_addr=tx_counterparty_addr, + good_ids=good_ids, + sender_supplied_quantities=sender_supplied_quantities, + counterparty_supplied_quantities=counterparty_supplied_quantities, + tx_amount=tx_amount, + tx_nonce=tx_nonce) + return tx_hash diff --git a/packages/skills/tac_negotiation/registration.py b/packages/skills/tac_negotiation/registration.py index 3321356cf8..4248e399c0 100644 --- a/packages/skills/tac_negotiation/registration.py +++ b/packages/skills/tac_negotiation/registration.py @@ -22,7 +22,7 @@ import datetime from typing import Optional -from aea.protocols.oef.models import Description +from aea.helpers.search.models import Description from aea.skills.base import SharedClass diff --git a/packages/skills/tac_negotiation/skill.yaml b/packages/skills/tac_negotiation/skill.yaml index 2735636747..f340fbe2de 100644 --- a/packages/skills/tac_negotiation/skill.yaml +++ b/packages/skills/tac_negotiation/skill.yaml @@ -1,47 +1,48 @@ name: 'tac_negotiation' -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" description: "The tac negotiation skill implements the logic for an AEA to do fipa negotiation in the TAC." url: "" behaviours: - - behaviour: - class_name: GoodsRegisterAndSearchBehaviour - args: - services_interval: 5 + tac_negotiation: + class_name: GoodsRegisterAndSearchBehaviour + args: + services_interval: 5 handlers: - - handler: - class_name: FIPANegotiationHandler - args: {} - - handler: - class_name: TransactionHandler - args: {} - - handler: - class_name: OEFSearchHandler - args: {} + fipa: + class_name: FIPANegotiationHandler + args: {} + transaction: + class_name: TransactionHandler + args: {} + oef: + class_name: OEFSearchHandler + args: {} tasks: - - task: - class_name: TransactionCleanUpTask - args: {} + clean_up: + class_name: TransactionCleanUpTask + args: {} shared_classes: - - shared_class: - class_name: Search - args: - search_interval: 5 - - shared_class: - class_name: Registration - args: - update_interval: 5 - - shared_class: - class_name: Strategy - args: - register_as: both - search_for: both - - shared_class: - class_name: Dialogues - args: {} - - shared_class: - class_name: Transactions - args: - pending_transaction_timeout: 30 + search: + class_name: Search + args: + search_interval: 5 + registration: + class_name: Registration + args: + update_interval: 5 + strategy: + class_name: Strategy + args: + register_as: both + search_for: both + dialogues: + class_name: Dialogues + args: {} + transactions: + class_name: Transactions + args: + pending_transaction_timeout: 30 protocols: ['oef', 'fipa'] diff --git a/packages/skills/tac_negotiation/strategy.py b/packages/skills/tac_negotiation/strategy.py index 2ec5e3d511..27b0470fb0 100644 --- a/packages/skills/tac_negotiation/strategy.py +++ b/packages/skills/tac_negotiation/strategy.py @@ -26,7 +26,7 @@ import sys from typing import Dict, Optional, cast, TYPE_CHECKING -from aea.protocols.oef.models import Query, Description +from aea.helpers.search.models import Query, Description from aea.decision_maker.messages.transaction import TransactionMessage from aea.skills.base import SharedClass @@ -102,9 +102,9 @@ def get_own_service_description(self, is_supply: bool) -> Description: """ transactions = cast(Transactions, self.context.transactions) ownership_state_after_locks = transactions.ownership_state_after_locks(is_seller=is_supply) - good_pbk_to_quantities = self._supplied_goods(ownership_state_after_locks.quantities_by_good_pbk) if is_supply else self._demanded_goods(ownership_state_after_locks.quantities_by_good_pbk) - currency = list(ownership_state_after_locks.amount_by_currency.keys())[0] - desc = build_goods_description(good_pbk_to_quantities=good_pbk_to_quantities, currency=currency, is_supply=is_supply) + good_id_to_quantities = self._supplied_goods(ownership_state_after_locks.quantities_by_good_id) if is_supply else self._demanded_goods(ownership_state_after_locks.quantities_by_good_id) + currency_id = list(ownership_state_after_locks.amount_by_currency_id.keys())[0] + desc = build_goods_description(good_id_to_quantities=good_id_to_quantities, currency_id=currency_id, is_supply=is_supply) return desc def _supplied_goods(self, good_holdings: Dict[str, int]) -> Dict[str, int]: @@ -115,8 +115,8 @@ def _supplied_goods(self, good_holdings: Dict[str, int]) -> Dict[str, int]: :return: a dictionary of quantities supplied """ supply = {} # type: Dict[str, int] - for good_pbk, quantity in good_holdings.items(): - supply[good_pbk] = quantity - 1 if quantity > 1 else 0 + for good_id, quantity in good_holdings.items(): + supply[good_id] = quantity - 1 if quantity > 1 else 0 return supply def _demanded_goods(self, good_holdings: Dict[str, int]) -> Dict[str, int]: @@ -127,8 +127,8 @@ def _demanded_goods(self, good_holdings: Dict[str, int]) -> Dict[str, int]: :return: a dictionary of quantities supplied """ demand = {} # type: Dict[str, int] - for good_pbk in good_holdings.keys(): - demand[good_pbk] = 1 + for good_id in good_holdings.keys(): + demand[good_id] = 1 return demand def get_own_services_query(self, is_searching_for_sellers: bool) -> Query: @@ -145,9 +145,9 @@ def get_own_services_query(self, is_searching_for_sellers: bool) -> Query: """ transactions = cast(Transactions, self.context.transactions) ownership_state_after_locks = transactions.ownership_state_after_locks(is_seller=not is_searching_for_sellers) - good_pbk_to_quantities = self._demanded_goods(ownership_state_after_locks.quantities_by_good_pbk) if is_searching_for_sellers else self._supplied_goods(ownership_state_after_locks.quantities_by_good_pbk) - currency = list(ownership_state_after_locks.amount_by_currency.keys())[0] - query = build_goods_query(good_pbks=list(good_pbk_to_quantities.keys()), currency=currency, is_searching_for_sellers=is_searching_for_sellers) + good_id_to_quantities = self._demanded_goods(ownership_state_after_locks.quantities_by_good_id) if is_searching_for_sellers else self._supplied_goods(ownership_state_after_locks.quantities_by_good_id) + currency_id = list(ownership_state_after_locks.amount_by_currency_id.keys())[0] + query = build_goods_query(good_ids=list(good_id_to_quantities.keys()), currency_id=currency_id, is_searching_for_sellers=is_searching_for_sellers) return query def _get_proposal_for_query(self, query: Query, is_seller: bool) -> Optional[Description]: @@ -198,22 +198,22 @@ def _generate_candidate_proposals(self, is_seller: bool): """ transactions = cast(Transactions, self.context.transactions) ownership_state_after_locks = transactions.ownership_state_after_locks(is_seller=is_seller) - good_pbk_to_quantities = self._supplied_goods(ownership_state_after_locks.quantities_by_good_pbk) if is_seller else self._demanded_goods(ownership_state_after_locks.quantities_by_good_pbk) - nil_proposal_dict = {good_pbk: 0 for good_pbk, quantity in good_pbk_to_quantities.items()} # type: Dict[str, int] + good_id_to_quantities = self._supplied_goods(ownership_state_after_locks.quantities_by_good_id) if is_seller else self._demanded_goods(ownership_state_after_locks.quantities_by_good_id) + nil_proposal_dict = {good_id: 0 for good_id, quantity in good_id_to_quantities.items()} # type: Dict[str, int] proposals = [] seller_tx_fee = self.context.agent_preferences.transaction_fees['seller_tx_fee'] buyer_tx_fee = self.context.agent_preferences.transaction_fees['buyer_tx_fee'] - currency = list(self.context.agent_ownership_state.amount_by_currency.keys())[0] - for good_pbk, quantity in good_pbk_to_quantities.items(): + currency_id = list(self.context.agent_ownership_state.amount_by_currency_id.keys())[0] + for good_id, quantity in good_id_to_quantities.items(): if is_seller and quantity == 0: continue proposal_dict = nil_proposal_dict - proposal_dict[good_pbk] = 1 - proposal = build_goods_description(good_pbk_to_quantities=proposal_dict, currency=currency, is_supply=is_seller) + proposal_dict[good_id] = 1 + proposal = build_goods_description(good_id_to_quantities=proposal_dict, currency_id=currency_id, is_supply=is_seller) if is_seller: - delta_good_holdings = {good_pbk: quantity * -1 for good_pbk, quantity in proposal_dict.items()} # type: Dict[str, int] + delta_quantities_by_good_id = {good_id: quantity * -1 for good_id, quantity in proposal_dict.items()} # type: Dict[str, int] else: - delta_good_holdings = proposal_dict - marginal_utility_from_delta_good_holdings = self.context.agent_preferences.marginal_utility(ownership_state=ownership_state_after_locks, delta_good_holdings=delta_good_holdings) + delta_quantities_by_good_id = proposal_dict + marginal_utility_from_delta_good_holdings = self.context.agent_preferences.marginal_utility(ownership_state=ownership_state_after_locks, delta_quantities_by_good_id=delta_quantities_by_good_id) switch = -1 if is_seller else 1 breakeven_price_rounded = round(marginal_utility_from_delta_good_holdings) * switch if is_seller: @@ -223,6 +223,8 @@ def _generate_candidate_proposals(self, is_seller: bool): proposal.values["seller_tx_fee"] = seller_tx_fee proposal.values["buyer_tx_fee"] = buyer_tx_fee if not proposal.values["price"] > 0: continue + tx_nonce = transactions.get_next_tx_nonce() + proposal.values["tx_nonce"] = tx_nonce proposals.append(proposal) return proposals diff --git a/packages/skills/tac_negotiation/transactions.py b/packages/skills/tac_negotiation/transactions.py index 795785b712..ff6fd37823 100644 --- a/packages/skills/tac_negotiation/transactions.py +++ b/packages/skills/tac_negotiation/transactions.py @@ -20,19 +20,28 @@ """This module contains a class to manage transactions.""" +import copy import datetime import logging from collections import defaultdict, deque -from typing import Dict, Tuple, Deque, cast +import sys +from typing import Dict, Tuple, Deque, TYPE_CHECKING from aea.decision_maker.base import OwnershipState -from aea.decision_maker.messages.transaction import TransactionMessage, TransactionId +from aea.decision_maker.messages.transaction import TransactionMessage, TransactionId, OFF_CHAIN from aea.helpers.dialogue.base import DialogueLabel +from aea.helpers.search.models import Description +from aea.mail.base import Address from aea.skills.base import SharedClass +if TYPE_CHECKING or "pytest" in sys.modules: + from packages.skills.tac_negotiation.helpers import tx_hash_from_values +else: + from tac_negotiation_skill.helpers import tx_hash_from_values + logger = logging.getLogger("aea.tac_negotiation_skill") -MESSAGE_ID = int +MessageId = int class Transactions(SharedClass): @@ -42,25 +51,72 @@ def __init__(self, **kwargs) -> None: """Initialize the transactions.""" self._pending_transaction_timeout = kwargs.pop('pending_transaction_timeout') if 'pending_transaction_timeout' in kwargs.keys() else 30 super().__init__(**kwargs) - self._pending_proposals = defaultdict(lambda: {}) # type: Dict[DialogueLabel, Dict[MESSAGE_ID, TransactionMessage]] - self._pending_initial_acceptances = defaultdict(lambda: {}) # type: Dict[DialogueLabel, Dict[MESSAGE_ID, TransactionMessage]] + self._pending_proposals = defaultdict(lambda: {}) # type: Dict[DialogueLabel, Dict[MessageId, TransactionMessage]] + self._pending_initial_acceptances = defaultdict(lambda: {}) # type: Dict[DialogueLabel, Dict[MessageId, TransactionMessage]] self._locked_txs = {} # type: Dict[TransactionId, TransactionMessage] self._locked_txs_as_buyer = {} # type: Dict[TransactionId, TransactionMessage] self._locked_txs_as_seller = {} # type: Dict[TransactionId, TransactionMessage] self._last_update_for_transactions = deque() # type: Deque[Tuple[datetime.datetime, TransactionId]] + self._tx_nonce = 0 + self._tx_id = 0 @property - def pending_proposals(self) -> Dict[DialogueLabel, Dict[MESSAGE_ID, TransactionMessage]]: + def pending_proposals(self) -> Dict[DialogueLabel, Dict[MessageId, TransactionMessage]]: """Get the pending proposals.""" return self._pending_proposals @property - def pending_initial_acceptances(self) -> Dict[DialogueLabel, Dict[MESSAGE_ID, TransactionMessage]]: + def pending_initial_acceptances(self) -> Dict[DialogueLabel, Dict[MessageId, TransactionMessage]]: """Get the pending initial acceptances.""" return self._pending_initial_acceptances + def get_next_tx_nonce(self) -> int: + """Get the next nonce.""" + self._tx_nonce += 1 + return self._tx_nonce + + def get_internal_tx_id(self) -> TransactionId: + """Get an id for internal reference of the tx.""" + self._tx_id += 1 + return str(self._tx_id) + + def generate_transaction_message(self, performative: TransactionMessage.Performative, proposal_description: Description, dialogue_label: DialogueLabel, is_seller: bool, agent_addr: Address) -> TransactionMessage: + """ + Generate the transaction message from the description and the dialogue. + + :param proposal_description: the description of the proposal + :param dialogue_label: the dialogue label + :param is_seller: the agent is a seller + :param agent_addr: the address of the agent + :return: a transaction message + """ + sender_tx_fee = proposal_description.values['seller_tx_fee'] if is_seller else proposal_description.values['buyer_tx_fee'] + counterparty_tx_fee = proposal_description.values['buyer_tx_fee'] if is_seller else proposal_description.values['seller_tx_fee'] + goods_component = copy.copy(proposal_description.values) + [goods_component.pop(key) for key in ['seller_tx_fee', 'buyer_tx_fee', 'price', 'currency_id', 'tx_nonce']] + tx_hash = tx_hash_from_values(tx_sender_addr=agent_addr, + tx_counterparty_addr=dialogue_label.dialogue_opponent_addr, + tx_quantities_by_good_id=goods_component, + tx_amount_by_currency_id={proposal_description.values['currency_id']: proposal_description.values['price']}, + tx_nonce=proposal_description.values['tx_nonce']) + skill_callback_ids = ['tac_participation'] if performative == TransactionMessage.Performative.PROPOSE_FOR_SETTLEMENT else ['tac_negotiation'] + transaction_msg = TransactionMessage(performative=performative, + skill_callback_ids=skill_callback_ids, + tx_id=self.get_internal_tx_id(), + tx_sender_addr=agent_addr, + tx_counterparty_addr=dialogue_label.dialogue_opponent_addr, + tx_amount_by_currency_id={proposal_description.values['currency_id']: proposal_description.values['price']}, + tx_sender_fee=sender_tx_fee, + tx_counterparty_fee=counterparty_tx_fee, + tx_quantities_by_good_id=goods_component, + ledger_id=OFF_CHAIN, + info={'dialogue_label': dialogue_label.json, + 'tx_nonce': proposal_description.values['tx_nonce']}, + signing_payload={'tx_hash': tx_hash}) + return transaction_msg + def cleanup_pending_transactions(self) -> None: """ Remove all the pending messages (i.e. either proposals or acceptances) that have been stored for an amount of time longer than the timeout. @@ -82,7 +138,7 @@ def cleanup_pending_transactions(self) -> None: # extract dialogue label and message id transaction_id = next_item - logger.debug("Removing transaction: {}".format(transaction_id)) + logger.debug("[{}]: Removing transaction from pending list: {}".format(self.context.agent_name, transaction_id)) # remove (safely) the associated pending proposal (if present) self._locked_txs.pop(transaction_id, None) @@ -171,7 +227,7 @@ def add_locked_tx(self, transaction_msg: TransactionMessage, as_seller: bool) -> :return: None """ - transaction_id = cast(TransactionId, transaction_msg.get("transaction_id")) + transaction_id = transaction_msg.tx_id assert transaction_id not in self._locked_txs self._register_transaction_with_time(transaction_id) self._locked_txs[transaction_id] = transaction_msg @@ -189,7 +245,7 @@ def pop_locked_tx(self, transaction_msg: TransactionMessage) -> TransactionMessa :return: the transaction """ - transaction_id = cast(TransactionId, transaction_msg.get("transaction_id")) + transaction_id = transaction_msg.tx_id assert transaction_id in self._locked_txs transaction_msg = self._locked_txs.pop(transaction_id) self._locked_txs_as_buyer.pop(transaction_id, None) @@ -207,5 +263,5 @@ def ownership_state_after_locks(self, is_seller: bool) -> OwnershipState: :return: the agent state with the locks applied to current state """ transaction_msgs = list(self._locked_txs_as_seller.values()) if is_seller else list(self._locked_txs_as_buyer.values()) - ownership_state_after_locks = self.context.agent_ownership_state.apply(transaction_msgs) + ownership_state_after_locks = self.context.agent_ownership_state.apply_transactions(transaction_msgs) return ownership_state_after_locks diff --git a/packages/skills/tac_participation/behaviours.py b/packages/skills/tac_participation/behaviours.py index b544713c40..5c7970ef09 100644 --- a/packages/skills/tac_participation/behaviours.py +++ b/packages/skills/tac_participation/behaviours.py @@ -23,14 +23,16 @@ import sys from typing import cast, TYPE_CHECKING -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF from aea.skills.base import Behaviour if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.oef.message import OEFMessage + from packages.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF from packages.skills.tac_participation.game import Game, Phase from packages.skills.tac_participation.search import Search else: + from oef_protocol.message import OEFMessage + from oef_protocol.serialization import OEFSerializer, DEFAULT_OEF from tac_participation_skill.game import Game, Phase from tac_participation_skill.search import Search @@ -82,10 +84,10 @@ def _search_for_tac(self) -> None: search_id = search.get_next_id() search.ids_for_tac.add(search_id) logger.info("[{}]: Searching for TAC, search_id={}".format(self.context.agent_name, search_id)) - oef_msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_SERVICES, + oef_msg = OEFMessage(type=OEFMessage.Type.SEARCH_SERVICES, id=search_id, query=query) self.context.outbox.put_message(to=DEFAULT_OEF, - sender=self.context.agent_public_key, + sender=self.context.agent_address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(oef_msg)) diff --git a/packages/skills/tac_participation/game.py b/packages/skills/tac_participation/game.py index 40dd3c77e5..10763c07c2 100644 --- a/packages/skills/tac_participation/game.py +++ b/packages/skills/tac_participation/game.py @@ -21,18 +21,17 @@ from enum import Enum import logging import sys -from typing import Dict, List, Optional, cast, TYPE_CHECKING +from typing import Dict, List, Optional, TYPE_CHECKING -from aea.protocols.oef.models import Query, Constraint, ConstraintType +from aea.helpers.search.models import Query, Constraint, ConstraintType from aea.skills.base import SharedClass +from aea.mail.base import Address if TYPE_CHECKING or "pytest" in sys.modules: from packages.protocols.tac.message import TACMessage else: from tac_protocol.message import TACMessage -Address = str - logger = logging.getLogger("aea.tac_participation_skill") @@ -52,25 +51,25 @@ class Configuration: def __init__(self, version_id: str, tx_fee: int, - agent_pbk_to_name: Dict[Address, str], - good_pbk_to_name: Dict[Address, str], - controller_pbk: Address): + agent_addr_to_name: Dict[Address, str], + good_id_to_name: Dict[str, str], + controller_addr: Address): """ Instantiate a game configuration. :param version_id: the version of the game. :param tx_fee: the fee for a transaction. - :param agent_pbk_to_name: a dictionary mapping agent public keys to agent names (as strings). - :param good_pbk_to_name: a dictionary mapping good public keys to good names (as strings). - :param controller_pbk: the public key of the controller + :param agent_addr_to_name: a dictionary mapping agent addresses to agent names (as strings). + :param good_id_to_name: a dictionary mapping good ids to good names (as strings). + :param controller_addr: the address of the controller """ self._version_id = version_id - self._nb_agents = len(agent_pbk_to_name) - self._nb_goods = len(good_pbk_to_name) + self._nb_agents = len(agent_addr_to_name) + self._nb_goods = len(good_id_to_name) self._tx_fee = tx_fee - self._agent_pbk_to_name = agent_pbk_to_name - self._good_pbk_to_name = good_pbk_to_name - self._controller_pbk = controller_pbk + self._agent_addr_to_name = agent_addr_to_name + self._good_id_to_name = good_id_to_name + self._controller_addr = controller_addr self._check_consistency() @@ -95,39 +94,39 @@ def tx_fee(self) -> int: return self._tx_fee @property - def agent_pbk_to_name(self) -> Dict[Address, str]: - """Map agent public keys to names.""" - return self._agent_pbk_to_name + def agent_addr_to_name(self) -> Dict[Address, str]: + """Map agent addresses to names.""" + return self._agent_addr_to_name @property - def good_pbk_to_name(self) -> Dict[Address, str]: - """Map good public keys to names.""" - return self._good_pbk_to_name + def good_id_to_name(self) -> Dict[Address, str]: + """Map good ids to names.""" + return self._good_id_to_name @property - def agent_pbks(self) -> List[Address]: - """List of agent public keys.""" - return list(self._agent_pbk_to_name.keys()) + def agent_addresses(self) -> List[Address]: + """List of agent addresses.""" + return list(self._agent_addr_to_name.keys()) @property def agent_names(self): """List of agent names.""" - return list(self._agent_pbk_to_name.values()) + return list(self._agent_addr_to_name.values()) @property - def good_pbks(self) -> List[Address]: - """List of good public keys.""" - return list(self._good_pbk_to_name.keys()) + def good_ids(self) -> List[Address]: + """List of good ids.""" + return list(self._good_id_to_name.keys()) @property def good_names(self) -> List[str]: """List of good names.""" - return list(self._good_pbk_to_name.values()) + return list(self._good_id_to_name.values()) @property - def controller_pbk(self) -> str: - """Get the controller pbk.""" - return self._controller_pbk + def controller_addr(self) -> str: + """Get the controller address.""" + return self._controller_addr def _check_consistency(self): """ @@ -140,9 +139,9 @@ def _check_consistency(self): assert self.tx_fee >= 0, "Tx fee must be non-negative." assert self.nb_agents > 1, "Must have at least two agents." assert self.nb_goods > 1, "Must have at least two goods." - assert len(self.agent_pbks) == self.nb_agents, "There must be one public key for each agent." + assert len(self.agent_addresses) == self.nb_agents, "There must be one address for each agent." assert len(set(self.agent_names)) == self.nb_agents, "Agents' names must be unique." - assert len(self.good_pbks) == self.nb_goods, "There must be one public key for each good." + assert len(self.good_ids) == self.nb_goods, "There must be one id for each good." assert len(set(self.good_names)) == self.nb_goods, "Goods' names must be unique." @@ -152,7 +151,7 @@ class Game(SharedClass): def __init__(self, **kwargs): """Instantiate the game class.""" self._expected_version_id = kwargs.pop('expected_version_id', '') # type: str - self._expected_controller_pbk = kwargs.pop('expected_controller_pbk', None) # type: Optional[str] + self._expected_controller_addr = kwargs.pop('expected_controller_addr', None) # type: Optional[str] super().__init__(**kwargs) self._phase = Phase.PRE_GAME self._configuration = None # type: Optional[Configuration] @@ -168,10 +167,10 @@ def phase(self) -> Phase: return self._phase @property - def expected_controller_pbk(self) -> Address: + def expected_controller_addr(self) -> Address: """Get the expected controller pbk.""" - assert self._expected_controller_pbk is not None, "Expected controller public key not assigned!" - return self._expected_controller_pbk + assert self._expected_controller_addr is not None, "Expected controller address not assigned!" + return self._expected_controller_addr @property def configuration(self) -> Configuration: @@ -179,34 +178,34 @@ def configuration(self) -> Configuration: assert self._configuration is not None, "Game configuration not assigned!" return self._configuration - def init(self, tac_message: TACMessage, controller_pbk: Address) -> None: + def init(self, tac_message: TACMessage, controller_addr: Address) -> None: """ Populate data structures with the game data. :param tac_message: the tac message with the game instance data - :param controller_pbk: the public key of the controller + :param controller_addr: the address of the controller :return: None """ - assert tac_message.get("type") == TACMessage.Type.GAME_DATA, "Wrong TACMessage for initialization of TAC game." - assert controller_pbk == self.expected_controller_pbk, "TACMessage from unexpected controller." - assert tac_message.get("version_id") == self.expected_version_id, "TACMessage for unexpected game." - self._configuration = Configuration(cast(str, tac_message.get("version_id")), - cast(int, tac_message.get("tx_fee")), - cast(Dict[str, str], tac_message.get("agent_pbk_to_name")), - cast(Dict[str, str], tac_message.get("good_pbk_to_name")), - controller_pbk) - - def update_expected_controller_pbk(self, controller_pbk: Address): + assert tac_message.type == TACMessage.Type.GAME_DATA, "Wrong TACMessage for initialization of TAC game." + assert controller_addr == self.expected_controller_addr, "TACMessage from unexpected controller." + assert tac_message.version_id == self.expected_version_id, "TACMessage for unexpected game." + self._configuration = Configuration(tac_message.version_id, + tac_message.tx_fee, + tac_message.agent_addr_to_name, + tac_message.good_id_to_name, + controller_addr) + + def update_expected_controller_addr(self, controller_addr: Address): """ Overwrite the expected controller pbk. - :param controller_pbk: the public key of the controller + :param controller_addr: the address of the controller :return: None """ - logger.warning("TAKE CARE! Circumventing controller identity check! For added security provide the expected controller key as an argument to the Game instance and check against it.") - self._expected_controller_pbk = controller_pbk + logger.warning("[{}]: TAKE CARE! Circumventing controller identity check! For added security provide the expected controller key as an argument to the Game instance and check against it.".format(self.context.agent_name)) + self._expected_controller_addr = controller_addr def update_game_phase(self, phase: Phase) -> None: """ diff --git a/packages/skills/tac_participation/handlers.py b/packages/skills/tac_participation/handlers.py index a1af033bb9..3ba0082757 100644 --- a/packages/skills/tac_participation/handlers.py +++ b/packages/skills/tac_participation/handlers.py @@ -21,29 +21,29 @@ import logging import sys -from typing import Dict, List, Optional, cast, TYPE_CHECKING +from typing import List, Optional, cast, TYPE_CHECKING from aea.configurations.base import ProtocolId from aea.decision_maker.messages.state_update import StateUpdateMessage from aea.decision_maker.messages.transaction import TransactionMessage from aea.protocols.base import Message -from aea.protocols.oef.message import OEFMessage from aea.skills.base import Handler +from aea.mail.base import Address if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.oef.message import OEFMessage from packages.protocols.tac.message import TACMessage from packages.protocols.tac.serialization import TACSerializer from packages.skills.tac_participation.game import Game, Phase from packages.skills.tac_participation.search import Search else: + from oef_protocol.message import OEFMessage from tac_protocol.message import TACMessage from tac_protocol.serialization import TACSerializer from tac_participation_skill.game import Game, Phase from tac_participation_skill.search import Search -Address = str - logger = logging.getLogger("aea.tac_participation_skill") @@ -65,16 +65,15 @@ def setup(self) -> None: """ pass - def handle(self, message: Message, sender: str) -> None: + def handle(self, message: Message) -> None: """ Implement the reaction to a message. :param message: the message - :param sender: the sender :return: None """ oef_message = cast(OEFMessage, message) - oef_type = oef_message.get("type") + oef_type = oef_message.type logger.debug("[{}]: Handling OEF message. type={}".format(self.context.agent_name, oef_type)) if oef_type == OEFMessage.Type.SEARCH_RESULT: @@ -101,7 +100,7 @@ def _on_oef_error(self, oef_error: OEFMessage) -> None: :return: None """ logger.error("[{}]: Received OEF error: answer_id={}, operation={}" - .format(self.context.agent_name, oef_error.get("id"), oef_error.get("operation"))) + .format(self.context.agent_name, oef_error.id, oef_error.operation)) def _on_dialogue_error(self, dialogue_error: OEFMessage) -> None: """ @@ -112,7 +111,7 @@ def _on_dialogue_error(self, dialogue_error: OEFMessage) -> None: :return: None """ logger.error("[{}]: Received Dialogue error: answer_id={}, dialogue_id={}, origin={}" - .format(self.context.agent_name, dialogue_error.get("id"), dialogue_error.get("dialogue_id"), dialogue_error.get("origin"))) + .format(self.context.agent_name, dialogue_error.id, dialogue_error.dialogue_id, dialogue_error.origin)) def _on_search_result(self, search_result: OEFMessage) -> None: """ @@ -123,20 +122,19 @@ def _on_search_result(self, search_result: OEFMessage) -> None: :return: None """ search = cast(Search, self.context.search) - search_id = search_result.get("id") - agents = search_result.get("agents") - agents = cast(List[str], agents) + search_id = search_result.id + agents = search_result.agents logger.debug("[{}]: on search result: {} {}".format(self.context.agent_name, search_id, agents)) if search_id in search.ids_for_tac: self._on_controller_search_result(agents) else: logger.debug("[{}]: Unknown search id: search_id={}".format(self.context.agent_name, search_id)) - def _on_controller_search_result(self, agent_pbks: List[Address]) -> None: + def _on_controller_search_result(self, agent_addresses: List[Address]) -> None: """ Process the search result for a controller. - :param agent_pbks: list of agent pbks + :param agent_addresses: list of agent addresses :return: None """ @@ -145,48 +143,48 @@ def _on_controller_search_result(self, agent_pbks: List[Address]) -> None: logger.debug("[{}]: Ignoring controller search result, the agent is already competing.".format(self.context.agent_name)) return - if len(agent_pbks) == 0: + if len(agent_addresses) == 0: logger.info("[{}]: Couldn't find the TAC controller. Retrying...".format(self.context.agent_name)) - elif len(agent_pbks) > 1: + elif len(agent_addresses) > 1: logger.error("[{}]: Found more than one TAC controller. Retrying...".format(self.context.agent_name)) # elif self._rejoin: # logger.debug("[{}]: Found the TAC controller. Rejoining...".format(self.context.agent_name)) - # controller_pbk = agent_pbks[0] - # self._rejoin_tac(controller_pbk) + # controller_addr = agent_addresses[0] + # self._rejoin_tac(controller_addr) else: logger.info("[{}]: Found the TAC controller. Registering...".format(self.context.agent_name)) - controller_pbk = agent_pbks[0] - self._register_to_tac(controller_pbk) + controller_addr = agent_addresses[0] + self._register_to_tac(controller_addr) - def _register_to_tac(self, controller_pbk: Address) -> None: + def _register_to_tac(self, controller_addr: Address) -> None: """ Register to active TAC Controller. - :param controller_pbk: the public key of the controller. + :param controller_addr: the address of the controller. :return: None """ game = cast(Game, self.context.game) - game.update_expected_controller_pbk(controller_pbk) + game.update_expected_controller_addr(controller_addr) game.update_game_phase(Phase.GAME_REGISTRATION) - tac_msg = TACMessage(tac_type=TACMessage.Type.REGISTER, agent_name=self.context.agent_name) + tac_msg = TACMessage(type=TACMessage.Type.REGISTER, agent_name=self.context.agent_name) tac_bytes = TACSerializer().encode(tac_msg) - self.context.outbox.put_message(to=controller_pbk, sender=self.context.agent_public_key, protocol_id=TACMessage.protocol_id, message=tac_bytes) + self.context.outbox.put_message(to=controller_addr, sender=self.context.agent_address, protocol_id=TACMessage.protocol_id, message=tac_bytes) - # def _rejoin_tac(self, controller_pbk: Address) -> None: + # def _rejoin_tac(self, controller_addr: Address) -> None: # """ # Rejoin the TAC run by a Controller. - # :param controller_pbk: the public key of the controller. + # :param controller_addr: the address of the controller. # :return: None # """ # game = cast(Game, self.context.game) - # game.update_expected_controller_pbk(controller_pbk) + # game.update_expected_controller_addr(controller_addr) # game.update_game_phase(Phase.GAME_SETUP) # tac_msg = TACMessage(tac_type=TACMessage.Type.GET_STATE_UPDATE) # tac_bytes = TACSerializer().encode(tac_msg) - # self.context.outbox.put_message(to=controller_pbk, sender=self.context.agent_public_key, protocol_id=TACMessage.protocol_id, message=tac_bytes) + # self.context.outbox.put_message(to=controller_addr, sender=self.context.agent_address, protocol_id=TACMessage.protocol_id, message=tac_bytes) class TACHandler(Handler): @@ -202,34 +200,33 @@ def setup(self) -> None: """ pass - def handle(self, message: Message, sender: str) -> None: + def handle(self, message: Message) -> None: """ Implement the reaction to a message. :param message: the message - :param sender: the sender :return: None """ tac_msg = cast(TACMessage, message) - tac_msg_type = TACMessage.Type(tac_msg.get("type")) + tac_msg_type = tac_msg.type game = cast(Game, self.context.game) logger.debug("[{}]: Handling controller response. type={}".format(self.context.agent_name, tac_msg_type)) try: - if sender != game.expected_controller_pbk: + if message.counterparty != game.expected_controller_addr: raise ValueError("The sender of the message is not the controller agent we registered with.") if tac_msg_type == TACMessage.Type.TAC_ERROR: - self._on_tac_error(tac_msg, sender) + self._on_tac_error(tac_msg) elif game.phase.value == Phase.PRE_GAME.value: raise ValueError("We do not expect a controller agent message in the pre game phase.") elif game.phase.value == Phase.GAME_REGISTRATION.value: if tac_msg_type == TACMessage.Type.GAME_DATA: - self._on_start(tac_msg, sender) + self._on_start(tac_msg) elif tac_msg_type == TACMessage.Type.CANCELLED: self._on_cancelled() elif game.phase.value == Phase.GAME.value: if tac_msg_type == TACMessage.Type.TRANSACTION_CONFIRMATION: - self._on_transaction_confirmed(tac_msg, sender) + self._on_transaction_confirmed(tac_msg) elif tac_msg_type == TACMessage.Type.CANCELLED: self._on_cancelled() # elif tac_msg_type == TACMessage.Type.STATE_UPDATE: @@ -247,22 +244,22 @@ def teardown(self) -> None: """ pass - def _on_tac_error(self, tac_message: TACMessage, controller_pbk: Address) -> None: + def _on_tac_error(self, tac_message: TACMessage) -> None: """ Handle 'on tac error' event emitted by the controller. - :param error: the error object + :param tac_message: The tac message. :return: None """ - error_code = TACMessage.ErrorCode(tac_message.get("error_code")) + error_code = tac_message.error_code logger.error("[{}]: Received error from the controller. error_msg={}".format(self.context.agent_name, TACMessage._from_ec_to_msg.get(error_code))) if error_code == TACMessage.ErrorCode.TRANSACTION_NOT_VALID: - start_idx_of_tx_id = len("Error in checking transaction: ") - transaction_id = cast(str, tac_message.get("error_msg"))[start_idx_of_tx_id:] - logger.warning("[{}]: Received error on transaction id: {}".format(self.context.agent_name, transaction_id)) + info = tac_message.info + transaction_id = cast(str, info.get("transaction_id")) if (info.get("transaction_id") is not None) else 'NO_TX_ID' + logger.warning("[{}]: Received error on transaction id: {}".format(self.context.agent_name, transaction_id[-10:])) - def _on_start(self, tac_message: TACMessage, controller_pbk: Address) -> None: + def _on_start(self, tac_message: TACMessage) -> None: """ Handle the 'start' event emitted by the controller. @@ -272,14 +269,14 @@ def _on_start(self, tac_message: TACMessage, controller_pbk: Address) -> None: """ logger.info("[{}]: Received start event from the controller. Starting to compete...".format(self.context.agent_name)) game = cast(Game, self.context.game) - game.init(tac_message, controller_pbk) + game.init(tac_message, tac_message.counterparty) game.update_game_phase(Phase.GAME) state_update_msg = StateUpdateMessage(performative=StateUpdateMessage.Performative.INITIALIZE, - amount_by_currency=cast(Dict[str, int], tac_message.get("amount_by_currency")), - quantities_by_good_pbk=cast(Dict[str, int], tac_message.get("quantities_by_good_pbk")), - exchange_params_by_currency=cast(Dict[str, float], tac_message.get("exchange_params_by_currency")), - utility_params_by_good_pbk=cast(Dict[str, float], tac_message.get("utility_params_by_good_pbk")), - tx_fee=cast(int, tac_message.get("tx_fee"))) + amount_by_currency_id=tac_message.amount_by_currency_id, + quantities_by_good_id=tac_message.quantities_by_good_id, + exchange_params_by_currency_id=tac_message.exchange_params_by_currency_id, + utility_params_by_good_id=tac_message.utility_params_by_good_id, + tx_fee=tac_message.tx_fee) self.context.decision_maker_message_queue.put_nowait(state_update_msg) def _on_cancelled(self) -> None: @@ -292,31 +289,31 @@ def _on_cancelled(self) -> None: game = cast(Game, self.context.game) game.update_game_phase(Phase.POST_GAME) - def _on_transaction_confirmed(self, message: TACMessage, controller_pbk: Address) -> None: + def _on_transaction_confirmed(self, message: TACMessage) -> None: """ Handle 'on transaction confirmed' event emitted by the controller. - :param tx_confirmation: the transaction confirmation + :param message: the TACMessage. :return: None """ - logger.info("[{}]: Received transaction confirmation from the controller: transaction_id={}".format(self.context.agent_name, message.get("transaction_id"))) + logger.info("[{}]: Received transaction confirmation from the controller: transaction_id={}".format(self.context.agent_name, message.tx_id[-10:])) state_update_msg = StateUpdateMessage(performative=StateUpdateMessage.Performative.APPLY, - amount_by_currency=cast(Dict[str, int], message.get("amount_by_currency")), - quantities_by_good_pbk=cast(Dict[str, int], message.get("quantities_by_good_pbk"))) + amount_by_currency_id=message.amount_by_currency_id, + quantities_by_good_id=message.quantities_by_good_id) self.context.decision_maker_message_queue.put_nowait(state_update_msg) - # def _on_state_update(self, tac_message: TACMessage, controller_pbk: Address) -> None: + # def _on_state_update(self, tac_message: TACMessage, controller_addr: Address) -> None: # """ # Update the game instance with a State Update from the controller. # :param tac_message: the state update - # :param controller_pbk: the public key of the controller + # :param controller_addr: the address of the controller # :return: None # """ # game = cast(Game, self.context.game) - # game.init(tac_message, controller_pbk) + # game.init(tac_message, controller_addr) # game.update_game_phase(Phase.GAME) # # for tx in message.get("transactions"): # # self.agent_state.update(tx, tac_message.get("initial_state").get("tx_fee")) @@ -324,20 +321,20 @@ def _on_transaction_confirmed(self, message: TACMessage, controller_pbk: Address # self._initial_agent_state = AgentStateUpdate(game_data.money, game_data.endowment, game_data.utility_params) # self._agent_state = AgentState(game_data.money, game_data.endowment, game_data.utility_params) # # if self.strategy.is_world_modeling: - # # opponent_pbks = self.game_configuration.agent_pbks - # # opponent_pbks.remove(agent_pbk) - # # self._world_state = WorldState(opponent_pbks, self.game_configuration.good_pbks, self.initial_agent_state) - - def _on_dialogue_error(self, tac_message: TACMessage, controller_pbk: Address) -> None: - """ - Handle dialogue error event emitted by the controller. + # # opponent_addrs = self.game_configuration.agent_addresses + # # opponent_addrs.remove(agent_addr) + # # self._world_state = WorldState(opponent_addrs, self.game_configuration.good_addrs, self.initial_agent_state) - :param message: the dialogue error message - :param controller_pbk: the address of the controller + # def _on_dialogue_error(self, tac_message: TACMessage) -> None: + # """ + # Handle dialogue error event emitted by the controller. - :return: None - """ - logger.warning("[{}]: Received Dialogue error from: details={}, sender={}".format(self.context.agent_name, tac_message.get("details"), controller_pbk)) + # :param tac_message: the dialogue error message + # :return: None + # """ + # logger.warning("[{}]: Received Dialogue error from: details={}, sender={}".format(self.context.agent_name, + # tac_message.details, + # tac_message.counterparty)) # def _request_state_update(self) -> None: # """ @@ -348,7 +345,7 @@ def _on_dialogue_error(self, tac_message: TACMessage, controller_pbk: Address) - # tac_msg = TACMessage(tac_type=TACMessage.Type.GET_STATE_UPDATE) # tac_bytes = TACSerializer().encode(tac_msg) # game = cast(Game, self.context.game) - # self.context.outbox.put_message(to=game.expected_controller_pbk, sender=self.context.agent_public_key, protocol_id=TACMessage.protocol_id, message=tac_bytes) + # self.context.outbox.put_message(to=game.expected_controller_addr, sender=self.context.agent_address, protocol_id=TACMessage.protocol_id, message=tac_bytes) class TransactionHandler(Handler): @@ -364,27 +361,31 @@ def setup(self) -> None: """ pass - def handle(self, message: Message, sender: str) -> None: + def handle(self, message: Message) -> None: """ Dispatch message to relevant handler and respond. :param message: the message - :param sender: the sender :return: None """ tx_message = cast(TransactionMessage, message) - if TransactionMessage.Performative(tx_message.get("performative")) == TransactionMessage.Performative.ACCEPT: + if tx_message.performative == TransactionMessage.Performative.SUCCESSFUL_SETTLEMENT: logger.info("[{}]: transaction confirmed by decision maker, sending to controller.".format(self.context.agent_name)) game = cast(Game, self.context.game) + tx_counterparty_signature = cast(bytes, tx_message.info.get('tx_counterparty_signature')) + assert tx_counterparty_signature is not None msg = TACMessage(type=TACMessage.Type.TRANSACTION, - transaction_id=tx_message.get("transaction_digest"), - counterparty=tx_message.get("counterparty"), - amount_by_currency={tx_message.get("currency"): tx_message.get("amount")}, - sender_tx_fee=tx_message.get("sender_tx_fee"), - counterparty_tx_fee=tx_message.get("counterparty_tx_fee"), - quantities_by_good_pbk=tx_message.get("quantities_by_good_pbk")) - self.context.outbox.put_message(to=game.configuration.controller_pbk, - sender=self.context.agent_public_key, + tx_id=tx_message.tx_id, + tx_sender_addr=tx_message.tx_sender_addr, + tx_counterparty_addr=tx_message.tx_counterparty_addr, + amount_by_currency_id=tx_message.tx_amount_by_currency_id, + tx_sender_fee=tx_message.tx_sender_fee, + tx_counterparty_fee=tx_message.tx_counterparty_fee, + quantities_by_good_id=tx_message.tx_quantities_by_good_id, + tx_sender_signature=tx_message.tx_signature, + tx_counterparty_signature=tx_message.info.get('tx_counterparty_signature')) + self.context.outbox.put_message(to=game.configuration.controller_addr, + sender=self.context.agent_address, protocol_id=TACMessage.protocol_id, message=TACSerializer().encode(msg)) else: diff --git a/packages/skills/tac_participation/skill.yaml b/packages/skills/tac_participation/skill.yaml index cfb7ca018a..2b5df669c5 100644 --- a/packages/skills/tac_participation/skill.yaml +++ b/packages/skills/tac_participation/skill.yaml @@ -1,29 +1,33 @@ name: tac_participation -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" description: "The tac participation skill implements the logic for an AEA to participate in the TAC." behaviours: - - behaviour: - class_name: TACBehaviour - args: {} + tac: + class_name: TACBehaviour + args: {} handlers: - - handler: - class_name: OEFHandler - args: {} - - handler: - class_name: TACHandler - args: {} -tasks: [] + oef: + class_name: OEFHandler + args: {} + tac: + class_name: TACHandler + args: {} + transaction: + class_name: TransactionHandler + args: {} +tasks: {} shared_classes: - - shared_class: - class_name: Search - args: - search_interval: 5 - - shared_class: - class_name: Game - args: - expected_version_id: v1 - # expected_controller_pbk: '' # optionally, provide a key for the controller you expect! + search: + class_name: Search + args: + search_interval: 5 + game: + class_name: Game + args: + expected_version_id: v1 + # expected_controller_addr: '' # optionally, provide an address for the controller you expect! protocols: ['oef', 'tac'] diff --git a/packages/skills/weather_client/behaviours.py b/packages/skills/weather_client/behaviours.py index dec0ca584f..1c6d16a67d 100644 --- a/packages/skills/weather_client/behaviours.py +++ b/packages/skills/weather_client/behaviours.py @@ -22,21 +22,30 @@ import sys from typing import cast, TYPE_CHECKING -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.serialization import DEFAULT_OEF, OEFSerializer -from aea.skills.base import Behaviour +from aea.skills.behaviours import TickerBehaviour if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.oef.message import OEFMessage + from packages.protocols.oef.serialization import DEFAULT_OEF, OEFSerializer from packages.skills.weather_client.strategy import Strategy else: + from oef_protocol.message import OEFMessage + from oef_protocol.serialization import DEFAULT_OEF, OEFSerializer from weather_client_skill.strategy import Strategy logger = logging.getLogger("aea.weather_client_skill") +DEFAULT_SEARCH_INTERVAL = 5.0 -class MySearchBehaviour(Behaviour): + +class MySearchBehaviour(TickerBehaviour): """This class scaffolds a behaviour.""" + def __init__(self, **kwargs): + """Initialize the search behaviour.""" + search_interval = cast(float, kwargs.pop('search_interval')) if 'search_interval' in kwargs.keys() else DEFAULT_SEARCH_INTERVAL + super().__init__(tick_interval=search_interval, **kwargs) + def setup(self) -> None: """Implement the setup for the behaviour.""" pass @@ -48,14 +57,14 @@ def act(self) -> None: :return: None """ strategy = cast(Strategy, self.context.strategy) - if strategy.is_time_to_search(): + if strategy.is_searching: query = strategy.get_service_query() search_id = strategy.get_next_search_id() - oef_msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_SERVICES, + oef_msg = OEFMessage(type=OEFMessage.Type.SEARCH_SERVICES, id=search_id, query=query) self.context.outbox.put_message(to=DEFAULT_OEF, - sender=self.context.agent_public_key, + sender=self.context.agent_address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(oef_msg)) diff --git a/packages/skills/weather_client/dialogues.py b/packages/skills/weather_client/dialogues.py index 4f956bb08a..730e5945cc 100644 --- a/packages/skills/weather_client/dialogues.py +++ b/packages/skills/weather_client/dialogues.py @@ -25,13 +25,18 @@ - Dialogues: The dialogues class keeps track of all dialogues. """ -from typing import Optional +import sys +from typing import Optional, TYPE_CHECKING from aea.helpers.dialogue.base import DialogueLabel -from aea.protocols.fipa.dialogues import FIPADialogues, FIPADialogue -from aea.protocols.oef.models import Description +from aea.helpers.search.models import Description from aea.skills.base import SharedClass +if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.fipa.dialogues import FIPADialogues, FIPADialogue +else: + from fipa_protocol.dialogues import FIPADialogues, FIPADialogue + class Dialogue(FIPADialogue): """The dialogue class maintains state of a dialogue and manages it.""" diff --git a/packages/skills/weather_client/handlers.py b/packages/skills/weather_client/handlers.py index 5b55ac984d..39c37f533a 100644 --- a/packages/skills/weather_client/handlers.py +++ b/packages/skills/weather_client/handlers.py @@ -24,19 +24,22 @@ from typing import List, Optional, cast, TYPE_CHECKING from aea.configurations.base import ProtocolId +from aea.helpers.search.models import Description from aea.protocols.base import Message from aea.protocols.default.message import DefaultMessage from aea.protocols.default.serialization import DefaultSerializer -from aea.protocols.fipa.message import FIPAMessage -from aea.protocols.fipa.serialization import FIPASerializer -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.models import Description from aea.skills.base import Handler if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.fipa.message import FIPAMessage + from packages.protocols.fipa.serialization import FIPASerializer + from packages.protocols.oef.message import OEFMessage from packages.skills.weather_client.dialogues import Dialogue, Dialogues from packages.skills.weather_client.strategy import Strategy else: + from fipa_protocol.message import FIPAMessage + from fipa_protocol.serialization import FIPASerializer + from oef_protocol.message import OEFMessage from weather_client_skill.dialogues import Dialogue, Dialogues from weather_client_skill.strategy import Strategy @@ -56,37 +59,34 @@ def setup(self) -> None: """ pass - def handle(self, message: Message, sender: str) -> None: + def handle(self, message: Message) -> None: """ Implement the reaction to a message. :param message: the message - :param sender: the sender :return: None """ # convenience representations fipa_msg = cast(FIPAMessage, message) - msg_performative = FIPAMessage.Performative(message.get('performative')) - message_id = cast(int, message.get("message_id")) # recover dialogue dialogues = cast(Dialogues, self.context.dialogues) - if dialogues.is_belonging_to_registered_dialogue(fipa_msg, sender, self.context.agent_public_key): - dialogue = cast(Dialogue, dialogues.get_dialogue(fipa_msg, sender, self.context.agent_public_key)) + if dialogues.is_belonging_to_registered_dialogue(fipa_msg, self.context.agent_address): + dialogue = cast(Dialogue, dialogues.get_dialogue(fipa_msg, self.context.agent_address)) dialogue.incoming_extend(fipa_msg) else: - self._handle_unidentified_dialogue(fipa_msg, sender) + self._handle_unidentified_dialogue(fipa_msg) return # handle message - if msg_performative == FIPAMessage.Performative.PROPOSE: - self._handle_propose(fipa_msg, sender, message_id, dialogue) - elif msg_performative == FIPAMessage.Performative.DECLINE: - self._handle_decline(fipa_msg, sender, message_id, dialogue) - elif msg_performative == FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS: - self._handle_match_accept(fipa_msg, sender, message_id, dialogue) - elif msg_performative == FIPAMessage.Performative.INFORM: - self._handle_inform(fipa_msg, sender, message_id, dialogue) + if fipa_msg.performative == FIPAMessage.Performative.PROPOSE: + self._handle_propose(fipa_msg, dialogue) + elif fipa_msg.performative == FIPAMessage.Performative.DECLINE: + self._handle_decline(fipa_msg, dialogue) + elif fipa_msg.performative == FIPAMessage.Performative.MATCH_ACCEPT_W_INFORM: + self._handle_match_accept(fipa_msg, dialogue) + elif fipa_msg.performative == FIPAMessage.Performative.INFORM: + self._handle_inform(fipa_msg, dialogue) def teardown(self) -> None: """ @@ -96,130 +96,119 @@ def teardown(self) -> None: """ pass - def _handle_unidentified_dialogue(self, msg: FIPAMessage, sender: str) -> None: + def _handle_unidentified_dialogue(self, msg: FIPAMessage) -> None: """ Handle an unidentified dialogue. :param msg: the message - :param sender: the sender """ logger.info("[{}]: unidentified dialogue.".format(self.context.agent_name)) default_msg = DefaultMessage(type=DefaultMessage.Type.ERROR, error_code=DefaultMessage.ErrorCode.INVALID_DIALOGUE.value, error_msg="Invalid dialogue.", error_data="fipa_message") # TODO: send back FIPASerializer().encode(msg)) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=DefaultMessage.protocol_id, message=DefaultSerializer().encode(default_msg)) - def _handle_propose(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: Dialogue) -> None: + def _handle_propose(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the propose. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ - new_message_id = message_id + 1 - new_target_id = message_id - proposals = cast(List[Description], msg.get("proposal")) + new_message_id = msg.message_id + 1 + new_target = msg.message_id + proposals = cast(List[Description], msg.proposal) if proposals is not []: # only take the first proposal proposal = proposals[0] logger.info("[{}]: received proposal={} from sender={}".format(self.context.agent_name, proposal.values, - sender[-5:])) + msg.counterparty[-5:])) strategy = cast(Strategy, self.context.strategy) acceptable = strategy.is_acceptable_proposal(proposal) if acceptable: strategy.is_searching = False logger.info("[{}]: accepting the proposal from sender={}".format(self.context.agent_name, - sender[-5:])) + msg.counterparty[-5:])) dialogue.proposal = proposal accept_msg = FIPAMessage(message_id=new_message_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, - target=new_target_id, + target=new_target, performative=FIPAMessage.Performative.ACCEPT) dialogue.outgoing_extend(accept_msg) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(accept_msg)) else: logger.info("[{}]: declining the proposal from sender={}".format(self.context.agent_name, - sender[-5:])) + msg.counterparty[-5:])) decline_msg = FIPAMessage(message_id=new_message_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, - target=new_target_id, + target=new_target, performative=FIPAMessage.Performative.DECLINE) dialogue.outgoing_extend(decline_msg) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(decline_msg)) - def _handle_decline(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: Dialogue) -> None: + def _handle_decline(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the decline. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ - logger.info("[{}]: received DECLINE from sender={}".format(self.context.agent_name, sender[-5:])) + logger.info("[{}]: received DECLINE from sender={}".format(self.context.agent_name, msg.counterparty[-5:])) - def _handle_match_accept(self, msg: FIPAMessage, sender: str, message_id: int, - dialogue: Dialogue) -> None: + def _handle_match_accept(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the match accept. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ - new_message_id = message_id + 1 - new_target_id = message_id - counterparty_pbk = dialogue.dialogue_label.dialogue_opponent_pbk + new_message_id = msg.message_id + 1 + new_target = msg.message_id inform_msg = FIPAMessage(message_id=new_message_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, - target=new_target_id, + target=new_target, performative=FIPAMessage.Performative.INFORM, - json_data={"Done": "Sending payment via bank transfer"}) + info={"Done": "Sending payment via bank transfer"}) dialogue.outgoing_extend(inform_msg) - self.context.outbox.put_message(to=counterparty_pbk, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(inform_msg)) logger.info("[{}]: informing counterparty={} of payment.".format(self.context.agent_name, - counterparty_pbk[-5:])) + msg.counterparty[-5:])) self._received_tx_message = True - def _handle_inform(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: Dialogue) -> None: + def _handle_inform(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the match inform. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ - logger.info("[{}]: received INFORM from sender={}".format(self.context.agent_name, sender[-5:])) - json_data = cast(dict, msg.get("json_data")) + logger.info("[{}]: received INFORM from sender={}".format(self.context.agent_name, msg.counterparty[-5:])) + json_data = msg.info if 'weather_data' in json_data.keys(): weather_data = json_data['weather_data'] logger.info("[{}]: received the following weather data={}".format(self.context.agent_name, pprint.pformat(weather_data))) else: logger.info("[{}]: received no data from sender={}".format(self.context.agent_name, - sender[-5:])) + msg.counterparty[-5:])) class OEFHandler(Handler): @@ -231,20 +220,18 @@ def setup(self) -> None: """Call to setup the handler.""" pass - def handle(self, message: Message, sender: str) -> None: + def handle(self, message: Message) -> None: """ Implement the reaction to a message. :param message: the message - :param sender: the sender :return: None """ # convenience representations oef_msg = cast(OEFMessage, message) - oef_msg_type = OEFMessage.Type(oef_msg.get("type")) - if oef_msg_type is OEFMessage.Type.SEARCH_RESULT: - agents = cast(List[str], oef_msg.get("agents")) + if oef_msg.type is OEFMessage.Type.SEARCH_RESULT: + agents = oef_msg.agents self._handle_search(agents) def teardown(self) -> None: @@ -268,19 +255,19 @@ def _handle_search(self, agents: List[str]) -> None: # stopping search strategy.is_searching = False # pick first agent found - opponent_pbk = agents[0] + opponent_addr = agents[0] dialogues = cast(Dialogues, self.context.dialogues) - dialogue = dialogues.create_self_initiated(opponent_pbk, self.context.agent_public_key, is_seller=False) + dialogue = dialogues.create_self_initiated(opponent_addr, self.context.agent_address, is_seller=False) query = strategy.get_service_query() - logger.info("[{}]: sending CFP to agent={}".format(self.context.agent_name, opponent_pbk[-5:])) + logger.info("[{}]: sending CFP to agent={}".format(self.context.agent_name, opponent_addr[-5:])) cfp_msg = FIPAMessage(message_id=FIPAMessage.STARTING_MESSAGE_ID, dialogue_reference=dialogue.dialogue_label.dialogue_reference, performative=FIPAMessage.Performative.CFP, target=FIPAMessage.STARTING_TARGET, query=query) dialogue.outgoing_extend(cfp_msg) - self.context.outbox.put_message(to=opponent_pbk, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=opponent_addr, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(cfp_msg)) else: diff --git a/packages/skills/weather_client/skill.yaml b/packages/skills/weather_client/skill.yaml index aeeba7ddf9..035fa5515a 100644 --- a/packages/skills/weather_client/skill.yaml +++ b/packages/skills/weather_client/skill.yaml @@ -1,32 +1,33 @@ name: weather_client -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" behaviours: - - behaviour: - class_name: MySearchBehaviour - args: {} + search: + class_name: MySearchBehaviour + args: + search_interval: 5 handlers: - - handler: - class_name: FIPAHandler - args: {} - - handler: - class_name: OEFHandler - args: {} -tasks: [] + fipa: + class_name: FIPAHandler + args: {} + oef: + class_name: OEFHandler + args: {} +tasks: {} shared_classes: - - shared_class: - class_name: Strategy - args: - country: UK - search_interval: 5 - max_row_price: 4 - max_buyer_tx_fee: 1 - currency_pbk: 'FET' - ledger_id: 'None' - - shared_class: - class_name: Dialogues - args: {} + strategy: + class_name: Strategy + args: + country: UK + max_row_price: 4 + max_buyer_tx_fee: 1 + currency_id: 'FET' + ledger_id: 'None' + dialogues: + class_name: Dialogues + args: {} protocols: ['fipa','default','oef'] ledgers: [] diff --git a/packages/skills/weather_client/strategy.py b/packages/skills/weather_client/strategy.py index 3fc61b5af8..d894a40ae4 100644 --- a/packages/skills/weather_client/strategy.py +++ b/packages/skills/weather_client/strategy.py @@ -19,15 +19,11 @@ """This module contains the strategy class.""" -import datetime -from typing import cast - -from aea.protocols.oef.models import Description, Query, Constraint, ConstraintType +from aea.helpers.search.models import Description, Query, Constraint, ConstraintType from aea.skills.base import SharedClass DEFAULT_COUNTRY = 'UK' SEARCH_TERM = 'country' -DEFAULT_SEARCH_INTERVAL = 5.0 DEFAULT_MAX_ROW_PRICE = 5 DEFAULT_MAX_TX_FEE = 2 DEFAULT_CURRENCY_PBK = 'FET' @@ -44,15 +40,13 @@ def __init__(self, **kwargs) -> None: :return: None """ self._country = kwargs.pop('country') if 'country' in kwargs.keys() else DEFAULT_COUNTRY - self._search_interval = cast(float, kwargs.pop('search_interval')) if 'search_interval' in kwargs.keys() else DEFAULT_SEARCH_INTERVAL self._max_row_price = kwargs.pop('max_row_price') if 'max_row_price' in kwargs.keys() else DEFAULT_MAX_ROW_PRICE self.max_buyer_tx_fee = kwargs.pop('max_tx_fee') if 'max_tx_fee' in kwargs.keys() else DEFAULT_MAX_TX_FEE - self._currency_pbk = kwargs.pop('currency_pbk') if 'currency_pbk' in kwargs.keys() else DEFAULT_CURRENCY_PBK + self._currency_id = kwargs.pop('currency_id') if 'currency_id' in kwargs.keys() else DEFAULT_CURRENCY_PBK self._ledger_id = kwargs.pop('ledger_id') if 'ledger_id' in kwargs.keys() else DEFAULT_LEDGER_ID super().__init__(**kwargs) self._search_id = 0 self.is_searching = True - self._last_search_time = datetime.datetime.now() def get_next_search_id(self) -> int: """ @@ -61,7 +55,6 @@ def get_next_search_id(self) -> int: :return: the next search id """ self._search_id += 1 - self._last_search_time = datetime.datetime.now() return self._search_id def get_service_query(self) -> Query: @@ -73,19 +66,6 @@ def get_service_query(self) -> Query: query = Query([Constraint(SEARCH_TERM, ConstraintType("==", self._country))], model=None) return query - def is_time_to_search(self) -> bool: - """ - Check whether it is time to search. - - :return: whether it is time to search - """ - if not self.is_searching: - return False - now = datetime.datetime.now() - diff = now - self._last_search_time - result = diff.total_seconds() > self._search_interval - return result - def is_acceptable_proposal(self, proposal: Description) -> bool: """ Check whether it is an acceptable proposal. @@ -94,6 +74,6 @@ def is_acceptable_proposal(self, proposal: Description) -> bool: """ result = (proposal.values['price'] - proposal.values['seller_tx_fee'] > 0) and \ (proposal.values['price'] <= self._max_row_price * proposal.values['rows']) and \ - (proposal.values['currency_pbk'] == self._currency_pbk) and \ + (proposal.values['currency_id'] == self._currency_id) and \ (proposal.values['ledger_id'] == self._ledger_id) return result diff --git a/packages/skills/weather_client_ledger/behaviours.py b/packages/skills/weather_client_ledger/behaviours.py index 78c4fe9830..7d85c20d53 100644 --- a/packages/skills/weather_client_ledger/behaviours.py +++ b/packages/skills/weather_client_ledger/behaviours.py @@ -24,24 +24,29 @@ from aea.crypto.ethereum import ETHEREUM from aea.crypto.fetchai import FETCHAI -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.serialization import DEFAULT_OEF, OEFSerializer -from aea.skills.base import Behaviour +from aea.skills.behaviours import TickerBehaviour if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.oef.message import OEFMessage + from packages.protocols.oef.serialization import DEFAULT_OEF, OEFSerializer from packages.skills.weather_client_ledger.strategy import Strategy else: + from oef_protocol.message import OEFMessage + from oef_protocol.serialization import DEFAULT_OEF, OEFSerializer from weather_client_ledger_skill.strategy import Strategy logger = logging.getLogger("aea.weather_client_ledger_skill") +DEFAULT_SEARCH_INTERVAL = 5.0 -class MySearchBehaviour(Behaviour): - """This class scaffolds a behaviour.""" + +class MySearchBehaviour(TickerBehaviour): + """This class implements a search behaviour.""" def __init__(self, **kwargs): - """Initialise the class.""" - super().__init__(**kwargs) + """Initialize the search behaviour.""" + search_interval = cast(float, kwargs.pop('search_interval')) if 'search_interval' in kwargs.keys() else DEFAULT_SEARCH_INTERVAL + super().__init__(tick_interval=search_interval, **kwargs) def setup(self) -> None: """Implement the setup for the behaviour.""" @@ -68,14 +73,14 @@ def act(self) -> None: :return: None """ strategy = cast(Strategy, self.context.strategy) - if strategy.is_time_to_search(): + if strategy.is_searching: query = strategy.get_service_query() search_id = strategy.get_next_search_id() - oef_msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_SERVICES, + oef_msg = OEFMessage(type=OEFMessage.Type.SEARCH_SERVICES, id=search_id, query=query) self.context.outbox.put_message(to=DEFAULT_OEF, - sender=self.context.agent_public_key, + sender=self.context.agent_address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(oef_msg)) diff --git a/packages/skills/weather_client_ledger/dialogues.py b/packages/skills/weather_client_ledger/dialogues.py index 4f956bb08a..730e5945cc 100644 --- a/packages/skills/weather_client_ledger/dialogues.py +++ b/packages/skills/weather_client_ledger/dialogues.py @@ -25,13 +25,18 @@ - Dialogues: The dialogues class keeps track of all dialogues. """ -from typing import Optional +import sys +from typing import Optional, TYPE_CHECKING from aea.helpers.dialogue.base import DialogueLabel -from aea.protocols.fipa.dialogues import FIPADialogues, FIPADialogue -from aea.protocols.oef.models import Description +from aea.helpers.search.models import Description from aea.skills.base import SharedClass +if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.fipa.dialogues import FIPADialogues, FIPADialogue +else: + from fipa_protocol.dialogues import FIPADialogues, FIPADialogue + class Dialogue(FIPADialogue): """The dialogue class maintains state of a dialogue and manages it.""" diff --git a/packages/skills/weather_client_ledger/handlers.py b/packages/skills/weather_client_ledger/handlers.py index c82b3fd1dc..8930c27111 100644 --- a/packages/skills/weather_client_ledger/handlers.py +++ b/packages/skills/weather_client_ledger/handlers.py @@ -21,24 +21,27 @@ import logging import pprint import sys -from typing import Dict, List, Optional, cast, TYPE_CHECKING +from typing import Any, Dict, List, Optional, cast, TYPE_CHECKING from aea.configurations.base import ProtocolId from aea.helpers.dialogue.base import DialogueLabel +from aea.helpers.search.models import Description from aea.protocols.base import Message from aea.protocols.default.message import DefaultMessage from aea.protocols.default.serialization import DefaultSerializer -from aea.protocols.fipa.message import FIPAMessage -from aea.protocols.fipa.serialization import FIPASerializer -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.models import Description from aea.skills.base import Handler from aea.decision_maker.messages.transaction import TransactionMessage if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.fipa.message import FIPAMessage + from packages.protocols.fipa.serialization import FIPASerializer + from packages.protocols.oef.message import OEFMessage from packages.skills.weather_client_ledger.dialogues import Dialogue, Dialogues from packages.skills.weather_client_ledger.strategy import Strategy else: + from fipa_protocol.message import FIPAMessage + from fipa_protocol.serialization import FIPASerializer + from oef_protocol.message import OEFMessage from weather_client_ledger_skill.dialogues import Dialogue, Dialogues from weather_client_ledger_skill.strategy import Strategy @@ -58,37 +61,34 @@ def setup(self) -> None: """ pass - def handle(self, message: Message, sender: str) -> None: + def handle(self, message: Message) -> None: """ Implement the reaction to a message. :param message: the message - :param sender: the sender :return: None """ # convenience representations fipa_msg = cast(FIPAMessage, message) - msg_performative = FIPAMessage.Performative(message.get('performative')) - message_id = cast(int, message.get("message_id")) # recover dialogue dialogues = cast(Dialogues, self.context.dialogues) - if dialogues.is_belonging_to_registered_dialogue(fipa_msg, sender, self.context.agent_public_key): - dialogue = cast(Dialogue, dialogues.get_dialogue(fipa_msg, sender, self.context.agent_public_key)) + if dialogues.is_belonging_to_registered_dialogue(fipa_msg, self.context.agent_address): + dialogue = cast(Dialogue, dialogues.get_dialogue(fipa_msg, self.context.agent_address)) dialogue.incoming_extend(fipa_msg) else: - self._handle_unidentified_dialogue(fipa_msg, sender) + self._handle_unidentified_dialogue(fipa_msg) return # handle message - if msg_performative == FIPAMessage.Performative.PROPOSE: - self._handle_propose(fipa_msg, sender, message_id, dialogue) - elif msg_performative == FIPAMessage.Performative.DECLINE: - self._handle_decline(fipa_msg, sender, message_id, dialogue) - elif msg_performative == FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS: - self._handle_match_accept(fipa_msg, sender, message_id, dialogue) - elif msg_performative == FIPAMessage.Performative.INFORM: - self._handle_inform(fipa_msg, sender, message_id, dialogue) + if fipa_msg.performative == FIPAMessage.Performative.PROPOSE: + self._handle_propose(fipa_msg, dialogue) + elif fipa_msg.performative == FIPAMessage.Performative.DECLINE: + self._handle_decline(fipa_msg, dialogue) + elif fipa_msg.performative == FIPAMessage.Performative.MATCH_ACCEPT_W_INFORM: + self._handle_match_accept(fipa_msg, dialogue) + elif fipa_msg.performative == FIPAMessage.Performative.INFORM: + self._handle_inform(fipa_msg, dialogue) def teardown(self) -> None: """ @@ -98,84 +98,79 @@ def teardown(self) -> None: """ pass - def _handle_unidentified_dialogue(self, msg: FIPAMessage, sender: str) -> None: + def _handle_unidentified_dialogue(self, msg: FIPAMessage) -> None: """ Handle an unidentified dialogue. :param msg: the message - :param sender: the sender """ logger.info("[{}]: unidentified dialogue.".format(self.context.agent_name)) default_msg = DefaultMessage(type=DefaultMessage.Type.ERROR, error_code=DefaultMessage.ErrorCode.INVALID_DIALOGUE.value, error_msg="Invalid dialogue.", error_data="fipa_message") # TODO: send back FIPASerializer().encode(msg)) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=DefaultMessage.protocol_id, message=DefaultSerializer().encode(default_msg)) - def _handle_propose(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: Dialogue) -> None: + def _handle_propose(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the propose. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ - new_message_id = message_id + 1 - new_target_id = message_id - proposals = cast(List[Description], msg.get("proposal")) + new_message_id = msg.message_id + 1 + new_target_id = msg.message_id + proposals = msg.proposal if proposals is not []: # only take the first proposal proposal = proposals[0] logger.info("[{}]: received proposal={} from sender={}".format(self.context.agent_name, proposal.values, - sender[-5:])) + msg.counterparty[-5:])) strategy = cast(Strategy, self.context.strategy) acceptable = strategy.is_acceptable_proposal(proposal) affordable = strategy.is_affordable_proposal(proposal) if acceptable and affordable: strategy.is_searching = False logger.info("[{}]: accepting the proposal from sender={}".format(self.context.agent_name, - sender[-5:])) + msg.counterparty[-5:])) dialogue.proposal = proposal accept_msg = FIPAMessage(message_id=new_message_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, target=new_target_id, performative=FIPAMessage.Performative.ACCEPT) dialogue.outgoing_extend(accept_msg) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(accept_msg)) else: logger.info("[{}]: declining the proposal from sender={}".format(self.context.agent_name, - sender[-5:])) + msg.counterparty[-5:])) decline_msg = FIPAMessage(message_id=new_message_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, target=new_target_id, performative=FIPAMessage.Performative.DECLINE) dialogue.outgoing_extend(decline_msg) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(decline_msg)) - def _handle_decline(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: Dialogue) -> None: + def _handle_decline(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the decline. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ - logger.info("[{}]: received DECLINE from sender={}".format(self.context.agent_name, sender[-5:])) + logger.info("[{}]: received DECLINE from sender={}".format(self.context.agent_name, msg.counterparty[-5:])) # target = msg.get("target") # dialogues = cast(Dialogues, self.context.dialogues) # if target == 1: @@ -183,60 +178,52 @@ def _handle_decline(self, msg: FIPAMessage, sender: str, message_id: int, dialog # elif target == 3: # dialogues.dialogue_stats.add_dialogue_endstate(Dialogue.EndState.DECLINED_ACCEPT) - def _handle_match_accept(self, msg: FIPAMessage, sender: str, message_id: int, - dialogue: Dialogue) -> None: + def _handle_match_accept(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the match accept. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ - logger.info("[{}]: received MATCH_ACCEPT_W_ADDRESS from sender={}".format(self.context.agent_name, sender[-5:])) - address = cast(str, msg.get("address")) + logger.info("[{}]: received MATCH_ACCEPT_W_INFORM from sender={}".format(self.context.agent_name, msg.counterparty[-5:])) + info = msg.info + address = cast(str, info.get("address")) strategy = cast(Strategy, self.context.strategy) proposal = cast(Description, dialogue.proposal) - ledger_id = cast(str, proposal.values.get("ledger_id")) - tx_msg = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE, - skill_id="weather_client_ledger", - transaction_id="transaction0", - sender=self.context.agent_public_keys[ledger_id], - counterparty=address, - is_sender_buyer=True, - currency_pbk=proposal.values['currency_pbk'], - amount=proposal.values['price'], - sender_tx_fee=strategy.max_buyer_tx_fee, - counterparty_tx_fee=proposal.values['seller_tx_fee'], - quantities_by_good_pbk={}, - dialogue_label=dialogue.dialogue_label.json, - ledger_id=ledger_id) + tx_msg = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE_FOR_SETTLEMENT, + skill_callback_ids=["weather_client_ledger"], + tx_id="transaction0", + tx_sender_addr=self.context.agent_addresses[proposal.values['ledger_id']], + tx_counterparty_addr=address, + tx_amount_by_currency_id={proposal.values['currency_id']: - proposal.values['price']}, + tx_sender_fee=strategy.max_buyer_tx_fee, + tx_counterparty_fee=proposal.values['seller_tx_fee'], + tx_quantities_by_good_id={}, + ledger_id=proposal.values['ledger_id'], + info={'dialogue_label': dialogue.dialogue_label.json}) self.context.decision_maker_message_queue.put_nowait(tx_msg) logger.info("[{}]: proposing the transaction to the decision maker. Waiting for confirmation ...".format( self.context.agent_name)) - def _handle_inform(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: Dialogue) -> None: + def _handle_inform(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the match inform. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ - logger.info("[{}]: received INFORM from sender={}".format(self.context.agent_name, sender[-5:])) - json_data = cast(dict, msg.get("json_data")) - if 'weather_data' in json_data.keys(): - weather_data = json_data['weather_data'] + logger.info("[{}]: received INFORM from sender={}".format(self.context.agent_name, msg.counterparty[-5:])) + if 'weather_data' in msg.info.keys(): + weather_data = msg.info['weather_data'] logger.info("[{}]: received the following weather data={}".format(self.context.agent_name, pprint.pformat(weather_data))) # dialogues = cast(Dialogues, self.context.dialogues) # dialogues.dialogue_stats.add_dialogue_endstate(Dialogue.EndState.SUCCESSFUL) else: logger.info("[{}]: received no data from sender={}".format(self.context.agent_name, - sender[-5:])) + msg.counterparty[-5:])) class OEFHandler(Handler): @@ -248,20 +235,17 @@ def setup(self) -> None: """Call to setup the handler.""" pass - def handle(self, message: Message, sender: str) -> None: + def handle(self, message: Message) -> None: """ Implement the reaction to a message. :param message: the message - :param sender: the sender :return: None """ # convenience representations oef_msg = cast(OEFMessage, message) - oef_msg_type = OEFMessage.Type(oef_msg.get("type")) - - if oef_msg_type is OEFMessage.Type.SEARCH_RESULT: - agents = cast(List[str], oef_msg.get("agents")) + if oef_msg.type is OEFMessage.Type.SEARCH_RESULT: + agents = oef_msg.agents self._handle_search(agents) def teardown(self) -> None: @@ -285,19 +269,19 @@ def _handle_search(self, agents: List[str]) -> None: # stopping search strategy.is_searching = False # pick first agent found - opponent_pbk = agents[0] + opponent_addr = agents[0] dialogues = cast(Dialogues, self.context.dialogues) - dialogue = dialogues.create_self_initiated(opponent_pbk, self.context.agent_public_key, is_seller=False) + dialogue = dialogues.create_self_initiated(opponent_addr, self.context.agent_address, is_seller=False) query = strategy.get_service_query() - logger.info("[{}]: sending CFP to agent={}".format(self.context.agent_name, opponent_pbk[-5:])) + logger.info("[{}]: sending CFP to agent={}".format(self.context.agent_name, opponent_addr[-5:])) cfp_msg = FIPAMessage(message_id=FIPAMessage.STARTING_MESSAGE_ID, dialogue_reference=dialogue.dialogue_label.dialogue_reference, performative=FIPAMessage.Performative.CFP, target=FIPAMessage.STARTING_TARGET, query=query) dialogue.outgoing_extend(cfp_msg) - self.context.outbox.put_message(to=opponent_pbk, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=opponent_addr, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(cfp_msg)) else: @@ -313,37 +297,38 @@ def setup(self) -> None: """Implement the setup for the handler.""" pass - def handle(self, message: Message, sender: str) -> None: + def handle(self, message: Message) -> None: """ Implement the reaction to a message. :param message: the message - :param sender: the sender :return: None """ tx_msg_response = cast(TransactionMessage, message) if tx_msg_response is not None and \ - TransactionMessage.Performative(tx_msg_response.get("performative")) == TransactionMessage.Performative.ACCEPT: + tx_msg_response.performative == TransactionMessage.Performative.SUCCESSFUL_SETTLEMENT: logger.info("[{}]: transaction was successful.".format(self.context.agent_name)) - json_data = {'transaction_digest': tx_msg_response.get("transaction_digest")} - dialogue_label = DialogueLabel.from_json(cast(Dict[str, str], tx_msg_response.get("dialogue_label"))) + json_data = {'transaction_digest': tx_msg_response.tx_digest} + info = cast(Dict[str, Any], tx_msg_response.info) + dialogue_label = DialogueLabel.from_json(cast(Dict[str, str], info.get("dialogue_label"))) dialogues = cast(Dialogues, self.context.dialogues) dialogue = dialogues.dialogues[dialogue_label] fipa_msg = cast(FIPAMessage, dialogue.last_incoming_message) - new_message_id = cast(int, fipa_msg.get("message_id")) + 1 - new_target_id = cast(int, fipa_msg.get("target")) + 1 - counterparty_pbk = dialogue.dialogue_label.dialogue_opponent_pbk + new_message_id = fipa_msg.message_id + 1 + new_target_id = fipa_msg.message_id + counterparty_addr = dialogue.dialogue_label.dialogue_opponent_addr inform_msg = FIPAMessage(message_id=new_message_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, target=new_target_id, performative=FIPAMessage.Performative.INFORM, - json_data=json_data) + info=json_data) dialogue.outgoing_extend(inform_msg) - self.context.outbox.put_message(to=counterparty_pbk, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=counterparty_addr, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(inform_msg)) - logger.info("[{}]: informing counterparty={} of transaction digest.".format(self.context.agent_name, counterparty_pbk[-5:])) + logger.info("[{}]: informing counterparty={} of transaction digest.".format(self.context.agent_name, + counterparty_addr[-5:])) self._received_tx_message = True else: logger.info("[{}]: transaction was not successful.".format(self.context.agent_name)) diff --git a/packages/skills/weather_client_ledger/skill.yaml b/packages/skills/weather_client_ledger/skill.yaml index 88f15b8b34..711c08a6d0 100644 --- a/packages/skills/weather_client_ledger/skill.yaml +++ b/packages/skills/weather_client_ledger/skill.yaml @@ -1,35 +1,36 @@ name: weather_client_ledger -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" behaviours: - - behaviour: - class_name: MySearchBehaviour - args: {} + search: + class_name: MySearchBehaviour + args: + search_interval: 5 handlers: - - handler: - class_name: FIPAHandler - args: {} - - handler: - class_name: OEFHandler - args: {} - - handler: - class_name: MyTransactionHandler - args: {} -tasks: [] + fipa: + class_name: FIPAHandler + args: {} + oef: + class_name: OEFHandler + args: {} + transaction: + class_name: MyTransactionHandler + args: {} +tasks: {} shared_classes: - - shared_class: - class_name: Strategy - args: - country: UK - search_interval: 5 - max_row_price: 4 - max_buyer_tx_fee: 1 - currency_pbk: 'FET' - ledger_id: 'fetchai' - - shared_class: - class_name: Dialogues - args: {} + strategy: + class_name: Strategy + args: + country: UK + max_row_price: 4 + max_buyer_tx_fee: 1 + currency_id: 'FET' + ledger_id: 'fetchai' + dialogues: + class_name: Dialogues + args: {} protocols: ['fipa','default','oef'] ledgers: ['fetchai'] diff --git a/packages/skills/weather_client_ledger/strategy.py b/packages/skills/weather_client_ledger/strategy.py index 60fa0f3279..ace2e54953 100644 --- a/packages/skills/weather_client_ledger/strategy.py +++ b/packages/skills/weather_client_ledger/strategy.py @@ -19,15 +19,13 @@ """This module contains the strategy class.""" -import datetime from typing import cast -from aea.protocols.oef.models import Description, Query, Constraint, ConstraintType +from aea.helpers.search.models import Description, Query, Constraint, ConstraintType from aea.skills.base import SharedClass DEFAULT_COUNTRY = 'UK' SEARCH_TERM = 'country' -DEFAULT_SEARCH_INTERVAL = 5.0 DEFAULT_MAX_ROW_PRICE = 5 DEFAULT_MAX_TX_FEE = 2 DEFAULT_CURRENCY_PBK = 'FET' @@ -44,15 +42,13 @@ def __init__(self, **kwargs) -> None: :return: None """ self._country = kwargs.pop('country') if 'country' in kwargs.keys() else DEFAULT_COUNTRY - self._search_interval = cast(float, kwargs.pop('search_interval')) if 'search_interval' in kwargs.keys() else DEFAULT_SEARCH_INTERVAL self._max_row_price = kwargs.pop('max_row_price') if 'max_row_price' in kwargs.keys() else DEFAULT_MAX_ROW_PRICE self.max_buyer_tx_fee = kwargs.pop('max_tx_fee') if 'max_tx_fee' in kwargs.keys() else DEFAULT_MAX_TX_FEE - self._currency_pbk = kwargs.pop('currency_pbk') if 'currency_pbk' in kwargs.keys() else DEFAULT_CURRENCY_PBK + self._currency_id = kwargs.pop('currency_id') if 'currency_id' in kwargs.keys() else DEFAULT_CURRENCY_PBK self._ledger_id = kwargs.pop('ledger_id') if 'ledger_id' in kwargs.keys() else DEFAULT_LEDGER_ID super().__init__(**kwargs) self._search_id = 0 self.is_searching = True - self._last_search_time = datetime.datetime.now() def get_next_search_id(self) -> int: """ @@ -61,7 +57,6 @@ def get_next_search_id(self) -> int: :return: the next search id """ self._search_id += 1 - self._last_search_time = datetime.datetime.now() return self._search_id def get_service_query(self) -> Query: @@ -73,19 +68,6 @@ def get_service_query(self) -> Query: query = Query([Constraint(SEARCH_TERM, ConstraintType("==", self._country))], model=None) return query - def is_time_to_search(self) -> bool: - """ - Check whether it is time to search. - - :return: whether it is time to search - """ - if not self.is_searching: - return False - now = datetime.datetime.now() - diff = now - self._last_search_time - result = diff.total_seconds() > self._search_interval - return result - def is_acceptable_proposal(self, proposal: Description) -> bool: """ Check whether it is an acceptable proposal. @@ -94,7 +76,7 @@ def is_acceptable_proposal(self, proposal: Description) -> bool: """ result = (proposal.values['price'] - proposal.values['seller_tx_fee'] > 0) and \ (proposal.values['price'] <= self._max_row_price * proposal.values['rows']) and \ - (proposal.values['currency_pbk'] == self._currency_pbk) and \ + (proposal.values['currency_id'] == self._currency_id) and \ (proposal.values['ledger_id'] == self._ledger_id) return result diff --git a/packages/skills/weather_station/behaviours.py b/packages/skills/weather_station/behaviours.py index 46b459b231..13feb0c8ba 100644 --- a/packages/skills/weather_station/behaviours.py +++ b/packages/skills/weather_station/behaviours.py @@ -19,34 +19,35 @@ """This package contains a scaffold of a behaviour.""" -import datetime import logging import sys from typing import cast, Optional, TYPE_CHECKING -from aea.skills.base import Behaviour -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.models import Description -from aea.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF +from aea.helpers.search.models import Description +from aea.skills.behaviours import TickerBehaviour if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.oef.message import OEFMessage + from packages.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF from packages.skills.weather_station.strategy import Strategy else: + from oef_protocol.message import OEFMessage + from oef_protocol.serialization import OEFSerializer, DEFAULT_OEF from weather_station_skill.strategy import Strategy logger = logging.getLogger("aea.weather_station_skill") SERVICE_ID = '' +DEFAULT_SERVICES_INTERVAL = 30.0 -class ServiceRegistrationBehaviour(Behaviour): +class ServiceRegistrationBehaviour(TickerBehaviour): """This class implements a behaviour.""" def __init__(self, **kwargs): """Initialise the behaviour.""" - self._services_interval = kwargs.pop('services_interval', 30) # type: int - super().__init__(**kwargs) - self._last_update_time = datetime.datetime.now() # type: datetime.datetime + services_interval = kwargs.pop('services_interval', DEFAULT_SERVICES_INTERVAL) # type: int + super().__init__(tick_interval=services_interval, **kwargs) self._registered_service_description = None # type: Optional[Description] def setup(self) -> None: @@ -63,9 +64,8 @@ def act(self) -> None: :return: None """ - if self._is_time_to_update_services(): - self._unregister_service() - self._register_service() + self._unregister_service() + self._register_service() def teardown(self) -> None: """ @@ -85,12 +85,12 @@ def _register_service(self) -> None: desc = strategy.get_service_description() self._registered_service_description = desc oef_msg_id = strategy.get_next_oef_msg_id() - msg = OEFMessage(oef_type=OEFMessage.Type.REGISTER_SERVICE, + msg = OEFMessage(type=OEFMessage.Type.REGISTER_SERVICE, id=oef_msg_id, service_description=desc, service_id=SERVICE_ID) self.context.outbox.put_message(to=DEFAULT_OEF, - sender=self.context.agent_public_key, + sender=self.context.agent_address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(msg)) logger.info("[{}]: updating weather station services on OEF.".format(self.context.agent_name)) @@ -103,26 +103,13 @@ def _unregister_service(self) -> None: """ strategy = cast(Strategy, self.context.strategy) oef_msg_id = strategy.get_next_oef_msg_id() - msg = OEFMessage(oef_type=OEFMessage.Type.UNREGISTER_SERVICE, + msg = OEFMessage(type=OEFMessage.Type.UNREGISTER_SERVICE, id=oef_msg_id, service_description=self._registered_service_description, service_id=SERVICE_ID) self.context.outbox.put_message(to=DEFAULT_OEF, - sender=self.context.agent_public_key, + sender=self.context.agent_address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(msg)) logger.info("[{}]: unregistering weather station services from OEF.".format(self.context.agent_name)) self._registered_service_description = None - - def _is_time_to_update_services(self) -> bool: - """ - Check if the agent should update the service directory. - - :return: bool indicating the action - """ - now = datetime.datetime.now() - diff = now - self._last_update_time - result = diff.total_seconds() > self._services_interval - if result: - self._last_update_time = now - return result diff --git a/packages/skills/weather_station/dialogues.py b/packages/skills/weather_station/dialogues.py index acb64fa080..3c0b03dcb4 100644 --- a/packages/skills/weather_station/dialogues.py +++ b/packages/skills/weather_station/dialogues.py @@ -25,13 +25,18 @@ - Dialogues: The dialogues class keeps track of all dialogues. """ -from typing import Any, Dict, Optional +import sys +from typing import Any, Dict, Optional, TYPE_CHECKING from aea.helpers.dialogue.base import DialogueLabel -from aea.protocols.fipa.dialogues import FIPADialogues, FIPADialogue -from aea.protocols.oef.models import Description +from aea.helpers.search.models import Description from aea.skills.base import SharedClass +if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.fipa.dialogues import FIPADialogues, FIPADialogue +else: + from fipa_protocol.dialogues import FIPADialogues, FIPADialogue + class Dialogue(FIPADialogue): """The dialogue class maintains state of a dialogue and manages it.""" diff --git a/packages/skills/weather_station/handlers.py b/packages/skills/weather_station/handlers.py index e8adbc05b6..50963304db 100644 --- a/packages/skills/weather_station/handlers.py +++ b/packages/skills/weather_station/handlers.py @@ -21,21 +21,23 @@ import logging import sys -from typing import Optional, Tuple, cast, TYPE_CHECKING +from typing import Optional, cast, TYPE_CHECKING from aea.configurations.base import ProtocolId +from aea.helpers.search.models import Query from aea.protocols.base import Message from aea.protocols.default.message import DefaultMessage from aea.protocols.default.serialization import DefaultSerializer -from aea.protocols.fipa.message import FIPAMessage -from aea.protocols.fipa.serialization import FIPASerializer -from aea.protocols.oef.models import Query # Description from aea.skills.base import Handler if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.fipa.message import FIPAMessage + from packages.protocols.fipa.serialization import FIPASerializer from packages.skills.weather_station.dialogues import Dialogue, Dialogues from packages.skills.weather_station.strategy import Strategy else: + from fipa_protocol.message import FIPAMessage + from fipa_protocol.serialization import FIPASerializer from weather_station_skill.dialogues import Dialogue, Dialogues from weather_station_skill.strategy import Strategy @@ -51,41 +53,39 @@ def setup(self) -> None: """Implement the setup for the handler.""" pass - def handle(self, message: Message, sender: str) -> None: + def handle(self, message: Message) -> None: """ Implement the reaction to a message. :param message: the message - :param sender: the sender :return: None """ # convenience representations fipa_msg = cast(FIPAMessage, message) - msg_performative = FIPAMessage.Performative(fipa_msg.get('performative')) - message_id = cast(int, fipa_msg.get('message_id')) - dialogue_reference = cast(Tuple[str, str], fipa_msg.get('dialogue_reference')) + dialogue_reference = fipa_msg.dialogue_reference # recover dialogue dialogues = cast(Dialogues, self.context.dialogues) - if dialogues.is_belonging_to_registered_dialogue(fipa_msg, sender, self.context.agent_public_key): - dialogue = cast(Dialogue, dialogues.get_dialogue(fipa_msg, sender, self.context.agent_public_key)) + if dialogues.is_belonging_to_registered_dialogue(fipa_msg, self.context.agent_address): + dialogue = cast(Dialogue, dialogues.get_dialogue(fipa_msg, self.context.agent_address)) dialogue.incoming_extend(fipa_msg) - elif dialogues.is_permitted_for_new_dialogue(fipa_msg, sender): - dialogue = cast(Dialogue, dialogues.create_opponent_initiated(sender, dialogue_reference, is_seller=True)) + elif dialogues.is_permitted_for_new_dialogue(fipa_msg): + dialogue = cast(Dialogue, dialogues.create_opponent_initiated(fipa_msg.counterparty, + dialogue_reference, is_seller=True)) dialogue.incoming_extend(fipa_msg) else: - self._handle_unidentified_dialogue(fipa_msg, sender) + self._handle_unidentified_dialogue(fipa_msg) return # handle message - if msg_performative == FIPAMessage.Performative.CFP: - self._handle_cfp(fipa_msg, sender, message_id, dialogue) - elif msg_performative == FIPAMessage.Performative.DECLINE: - self._handle_decline(fipa_msg, sender, message_id, dialogue) - elif msg_performative == FIPAMessage.Performative.ACCEPT: - self._handle_accept(fipa_msg, sender, message_id, dialogue) - elif msg_performative == FIPAMessage.Performative.INFORM: - self._handle_inform(fipa_msg, sender, message_id, dialogue) + if fipa_msg.performative == FIPAMessage.Performative.CFP: + self._handle_cfp(fipa_msg, dialogue) + elif fipa_msg.performative == FIPAMessage.Performative.DECLINE: + self._handle_decline(fipa_msg, dialogue) + elif fipa_msg.performative == FIPAMessage.Performative.ACCEPT: + self._handle_accept(fipa_msg, dialogue) + elif fipa_msg.performative == FIPAMessage.Performative.INFORM: + self._handle_inform(fipa_msg, dialogue) def teardown(self) -> None: """ @@ -95,14 +95,13 @@ def teardown(self) -> None: """ pass - def _handle_unidentified_dialogue(self, msg: FIPAMessage, sender: str) -> None: + def _handle_unidentified_dialogue(self, msg: FIPAMessage) -> None: """ Handle an unidentified dialogue. Respond to the sender with a default message containing the appropriate error information. :param msg: the message - :param sender: the sender :return: None """ logger.info("[{}]: unidentified dialogue.".format(self.context.agent_name)) @@ -110,28 +109,26 @@ def _handle_unidentified_dialogue(self, msg: FIPAMessage, sender: str) -> None: error_code=DefaultMessage.ErrorCode.INVALID_DIALOGUE.value, error_msg="Invalid dialogue.", error_data="fipa_message") # FIPASerializer().encode(msg) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=DefaultMessage.protocol_id, message=DefaultSerializer().encode(default_msg)) - def _handle_cfp(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: Dialogue) -> None: + def _handle_cfp(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the CFP. If the CFP matches the supplied services then send a PROPOSE, otherwise send a DECLINE. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ - new_message_id = message_id + 1 - new_target = message_id + new_message_id = msg.message_id + 1 + new_target = msg.message_id logger.info("[{}]: received CFP from sender={}".format(self.context.agent_name, - sender[-5:])) - query = cast(Query, msg.get("query")) + msg.counterparty[-5:])) + query = cast(Query, msg.query) strategy = cast(Strategy, self.context.strategy) if strategy.is_matching_supply(query): @@ -139,7 +136,7 @@ def _handle_cfp(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: dialogue.weather_data = weather_data dialogue.proposal = proposal logger.info("[{}]: sending sender={} a PROPOSE with proposal={}".format(self.context.agent_name, - sender[-5:], + msg.counterparty[-5:], proposal.values)) proposal_msg = FIPAMessage(message_id=new_message_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, @@ -147,74 +144,68 @@ def _handle_cfp(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: performative=FIPAMessage.Performative.PROPOSE, proposal=[proposal]) dialogue.outgoing_extend(proposal_msg) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(proposal_msg)) else: logger.info("[{}]: declined the CFP from sender={}".format(self.context.agent_name, - sender[-5:])) + msg.counterparty[-5:])) decline_msg = FIPAMessage(message_id=new_message_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, target=new_target, performative=FIPAMessage.Performative.DECLINE) dialogue.outgoing_extend(decline_msg) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(decline_msg)) - def _handle_decline(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: Dialogue) -> None: + def _handle_decline(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the DECLINE. Close the dialogue. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ logger.info("[{}]: received DECLINE from sender={}".format(self.context.agent_name, - sender[-5:])) + msg.counterparty[-5:])) # dialogues = cast(Dialogues, self.context.dialogues) # dialogues.dialogue_stats.add_dialogue_endstate(Dialogue.EndState.DECLINED_PROPOSE) - def _handle_accept(self, msg: FIPAMessage, sender: str, message_id: int, - dialogue: Dialogue) -> None: + def _handle_accept(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the ACCEPT. - Respond with a MATCH_ACCEPT_W_ADDRESS which contains the address to send the funds to. + Respond with a MATCH_ACCEPT_W_INFORM which contains the address to send the funds to. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ - new_message_id = message_id + 1 - new_target = message_id + new_message_id = msg.message_id + 1 + new_target = msg.message_id logger.info("[{}]: received ACCEPT from sender={}".format(self.context.agent_name, - sender[-5:])) - logger.info("[{}]: sending MATCH_ACCEPT_W_ADDRESS to sender={}".format(self.context.agent_name, - sender[-5:])) + msg.counterparty[-5:])) + logger.info("[{}]: sending MATCH_ACCEPT_W_INFORM to sender={}".format(self.context.agent_name, + msg.counterparty[-5:])) # proposal = cast(Description, dialogue.proposal) # identifier = cast(str, proposal.values.get("ledger_id")) match_accept_msg = FIPAMessage(message_id=new_message_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, target=new_target, - performative=FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS, - address="no_address") + performative=FIPAMessage.Performative.MATCH_ACCEPT_W_INFORM, + info={"address": "no_address"}) dialogue.outgoing_extend(match_accept_msg) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(match_accept_msg)) - def _handle_inform(self, msg: FIPAMessage, sender: str, message_id: int, - dialogue: Dialogue) -> None: + def _handle_inform(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the INFORM. @@ -222,27 +213,24 @@ def _handle_inform(self, msg: FIPAMessage, sender: str, message_id: int, If the transaction is settled send the weather data, otherwise do nothing. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ - new_message_id = message_id + 1 - new_target = message_id + new_message_id = msg.message_id + 1 + new_target = msg.message_id logger.info("[{}]: received INFORM from sender={}".format(self.context.agent_name, - sender[-5:])) + msg.counterparty[-5:])) - json_data = cast(dict, msg.get("json_data")) + json_data = msg.info if "Done" in json_data: inform_msg = FIPAMessage(message_id=new_message_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, target=new_target, performative=FIPAMessage.Performative.INFORM, - json_data=dialogue.weather_data) + info=dialogue.weather_data) dialogue.outgoing_extend(inform_msg) - # import pdb; pdb.set_trace() - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(inform_msg)) # dialogues = cast(Dialogues, self.context.dialogues) diff --git a/packages/skills/weather_station/skill.yaml b/packages/skills/weather_station/skill.yaml index 3676aa7ece..69a4b484ae 100644 --- a/packages/skills/weather_station/skill.yaml +++ b/packages/skills/weather_station/skill.yaml @@ -1,30 +1,31 @@ name: weather_station -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" behaviours: - - behaviour: - class_name: ServiceRegistrationBehaviour - args: - services_interval: 60 + service_registration: + class_name: ServiceRegistrationBehaviour + args: + services_interval: 60 handlers: - - handler: - class_name: FIPAHandler - args: {} -tasks: [] + fipa: + class_name: FIPAHandler + args: {} +tasks: {} shared_classes: - - shared_class: - class_name: Strategy - args: - date_one: "1/10/2019" - date_two: "1/12/2019" - price_per_row: 1 - seller_tx_fee: 0 - currency_pbk: 'FET' - ledger_id: 'None' - - shared_class: - class_name: Dialogues - args: {} + strategy: + class_name: Strategy + args: + date_one: "1/10/2019" + date_two: "1/12/2019" + price_per_row: 1 + seller_tx_fee: 0 + currency_id: 'FET' + ledger_id: 'None' + dialogues: + class_name: Dialogues + args: {} protocols: ['fipa', 'oef', 'default'] ledgers: [] diff --git a/packages/skills/weather_station/strategy.py b/packages/skills/weather_station/strategy.py index 7409c768a4..40b3371d62 100644 --- a/packages/skills/weather_station/strategy.py +++ b/packages/skills/weather_station/strategy.py @@ -22,7 +22,7 @@ import sys from typing import Any, Dict, List, Tuple, TYPE_CHECKING -from aea.protocols.oef.models import Description, Query +from aea.helpers.search.models import Description, Query from aea.skills.base import SharedClass if TYPE_CHECKING or "pytest" in sys.modules: @@ -54,7 +54,7 @@ def __init__(self, **kwargs) -> None: """ self._price_per_row = kwargs.pop('price_per_row') if 'price_per_row' in kwargs.keys() else DEFAULT_PRICE_PER_ROW self._seller_tx_fee = kwargs.pop('seller_tx_fee') if 'seller_tx_fee' in kwargs.keys() else DEFAULT_SELLER_TX_FEE - self._currency_pbk = kwargs.pop('currency_pbk') if 'currency_pbk' in kwargs.keys() else DEFAULT_CURRENCY_PBK + self._currency_id = kwargs.pop('currency_id') if 'currency_id' in kwargs.keys() else DEFAULT_CURRENCY_PBK self._ledger_id = kwargs.pop('ledger_id') if 'ledger_id' in kwargs.keys() else DEFAULT_LEDGER_ID self._date_one = kwargs.pop('date_one') if 'date_one' in kwargs.keys() else DEFAULT_DATE_ONE self._date_two = kwargs.pop('date_two') if 'date_two' in kwargs.keys() else DEFAULT_DATE_TWO @@ -104,7 +104,7 @@ def generate_proposal_and_data(self, query: Query) -> Tuple[Description, Dict[st proposal = Description({"rows": rows, "price": total_price, "seller_tx_fee": self._seller_tx_fee, - "currency_pbk": self._currency_pbk, + "currency_id": self._currency_id, "ledger_id": self._ledger_id}) return (proposal, weather_data) diff --git a/packages/skills/weather_station/weather_station_data_model.py b/packages/skills/weather_station/weather_station_data_model.py index 778de82e95..a5b7d9f847 100644 --- a/packages/skills/weather_station/weather_station_data_model.py +++ b/packages/skills/weather_station/weather_station_data_model.py @@ -19,7 +19,7 @@ """This package contains the dataModel for the weather agent.""" -from aea.protocols.oef.models import DataModel, Attribute +from aea.helpers.search.models import DataModel, Attribute SCHEME = {'country': "UK", 'city': "Cambridge"} diff --git a/packages/skills/weather_station_ledger/behaviours.py b/packages/skills/weather_station_ledger/behaviours.py index 49b439c551..86209979fe 100644 --- a/packages/skills/weather_station_ledger/behaviours.py +++ b/packages/skills/weather_station_ledger/behaviours.py @@ -19,36 +19,37 @@ """This package contains a scaffold of a behaviour.""" -import datetime import logging import sys from typing import cast, Optional, TYPE_CHECKING from aea.crypto.ethereum import ETHEREUM from aea.crypto.fetchai import FETCHAI -from aea.skills.base import Behaviour -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.models import Description -from aea.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF +from aea.helpers.search.models import Description +from aea.skills.behaviours import TickerBehaviour if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.oef.message import OEFMessage + from packages.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF from packages.skills.weather_station_ledger.strategy import Strategy else: from weather_station_ledger_skill.strategy import Strategy + from oef_protocol.message import OEFMessage + from oef_protocol.serialization import OEFSerializer, DEFAULT_OEF logger = logging.getLogger("aea.weather_station_ledger_skill") SERVICE_ID = '' +DEFAULT_SERVICES_INTERVAL = 30.0 -class ServiceRegistrationBehaviour(Behaviour): +class ServiceRegistrationBehaviour(TickerBehaviour): """This class implements a behaviour.""" def __init__(self, **kwargs): """Initialise the behaviour.""" - self._services_interval = kwargs.pop('services_interval', 30) # type: int - super().__init__(**kwargs) - self._last_update_time = datetime.datetime.now() # type: datetime.datetime + services_interval = kwargs.pop('services_interval', DEFAULT_SERVICES_INTERVAL) # type: int + super().__init__(tick_interval=services_interval, **kwargs) self._registered_service_description = None # type: Optional[Description] def setup(self) -> None: @@ -79,9 +80,8 @@ def act(self) -> None: :return: None """ - if self._is_time_to_update_services(): - self._unregister_service() - self._register_service() + self._unregister_service() + self._register_service() def teardown(self) -> None: """ @@ -109,12 +109,12 @@ def _register_service(self) -> None: desc = strategy.get_service_description() self._registered_service_description = desc oef_msg_id = strategy.get_next_oef_msg_id() - msg = OEFMessage(oef_type=OEFMessage.Type.REGISTER_SERVICE, + msg = OEFMessage(type=OEFMessage.Type.REGISTER_SERVICE, id=oef_msg_id, service_description=desc, service_id=SERVICE_ID) self.context.outbox.put_message(to=DEFAULT_OEF, - sender=self.context.agent_public_key, + sender=self.context.agent_address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(msg)) logger.info("[{}]: updating weather station services on OEF.".format(self.context.agent_name)) @@ -127,26 +127,13 @@ def _unregister_service(self) -> None: """ strategy = cast(Strategy, self.context.strategy) oef_msg_id = strategy.get_next_oef_msg_id() - msg = OEFMessage(oef_type=OEFMessage.Type.UNREGISTER_SERVICE, + msg = OEFMessage(type=OEFMessage.Type.UNREGISTER_SERVICE, id=oef_msg_id, service_description=self._registered_service_description, service_id=SERVICE_ID) self.context.outbox.put_message(to=DEFAULT_OEF, - sender=self.context.agent_public_key, + sender=self.context.agent_address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(msg)) logger.info("[{}]: unregistering weather station services from OEF.".format(self.context.agent_name)) self._registered_service_description = None - - def _is_time_to_update_services(self) -> bool: - """ - Check if the agent should update the service directory. - - :return: bool indicating the action - """ - now = datetime.datetime.now() - diff = now - self._last_update_time - result = diff.total_seconds() > self._services_interval - if result: - self._last_update_time = now - return result diff --git a/packages/skills/weather_station_ledger/dialogues.py b/packages/skills/weather_station_ledger/dialogues.py index acb64fa080..3c0b03dcb4 100644 --- a/packages/skills/weather_station_ledger/dialogues.py +++ b/packages/skills/weather_station_ledger/dialogues.py @@ -25,13 +25,18 @@ - Dialogues: The dialogues class keeps track of all dialogues. """ -from typing import Any, Dict, Optional +import sys +from typing import Any, Dict, Optional, TYPE_CHECKING from aea.helpers.dialogue.base import DialogueLabel -from aea.protocols.fipa.dialogues import FIPADialogues, FIPADialogue -from aea.protocols.oef.models import Description +from aea.helpers.search.models import Description from aea.skills.base import SharedClass +if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.fipa.dialogues import FIPADialogues, FIPADialogue +else: + from fipa_protocol.dialogues import FIPADialogues, FIPADialogue + class Dialogue(FIPADialogue): """The dialogue class maintains state of a dialogue and manages it.""" diff --git a/packages/skills/weather_station_ledger/handlers.py b/packages/skills/weather_station_ledger/handlers.py index 585812f3cd..ec633651ce 100644 --- a/packages/skills/weather_station_ledger/handlers.py +++ b/packages/skills/weather_station_ledger/handlers.py @@ -21,21 +21,23 @@ import logging import sys -from typing import Optional, Tuple, cast, TYPE_CHECKING +from typing import Optional, cast, TYPE_CHECKING from aea.configurations.base import ProtocolId +from aea.helpers.search.models import Description, Query from aea.protocols.base import Message from aea.protocols.default.message import DefaultMessage from aea.protocols.default.serialization import DefaultSerializer -from aea.protocols.fipa.message import FIPAMessage -from aea.protocols.fipa.serialization import FIPASerializer -from aea.protocols.oef.models import Description, Query from aea.skills.base import Handler if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.fipa.message import FIPAMessage + from packages.protocols.fipa.serialization import FIPASerializer from packages.skills.weather_station_ledger.dialogues import Dialogue, Dialogues from packages.skills.weather_station_ledger.strategy import Strategy else: + from fipa_protocol.message import FIPAMessage + from fipa_protocol.serialization import FIPASerializer from weather_station_ledger_skill.dialogues import Dialogue, Dialogues from weather_station_ledger_skill.strategy import Strategy @@ -51,41 +53,40 @@ def setup(self) -> None: """Implement the setup for the handler.""" pass - def handle(self, message: Message, sender: str) -> None: + def handle(self, message: Message) -> None: """ Implement the reaction to a message. :param message: the message - :param sender: the sender :return: None """ # convenience representations fipa_msg = cast(FIPAMessage, message) - msg_performative = FIPAMessage.Performative(fipa_msg.get('performative')) - message_id = cast(int, fipa_msg.get('message_id')) - dialogue_reference = cast(Tuple[str, str], fipa_msg.get('dialogue_reference')) + dialogue_reference = fipa_msg.dialogue_reference # recover dialogue dialogues = cast(Dialogues, self.context.dialogues) - if dialogues.is_belonging_to_registered_dialogue(fipa_msg, sender, self.context.agent_public_key): - dialogue = cast(Dialogue, dialogues.get_dialogue(fipa_msg, sender, self.context.agent_public_key)) + if dialogues.is_belonging_to_registered_dialogue(fipa_msg, self.context.agent_address): + dialogue = cast(Dialogue, dialogues.get_dialogue(fipa_msg, self.context.agent_address)) dialogue.incoming_extend(fipa_msg) - elif dialogues.is_permitted_for_new_dialogue(fipa_msg, sender): - dialogue = cast(Dialogue, dialogues.create_opponent_initiated(sender, dialogue_reference, is_seller=True)) + elif dialogues.is_permitted_for_new_dialogue(fipa_msg): + dialogue = cast(Dialogue, dialogues.create_opponent_initiated(message.counterparty, + dialogue_reference=dialogue_reference, + is_seller=True)) dialogue.incoming_extend(fipa_msg) else: - self._handle_unidentified_dialogue(fipa_msg, sender) + self._handle_unidentified_dialogue(fipa_msg) return # handle message - if msg_performative == FIPAMessage.Performative.CFP: - self._handle_cfp(fipa_msg, sender, message_id, dialogue) - elif msg_performative == FIPAMessage.Performative.DECLINE: - self._handle_decline(fipa_msg, sender, message_id, dialogue) - elif msg_performative == FIPAMessage.Performative.ACCEPT: - self._handle_accept(fipa_msg, sender, message_id, dialogue) - elif msg_performative == FIPAMessage.Performative.INFORM: - self._handle_inform(fipa_msg, sender, message_id, dialogue) + if fipa_msg.performative == FIPAMessage.Performative.CFP: + self._handle_cfp(fipa_msg, dialogue) + elif fipa_msg.performative == FIPAMessage.Performative.DECLINE: + self._handle_decline(fipa_msg, dialogue) + elif fipa_msg.performative == FIPAMessage.Performative.ACCEPT: + self._handle_accept(fipa_msg, dialogue) + elif fipa_msg.performative == FIPAMessage.Performative.INFORM: + self._handle_inform(fipa_msg, dialogue) def teardown(self) -> None: """ @@ -95,14 +96,14 @@ def teardown(self) -> None: """ pass - def _handle_unidentified_dialogue(self, msg: FIPAMessage, sender: str) -> None: + def _handle_unidentified_dialogue(self, msg: FIPAMessage) -> None: """ Handle an unidentified dialogue. Respond to the sender with a default message containing the appropriate error information. :param msg: the message - :param sender: the sender + :return: None """ logger.info("[{}]: unidentified dialogue.".format(self.context.agent_name)) @@ -110,28 +111,26 @@ def _handle_unidentified_dialogue(self, msg: FIPAMessage, sender: str) -> None: error_code=DefaultMessage.ErrorCode.INVALID_DIALOGUE.value, error_msg="Invalid dialogue.", error_data="fipa_message") # FIPASerializer().encode(msg) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=DefaultMessage.protocol_id, message=DefaultSerializer().encode(default_msg)) - def _handle_cfp(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: Dialogue) -> None: + def _handle_cfp(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the CFP. If the CFP matches the supplied services then send a PROPOSE, otherwise send a DECLINE. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ - new_message_id = message_id + 1 - new_target = message_id + new_message_id = msg.message_id + 1 + new_target = msg.message_id logger.info("[{}]: received CFP from sender={}".format(self.context.agent_name, - sender[-5:])) - query = cast(Query, msg.get("query")) + msg.counterparty[-5:])) + query = cast(Query, msg.query) strategy = cast(Strategy, self.context.strategy) if strategy.is_matching_supply(query): @@ -139,7 +138,7 @@ def _handle_cfp(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: dialogue.weather_data = weather_data dialogue.proposal = proposal logger.info("[{}]: sending sender={} a PROPOSE with proposal={}".format(self.context.agent_name, - sender[-5:], + msg.counterparty[-5:], proposal.values)) proposal_msg = FIPAMessage(message_id=new_message_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, @@ -147,72 +146,68 @@ def _handle_cfp(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: performative=FIPAMessage.Performative.PROPOSE, proposal=[proposal]) dialogue.outgoing_extend(proposal_msg) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(proposal_msg)) else: logger.info("[{}]: declined the CFP from sender={}".format(self.context.agent_name, - sender[-5:])) + msg.counterparty[-5:])) decline_msg = FIPAMessage(message_id=new_message_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, target=new_target, performative=FIPAMessage.Performative.DECLINE) dialogue.outgoing_extend(decline_msg) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(decline_msg)) - def _handle_decline(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: Dialogue) -> None: + def _handle_decline(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the DECLINE. Close the dialogue. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ logger.info("[{}]: received DECLINE from sender={}".format(self.context.agent_name, - sender[-5:])) + msg.counterparty[-5:])) # dialogues = cast(Dialogues, self.context.dialogues) # dialogues.dialogue_stats.add_dialogue_endstate(Dialogue.EndState.DECLINED_PROPOSE) - def _handle_accept(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: Dialogue) -> None: + def _handle_accept(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the ACCEPT. - Respond with a MATCH_ACCEPT_W_ADDRESS which contains the address to send the funds to. + Respond with a MATCH_ACCEPT_W_INFORM which contains the address to send the funds to. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ - new_message_id = message_id + 1 - new_target = message_id + new_message_id = msg.message_id + 1 + new_target = msg.message_id logger.info("[{}]: received ACCEPT from sender={}".format(self.context.agent_name, - sender[-5:])) - logger.info("[{}]: sending MATCH_ACCEPT_W_ADDRESS to sender={}".format(self.context.agent_name, - sender[-5:])) + msg.counterparty[-5:])) + logger.info("[{}]: sending MATCH_ACCEPT_W_INFORM to sender={}".format(self.context.agent_name, + msg.counterparty[-5:])) proposal = cast(Description, dialogue.proposal) identifier = cast(str, proposal.values.get("ledger_id")) match_accept_msg = FIPAMessage(message_id=new_message_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, target=new_target, - performative=FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS, - address=self.context.agent_addresses[identifier]) + performative=FIPAMessage.Performative.MATCH_ACCEPT_W_INFORM, + info={"address": self.context.agent_addresses[identifier]}) dialogue.outgoing_extend(match_accept_msg) - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(match_accept_msg)) - def _handle_inform(self, msg: FIPAMessage, sender: str, message_id: int, dialogue: Dialogue) -> None: + def _handle_inform(self, msg: FIPAMessage, dialogue: Dialogue) -> None: """ Handle the INFORM. @@ -220,17 +215,15 @@ def _handle_inform(self, msg: FIPAMessage, sender: str, message_id: int, dialogu If the transaction is settled send the weather data, otherwise do nothing. :param msg: the message - :param sender: the sender - :param message_id: the message id :param dialogue: the dialogue object :return: None """ - new_message_id = message_id + 1 - new_target = message_id + new_message_id = msg.message_id + 1 + new_target = msg.message_id logger.info("[{}]: received INFORM from sender={}".format(self.context.agent_name, - sender[-5:])) + msg.counterparty[-5:])) - json_data = cast(dict, msg.get("json_data")) + json_data = msg.info if "transaction_digest" in json_data.keys(): tx_digest = json_data['transaction_digest'] logger.info("[{}]: checking whether transaction={} has been received ...".format(self.context.agent_name, @@ -245,16 +238,16 @@ def _handle_inform(self, msg: FIPAMessage, sender: str, message_id: int, dialogu logger.info("[{}]: transaction={} settled, new balance={}. Sending data to sender={}".format(self.context.agent_name, tx_digest, token_balance, - sender[-5:])) + msg.counterparty[-5:])) inform_msg = FIPAMessage(message_id=new_message_id, dialogue_reference=dialogue.dialogue_label.dialogue_reference, target=new_target, performative=FIPAMessage.Performative.INFORM, - json_data=dialogue.weather_data) + info=dialogue.weather_data) dialogue.outgoing_extend(inform_msg) # import pdb; pdb.set_trace() - self.context.outbox.put_message(to=sender, - sender=self.context.agent_public_key, + self.context.outbox.put_message(to=msg.counterparty, + sender=self.context.agent_address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(inform_msg)) # dialogues = cast(Dialogues, self.context.dialogues) @@ -264,4 +257,4 @@ def _handle_inform(self, msg: FIPAMessage, sender: str, message_id: int, dialogu tx_digest)) else: logger.info("[{}]: did not receive transaction digest from sender={}.".format(self.context.agent_name, - sender[-5:])) + msg.counterparty[-5:])) diff --git a/packages/skills/weather_station_ledger/skill.yaml b/packages/skills/weather_station_ledger/skill.yaml index b645b195c7..34fd9b23bf 100644 --- a/packages/skills/weather_station_ledger/skill.yaml +++ b/packages/skills/weather_station_ledger/skill.yaml @@ -1,30 +1,31 @@ name: weather_station_ledger -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" behaviours: - - behaviour: - class_name: ServiceRegistrationBehaviour - args: - services_interval: 60 + service_registration: + class_name: ServiceRegistrationBehaviour + args: + services_interval: 60 handlers: - - handler: - class_name: FIPAHandler - args: {} -tasks: [] + fipa: + class_name: FIPAHandler + args: {} +tasks: {} shared_classes: - - shared_class: - class_name: Strategy - args: - date_one: "1/10/2019" - date_two: "1/12/2019" - price_per_row: 1 - seller_tx_fee: 0 - currency_pbk: 'FET' - ledger_id: 'fetchai' - - shared_class: - class_name: Dialogues - args: {} + strategy: + class_name: Strategy + args: + date_one: "1/10/2019" + date_two: "1/12/2019" + price_per_row: 1 + seller_tx_fee: 0 + currency_id: 'FET' + ledger_id: 'fetchai' + dialogues: + class_name: Dialogues + args: {} protocols: ['fipa', 'oef', 'default'] ledgers: ['fetchai'] diff --git a/packages/skills/weather_station_ledger/strategy.py b/packages/skills/weather_station_ledger/strategy.py index e62d874506..2bc8f083c8 100644 --- a/packages/skills/weather_station_ledger/strategy.py +++ b/packages/skills/weather_station_ledger/strategy.py @@ -23,7 +23,7 @@ import time from typing import Any, Dict, List, Tuple, TYPE_CHECKING -from aea.protocols.oef.models import Description, Query +from aea.helpers.search.models import Description, Query from aea.skills.base import SharedClass if TYPE_CHECKING or "pytest" in sys.modules: @@ -55,7 +55,7 @@ def __init__(self, **kwargs) -> None: """ self._price_per_row = kwargs.pop('price_per_row') if 'price_per_row' in kwargs.keys() else DEFAULT_PRICE_PER_ROW self._seller_tx_fee = kwargs.pop('seller_tx_fee') if 'seller_tx_fee' in kwargs.keys() else DEFAULT_SELLER_TX_FEE - self._currency_pbk = kwargs.pop('currency_pbk') if 'currency_pbk' in kwargs.keys() else DEFAULT_CURRENCY_PBK + self._currency_id = kwargs.pop('currency_id') if 'currency_id' in kwargs.keys() else DEFAULT_CURRENCY_PBK self._ledger_id = kwargs.pop('ledger_id') if 'ledger_id' in kwargs.keys() else DEFAULT_LEDGER_ID self._date_one = kwargs.pop('date_one') if 'date_one' in kwargs.keys() else DEFAULT_DATE_ONE self._date_two = kwargs.pop('date_two') if 'date_two' in kwargs.keys() else DEFAULT_DATE_TWO @@ -105,7 +105,7 @@ def generate_proposal_and_data(self, query: Query) -> Tuple[Description, Dict[st proposal = Description({"rows": rows, "price": total_price, "seller_tx_fee": self._seller_tx_fee, - "currency_pbk": self._currency_pbk, + "currency_id": self._currency_id, "ledger_id": self._ledger_id}) return (proposal, weather_data) diff --git a/packages/skills/weather_station_ledger/weather_station_data_model.py b/packages/skills/weather_station_ledger/weather_station_data_model.py index 778de82e95..a5b7d9f847 100644 --- a/packages/skills/weather_station_ledger/weather_station_data_model.py +++ b/packages/skills/weather_station_ledger/weather_station_data_model.py @@ -19,7 +19,7 @@ """This package contains the dataModel for the weather agent.""" -from aea.protocols.oef.models import DataModel, Attribute +from aea.helpers.search.models import DataModel, Attribute SCHEME = {'country': "UK", 'city': "Cambridge"} diff --git a/setup.cfg b/setup.cfg index f9e60ed70d..1e8ebe70e1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,9 +38,6 @@ ignore_missing_imports = True [mypy-cryptography.hazmat.primitives.serialization] ignore_missing_imports = True -[mypy-aea/protocols/fipa/fipa_pb2] -ignore_errors = True - [mypy-click_log] ignore_missing_imports = True @@ -77,7 +74,7 @@ ignore_missing_imports = True [mypy-hexbytes] ignore_missing_imports = True -[mypy-fetch.p2p.api.*] +[mypy-fetch.*] ignore_missing_imports = True # Per-module options for examples dir: @@ -101,8 +98,14 @@ ignore_missing_imports = True # Package ignores +[mypy-packages/protocols/fipa/fipa_pb2] +ignore_errors = True + [mypy-packages/protocols/tac/tac_pb2] ignore_errors = True [mypy-psycopg2] -ignore_errors = True +ignore_missing_imports = True + +[mypy-tensorflow.*] +ignore_missing_imports = True diff --git a/setup.py b/setup.py index c6e023a7f5..03cc9b3789 100644 --- a/setup.py +++ b/setup.py @@ -127,7 +127,6 @@ def get_all_extras() -> Dict: "base58", *all_extras.get("crypto", []), *all_extras.get("cli", []), - *all_extras.get("oef_connection", []), ], tests_require=["tox"], extras_require=all_extras, diff --git a/tests/conftest.py b/tests/conftest.py index fbcba352f9..a472b03276 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,7 +39,7 @@ from aea import AEA_DIR from aea.configurations.base import ConnectionConfig from aea.connections.base import Connection -from aea.mail.base import Envelope +from aea.mail.base import Envelope, Address logger = logging.getLogger(__name__) @@ -133,7 +133,7 @@ def put(self, envelope: Envelope): self._queue.put_nowait(envelope) @classmethod - def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection': + def from_config(cls, address: Address, connection_configuration: ConnectionConfig) -> 'Connection': """Return a connection obj fom a configuration.""" diff --git a/tests/data/aea-config.example.yaml b/tests/data/aea-config.example.yaml index e0ad6ec132..b1e1b6f6d1 100644 --- a/tests/data/aea-config.example.yaml +++ b/tests/data/aea-config.example.yaml @@ -1,8 +1,9 @@ aea_version: 0.1.1 agent_name: myagent -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" connections: - default-oef diff --git a/tests/data/aea-config.example_w_keys.yaml b/tests/data/aea-config.example_w_keys.yaml index d12c285049..cc4c7a79c2 100644 --- a/tests/data/aea-config.example_w_keys.yaml +++ b/tests/data/aea-config.example_w_keys.yaml @@ -1,8 +1,9 @@ aea_version: 0.1.1 agent_name: myagent -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" connections: - default-oef diff --git a/tests/data/dependencies_skill/skill.yaml b/tests/data/dependencies_skill/skill.yaml index 8938b4f578..54c31902b5 100644 --- a/tests/data/dependencies_skill/skill.yaml +++ b/tests/data/dependencies_skill/skill.yaml @@ -1,17 +1,27 @@ name: dummy -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" -behaviours: [] -handlers: [] -tasks: [] -shared_classes: [] +behaviours: {} +handlers: {} +tasks: {} +shared_classes: {} protocols: ["default"] dependencies: - - dep1==1.0.0 - - dep2~=1.1.2 - - dep3>=1.10.11a1 - - dep4<=1.11.12b2 - - dep5 >= 1.4.5.0 -description: "a skill for testing purposes." \ No newline at end of file + dep1: {version: "==1.0.0"} + dep2: {version: "~=1.1.2"} + dep3: {version: ">=1.10.11a1"} + dep4: {version: "==1.11.12b2"} + dep5: {version: ">=1.4.5.0"} + dep6: + index: https://test.pypi.org/simple + dep7: + git: https://github.com/a-random-username/a-repository.git + ref: master + dep8: # if both 'index' and 'git' are specified, 'git' will be preferred. + index: https://test.pypi.org/simple + git: https://github.com/a-random-username/a-repository.git + ref: master +description: "a skill for testing purposes." diff --git a/tests/data/dummy_aea/aea-config.yaml b/tests/data/dummy_aea/aea-config.yaml index f10e1c5b75..dbe120b7d0 100644 --- a/tests/data/dummy_aea/aea-config.yaml +++ b/tests/data/dummy_aea/aea-config.yaml @@ -1,11 +1,12 @@ aea_version: 0.1.7 agent_name: Agent0 -authors: Fetch.AI Limited +author: fetchai connections: - local default_connection: local description: dummy_aea agent description [Fill in] license: Apache 2.0 +fingerprint: "" logging_config: disable_existing_loggers: false version: 1 diff --git a/tests/data/dummy_aea/bad_requirements.txt b/tests/data/dummy_aea/bad_requirements.txt new file mode 100644 index 0000000000..5457584a36 --- /dev/null +++ b/tests/data/dummy_aea/bad_requirements.txt @@ -0,0 +1 @@ +@#%$^&*^ diff --git a/tests/data/dummy_aea/connections/local/connection.py b/tests/data/dummy_aea/connections/local/connection.py index dbefefc833..d14ec4d613 100644 --- a/tests/data/dummy_aea/connections/local/connection.py +++ b/tests/data/dummy_aea/connections/local/connection.py @@ -23,15 +23,21 @@ import logging from asyncio import Queue, AbstractEventLoop from collections import defaultdict +import sys from threading import Thread -from typing import Dict, List, Optional, cast, Set +from typing import Dict, List, Optional, Set, cast, TYPE_CHECKING from aea.configurations.base import ConnectionConfig from aea.connections.base import Connection -from aea.mail.base import Envelope, AEAConnectionError -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.models import Description, Query -from aea.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF +from aea.helpers.search.models import Description, Query +from aea.mail.base import Envelope, AEAConnectionError, Address + +if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.oef.message import OEFMessage + from packages.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF +else: + from oef_protocol.message import OEFMessage + from oef_protocol.serialization import OEFSerializer, DEFAULT_OEF logger = logging.getLogger(__name__) @@ -78,20 +84,20 @@ def _run_loop(self): self._loop.run_forever() logger.debug("Asyncio loop has been stopped.") - async def connect(self, public_key: str, writer: asyncio.Queue) -> Optional[asyncio.Queue]: + async def connect(self, address: Address, writer: asyncio.Queue) -> Optional[asyncio.Queue]: """ - Connect a public key to the node. + Connect an address to the node. - :param public_key: the public key of the agent. + :param address: the address of the agent. :param writer: the queue where the client is listening. :return: an asynchronous queue, that constitutes the communication channel. """ - if public_key in self._out_queues.keys(): + if address in self._out_queues.keys(): return None assert self._in_queue is not None q = self._in_queue # type: asyncio.Queue - self._out_queues[public_key] = writer + self._out_queues[address] = writer return q @@ -140,21 +146,22 @@ async def _handle_oef_message(self, envelope: Envelope) -> None: :return: None """ oef_message = OEFSerializer().decode(envelope.message) + oef_message = cast(OEFMessage, oef_message) sender = envelope.sender - request_id = cast(int, oef_message.get("id")) - oef_type = OEFMessage.Type(oef_message.get("type")) + request_id = oef_message.id + oef_type = oef_message.type if oef_type == OEFMessage.Type.REGISTER_SERVICE: - await self._register_service(sender, cast(Description, oef_message.get("service_description"))) + await self._register_service(sender, oef_message.service_description) elif oef_type == OEFMessage.Type.REGISTER_AGENT: - await self._register_agent(sender, cast(Description, oef_message.get("agent_description"))) + await self._register_agent(sender, oef_message.agent_description) elif oef_type == OEFMessage.Type.UNREGISTER_SERVICE: - await self._unregister_service(sender, request_id, cast(Description, oef_message.get("service_description"))) + await self._unregister_service(sender, request_id, oef_message.service_description) elif oef_type == OEFMessage.Type.UNREGISTER_AGENT: - await self._unregister_agent(sender, request_id, cast(Description, oef_message.get("agent_description"))) + await self._unregister_agent(sender, request_id, oef_message.agent_description) elif oef_type == OEFMessage.Type.SEARCH_AGENTS: - await self._search_agents(sender, request_id, cast(Query, oef_message.get("query"))) + await self._search_agents(sender, request_id, oef_message.query) elif oef_type == OEFMessage.Type.SEARCH_SERVICES: - await self._search_services(sender, request_id, cast(Query, oef_message.get("query"))) + await self._search_services(sender, request_id, oef_message.query) else: # request not recognized pass @@ -169,7 +176,7 @@ async def _handle_agent_message(self, envelope: Envelope) -> None: destination = envelope.to if destination not in self._out_queues.keys(): - msg = OEFMessage(oef_type=OEFMessage.Type.DIALOGUE_ERROR, id=STUB_DIALOGUE_ID, dialogue_id=STUB_DIALOGUE_ID, origin=destination) + msg = OEFMessage(type=OEFMessage.Type.DIALOGUE_ERROR, id=STUB_DIALOGUE_ID, dialogue_id=STUB_DIALOGUE_ID, origin=destination) msg_bytes = OEFSerializer().encode(msg) error_envelope = Envelope(to=envelope.sender, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) await self._send(error_envelope) @@ -177,80 +184,80 @@ async def _handle_agent_message(self, envelope: Envelope) -> None: else: await self._send(envelope) - async def _register_service(self, public_key: str, service_description: Description): + async def _register_service(self, address: Address, service_description: Description): """ Register a service agent in the service directory of the node. - :param public_key: the public key of the service agent to be registered. + :param address: the address of the service agent to be registered. :param service_description: the description of the service agent to be registered. :return: None """ async with self._lock: - self.services[public_key].append(service_description) + self.services[address].append(service_description) - async def _register_agent(self, public_key: str, agent_description: Description): + async def _register_agent(self, address: Address, agent_description: Description): """ Register a service agent in the service directory of the node. - :param public_key: the public key of the service agent to be registered. + :param address: the address of the service agent to be registered. :param agent_description: the description of the service agent to be registered. :return: None """ async with self._lock: - self.agents[public_key].append(agent_description) + self.agents[address].append(agent_description) - async def _register_service_wide(self, public_key: str, service_description: Description): + async def _register_service_wide(self, address: Address, service_description: Description): """Register service wide.""" raise NotImplementedError # pragma: no cover - async def _unregister_service(self, public_key: str, msg_id: int, service_description: Description) -> None: + async def _unregister_service(self, address: Address, msg_id: int, service_description: Description) -> None: """ Unregister a service agent. - :param public_key: the public key of the service agent to be unregistered. + :param address: the address of the service agent to be unregistered. :param msg_id: the message id of the request. :param service_description: the description of the service agent to be unregistered. :return: None """ async with self._lock: - if public_key not in self.services: - msg = OEFMessage(oef_type=OEFMessage.Type.OEF_ERROR, id=msg_id, operation=OEFMessage.OEFErrorOperation.UNREGISTER_SERVICE) + if address not in self.services: + msg = OEFMessage(type=OEFMessage.Type.OEF_ERROR, id=msg_id, operation=OEFMessage.OEFErrorOperation.UNREGISTER_SERVICE) msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=address, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) await self._send(envelope) else: - self.services[public_key].remove(service_description) - if len(self.services[public_key]) == 0: - self.services.pop(public_key) + self.services[address].remove(service_description) + if len(self.services[address]) == 0: + self.services.pop(address) - async def _unregister_agent(self, public_key: str, msg_id: int, agent_description: Description) -> None: + async def _unregister_agent(self, address: Address, msg_id: int, agent_description: Description) -> None: """ Unregister an agent. :param agent_description: - :param public_key: the public key of the service agent to be unregistered. + :param address: the address of the service agent to be unregistered. :param msg_id: the message id of the request. :return: None """ async with self._lock: - if public_key not in self.agents: - msg = OEFMessage(oef_type=OEFMessage.Type.OEF_ERROR, id=msg_id, operation=OEFMessage.OEFErrorOperation.UNREGISTER_AGENT) + if address not in self.agents: + msg = OEFMessage(type=OEFMessage.Type.OEF_ERROR, id=msg_id, operation=OEFMessage.OEFErrorOperation.UNREGISTER_AGENT) msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=address, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) await self._send(envelope) else: - self.agents[public_key].remove(agent_description) - if len(self.agents[public_key]) == 0: - self.agents.pop(public_key) + self.agents[address].remove(agent_description) + if len(self.agents[address]) == 0: + self.agents.pop(address) - async def _search_agents(self, public_key: str, search_id: int, query: Query) -> None: + async def _search_agents(self, address: Address, search_id: int, query: Query) -> None: """ Search the agents in the local Agent Directory, and send back the result. This is actually a dummy search, it will return all the registered agents with the specified data model. If the data model is not specified, it will return all the agents. - :param public_key: the source of the search request. + :param address: the source of the search request. :param search_id: the search identifier associated with the search request. :param query: the query that constitutes the search. :return: None @@ -259,24 +266,24 @@ async def _search_agents(self, public_key: str, search_id: int, query: Query) -> if query.model is None: result = list(set(self.services.keys())) else: - for agent_public_key, descriptions in self.agents.items(): + for agent_address, descriptions in self.agents.items(): for description in descriptions: if query.model == description.data_model: - result.append(agent_public_key) + result.append(agent_address) - msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_RESULT, id=search_id, agents=sorted(set(result))) + msg = OEFMessage(type=OEFMessage.Type.SEARCH_RESULT, id=search_id, agents=sorted(set(result))) msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=address, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) await self._send(envelope) - async def _search_services(self, public_key: str, search_id: int, query: Query) -> None: + async def _search_services(self, address: Address, search_id: int, query: Query) -> None: """ Search the agents in the local Service Directory, and send back the result. This is actually a dummy search, it will return all the registered agents with the specified data model. If the data model is not specified, it will return all the agents. - :param public_key: the source of the search request. + :param address: the source of the search request. :param search_id: the search identifier associated with the search request. :param query: the query that constitutes the search. :return: None @@ -285,14 +292,14 @@ async def _search_services(self, public_key: str, search_id: int, query: Query) if query.model is None: result = list(set(self.services.keys())) else: - for agent_public_key, descriptions in self.services.items(): + for agent_address, descriptions in self.services.items(): for description in descriptions: if description.data_model == query.model: - result.append(agent_public_key) + result.append(agent_address) - msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_RESULT, id=search_id, agents=sorted(set(result))) + msg = OEFMessage(type=OEFMessage.Type.SEARCH_RESULT, id=search_id, agents=sorted(set(result))) msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=address, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) await self._send(envelope) async def _send(self, envelope: Envelope): @@ -302,17 +309,17 @@ async def _send(self, envelope: Envelope): destination_queue._loop.call_soon_threadsafe(destination_queue.put_nowait, envelope) # type: ignore logger.debug("Send envelope {}".format(envelope)) - async def disconnect(self, public_key: str) -> None: + async def disconnect(self, address: Address) -> None: """ Disconnect. - :param public_key: the public key + :param address: the address of the agent :return: None """ async with self._lock: - self._out_queues.pop(public_key, None) - self.services.pop(public_key, None) - self.agents.pop(public_key, None) + self._out_queues.pop(address, None) + self.services.pop(address, None) + self.agents.pop(address, None) class OEFLocalConnection(Connection): @@ -323,38 +330,39 @@ class OEFLocalConnection(Connection): It is useful for local testing. """ - def __init__(self, public_key: str, local_node: LocalNode, connection_id: str = "local", - restricted_to_protocols: Optional[Set[str]] = None): + def __init__(self, address: Address, local_node: LocalNode, connection_id: str = "local", + restricted_to_protocols: Optional[Set[str]] = None, excluded_protocols: Optional[Set[str]] = None): """ Initialize a OEF proxy for a local OEF Node (that is, :class:`~oef.proxy.OEFLocalProxy.LocalNode`. - :param public_key: the public key used in the protocols. + :param address: the address used in the protocols. :param local_node: the Local OEF Node object. This reference must be the same across the agents of interest. """ - super().__init__(connection_id=connection_id, restricted_to_protocols=restricted_to_protocols) - self._public_key = public_key + super().__init__(connection_id=connection_id, restricted_to_protocols=restricted_to_protocols, + excluded_protocols=excluded_protocols) + self._address = address self._local_node = local_node self._reader = None # type: Optional[Queue] self._writer = None # type: Optional[Queue] @property - def public_key(self) -> str: - """Get the public key.""" - return self._public_key + def address(self) -> str: + """Get the address.""" + return self._address async def connect(self) -> None: """Connect to the local OEF Node.""" if not self.connection_status.is_connected: self._reader = Queue() - self._writer = await self._local_node.connect(self._public_key, self._reader) + self._writer = await self._local_node.connect(self._address, self._reader) self.connection_status.is_connected = True async def disconnect(self) -> None: """Disconnect from the local OEF Node.""" if self.connection_status.is_connected: assert self._reader is not None - await self._local_node.disconnect(self.public_key) + await self._local_node.disconnect(self.address) await self._reader.put(None) self._reader, self._writer = None, None self.connection_status.is_connected = False @@ -385,14 +393,15 @@ async def receive(self, *args, **kwargs) -> Optional['Envelope']: return None @classmethod - def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection': + def from_config(cls, address: Address, connection_configuration: ConnectionConfig) -> 'Connection': """Get the Local OEF connection from the connection configuration. - :param public_key: the public key of the agent. + :param address: the address of the agent. :param connection_configuration: the connection configuration object. :return: the connection object """ local_node = LocalNode() - return OEFLocalConnection(public_key, local_node, + return OEFLocalConnection(address, local_node, connection_id=connection_configuration.name, - restricted_to_protocols=set(connection_configuration.restricted_to_protocols)) + restricted_to_protocols=set(connection_configuration.restricted_to_protocols), + excluded_protocols=set(connection_configuration.excluded_protocols)) diff --git a/tests/data/dummy_aea/connections/local/connection.yaml b/tests/data/dummy_aea/connections/local/connection.yaml index 6697ab36e2..c0b41f5d1f 100644 --- a/tests/data/dummy_aea/connections/local/connection.yaml +++ b/tests/data/dummy_aea/connections/local/connection.yaml @@ -1,9 +1,12 @@ name: local -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" description: "The local connection provides a stub for an OEF node." class_name: OEFLocalConnection +protocols: ["oef"] restricted_to_protocols: [] +excluded_protocols: [] config: {} diff --git a/tests/data/dummy_aea/protocols/default/message.py b/tests/data/dummy_aea/protocols/default/message.py index 8baa2f2d19..885e1b61c6 100644 --- a/tests/data/dummy_aea/protocols/default/message.py +++ b/tests/data/dummy_aea/protocols/default/message.py @@ -20,7 +20,7 @@ """This module contains the default message definition.""" from enum import Enum -from typing import Optional +from typing import cast, Dict, Any from aea.protocols.base import Message @@ -49,7 +49,7 @@ class ErrorCode(Enum): UNSUPPORTED_SKILL = -10004 INVALID_DIALOGUE = -10005 - def __init__(self, type: Optional[Type] = None, + def __init__(self, type: Type, **kwargs): """ Initialize. @@ -59,20 +59,49 @@ def __init__(self, type: Optional[Type] = None, super().__init__(type=type, **kwargs) assert self.check_consistency(), "DefaultMessage initialization inconsistent." + @property + def type(self) -> Type: # noqa: F821 + """Get the type of the message.""" + assert self.is_set("type"), "type is not set" + return DefaultMessage.Type(self.get("type")) + + @property + def content(self) -> bytes: + """Get the content of the message.""" + assert self.is_set("content"), "content is not set!" + return cast(bytes, self.get("content")) + + @property + def error_code(self) -> ErrorCode: # noqa: F821 + """Get the error_code of the message.""" + assert self.is_set("error_code"), "error_code is not set" + return DefaultMessage.ErrorCode(self.get("error_code")) + + @property + def error_msg(self) -> str: + """Get the error message.""" + assert self.is_set("error_msg"), "error_msg is not set" + return cast(str, self.get("error_msg")) + + @property + def error_data(self) -> Dict[str, Any]: + """Get the data of the error message.""" + assert self.is_set("error_data"), "error_data is not set." + return cast(Dict[str, Any], self.get("error_data")) + def check_consistency(self) -> bool: """Check that the data is consistent.""" try: - ttype = DefaultMessage.Type(self.get("type")) - if ttype == DefaultMessage.Type.BYTES: - assert self.is_set("content") - content = self.get("content") - assert isinstance(content, bytes) - elif ttype == DefaultMessage.Type.ERROR: - assert self.is_set("error_code") - error_code = DefaultMessage.ErrorCode(self.get("error_code")) - assert error_code in DefaultMessage.ErrorCode - assert self.is_set("error_msg") - assert self.is_set("error_data") + assert isinstance(self.type, DefaultMessage.Type) + if self.type == DefaultMessage.Type.BYTES: + assert isinstance(self.content, bytes), "Expect the content to be bytes" + assert len(self.body) == 2 + elif self.type == DefaultMessage.Type.ERROR: + assert self.error_code in DefaultMessage.ErrorCode, "ErrorCode is not valid" + assert isinstance(self.error_code, DefaultMessage.ErrorCode), "error_code has wrong type." + assert isinstance(self.error_msg, str), "error_msg should be str" + assert isinstance(self.error_data, dict), "error_data should be dict" + assert len(self.body) == 4 else: raise ValueError("Type not recognized.") diff --git a/tests/data/dummy_aea/protocols/default/protocol.yaml b/tests/data/dummy_aea/protocols/default/protocol.yaml index b07cec7c60..15940ced51 100644 --- a/tests/data/dummy_aea/protocols/default/protocol.yaml +++ b/tests/data/dummy_aea/protocols/default/protocol.yaml @@ -1,7 +1,8 @@ name: 'default' -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" description: "The default protocol allows for any bytes message." -dependencies: [] \ No newline at end of file +dependencies: {} diff --git a/tests/data/dummy_aea/protocols/default/serialization.py b/tests/data/dummy_aea/protocols/default/serialization.py index 080b8f386b..c2da39819d 100644 --- a/tests/data/dummy_aea/protocols/default/serialization.py +++ b/tests/data/dummy_aea/protocols/default/serialization.py @@ -33,18 +33,16 @@ class DefaultSerializer(Serializer): def encode(self, msg: Message) -> bytes: """Encode a 'default' message into bytes.""" + msg = cast(DefaultMessage, msg) body = {} # Dict[str, Any] + body["type"] = msg.type.value - msg_type = DefaultMessage.Type(msg.get("type")) - body["type"] = str(msg_type.value) - - if msg_type == DefaultMessage.Type.BYTES: - content = cast(bytes, msg.get("content")) - body["content"] = base64.b64encode(content).decode("utf-8") - elif msg_type == DefaultMessage.Type.ERROR: - body["error_code"] = cast(str, msg.get("error_code")) - body["error_msg"] = cast(str, msg.get("error_msg")) - body["error_data"] = cast(str, msg.get("error_data")) + if msg.type == DefaultMessage.Type.BYTES: + body["content"] = base64.b64encode(msg.content).decode("utf-8") + elif msg.type == DefaultMessage.Type.ERROR: + body["error_code"] = msg.error_code.value + body["error_msg"] = msg.error_msg + body["error_data"] = msg.error_data else: raise ValueError("Type not recognized.") diff --git a/tests/data/dummy_aea/protocols/fipa/README.md b/tests/data/dummy_aea/protocols/fipa/README.md deleted file mode 100644 index 70b80ac297..0000000000 --- a/tests/data/dummy_aea/protocols/fipa/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# fipa protocol - -## To update the `fipa_pb2.py` file - - cd aea/protocols/fipa - protoc --python_out=. fipa.proto \ No newline at end of file diff --git a/tests/data/dummy_aea/protocols/fipa/dialogues.py b/tests/data/dummy_aea/protocols/fipa/dialogues.py index 2968a7aafc..abb8d903fa 100644 --- a/tests/data/dummy_aea/protocols/fipa/dialogues.py +++ b/tests/data/dummy_aea/protocols/fipa/dialogues.py @@ -27,12 +27,17 @@ """ from enum import Enum -from typing import Dict, Tuple, cast +import sys +from typing import Dict, Tuple, cast, TYPE_CHECKING from aea.helpers.dialogue.base import DialogueLabel, Dialogue, Dialogues from aea.mail.base import Address from aea.protocols.base import Message -from aea.protocols.fipa.message import FIPAMessage, VALID_PREVIOUS_PERFORMATIVES + +if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.fipa.message import FIPAMessage, VALID_PREVIOUS_PERFORMATIVES +else: + from fipa_protocol.message import FIPAMessage, VALID_PREVIOUS_PERFORMATIVES # pragma: no cover class FIPADialogue(Dialogue): @@ -75,24 +80,25 @@ def role(self) -> 'FIPADialogue.AgentRole': """Get role of agent in dialogue.""" return self._role - def is_valid_next_message(self, fipa_msg: FIPAMessage) -> bool: + def is_valid_next_message(self, fipa_msg: Message) -> bool: """ Check whether this is a valid next message in the dialogue. :return: True if yes, False otherwise. """ - this_message_id = fipa_msg.get("message_id") - this_target = fipa_msg.get("target") - this_performative = cast(FIPAMessage.Performative, fipa_msg.get("performative")) - last_outgoing_message = self.last_outgoing_message + fipa_msg = cast(FIPAMessage, fipa_msg) + this_message_id = fipa_msg.message_id + this_target = fipa_msg.target + this_performative = fipa_msg.performative + last_outgoing_message = cast(FIPAMessage, self.last_outgoing_message) if last_outgoing_message is None: result = this_message_id == FIPAMessage.STARTING_MESSAGE_ID and \ this_target == FIPAMessage.STARTING_TARGET and \ this_performative == FIPAMessage.Performative.CFP else: - last_message_id = cast(int, last_outgoing_message.get("message_id")) - last_target = cast(int, last_outgoing_message.get("target")) - last_performative = cast(FIPAMessage.Performative, last_outgoing_message.get("performative")) + last_message_id = last_outgoing_message.message_id + last_target = last_outgoing_message.target + last_performative = last_outgoing_message.performative result = this_message_id == last_message_id + 1 and \ this_target == last_target + 1 and \ last_performative in VALID_PREVIOUS_PERFORMATIVES[this_performative] @@ -108,8 +114,8 @@ def assign_final_dialogue_label(self, final_dialogue_label: DialogueLabel) -> No assert self.dialogue_label.dialogue_starter_reference == final_dialogue_label.dialogue_starter_reference assert self.dialogue_label.dialogue_responder_reference == '' assert final_dialogue_label.dialogue_responder_reference != '' - assert self.dialogue_label.dialogue_opponent_pbk == final_dialogue_label.dialogue_opponent_pbk - assert self.dialogue_label.dialogue_starter_pbk == final_dialogue_label.dialogue_starter_pbk + assert self.dialogue_label.dialogue_opponent_addr == final_dialogue_label.dialogue_opponent_addr + assert self.dialogue_label.dialogue_starter_addr == final_dialogue_label.dialogue_starter_addr self._dialogue_label = final_dialogue_label @@ -182,45 +188,44 @@ def dialogue_stats(self) -> FIPADialogueStats: """Get the dialogue statistics.""" return self._dialogue_stats - def is_permitted_for_new_dialogue(self, fipa_msg: Message, sender: Address) -> bool: + def is_permitted_for_new_dialogue(self, fipa_msg: Message) -> bool: """ Check whether a fipa message is permitted for a new dialogue. That is, the message has to - be a CFP, and - - have the correct msg id and message target. + - have the correct msg id and message target + - have msg counterparty set. :param message: the fipa message - :param sender: the sender :return: a boolean indicating whether the message is permitted for a new dialogue """ fipa_msg = cast(FIPAMessage, fipa_msg) - this_message_id = fipa_msg.get("message_id") - this_target = fipa_msg.get("target") - this_performative = fipa_msg.get("performative") + this_message_id = fipa_msg.message_id + this_target = fipa_msg.target + this_performative = fipa_msg.performative result = this_message_id == FIPAMessage.STARTING_MESSAGE_ID and \ this_target == FIPAMessage.STARTING_TARGET and \ this_performative == FIPAMessage.Performative.CFP return result - def is_belonging_to_registered_dialogue(self, fipa_msg: Message, sender: Address, agent_pbk: Address) -> bool: + def is_belonging_to_registered_dialogue(self, fipa_msg: Message, agent_addr: Address) -> bool: """ Check whether an agent message is part of a registered dialogue. :param fipa_msg: the fipa message - :param sender: the sender - :param agent_pbk: the public key of the agent + :param agent_addr: the address of the agent :return: boolean indicating whether the message belongs to a registered dialogue """ fipa_msg = cast(FIPAMessage, fipa_msg) - dialogue_reference = cast(Tuple[str, str], fipa_msg.get("dialogue_reference")) + dialogue_reference = fipa_msg.dialogue_reference alt_dialogue_reference = (dialogue_reference[0], '') - self_initiated_dialogue_label = DialogueLabel(dialogue_reference, sender, agent_pbk) - alt_self_initiated_dialogue_label = DialogueLabel(alt_dialogue_reference, sender, agent_pbk) - other_initiated_dialogue_label = DialogueLabel(dialogue_reference, sender, sender) + self_initiated_dialogue_label = DialogueLabel(dialogue_reference, fipa_msg.counterparty, agent_addr) + alt_self_initiated_dialogue_label = DialogueLabel(alt_dialogue_reference, fipa_msg.counterparty, agent_addr) + other_initiated_dialogue_label = DialogueLabel(dialogue_reference, fipa_msg.counterparty, fipa_msg.counterparty) result = False if other_initiated_dialogue_label in self.dialogues: other_initiated_dialogue = cast(FIPADialogue, self.dialogues[other_initiated_dialogue_label]) @@ -233,25 +238,24 @@ def is_belonging_to_registered_dialogue(self, fipa_msg: Message, sender: Address result = self_initiated_dialogue.is_valid_next_message(fipa_msg) if result: self._initiated_dialogues.pop(alt_self_initiated_dialogue_label) - final_dialogue_label = DialogueLabel(dialogue_reference, alt_self_initiated_dialogue_label.dialogue_opponent_pbk, alt_self_initiated_dialogue_label.dialogue_starter_pbk) + final_dialogue_label = DialogueLabel(dialogue_reference, alt_self_initiated_dialogue_label.dialogue_opponent_addr, alt_self_initiated_dialogue_label.dialogue_starter_addr) self_initiated_dialogue.assign_final_dialogue_label(final_dialogue_label) self._add(self_initiated_dialogue) return result - def get_dialogue(self, fipa_msg: Message, sender: Address, agent_pbk: Address) -> Dialogue: + def get_dialogue(self, fipa_msg: Message, agent_addr: Address) -> Dialogue: """ Retrieve dialogue. :param fipa_msg: the fipa message - :param sender_pbk: the sender public key - :param agent_pbk: the public key of the agent + :param agent_addr: the address of the agent :return: the dialogue """ fipa_msg = cast(FIPAMessage, fipa_msg) - dialogue_reference = cast(Tuple[str, str], fipa_msg.get("dialogue_reference")) - self_initiated_dialogue_label = DialogueLabel(dialogue_reference, sender, agent_pbk) - other_initiated_dialogue_label = DialogueLabel(dialogue_reference, sender, sender) + dialogue_reference = fipa_msg.dialogue_reference + self_initiated_dialogue_label = DialogueLabel(dialogue_reference, fipa_msg.counterparty, agent_addr) + other_initiated_dialogue_label = DialogueLabel(dialogue_reference, fipa_msg.counterparty, fipa_msg.counterparty) if other_initiated_dialogue_label in self.dialogues: other_initiated_dialogue = cast(FIPADialogue, self.dialogues[other_initiated_dialogue_label]) if other_initiated_dialogue.is_valid_next_message(fipa_msg): @@ -264,35 +268,35 @@ def get_dialogue(self, fipa_msg: Message, sender: Address, agent_pbk: Address) - raise ValueError('Should have found dialogue.') return result - def create_self_initiated(self, dialogue_opponent_pbk: Address, dialogue_starter_pbk: Address, is_seller: bool) -> Dialogue: + def create_self_initiated(self, dialogue_opponent_addr: Address, dialogue_starter_addr: Address, is_seller: bool) -> Dialogue: """ Create a self initiated dialogue. - :param dialogue_opponent_pbk: the pbk of the agent with which the dialogue is kept. - :param dialogue_starter_pbk: the pbk of the agent which started the dialogue + :param dialogue_opponent_addr: the pbk of the agent with which the dialogue is kept. + :param dialogue_starter_addr: the pbk of the agent which started the dialogue :param is_seller: boolean indicating the agent role :return: the created dialogue. """ dialogue_reference = (str(self._next_dialogue_nonce()), '') - dialogue_label = DialogueLabel(dialogue_reference, dialogue_opponent_pbk, dialogue_starter_pbk) + dialogue_label = DialogueLabel(dialogue_reference, dialogue_opponent_addr, dialogue_starter_addr) dialogue = FIPADialogue(dialogue_label, is_seller) self._initiated_dialogues.update({dialogue_label: dialogue}) return dialogue - def create_opponent_initiated(self, dialogue_opponent_pbk: Address, dialogue_reference: Tuple[str, str], is_seller: bool) -> Dialogue: + def create_opponent_initiated(self, dialogue_opponent_addr: Address, dialogue_reference: Tuple[str, str], + is_seller: bool) -> Dialogue: """ Save an opponent initiated dialogue. - :param dialogue_opponent_pbk: the pbk of the agent with which the dialogue is kept. - :param dialogue_id: the id of the dialogue - :param sender: the pbk of the sender - + :param dialogue_opponent_addr: the address of the agent with which the dialogue is kept. + :param dialogue_reference: the reference of the dialogue. + :param is_seller: keeps track if the counterparty is a seller. :return: the created dialogue """ assert dialogue_reference[0] != '' and dialogue_reference[1] == '', "Cannot initiate dialogue with preassigned dialogue_responder_reference!" new_dialogue_reference = (dialogue_reference[0], str(self._next_dialogue_nonce())) - dialogue_label = DialogueLabel(new_dialogue_reference, dialogue_opponent_pbk, dialogue_opponent_pbk) + dialogue_label = DialogueLabel(new_dialogue_reference, dialogue_opponent_addr, dialogue_opponent_addr) result = self._create(dialogue_label, is_seller) return result diff --git a/tests/data/dummy_aea/protocols/fipa/fipa.proto b/tests/data/dummy_aea/protocols/fipa/fipa.proto index 65ab5f6ca7..310a5aab2b 100644 --- a/tests/data/dummy_aea/protocols/fipa/fipa.proto +++ b/tests/data/dummy_aea/protocols/fipa/fipa.proto @@ -20,24 +20,18 @@ message FIPAMessage{ message MatchAccept{} - message Accept_W_Address{ - string address = 1; - } - - message MatchAccept_W_Address{ - string address = 1; - } message Decline{} + message Inform{ bytes bytes = 1; } - message AcceptWAddress{ - string address = 1; + message AcceptWInform{ + bytes bytes = 1; } - message MatchAcceptWAddress{ - string address = 1; + message MatchAcceptWInform{ + bytes bytes = 1; } int32 message_id = 1; @@ -51,7 +45,7 @@ message FIPAMessage{ MatchAccept match_accept = 8; Decline decline = 9; Inform inform = 10; - AcceptWAddress accept_w_address = 11; - MatchAcceptWAddress match_accept_w_address = 12; + AcceptWInform accept_w_inform = 11; + MatchAcceptWInform match_accept_w_inform = 12; } } diff --git a/tests/data/dummy_aea/protocols/fipa/fipa_pb2.py b/tests/data/dummy_aea/protocols/fipa/fipa_pb2.py index 9407ded226..04675f843f 100644 --- a/tests/data/dummy_aea/protocols/fipa/fipa_pb2.py +++ b/tests/data/dummy_aea/protocols/fipa/fipa_pb2.py @@ -20,7 +20,7 @@ package='fetch.aea.fipa', syntax='proto3', serialized_options=None, - serialized_pb=_b('\n\nfipa.proto\x12\x0e\x66\x65tch.aea.fipa\"\xe6\x07\n\x0b\x46IPAMessage\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.\n\x03\x63\x66p\x18\x05 \x01(\x0b\x32\x1f.fetch.aea.fipa.FIPAMessage.CFPH\x00\x12\x36\n\x07propose\x18\x06 \x01(\x0b\x32#.fetch.aea.fipa.FIPAMessage.ProposeH\x00\x12\x34\n\x06\x61\x63\x63\x65pt\x18\x07 \x01(\x0b\x32\".fetch.aea.fipa.FIPAMessage.AcceptH\x00\x12?\n\x0cmatch_accept\x18\x08 \x01(\x0b\x32\'.fetch.aea.fipa.FIPAMessage.MatchAcceptH\x00\x12\x36\n\x07\x64\x65\x63line\x18\t \x01(\x0b\x32#.fetch.aea.fipa.FIPAMessage.DeclineH\x00\x12\x34\n\x06inform\x18\n \x01(\x0b\x32\".fetch.aea.fipa.FIPAMessage.InformH\x00\x12\x46\n\x10\x61\x63\x63\x65pt_w_address\x18\x0b \x01(\x0b\x32*.fetch.aea.fipa.FIPAMessage.AcceptWAddressH\x00\x12Q\n\x16match_accept_w_address\x18\x0c \x01(\x0b\x32/.fetch.aea.fipa.FIPAMessage.MatchAcceptWAddressH\x00\x1a}\n\x03\x43\x46P\x12\x0f\n\x05\x62ytes\x18\x01 \x01(\x0cH\x00\x12:\n\x07nothing\x18\x02 \x01(\x0b\x32\'.fetch.aea.fipa.FIPAMessage.CFP.NothingH\x00\x12\x15\n\x0bquery_bytes\x18\x03 \x01(\x0cH\x00\x1a\t\n\x07NothingB\x07\n\x05query\x1a\x1b\n\x07Propose\x12\x10\n\x08proposal\x18\x01 \x03(\x0c\x1a\x08\n\x06\x41\x63\x63\x65pt\x1a\r\n\x0bMatchAccept\x1a#\n\x10\x41\x63\x63\x65pt_W_Address\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x1a(\n\x15MatchAccept_W_Address\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x1a\t\n\x07\x44\x65\x63line\x1a\x17\n\x06Inform\x12\r\n\x05\x62ytes\x18\x01 \x01(\x0c\x1a!\n\x0e\x41\x63\x63\x65ptWAddress\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x1a&\n\x13MatchAcceptWAddress\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\tB\x0e\n\x0cperformativeb\x06proto3') + serialized_pb=_b('\n\nfipa.proto\x12\x0e\x66\x65tch.aea.fipa\"\x8d\x07\n\x0b\x46IPAMessage\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.\n\x03\x63\x66p\x18\x05 \x01(\x0b\x32\x1f.fetch.aea.fipa.FIPAMessage.CFPH\x00\x12\x36\n\x07propose\x18\x06 \x01(\x0b\x32#.fetch.aea.fipa.FIPAMessage.ProposeH\x00\x12\x34\n\x06\x61\x63\x63\x65pt\x18\x07 \x01(\x0b\x32\".fetch.aea.fipa.FIPAMessage.AcceptH\x00\x12?\n\x0cmatch_accept\x18\x08 \x01(\x0b\x32\'.fetch.aea.fipa.FIPAMessage.MatchAcceptH\x00\x12\x36\n\x07\x64\x65\x63line\x18\t \x01(\x0b\x32#.fetch.aea.fipa.FIPAMessage.DeclineH\x00\x12\x34\n\x06inform\x18\n \x01(\x0b\x32\".fetch.aea.fipa.FIPAMessage.InformH\x00\x12\x44\n\x0f\x61\x63\x63\x65pt_w_inform\x18\x0b \x01(\x0b\x32).fetch.aea.fipa.FIPAMessage.AcceptWInformH\x00\x12O\n\x15match_accept_w_inform\x18\x0c \x01(\x0b\x32..fetch.aea.fipa.FIPAMessage.MatchAcceptWInformH\x00\x1a}\n\x03\x43\x46P\x12\x0f\n\x05\x62ytes\x18\x01 \x01(\x0cH\x00\x12:\n\x07nothing\x18\x02 \x01(\x0b\x32\'.fetch.aea.fipa.FIPAMessage.CFP.NothingH\x00\x12\x15\n\x0bquery_bytes\x18\x03 \x01(\x0cH\x00\x1a\t\n\x07NothingB\x07\n\x05query\x1a\x1b\n\x07Propose\x12\x10\n\x08proposal\x18\x01 \x03(\x0c\x1a\x08\n\x06\x41\x63\x63\x65pt\x1a\r\n\x0bMatchAccept\x1a\t\n\x07\x44\x65\x63line\x1a\x17\n\x06Inform\x12\r\n\x05\x62ytes\x18\x01 \x01(\x0c\x1a\x1e\n\rAcceptWInform\x12\r\n\x05\x62ytes\x18\x01 \x01(\x0c\x1a#\n\x12MatchAcceptWInform\x12\r\n\x05\x62ytes\x18\x01 \x01(\x0c\x42\x0e\n\x0cperformativeb\x06proto3') ) @@ -45,8 +45,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=751, - serialized_end=760, + serialized_start=747, + serialized_end=756, ) _FIPAMESSAGE_CFP = _descriptor.Descriptor( @@ -92,8 +92,8 @@ name='query', full_name='fetch.aea.fipa.FIPAMessage.CFP.query', index=0, containing_type=None, fields=[]), ], - serialized_start=644, - serialized_end=769, + serialized_start=640, + serialized_end=765, ) _FIPAMESSAGE_PROPOSE = _descriptor.Descriptor( @@ -122,8 +122,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=771, - serialized_end=798, + serialized_start=767, + serialized_end=794, ) _FIPAMESSAGE_ACCEPT = _descriptor.Descriptor( @@ -145,8 +145,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=800, - serialized_end=808, + serialized_start=796, + serialized_end=804, ) _FIPAMESSAGE_MATCHACCEPT = _descriptor.Descriptor( @@ -168,68 +168,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=810, - serialized_end=823, -) - -_FIPAMESSAGE_ACCEPT_W_ADDRESS = _descriptor.Descriptor( - name='Accept_W_Address', - full_name='fetch.aea.fipa.FIPAMessage.Accept_W_Address', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='address', full_name='fetch.aea.fipa.FIPAMessage.Accept_W_Address.address', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=825, - serialized_end=860, -) - -_FIPAMESSAGE_MATCHACCEPT_W_ADDRESS = _descriptor.Descriptor( - name='MatchAccept_W_Address', - full_name='fetch.aea.fipa.FIPAMessage.MatchAccept_W_Address', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='address', full_name='fetch.aea.fipa.FIPAMessage.MatchAccept_W_Address.address', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=862, - serialized_end=902, + serialized_start=806, + serialized_end=819, ) _FIPAMESSAGE_DECLINE = _descriptor.Descriptor( @@ -251,8 +191,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=904, - serialized_end=913, + serialized_start=821, + serialized_end=830, ) _FIPAMESSAGE_INFORM = _descriptor.Descriptor( @@ -281,21 +221,21 @@ extension_ranges=[], oneofs=[ ], - serialized_start=915, - serialized_end=938, + serialized_start=832, + serialized_end=855, ) -_FIPAMESSAGE_ACCEPTWADDRESS = _descriptor.Descriptor( - name='AcceptWAddress', - full_name='fetch.aea.fipa.FIPAMessage.AcceptWAddress', +_FIPAMESSAGE_ACCEPTWINFORM = _descriptor.Descriptor( + name='AcceptWInform', + full_name='fetch.aea.fipa.FIPAMessage.AcceptWInform', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='address', full_name='fetch.aea.fipa.FIPAMessage.AcceptWAddress.address', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), + name='bytes', full_name='fetch.aea.fipa.FIPAMessage.AcceptWInform.bytes', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), @@ -311,21 +251,21 @@ extension_ranges=[], oneofs=[ ], - serialized_start=940, - serialized_end=973, + serialized_start=857, + serialized_end=887, ) -_FIPAMESSAGE_MATCHACCEPTWADDRESS = _descriptor.Descriptor( - name='MatchAcceptWAddress', - full_name='fetch.aea.fipa.FIPAMessage.MatchAcceptWAddress', +_FIPAMESSAGE_MATCHACCEPTWINFORM = _descriptor.Descriptor( + name='MatchAcceptWInform', + full_name='fetch.aea.fipa.FIPAMessage.MatchAcceptWInform', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='address', full_name='fetch.aea.fipa.FIPAMessage.MatchAcceptWAddress.address', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), + name='bytes', full_name='fetch.aea.fipa.FIPAMessage.MatchAcceptWInform.bytes', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), @@ -341,8 +281,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=975, - serialized_end=1013, + serialized_start=889, + serialized_end=924, ) _FIPAMESSAGE = _descriptor.Descriptor( @@ -423,14 +363,14 @@ is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='accept_w_address', full_name='fetch.aea.fipa.FIPAMessage.accept_w_address', index=10, + name='accept_w_inform', full_name='fetch.aea.fipa.FIPAMessage.accept_w_inform', index=10, number=11, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='match_accept_w_address', full_name='fetch.aea.fipa.FIPAMessage.match_accept_w_address', index=11, + name='match_accept_w_inform', full_name='fetch.aea.fipa.FIPAMessage.match_accept_w_inform', index=11, number=12, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, @@ -439,7 +379,7 @@ ], extensions=[ ], - nested_types=[_FIPAMESSAGE_CFP, _FIPAMESSAGE_PROPOSE, _FIPAMESSAGE_ACCEPT, _FIPAMESSAGE_MATCHACCEPT, _FIPAMESSAGE_ACCEPT_W_ADDRESS, _FIPAMESSAGE_MATCHACCEPT_W_ADDRESS, _FIPAMESSAGE_DECLINE, _FIPAMESSAGE_INFORM, _FIPAMESSAGE_ACCEPTWADDRESS, _FIPAMESSAGE_MATCHACCEPTWADDRESS, ], + nested_types=[_FIPAMESSAGE_CFP, _FIPAMESSAGE_PROPOSE, _FIPAMESSAGE_ACCEPT, _FIPAMESSAGE_MATCHACCEPT, _FIPAMESSAGE_DECLINE, _FIPAMESSAGE_INFORM, _FIPAMESSAGE_ACCEPTWINFORM, _FIPAMESSAGE_MATCHACCEPTWINFORM, ], enum_types=[ ], serialized_options=None, @@ -452,7 +392,7 @@ index=0, containing_type=None, fields=[]), ], serialized_start=31, - serialized_end=1029, + serialized_end=940, ) _FIPAMESSAGE_CFP_NOTHING.containing_type = _FIPAMESSAGE_CFP @@ -470,20 +410,18 @@ _FIPAMESSAGE_PROPOSE.containing_type = _FIPAMESSAGE _FIPAMESSAGE_ACCEPT.containing_type = _FIPAMESSAGE _FIPAMESSAGE_MATCHACCEPT.containing_type = _FIPAMESSAGE -_FIPAMESSAGE_ACCEPT_W_ADDRESS.containing_type = _FIPAMESSAGE -_FIPAMESSAGE_MATCHACCEPT_W_ADDRESS.containing_type = _FIPAMESSAGE _FIPAMESSAGE_DECLINE.containing_type = _FIPAMESSAGE _FIPAMESSAGE_INFORM.containing_type = _FIPAMESSAGE -_FIPAMESSAGE_ACCEPTWADDRESS.containing_type = _FIPAMESSAGE -_FIPAMESSAGE_MATCHACCEPTWADDRESS.containing_type = _FIPAMESSAGE +_FIPAMESSAGE_ACCEPTWINFORM.containing_type = _FIPAMESSAGE +_FIPAMESSAGE_MATCHACCEPTWINFORM.containing_type = _FIPAMESSAGE _FIPAMESSAGE.fields_by_name['cfp'].message_type = _FIPAMESSAGE_CFP _FIPAMESSAGE.fields_by_name['propose'].message_type = _FIPAMESSAGE_PROPOSE _FIPAMESSAGE.fields_by_name['accept'].message_type = _FIPAMESSAGE_ACCEPT _FIPAMESSAGE.fields_by_name['match_accept'].message_type = _FIPAMESSAGE_MATCHACCEPT _FIPAMESSAGE.fields_by_name['decline'].message_type = _FIPAMESSAGE_DECLINE _FIPAMESSAGE.fields_by_name['inform'].message_type = _FIPAMESSAGE_INFORM -_FIPAMESSAGE.fields_by_name['accept_w_address'].message_type = _FIPAMESSAGE_ACCEPTWADDRESS -_FIPAMESSAGE.fields_by_name['match_accept_w_address'].message_type = _FIPAMESSAGE_MATCHACCEPTWADDRESS +_FIPAMESSAGE.fields_by_name['accept_w_inform'].message_type = _FIPAMESSAGE_ACCEPTWINFORM +_FIPAMESSAGE.fields_by_name['match_accept_w_inform'].message_type = _FIPAMESSAGE_MATCHACCEPTWINFORM _FIPAMESSAGE.oneofs_by_name['performative'].fields.append( _FIPAMESSAGE.fields_by_name['cfp']) _FIPAMESSAGE.fields_by_name['cfp'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] @@ -503,11 +441,11 @@ _FIPAMESSAGE.fields_by_name['inform']) _FIPAMESSAGE.fields_by_name['inform'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] _FIPAMESSAGE.oneofs_by_name['performative'].fields.append( - _FIPAMESSAGE.fields_by_name['accept_w_address']) -_FIPAMESSAGE.fields_by_name['accept_w_address'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] + _FIPAMESSAGE.fields_by_name['accept_w_inform']) +_FIPAMESSAGE.fields_by_name['accept_w_inform'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] _FIPAMESSAGE.oneofs_by_name['performative'].fields.append( - _FIPAMESSAGE.fields_by_name['match_accept_w_address']) -_FIPAMESSAGE.fields_by_name['match_accept_w_address'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] + _FIPAMESSAGE.fields_by_name['match_accept_w_inform']) +_FIPAMESSAGE.fields_by_name['match_accept_w_inform'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] DESCRIPTOR.message_types_by_name['FIPAMessage'] = _FIPAMESSAGE _sym_db.RegisterFileDescriptor(DESCRIPTOR) @@ -548,20 +486,6 @@ )) , - Accept_W_Address = _reflection.GeneratedProtocolMessageType('Accept_W_Address', (_message.Message,), dict( - DESCRIPTOR = _FIPAMESSAGE_ACCEPT_W_ADDRESS, - __module__ = 'fipa_pb2' - # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.Accept_W_Address) - )) - , - - MatchAccept_W_Address = _reflection.GeneratedProtocolMessageType('MatchAccept_W_Address', (_message.Message,), dict( - DESCRIPTOR = _FIPAMESSAGE_MATCHACCEPT_W_ADDRESS, - __module__ = 'fipa_pb2' - # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.MatchAccept_W_Address) - )) - , - Decline = _reflection.GeneratedProtocolMessageType('Decline', (_message.Message,), dict( DESCRIPTOR = _FIPAMESSAGE_DECLINE, __module__ = 'fipa_pb2' @@ -576,17 +500,17 @@ )) , - AcceptWAddress = _reflection.GeneratedProtocolMessageType('AcceptWAddress', (_message.Message,), dict( - DESCRIPTOR = _FIPAMESSAGE_ACCEPTWADDRESS, + AcceptWInform = _reflection.GeneratedProtocolMessageType('AcceptWInform', (_message.Message,), dict( + DESCRIPTOR = _FIPAMESSAGE_ACCEPTWINFORM, __module__ = 'fipa_pb2' - # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.AcceptWAddress) + # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.AcceptWInform) )) , - MatchAcceptWAddress = _reflection.GeneratedProtocolMessageType('MatchAcceptWAddress', (_message.Message,), dict( - DESCRIPTOR = _FIPAMESSAGE_MATCHACCEPTWADDRESS, + MatchAcceptWInform = _reflection.GeneratedProtocolMessageType('MatchAcceptWInform', (_message.Message,), dict( + DESCRIPTOR = _FIPAMESSAGE_MATCHACCEPTWINFORM, __module__ = 'fipa_pb2' - # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.MatchAcceptWAddress) + # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.MatchAcceptWInform) )) , DESCRIPTOR = _FIPAMESSAGE, @@ -599,12 +523,10 @@ _sym_db.RegisterMessage(FIPAMessage.Propose) _sym_db.RegisterMessage(FIPAMessage.Accept) _sym_db.RegisterMessage(FIPAMessage.MatchAccept) -_sym_db.RegisterMessage(FIPAMessage.Accept_W_Address) -_sym_db.RegisterMessage(FIPAMessage.MatchAccept_W_Address) _sym_db.RegisterMessage(FIPAMessage.Decline) _sym_db.RegisterMessage(FIPAMessage.Inform) -_sym_db.RegisterMessage(FIPAMessage.AcceptWAddress) -_sym_db.RegisterMessage(FIPAMessage.MatchAcceptWAddress) +_sym_db.RegisterMessage(FIPAMessage.AcceptWInform) +_sym_db.RegisterMessage(FIPAMessage.MatchAcceptWInform) # @@protoc_insertion_point(module_scope) diff --git a/tests/data/dummy_aea/protocols/fipa/message.py b/tests/data/dummy_aea/protocols/fipa/message.py index 7941becb82..6bcb368490 100644 --- a/tests/data/dummy_aea/protocols/fipa/message.py +++ b/tests/data/dummy_aea/protocols/fipa/message.py @@ -20,10 +20,10 @@ """This module contains the FIPA message definition.""" from enum import Enum -from typing import Dict, List, Optional, Tuple, Union, cast +from typing import Any, Dict, List, Tuple, Union, cast +from aea.helpers.search.models import Description, Query from aea.protocols.base import Message -from aea.protocols.oef.models import Description, Query class FIPAMessage(Message): @@ -43,17 +43,17 @@ class Performative(Enum): MATCH_ACCEPT = "match_accept" DECLINE = "decline" INFORM = "inform" - ACCEPT_W_ADDRESS = "accept_w_address" - MATCH_ACCEPT_W_ADDRESS = "match_accept_w_address" + ACCEPT_W_INFORM = "accept_w_inform" + MATCH_ACCEPT_W_INFORM = "match_accept_w_inform" def __str__(self): """Get string representation.""" return self.value - def __init__(self, dialogue_reference: Tuple[str, str] = None, - message_id: Optional[int] = None, - target: Optional[int] = None, - performative: Optional[Union[str, Performative]] = None, + def __init__(self, dialogue_reference: Tuple[str, str], + message_id: int, + target: int, + performative: Performative, **kwargs): """ Initialize. @@ -70,41 +70,69 @@ def __init__(self, dialogue_reference: Tuple[str, str] = None, **kwargs) assert self.check_consistency(), "FIPAMessage initialization inconsistent." + @property + def dialogue_reference(self) -> Tuple[str, str]: + """Get the dialogue_reference of the message.""" + assert self.is_set("dialogue_reference"), " dialogue_reference is not set" + return cast(Tuple[str, str], self.get("dialogue_reference")) + + @property + def message_id(self) -> int: + """Get the message_id of the message.""" + assert self.is_set("message_id"), "message_id is not set" + return cast(int, self.get("message_id")) + + @property + def target(self) -> int: + """Get the target of the message.""" + assert self.is_set("target"), "target is not set." + return cast(int, self.get("target")) + + @property + def performative(self) -> Performative: # noqa: F821 + """Get the performative of the message.""" + return FIPAMessage.Performative(self.get("performative")) + + @property + def query(self) -> Union[Query, bytes, None]: + """Get the query of the message.""" + assert self.is_set("query"), "query is not set." + return cast(Union[Query, bytes, None], self.get("query")) + + @property + def proposal(self) -> List[Description]: + """Get the proposal list from the message.""" + assert self.is_set("proposal"), "proposal is not set." + return cast(List[Description], self.get("proposal")) + + @property + def info(self) -> Dict[str, Any]: + """Get hte info from the message.""" + assert self.is_set("info"), "info is not set." + return cast(Dict[str, Any], self.get("info")) + def check_consistency(self) -> bool: """Check that the data is consistent.""" try: - assert self.is_set("dialogue_reference") - dialogue_reference = self.get("dialogue_reference") - assert type(dialogue_reference) == tuple - dialogue_reference = cast(Tuple, dialogue_reference) - assert type(dialogue_reference[0]) == str and type(dialogue_reference[0]) == str - assert self.is_set("message_id") - assert type(self.get("message_id")) == int - assert self.is_set("target") - assert type(self.get("target")) == int - performative = FIPAMessage.Performative(self.get("performative")) - if performative == FIPAMessage.Performative.CFP: - assert self.is_set("query") - query = self.get("query") - assert isinstance(query, Query) or isinstance(query, bytes) or query is None + assert isinstance(self.performative, FIPAMessage.Performative) + assert isinstance(self.dialogue_reference, tuple) + assert isinstance(self.dialogue_reference[0], str) and isinstance(self.dialogue_reference[1], str) + assert isinstance(self.message_id, int) + assert isinstance(self.target, int) + if self.performative == FIPAMessage.Performative.CFP: + assert isinstance(self.query, Query) or isinstance(self.query, bytes) or self.query is None assert len(self.body) == 5 - elif performative == FIPAMessage.Performative.PROPOSE: - assert self.is_set("proposal") - proposal = self.get("proposal") - assert type(proposal) == list and all(isinstance(d, Description) or type(d) == bytes for d in proposal) # type: ignore + elif self.performative == FIPAMessage.Performative.PROPOSE: + assert isinstance(self.proposal, list) and all(isinstance(d, Description) for d in self.proposal) assert len(self.body) == 5 - elif performative == FIPAMessage.Performative.ACCEPT \ - or performative == FIPAMessage.Performative.MATCH_ACCEPT \ - or performative == FIPAMessage.Performative.DECLINE: + elif self.performative == FIPAMessage.Performative.ACCEPT \ + or self.performative == FIPAMessage.Performative.MATCH_ACCEPT \ + or self.performative == FIPAMessage.Performative.DECLINE: assert len(self.body) == 4 - elif performative == FIPAMessage.Performative.ACCEPT_W_ADDRESS\ - or performative == FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS: - assert self.is_set("address") - assert len(self.body) == 5 - elif performative == FIPAMessage.Performative.INFORM: - assert self.is_set("json_data") - json_data = self.get("json_data") - assert isinstance(json_data, dict) + elif self.performative == FIPAMessage.Performative.ACCEPT_W_INFORM\ + or self.performative == FIPAMessage.Performative.MATCH_ACCEPT_W_INFORM\ + or self.performative == FIPAMessage.Performative.INFORM: + assert isinstance(self.info, dict) assert len(self.body) == 5 else: raise ValueError("Performative not recognized.") @@ -119,9 +147,9 @@ def check_consistency(self) -> bool: FIPAMessage.Performative.CFP: [None], FIPAMessage.Performative.PROPOSE: [FIPAMessage.Performative.CFP], FIPAMessage.Performative.ACCEPT: [FIPAMessage.Performative.PROPOSE], - FIPAMessage.Performative.ACCEPT_W_ADDRESS: [FIPAMessage.Performative.PROPOSE], - FIPAMessage.Performative.MATCH_ACCEPT: [FIPAMessage.Performative.ACCEPT, FIPAMessage.Performative.ACCEPT_W_ADDRESS], - FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS: [FIPAMessage.Performative.ACCEPT, FIPAMessage.Performative.ACCEPT_W_ADDRESS], - FIPAMessage.Performative.INFORM: [FIPAMessage.Performative.MATCH_ACCEPT, FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS, FIPAMessage.Performative.INFORM], - FIPAMessage.Performative.DECLINE: [FIPAMessage.Performative.CFP, FIPAMessage.Performative.PROPOSE, FIPAMessage.Performative.ACCEPT, FIPAMessage.Performative.ACCEPT_W_ADDRESS] + FIPAMessage.Performative.ACCEPT_W_INFORM: [FIPAMessage.Performative.PROPOSE], + FIPAMessage.Performative.MATCH_ACCEPT: [FIPAMessage.Performative.ACCEPT, FIPAMessage.Performative.ACCEPT_W_INFORM], + FIPAMessage.Performative.MATCH_ACCEPT_W_INFORM: [FIPAMessage.Performative.ACCEPT, FIPAMessage.Performative.ACCEPT_W_INFORM], + FIPAMessage.Performative.INFORM: [FIPAMessage.Performative.MATCH_ACCEPT, FIPAMessage.Performative.MATCH_ACCEPT_W_INFORM, FIPAMessage.Performative.INFORM], + FIPAMessage.Performative.DECLINE: [FIPAMessage.Performative.CFP, FIPAMessage.Performative.PROPOSE, FIPAMessage.Performative.ACCEPT, FIPAMessage.Performative.ACCEPT_W_INFORM] } # type: Dict[FIPAMessage.Performative, List[Union[None, FIPAMessage.Performative]]] diff --git a/tests/data/dummy_aea/protocols/fipa/protocol.yaml b/tests/data/dummy_aea/protocols/fipa/protocol.yaml index f9197cdcb6..640086227e 100644 --- a/tests/data/dummy_aea/protocols/fipa/protocol.yaml +++ b/tests/data/dummy_aea/protocols/fipa/protocol.yaml @@ -1,8 +1,9 @@ name: 'fipa' -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" dependencies: - - protobuf + protobuf: {} description: "The fipa protocol implements the FIPA ACL." diff --git a/tests/data/dummy_aea/protocols/fipa/serialization.py b/tests/data/dummy_aea/protocols/fipa/serialization.py index 959e889aaa..702b720e20 100644 --- a/tests/data/dummy_aea/protocols/fipa/serialization.py +++ b/tests/data/dummy_aea/protocols/fipa/serialization.py @@ -21,13 +21,19 @@ """Serialization for the FIPA protocol.""" import json import pickle -from typing import Tuple, cast +import sys +from typing import cast, TYPE_CHECKING +from aea.helpers.search.models import Description, Query from aea.protocols.base import Message from aea.protocols.base import Serializer -from aea.protocols.fipa import fipa_pb2 -from aea.protocols.fipa.message import FIPAMessage -from aea.protocols.oef.models import Description, Query + +if TYPE_CHECKING or "pytest" in sys.modules: + from packages.protocols.fipa import fipa_pb2 + from packages.protocols.fipa.message import FIPAMessage +else: + import fipa_protocol.fipa_pb2 as fipa_pb2 # pragma: no cover + from fipa_protocol.message import FIPAMessage # pragma: no cover class FIPASerializer(Serializer): @@ -35,17 +41,18 @@ class FIPASerializer(Serializer): def encode(self, msg: Message) -> bytes: """Encode a FIPA message into bytes.""" + msg = cast(FIPAMessage, msg) fipa_msg = fipa_pb2.FIPAMessage() - fipa_msg.message_id = msg.get("message_id") - dialogue_reference = cast(Tuple[str, str], msg.get("dialogue_reference")) + fipa_msg.message_id = msg.message_id + dialogue_reference = msg.dialogue_reference fipa_msg.dialogue_starter_reference = dialogue_reference[0] fipa_msg.dialogue_responder_reference = dialogue_reference[1] - fipa_msg.target = msg.get("target") + fipa_msg.target = msg.target - performative_id = FIPAMessage.Performative(msg.get("performative")) + performative_id = msg.performative if performative_id == FIPAMessage.Performative.CFP: performative = fipa_pb2.FIPAMessage.CFP() # type: ignore - query = msg.get("query") + query = msg.query if query is None or query == b"": nothing = fipa_pb2.FIPAMessage.CFP.Nothing() # type: ignore performative.nothing.CopyFrom(nothing) @@ -59,7 +66,7 @@ def encode(self, msg: Message) -> bytes: fipa_msg.cfp.CopyFrom(performative) elif performative_id == FIPAMessage.Performative.PROPOSE: performative = fipa_pb2.FIPAMessage.Propose() # type: ignore - proposal = cast(Description, msg.get("proposal")) + proposal = msg.proposal p_array_bytes = [pickle.dumps(p) for p in proposal] performative.proposal.extend(p_array_bytes) fipa_msg.propose.CopyFrom(performative) @@ -69,24 +76,24 @@ def encode(self, msg: Message) -> bytes: elif performative_id == FIPAMessage.Performative.MATCH_ACCEPT: performative = fipa_pb2.FIPAMessage.MatchAccept() # type: ignore fipa_msg.match_accept.CopyFrom(performative) - elif performative_id == FIPAMessage.Performative.ACCEPT_W_ADDRESS: - performative = fipa_pb2.FIPAMessage.AcceptWAddress() # type: ignore - address = msg.get("address") - if type(address) == str: - performative.address = address - fipa_msg.accept_w_address.CopyFrom(performative) - elif performative_id == FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS: - performative = fipa_pb2.FIPAMessage.MatchAcceptWAddress() # type: ignore - address = msg.get("address") - if type(address) == str: - performative.address = address - fipa_msg.match_accept_w_address.CopyFrom(performative) + elif performative_id == FIPAMessage.Performative.ACCEPT_W_INFORM: + performative = fipa_pb2.FIPAMessage.AcceptWInform() # type: ignore + data = msg.info + data_bytes = json.dumps(data).encode("utf-8") + performative.bytes = data_bytes + fipa_msg.accept_w_inform.CopyFrom(performative) + elif performative_id == FIPAMessage.Performative.MATCH_ACCEPT_W_INFORM: + performative = fipa_pb2.FIPAMessage.MatchAcceptWInform() # type: ignore + data = msg.info + data_bytes = json.dumps(data).encode("utf-8") + performative.bytes = data_bytes + fipa_msg.match_accept_w_inform.CopyFrom(performative) elif performative_id == FIPAMessage.Performative.DECLINE: performative = fipa_pb2.FIPAMessage.Decline() # type: ignore fipa_msg.decline.CopyFrom(performative) elif performative_id == FIPAMessage.Performative.INFORM: performative = fipa_pb2.FIPAMessage.Inform() # type: ignore - data = msg.get("json_data") + data = msg.info data_bytes = json.dumps(data).encode("utf-8") performative.bytes = data_bytes fipa_msg.inform.CopyFrom(performative) @@ -128,17 +135,17 @@ def decode(self, obj: bytes) -> Message: pass elif performative_id == FIPAMessage.Performative.MATCH_ACCEPT: pass - elif performative_id == FIPAMessage.Performative.ACCEPT_W_ADDRESS: - address = fipa_pb.accept_w_address.address - performative_content['address'] = address - elif performative_id == FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS: - address = fipa_pb.match_accept_w_address.address - performative_content['address'] = address + elif performative_id == FIPAMessage.Performative.ACCEPT_W_INFORM: + info = json.loads(fipa_pb.accept_w_inform.bytes) + performative_content['info'] = info + elif performative_id == FIPAMessage.Performative.MATCH_ACCEPT_W_INFORM: + info = json.loads(fipa_pb.match_accept_w_inform.bytes) + performative_content['info'] = info elif performative_id == FIPAMessage.Performative.DECLINE: pass elif performative_id == FIPAMessage.Performative.INFORM: - data = json.loads(fipa_pb.inform.bytes) - performative_content["json_data"] = data + info = json.loads(fipa_pb.inform.bytes) + performative_content["info"] = info else: raise ValueError("Performative not valid: {}.".format(performative)) diff --git a/tests/data/dummy_aea/skills/dummy/handlers.py b/tests/data/dummy_aea/skills/dummy/handlers.py index e51ea8493a..6970a7575d 100644 --- a/tests/data/dummy_aea/skills/dummy/handlers.py +++ b/tests/data/dummy_aea/skills/dummy/handlers.py @@ -43,12 +43,11 @@ def setup(self) -> None: """ pass - def handle(self, message: Message, sender: str) -> None: + def handle(self, message: Message) -> None: """ Handle message. :param message: the message - :param sender: the sender :return: None """ self.handled_messages.append(message) @@ -82,12 +81,11 @@ def setup(self) -> None: """ pass - def handle(self, message: Message, sender: str) -> None: + def handle(self, message: Message) -> None: """ Handle message. :param message: the message - :param sender: the sender :return: None """ self.handled_internal_messages.append(message) diff --git a/tests/data/dummy_aea/skills/dummy/skill.yaml b/tests/data/dummy_aea/skills/dummy/skill.yaml index fdb60a73f3..21f1e6de88 100644 --- a/tests/data/dummy_aea/skills/dummy/skill.yaml +++ b/tests/data/dummy_aea/skills/dummy/skill.yaml @@ -1,37 +1,38 @@ name: dummy -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" behaviours: - - behaviour: - class_name: DummyBehaviour - args: - behaviour_arg_1: 1 - behaviour_arg_2: "2" + dummy: + class_name: DummyBehaviour + args: + behaviour_arg_1: 1 + behaviour_arg_2: "2" handlers: - - handler: - class_name: DummyHandler - args: - handler_arg_1: 1 - handler_arg_2: "2" - - handler: - class_name: DummyInternalHandler - args: - handler_arg_1: 1 - handler_arg_2: "2" + dummy: + class_name: DummyHandler + args: + handler_arg_1: 1 + handler_arg_2: "2" + dummy_internal: + class_name: DummyInternalHandler + args: + handler_arg_1: 1 + handler_arg_2: "2" tasks: - - task: - class_name: DummyTask - args: - task_arg_1: 1 - task_arg_2: "2" + dummy: + class_name: DummyTask + args: + task_arg_1: 1 + task_arg_2: "2" shared_classes: - - shared_class: - class_name: DummySharedClass - args: - shared_class_arg_1: 1 - shared_class_arg_2: "2" + dummy: + class_name: DummySharedClass + args: + shared_class_arg_1: 1 + shared_class_arg_2: "2" protocols: ["default"] -dependencies: [] -description: "a dummy_skill for testing purposes." \ No newline at end of file +dependencies: {} +description: "a dummy_skill for testing purposes." diff --git a/tests/data/dummy_aea/skills/error/handlers.py b/tests/data/dummy_aea/skills/error/handlers.py index 2ece1c7f38..4ad94de5d7 100644 --- a/tests/data/dummy_aea/skills/error/handlers.py +++ b/tests/data/dummy_aea/skills/error/handlers.py @@ -44,12 +44,11 @@ def setup(self) -> None: :return: None """ - def handle(self, message: Message, sender: str) -> None: + def handle(self, message: Message) -> None: """ Implement the reaction to an envelope. :param message: the message - :param sender: the sender """ def teardown(self) -> None: @@ -68,10 +67,10 @@ def send_unsupported_protocol(self, envelope: Envelope) -> None: """ logger.warning("Unsupported protocol: {}".format(envelope.protocol_id)) reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.UNSUPPORTED_PROTOCOL.value, + error_code=DefaultMessage.ErrorCode.UNSUPPORTED_PROTOCOL, error_msg="Unsupported protocol.", error_data={"protocol_id": envelope.protocol_id}) - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_address, protocol_id=DefaultMessage.protocol_id, message=DefaultSerializer().encode(reply)) @@ -85,10 +84,10 @@ def send_decoding_error(self, envelope: Envelope) -> None: logger.warning("Decoding error: {}.".format(envelope)) encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.DECODING_ERROR.value, + error_code=DefaultMessage.ErrorCode.DECODING_ERROR, error_msg="Decoding error.", error_data={"envelope": encoded_envelope}) - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_address, protocol_id=DefaultMessage.protocol_id, message=DefaultSerializer().encode(reply)) @@ -102,10 +101,10 @@ def send_invalid_message(self, envelope: Envelope) -> None: logger.warning("Invalid message wrt protocol: {}.".format(envelope.protocol_id)) encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.INVALID_MESSAGE.value, + error_code=DefaultMessage.ErrorCode.INVALID_MESSAGE, error_msg="Invalid message.", error_data={"envelope": encoded_envelope}) - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_address, protocol_id=DefaultMessage.protocol_id, message=DefaultSerializer().encode(reply)) @@ -119,9 +118,9 @@ def send_unsupported_skill(self, envelope: Envelope) -> None: logger.warning("Cannot handle envelope: no handler registered for the protocol '{}'.".format(envelope.protocol_id)) encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.UNSUPPORTED_SKILL.value, + error_code=DefaultMessage.ErrorCode.UNSUPPORTED_SKILL, error_msg="Unsupported skill.", error_data={"envelope": encoded_envelope}) - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_address, protocol_id=DefaultMessage.protocol_id, message=DefaultSerializer().encode(reply)) diff --git a/tests/data/dummy_aea/skills/error/skill.yaml b/tests/data/dummy_aea/skills/error/skill.yaml index 33806a3345..7b33cf4393 100644 --- a/tests/data/dummy_aea/skills/error/skill.yaml +++ b/tests/data/dummy_aea/skills/error/skill.yaml @@ -1,15 +1,16 @@ name: error -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" description: "The error skill implements basic error handling required by all AEAs." -behaviours: [] +behaviours: {} handlers: - - handler: - class_name: ErrorHandler - args: - foo: bar -tasks: [] -shared_classes: [] + error: + class_name: ErrorHandler + args: + foo: bar +tasks: {} +shared_classes: {} protocols: ['default'] diff --git a/tests/data/dummy_connection/connection.yaml b/tests/data/dummy_connection/connection.yaml index 86af1329ad..734478c5a2 100644 --- a/tests/data/dummy_connection/connection.yaml +++ b/tests/data/dummy_connection/connection.yaml @@ -1,15 +1,19 @@ name: dummy -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" class_name: DummyConnection +protocols: [] restricted_to_protocols: - "default" +excluded_protocols: [] config: {} dependencies: - - dep1==1.0.0 - - dep2~=1.1.2 - - dep3>=1.10.11a1 - - dep4<=1.11.12b2 -description: "dummy_connection connection description [Fill in]" \ No newline at end of file + dep1: {version: "==1.0.0"} + dep2: {version: "~=1.1.2"} + dep3: {version: ">=1.10.11a1"} + dep4: {version: "<=1.11.12b2"} + +description: "dummy_connection connection description." diff --git a/tests/data/dummy_skill/handlers.py b/tests/data/dummy_skill/handlers.py index 7f33301be3..e413da7453 100644 --- a/tests/data/dummy_skill/handlers.py +++ b/tests/data/dummy_skill/handlers.py @@ -24,7 +24,7 @@ class DummyHandler(Handler): - """Echo handler.""" + """Dummy handler.""" SUPPORTED_PROTOCOL = "default" @@ -43,6 +43,44 @@ def setup(self) -> None: """ pass + def handle(self, message: Message) -> None: + """ + Handle message. + + :param message: the message + :return: None + """ + self.handled_messages.append(message) + + def teardown(self) -> None: + """ + Teardown the handler. + + :return: None + """ + self.nb_teardown_called += 1 + + +class DummyInternalHandler(Handler): + """Dummy internal handler.""" + + SUPPORTED_PROTOCOL = "internal" + + def __init__(self, **kwargs): + """Initialize the handler.""" + super().__init__(**kwargs) + self.kwargs = kwargs + self.handled_internal_messages = [] + self.nb_teardown_called = 0 + + def setup(self) -> None: + """ + Implement the setup. + + :return: None + """ + pass + def handle(self, message: Message, sender: str) -> None: """ Handle message. @@ -51,7 +89,7 @@ def handle(self, message: Message, sender: str) -> None: :param sender: the sender :return: None """ - self.handled_messages.append(message) + self.handled_internal_messages.append(message) def teardown(self) -> None: """ diff --git a/tests/data/dummy_skill/skill.yaml b/tests/data/dummy_skill/skill.yaml index cdb325074e..21f1e6de88 100644 --- a/tests/data/dummy_skill/skill.yaml +++ b/tests/data/dummy_skill/skill.yaml @@ -1,32 +1,38 @@ name: dummy -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" behaviours: - - behaviour: - class_name: DummyBehaviour - args: - behaviour_arg_1: 1 - behaviour_arg_2: "2" + dummy: + class_name: DummyBehaviour + args: + behaviour_arg_1: 1 + behaviour_arg_2: "2" handlers: - - handler: - class_name: DummyHandler - args: - handler_arg_1: 1 - handler_arg_2: "2" + dummy: + class_name: DummyHandler + args: + handler_arg_1: 1 + handler_arg_2: "2" + dummy_internal: + class_name: DummyInternalHandler + args: + handler_arg_1: 1 + handler_arg_2: "2" tasks: - - task: - class_name: DummyTask - args: - task_arg_1: 1 - task_arg_2: "2" + dummy: + class_name: DummyTask + args: + task_arg_1: 1 + task_arg_2: "2" shared_classes: - - shared_class: - class_name: DummySharedClass - args: - shared_class_arg_1: 1 - shared_class_arg_2: "2" + dummy: + class_name: DummySharedClass + args: + shared_class_arg_1: 1 + shared_class_arg_2: "2" protocols: ["default"] -dependencies: [] -description: "a dummy_skill for testing purposes." \ No newline at end of file +dependencies: {} +description: "a dummy_skill for testing purposes." diff --git a/tests/data/exception_skill/skill.yaml b/tests/data/exception_skill/skill.yaml index 2454ebe9d6..0e17f7fd08 100644 --- a/tests/data/exception_skill/skill.yaml +++ b/tests/data/exception_skill/skill.yaml @@ -1,15 +1,16 @@ name: exception -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" url: "" -behaviours: [] -handlers: [] +behaviours: {} +handlers: {} tasks: - - task: - class_name: ExceptionTask - args: {} -shared_classes: [] + exception: + class_name: ExceptionTask + args: {} +shared_classes: {} protocols: [] -dependencies: [] -description: "Raise an exception, at some point." \ No newline at end of file +dependencies: {} +description: "Raise an exception, at some point." diff --git a/tests/test_aea.py b/tests/test_aea.py index 1fd0849037..0e9904cf8f 100644 --- a/tests/test_aea.py +++ b/tests/test_aea.py @@ -29,25 +29,25 @@ from aea import AEA_DIR from aea.aea import AEA from aea.configurations.base import ProtocolConfig -from aea.connections.local.connection import LocalNode, OEFLocalConnection from aea.crypto.ledger_apis import LedgerApis from aea.crypto.wallet import Wallet from aea.mail.base import Envelope from aea.protocols.base import Protocol from aea.protocols.default.message import DefaultMessage from aea.protocols.default.serialization import DefaultSerializer -from aea.protocols.fipa.message import FIPAMessage -from aea.protocols.fipa.serialization import FIPASerializer from aea.registries.base import Resources from aea.skills.base import Skill +from packages.connections.local.connection import LocalNode, OEFLocalConnection +from packages.protocols.fipa.message import FIPAMessage +from packages.protocols.fipa.serialization import FIPASerializer from .conftest import CUR_PATH def test_initialise_AEA(): """Tests the initialisation of the AEA.""" node = LocalNode() - public_key_1 = "public_key" - connections1 = [OEFLocalConnection(public_key_1, node)] + address_1 = "address" + connections1 = [OEFLocalConnection(address_1, node)] private_key_pem_path = os.path.join(CUR_PATH, "data", "priv.pem") wallet = Wallet({'default': private_key_pem_path}) ledger_apis = LedgerApis({}) @@ -60,6 +60,7 @@ def test_initialise_AEA(): my_AEA.resources = Resources(str(Path(CUR_PATH, "aea"))) assert my_AEA.resources is not None,\ "Resources must not be None after set" + my_AEA.stop() def test_act(): @@ -69,8 +70,8 @@ def test_act(): private_key_pem_path = os.path.join(CUR_PATH, "data", "priv.pem") wallet = Wallet({'default': private_key_pem_path}) ledger_apis = LedgerApis({}) - public_key = wallet.public_keys['default'] - connections = [OEFLocalConnection(public_key, node)] + address = wallet.addresses['default'] + connections = [OEFLocalConnection(address, node)] agent = AEA( agent_name, @@ -97,16 +98,17 @@ def test_react(): private_key_pem_path = os.path.join(CUR_PATH, "data", "priv.pem") wallet = Wallet({'default': private_key_pem_path}) ledger_apis = LedgerApis({}) - public_key = wallet.public_keys['default'] - connection = OEFLocalConnection(public_key, node) + address = wallet.addresses['default'] + connection = OEFLocalConnection(address, node) connections = [connection] msg = DefaultMessage(type=DefaultMessage.Type.BYTES, content=b"hello") + msg.counterparty = address message_bytes = DefaultSerializer().encode(msg) envelope = Envelope( - to=public_key, - sender=public_key, + to=address, + sender=address, protocol_id="default", message=message_bytes) @@ -123,6 +125,7 @@ def test_react(): agent.outbox.put(envelope) time.sleep(0.5) handler = agent.resources.handler_registry.fetch_by_skill('default', "dummy") + assert handler is not None, "Handler is not set." assert msg in handler.handled_messages, "The message is not inside the handled_messages." except Exception: raise @@ -139,16 +142,17 @@ async def test_handle(): private_key_pem_path = os.path.join(CUR_PATH, "data", "priv.pem") wallet = Wallet({'default': private_key_pem_path}) ledger_apis = LedgerApis({}) - public_key = wallet.public_keys['default'] - connection = OEFLocalConnection(public_key, node) + address = wallet.addresses['default'] + connection = OEFLocalConnection(address, node) connections = [connection] msg = DefaultMessage(type=DefaultMessage.Type.BYTES, content=b"hello") + msg.counterparty = agent_name message_bytes = DefaultSerializer().encode(msg) envelope = Envelope( - to=public_key, - sender=public_key, + to=address, + sender=address, protocol_id="unknown_protocol", message=message_bytes) @@ -161,25 +165,25 @@ async def test_handle(): t = Thread(target=agent.start) try: t.start() - time.sleep(1.0) + time.sleep(2.0) dummy_skill = agent.resources.get_skill("dummy") - dummy_handler = dummy_skill.handlers[0] + dummy_handler = dummy_skill.handlers["dummy"] expected_envelope = envelope agent.outbox.put(expected_envelope) - time.sleep(1.0) + time.sleep(2.0) assert len(dummy_handler.handled_messages) == 1 # DECODING ERROR msg = "hello".encode("utf-8") envelope = Envelope( - to=public_key, - sender=public_key, + to=address, + sender=address, protocol_id='default', message=msg) expected_envelope = envelope agent.outbox.put(expected_envelope) - time.sleep(1.0) + time.sleep(2.0) assert len(dummy_handler.handled_messages) == 2 # UNSUPPORTED SKILL @@ -189,13 +193,13 @@ async def test_handle(): dialogue_reference=(str(0), ''), target=1)) envelope = Envelope( - to=public_key, - sender=public_key, + to=address, + sender=address, protocol_id="fipa", message=msg) expected_envelope = envelope agent.outbox.put(expected_envelope) - time.sleep(1.0) + time.sleep(2.0) assert len(dummy_handler.handled_messages) == 3 finally: @@ -222,6 +226,7 @@ def setup_class(cls): cls.aea = AEA(cls.agent_name, cls.connections, cls.wallet, cls.ledger_apis, cls.resources) cls.expected_message = DefaultMessage(type=DefaultMessage.Type.BYTES, content=b"hello") + cls.expected_message.counterparty = cls.agent_name envelope = Envelope(to=cls.agent_name, sender=cls.agent_name, protocol_id="default", message=DefaultSerializer().encode(cls.expected_message)) cls.t = Thread(target=cls.aea.start) @@ -286,6 +291,7 @@ def setup_class(cls): cls.resources.add_skill(cls.error_skill) cls.expected_message = DefaultMessage(type=DefaultMessage.Type.BYTES, content=b"hello") + cls.expected_message.counterparty = cls.agent_name cls.t = Thread(target=cls.aea.start) cls.t.start() diff --git a/tests/test_agent.py b/tests/test_agent.py index 9df760339b..1d86985337 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -23,9 +23,9 @@ from threading import Thread from aea.agent import Agent, AgentState -from aea.connections.local.connection import LocalNode, OEFLocalConnection from aea.crypto.wallet import Wallet from aea.mail.base import InBox, OutBox +from packages.connections.local.connection import LocalNode, OEFLocalConnection from .conftest import CUR_PATH diff --git a/tests/test_cli/constants.py b/tests/test_cli/constants.py new file mode 100644 index 0000000000..f06de0f104 --- /dev/null +++ b/tests/test_cli/constants.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""Constants used for CLI testing.""" + +FORMAT_ITEMS_SAMPLE_OUTPUT = 'Correct items' diff --git a/tests/test_cli/test_add/test_connection.py b/tests/test_cli/test_add/test_connection.py index 5a7a6cd8fb..e2dd26c3d7 100644 --- a/tests/test_cli/test_add/test_connection.py +++ b/tests/test_cli/test_add/test_connection.py @@ -31,7 +31,7 @@ import aea.cli.common import aea.configurations.base from aea.cli import cli -from tests.conftest import CLI_LOG_OPTION +from ...conftest import CLI_LOG_OPTION, CUR_PATH class TestAddConnectionFailsWhenConnectionAlreadyExists: @@ -48,6 +48,9 @@ def setup_class(cls): cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') cls.mocked_logger_error = cls.patch.__enter__() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) + os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False) assert result.exit_code == 0 @@ -94,7 +97,7 @@ def test_error_message_connection_already_existing(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -116,6 +119,9 @@ def setup_class(cls): cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') cls.mocked_logger_error = cls.patch.__enter__() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) + os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False) assert result.exit_code == 0 @@ -136,7 +142,7 @@ def test_error_message_connection_already_existing(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -158,6 +164,9 @@ def setup_class(cls): cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') cls.mocked_logger_error = cls.patch.__enter__() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) + os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False) assert result.exit_code == 0 @@ -183,7 +192,7 @@ def test_configuration_file_not_valid(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: @@ -206,6 +215,9 @@ def setup_class(cls): cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') cls.mocked_logger_error = cls.patch.__enter__() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) + os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False) assert result.exit_code == 0 @@ -228,7 +240,7 @@ def test_file_exists_error(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) diff --git a/tests/test_cli/test_add/test_protocol.py b/tests/test_cli/test_add/test_protocol.py index 3731ba56ad..6c44c0a5b2 100644 --- a/tests/test_cli/test_add/test_protocol.py +++ b/tests/test_cli/test_add/test_protocol.py @@ -31,7 +31,7 @@ import aea.cli.common import aea.configurations.base from aea.cli import cli -from tests.conftest import CLI_LOG_OPTION +from ...conftest import CLI_LOG_OPTION, CUR_PATH class TestAddProtocolFailsWhenProtocolAlreadyExists: @@ -44,10 +44,13 @@ def setup_class(cls): cls.agent_name = "myagent" cls.cwd = os.getcwd() cls.t = tempfile.mkdtemp() - cls.protocol_name = "oef" + cls.protocol_name = "gym" cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') cls.mocked_logger_error = cls.patch.__enter__() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) + os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False) assert result.exit_code == 0 @@ -92,7 +95,7 @@ def test_add_protocol_from_registry_positive( @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -114,6 +117,9 @@ def setup_class(cls): cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') cls.mocked_logger_error = cls.patch.__enter__() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) + os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False) assert result.exit_code == 0 @@ -134,7 +140,7 @@ def test_error_message_protocol_already_existing(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -152,10 +158,13 @@ def setup_class(cls): cls.agent_name = "myagent" cls.cwd = os.getcwd() cls.t = tempfile.mkdtemp() - cls.protocol_name = "oef" + cls.protocol_name = "gym" cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') cls.mocked_logger_error = cls.patch.__enter__() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) + os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False) assert result.exit_code == 0 @@ -181,7 +190,7 @@ def test_configuration_file_not_valid(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: @@ -200,10 +209,13 @@ def setup_class(cls): cls.agent_name = "myagent" cls.cwd = os.getcwd() cls.t = tempfile.mkdtemp() - cls.protocol_name = "oef" + cls.protocol_name = "gym" cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') cls.mocked_logger_error = cls.patch.__enter__() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) + os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False) assert result.exit_code == 0 @@ -226,7 +238,7 @@ def test_file_exists_error(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) diff --git a/tests/test_cli/test_add/test_skill.py b/tests/test_cli/test_add/test_skill.py index ea67e4b225..450f75c814 100644 --- a/tests/test_cli/test_add/test_skill.py +++ b/tests/test_cli/test_add/test_skill.py @@ -32,7 +32,7 @@ import aea.cli.common from aea.cli import cli from aea.configurations.base import AgentConfig, DEFAULT_AEA_CONFIG_FILE -from tests.conftest import ROOT_DIR, CLI_LOG_OPTION +from ...conftest import ROOT_DIR, CLI_LOG_OPTION, CUR_PATH class TestAddSkillFailsWhenSkillAlreadyExists: @@ -49,17 +49,15 @@ def setup_class(cls): cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') cls.mocked_logger_error = cls.patch.__enter__() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) + os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False) # this also by default adds the oef connection and error skill assert result.exit_code == 0 os.chdir(cls.agent_name) - # change default registry path - config = AgentConfig.from_json(yaml.safe_load(open(DEFAULT_AEA_CONFIG_FILE))) - config.registry_path = os.path.join(ROOT_DIR, "packages") - yaml.safe_dump(config.json, open(DEFAULT_AEA_CONFIG_FILE, "w")) - # add the error skill again cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "skill", cls.skill_name], standalone_mode=False) @@ -99,7 +97,7 @@ def test_add_skill_from_registry_positive( @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -121,6 +119,9 @@ def setup_class(cls): cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') cls.mocked_logger_error = cls.patch.__enter__() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) + os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False) assert result.exit_code == 0 @@ -141,7 +142,7 @@ def test_error_message_skill_already_existing(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -163,6 +164,9 @@ def setup_class(cls): cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') cls.mocked_logger_error = cls.patch.__enter__() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) + os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False) assert result.exit_code == 0 @@ -193,7 +197,7 @@ def test_configuration_file_not_valid(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: @@ -216,6 +220,9 @@ def setup_class(cls): cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') cls.mocked_logger_error = cls.patch.__enter__() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) + os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False) assert result.exit_code == 0 @@ -243,7 +250,7 @@ def test_file_exists_error(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) diff --git a/tests/test_cli/test_add_key.py b/tests/test_cli/test_add_key.py index a77c06519f..7a53e07a67 100644 --- a/tests/test_cli/test_add_key.py +++ b/tests/test_cli/test_add_key.py @@ -33,7 +33,7 @@ from aea.crypto.ethereum import ETHEREUM from aea.crypto.fetchai import FetchAICrypto, FETCHAI from aea.crypto.helpers import DEFAULT_PRIVATE_KEY_FILE, FETCHAI_PRIVATE_KEY_FILE, ETHEREUM_PRIVATE_KEY_FILE -from tests.conftest import CLI_LOG_OPTION +from ..conftest import CLI_LOG_OPTION from ..common.click_testing import CliRunner @@ -122,7 +122,7 @@ def test_ethereum(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) shutil.rmtree(cls.t) diff --git a/tests/test_cli/test_common.py b/tests/test_cli/test_common.py new file mode 100644 index 0000000000..be9030deea --- /dev/null +++ b/tests/test_cli/test_common.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for cli.common module.""" +from unittest import TestCase + +from aea.cli.common import format_items, format_skills + + +class FormatItemsTestCase(TestCase): + """Test case for format_items method.""" + + def test_format_items_positive(self): + """Test format_items positive result.""" + items = [ + { + 'public_id': 'owner/name:version', + 'name': 'obj-name', + 'description': 'Some description', + 'version': '1.0' + } + ] + result = format_items(items) + expected_result = ( + '------------------------------\n' + 'Public ID: owner/name:version\n' + 'Name: obj-name\n' + 'Description: Some description\n' + 'Version: 1.0\n' + '------------------------------\n' + ) + self.assertEqual(result, expected_result) + + +class FormatSkillsTestCase(TestCase): + """Test case for format_skills method.""" + + def test_format_skills_positive(self): + """Test format_skills positive result.""" + items = [ + { + 'public_id': 'owner/name:version', + 'name': 'obj-name', + 'description': 'Some description', + 'version': '1.0', + 'protocol_names': ['p1', 'p2', 'p3'] + } + ] + result = format_skills(items) + expected_result = ( + '------------------------------\n' + 'Public ID: owner/name:version\n' + 'Name: obj-name\n' + 'Description: Some description\n' + 'Protocols: p1 | p2 | p3 | \n' + 'Version: 1.0\n' + '------------------------------\n' + ) + self.assertEqual(result, expected_result) diff --git a/tests/test_cli/test_config.py b/tests/test_cli/test_config.py new file mode 100644 index 0000000000..d440ba662b --- /dev/null +++ b/tests/test_cli/test_config.py @@ -0,0 +1,219 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the `aea config` sub-command.""" +import os +import shutil +import tempfile +import unittest.mock +from pathlib import Path + +import aea.cli.common +from aea.cli import cli +from ..conftest import CLI_LOG_OPTION, CUR_PATH +from ..common.click_testing import CliRunner + + +class TestConfigGet: + """Test that the command 'aea config get' works as expected.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + shutil.copytree(Path(CUR_PATH, "data", "dummy_aea"), Path(cls.t, "dummy_aea")) + os.chdir(Path(cls.t, "dummy_aea")) + + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + cls.runner = CliRunner() + + def test_get_agent_name(self): + """Test getting the agent name.""" + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "get", "agent.agent_name"], standalone_mode=False) + assert result.exit_code == 0 + assert result.output == "Agent0\n" + + def test_get_skill_name(self): + """Test getting the 'dummy' skill name.""" + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "get", "skills.dummy.name"], standalone_mode=False) + assert result.exit_code == 0 + assert result.output == "dummy\n" + + def test_get_nested_attribute(self): + """Test getting the 'dummy' skill name.""" + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "get", "skills.dummy.behaviours.dummy.class_name"], standalone_mode=False) + assert result.exit_code == 0 + assert result.output == "DummyBehaviour\n" + + def test_no_recognized_root(self): + """Test that the 'get' fails because the root is not recognized.""" + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "get", "wrong_root.agent_name"], standalone_mode=False) + assert result.exit_code == 1 + assert result.exception.message == "The root of the dotted path must be one of: ['agent', 'skills', 'protocols', 'connections']" + + def test_too_short_path_but_root_correct(self): + """Test that the 'get' fails because the path is too short but the root is correct.""" + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "get", "agent"], standalone_mode=False) + assert result.exit_code == 1 + assert result.exception.message == "The path is too short. Please specify a path up to an attribute name." + + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "get", "skills.dummy"], standalone_mode=False) + assert result.exit_code == 1 + assert result.exception.message == "The path is too short. Please specify a path up to an attribute name." + + def test_resource_not_existing(self): + """Test that the 'get' fails because the resource does not exist.""" + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "get", "connections.non_existing_connection.name"], standalone_mode=False) + assert result.exit_code == 1 + assert result.exception.message == "Resource connections/non_existing_connection does not exist." + + def test_attribute_not_found(self): + """Test that the 'get' fails because the attribute is not found.""" + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "get", "skills.dummy.non_existing_attribute"], standalone_mode=False) + assert result.exit_code == 1 + self.mocked_logger_error.assert_called_with("Attribute 'non_existing_attribute' not found.") + + def test_get_fails_when_getting_non_primitive_type(self): + """Test that getting the 'dummy' skill behaviours fails because not a primitive type.""" + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "get", "skills.dummy.behaviours"], + standalone_mode=False) + assert result.exit_code == 1 + self.mocked_logger_error.assert_called_with("Attribute 'behaviours' is not of primitive type.") + + def test_get_fails_when_getting_nested_object(self): + """Test that getting a nested object in 'dummy' skill fails because path is not valid.""" + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "get", "skills.dummy.non_existing_attribute.dummy"], + standalone_mode=False) + assert result.exit_code == 1 + self.mocked_logger_error.assert_called_with("Cannot get attribute 'non_existing_attribute'") + + def test_get_fails_when_getting_non_dict_attribute(self): + """Test that the get fails because the path point to a non-dict object.""" + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "get", "skills.dummy.protocols.protocol"], + standalone_mode=False) + assert result.exit_code == 1 + self.mocked_logger_error.assert_called_with("The target object is not a dictionary.") + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestConfigSet: + """Test that the command 'aea config set' works as expected.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + shutil.copytree(Path(CUR_PATH, "data", "dummy_aea"), Path(cls.t, "dummy_aea")) + os.chdir(Path(cls.t, "dummy_aea")) + + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + cls.runner = CliRunner() + + def test_set_agent_name(self): + """Test setting the agent name.""" + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "set", "agent.agent_name", "new_name"], standalone_mode=False) + assert result.exit_code == 0 + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "get", "agent.agent_name"], standalone_mode=False) + assert result.exit_code == 0 + assert result.output == "new_name\n" + + def test_set_skill_name(self): + """Test setting the 'dummy' skill name.""" + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "set", "skills.dummy.name", "new_dummy_name"], standalone_mode=False) + assert result.exit_code == 0 + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "get", "skills.dummy.name"], standalone_mode=False) + assert result.exit_code == 0 + assert result.output == "new_dummy_name\n" + + def test_set_nested_attribute(self): + """Test setting a nested attribute.""" + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "set", "skills.dummy.behaviours.dummy.class_name", "new_dummy_name"], standalone_mode=False) + assert result.exit_code == 0 + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "get", "skills.dummy.behaviours.dummy.class_name"], standalone_mode=False) + assert result.exit_code == 0 + assert result.output == "new_dummy_name\n" + + def test_no_recognized_root(self): + """Test that the 'get' fails because the root is not recognized.""" + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "set", "wrong_root.agent_name", "value"], standalone_mode=False) + assert result.exit_code == 1 + assert result.exception.message == "The root of the dotted path must be one of: ['agent', 'skills', 'protocols', 'connections']" + + def test_too_short_path_but_root_correct(self): + """Test that the 'get' fails because the path is too short but the root is correct.""" + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "set", "agent"], standalone_mode=False) + assert result.exit_code == 1 + assert result.exception.message == "The path is too short. Please specify a path up to an attribute name." + + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "set", "skills.dummy", "value"], standalone_mode=False) + assert result.exit_code == 1 + assert result.exception.message == "The path is too short. Please specify a path up to an attribute name." + + def test_resource_not_existing(self): + """Test that the 'get' fails because the resource does not exist.""" + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "set", "connections.non_existing_connection.name", "value"], standalone_mode=False) + assert result.exit_code == 1 + assert result.exception.message == "Resource connections/non_existing_connection does not exist." + + def test_attribute_not_found(self): + """Test that the 'get' fails because the attribute is not found.""" + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "set", "skills.dummy.non_existing_attribute", "value"], standalone_mode=False) + assert result.exit_code == 1 + self.mocked_logger_error.assert_called_with("Attribute 'non_existing_attribute' not found.") + + def test_set_fails_when_setting_non_primitive_type(self): + """Test that setting the 'dummy' skill behaviours fails because not a primitive type.""" + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "set", "skills.dummy.behaviours", "value"], standalone_mode=False) + assert result.exit_code == 1 + self.mocked_logger_error.assert_called_with("Attribute 'behaviours' is not of primitive type.") + + def test_get_fails_when_setting_nested_object(self): + """Test that setting a nested object in 'dummy' skill fails because path is not valid.""" + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "set", "skills.dummy.non_existing_attribute.dummy", "new_value"], + standalone_mode=False) + assert result.exit_code == 1 + self.mocked_logger_error.assert_called_with("Cannot get attribute 'non_existing_attribute'") + + def test_get_fails_when_setting_non_dict_attribute(self): + """Test that the set fails because the path point to a non-dict object.""" + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "config", "set", "skills.dummy.protocols.protocol", "new_value"], + standalone_mode=False) + assert result.exit_code == 1 + self.mocked_logger_error.assert_called_with("The target object is not a dictionary.") + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass diff --git a/tests/test_cli/test_create.py b/tests/test_cli/test_create.py index 7b88b52990..47253c55cf 100644 --- a/tests/test_cli/test_create.py +++ b/tests/test_cli/test_create.py @@ -39,7 +39,7 @@ from aea.cli import cli from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE from aea.configurations.loader import ConfigLoader -from tests.conftest import AGENT_CONFIGURATION_SCHEMA, ROOT_DIR, CONFIGURATION_SCHEMA_DIR, CLI_LOG_OPTION +from ..conftest import AGENT_CONFIGURATION_SCHEMA, ROOT_DIR, CONFIGURATION_SCHEMA_DIR, CLI_LOG_OPTION class TestCreate: @@ -103,17 +103,17 @@ def test_agent_name_is_correct(self): def test_authors_field_is_empty_string(self): """Check that the 'authors' field in the config file is the empty string.""" agent_config_instance = self._load_config_file() - assert agent_config_instance["authors"] == "" + assert agent_config_instance["author"] == "" - def test_connections_contains_only_oef(self): - """Check that the 'connections' list contains only the 'oef' connection.""" + def test_connections_contains_only_stub(self): + """Check that the 'connections' list contains only the 'stub' connection.""" agent_config_instance = self._load_config_file() - assert agent_config_instance["connections"] == ["oef"] + assert agent_config_instance["connections"] == ["stub"] - def test_default_connection_field_is_oef(self): - """Check that the 'default_connection' is the 'oef' connection.""" + def test_default_connection_field_is_stub(self): + """Check that the 'default_connection' is the 'stub' connection.""" agent_config_instance = self._load_config_file() - assert agent_config_instance["default_connection"] == "oef" + assert agent_config_instance["default_connection"] == "stub" def test_license_field_is_empty_string(self): """Check that the 'license' is the empty string.""" @@ -151,21 +151,21 @@ def test_connections_directory_exists(self): assert connections_dirpath.exists() assert connections_dirpath.is_dir() - def test_connections_contains_oef_connection(self): - """Check that the connections directory contains the oef directory.""" - oef_connection_dirpath = Path(self.agent_name, "connections", "oef") - assert oef_connection_dirpath.exists() - assert oef_connection_dirpath.is_dir() + def test_connections_contains_stub_connection(self): + """Check that the connections directory contains the stub directory.""" + stub_connection_dirpath = Path(self.agent_name, "connections", "stub") + assert stub_connection_dirpath.exists() + assert stub_connection_dirpath.is_dir() - def test_oef_connection_directory_is_equal_to_library_oef_connection(self): - """Check that the oef connection directory is equal to the package's one (aea.connections.oef).""" - oef_connection_dirpath = Path(self.agent_name, "connections", "oef") - comparison = filecmp.dircmp(str(oef_connection_dirpath), str(Path(ROOT_DIR, "aea", "connections", "oef"))) + def test_stub_connection_directory_is_equal_to_library_stub_connection(self): + """Check that the stub connection directory is equal to the package's one (aea.connections.stub).""" + stub_connection_dirpath = Path(self.agent_name, "connections", "stub") + comparison = filecmp.dircmp(str(stub_connection_dirpath), str(Path(ROOT_DIR, "aea", "connections", "stub"))) assert comparison.diff_files == [] @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -207,7 +207,7 @@ def test_log_error_message(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: @@ -245,7 +245,7 @@ def test_agent_folder_is_not_created(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: @@ -283,10 +283,57 @@ def test_agent_folder_is_not_created(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: shutil.rmtree(cls.t) except (OSError, IOError): pass + + +class TestCreateFailsWhenAlreadyInAEAProject: + """Test that 'aea create' sub-command fails when it is called within an AEA project.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False) + assert cls.result.exit_code == 0 + + # calling 'aea create myagent' again within an AEA project - recursively. + os.chdir(cls.agent_name) + os.mkdir("another_subdir") + os.chdir("another_subdir") + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False) + + def test_exit_code_equal_to_1(self): + """Test that the error code is equal to 1 (i.e. catchall for general errors).""" + assert self.result.exit_code == 1 + + def test_log_error_message(self): + """Test that the log error message is fixed. + + The expected message is: "The current folder is already an AEA project. Please move to the parent folder.". + """ + s = "The current folder is already an AEA project. Please move to the parent folder." + self.mocked_logger_error.assert_called_once_with(s) + + @classmethod + def teardown_class(cls): + """Tear the test down.""" + cls.mocked_logger_error = cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass diff --git a/tests/test_cli/test_delete.py b/tests/test_cli/test_delete.py index 11c1117485..cabe18e5eb 100644 --- a/tests/test_cli/test_delete.py +++ b/tests/test_cli/test_delete.py @@ -29,7 +29,7 @@ import aea import aea.cli.common from aea.cli import cli -from tests.conftest import CLI_LOG_OPTION +from ..conftest import CLI_LOG_OPTION class TestDelete: @@ -58,7 +58,7 @@ def test_agent_directory_path_does_not_exists(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -87,7 +87,7 @@ def setup_class(cls): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -131,7 +131,7 @@ def test_log_error_message(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) cls.mocked_logger_error.__exit__() try: @@ -173,7 +173,7 @@ def test_log_error_message(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: diff --git a/tests/test_cli/test_freeze.py b/tests/test_cli/test_freeze.py index a03e6afaf9..39fe16f777 100644 --- a/tests/test_cli/test_freeze.py +++ b/tests/test_cli/test_freeze.py @@ -23,11 +23,11 @@ from pathlib import Path import jsonschema -from ..common.click_testing import CliRunner from jsonschema import Draft4Validator from aea.cli import cli -from tests.conftest import AGENT_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR, CLI_LOG_OPTION, CUR_PATH +from ..conftest import AGENT_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR, CLI_LOG_OPTION, CUR_PATH +from ..common.click_testing import CliRunner class TestFreeze: @@ -56,5 +56,5 @@ def test_correct_output(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) diff --git a/tests/test_cli/test_generate_key.py b/tests/test_cli/test_generate_key.py index 04f9e6d48e..f6aeb11a21 100644 --- a/tests/test_cli/test_generate_key.py +++ b/tests/test_cli/test_generate_key.py @@ -23,14 +23,13 @@ import tempfile from pathlib import Path -from ..common.click_testing import CliRunner - from aea.cli import cli from aea.crypto.default import DefaultCrypto from aea.crypto.ethereum import EthereumCrypto from aea.crypto.fetchai import FetchAICrypto from aea.crypto.helpers import DEFAULT_PRIVATE_KEY_FILE, FETCHAI_PRIVATE_KEY_FILE, ETHEREUM_PRIVATE_KEY_FILE -from tests.conftest import CLI_LOG_OPTION +from ..conftest import CLI_LOG_OPTION +from ..common.click_testing import CliRunner class TestGenerateKey: @@ -90,6 +89,46 @@ def test_all(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" + os.chdir(cls.cwd) + shutil.rmtree(cls.t) + + +class TestGenerateKeyWhenAlreadyExists: + """Test that the command 'aea generate-key' asks for confirmation when a key already exists.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + + def test_default(self): + """Test that the default private key is overwritten or not dependending on the user input.""" + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "generate-key", "default"]) + assert result.exit_code == 0 + assert Path(DEFAULT_PRIVATE_KEY_FILE).exists() + + # This tests if the file has been created and its content is correct. + DefaultCrypto(DEFAULT_PRIVATE_KEY_FILE) + content = Path(DEFAULT_PRIVATE_KEY_FILE).read_bytes() + + # Saying 'no' leave the files as it is. + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "generate-key", "default"], input="n") + assert result.exit_code == 0 + assert Path(DEFAULT_PRIVATE_KEY_FILE).read_bytes() == content + + # Saying 'yes' overwrites the file. + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "generate-key", "default"], input="y") + assert result.exit_code == 0 + assert Path(DEFAULT_PRIVATE_KEY_FILE).read_bytes() != content + DefaultCrypto(DEFAULT_PRIVATE_KEY_FILE) + + @classmethod + def teardown_class(cls): + """Tear the test down.""" os.chdir(cls.cwd) shutil.rmtree(cls.t) diff --git a/tests/test_cli/test_gui.py b/tests/test_cli/test_gui.py index 8700583220..0977def46c 100644 --- a/tests/test_cli/test_gui.py +++ b/tests/test_cli/test_gui.py @@ -30,7 +30,7 @@ import pytest from jsonschema import Draft4Validator -from tests.conftest import AGENT_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR, CLI_LOG_OPTION, tcpping +from ..conftest import AGENT_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR, CLI_LOG_OPTION, tcpping class TestGui: @@ -59,7 +59,7 @@ def test_gui(self, pytestconfig): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.proc.terminate() cls.proc.wait(2.0) os.chdir(cls.cwd) diff --git a/tests/test_cli/test_install.py b/tests/test_cli/test_install.py index bd726aab15..e75813b9ec 100644 --- a/tests/test_cli/test_install.py +++ b/tests/test_cli/test_install.py @@ -29,7 +29,7 @@ import aea.cli.common from aea.cli import cli from aea.configurations.base import DEFAULT_PROTOCOL_CONFIG_FILE -from tests.conftest import CLI_LOG_OPTION, CUR_PATH +from ..conftest import CLI_LOG_OPTION, CUR_PATH class TestInstall: @@ -49,7 +49,7 @@ def test_exit_code_equal_to_zero(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) @@ -71,11 +71,11 @@ def test_exit_code_equal_to_zero(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) -class TestInstallFails: +class TestInstallFailsWhenDependencyDoesNotExist: """Test that the command 'aea install' fails when a dependency is not found.""" @classmethod @@ -98,7 +98,18 @@ def setup_class(cls): config_path = Path("protocols", "my_protocol", DEFAULT_PROTOCOL_CONFIG_FILE) config = yaml.safe_load(open(config_path)) - config.setdefault("dependencies", []).append("this_dependency_does_not_exist") + config.setdefault("dependencies", {}).update( + { + "this_is_a_test_dependency": { + "version": "==0.1.0", + "index": "https://test.pypi.org/simple" + }, + "this_is_a_test_dependency_on_git": { + "git": "https://github.com/an_user/a_repo.git", + "ref": "master" + } + } + ) yaml.safe_dump(config, open(config_path, "w")) cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "install"], standalone_mode=False) @@ -108,5 +119,27 @@ def test_exit_code_equal_to_1(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" + os.chdir(cls.cwd) + + +class TestInstallWithRequirementFailsWhenFileIsBad: + """Test that the command 'aea install -r REQ_FILE' fails if the requirement file is not good.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.cwd = os.getcwd() + os.chdir(Path(CUR_PATH, "data", "dummy_aea")) + + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "install", "-r", "bad_requirements.txt"], standalone_mode=False) + + def test_exit_code_equal_to_zero(self): + """Assert that the exit code is equal to zero (i.e. success).""" + assert self.result.exit_code == 1 + + @classmethod + def teardown_class(cls): + """Tear the test down.""" os.chdir(cls.cwd) diff --git a/tests/test_cli/test_list.py b/tests/test_cli/test_list.py index 3f12ef7088..1f18f0f084 100644 --- a/tests/test_cli/test_list.py +++ b/tests/test_cli/test_list.py @@ -21,13 +21,15 @@ import json import os from pathlib import Path +from unittest import mock import jsonschema from ..common.click_testing import CliRunner from jsonschema import Draft4Validator from aea.cli import cli -from tests.conftest import AGENT_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR, CLI_LOG_OPTION, CUR_PATH +from ..conftest import AGENT_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR, CLI_LOG_OPTION, CUR_PATH +from tests.test_cli.constants import FORMAT_ITEMS_SAMPLE_OUTPUT class TestListProtocols: @@ -44,7 +46,9 @@ def setup_class(cls): cls.agent_name = "myagent" cls.cwd = os.getcwd() os.chdir(Path(CUR_PATH, "data", "dummy_aea")) - cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "list", "protocols"], standalone_mode=False) + + with mock.patch('aea.cli.list.format_items', return_value=FORMAT_ITEMS_SAMPLE_OUTPUT): + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "list", "protocols"], standalone_mode=False) def test_exit_code_equal_to_zero(self): """Assert that the exit code is equal to zero (i.e. success).""" @@ -52,23 +56,12 @@ def test_exit_code_equal_to_zero(self): def test_correct_output(self): """Test that the command has printed the correct output.""" - compare_text = """------------------------------ -Name: default -Description: The default protocol allows for any bytes message. -Version: 0.1.0 ------------------------------- ------------------------------- -Name: fipa -Description: The fipa protocol implements the FIPA ACL. -Version: 0.1.0 ------------------------------- - -""" + compare_text = "{}\n".format(FORMAT_ITEMS_SAMPLE_OUTPUT) assert self.result.output == compare_text @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) @@ -86,7 +79,9 @@ def setup_class(cls): cls.agent_name = "myagent" cls.cwd = os.getcwd() os.chdir(Path(CUR_PATH, "data", "dummy_aea")) - cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "list", "connections"], standalone_mode=False) + + with mock.patch('aea.cli.list.format_items', return_value=FORMAT_ITEMS_SAMPLE_OUTPUT): + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "list", "connections"], standalone_mode=False) def test_exit_code_equal_to_zero(self): """Assert that the exit code is equal to zero (i.e. success).""" @@ -94,18 +89,12 @@ def test_exit_code_equal_to_zero(self): def test_correct_output(self): """Test that the command has printed the correct output.""" - compare_text = """------------------------------ -Name: local -Description: The local connection provides a stub for an OEF node. -Version: 0.1.0 ------------------------------- - -""" + compare_text = "{}\n".format(FORMAT_ITEMS_SAMPLE_OUTPUT) assert self.result.output == compare_text @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) @@ -123,7 +112,9 @@ def setup_class(cls): cls.agent_name = "myagent" cls.cwd = os.getcwd() os.chdir(Path(CUR_PATH, "data", "dummy_aea")) - cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "list", "skills"], standalone_mode=False) + + with mock.patch('aea.cli.list.format_items', return_value=FORMAT_ITEMS_SAMPLE_OUTPUT): + cls.result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "list", "skills"], standalone_mode=False) def test_exit_code_equal_to_zero(self): """Assert that the exit code is equal to zero (i.e. success).""" @@ -131,21 +122,10 @@ def test_exit_code_equal_to_zero(self): def test_correct_output(self): """Test that the command has printed the correct output.""" - compare_text = """------------------------------ -Name: dummy -Description: a dummy_skill for testing purposes. -Version: 0.1.0 ------------------------------- ------------------------------- -Name: error -Description: The error skill implements basic error handling required by all AEAs. -Version: 0.1.0 ------------------------------- - -""" + compare_text = "{}\n".format(FORMAT_ITEMS_SAMPLE_OUTPUT) assert self.result.output == compare_text @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) diff --git a/tests/test_cli/test_login.py b/tests/test_cli/test_login.py new file mode 100644 index 0000000000..ce16974b8f --- /dev/null +++ b/tests/test_cli/test_login.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This test module contains the tests for CLI login command.""" +from unittest import TestCase, mock + +from aea.cli import cli + +from tests.common.click_testing import CliRunner +from ..conftest import CLI_LOG_OPTION + + +@mock.patch('aea.cli.login.registry_login', return_value='token') +@mock.patch('aea.cli.login.write_cli_config') +class LoginTestCase(TestCase): + """Test case for CLI login command.""" + + def setUp(self): + """Set it up.""" + self.runner = CliRunner() + + def test_login_positive(self, write_cli_config_mock, registry_login_mock): + """Test for CLI login positive result.""" + username, password = ('Username', 'Password') + result = self.runner.invoke( + cli, + [ + *CLI_LOG_OPTION, + "login", + username, + password + ], + standalone_mode=False + ) + expected_output = ( + 'Signing in as Username...\n' + 'Successfully signed in: Username.\n' + ) + self.assertEqual(result.output, expected_output) + registry_login_mock.assert_called_once_with(username, password) + write_cli_config_mock.assert_called_once_with({'auth_token': 'token'}) diff --git a/tests/test_cli/test_misc.py b/tests/test_cli/test_misc.py index 376ee7a455..5e100ceb71 100644 --- a/tests/test_cli/test_misc.py +++ b/tests/test_cli/test_misc.py @@ -55,13 +55,18 @@ def test_flag_help(): Commands: add Add a resource to the agent. add-key Add a private key to the wallet. + config Read or modify a configuration. create Create an agent. delete Delete an agent. + fetch Fetch Agent from Registry. freeze Get the dependencies. generate-key Generate private keys. gui Run the CLI GUI. install Install the dependencies. list List the installed resources. + login Login to Registry account + publish Publish Agent to Registry. + push Push item to Registry. remove Remove a resource from the agent. run Run the agent. scaffold Scaffold a resource for the agent. diff --git a/tests/test_cli/test_registry/test_fetch.py b/tests/test_cli/test_registry/test_fetch.py new file mode 100644 index 0000000000..6f9eca3161 --- /dev/null +++ b/tests/test_cli/test_registry/test_fetch.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This test module contains the tests for CLI Registry fetch methods.""" + +from unittest import TestCase, mock + +from aea.cli.registry.fetch import fetch_agent + + +@mock.patch( + 'aea.cli.registry.fetch.split_public_id', + return_value=['owner', 'name', 'version'] +) +@mock.patch( + 'aea.cli.registry.fetch.request_api', + return_value={'file': 'url'} +) +@mock.patch( + 'aea.cli.registry.fetch.download_file', + return_value='filepath' +) +@mock.patch('aea.cli.registry.fetch.extract') +@mock.patch('aea.cli.registry.fetch.os.getcwd', return_value='cwd') +class FetchAgentTestCase(TestCase): + """Test case for fetch_package method.""" + + def test_fetch_agent_positive( + self, + getcwd_mock, + extract_mock, + download_file_mock, + request_api_mock, + split_public_id_mock + ): + """Test for fetch_agent method positive result.""" + public_id = 'owner/name:version' + + fetch_agent(public_id) + split_public_id_mock.assert_called_with(public_id) + request_api_mock.assert_called_with( + 'GET', '/agents/owner/name/version' + ) + download_file_mock.assert_called_once_with('url', 'cwd') + extract_mock.assert_called_once_with('filepath', 'cwd/name') diff --git a/tests/test_cli/test_registry/test_publish.py b/tests/test_cli/test_registry/test_publish.py new file mode 100644 index 0000000000..64f9ddcf0c --- /dev/null +++ b/tests/test_cli/test_registry/test_publish.py @@ -0,0 +1,85 @@ +# -*- 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. +# +# ------------------------------------------------------------------------------ +"""Test module for Registry publish methods.""" + +from unittest import TestCase, mock + +from click import ClickException + +from aea.cli.registry.publish import publish_agent + + +@mock.patch('aea.cli.registry.utils._rm_tarfiles') +@mock.patch('aea.cli.registry.publish.os.getcwd', return_value='cwd') +@mock.patch('aea.cli.registry.publish._compress') +@mock.patch( + 'aea.cli.registry.publish.load_yaml', + return_value={ + 'agent_name': 'agent-name', + 'description': 'some-description', + 'version': 'some-version' + } +) +@mock.patch( + 'aea.cli.registry.publish.request_api', + return_value={'public_id': 'public-id'} +) +class PublishAgentTestCase(TestCase): + """Test case for publish_agent method.""" + + @mock.patch('aea.cli.registry.publish.os.path.exists', return_value=True) + def test_push_item_positive( + self, + path_exists_mock, + request_api_mock, + load_yaml_mock, + compress_mock, + getcwd_mock, + rm_tarfiles_mock + ): + """Test for publish_agent positive result.""" + publish_agent() + request_api_mock.assert_called_once_with( + 'POST', + '/agents/create', + data={ + 'name': 'agent-name', + 'description': 'some-description', + 'version': 'some-version', + + }, + auth=True, + filepath='cwd/agent-name.tar.gz' + ) + + @mock.patch('aea.cli.registry.push.os.path.exists', return_value=False) + def test_publish_agent_config_not_found( + self, + path_exists_mock, + request_api_mock, + load_yaml_mock, + compress_mock, + getcwd_mock, + rm_tarfiles_mock + ): + """Test for publish_agent - config not found.""" + with self.assertRaises(ClickException): + publish_agent() + + request_api_mock.assert_not_called() diff --git a/tests/test_cli/test_registry/test_push.py b/tests/test_cli/test_registry/test_push.py new file mode 100644 index 0000000000..dd7dbc0d32 --- /dev/null +++ b/tests/test_cli/test_registry/test_push.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""Test module for Registry push methods.""" + +import os + +from unittest import TestCase, mock + +from click import ClickException + +from aea.cli.registry.push import push_item, _remove_pycache + + +@mock.patch('aea.cli.registry.utils._rm_tarfiles') +@mock.patch('aea.cli.registry.push.os.getcwd', return_value='cwd') +@mock.patch('aea.cli.registry.push._compress_dir') +@mock.patch( + 'aea.cli.registry.push.load_yaml', + return_value={'description': 'some-description', 'version': 'some-version'} +) +@mock.patch( + 'aea.cli.registry.push.request_api', return_value={'public_id': 'public-id'} +) +class PushItemTestCase(TestCase): + """Test case for push_item method.""" + + @mock.patch('aea.cli.registry.push.os.path.exists', return_value=True) + def test_push_item_positive( + self, + path_exists_mock, + request_api_mock, + load_yaml_mock, + compress_mock, + getcwd_mock, + rm_tarfiles_mock + ): + """Test for push_item positive result.""" + push_item('some-type', 'item-name') + request_api_mock.assert_called_once_with( + 'POST', + '/some-types/create', + data={ + 'name': 'item-name', + 'description': 'some-description', + 'version': 'some-version', + + }, + auth=True, + filepath='cwd/item-name.tar.gz' + ) + + @mock.patch('aea.cli.registry.push.os.path.exists', return_value=False) + def test_push_item_item_not_found( + self, + path_exists_mock, + request_api_mock, + load_yaml_mock, + compress_mock, + getcwd_mock, + rm_tarfiles_mock + ): + """Test for push_item - item not found.""" + with self.assertRaises(ClickException): + push_item('some-type', 'item-name') + + request_api_mock.assert_not_called() + + +@mock.patch('aea.cli.registry.push.shutil.rmtree') +class RemovePycacheTestCase(TestCase): + """Test case for _remove_pycache method.""" + + @mock.patch('aea.cli.registry.push.os.path.exists', return_value=True) + def test_remove_pycache_positive(self, path_exists_mock, rmtree_mock): + """Test for _remove_pycache positive result.""" + source_dir = 'somedir' + pycache_path = os.path.join(source_dir, '__pycache__') + + _remove_pycache(source_dir) + rmtree_mock.assert_called_once_with(pycache_path) + + @mock.patch('aea.cli.registry.push.os.path.exists', return_value=False) + def test_remove_pycache_no_pycache(self, path_exists_mock, rmtree_mock): + """Test for _remove_pycache if there's no pycache.""" + source_dir = 'somedir' + _remove_pycache(source_dir) + rmtree_mock.assert_not_called() diff --git a/tests/test_cli/test_registry/test_utils.py b/tests/test_cli/test_registry/test_utils.py index 3b72db26b0..37fe7ebee9 100644 --- a/tests/test_cli/test_registry/test_utils.py +++ b/tests/test_cli/test_registry/test_utils.py @@ -21,9 +21,11 @@ from unittest import TestCase, mock from click import ClickException +from yaml import YAMLError from aea.cli.registry.utils import ( - fetch_package, request_api, split_public_id, _download_file, _extract + fetch_package, request_api, split_public_id, download_file, extract, + _init_config_folder, write_cli_config, read_cli_config ) from aea.cli.registry.settings import REGISTRY_API_URL @@ -37,17 +39,17 @@ return_value={'file': 'url'} ) @mock.patch( - 'aea.cli.registry.utils._download_file', + 'aea.cli.registry.utils.download_file', return_value='filepath' ) -@mock.patch('aea.cli.registry.utils._extract') +@mock.patch('aea.cli.registry.utils.extract') class FetchPackageTestCase(TestCase): """Test case for fetch_package method.""" def test_fetch_package_positive( self, - _extract_mock, - _download_file_mock, + extract_mock, + download_file_mock, request_api_mock, split_public_id_mock ): @@ -61,8 +63,8 @@ def test_fetch_package_positive( request_api_mock.assert_called_with( 'GET', '/connections/owner/name/version' ) - _download_file_mock.assert_called_once_with('url', 'cwd') - _extract_mock.assert_called_once_with('filepath', 'cwd/connections') + download_file_mock.assert_called_once_with('url', 'cwd') + extract_mock.assert_called_once_with('filepath', 'cwd/connections') @mock.patch('aea.cli.registry.utils.requests.request') @@ -82,6 +84,9 @@ def test_request_api_positive(self, request_mock): request_mock.assert_called_once_with( method='GET', params=None, + data=None, + files=None, + headers={}, url=REGISTRY_API_URL + '/path' ) self.assertEqual(result, expected_result) @@ -102,6 +107,15 @@ def test_request_api_403(self, request_mock): with self.assertRaises(ClickException): request_api('GET', '/path') + def test_request_api_409(self, request_mock): + """Test for fetch_package method conflict sever response.""" + resp_mock = mock.Mock() + resp_mock.status_code = 409 + resp_mock.json = lambda: {'detail': 'some'} + request_mock.return_value = resp_mock + with self.assertRaises(ClickException): + request_api('GET', '/path') + def test_request_api_unexpected_response(self, request_mock): """Test for fetch_package method unexpected sever response.""" resp_mock = mock.Mock() @@ -124,11 +138,11 @@ def test_split_public_id_positive(self): @mock.patch('aea.cli.registry.utils.requests.get') class DownloadFileTestCase(TestCase): - """Test case for _download_file method.""" + """Test case for download_file method.""" @mock.patch('builtins.open', mock.mock_open()) def test_download_file_positive(self, get_mock): - """Test for _download_file method positive result.""" + """Test for download_file method positive result.""" filename = 'filename.tar.gz' url = 'url/{}'.format(filename) cwd = 'cwd' @@ -142,28 +156,28 @@ def test_download_file_positive(self, get_mock): resp_mock.status_code = 200 get_mock.return_value = resp_mock - result = _download_file(url, cwd) + result = download_file(url, cwd) expected_result = filepath self.assertEqual(result, expected_result) get_mock.assert_called_once_with(url, stream=True) def test_download_file_wrong_response(self, get_mock): - """Test for _download_file method wrong response from file server.""" + """Test for download_file method wrong response from file server.""" resp_mock = mock.Mock() resp_mock.status_code = 404 get_mock.return_value = resp_mock with self.assertRaises(ClickException): - _download_file('url', 'cwd') + download_file('url', 'cwd') class ExtractTestCase(TestCase): - """Test case for _extract method.""" + """Test case for extract method.""" @mock.patch('aea.cli.registry.utils.os.remove') @mock.patch('aea.cli.registry.utils.tarfile.open') def test_extract_positive(self, tarfile_open_mock, os_remove_mock): - """Test for _extract method positive result.""" + """Test for extract method positive result.""" source = 'file.tar.gz' target = 'target-folder' @@ -172,13 +186,71 @@ def test_extract_positive(self, tarfile_open_mock, os_remove_mock): tar_mock.close = lambda: None tarfile_open_mock.return_value = tar_mock - _extract(source, target) + extract(source, target) tarfile_open_mock.assert_called_once_with(source, 'r:gz') os_remove_mock.assert_called_once_with(source) def test_extract_wrong_file_type(self): - """Test for _extract method wrong file type.""" + """Test for extract method wrong file type.""" source = 'file.wrong' target = 'target-folder' with self.assertRaises(Exception): - _extract(source, target) + extract(source, target) + + +@mock.patch('aea.cli.registry.utils.os.path.dirname', return_value='dir-name') +@mock.patch('aea.cli.registry.utils.os.path.exists', return_value=False) +@mock.patch('aea.cli.registry.utils.os.makedirs') +class InitConfigFolderTestCase(TestCase): + """Test case for _init_config_folder method.""" + + def test_init_config_folder_positive( + self, makedirs_mock, exists_mock, dirname_mock + ): + """Test for _init_config_folder method positive result.""" + _init_config_folder() + dirname_mock.assert_called_once() + exists_mock.assert_called_once_with('dir-name') + makedirs_mock.assert_called_once_with('dir-name') + + +@mock.patch('aea.cli.registry.utils._init_config_folder') +@mock.patch('aea.cli.registry.utils.yaml.dump') +@mock.patch('builtins.open', mock.mock_open()) +class WriteCLIConfigTestCase(TestCase): + """Test case for write_cli_config method.""" + + def test_write_cli_config_positive(self, dump_mock, icf_mock): + """Test for write_cli_config method positive result.""" + write_cli_config({'some': 'config'}) + icf_mock.assert_called_once() + dump_mock.assert_called_once() + + +def _raise_yamlerror(*args): + raise YAMLError() + + +@mock.patch('builtins.open', mock.mock_open()) +class ReadCLIConfigTestCase(TestCase): + """Test case for read_cli_config method.""" + + @mock.patch( + 'aea.cli.registry.utils.yaml.safe_load', + return_value={'correct': 'output'} + ) + def test_read_cli_config_positive(self, safe_load_mock): + """Test for read_cli_config method positive result.""" + result = read_cli_config() + expected_result = {'correct': 'output'} + self.assertEqual(result, expected_result) + safe_load_mock.assert_called_once() + + @mock.patch( + 'aea.cli.registry.utils.yaml.safe_load', + _raise_yamlerror + ) + def test_read_cli_config_bad_yaml(self): + """Test for read_cli_config method bad yaml behavior.""" + with self.assertRaises(ClickException): + read_cli_config() diff --git a/tests/test_cli/test_remove/test_connection.py b/tests/test_cli/test_remove/test_connection.py index 4fb86d8bca..21e63c84c8 100644 --- a/tests/test_cli/test_remove/test_connection.py +++ b/tests/test_cli/test_remove/test_connection.py @@ -32,7 +32,7 @@ import aea.configurations.base from aea.cli import cli from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE -from tests.conftest import CLI_LOG_OPTION +from ...conftest import CLI_LOG_OPTION, CUR_PATH class TestRemoveConnection: @@ -45,6 +45,8 @@ def setup_class(cls): cls.agent_name = "myagent" cls.cwd = os.getcwd() cls.t = tempfile.mkdtemp() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) cls.connection_name = "local" cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') cls.mocked_logger_error = cls.patch.__enter__() @@ -72,7 +74,7 @@ def test_connection_not_present_in_agent_config(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -115,7 +117,7 @@ def test_error_message_connection_not_existing(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -133,6 +135,8 @@ def setup_class(cls): cls.agent_name = "myagent" cls.cwd = os.getcwd() cls.t = tempfile.mkdtemp() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) cls.connection_name = "local" cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') cls.mocked_logger_error = cls.patch.__enter__() @@ -155,7 +159,7 @@ def test_exit_code_equal_to_1(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: diff --git a/tests/test_cli/test_remove/test_protocol.py b/tests/test_cli/test_remove/test_protocol.py index 0b9fefb4e9..e6ed40bd15 100644 --- a/tests/test_cli/test_remove/test_protocol.py +++ b/tests/test_cli/test_remove/test_protocol.py @@ -32,7 +32,7 @@ import aea.configurations.base from aea.cli import cli from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE -from tests.conftest import CLI_LOG_OPTION +from ...conftest import CLI_LOG_OPTION, CUR_PATH class TestRemoveProtocol: @@ -45,7 +45,9 @@ def setup_class(cls): cls.agent_name = "myagent" cls.cwd = os.getcwd() cls.t = tempfile.mkdtemp() - cls.protocol_name = "oef" + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) + cls.protocol_name = "gym" cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') cls.mocked_logger_error = cls.patch.__enter__() @@ -72,7 +74,7 @@ def test_protocol_not_present_in_agent_config(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -90,7 +92,7 @@ def setup_class(cls): cls.agent_name = "myagent" cls.cwd = os.getcwd() cls.t = tempfile.mkdtemp() - cls.protocol_name = "oef" + cls.protocol_name = "gym" cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') cls.mocked_logger_error = cls.patch.__enter__() @@ -115,7 +117,7 @@ def test_error_message_protocol_not_existing(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -133,7 +135,9 @@ def setup_class(cls): cls.agent_name = "myagent" cls.cwd = os.getcwd() cls.t = tempfile.mkdtemp() - cls.protocol_name = "oef" + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) + cls.protocol_name = "gym" cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') cls.mocked_logger_error = cls.patch.__enter__() @@ -155,7 +159,7 @@ def test_exit_code_equal_to_1(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: diff --git a/tests/test_cli/test_remove/test_skill.py b/tests/test_cli/test_remove/test_skill.py index 732dc1ac99..73da79219b 100644 --- a/tests/test_cli/test_remove/test_skill.py +++ b/tests/test_cli/test_remove/test_skill.py @@ -32,7 +32,7 @@ import aea.configurations.base from aea.cli import cli from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE, AgentConfig -from tests.conftest import ROOT_DIR, CLI_LOG_OPTION +from ...conftest import ROOT_DIR, CLI_LOG_OPTION class TestRemoveSkill: @@ -78,7 +78,7 @@ def test_skill_not_present_in_agent_config(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -121,7 +121,7 @@ def test_error_message_skill_not_existing(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -167,7 +167,7 @@ def test_exit_code_equal_to_1(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: diff --git a/tests/test_cli/test_run.py b/tests/test_cli/test_run.py index 3ea08d1246..8e95f38c16 100644 --- a/tests/test_cli/test_run.py +++ b/tests/test_cli/test_run.py @@ -36,7 +36,7 @@ from aea.cli import cli from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE, DEFAULT_CONNECTION_CONFIG_FILE -from tests.conftest import CLI_LOG_OPTION, CUR_PATH +from ..conftest import CLI_LOG_OPTION, CUR_PATH def test_run(pytestconfig): @@ -48,6 +48,9 @@ def test_run(pytestconfig): agent_name = "myagent" cwd = os.getcwd() t = tempfile.mkdtemp() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(t, "packages")) + os.chdir(t) result = runner.invoke(cli, [*CLI_LOG_OPTION, "create", agent_name]) assert result.exit_code == 0 @@ -101,6 +104,9 @@ def test_run_multiple_connections(pytestconfig, connection_names): agent_name = "myagent" cwd = os.getcwd() t = tempfile.mkdtemp() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(t, "packages")) + os.chdir(t) result = runner.invoke(cli, [*CLI_LOG_OPTION, "create", agent_name]) assert result.exit_code == 0 @@ -110,8 +116,9 @@ def test_run_multiple_connections(pytestconfig, connection_names): result = runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "local"]) assert result.exit_code == 0 + # stub is the default connection, so it should fail result = runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "stub"]) - assert result.exit_code == 0 + assert result.exit_code == 1 process = subprocess.Popen([ sys.executable, @@ -152,6 +159,9 @@ def test_run_unknown_private_key(pytestconfig): agent_name = "myagent" cwd = os.getcwd() t = tempfile.mkdtemp() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(t, "packages")) + os.chdir(t) result = runner.invoke(cli, [*CLI_LOG_OPTION, "create", agent_name]) assert result.exit_code == 0 @@ -209,6 +219,9 @@ def test_run_unknown_ledger(pytestconfig): agent_name = "myagent" cwd = os.getcwd() t = tempfile.mkdtemp() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(t, "packages")) + os.chdir(t) result = runner.invoke(cli, [*CLI_LOG_OPTION, "create", agent_name]) assert result.exit_code == 0 @@ -264,6 +277,9 @@ def test_run_default_private_key_config(pytestconfig): agent_name = "myagent" cwd = os.getcwd() t = tempfile.mkdtemp() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(t, "packages")) + os.chdir(t) result = runner.invoke(cli, [*CLI_LOG_OPTION, "create", agent_name]) assert result.exit_code == 0 @@ -317,6 +333,9 @@ def test_run_fet_private_key_config(pytestconfig): agent_name = "myagent" cwd = os.getcwd() t = tempfile.mkdtemp() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(t, "packages")) + os.chdir(t) result = runner.invoke(cli, [*CLI_LOG_OPTION, "create", agent_name]) assert result.exit_code == 0 @@ -370,6 +389,9 @@ def test_run_ethereum_private_key_config(pytestconfig): agent_name = "myagent" cwd = os.getcwd() t = tempfile.mkdtemp() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(t, "packages")) + os.chdir(t) result = runner.invoke(cli, [*CLI_LOG_OPTION, "create", agent_name]) assert result.exit_code == 0 @@ -423,6 +445,9 @@ def test_run_ledger_apis(pytestconfig): agent_name = "myagent" cwd = os.getcwd() t = tempfile.mkdtemp() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(t, "packages")) + os.chdir(t) result = runner.invoke(cli, [*CLI_LOG_OPTION, "create", agent_name]) assert result.exit_code == 0 @@ -497,6 +522,9 @@ def test_run_fet_ledger_apis(pytestconfig): agent_name = "myagent" cwd = os.getcwd() t = tempfile.mkdtemp() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(t, "packages")) + os.chdir(t) result = runner.invoke(cli, [*CLI_LOG_OPTION, "create", agent_name]) assert result.exit_code == 0 @@ -567,6 +595,9 @@ def test_run_with_install_deps(pytestconfig): agent_name = "myagent" cwd = os.getcwd() t = tempfile.mkdtemp() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(t, "packages")) + os.chdir(t) result = runner.invoke(cli, [*CLI_LOG_OPTION, "create", agent_name]) assert result.exit_code == 0 @@ -614,6 +645,9 @@ def test_run_with_install_deps_and_requirement_file(pytestconfig): agent_name = "myagent" cwd = os.getcwd() t = tempfile.mkdtemp() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(t, "packages")) + os.chdir(t) result = runner.invoke(cli, [*CLI_LOG_OPTION, "create", agent_name]) assert result.exit_code == 0 @@ -667,6 +701,9 @@ def setup_class(cls): cls.agent_name = "myagent" cls.cwd = os.getcwd() cls.t = tempfile.mkdtemp() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) + os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False) assert result.exit_code == 0 @@ -693,7 +730,7 @@ def test_exit_code_equal_to_1(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -713,6 +750,9 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() cls.cwd = os.getcwd() cls.t = tempfile.mkdtemp() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) + os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False) assert result.exit_code == 0 @@ -736,7 +776,7 @@ def test_log_error_message(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: @@ -757,6 +797,9 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() cls.cwd = os.getcwd() cls.t = tempfile.mkdtemp() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) + os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False) assert result.exit_code == 0 @@ -781,7 +824,7 @@ def test_log_error_message(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: @@ -803,6 +846,9 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() cls.cwd = os.getcwd() cls.t = tempfile.mkdtemp() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) + os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False) assert result.exit_code == 0 @@ -825,7 +871,7 @@ def test_log_error_message(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: @@ -847,6 +893,9 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() cls.cwd = os.getcwd() cls.t = tempfile.mkdtemp() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) + os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False) assert result.exit_code == 0 @@ -871,7 +920,7 @@ def test_log_error_message(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: @@ -888,11 +937,14 @@ def setup_class(cls): """Set the test up.""" cls.runner = CliRunner() cls.agent_name = "myagent" - cls.connection_name = "stub" + cls.connection_name = "local" cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') cls.mocked_logger_error = cls.patch.__enter__() cls.cwd = os.getcwd() cls.t = tempfile.mkdtemp() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) + os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False) assert result.exit_code == 0 @@ -917,7 +969,7 @@ def test_log_error_message(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: @@ -934,11 +986,14 @@ def setup_class(cls): """Set the test up.""" cls.runner = CliRunner() cls.agent_name = "myagent" - cls.connection_name = "stub" + cls.connection_name = "local" cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') cls.mocked_logger_error = cls.patch.__enter__() cls.cwd = os.getcwd() cls.t = tempfile.mkdtemp() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) + os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False) assert result.exit_code == 0 @@ -958,12 +1013,12 @@ def test_exit_code_equal_to_1(self): def test_log_error_message(self): """Test that the log error message is fixed.""" - s = "Connection class '{}' not found.".format("StubConnection") + s = "Connection class '{}' not found.".format("OEFLocalConnection") self.mocked_logger_error.assert_called_once_with(s) @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: @@ -985,6 +1040,9 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() cls.cwd = os.getcwd() cls.t = tempfile.mkdtemp() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) + os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False) assert result.exit_code == 0 @@ -1008,7 +1066,7 @@ def test_log_error_message(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: @@ -1030,6 +1088,9 @@ def setup_class(cls): cls.mocked_logger_error = cls.patch.__enter__() cls.cwd = os.getcwd() cls.t = tempfile.mkdtemp() + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(cls.t, "packages")) + os.chdir(cls.t) result = cls.runner.invoke(cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False) assert result.exit_code == 0 @@ -1053,7 +1114,7 @@ def test_log_error_message(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: diff --git a/tests/test_cli/test_scaffold/test_connection.py b/tests/test_cli/test_scaffold/test_connection.py index b0544e508f..f2d484adc7 100644 --- a/tests/test_cli/test_scaffold/test_connection.py +++ b/tests/test_cli/test_scaffold/test_connection.py @@ -36,7 +36,7 @@ import aea.configurations.base from aea.configurations.base import DEFAULT_CONNECTION_CONFIG_FILE from aea.cli import cli -from tests.conftest import CLI_LOG_OPTION, CONNECTION_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR +from ...conftest import CLI_LOG_OPTION, CONNECTION_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR class TestScaffoldConnection: @@ -82,7 +82,7 @@ def test_resource_folder_contains_configuration_file(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -133,7 +133,7 @@ def test_resource_directory_exists(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -186,7 +186,7 @@ def test_resource_directory_exists(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -239,7 +239,7 @@ def test_resource_directory_does_not_exists(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: @@ -283,7 +283,7 @@ def test_resource_directory_does_not_exists(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: diff --git a/tests/test_cli/test_scaffold/test_protocols.py b/tests/test_cli/test_scaffold/test_protocols.py index 2ce5639c6f..a800ca8ac3 100644 --- a/tests/test_cli/test_scaffold/test_protocols.py +++ b/tests/test_cli/test_scaffold/test_protocols.py @@ -36,7 +36,7 @@ import aea.configurations.base from aea.configurations.base import DEFAULT_PROTOCOL_CONFIG_FILE from aea.cli import cli -from tests.conftest import CLI_LOG_OPTION, PROTOCOL_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR +from ...conftest import CLI_LOG_OPTION, PROTOCOL_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR class TestScaffoldProtocol: @@ -88,7 +88,7 @@ def test_resource_folder_contains_configuration_file(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -139,7 +139,7 @@ def test_resource_directory_exists(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -192,7 +192,7 @@ def test_resource_directory_exists(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -245,7 +245,7 @@ def test_resource_directory_does_not_exists(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: @@ -289,7 +289,7 @@ def test_resource_directory_does_not_exists(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: diff --git a/tests/test_cli/test_scaffold/test_skills.py b/tests/test_cli/test_scaffold/test_skills.py index 18c32d5c42..3111cdb74c 100644 --- a/tests/test_cli/test_scaffold/test_skills.py +++ b/tests/test_cli/test_scaffold/test_skills.py @@ -36,7 +36,7 @@ import aea.configurations.base from aea.configurations.base import DEFAULT_SKILL_CONFIG_FILE from aea.cli import cli -from tests.conftest import CLI_LOG_OPTION, SKILL_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR +from ...conftest import CLI_LOG_OPTION, SKILL_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR class TestScaffoldSkill: @@ -100,7 +100,7 @@ def test_resource_folder_contains_configuration_file(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -151,7 +151,7 @@ def test_resource_directory_exists(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -204,7 +204,7 @@ def test_resource_directory_exists(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) try: shutil.rmtree(cls.t) @@ -257,7 +257,7 @@ def test_resource_directory_does_not_exists(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: @@ -301,7 +301,7 @@ def test_resource_directory_does_not_exists(self): @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" cls.patch.__exit__() os.chdir(cls.cwd) try: diff --git a/tests/test_cli/test_search.py b/tests/test_cli/test_search.py index 7b4fd4d671..73c99e9fcb 100644 --- a/tests/test_cli/test_search.py +++ b/tests/test_cli/test_search.py @@ -20,7 +20,7 @@ """This test module contains the tests for the `aea search` sub-command.""" import json import os -from unittest import mock +from unittest import mock, TestCase from pathlib import Path import jsonschema @@ -29,7 +29,8 @@ from aea import AEA_DIR from aea.cli import cli -from tests.conftest import AGENT_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR, CLI_LOG_OPTION +from ..conftest import AGENT_CONFIGURATION_SCHEMA, CONFIGURATION_SCHEMA_DIR, CLI_LOG_OPTION +from tests.test_cli.constants import FORMAT_ITEMS_SAMPLE_OUTPUT class TestSearchProtocols: @@ -45,80 +46,16 @@ def setup_class(cls): cls.cwd = os.getcwd() cls.runner = CliRunner() - def _generated_expected_output(self): - return """Available protocols: ------------------------------- -Name: default -Description: The default protocol allows for any bytes message. -Version: 0.1.0 ------------------------------- ------------------------------- -Name: fipa -Description: The fipa protocol implements the FIPA ACL. -Version: 0.1.0 ------------------------------- ------------------------------- -Name: gym -Description: The gym protocol implements the messages an agent needs to engage with a gym connection. -Version: 0.1.0 ------------------------------- ------------------------------- -Name: oef -Description: The oef protocol implements the OEF specific messages. -Version: 0.1.0 ------------------------------- ------------------------------- -Name: tac -Description: The tac protocol implements the messages an AEA needs to participate in the TAC. -Version: 0.1.0 ------------------------------- - -""" - - def test_correct_output_default_registry(self): + @mock.patch('aea.cli.search.format_items', return_value=FORMAT_ITEMS_SAMPLE_OUTPUT) + def test_correct_output_default_registry(self, _): """Test that the command has printed the correct output when using the default registry.""" os.chdir(AEA_DIR) self.result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "search", "protocols"], standalone_mode=False) - - assert self.result.output == self._generated_expected_output() - - def test_correct_output_registry_api(self): - """Test that the command has printed the correct output when using Registry API.""" - resp = [ - { - "name": "protocol-1", - "description": "Protocol 1", - "version": "1", - } - ] - with mock.patch('aea.cli.search.request_api', return_value=resp): - self.result = self.runner.invoke( - cli, [*CLI_LOG_OPTION, "search", "--registry", "protocols", "--query=some"], standalone_mode=False - ) - expected_output = ( - 'Searching for "some"...\n' - 'Protocols found:\n\n' - '------------------------------\n' - 'Name: protocol-1\n' - 'Description: Protocol 1\n' - 'Version: 1\n' - '------------------------------\n\n' - ) - assert self.result.output == expected_output - - with mock.patch('aea.cli.search.request_api', return_value=[]): - self.result = self.runner.invoke( - cli, [*CLI_LOG_OPTION, "search", "--registry", "protocols", "--query=some"], standalone_mode=False - ) - expected_output = ( - 'Searching for "some"...\n' - 'No protocols found.\n' - ) - assert self.result.output == expected_output + assert self.result.output == "Available protocols:\n{}\n".format(FORMAT_ITEMS_SAMPLE_OUTPUT) @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) @@ -135,85 +72,21 @@ def setup_class(cls): cls.cwd = os.getcwd() cls.runner = CliRunner() - def _generated_expected_output(self): - return """Available connections: ------------------------------- -Name: gym -Description: The gym connection wraps an OpenAI gym. -Version: 0.1.0 ------------------------------- ------------------------------- -Name: local -Description: The local connection provides a stub for an OEF node. -Version: 0.1.0 ------------------------------- ------------------------------- -Name: oef -Description: The oef connection provides a wrapper around the OEF sdk. -Version: 0.1.0 ------------------------------- ------------------------------- -Name: p2p -Description: The p2p connection provides a connection with the fetch.ai mail provider. -Version: 0.1.0 ------------------------------- ------------------------------- -Name: stub -Description: The stub connection implements a connection stub which reads/writes messages from/to file. -Version: 0.1.0 ------------------------------- ------------------------------- -Name: tcp -Description: None -Version: 0.1.0 ------------------------------- - -""" - - def test_correct_output_default_registry(self): + @mock.patch( + 'aea.cli.search.format_items', + return_value=FORMAT_ITEMS_SAMPLE_OUTPUT + ) + def test_correct_output_default_registry(self, _): """Test that the command has printed the correct output when using the default registry.""" os.chdir(AEA_DIR) self.result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "search", "connections"], standalone_mode=False) - assert self.result.output == self._generated_expected_output() - - def test_correct_output_registry_api(self): - """Test that the command has printed the correct output when using Registry API.""" - resp = [ - { - "name": "connection-1", - "description": "Connection 1", - "version": "1", - } - ] - with mock.patch('aea.cli.search.request_api', return_value=resp): - self.result = self.runner.invoke( - cli, [*CLI_LOG_OPTION, "search", "--registry", "connections", "--query=some"], standalone_mode=False - ) - - expected_output = ( - 'Searching for "some"...\n' - 'Connections found:\n\n' - '------------------------------\n' - 'Name: connection-1\n' - 'Description: Connection 1\n' - 'Version: 1\n' - '------------------------------\n\n' - ) - assert self.result.output == expected_output - - with mock.patch('aea.cli.search.request_api', return_value=[]): - self.result = self.runner.invoke( - cli, [*CLI_LOG_OPTION, "search", "--registry", "connections", "--query=some"], standalone_mode=False - ) - expected_output = ( - 'Searching for "some"...\n' - 'No connections found.\n' + assert self.result.output == "Available connections:\n{}\n".format( + FORMAT_ITEMS_SAMPLE_OUTPUT ) - assert self.result.output == expected_output @classmethod def teardown_class(cls): - """Teardowm the test.""" + """Tear the test down.""" os.chdir(cls.cwd) @@ -230,115 +103,116 @@ def setup_class(cls): cls.cwd = os.getcwd() cls.runner = CliRunner() - def _generated_expected_output(self): - return """Available skills: ------------------------------- -Name: carpark_client -Description: None -Version: 0.1.0 ------------------------------- ------------------------------- -Name: carpark_detection -Description: None -Version: 0.1.0 ------------------------------- ------------------------------- -Name: echo -Description: The echo skill implements simple echo functionality. -Version: 0.1.0 ------------------------------- ------------------------------- -Name: error -Description: The error skill implements basic error handling required by all AEAs. -Version: 0.1.0 ------------------------------- ------------------------------- -Name: gym -Description: The gym skill wraps an RL agent. -Version: 0.1.0 ------------------------------- ------------------------------- -Name: tac_control -Description: The tac control skill implements the logic for an AEA to control an instance of the TAC. -Version: 0.1.0 ------------------------------- ------------------------------- -Name: tac_negotiation -Description: The tac negotiation skill implements the logic for an AEA to do fipa negotiation in the TAC. -Version: 0.1.0 ------------------------------- ------------------------------- -Name: tac_participation -Description: The tac participation skill implements the logic for an AEA to participate in the TAC. -Version: 0.1.0 ------------------------------- ------------------------------- -Name: weather_client -Description: None -Version: 0.1.0 ------------------------------- ------------------------------- -Name: weather_client_ledger -Description: None -Version: 0.1.0 ------------------------------- ------------------------------- -Name: weather_station -Description: None -Version: 0.1.0 ------------------------------- ------------------------------- -Name: weather_station_ledger -Description: None -Version: 0.1.0 ------------------------------- - -""" - - def test_correct_output_default_registry(self): + @mock.patch( + 'aea.cli.search.format_items', + return_value=FORMAT_ITEMS_SAMPLE_OUTPUT + ) + def test_correct_output_default_registry(self, _): """Test that the command has printed the correct output when using the default registry.""" os.chdir(AEA_DIR) self.result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "search", "skills"], standalone_mode=False) - assert self.result.output == self._generated_expected_output() + assert self.result.output == "Available skills:\n{}\n".format(FORMAT_ITEMS_SAMPLE_OUTPUT) - def test_correct_output_registry_api(self): - """Test that the command has printed the correct output when using Registry API.""" - resp = [ - { - "name": "skill-1", - "description": "Skill 1", - "version": "1", - "protocol_names": ['p1', 'p2'], - } - ] - with mock.patch('aea.cli.search.request_api', return_value=resp): - self.result = self.runner.invoke( - cli, [*CLI_LOG_OPTION, "search", "--registry", "skills", "--query=some"], standalone_mode=False - ) - expected_output = ( - 'Searching for "some"...\n' - 'Skills found:\n\n' - '------------------------------\n' - 'Name: skill-1\n' - 'Description: Skill 1\n' - 'Protocols: p1 | p2 | \n' - 'Version: 1\n' - '------------------------------\n\n' - ) + @classmethod + def teardown_class(cls): + """Tear the test down.""" + os.chdir(cls.cwd) - assert self.result.output == expected_output - with mock.patch('aea.cli.search.request_api', return_value=[]): - self.result = self.runner.invoke( - cli, [*CLI_LOG_OPTION, "search", "--registry", "skills", "--query=some"], standalone_mode=False - ) +@mock.patch( + 'aea.cli.search.request_api', + return_value=['correct', 'results'] +) +@mock.patch( + 'aea.cli.search.format_items', + return_value=FORMAT_ITEMS_SAMPLE_OUTPUT +) +class RegistrySearchTestCase(TestCase): + """Test case for search --registry CLI command.""" + + def setUp(self): + """Set it up.""" + self.runner = CliRunner() + + def test_search_connections_positive( + self, format_items_mock, request_api_mock + ): + """Test for CLI search --registry connections positive result.""" + result = self.runner.invoke( + cli, + [ + *CLI_LOG_OPTION, + "search", + "--registry", + "connections", + "--query=some" + ], + standalone_mode=False + ) expected_output = ( 'Searching for "some"...\n' - 'No skills found.\n' + 'Connections found:\n\n' + '{}\n'.format(FORMAT_ITEMS_SAMPLE_OUTPUT) ) - assert self.result.output == expected_output - - @classmethod - def teardown_class(cls): - """Teardowm the test.""" - os.chdir(cls.cwd) + self.assertEqual(result.output, expected_output) + request_api_mock.assert_called_once_with( + 'GET', '/connections', params={'search': 'some'} + ) + format_items_mock.assert_called_once_with(['correct', 'results']) + + def test_search_protocols_positive( + self, format_items_mock, request_api_mock + ): + """Test for CLI search --registry protocols positive result.""" + result = self.runner.invoke( + cli, + [ + *CLI_LOG_OPTION, + "search", + "--registry", + "protocols", + "--query=some" + ], + standalone_mode=False + ) + expected_output = ( + 'Searching for "some"...\n' + 'Protocols found:\n\n' + '{}\n'.format(FORMAT_ITEMS_SAMPLE_OUTPUT) + ) + self.assertEqual(result.output, expected_output) + request_api_mock.assert_called_once_with( + 'GET', '/protocols', params={'search': 'some'} + ) + format_items_mock.assert_called_once_with(['correct', 'results']) + + @mock.patch( + 'aea.cli.search.format_skills', + return_value=FORMAT_ITEMS_SAMPLE_OUTPUT + ) + def test_search_skills_positive( + self, format_skills_mock, format_items_mock, request_api_mock + ): + """Test for CLI search --registry skills positive result.""" + result = self.runner.invoke( + cli, + [ + *CLI_LOG_OPTION, + "search", + "--registry", + "skills", + "--query=some" + ], + standalone_mode=False + ) + expected_output = ( + 'Searching for "some"...\n' + 'Skills found:\n\n' + '{}\n'.format(FORMAT_ITEMS_SAMPLE_OUTPUT) + ) + self.assertEqual(result.output, expected_output) + request_api_mock.assert_called_once_with( + 'GET', '/skills', params={'search': 'some'} + ) + format_skills_mock.assert_called_once_with(['correct', 'results']) + format_items_mock.assert_not_called() diff --git a/tests/test_configurations/test_schema.py b/tests/test_configurations/test_schema.py index 005d6c68dc..a74b3d4dbe 100644 --- a/tests/test_configurations/test_schema.py +++ b/tests/test_configurations/test_schema.py @@ -93,10 +93,11 @@ def setup_class(cls): @pytest.mark.parametrize("protocol_path", [ os.path.join(ROOT_DIR, "aea", "protocols", "default"), - os.path.join(ROOT_DIR, "aea", "protocols", "fipa"), - os.path.join(ROOT_DIR, "aea", "protocols", "oef"), + os.path.join(ROOT_DIR, "packages", "protocols", "fipa"), + os.path.join(ROOT_DIR, "packages", "protocols", "oef"), os.path.join(ROOT_DIR, "aea", "protocols", "scaffold"), os.path.join(ROOT_DIR, "packages", "protocols", "gym"), + os.path.join(ROOT_DIR, "packages", "protocols", "ml_trade"), os.path.join(ROOT_DIR, "packages", "protocols", "tac"), ]) def test_validate_protocol_config(self, protocol_path): @@ -117,8 +118,8 @@ def setup_class(cls): @pytest.mark.parametrize("connection_path", [ - os.path.join(ROOT_DIR, "aea", "connections", "local"), - os.path.join(ROOT_DIR, "aea", "connections", "oef"), + os.path.join(ROOT_DIR, "packages", "connections", "local"), + os.path.join(ROOT_DIR, "packages", "connections", "oef"), os.path.join(ROOT_DIR, "aea", "connections", "scaffold"), os.path.join(ROOT_DIR, "packages", "connections", "gym"), os.path.join(CUR_PATH, "data", "dummy_connection") @@ -143,9 +144,22 @@ def setup_class(cls): [ os.path.join(ROOT_DIR, "aea", "skills", "error"), os.path.join(ROOT_DIR, "aea", "skills", "scaffold"), + os.path.join(ROOT_DIR, "packages", "skills", "carpark_client"), + os.path.join(ROOT_DIR, "packages", "skills", "carpark_detection"), os.path.join(ROOT_DIR, "packages", "skills", "echo"), os.path.join(ROOT_DIR, "packages", "skills", "gym"), + os.path.join(ROOT_DIR, "packages", "skills", "ml_data_provider"), + os.path.join(ROOT_DIR, "packages", "skills", "ml_train"), + os.path.join(ROOT_DIR, "packages", "skills", "tac_control"), + os.path.join(ROOT_DIR, "packages", "skills", "tac_negotiation"), + os.path.join(ROOT_DIR, "packages", "skills", "tac_participation"), + os.path.join(ROOT_DIR, "packages", "skills", "weather_client"), + os.path.join(ROOT_DIR, "packages", "skills", "weather_client_ledger"), + os.path.join(ROOT_DIR, "packages", "skills", "weather_station"), + os.path.join(ROOT_DIR, "packages", "skills", "weather_station_ledger"), os.path.join(CUR_PATH, "data", "dummy_skill"), + os.path.join(CUR_PATH, "data", "dummy_aea", "skills", "dummy"), + os.path.join(CUR_PATH, "data", "dummy_aea", "skills", "error"), os.path.join(CUR_PATH, "data", "dependencies_skill"), os.path.join(CUR_PATH, "data", "exception_skill") ]) diff --git a/tests/test_connections/test_scaffold_connection.py b/tests/test_connections/test_scaffold_connection.py index 6c1ac2763b..6a3cc2db91 100644 --- a/tests/test_connections/test_scaffold_connection.py +++ b/tests/test_connections/test_scaffold_connection.py @@ -27,5 +27,5 @@ class TestScaffold: def test_scaffold_connection(self): """Test the initialisation of the scaffold_connection.""" - m_connection = MyScaffoldConnection("my_scaffold_connection", public_key="pk") - assert m_connection.public_key == "pk" + m_connection = MyScaffoldConnection("my_scaffold_connection", address="pk") + assert m_connection.address == "pk" diff --git a/tests/test_connections/test_stub.py b/tests/test_connections/test_stub.py index 064c6dcb30..034906c4f8 100644 --- a/tests/test_connections/test_stub.py +++ b/tests/test_connections/test_stub.py @@ -87,7 +87,7 @@ def test_connection_is_established(self): def test_connection_from_config(self): """Test loading a connection from config file.""" - stub_con = StubConnection.from_config(public_key="pk", connection_configuration=ConnectionConfig()) + stub_con = StubConnection.from_config(address="pk", connection_configuration=ConnectionConfig()) assert not stub_con.connection_status.is_connected def test_send_message(self): @@ -125,7 +125,7 @@ def test_connection_from_config(): d.mkdir(parents=True) input_file_path = d / "input_file.csv" output_file_path = d / "input_file.csv" - stub_con = StubConnection.from_config(public_key="pk", connection_configuration=ConnectionConfig( + stub_con = StubConnection.from_config(address="pk", connection_configuration=ConnectionConfig( input_file=input_file_path, output_file=output_file_path )) diff --git a/tests/test_crypto/test_ethereum_crypto.py b/tests/test_crypto/test_ethereum_crypto.py index ca91484e60..b6610c8b7c 100644 --- a/tests/test_crypto/test_ethereum_crypto.py +++ b/tests/test_crypto/test_ethereum_crypto.py @@ -21,6 +21,7 @@ import os from aea.crypto.ethereum import EthereumCrypto +from web3 import Web3 from ..conftest import ROOT_DIR PRIVATE_KEY_PATH = os.path.join(ROOT_DIR, "/tests/data/eth_private_key.txt") @@ -42,8 +43,9 @@ def test_initialization(): assert account.entity is not None, "After creation the entity must no be None" -def test_sign_message(): +def test_sign_transaction(): """Test the signing function for the eth_crypto.""" account = EthereumCrypto(PRIVATE_KEY_PATH) - sign_bytes = account.sign_message('Hello') + tx = Web3.solidityKeccak(['bytes'], [b'hello']) + sign_bytes = account.sign_transaction(tx) assert len(sign_bytes) > 0, "The len(signature) must not be 0" diff --git a/tests/test_crypto/test_fetchai_crypto.py b/tests/test_crypto/test_fetchai_crypto.py index ad396c1f9e..addc503106 100644 --- a/tests/test_crypto/test_fetchai_crypto.py +++ b/tests/test_crypto/test_fetchai_crypto.py @@ -42,10 +42,10 @@ def test_get_address(): assert fet_crypto.get_address_from_public_key(fet_crypto.public_key) is not None, "Get address must work" -def test_sign_message(): +def test_sign_transaction(): """Test the signing process.""" fet_crypto = FetchAICrypto() - signature = fet_crypto.sign_transaction(message=b'HelloWorld') + signature = fet_crypto.sign_transaction(tx_hash=b'HelloWorld') assert len(signature) > 1, "The len(signature) must be more than 0" diff --git a/tests/test_crypto/test_helpers.py b/tests/test_crypto/test_helpers.py index ce6da23a50..3f7229231c 100644 --- a/tests/test_crypto/test_helpers.py +++ b/tests/test_crypto/test_helpers.py @@ -26,7 +26,7 @@ from aea.crypto.helpers import _try_validate_private_key_pem_path, _try_validate_fet_private_key_path, \ _try_validate_ethereum_private_key_path -from tests.conftest import CUR_PATH +from ..conftest import CUR_PATH logger = logging.getLogger(__name__) diff --git a/tests/test_crypto/test_ledger_apis.py b/tests/test_crypto/test_ledger_apis.py index 81b0b7de1c..798e01ae34 100644 --- a/tests/test_crypto/test_ledger_apis.py +++ b/tests/test_crypto/test_ledger_apis.py @@ -32,7 +32,7 @@ from aea.crypto.ledger_apis import LedgerApis, DEFAULT_FETCHAI_CONFIG, \ _try_to_instantiate_fetchai_ledger_api, \ _try_to_instantiate_ethereum_ledger_api -from tests.conftest import CUR_PATH +from ..conftest import CUR_PATH logger = logging.getLogger(__name__) @@ -108,7 +108,7 @@ def test_transfer_fetchai(self): with mock.patch.object(ledger_apis.apis.get(FETCHAI).tokens, 'transfer', return_value="97fcacaaf94b62318c4e4bbf53fd2608c15062f17a6d1bffee0ba7af9b710e35"): with mock.patch.object(ledger_apis.apis.get(FETCHAI), 'sync'): - tx_digest = ledger_apis.transfer(FETCHAI, fet_obj, fet_address, amount=10, tx_fee=10) + tx_digest = ledger_apis.transfer(fet_obj, fet_address, amount=10, tx_fee=10) assert tx_digest is not None assert ledger_apis.last_tx_statuses[FETCHAI] == 'OK' @@ -122,7 +122,7 @@ def test_failed_transfer_fetchai(self): with mock.patch.object(ledger_apis.apis.get(FETCHAI).tokens, 'transfer', return_value="97fcacaaf94b62318c4e4bbf53fd2608c15062f17a6d1bffee0ba7af9b710e35"): with mock.patch.object(ledger_apis.apis.get(FETCHAI), 'sync', side_effect=Exception): - tx_digest = ledger_apis.transfer(FETCHAI, fet_obj, fet_address, amount=10, tx_fee=10) + tx_digest = ledger_apis.transfer(fet_obj, fet_address, amount=10, tx_fee=10) assert tx_digest is None assert ledger_apis.last_tx_statuses[FETCHAI] == 'ERROR' @@ -140,7 +140,7 @@ def test_transfer_ethereum(self): return_value=result): with mock.patch.object(ledger_apis.apis.get(ETHEREUM).eth, "getTransactionReceipt", return_value=b'0xa13f2f926233bc4638a20deeb8aaa7e8d6a96e487392fa55823f925220f6efed'): - tx_digest = ledger_apis.transfer(ETHEREUM, eth_obj, eth_address, amount=10, tx_fee=200000) + tx_digest = ledger_apis.transfer(eth_obj, eth_address, amount=10, tx_fee=200000) assert tx_digest is not None assert ledger_apis.last_tx_statuses[ETHEREUM] == 'OK' @@ -151,7 +151,7 @@ def test_failed_transfer_ethereum(self): ledger_apis = LedgerApis({ETHEREUM: DEFAULT_ETHEREUM_CONFIG, FETCHAI: DEFAULT_FETCHAI_CONFIG}) with mock.patch.object(ledger_apis.apis.get(ETHEREUM).eth, 'getTransactionCount', return_value=5, side_effect=Exception): - tx_digest = ledger_apis.transfer(ETHEREUM, eth_obj, eth_address, amount=10, tx_fee=200000) + tx_digest = ledger_apis.transfer(eth_obj, eth_address, amount=10, tx_fee=200000) assert tx_digest is None assert ledger_apis.last_tx_statuses[ETHEREUM] == 'ERROR' diff --git a/tests/test_decision_maker/test_base.py b/tests/test_decision_maker/test_base.py index 06fe8844ab..4dc10c2393 100644 --- a/tests/test_decision_maker/test_base.py +++ b/tests/test_decision_maker/test_base.py @@ -30,11 +30,12 @@ from aea.crypto.ledger_apis import LedgerApis, DEFAULT_FETCHAI_CONFIG from aea.crypto.wallet import Wallet, FETCHAI from aea.decision_maker.base import OwnershipState, Preferences, DecisionMaker +from aea.decision_maker.messages.base import InternalMessage from aea.decision_maker.messages.state_update import StateUpdateMessage from aea.decision_maker.messages.transaction import TransactionMessage from aea.mail.base import OutBox, Multiplexer # , Envelope from aea.protocols.default.message import DefaultMessage -from tests.conftest import CUR_PATH, DummyConnection +from ..conftest import CUR_PATH, DummyConnection MAX_REACTIONS = 10 @@ -52,185 +53,202 @@ def setup_class(cls): def test_properties(self): """Test the assertion error for *_holdings.""" with pytest.raises(AssertionError): - self.ownership_state.amount_by_currency + self.ownership_state.amount_by_currency_id with pytest.raises(AssertionError): - self.ownership_state.quantities_by_good_pbk + self.ownership_state.quantities_by_good_id def test_initialisation(self): """Test the initialisation of the ownership_state.""" currency_endowment = {"FET": 100} - good_endowment = {"good_pbk": 2} - self.ownership_state.init(amount_by_currency=currency_endowment, quantities_by_good_pbk=good_endowment) - assert self.ownership_state.amount_by_currency is not None - assert self.ownership_state.quantities_by_good_pbk is not None + good_endowment = {"good_id": 2} + self.ownership_state.init(amount_by_currency_id=currency_endowment, quantities_by_good_id=good_endowment) + assert self.ownership_state.amount_by_currency_id is not None + assert self.ownership_state.quantities_by_good_id is not None assert self.ownership_state.is_initialized + def test_body(self): + """Test the setter for the body.""" + msg = InternalMessage() + msg.body = {"test_key": "test_value"} + + other_msg = InternalMessage(body={"test_key": "test_value"}) + assert msg == other_msg, "Messages should be equal." + assert msg.check_consistency(), "It is true." + assert str(msg) == 'InternalMessage(test_key=test_value)' + assert msg._body is not None + msg.body = {"Test": "My_test"} + assert msg._body == {"Test": "My_test"}, "Message body must be equal with the above dictionary." + msg.set("Test", 2) + assert msg._body["Test"] == 2, "body['Test'] should be equal to 2." + msg.unset("Test") + assert "Test" not in msg._body.keys(), "Test should not exist." + def test_transaction_is_consistent(self): """Test the consistency of the transaction message.""" currency_endowment = {"FET": 100} - good_endowment = {"good_pbk": 2} - self.ownership_state.init(amount_by_currency=currency_endowment, quantities_by_good_pbk=good_endowment) - tx_message = TransactionMessage(performative=TransactionMessage.Performative.ACCEPT, - skill_id="default", - transaction_id="transaction0", - sender="agent_1", - counterparty="pk", - is_sender_buyer=True, - currency_pbk="FET", - amount=1, - sender_tx_fee=0, - counterparty_tx_fee=0, - quantities_by_good_pbk={"good_pbk": 10}, + good_endowment = {"good_id": 20} + self.ownership_state.init(amount_by_currency_id=currency_endowment, quantities_by_good_id=good_endowment) + tx_message = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE_FOR_SETTLEMENT, + skill_callback_ids=["default"], + tx_id="transaction0", + tx_sender_addr="agent_1", + tx_counterparty_addr="pk", + tx_amount_by_currency_id={"FET": -1}, + tx_sender_fee=0, + tx_counterparty_fee=0, + tx_quantities_by_good_id={"good_id": 10}, + info={'some_info_key': 'some_info_value'}, ledger_id="fetchai") assert self.ownership_state.check_transaction_is_consistent(tx_message=tx_message),\ "We should have the money for the transaction!" - tx_message = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE, - skill_id="default", - transaction_id="transaction0", - sender="agent_1", - counterparty="pk", - is_sender_buyer=False, - currency_pbk="FET", - amount=1, - sender_tx_fee=0, - counterparty_tx_fee=0, - quantities_by_good_pbk={"good_pbk": 10}, + tx_message = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE_FOR_SETTLEMENT, + skill_callback_ids=["default"], + tx_id="transaction0", + tx_sender_addr="agent_1", + tx_counterparty_addr="pk", + tx_amount_by_currency_id={"FET": 1}, + tx_sender_fee=0, + tx_counterparty_fee=0, + tx_quantities_by_good_id={"good_id": -10}, + info={'some_info_key': 'some_info_value'}, ledger_id="fetchai") assert self.ownership_state.check_transaction_is_consistent(tx_message=tx_message), \ - "We should have the money for the transaction!" + "We should have the goods for the transaction!" def test_apply(self): """Test the apply function.""" currency_endowment = {"FET": 100} - good_endowment = {"good_pbk": 2} - self.ownership_state.init(amount_by_currency=currency_endowment, quantities_by_good_pbk=good_endowment) - tx_message = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE, - skill_id="default", - transaction_id="transaction0", - sender="agent_1", - counterparty="pk", - is_sender_buyer=True, - currency_pbk="FET", - amount=20, - sender_tx_fee=5, - counterparty_tx_fee=0, - quantities_by_good_pbk={"good_pbk": 10}, + good_endowment = {"good_id": 2} + self.ownership_state.init(amount_by_currency_id=currency_endowment, quantities_by_good_id=good_endowment) + tx_message = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE_FOR_SETTLEMENT, + skill_callback_ids=["default"], + tx_id="transaction0", + tx_sender_addr="agent_1", + tx_counterparty_addr="pk", + tx_amount_by_currency_id={"FET": -20}, + tx_sender_fee=5, + tx_counterparty_fee=0, + tx_quantities_by_good_id={"good_id": 10}, + info={'some_info_key': 'some_info_value'}, ledger_id="fetchai") list_of_transactions = [tx_message] state = self.ownership_state - new_state = self.ownership_state.apply(transactions=list_of_transactions) + new_state = self.ownership_state.apply_transactions(transactions=list_of_transactions) assert state != new_state, "after applying a list_of_transactions must have a different state!" def test_transaction_update(self): """Test the tranasction update.""" currency_endowment = {"FET": 100} - good_endowment = {"good_pbk": 2} - - self.ownership_state.init(amount_by_currency=currency_endowment, quantities_by_good_pbk=good_endowment) - tx_message = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE, - skill_id="default", - transaction_id="transaction0", - sender="agent_1", - counterparty="pk", - is_sender_buyer=True, - currency_pbk="FET", - amount=20, - sender_tx_fee=5, - counterparty_tx_fee=0, - quantities_by_good_pbk={"good_pbk": 10}, + good_endowment = {"good_id": 20} + + self.ownership_state.init(amount_by_currency_id=currency_endowment, quantities_by_good_id=good_endowment) + assert self.ownership_state.amount_by_currency_id == currency_endowment + assert self.ownership_state.quantities_by_good_id == good_endowment + tx_message = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE_FOR_SETTLEMENT, + skill_callback_ids=["default"], + tx_id="transaction0", + tx_sender_addr="agent_1", + tx_counterparty_addr="pk", + tx_amount_by_currency_id={"FET": -20}, + tx_sender_fee=5, + tx_counterparty_fee=0, + tx_quantities_by_good_id={"good_id": 10}, + info={'some_info_key': 'some_info_value'}, ledger_id="fetchai") - cur_holdings = self.ownership_state.amount_by_currency['FET'] self.ownership_state.update(tx_message=tx_message) - assert self.ownership_state.amount_by_currency['FET'] < cur_holdings - - tx_message = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE, - skill_id="default", - transaction_id="transaction0", - sender="agent_1", - counterparty="pk", - is_sender_buyer=False, - currency_pbk="FET", - amount=20, - sender_tx_fee=5, - counterparty_tx_fee=0, - quantities_by_good_pbk={"good_pbk": 10}, + expected_amount_by_currency_id = {"FET": 75} + expected_quantities_by_good_id = {"good_id": 30} + assert self.ownership_state.amount_by_currency_id == expected_amount_by_currency_id + assert self.ownership_state.quantities_by_good_id == expected_quantities_by_good_id + + tx_message = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE_FOR_SETTLEMENT, + skill_callback_ids=["default"], + tx_id="transaction0", + tx_sender_addr="agent_1", + tx_counterparty_addr="pk", + tx_amount_by_currency_id={"FET": 20}, + tx_sender_fee=5, + tx_counterparty_fee=0, + tx_quantities_by_good_id={"good_id": -10}, + info={'some_info_key': 'some_info_value'}, ledger_id="fetchai") - cur_holdings = self.ownership_state.amount_by_currency['FET'] self.ownership_state.update(tx_message=tx_message) - assert self.ownership_state.amount_by_currency['FET'] > cur_holdings + expected_amount_by_currency_id = {"FET": 90} + expected_quantities_by_good_id = {"good_id": 20} + assert self.ownership_state.amount_by_currency_id == expected_amount_by_currency_id + assert self.ownership_state.quantities_by_good_id == expected_quantities_by_good_id # # PREFERENCES def test_preferences_properties(self): """Test the properties of the preferences class.""" with pytest.raises(AssertionError): - self.preferences.exchange_params_by_currency + self.preferences.exchange_params_by_currency_id with pytest.raises(AssertionError): - self.preferences.utility_params_by_good_pbk + self.preferences.utility_params_by_good_id def test_preferences_init(self): """Test the preferences init().""" - utility_params = {"good_pbk": 20.0} + utility_params = {"good_id": 20.0} exchange_params = {"FET": 10.0} tx_fee = 9 - self.preferences.init(exchange_params_by_currency=exchange_params, utility_params_by_good_pbk=utility_params, tx_fee=tx_fee) - assert self.preferences.utility_params_by_good_pbk is not None - assert self.preferences.exchange_params_by_currency is not None + self.preferences.init(exchange_params_by_currency_id=exchange_params, utility_params_by_good_id=utility_params, tx_fee=tx_fee) + assert self.preferences.utility_params_by_good_id is not None + assert self.preferences.exchange_params_by_currency_id is not None assert self.preferences.transaction_fees['seller_tx_fee'] == 4 assert self.preferences.transaction_fees['buyer_tx_fee'] == 5 assert self.preferences.is_initialized def test_utilities(self): """Test the utilities.""" - good_holdings = {"good_pbk": 2} + good_holdings = {"good_id": 2} currency_holdings = {"FET": 100} - utility_params = {"good_pbk": 20.0} + utility_params = {"good_id": 20.0} exchange_params = {"FET": 10.0} tx_fee = 9 - self.preferences.init(utility_params_by_good_pbk=utility_params, exchange_params_by_currency=exchange_params, tx_fee=tx_fee) - log_utility = self.preferences.logarithmic_utility(quantities_by_good_pbk=good_holdings) + self.preferences.init(utility_params_by_good_id=utility_params, exchange_params_by_currency_id=exchange_params, tx_fee=tx_fee) + log_utility = self.preferences.logarithmic_utility(quantities_by_good_id=good_holdings) assert log_utility is not None - linear_utility = self.preferences.linear_utility(amount_by_currency=currency_holdings) + linear_utility = self.preferences.linear_utility(amount_by_currency_id=currency_holdings) assert linear_utility is not None - score = self.preferences.get_score(quantities_by_good_pbk=good_holdings, amount_by_currency=currency_holdings) + score = self.preferences.get_score(quantities_by_good_id=good_holdings, amount_by_currency_id=currency_holdings) assert score == log_utility + linear_utility - delta_good_holdings = {"good_pbk": 1} + delta_good_holdings = {"good_id": 1} delta_currency_holdings = {"FET": -5} - self.ownership_state.init(amount_by_currency=currency_holdings, quantities_by_good_pbk=good_holdings) - marginal_utility = self.preferences.marginal_utility(ownership_state=self.ownership_state, delta_good_holdings=delta_good_holdings, delta_currency_holdings=delta_currency_holdings) + self.ownership_state.init(amount_by_currency_id=currency_holdings, quantities_by_good_id=good_holdings) + marginal_utility = self.preferences.marginal_utility(ownership_state=self.ownership_state, delta_quantities_by_good_id=delta_good_holdings, delta_amount_by_currency_id=delta_currency_holdings) assert marginal_utility is not None def test_score_diff_from_transaction(self): """Test the difference between the scores.""" - good_holdings = {"good_pbk": 2} + good_holdings = {"good_id": 2} currency_holdings = {"FET": 100} - utility_params = {"good_pbk": 20.0} + utility_params = {"good_id": 20.0} exchange_params = {"FET": 10.0} tx_fee = 3 - self.ownership_state.init(amount_by_currency=currency_holdings, quantities_by_good_pbk=good_holdings) - self.preferences.init(utility_params_by_good_pbk=utility_params, exchange_params_by_currency=exchange_params, tx_fee=tx_fee) - tx_message = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE, - skill_id="default", - transaction_id="transaction0", - sender="agent_1", - counterparty="pk", - is_sender_buyer=False, - currency_pbk="FET", - amount=20, - sender_tx_fee=self.preferences.transaction_fees['seller_tx_fee'], - counterparty_tx_fee=self.preferences.transaction_fees['buyer_tx_fee'], - quantities_by_good_pbk={"good_pbk": 10}, + self.ownership_state.init(amount_by_currency_id=currency_holdings, quantities_by_good_id=good_holdings) + self.preferences.init(utility_params_by_good_id=utility_params, exchange_params_by_currency_id=exchange_params, tx_fee=tx_fee) + tx_message = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE_FOR_SETTLEMENT, + skill_callback_ids=["default"], + tx_id="transaction0", + tx_sender_addr="agent_1", + tx_counterparty_addr="pk", + tx_amount_by_currency_id={"FET": -20}, + tx_sender_fee=self.preferences.transaction_fees['seller_tx_fee'], + tx_counterparty_fee=self.preferences.transaction_fees['buyer_tx_fee'], + tx_quantities_by_good_id={"good_id": 10}, + info={'some_info_key': 'some_info_value'}, ledger_id="fetchai") - cur_score = self.preferences.get_score(quantities_by_good_pbk=good_holdings, amount_by_currency=currency_holdings) - new_state = self.ownership_state.apply([tx_message]) - new_score = self.preferences.get_score(quantities_by_good_pbk=new_state.quantities_by_good_pbk, amount_by_currency=new_state.amount_by_currency) + cur_score = self.preferences.get_score(quantities_by_good_id=good_holdings, amount_by_currency_id=currency_holdings) + new_state = self.ownership_state.apply_transactions([tx_message]) + new_score = self.preferences.get_score(quantities_by_good_id=new_state.quantities_by_good_id, amount_by_currency_id=new_state.amount_by_currency_id) dif_scores = new_score - cur_score score_difference = self.preferences.get_score_diff_from_transaction(ownership_state=self.ownership_state, tx_message=tx_message) assert score_difference == dif_scores @@ -278,17 +296,16 @@ def test_properties(self): def test_decision_maker_execute(self): """Test the execute method.""" - tx_message = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE, - skill_id="default", - transaction_id="transaction0", - sender="agent_1", - counterparty="pk", - is_sender_buyer=True, - currency_pbk="FET", - amount=2, - sender_tx_fee=0, - counterparty_tx_fee=0, - quantities_by_good_pbk={"good_pbk": 10}, + tx_message = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE_FOR_SETTLEMENT, + skill_callback_ids=["default"], + tx_id="transaction0", + tx_sender_addr="agent_1", + tx_counterparty_addr="pk", + tx_amount_by_currency_id={"FET": -20}, + tx_sender_fee=0, + tx_counterparty_fee=0, + tx_quantities_by_good_id={"good_id": 10}, + info={'some_info_key': 'some_info_value'}, ledger_id="fetchai") self.decision_maker.message_in_queue.put_nowait(tx_message) @@ -298,47 +315,46 @@ def test_decision_maker_execute(self): def test_decision_maker_handle_state_update(self): """Test the execute method.""" - good_holdings = {"good_pbk": 2} + good_holdings = {"good_id": 2} currency_holdings = {"FET": 100} - utility_params = {"good_pbk": 20.0} + utility_params = {"good_id": 20.0} exchange_params = {"FET": 10.0} tx_fee = 1 state_update_message = StateUpdateMessage(performative=StateUpdateMessage.Performative.INITIALIZE, - amount_by_currency=currency_holdings, - quantities_by_good_pbk=good_holdings, - exchange_params_by_currency=exchange_params, - utility_params_by_good_pbk=utility_params, + amount_by_currency_id=currency_holdings, + quantities_by_good_id=good_holdings, + exchange_params_by_currency_id=exchange_params, + utility_params_by_good_id=utility_params, tx_fee=tx_fee) self.decision_maker.handle(state_update_message) - assert self.decision_maker.ownership_state.amount_by_currency is not None - assert self.decision_maker.ownership_state.quantities_by_good_pbk is not None - assert self.decision_maker.preferences.exchange_params_by_currency is not None - assert self.decision_maker.preferences.utility_params_by_good_pbk is not None + assert self.decision_maker.ownership_state.amount_by_currency_id is not None + assert self.decision_maker.ownership_state.quantities_by_good_id is not None + assert self.decision_maker.preferences.exchange_params_by_currency_id is not None + assert self.decision_maker.preferences.utility_params_by_good_id is not None currency_deltas = {"FET": -10} - good_deltas = {"good_pbk": 1} + good_deltas = {"good_id": 1} state_update_message = StateUpdateMessage(performative=StateUpdateMessage.Performative.APPLY, - amount_by_currency=currency_deltas, - quantities_by_good_pbk=good_deltas) + amount_by_currency_id=currency_deltas, + quantities_by_good_id=good_deltas) self.decision_maker.handle(state_update_message) - expected_amount_by_currency = {key: currency_holdings.get(key, 0) + currency_deltas.get(key, 0) for key in set(currency_holdings) | set(currency_deltas)} - expected_quantities_by_good_pbk = {key: good_holdings.get(key, 0) + good_deltas.get(key, 0) for key in set(good_holdings) | set(good_deltas)} - assert self.decision_maker.ownership_state.amount_by_currency == expected_amount_by_currency - assert self.decision_maker.ownership_state.quantities_by_good_pbk == expected_quantities_by_good_pbk + expected_amount_by_currency_id = {key: currency_holdings.get(key, 0) + currency_deltas.get(key, 0) for key in set(currency_holdings) | set(currency_deltas)} + expected_quantities_by_good_id = {key: good_holdings.get(key, 0) + good_deltas.get(key, 0) for key in set(good_holdings) | set(good_deltas)} + assert self.decision_maker.ownership_state.amount_by_currency_id == expected_amount_by_currency_id + assert self.decision_maker.ownership_state.quantities_by_good_id == expected_quantities_by_good_id def test_decision_maker_handle_tx_message(self): """Test the handle tx meessa method.""" - tx_message = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE, - skill_id="default", - transaction_id="transaction0", - sender="agent_1", - counterparty="pk", - is_sender_buyer=True, - currency_pbk="FET", - amount=2, - sender_tx_fee=0, - counterparty_tx_fee=0, - quantities_by_good_pbk={"good_pbk": 10}, + tx_message = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE_FOR_SETTLEMENT, + skill_callback_ids=["default"], + tx_id="transaction0", + tx_sender_addr="agent_1", + tx_counterparty_addr="pk", + tx_amount_by_currency_id={"FET": -2}, + tx_sender_fee=0, + tx_counterparty_fee=0, + tx_quantities_by_good_id={"good_id": 10}, + info={'some_info_key': 'some_info_value'}, ledger_id="fetchai") with mock.patch.object(self.decision_maker.ledger_apis, "token_balance", return_value=1000000): @@ -353,25 +369,25 @@ def test_decision_maker_handle_tx_message(self): self.decision_maker.handle(tx_message) assert not self.decision_maker.goal_pursuit_readiness.is_ready - tx_message = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE, - skill_id="default", - transaction_id="transaction0", - sender="agent_1", - counterparty="pk", - is_sender_buyer=True, - currency_pbk="FET", - amount=2, - sender_tx_fee=0, - counterparty_tx_fee=0, - quantities_by_good_pbk={"good_pbk": 10}) + tx_message = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE_FOR_SETTLEMENT, + skill_callback_ids=["default"], + tx_id="transaction0", + tx_sender_addr="agent_1", + tx_counterparty_addr="pk", + tx_amount_by_currency_id={"FET": -2}, + tx_sender_fee=0, + tx_counterparty_fee=0, + tx_quantities_by_good_id={"good_id": 10}, + info={'some_info_key': 'some_info_value'}, + ledger_id="fetchai") self.decision_maker.handle(tx_message) assert not self.decision_maker.message_out_queue.empty() - with mock.patch.object(self.decision_maker, '_is_acceptable_tx', return_value=True): + with mock.patch.object(self.decision_maker, '_is_acceptable_for_settlement', return_value=True): self.decision_maker.handle(tx_message) assert not self.decision_maker.message_out_queue.empty() - with mock.patch.object(self.decision_maker, '_is_acceptable_tx', return_value=True): + with mock.patch.object(self.decision_maker, '_is_acceptable_for_settlement', return_value=True): with mock.patch.object(self.decision_maker, '_settle_tx', return_value=None): self.decision_maker.handle(tx_message) assert not self.decision_maker.message_out_queue.empty() @@ -386,6 +402,55 @@ def test_decision_maker_execute_w_wrong_input(self): self.mocked_logger_warning.assert_called_with("[{}]: Message received by the decision maker is not of protocol_id=internal.".format(self.agent_name)) + def test_is_affordable_off_chain(self): + """Test the off_chain message.""" + tx_message = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE_FOR_SETTLEMENT, + skill_callback_ids=["default"], + tx_id="transaction0", + tx_sender_addr="agent_1", + tx_counterparty_addr="pk", + tx_amount_by_currency_id={"FET": -20}, + tx_sender_fee=0, + tx_counterparty_fee=0, + tx_quantities_by_good_id={"good_id": 10}, + ledger_id="off_chain", + info={'some_info_key': 'some_info_value'}) + + assert self.decision_maker._is_affordable(tx_message) + + def test_settle_tx_off_chain(self): + """Test the off_chain message.""" + tx_message = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE_FOR_SETTLEMENT, + skill_callback_ids=["default"], + tx_id="transaction0", + tx_sender_addr="agent_1", + tx_counterparty_addr="pk", + tx_amount_by_currency_id={"FET": -20}, + tx_sender_fee=0, + tx_counterparty_fee=0, + tx_quantities_by_good_id={"good_id": 10}, + ledger_id="off_chain", + info={'some_info_key': 'some_info_value'}) + + tx_digest = self.decision_maker._settle_tx(tx_message) + assert tx_digest == "off_chain_settlement" + + def test__is_utility_enhancing(self): + """Test the utility enhancing for off_chain message.""" + tx_message = TransactionMessage(performative=TransactionMessage.Performative.PROPOSE_FOR_SETTLEMENT, + skill_callback_ids=["default"], + tx_id="transaction0", + tx_sender_addr="agent_1", + tx_counterparty_addr="pk", + tx_amount_by_currency_id={"FET": -20}, + tx_sender_fee=0, + tx_counterparty_fee=0, + tx_quantities_by_good_id={"good_id": 10}, + ledger_id="off_chain", + info={'some_info_key': 'some_info_value'}) + self.decision_maker.ownership_state._quantities_by_good_id = None + assert self.decision_maker._is_utility_enhancing(tx_message) + @classmethod def teardown_class(cls): """Tear the tests down.""" diff --git a/tests/test_decision_maker/test_messages/test_state_update.py b/tests/test_decision_maker/test_messages/test_state_update.py index 57e01a79ba..832ab40e85 100644 --- a/tests/test_decision_maker/test_messages/test_state_update.py +++ b/tests/test_decision_maker/test_messages/test_state_update.py @@ -33,11 +33,12 @@ def test_message_consistency(self): good_endowment = {"a_good": 2} exchange_params = {"FET": 10.0} utility_params = {"a_good": 20.0} - assert StateUpdateMessage(performative=StateUpdateMessage.Performative.INITIALIZE, amount_by_currency=currency_endowment, quantities_by_good_pbk=good_endowment, - exchange_params_by_currency=exchange_params, utility_params_by_good_pbk=utility_params) - currency_change = {"FET": - 10} + tx_fee = 10 + assert StateUpdateMessage(performative=StateUpdateMessage.Performative.INITIALIZE, amount_by_currency_id=currency_endowment, quantities_by_good_id=good_endowment, + exchange_params_by_currency_id=exchange_params, utility_params_by_good_id=utility_params, tx_fee=tx_fee) + currency_change = {"FET": 10} good_change = {"a_good": 1} - assert StateUpdateMessage(performative=StateUpdateMessage.Performative.APPLY, amount_by_currency=currency_change, quantities_by_good_pbk=good_change) + assert StateUpdateMessage(performative=StateUpdateMessage.Performative.APPLY, amount_by_currency_id=currency_change, quantities_by_good_id=good_change) def test_message_inconsistency(self): """Test for an error in consistency of a message.""" @@ -46,5 +47,6 @@ def test_message_inconsistency(self): good_endowment = {"a_good": 2} exchange_params = {"UNKNOWN": 10.0} utility_params = {"a_good": 20.0} - assert StateUpdateMessage(performative=StateUpdateMessage.Performative.INITIALIZE, amount_by_currency=currency_endowment, quantities_by_good_pbk=good_endowment, - exchange_params_by_currency=exchange_params, utility_params_by_good_pbk=utility_params) + tx_fee = 10 + assert StateUpdateMessage(performative=StateUpdateMessage.Performative.INITIALIZE, amount_by_currency_id=currency_endowment, quantities_by_good_id=good_endowment, + exchange_params_by_currency_id=exchange_params, utility_params_by_good_id=utility_params, tx_fee=tx_fee) diff --git a/tests/test_decision_maker/test_messages/test_transaction.py b/tests/test_decision_maker/test_messages/test_transaction.py index 26bc57cee4..9c68794443 100644 --- a/tests/test_decision_maker/test_messages/test_transaction.py +++ b/tests/test_decision_maker/test_messages/test_transaction.py @@ -28,48 +28,27 @@ class TestTransaction: def test_message_consistency(self): """Test for an error in consistency of a message.""" + assert TransactionMessage(performative=TransactionMessage.Performative.SUCCESSFUL_SETTLEMENT, + skill_callback_ids=["default"], + tx_id="transaction0", + tx_sender_addr="pk1", + tx_counterparty_addr="pk2", + tx_amount_by_currency_id={"Unknown": 2}, + tx_sender_fee=0, + tx_counterparty_fee=0, + tx_quantities_by_good_id={"Unknown": 10}, + ledger_id="fetchai", + info={'some_string': [1, 2]}, + tx_digest='some_string') with pytest.raises(AssertionError): - TransactionMessage(performative="performative", - skill_id="default", - transaction_id="transaction0", - sender="pk", - counterparty="pk", - is_sender_buyer=True, - currency_pbk="Unknown", - amount=2, - sender_tx_fee=0, - counterparty_tx_fee=0, - quantities_by_good_pbk={"Unknown": 10}, - ledger_id="fetchai") - - def test_matches(self): - """Test if the transaction matches with another transaction.""" - msg = TransactionMessage(performative=TransactionMessage.Performative.ACCEPT, - skill_id="default", - transaction_id="transaction0", - sender="agent_1", - counterparty="pk", - is_sender_buyer=True, - currency_pbk="FET", - amount=2, - sender_tx_fee=0, - counterparty_tx_fee=0, - quantities_by_good_pbk={"FET": 10}, - ledger_id="fetchai") - assert not msg.matches(msg), "It shouldn't match since it is the same message." - assert msg == msg, "It should be equal since is the same message." - - mirrored_message = TransactionMessage(performative=TransactionMessage.Performative.ACCEPT, - skill_id="default", - transaction_id="transaction0", - sender="pk", - counterparty="agent_1", - is_sender_buyer=False, - currency_pbk="FET", - amount=2, - sender_tx_fee=0, - counterparty_tx_fee=0, - quantities_by_good_pbk={"FET": 10}, - ledger_id="fetchai") - - assert msg.matches(mirrored_message), "It should match since the messages mirror each other" + TransactionMessage(performative=TransactionMessage.Performative.SUCCESSFUL_SETTLEMENT, + skill_callback_ids=["default"], + tx_id="transaction0", + tx_sender_addr="pk", + tx_counterparty_addr="pk", + tx_amount_by_currency_id={"Unknown": 2}, + tx_sender_fee=0, + tx_counterparty_fee=0, + tx_quantities_by_good_id={"Unknown": 10}, + ledger_id="fetchai", + info={'info': "info_value"}) diff --git a/tests/test_gui/test_add.py b/tests/test_gui/test_add.py index c0b4d84db5..0c2a7fb62f 100644 --- a/tests/test_gui/test_add.py +++ b/tests/test_gui/test_add.py @@ -20,7 +20,6 @@ """This test module contains the tests for the `aea gui` sub-commands.""" import json - import unittest.mock from .test_base import create_app diff --git a/tests/test_gui/test_base.py b/tests/test_gui/test_base.py index 1a372994f7..97de145f7d 100644 --- a/tests/test_gui/test_base.py +++ b/tests/test_gui/test_base.py @@ -58,6 +58,14 @@ def __init__(self): self.cwd = os.getcwd() os.chdir(self.temp_dir) + def __enter__(self): + """Create the empty directory in a context manager.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Restore the initial conditions.""" + self.destroy() + def destroy(self): """Destroy the cwd and restore the old one.""" os.chdir(self.cwd) diff --git a/tests/test_gui/test_create.py b/tests/test_gui/test_create.py index 572d237949..9ea2ac543e 100644 --- a/tests/test_gui/test_create.py +++ b/tests/test_gui/test_create.py @@ -19,11 +19,15 @@ """This test module contains the tests for the `aea gui` sub-commands.""" import json +import shutil +import sys import time import unittest.mock +from pathlib import Path from .test_base import create_app, TempCWD +from ..conftest import CUR_PATH def test_create_agent(): @@ -32,9 +36,11 @@ def test_create_agent(): agent_name = "test_agent_id" def _dummy_call_aea(param_list, dir): - assert param_list[0] == "aea" - assert param_list[1] == "create" - assert param_list[2] == agent_name + assert param_list[0] == sys.executable + assert param_list[1] == "-m" + assert param_list[2] == "aea.cli" + assert param_list[3] == "create" + assert param_list[4] == agent_name return 0 with unittest.mock.patch("aea.cli_gui._call_aea", _dummy_call_aea): @@ -54,9 +60,11 @@ def test_create_agent_fail(): agent_name = "test_agent_id" def _dummy_call_aea(param_list, dir): - assert param_list[0] == "aea" - assert param_list[1] == "create" - assert param_list[2] == agent_name + assert param_list[0] == sys.executable + assert param_list[1] == "-m" + assert param_list[2] == "aea.cli" + assert param_list[3] == "create" + assert param_list[4] == agent_name return 1 with unittest.mock.patch("aea.cli_gui._call_aea", _dummy_call_aea): @@ -73,43 +81,43 @@ def _dummy_call_aea(param_list, dir): def test_real_create(): """Really create an agent (have to test the call_aea at some point).""" # Set up a temporary current working directory to make agents in - temp_cwd = TempCWD() - app = create_app() + with TempCWD() as temp_cwd: + app = create_app() - agent_id = "test_agent_id" - response_create = app.post( - 'api/agent', - content_type='application/json', - data=json.dumps(agent_id)) - assert response_create.status_code == 201 - data = json.loads(response_create.get_data(as_text=True)) - assert data == agent_id - - # Give it a bit of time so the polling funcionts get called - time.sleep(1) - - # Check that we can actually see this agent too - response_agents = app.get( - 'api/agent', - data=None, - content_type='application/json', - ) - data = json.loads(response_agents.get_data(as_text=True)) - assert response_agents.status_code == 200 - assert len(data) == 1 - assert data[0]['id'] == agent_id - assert data[0]['description'] == "placeholder description" - - # do same but this time find that this is not an agent directory. - with unittest.mock.patch("os.path.isdir", return_value=False): + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(temp_cwd.temp_dir, "packages")) + + agent_id = "test_agent_id" + response_create = app.post( + 'api/agent', + content_type='application/json', + data=json.dumps(agent_id)) + assert response_create.status_code == 201 + data = json.loads(response_create.get_data(as_text=True)) + assert data == agent_id + + # Give it a bit of time so the polling funcionts get called + time.sleep(1) + + # Check that we can actually see this agent too response_agents = app.get( 'api/agent', data=None, content_type='application/json', ) - data = json.loads(response_agents.get_data(as_text=True)) - assert response_agents.status_code == 200 - assert len(data) == 0 - - # Destroy the temporary current working directory and put cwd back to what it was before - temp_cwd.destroy() + data = json.loads(response_agents.get_data(as_text=True)) + assert response_agents.status_code == 200 + assert len(data) == 1 + assert data[0]['id'] == agent_id + assert data[0]['description'] == "placeholder description" + + # do same but this time find that this is not an agent directory. + with unittest.mock.patch("os.path.isdir", return_value=False): + response_agents = app.get( + 'api/agent', + data=None, + content_type='application/json', + ) + data = json.loads(response_agents.get_data(as_text=True)) + assert response_agents.status_code == 200 + assert len(data) == 0 diff --git a/tests/test_gui/test_delete.py b/tests/test_gui/test_delete.py index 364a657396..cffaf098fb 100644 --- a/tests/test_gui/test_delete.py +++ b/tests/test_gui/test_delete.py @@ -20,6 +20,7 @@ """This test module contains the tests for the `aea gui` sub-commands.""" import json +import sys import unittest.mock @@ -32,9 +33,11 @@ def test_delete_agent(): agent_name = "test_agent_id" def _dummy_call_aea(param_list, dir): - assert param_list[0] == "aea" - assert param_list[1] == "delete" - assert param_list[2] == agent_name + assert param_list[0] == sys.executable + assert param_list[1] == "-m" + assert param_list[2] == "aea.cli" + assert param_list[3] == "delete" + assert param_list[4] == agent_name return 0 with unittest.mock.patch("aea.cli_gui._call_aea", _dummy_call_aea): @@ -54,9 +57,11 @@ def test_delete_agent_fail(): agent_name = "test_agent_id" def _dummy_call_aea(param_list, dir): - assert param_list[0] == "aea" - assert param_list[1] == "delete" - assert param_list[2] == agent_name + assert param_list[0] == sys.executable + assert param_list[1] == "-m" + assert param_list[2] == "aea.cli" + assert param_list[3] == "delete" + assert param_list[4] == agent_name return 1 with unittest.mock.patch("aea.cli_gui._call_aea", _dummy_call_aea): diff --git a/tests/test_gui/test_list.py b/tests/test_gui/test_list.py index 1ec8fbfebe..828c31717d 100644 --- a/tests/test_gui/test_list.py +++ b/tests/test_gui/test_list.py @@ -19,6 +19,7 @@ """This test module contains the tests for the `aea gui` sub-commands.""" import json +import sys import unittest.mock @@ -47,9 +48,11 @@ def _test_list_items(item_type: str): agent_name = "test_agent_id" def _dummy_call_aea_async(param_list, dir_arg): - assert param_list[0] == "aea" - assert param_list[1] == "list" - assert param_list[2] == item_type + "s" + assert param_list[0] == sys.executable + assert param_list[1] == "-m" + assert param_list[2] == "aea.cli" + assert param_list[3] == "list" + assert param_list[4] == item_type + "s" assert agent_name in dir_arg return pid @@ -76,9 +79,11 @@ def _test_list_items_none(item_type: str): agent_name = "NONE" def _dummy_call_aea_async(param_list, dir_arg): - assert param_list[0] == "aea" - assert param_list[1] == "list" - assert param_list[2] == item_type + "s" + assert param_list[0] == sys.executable + assert param_list[1] == "-m" + assert param_list[2] == "aea.cli" + assert param_list[3] == "list" + assert param_list[4] == item_type + "s" return pid with unittest.mock.patch("aea.cli_gui._call_aea_async", _dummy_call_aea_async): @@ -99,9 +104,11 @@ def _test_list_items_fail(item_type: str): agent_name = "test_agent_id" def _dummy_call_aea_async(param_list, dir_arg): - assert param_list[0] == "aea" - assert param_list[1] == "list" - assert param_list[2] == item_type + "s" + assert param_list[0] == sys.executable + assert param_list[1] == "-m" + assert param_list[2] == "aea.cli" + assert param_list[3] == "list" + assert param_list[4] == item_type + "s" assert agent_name in dir_arg return pid diff --git a/tests/test_gui/test_remove.py b/tests/test_gui/test_remove.py index 15decd8ad4..0ffacc18f0 100644 --- a/tests/test_gui/test_remove.py +++ b/tests/test_gui/test_remove.py @@ -20,6 +20,7 @@ """This test module contains the tests for the `aea gui` sub-commands.""" import json +import sys import unittest.mock @@ -37,10 +38,12 @@ def test_remove_item(): connection_name = "test_connection" def _dummy_call_aea(param_list, dir): - assert param_list[0] == "aea" - assert param_list[1] == "remove" - assert param_list[2] == "connection" - assert param_list[3] == connection_name + assert param_list[0] == sys.executable + assert param_list[1] == "-m" + assert param_list[2] == "aea.cli" + assert param_list[3] == "remove" + assert param_list[4] == "connection" + assert param_list[5] == connection_name assert agent_name in dir return 0 @@ -66,10 +69,12 @@ def test_delete_agent_fail(): connection_name = "test_connection" def _dummy_call_aea(param_list, dir): - assert param_list[0] == "aea" - assert param_list[1] == "remove" - assert param_list[2] == "connection" - assert param_list[3] == connection_name + assert param_list[0] == sys.executable + assert param_list[1] == "-m" + assert param_list[2] == "aea.cli" + assert param_list[3] == "remove" + assert param_list[4] == "connection" + assert param_list[5] == connection_name assert agent_name in dir return 1 diff --git a/tests/test_gui/test_run_agent.py b/tests/test_gui/test_run_agent.py index 1aeb540c40..f98667dc5d 100644 --- a/tests/test_gui/test_run_agent.py +++ b/tests/test_gui/test_run_agent.py @@ -19,203 +19,210 @@ """This test module contains the tests for the `aea gui` sub-commands.""" import json +import shutil +import sys import time import unittest.mock +from pathlib import Path + import aea from .test_base import create_app, TempCWD +from ..conftest import CUR_PATH def test_create_and_run_agent(): """Test for running and agent, reading TTY and errors.""" # Set up a temporary current working directory in which to make agents - temp_cwd = TempCWD() - app = create_app() - - agent_id = "test_agent" - - # Make an agent - response_create = app.post( - 'api/agent', - content_type='application/json', - data=json.dumps(agent_id)) - assert response_create.status_code == 201 - data = json.loads(response_create.get_data(as_text=True)) - assert data == agent_id - - # Add the local connection - response_add = app.post( - 'api/agent/' + agent_id + "/connection", - content_type='application/json', - data=json.dumps("local") - ) - assert response_add.status_code == 201 - - # Get the running status before we have run it - response_status = app.get( - 'api/agent/' + agent_id + "/run", - data=None, - content_type='application/json', - ) - assert response_status.status_code == 200 - data = json.loads(response_status.get_data(as_text=True)) - assert "NOT_STARTED" in data["status"] - - # run the agent with a non existent connection - response_run = app.post( - 'api/agent/' + agent_id + "/run", - content_type='application/json', - data=json.dumps("non-existent-connection") - ) - assert response_run.status_code == 400 - - # run the agent with default connection - should be something in the error output? - response_run = app.post( - 'api/agent/' + agent_id + "/run", - content_type='application/json', - data=json.dumps("") - ) - assert response_run.status_code == 201 - time.sleep(2) - - # Stop the agent running - response_stop = app.delete( - 'api/agent/' + agent_id + "/run", - data=None, - content_type='application/json', - ) - assert response_stop.status_code == 200 - time.sleep(2) - - # run the agent with local connection (as no OEF node is running) - response_run = app.post( - 'api/agent/' + agent_id + "/run", - content_type='application/json', - data=json.dumps("local") - ) - assert response_run.status_code == 201 - - time.sleep(2) - - # Try running it again (this should fail) - response_run = app.post( - 'api/agent/' + agent_id + "/run", - content_type='application/json', - data=json.dumps("local") - ) - assert response_run.status_code == 400 - - # Get the running status - response_status = app.get( - 'api/agent/' + agent_id + "/run", - data=None, - content_type='application/json', - ) - assert response_status.status_code == 200 - data = json.loads(response_status.get_data(as_text=True)) - - assert data["error"] == "" - assert "RUNNING" in data["status"] - - # Create a stop agent function that behaves as if the agent had stopped itself - def _stop_agent_override(loc_agent_id: str): - # Test if we have the process id - assert loc_agent_id in aea.cli_gui.app_context.agent_processes - - aea.cli_gui.app_context.agent_processes[loc_agent_id].terminate() - aea.cli_gui.app_context.agent_processes[loc_agent_id].wait() - - return "stop_agent: All fine {}".format(loc_agent_id), 200 # 200 (OK) - - with unittest.mock.patch("aea.cli_gui._stop_agent", _stop_agent_override): - app.delete( + with TempCWD() as temp_cwd: + app = create_app() + + # copy the 'packages' directory in the parent of the agent folder. + shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(temp_cwd.temp_dir, "packages")) + + agent_id = "test_agent" + + # Make an agent + response_create = app.post( + 'api/agent', + content_type='application/json', + data=json.dumps(agent_id)) + assert response_create.status_code == 201 + data = json.loads(response_create.get_data(as_text=True)) + assert data == agent_id + + # Add the local connection + response_add = app.post( + 'api/agent/' + agent_id + "/connection", + content_type='application/json', + data=json.dumps("local") + ) + assert response_add.status_code == 201 + + # Get the running status before we have run it + response_status = app.get( + 'api/agent/' + agent_id + "/run", + data=None, + content_type='application/json', + ) + assert response_status.status_code == 200 + data = json.loads(response_status.get_data(as_text=True)) + assert "NOT_STARTED" in data["status"] + + # run the agent with a non existent connection + response_run = app.post( + 'api/agent/' + agent_id + "/run", + content_type='application/json', + data=json.dumps("non-existent-connection") + ) + assert response_run.status_code == 400 + + # run the agent with default connection - should be something in the error output? + response_run = app.post( + 'api/agent/' + agent_id + "/run", + content_type='application/json', + data=json.dumps("") + ) + assert response_run.status_code == 201 + time.sleep(2) + + # Stop the agent running + response_stop = app.delete( + 'api/agent/' + agent_id + "/run", + data=None, + content_type='application/json', + ) + assert response_stop.status_code == 200 + time.sleep(2) + + # run the agent with local connection (as no OEF node is running) + response_run = app.post( + 'api/agent/' + agent_id + "/run", + content_type='application/json', + data=json.dumps("local") + ) + assert response_run.status_code == 201 + + time.sleep(2) + + # Try running it again (this should fail) + response_run = app.post( + 'api/agent/' + agent_id + "/run", + content_type='application/json', + data=json.dumps("local") + ) + assert response_run.status_code == 400 + + # Get the running status + response_status = app.get( 'api/agent/' + agent_id + "/run", data=None, content_type='application/json', ) - time.sleep(1) - - # Get the running status - response_status = app.get( - 'api/agent/' + agent_id + "/run", - data=None, - content_type='application/json', - ) - assert response_status.status_code == 200 - data = json.loads(response_status.get_data(as_text=True)) - assert "process terminate" in data["error"] - assert "FINISHED" in data["status"] - - # run the agent again (takes a different path through code) - response_run = app.post( - 'api/agent/' + agent_id + "/run", - content_type='application/json', - data=json.dumps("local") - ) - assert response_run.status_code == 201 - - time.sleep(2) - - # Get the running status - response_status = app.get( - 'api/agent/' + agent_id + "/run", - data=None, - content_type='application/json', - ) - assert response_status.status_code == 200 - data = json.loads(response_status.get_data(as_text=True)) - - assert data["error"] == "" - assert "RUNNING" in data["status"] - - # Stop the agent running - response_stop = app.delete( - 'api/agent/' + agent_id + "/run", - data=None, - content_type='application/json', - ) - assert response_stop.status_code == 200 - time.sleep(2) - - # Get the running status - response_status = app.get( - 'api/agent/' + agent_id + "/run", - data=None, - content_type='application/json', - ) - assert response_status.status_code == 200 - data = json.loads(response_status.get_data(as_text=True)) - - assert "process terminate" in data["error"] - assert "NOT_STARTED" in data["status"] - - # Stop a none existent agent running - response_stop = app.delete( - 'api/agent/' + agent_id + "_NOT" + "/run", - data=None, - content_type='application/json', - ) - assert response_stop.status_code == 400 - time.sleep(2) - - genuine_func = aea.cli_gui._call_aea_async - - def _dummy_call_aea_async(param_list, dir_arg): - assert param_list[0] == "aea" - if param_list[1] == "run": - return None - else: - return genuine_func(param_list, dir_arg) - - # Run when process files (but other call - such as status should not fail) - with unittest.mock.patch("aea.cli_gui._call_aea_async", _dummy_call_aea_async): + assert response_status.status_code == 200 + data = json.loads(response_status.get_data(as_text=True)) + + assert data["error"] == "" + assert "RUNNING" in data["status"] + + # Create a stop agent function that behaves as if the agent had stopped itself + def _stop_agent_override(loc_agent_id: str): + # Test if we have the process id + assert loc_agent_id in aea.cli_gui.app_context.agent_processes + + aea.cli_gui.app_context.agent_processes[loc_agent_id].terminate() + aea.cli_gui.app_context.agent_processes[loc_agent_id].wait() + + return "stop_agent: All fine {}".format(loc_agent_id), 200 # 200 (OK) + + with unittest.mock.patch("aea.cli_gui._stop_agent", _stop_agent_override): + app.delete( + 'api/agent/' + agent_id + "/run", + data=None, + content_type='application/json', + ) + time.sleep(1) + + # Get the running status + response_status = app.get( + 'api/agent/' + agent_id + "/run", + data=None, + content_type='application/json', + ) + assert response_status.status_code == 200 + data = json.loads(response_status.get_data(as_text=True)) + assert "process terminate" in data["error"] + assert "FINISHED" in data["status"] + + # run the agent again (takes a different path through code) response_run = app.post( 'api/agent/' + agent_id + "/run", content_type='application/json', data=json.dumps("local") ) - assert response_run.status_code == 400 + assert response_run.status_code == 201 + + time.sleep(2) - # Destroy the temporary current working directory and put cwd back to what it was before - temp_cwd.destroy() + # Get the running status + response_status = app.get( + 'api/agent/' + agent_id + "/run", + data=None, + content_type='application/json', + ) + assert response_status.status_code == 200 + data = json.loads(response_status.get_data(as_text=True)) + + assert data["error"] == "" + assert "RUNNING" in data["status"] + + # Stop the agent running + response_stop = app.delete( + 'api/agent/' + agent_id + "/run", + data=None, + content_type='application/json', + ) + assert response_stop.status_code == 200 + time.sleep(2) + + # Get the running status + response_status = app.get( + 'api/agent/' + agent_id + "/run", + data=None, + content_type='application/json', + ) + assert response_status.status_code == 200 + data = json.loads(response_status.get_data(as_text=True)) + + assert "process terminate" in data["error"] + assert "NOT_STARTED" in data["status"] + + # Stop a none existent agent running + response_stop = app.delete( + 'api/agent/' + agent_id + "_NOT" + "/run", + data=None, + content_type='application/json', + ) + assert response_stop.status_code == 400 + time.sleep(2) + + genuine_func = aea.cli_gui._call_aea_async + + def _dummy_call_aea_async(param_list, dir_arg): + assert param_list[0] == sys.executable + assert param_list[1] == "-m" + assert param_list[2] == "aea.cli" + if param_list[3] == "run": + return None + else: + return genuine_func(param_list, dir_arg) + + # Run when process files (but other call - such as status should not fail) + with unittest.mock.patch("aea.cli_gui._call_aea_async", _dummy_call_aea_async): + response_run = app.post( + 'api/agent/' + agent_id + "/run", + content_type='application/json', + data=json.dumps("local") + ) + assert response_run.status_code == 400 diff --git a/tests/test_gui/test_run_oef.py b/tests/test_gui/test_run_oef.py index d73a890efe..13aa8e8350 100644 --- a/tests/test_gui/test_run_oef.py +++ b/tests/test_gui/test_run_oef.py @@ -19,6 +19,7 @@ """This test module contains the tests for the `aea gui` sub-commands.""" import json +import sys import time import unittest.mock @@ -32,7 +33,7 @@ def test_create_and_run_oef(): pid = DummyPID(None, "A thing of beauty is a joy forever\n", "Testing Error\n") def _dummy_call_aea_async(param_list, dir_arg): - assert param_list[0] == "python" + assert param_list[0] == sys.executable assert "launch.py" in param_list[1] return pid @@ -116,7 +117,7 @@ def test_create_and_run_oef_fail(): app = create_app() def _dummy_call_aea_async(param_list, dir_arg): - assert param_list[0] == "python" + assert param_list[0] == sys.executable assert "launch.py" in param_list[1] return None diff --git a/tests/test_gui/test_scaffold.py b/tests/test_gui/test_scaffold.py index c7b510394e..dcf5db1cdf 100644 --- a/tests/test_gui/test_scaffold.py +++ b/tests/test_gui/test_scaffold.py @@ -20,6 +20,7 @@ """This test module contains the tests for the `aea gui` sub-commands.""" import json +import sys import unittest.mock @@ -37,10 +38,12 @@ def test_scaffold_item(): connection_name = "test_connection" def _dummy_call_aea(param_list, dir): - assert param_list[0] == "aea" - assert param_list[1] == "scaffold" - assert param_list[2] == "connection" - assert param_list[3] == connection_name + assert param_list[0] == sys.executable + assert param_list[1] == "-m" + assert param_list[2] == "aea.cli" + assert param_list[3] == "scaffold" + assert param_list[4] == "connection" + assert param_list[5] == connection_name assert agent_name in dir return 0 @@ -66,10 +69,12 @@ def test_scaffold_agent_fail(): connection_name = "test_connection" def _dummy_call_aea(param_list, dir): - assert param_list[0] == "aea" - assert param_list[1] == "scaffold" - assert param_list[2] == "connection" - assert param_list[3] == connection_name + assert param_list[0] == sys.executable + assert param_list[1] == "-m" + assert param_list[2] == "aea.cli" + assert param_list[3] == "scaffold" + assert param_list[4] == "connection" + assert param_list[5] == connection_name assert agent_name in dir return 1 diff --git a/tests/test_gui/test_search.py b/tests/test_gui/test_search.py index 66172dece9..f4f9d8e35c 100644 --- a/tests/test_gui/test_search.py +++ b/tests/test_gui/test_search.py @@ -56,14 +56,13 @@ def _test_search_items_with_query(item_type: str, query: str): ) assert response_list.status_code == 200 data = json.loads(response_list.get_data(as_text=True)) - assert len(data[0]) == 2 - assert data[0][0]['id'] == 'default' - assert data[0][0]['description'] == 'The default item allows for any byte logic.' - assert data[0][1]['id'] == 'oef' - assert data[0][1]['description'] == 'The oef item implements the OEF specific logic.' - assert data[0][1]['description'] == 'The oef item implements the OEF specific logic.' - assert data[1] == item_type - assert data[2] == 'test' + assert len(data["search_result"]) == 2 + assert data["search_result"][0]['id'] == 'default' + assert data["search_result"][0]['description'] == 'The default item allows for any byte logic.' + assert data["search_result"][1]['id'] == 'oef' + assert data["search_result"][1]['description'] == 'The oef item implements the OEF specific logic.' + assert data["item_type"] == item_type + assert data["search_term"] == 'test' def _test_search_items(item_type: str): @@ -139,29 +138,23 @@ def test_real_search(): ) assert response_list.status_code == 200 data = json.loads(response_list.get_data(as_text=True)) - assert len(data) == 6 i = 0 + assert data[i]['id'] == 'gym' assert data[i]['description'] == 'The gym connection wraps an OpenAI gym.' i += 1 - assert data[i]['id'] == 'local' assert data[i]['description'] == 'The local connection provides a stub for an OEF node.' i += 1 - assert data[i]['id'] == 'oef' assert data[i]['description'] == 'The oef connection provides a wrapper around the OEF sdk.' i += 1 - assert data[i]['id'] == 'p2p' assert data[i]['description'] == 'The p2p connection provides a connection with the fetch.ai mail provider.' i += 1 - assert data[i]['id'] == 'stub' assert data[i]['description'] == 'The stub connection implements a connection stub which reads/writes messages from/to file.' i += 1 - assert data[i]['id'] == 'tcp' - assert data[i]['description'] == 'None' - i += 1 + assert data[i]['description'] == '' diff --git a/tests/test_helpers/test_base.py b/tests/test_helpers/test_base.py index 9e9f7040ee..a6385961c8 100644 --- a/tests/test_helpers/test_base.py +++ b/tests/test_helpers/test_base.py @@ -21,8 +21,8 @@ import os import sys -from aea.connections.oef.connection import OEFConnection from aea.helpers.base import locate +from packages.connections.oef.connection import OEFConnection from ..conftest import CUR_PATH @@ -44,7 +44,7 @@ def test_locate_class(self): cwd = os.getcwd() os.chdir(os.path.join(CUR_PATH, "..")) expected_class = OEFConnection - actual_class = locate("aea.connections.oef.connection.OEFConnection") + actual_class = locate("packages.connections.oef.connection.OEFConnection") # although they are the same class, they are different instances in memory # and the build-in default "__eq__" method does not compare the attributes. # so compare the names diff --git a/tests/test_helpers/test_dialogue/test_base.py b/tests/test_helpers/test_dialogue/test_base.py index 5b92b395ca..2f22261fc0 100644 --- a/tests/test_helpers/test_dialogue/test_base.py +++ b/tests/test_helpers/test_dialogue/test_base.py @@ -30,8 +30,8 @@ class TestDialogueBase: @classmethod def setup(cls): """Initialise the class.""" - cls.dialogue_label = DialogueLabel(dialogue_reference=(str(0), ''), dialogue_opponent_pbk="opponent", - dialogue_starter_pbk="starter") + cls.dialogue_label = DialogueLabel(dialogue_reference=(str(0), ''), dialogue_opponent_addr="opponent", + dialogue_starter_addr="starter") cls.dialogue = Dialogue(dialogue_label=cls.dialogue_label) cls.dialogues = Dialogues() @@ -39,11 +39,15 @@ def test_dialogue_label(self): """Test the dialogue_label.""" assert self.dialogue_label.dialogue_starter_reference == str(0) assert self.dialogue_label.dialogue_responder_reference == '' - assert self.dialogue_label.dialogue_opponent_pbk == "opponent" - assert self.dialogue_label.dialogue_starter_pbk == "starter" + assert self.dialogue_label.dialogue_opponent_addr == "opponent" + assert self.dialogue_label.dialogue_starter_addr == "starter" + assert str(self.dialogue_label) == "{}_{}_{}_{}".format(self.dialogue_label.dialogue_starter_reference, + self.dialogue_label.dialogue_responder_reference, + self.dialogue_label.dialogue_opponent_addr, + self.dialogue_label.dialogue_starter_addr) - dialogue_label2 = DialogueLabel(dialogue_reference=(str(0), ''), dialogue_opponent_pbk="opponent", - dialogue_starter_pbk="starter") + dialogue_label2 = DialogueLabel(dialogue_reference=(str(0), ''), dialogue_opponent_addr="opponent", + dialogue_starter_addr="starter") assert dialogue_label2 == self.dialogue_label @@ -56,8 +60,8 @@ def test_dialogue_label(self): assert self.dialogue_label.json == dict( dialogue_starter_reference=str(0), dialogue_responder_reference='', - dialogue_opponent_pbk="opponent", - dialogue_starter_pbk="starter" + dialogue_opponent_addr="opponent", + dialogue_starter_addr="starter" ) assert DialogueLabel.from_json(self.dialogue_label.json) == self.dialogue_label @@ -65,7 +69,7 @@ def test_dialogue(self): """Test the dialogue.""" assert self.dialogue.is_self_initiated msg = DefaultMessage(type=DefaultMessage.Type.BYTES, content=b'Hello') - + msg.counterparty = "my_agent" assert self.dialogue.last_incoming_message is None assert self.dialogue.last_outgoing_message is None diff --git a/tests/test_mail.py b/tests/test_mail.py index 8fb00d430b..b202c1f01a 100644 --- a/tests/test_mail.py +++ b/tests/test_mail.py @@ -24,15 +24,32 @@ import pytest import aea -from aea.connections.local.connection import LocalNode, OEFLocalConnection -from aea.mail.base import Envelope, InBox, OutBox, Multiplexer +from aea.mail.base import Envelope, InBox, OutBox, Multiplexer, URI from aea.protocols.base import Message from aea.protocols.base import ProtobufSerializer from aea.protocols.default.message import DefaultMessage from aea.protocols.default.serialization import DefaultSerializer +from packages.connections.local.connection import LocalNode, OEFLocalConnection from .conftest import DummyConnection +def test_uri(): + """Testing the uri initialisation.""" + uri_raw = 'http://user:pwd@NetLoc:80/path;param?query=arg#frag' + uri = URI(uri_raw=uri_raw) + assert uri_raw == str(uri) + assert uri.scheme == 'http' + assert uri.netloc == 'user:pwd@NetLoc:80' + assert uri.path == '/path' + assert uri.params == 'param' + assert uri.query == 'query=arg' + assert uri.fragment == 'frag' + assert uri.host == 'netloc' + assert uri.port == 80 + assert uri.username == 'user' + assert uri.password == 'pwd' + + def test_envelope_initialisation(): """Testing the envelope initialisation.""" msg = Message(content='hello') @@ -149,8 +166,8 @@ def test_outbox_empty(): def test_multiplexer(): """Tests if the multiplexer is connected.""" with LocalNode() as node: - public_key_1 = "public_key_1" - multiplexer = Multiplexer([OEFLocalConnection(public_key_1, node)]) + address_1 = "address_1" + multiplexer = Multiplexer([OEFLocalConnection(address_1, node)]) multiplexer.connect() assert multiplexer.is_connected, "Mailbox cannot connect to the specific Connection(OEFLocalConnection)" multiplexer.disconnect() diff --git a/tests/test_multiplexer.py b/tests/test_multiplexer.py index 9ee9e95782..3018ef1ace 100644 --- a/tests/test_multiplexer.py +++ b/tests/test_multiplexer.py @@ -30,11 +30,11 @@ import pytest import aea -from aea.connections.local.connection import LocalNode, OEFLocalConnection from aea.connections.stub.connection import StubConnection from aea.mail.base import Multiplexer, AEAConnectionError, Envelope, EnvelopeContext from aea.protocols.default.message import DefaultMessage from aea.protocols.default.serialization import DefaultSerializer +from packages.connections.local.connection import LocalNode, OEFLocalConnection from .conftest import DummyConnection @@ -117,7 +117,7 @@ def test_multiplexer_connect_one_raises_error_many_connections(): input_file_path = d / "input_file.csv" output_file_path = d / "input_file.csv" - connection_1 = OEFLocalConnection("my_pbk", node) + connection_1 = OEFLocalConnection("my_addr", node) connection_2 = StubConnection(input_file_path, output_file_path) connection_3 = DummyConnection() multiplexer = Multiplexer([connection_1, connection_2, connection_3]) @@ -172,7 +172,7 @@ def test_multiplexer_disconnect_one_raises_error_many_connections(): input_file_path = d / "input_file.csv" output_file_path = d / "input_file.csv" - connection_1 = OEFLocalConnection("my_pbk", node) + connection_1 = OEFLocalConnection("my_addr", node) connection_2 = StubConnection(input_file_path, output_file_path) connection_3 = DummyConnection() multiplexer = Multiplexer([connection_1, connection_2, connection_3]) @@ -284,13 +284,13 @@ def test_get_from_multiplexer_when_empty(): def test_multiple_connection(): """Test that we can send a message with two different connections.""" with LocalNode() as node: - public_key_1 = "public_key_1" - public_key_2 = "public_key_2" + address_1 = "address_1" + address_2 = "address_2" connection_1_id = "local_1" connection_2_id = "local_2" - connection_1 = OEFLocalConnection(public_key_1, node, connection_id=connection_1_id) - connection_2 = OEFLocalConnection(public_key_2, node, connection_id=connection_2_id) + connection_1 = OEFLocalConnection(address_1, node, connection_id=connection_1_id) + connection_2 = OEFLocalConnection(address_2, node, connection_id=connection_2_id) multiplexer = Multiplexer([connection_1, connection_2]) assert not connection_1.connection_status.is_connected @@ -302,7 +302,7 @@ def test_multiple_connection(): assert connection_2.connection_status.is_connected message = DefaultMessage(type=DefaultMessage.Type.BYTES, content=b"hello") - envelope_from_1_to_2 = Envelope(to=public_key_2, sender=public_key_1, protocol_id=DefaultMessage.protocol_id, + envelope_from_1_to_2 = Envelope(to=address_2, sender=address_1, protocol_id=DefaultMessage.protocol_id, message=DefaultSerializer().encode(message), context=EnvelopeContext(connection_id=connection_1_id)) multiplexer.put(envelope_from_1_to_2) @@ -310,7 +310,7 @@ def test_multiple_connection(): actual_envelope = multiplexer.get(block=True, timeout=2.0) assert envelope_from_1_to_2 == actual_envelope - envelope_from_2_to_1 = Envelope(to=public_key_1, sender=public_key_2, protocol_id=DefaultMessage.protocol_id, + envelope_from_2_to_1 = Envelope(to=address_1, sender=address_2, protocol_id=DefaultMessage.protocol_id, message=DefaultSerializer().encode(message), context=EnvelopeContext(connection_id=connection_2_id)) multiplexer.put(envelope_from_2_to_1) @@ -324,17 +324,18 @@ def test_multiple_connection(): def test_send_message_no_supported_protocol(): """Test the case when we send an envelope with a specific connection that does not support the protocol.""" with LocalNode() as node: - public_key_1 = "public_key_1" + address_1 = "address_1" connection_1_id = "local_1" - connection_1 = OEFLocalConnection(public_key_1, node, connection_id=connection_1_id, - restricted_to_protocols={"my_private_protocol"}) + connection_1 = OEFLocalConnection(address_1, node, connection_id=connection_1_id, + restricted_to_protocols={"my_private_protocol"}, + excluded_protocols={"my_other_protocol"}) multiplexer = Multiplexer([connection_1]) multiplexer.connect() with mock.patch.object(aea.mail.base.logger, "warning") as mock_logger_warning: protocol_id = "this_is_a_non_existing_protocol_id" - envelope = Envelope(to=public_key_1, sender=public_key_1, + envelope = Envelope(to=address_1, sender=address_1, protocol_id=protocol_id, message=b"some bytes") multiplexer.put(envelope) diff --git a/tests/test_packages/test_connections/test_gym.py b/tests/test_packages/test_connections/test_gym.py index c56ccb0f56..7d6ea6fcb7 100644 --- a/tests/test_packages/test_connections/test_gym.py +++ b/tests/test_packages/test_connections/test_gym.py @@ -20,17 +20,15 @@ """This module contains the tests of the gym connection module.""" import logging +import gym import pytest from aea.configurations.base import ConnectionConfig from aea.mail.base import Envelope from packages.connections.gym.connection import GymConnection -import gym - from packages.protocols.gym.message import GymMessage from packages.protocols.gym.serialization import GymSerializer - logger = logging.getLogger(__name__) @@ -41,7 +39,7 @@ class TestGymConnection: def setup_class(cls): """Initialise the class.""" cls.env = gym.GoalEnv() - cls.gym_con = GymConnection(public_key="my_key", gym_env=cls.env) + cls.gym_con = GymConnection(address="my_key", gym_env=cls.env) def test_gym_connection_initialization(self): """Test the connection None return value after connect().""" @@ -73,9 +71,10 @@ async def test_receive_connection_error(self): with pytest.raises(ConnectionError): await self.gym_con.receive() - def test_gym_from_config(self): - """Test the Connection from config File.""" - conf = ConnectionConfig() - conf.config['env'] = "tests.conftest.DUMMY_ENV" - con = GymConnection.from_config(public_key="pk", connection_configuration=conf) - assert con is not None + +def test_gym_from_config(): + """Test the Connection from config File.""" + conf = ConnectionConfig() + conf.config['env'] = "tests.conftest.DUMMY_ENV" + con = GymConnection.from_config(address="pk", connection_configuration=conf) + assert con is not None diff --git a/tests/test_connections/test_local/__init__.py b/tests/test_packages/test_connections/test_local/__init__.py similarity index 100% rename from tests/test_connections/test_local/__init__.py rename to tests/test_packages/test_connections/test_local/__init__.py diff --git a/tests/test_connections/test_local/test_misc.py b/tests/test_packages/test_connections/test_local/test_misc.py similarity index 88% rename from tests/test_connections/test_local/test_misc.py rename to tests/test_packages/test_connections/test_local/test_misc.py index 8513d2f91a..560dfbc14e 100644 --- a/tests/test_connections/test_local/test_misc.py +++ b/tests/test_packages/test_connections/test_local/test_misc.py @@ -23,12 +23,12 @@ import pytest -from aea.connections.local.connection import LocalNode, OEFLocalConnection from aea.mail.base import Envelope, AEAConnectionError, Multiplexer from aea.protocols.default.message import DefaultMessage from aea.protocols.default.serialization import DefaultSerializer -from aea.protocols.fipa.message import FIPAMessage -from aea.protocols.fipa.serialization import FIPASerializer +from packages.connections.local.connection import LocalNode, OEFLocalConnection +from packages.protocols.fipa.message import FIPAMessage +from packages.protocols.fipa.serialization import FIPASerializer def test_connection(): @@ -49,13 +49,13 @@ def test_connection(): async def test_connection_twice_return_none(): """Test that connecting twice works.""" with LocalNode() as node: - public_key = "public_key" - connection = OEFLocalConnection(public_key, node) + address = "address" + connection = OEFLocalConnection(address, node) await connection.connect() - await node.connect(public_key, connection._reader) + await node.connect(address, connection._reader) message = DefaultMessage(type=DefaultMessage.Type.BYTES, content=b"hello") message_bytes = DefaultSerializer().encode(message) - expected_envelope = Envelope(to=public_key, sender=public_key, protocol_id="default", message=message_bytes) + expected_envelope = Envelope(to=address, sender=address, protocol_id="default", message=message_bytes) await connection.send(expected_envelope) actual_envelope = await connection.receive() @@ -69,8 +69,8 @@ async def test_receiving_when_not_connected_raise_exception(): """Test that when we try to receive an envelope from a not connected connection we raise exception.""" with pytest.raises(AEAConnectionError, match="Connection not established yet."): with LocalNode() as node: - public_key = "public_key" - connection = OEFLocalConnection(public_key, node) + address = "address" + connection = OEFLocalConnection(address, node) await connection.receive() @@ -78,8 +78,8 @@ async def test_receiving_when_not_connected_raise_exception(): async def test_receiving_returns_none_when_error_occurs(): """Test that when we try to receive an envelope and an error occurs we return None.""" with LocalNode() as node: - public_key = "public_key" - connection = OEFLocalConnection(public_key, node) + address = "address" + connection = OEFLocalConnection(address, node) await connection.connect() with unittest.mock.patch.object(connection._reader, "get", side_effect=Exception): @@ -152,10 +152,10 @@ def test_communication(): async def test_connecting_to_node_with_same_key(): """Test that connecting twice with the same key works correctly.""" with LocalNode() as node: - public_key = "my_public_key" + address = "my_address" my_queue = asyncio.Queue() - ret = await node.connect(public_key, my_queue) + ret = await node.connect(address, my_queue) assert ret is not None and isinstance(ret, asyncio.Queue) - ret = await node.connect(public_key, my_queue) + ret = await node.connect(address, my_queue) assert ret is None diff --git a/tests/test_connections/test_local/test_search_services.py b/tests/test_packages/test_connections/test_local/test_search_services.py similarity index 69% rename from tests/test_connections/test_local/test_search_services.py rename to tests/test_packages/test_connections/test_local/test_search_services.py index 3aea443d7b..ffe100aae3 100644 --- a/tests/test_connections/test_local/test_search_services.py +++ b/tests/test_packages/test_connections/test_local/test_search_services.py @@ -23,13 +23,13 @@ import pytest from aea.configurations.base import ConnectionConfig -from aea.connections.local.connection import LocalNode, OEFLocalConnection +from aea.helpers.search.models import Query, DataModel, Description, Constraint, ConstraintType from aea.mail.base import Envelope, AEAConnectionError, Multiplexer, InBox -from aea.protocols.fipa.message import FIPAMessage -from aea.protocols.fipa.serialization import FIPASerializer -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.models import Query, DataModel, Description, Constraint, ConstraintType -from aea.protocols.oef.serialization import DEFAULT_OEF, OEFSerializer +from packages.connections.local.connection import LocalNode, OEFLocalConnection +from packages.protocols.fipa.message import FIPAMessage +from packages.protocols.fipa.serialization import FIPASerializer +from packages.protocols.oef.message import OEFMessage +from packages.protocols.oef.serialization import DEFAULT_OEF, OEFSerializer class TestEmptySearch: @@ -41,8 +41,8 @@ def setup_class(cls): cls.node = LocalNode() cls.node.start() - cls.public_key_1 = "public_key_1" - cls.multiplexer = Multiplexer([OEFLocalConnection(cls.public_key_1, cls.node)]) + cls.address_1 = "address_1" + cls.multiplexer = Multiplexer([OEFLocalConnection(cls.address_1, cls.node)]) cls.multiplexer.connect() @@ -52,25 +52,25 @@ def test_empty_search_result(self): query = Query(constraints=[], model=None) # build and send the request - search_services_request = OEFMessage(oef_type=OEFMessage.Type.SEARCH_SERVICES, id=request_id, query=query) + search_services_request = OEFMessage(type=OEFMessage.Type.SEARCH_SERVICES, id=request_id, query=query) msg_bytes = OEFSerializer().encode(search_services_request) - envelope = Envelope(to=DEFAULT_OEF, sender=self.public_key_1, protocol_id=OEFMessage.protocol_id, + envelope = Envelope(to=DEFAULT_OEF, sender=self.address_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) self.multiplexer.put(envelope) # check the result response_envelope = self.multiplexer.get(block=True, timeout=2.0) assert response_envelope.protocol_id == OEFMessage.protocol_id - assert response_envelope.to == self.public_key_1 + assert response_envelope.to == self.address_1 assert response_envelope.sender == DEFAULT_OEF search_result = OEFSerializer().decode(response_envelope.message) assert search_result.get("type") == OEFMessage.Type.SEARCH_RESULT assert search_result.get("agents") == [] # build and send the request - search_agents_request = OEFMessage(oef_type=OEFMessage.Type.SEARCH_AGENTS, id=request_id, query=query) + search_agents_request = OEFMessage(type=OEFMessage.Type.SEARCH_AGENTS, id=request_id, query=query) msg_bytes = OEFSerializer().encode(search_agents_request) - envelope = Envelope(to=DEFAULT_OEF, sender=self.public_key_1, protocol_id=OEFMessage.protocol_id, + envelope = Envelope(to=DEFAULT_OEF, sender=self.address_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) self.multiplexer.put(envelope) @@ -98,8 +98,8 @@ def setup_class(cls): cls.node = LocalNode() cls.node.start() - cls.public_key_1 = "public_key" - cls.multiplexer = Multiplexer([OEFLocalConnection(cls.public_key_1, cls.node)]) + cls.address_1 = "address" + cls.multiplexer = Multiplexer([OEFLocalConnection(cls.address_1, cls.node)]) cls.multiplexer.connect() @@ -108,10 +108,10 @@ def setup_class(cls): service_id = '' cls.data_model = DataModel("foobar", attributes=[]) service_description = Description({"foo": 1, "bar": "baz"}, data_model=cls.data_model) - register_service_request = OEFMessage(oef_type=OEFMessage.Type.REGISTER_SERVICE, id=request_id, + register_service_request = OEFMessage(type=OEFMessage.Type.REGISTER_SERVICE, id=request_id, service_description=service_description, service_id=service_id) msg_bytes = OEFSerializer().encode(register_service_request) - envelope = Envelope(to=DEFAULT_OEF, sender=cls.public_key_1, protocol_id=OEFMessage.protocol_id, + envelope = Envelope(to=DEFAULT_OEF, sender=cls.address_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) cls.multiplexer.put(envelope) @@ -121,20 +121,20 @@ def test_not_empty_search_result(self): query = Query(constraints=[], model=self.data_model) # build and send the request - search_services_request = OEFMessage(oef_type=OEFMessage.Type.SEARCH_SERVICES, id=request_id, query=query) + search_services_request = OEFMessage(type=OEFMessage.Type.SEARCH_SERVICES, id=request_id, query=query) msg_bytes = OEFSerializer().encode(search_services_request) - envelope = Envelope(to=DEFAULT_OEF, sender=self.public_key_1, protocol_id=OEFMessage.protocol_id, + envelope = Envelope(to=DEFAULT_OEF, sender=self.address_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) self.multiplexer.put(envelope) # check the result response_envelope = self.multiplexer.get(block=True, timeout=2.0) assert response_envelope.protocol_id == OEFMessage.protocol_id - assert response_envelope.to == self.public_key_1 + assert response_envelope.to == self.address_1 assert response_envelope.sender == DEFAULT_OEF search_result = OEFSerializer().decode(response_envelope.message) assert search_result.get("type") == OEFMessage.Type.SEARCH_RESULT - assert search_result.get("agents") == [self.public_key_1] + assert search_result.get("agents") == [self.address_1] @classmethod def teardown_class(cls): @@ -152,10 +152,10 @@ def setup_class(cls): cls.node = LocalNode() cls.node.start() - cls.public_key_1 = "public_key_1" - cls.multiplexer1 = Multiplexer([OEFLocalConnection(cls.public_key_1, cls.node)]) - cls.public_key_2 = "public_key_2" - cls.multiplexer2 = Multiplexer([OEFLocalConnection(cls.public_key_2, cls.node)]) + cls.address_1 = "address_1" + cls.multiplexer1 = Multiplexer([OEFLocalConnection(cls.address_1, cls.node)]) + cls.address_2 = "address_2" + cls.multiplexer2 = Multiplexer([OEFLocalConnection(cls.address_2, cls.node)]) cls.multiplexer1.connect() cls.multiplexer2.connect() @@ -163,10 +163,10 @@ def test_unregister_service_result(self): """Test that at the beginning, the search request returns an empty search result.""" data_model = DataModel("foobar", attributes=[]) service_description = Description({"foo": 1, "bar": "baz"}, data_model=data_model) - msg = OEFMessage(oef_type=OEFMessage.Type.UNREGISTER_SERVICE, id=0, service_description=service_description, + msg = OEFMessage(type=OEFMessage.Type.UNREGISTER_SERVICE, id=0, service_description=service_description, service_id="Test_service") msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=DEFAULT_OEF, sender=self.public_key_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=DEFAULT_OEF, sender=self.address_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) self.multiplexer1.put(envelope) # check the result @@ -176,16 +176,16 @@ def test_unregister_service_result(self): result = OEFSerializer().decode(response_envelope.message) assert result.get("type") == OEFMessage.Type.OEF_ERROR - msg = OEFMessage(oef_type=OEFMessage.Type.REGISTER_SERVICE, id=0, service_description=service_description, + msg = OEFMessage(type=OEFMessage.Type.REGISTER_SERVICE, id=0, service_description=service_description, service_id="Test_Service") msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=DEFAULT_OEF, sender=self.public_key_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=DEFAULT_OEF, sender=self.address_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) self.multiplexer1.put(envelope) # Search for the register agent - msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_AGENTS, id=0, query=Query([Constraint("foo", ConstraintType("==", 1))])) + msg = OEFMessage(type=OEFMessage.Type.SEARCH_AGENTS, id=0, query=Query([Constraint("foo", ConstraintType("==", 1))])) msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=DEFAULT_OEF, sender=self.public_key_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=DEFAULT_OEF, sender=self.address_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) self.multiplexer1.put(envelope) # check the result response_envelope = self.multiplexer1.get(block=True, timeout=5.0) @@ -198,17 +198,17 @@ def test_unregister_service_result(self): # unregister the service data_model = DataModel("foobar", attributes=[]) service_description = Description({"foo": 1, "bar": "baz"}, data_model=data_model) - msg = OEFMessage(oef_type=OEFMessage.Type.UNREGISTER_SERVICE, id=0, service_description=service_description, + msg = OEFMessage(type=OEFMessage.Type.UNREGISTER_SERVICE, id=0, service_description=service_description, service_id="Test_service") msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=DEFAULT_OEF, sender=self.public_key_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=DEFAULT_OEF, sender=self.address_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) self.multiplexer1.put(envelope) # the same query returns empty # Search for the register agent - msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_AGENTS, id=0, query=Query([Constraint("foo", ConstraintType("==", 1))])) + msg = OEFMessage(type=OEFMessage.Type.SEARCH_AGENTS, id=0, query=Query([Constraint("foo", ConstraintType("==", 1))])) msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=DEFAULT_OEF, sender=self.public_key_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=DEFAULT_OEF, sender=self.address_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) self.multiplexer1.put(envelope) # check the result response_envelope = self.multiplexer1.get(block=True, timeout=5.0) @@ -225,18 +225,18 @@ def test_search_agent(self): query = Query(constraints=[], model=data_model) # Register an agent - msg = OEFMessage(oef_type=OEFMessage.Type.REGISTER_AGENT, id=0, agent_description=agent_description, + msg = OEFMessage(type=OEFMessage.Type.REGISTER_AGENT, id=0, agent_description=agent_description, agent_id="Test_agent") msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=DEFAULT_OEF, sender=self.public_key_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=DEFAULT_OEF, sender=self.address_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) self.multiplexer1.put(envelope) time.sleep(0.1) # Search for the register agent - msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_AGENTS, id=0, query=query) + msg = OEFMessage(type=OEFMessage.Type.SEARCH_AGENTS, id=0, query=query) msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=DEFAULT_OEF, sender=self.public_key_2, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=DEFAULT_OEF, sender=self.address_2, protocol_id=OEFMessage.protocol_id, message=msg_bytes) self.multiplexer2.put(envelope) # check the result @@ -247,19 +247,19 @@ def test_search_agent(self): assert len(result.get("agents")) == 1, "There are registered agents!" # Send unregister message. - msg = OEFMessage(oef_type=OEFMessage.Type.UNREGISTER_AGENT, id=0, agent_description=agent_description, + msg = OEFMessage(type=OEFMessage.Type.UNREGISTER_AGENT, id=0, agent_description=agent_description, agent_id="Test_agent") msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=DEFAULT_OEF, sender=self.public_key_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=DEFAULT_OEF, sender=self.address_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) self.multiplexer1.put(envelope) time.sleep(0.1) # Trigger error message. - msg = OEFMessage(oef_type=OEFMessage.Type.UNREGISTER_AGENT, id=0, agent_description=agent_description, + msg = OEFMessage(type=OEFMessage.Type.UNREGISTER_AGENT, id=0, agent_description=agent_description, agent_id="Unknown_Agent") msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=DEFAULT_OEF, sender=self.public_key_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=DEFAULT_OEF, sender=self.address_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) self.multiplexer1.put(envelope) # check the result @@ -278,7 +278,7 @@ def teardown_class(cls): class TestAgentMessage: - """Test the the OEF will return Dialogue Error if it doesn't know the public key.""" + """Test the the OEF will return Dialogue Error if it doesn't know the agent address.""" @classmethod def setup_class(cls): @@ -286,23 +286,23 @@ def setup_class(cls): cls.node = LocalNode() cls.node.start() - cls.public_key_1 = "public_key_1" - cls.multiplexer1 = Multiplexer([OEFLocalConnection(cls.public_key_1, cls.node)]) + cls.address_1 = "address_1" + cls.multiplexer1 = Multiplexer([OEFLocalConnection(cls.address_1, cls.node)]) @pytest.mark.asyncio async def test_messages(self): """Test that at the beginning, the search request returns an empty search result.""" msg = FIPAMessage((str(0), ''), 0, 0, FIPAMessage.Performative.CFP, query=None) msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to=DEFAULT_OEF, sender=self.public_key_1, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=DEFAULT_OEF, sender=self.address_1, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) with pytest.raises(AEAConnectionError): - await OEFLocalConnection(self.public_key_1, self.node).send(envelope) + await OEFLocalConnection(self.address_1, self.node).send(envelope) self.multiplexer1.connect() msg = FIPAMessage((str(0), str(1)), 0, 0, FIPAMessage.Performative.CFP, query=None) msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to="this_public_key_does_not_exist", - sender=self.public_key_1, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to="this_address_does_not_exist", + sender=self.address_1, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) self.multiplexer1.put(envelope) # check the result @@ -327,11 +327,11 @@ def setup_class(cls): """Set up the test.""" cls.node = LocalNode() cls.node.start() - cls.public_key_1 = "public_key_1" + cls.address_1 = "address_1" def test_from_config(self): """Test the configuration loading.""" - con = OEFLocalConnection.from_config(public_key="pk", connection_configuration=ConnectionConfig()) + con = OEFLocalConnection.from_config(address="pk", connection_configuration=ConnectionConfig()) assert not con.connection_status.is_connected, "We are connected..." @classmethod @@ -349,10 +349,10 @@ def setup_class(cls): cls.node = LocalNode() cls.node.start() - cls.public_key_1 = "multiplexer1" - cls.public_key_2 = "multiplexer2" - cls.multiplexer1 = Multiplexer([OEFLocalConnection(cls.public_key_1, cls.node)]) - cls.multiplexer2 = Multiplexer([OEFLocalConnection(cls.public_key_2, cls.node)]) + cls.address_1 = "multiplexer1" + cls.address_2 = "multiplexer2" + cls.multiplexer1 = Multiplexer([OEFLocalConnection(cls.address_1, cls.node)]) + cls.multiplexer2 = Multiplexer([OEFLocalConnection(cls.address_2, cls.node)]) cls.multiplexer1.connect() cls.multiplexer2.connect() @@ -361,10 +361,10 @@ def setup_class(cls): service_id = '' cls.data_model_foobar = DataModel("foobar", attributes=[]) service_description = Description({"foo": 1, "bar": "baz"}, data_model=cls.data_model_foobar) - register_service_request = OEFMessage(oef_type=OEFMessage.Type.REGISTER_SERVICE, id=request_id, + register_service_request = OEFMessage(type=OEFMessage.Type.REGISTER_SERVICE, id=request_id, service_description=service_description, service_id=service_id) msg_bytes = OEFSerializer().encode(register_service_request) - envelope = Envelope(to=DEFAULT_OEF, sender=cls.public_key_1, protocol_id=OEFMessage.protocol_id, + envelope = Envelope(to=DEFAULT_OEF, sender=cls.address_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) cls.multiplexer1.put(envelope) @@ -373,20 +373,20 @@ def setup_class(cls): # register 'multiplexer2' as a service 'barfoo'. cls.data_model_barfoo = DataModel("barfoo", attributes=[]) service_description = Description({"foo": 1, "bar": "baz"}, data_model=cls.data_model_barfoo) - register_service_request = OEFMessage(oef_type=OEFMessage.Type.REGISTER_SERVICE, id=request_id, + register_service_request = OEFMessage(type=OEFMessage.Type.REGISTER_SERVICE, id=request_id, service_description=service_description, service_id=service_id) msg_bytes = OEFSerializer().encode(register_service_request) - envelope = Envelope(to=DEFAULT_OEF, sender=cls.public_key_2, protocol_id=OEFMessage.protocol_id, + envelope = Envelope(to=DEFAULT_OEF, sender=cls.address_2, protocol_id=OEFMessage.protocol_id, message=msg_bytes) cls.multiplexer2.put(envelope) # unregister multiplexer1 data_model = DataModel("foobar", attributes=[]) service_description = Description({"foo": 1, "bar": "baz"}, data_model=data_model) - msg = OEFMessage(oef_type=OEFMessage.Type.UNREGISTER_SERVICE, id=0, service_description=service_description, + msg = OEFMessage(type=OEFMessage.Type.UNREGISTER_SERVICE, id=0, service_description=service_description, service_id="Test_service") msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=DEFAULT_OEF, sender=cls.public_key_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=DEFAULT_OEF, sender=cls.address_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) cls.multiplexer1.put(envelope) def test_filtered_search_result(self): @@ -395,20 +395,20 @@ def test_filtered_search_result(self): query = Query(constraints=[], model=self.data_model_barfoo) # build and send the request - search_services_request = OEFMessage(oef_type=OEFMessage.Type.SEARCH_SERVICES, id=request_id, query=query) + search_services_request = OEFMessage(type=OEFMessage.Type.SEARCH_SERVICES, id=request_id, query=query) msg_bytes = OEFSerializer().encode(search_services_request) - envelope = Envelope(to=DEFAULT_OEF, sender=self.public_key_1, protocol_id=OEFMessage.protocol_id, + envelope = Envelope(to=DEFAULT_OEF, sender=self.address_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) self.multiplexer1.put(envelope) # check the result response_envelope = InBox(self.multiplexer1).get(block=True, timeout=5.0) assert response_envelope.protocol_id == OEFMessage.protocol_id - assert response_envelope.to == self.public_key_1 + assert response_envelope.to == self.address_1 assert response_envelope.sender == DEFAULT_OEF search_result = OEFSerializer().decode(response_envelope.message) assert search_result.get("type") == OEFMessage.Type.SEARCH_RESULT - assert search_result.get("agents") == [self.public_key_2] + assert search_result.get("agents") == [self.address_2] @classmethod def teardown_class(cls): diff --git a/tests/test_connections/test_oef/__init__.py b/tests/test_packages/test_connections/test_oef/__init__.py similarity index 100% rename from tests/test_connections/test_oef/__init__.py rename to tests/test_packages/test_connections/test_oef/__init__.py diff --git a/tests/test_connections/test_oef/test_communication.py b/tests/test_packages/test_connections/test_oef/test_communication.py similarity index 78% rename from tests/test_connections/test_oef/test_communication.py rename to tests/test_packages/test_connections/test_oef/test_communication.py index 280d9c4e8e..c2a5501e85 100644 --- a/tests/test_connections/test_oef/test_communication.py +++ b/tests/test_packages/test_connections/test_oef/test_communication.py @@ -31,23 +31,22 @@ from oef.messages import OEFErrorOperation from oef.query import ConstraintExpr -import aea +import packages from aea.configurations.base import ConnectionConfig -from aea.connections.oef.connection import OEFConnection -from aea.connections.oef.connection import OEFObjectTranslator from aea.crypto.default import DefaultCrypto from aea.crypto.wallet import Wallet +from aea.helpers.search.models import Description, DataModel, Attribute, Query, Constraint, ConstraintType, \ + ConstraintTypes from aea.mail.base import Envelope, Multiplexer from aea.protocols.default.message import DefaultMessage from aea.protocols.default.serialization import DefaultSerializer -from aea.protocols.fipa import fipa_pb2 -from aea.protocols.fipa.message import FIPAMessage -from aea.protocols.fipa.serialization import FIPASerializer -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.models import Description, DataModel, Attribute, Query, Constraint, ConstraintType, \ - ConstraintTypes -from aea.protocols.oef.serialization import DEFAULT_OEF, OEFSerializer -from ...conftest import CUR_PATH +from packages.connections.oef.connection import OEFConnection, OEFObjectTranslator +from packages.protocols.fipa import fipa_pb2 +from packages.protocols.fipa.message import FIPAMessage +from packages.protocols.fipa.serialization import FIPASerializer +from packages.protocols.oef.message import OEFMessage +from packages.protocols.oef.serialization import DEFAULT_OEF, OEFSerializer +from ....conftest import CUR_PATH logger = logging.getLogger(__name__) @@ -63,17 +62,16 @@ def _start_oef_node(self, network_node): def setup_class(cls): """Set the test up.""" cls.crypto1 = DefaultCrypto() - cls.connection = OEFConnection(cls.crypto1.public_key, oef_addr="127.0.0.1", oef_port=10000) + cls.connection = OEFConnection(cls.crypto1.address, oef_addr="127.0.0.1", oef_port=10000) cls.multiplexer = Multiplexer([cls.connection]) cls.multiplexer.connect() def test_send_message(self): """Test that a default byte message can be sent correctly.""" msg = DefaultMessage(type=DefaultMessage.Type.BYTES, content=b"hello") - self.multiplexer.put(Envelope(to=self.crypto1.public_key, sender=self.crypto1.public_key, + self.multiplexer.put(Envelope(to=self.crypto1.address, sender=self.crypto1.address, protocol_id=DefaultMessage.protocol_id, message=DefaultSerializer().encode(msg))) - recv_msg = self.multiplexer.get(block=True, timeout=3.0) assert recv_msg is not None @@ -97,7 +95,7 @@ class TestSearchServices: def setup_class(cls): """Set the test up.""" cls.crypto1 = DefaultCrypto() - cls.connection = OEFConnection(cls.crypto1.public_key, oef_addr="127.0.0.1", oef_port=10000) + cls.connection = OEFConnection(cls.crypto1.address, oef_addr="127.0.0.1", oef_port=10000) cls.multiplexer = Multiplexer([cls.connection]) cls.multiplexer.connect() @@ -108,9 +106,9 @@ def test_search_services_with_query_without_model(self): """ request_id = 1 search_query_empty_model = Query([Constraint("foo", ConstraintType("==", "bar"))], model=None) - search_request = OEFMessage(oef_type=OEFMessage.Type.SEARCH_SERVICES, id=request_id, + search_request = OEFMessage(type=OEFMessage.Type.SEARCH_SERVICES, id=request_id, query=search_query_empty_model) - self.multiplexer.put(Envelope(to=DEFAULT_OEF, sender=self.crypto1.public_key, + self.multiplexer.put(Envelope(to=DEFAULT_OEF, sender=self.crypto1.address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(search_request))) @@ -128,8 +126,8 @@ def test_search_services_with_query_with_model(self): request_id = 2 data_model = DataModel("foobar", [Attribute("foo", str, True)]) search_query = Query([Constraint("foo", ConstraintType("==", "bar"))], model=data_model) - search_request = OEFMessage(oef_type=OEFMessage.Type.SEARCH_SERVICES, id=request_id, query=search_query) - self.multiplexer.put(Envelope(to=DEFAULT_OEF, sender=self.crypto1.public_key, + search_request = OEFMessage(type=OEFMessage.Type.SEARCH_SERVICES, id=request_id, query=search_query) + self.multiplexer.put(Envelope(to=DEFAULT_OEF, sender=self.crypto1.address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(search_request))) @@ -151,7 +149,7 @@ class TestRegisterService: def setup_class(cls): """Set the test up.""" cls.crypto1 = DefaultCrypto() - cls.connection = OEFConnection(cls.crypto1.public_key, oef_addr="127.0.0.1", oef_port=10000) + cls.connection = OEFConnection(cls.crypto1.address, oef_addr="127.0.0.1", oef_port=10000) cls.multiplexer = Multiplexer([cls.connection]) cls.multiplexer.connect() @@ -159,23 +157,23 @@ def test_register_service(self): """Test that a register service request works correctly.""" foo_datamodel = DataModel("foo", [Attribute("bar", int, True, "A bar attribute.")]) desc = Description({"bar": 1}, data_model=foo_datamodel) - msg = OEFMessage(oef_type=OEFMessage.Type.REGISTER_SERVICE, id=1, service_description=desc, service_id="") + msg = OEFMessage(type=OEFMessage.Type.REGISTER_SERVICE, id=1, service_description=desc, service_id="") msg_bytes = OEFSerializer().encode(msg) - self.multiplexer.put(Envelope(to=DEFAULT_OEF, sender=self.crypto1.public_key, + self.multiplexer.put(Envelope(to=DEFAULT_OEF, sender=self.crypto1.address, protocol_id=OEFMessage.protocol_id, message=msg_bytes)) time.sleep(0.5) - search_request = OEFMessage(oef_type=OEFMessage.Type.SEARCH_SERVICES, id=2, + search_request = OEFMessage(type=OEFMessage.Type.SEARCH_SERVICES, id=2, query=Query([Constraint("bar", ConstraintType("==", 1))], model=foo_datamodel)) - self.multiplexer.put(Envelope(to=DEFAULT_OEF, sender=self.crypto1.public_key, + self.multiplexer.put(Envelope(to=DEFAULT_OEF, sender=self.crypto1.address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(search_request))) envelope = self.multiplexer.get(block=True, timeout=5.0) search_result = OEFSerializer().decode(envelope.message) assert search_result.get("type") == OEFMessage.Type.SEARCH_RESULT assert search_result.get("id") == 2 - if search_result.get("agents") != [self.crypto1.public_key]: - logger.warning('search_result.get("agents") != [self.crypto1.public_key] FAILED in test_oef/test_communication.py') + if search_result.get("agents") != [self.crypto1.address]: + logger.warning('search_result.get("agents") != [self.crypto1.address] FAILED in test_oef/test_communication.py') @classmethod def teardown_class(cls): @@ -195,35 +193,35 @@ def setup_class(cls): - Check that the registration worked. """ cls.crypto1 = DefaultCrypto() - cls.connection = OEFConnection(cls.crypto1.public_key, oef_addr="127.0.0.1", oef_port=10000) + cls.connection = OEFConnection(cls.crypto1.address, oef_addr="127.0.0.1", oef_port=10000) cls.multiplexer = Multiplexer([cls.connection]) cls.multiplexer.connect() cls.request_id = 1 cls.foo_datamodel = DataModel("foo", [Attribute("bar", int, True, "A bar attribute.")]) cls.desc = Description({"bar": 1}, data_model=cls.foo_datamodel) - msg = OEFMessage(oef_type=OEFMessage.Type.REGISTER_SERVICE, id=cls.request_id, service_description=cls.desc, + msg = OEFMessage(type=OEFMessage.Type.REGISTER_SERVICE, id=cls.request_id, service_description=cls.desc, service_id="") msg_bytes = OEFSerializer().encode(msg) - cls.multiplexer.put(Envelope(to=DEFAULT_OEF, sender=cls.crypto1.public_key, + cls.multiplexer.put(Envelope(to=DEFAULT_OEF, sender=cls.crypto1.address, protocol_id=OEFMessage.protocol_id, message=msg_bytes)) time.sleep(1.0) cls.request_id += 1 - search_request = OEFMessage(oef_type=OEFMessage.Type.SEARCH_SERVICES, id=cls.request_id, + search_request = OEFMessage(type=OEFMessage.Type.SEARCH_SERVICES, id=cls.request_id, query=Query([Constraint("bar", ConstraintType("==", 1))], model=cls.foo_datamodel)) - cls.multiplexer.put(Envelope(to=DEFAULT_OEF, sender=cls.crypto1.public_key, + cls.multiplexer.put(Envelope(to=DEFAULT_OEF, sender=cls.crypto1.address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(search_request))) envelope = cls.multiplexer.get(block=True, timeout=5.0) search_result = OEFSerializer().decode(envelope.message) assert search_result.get("type") == OEFMessage.Type.SEARCH_RESULT assert search_result.get("id") == cls.request_id - if search_result.get("agents") != [cls.crypto1.public_key]: + if search_result.get("agents") != [cls.crypto1.address]: logger.warning( - 'search_result.get("agents") != [self.crypto1.public_key] FAILED in test_oef/test_communication.py') + 'search_result.get("agents") != [self.crypto1.address] FAILED in test_oef/test_communication.py') def test_unregister_service(self): """Test that an unregister service request works correctly. @@ -234,19 +232,19 @@ def test_unregister_service(self): 4. assert that no result is found. """ self.request_id += 1 - msg = OEFMessage(oef_type=OEFMessage.Type.UNREGISTER_SERVICE, id=self.request_id, + msg = OEFMessage(type=OEFMessage.Type.UNREGISTER_SERVICE, id=self.request_id, service_description=self.desc, service_id="") msg_bytes = OEFSerializer().encode(msg) - self.multiplexer.put(Envelope(to=DEFAULT_OEF, sender=self.crypto1.public_key, + self.multiplexer.put(Envelope(to=DEFAULT_OEF, sender=self.crypto1.address, protocol_id=OEFMessage.protocol_id, message=msg_bytes)) time.sleep(1.0) self.request_id += 1 - search_request = OEFMessage(oef_type=OEFMessage.Type.SEARCH_SERVICES, id=self.request_id, + search_request = OEFMessage(type=OEFMessage.Type.SEARCH_SERVICES, id=self.request_id, query=Query([Constraint("bar", ConstraintType("==", 1))], model=self.foo_datamodel)) - self.multiplexer.put(Envelope(to=DEFAULT_OEF, sender=self.crypto1.public_key, + self.multiplexer.put(Envelope(to=DEFAULT_OEF, sender=self.crypto1.address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(search_request))) @@ -268,7 +266,7 @@ class TestMailStats: def setup_class(cls): """Set the tests up.""" cls.crypto1 = DefaultCrypto() - cls.connection = OEFConnection(cls.crypto1.public_key, oef_addr="127.0.0.1", oef_port=10000) + cls.connection = OEFConnection(cls.crypto1.address, oef_addr="127.0.0.1", oef_port=10000) cls.multiplexer = Multiplexer([cls.connection]) cls.multiplexer.connect() @@ -278,9 +276,9 @@ def test_search_count_increases(self): """Test that the search count increases.""" request_id = 1 search_query_empty_model = Query([Constraint("foo", ConstraintType("==", "bar"))], model=None) - search_request = OEFMessage(oef_type=OEFMessage.Type.SEARCH_SERVICES, id=request_id, + search_request = OEFMessage(type=OEFMessage.Type.SEARCH_SERVICES, id=request_id, query=search_query_empty_model) - self.multiplexer.put(Envelope(to=DEFAULT_OEF, sender=self.crypto1.public_key, + self.multiplexer.put(Envelope(to=DEFAULT_OEF, sender=self.crypto1.address, protocol_id=OEFMessage.protocol_id, message=OEFSerializer().encode(search_request))) @@ -309,8 +307,8 @@ def setup_class(cls): """Set up the test class.""" cls.crypto1 = DefaultCrypto() cls.crypto2 = DefaultCrypto() - cls.connection1 = OEFConnection(cls.crypto1.public_key, oef_addr="127.0.0.1", oef_port=10000) - cls.connection2 = OEFConnection(cls.crypto2.public_key, oef_addr="127.0.0.1", oef_port=10000) + cls.connection1 = OEFConnection(cls.crypto1.address, oef_addr="127.0.0.1", oef_port=10000) + cls.connection2 = OEFConnection(cls.crypto2.address, oef_addr="127.0.0.1", oef_port=10000) cls.multiplexer1 = Multiplexer([cls.connection1]) cls.multiplexer2 = Multiplexer([cls.connection2]) cls.multiplexer1.connect() @@ -318,32 +316,39 @@ def setup_class(cls): def test_cfp(self): """Test that a CFP can be sent correctly.""" - cfp_bytes = FIPAMessage(message_id=0, dialogue_reference=(str(0), ''), target=0, performative=FIPAMessage.Performative.CFP, - query=Query([Constraint('something', ConstraintType('>', 1))])) - self.multiplexer1.put(Envelope(to=self.crypto2.public_key, sender=self.crypto1.public_key, + cfp_message = FIPAMessage(message_id=0, dialogue_reference=(str(0), ''), target=0, performative=FIPAMessage.Performative.CFP, + query=Query([Constraint('something', ConstraintType('>', 1))])) + cfp_message.counterparty = self.crypto2.address + self.multiplexer1.put(Envelope(to=self.crypto2.address, sender=self.crypto1.address, protocol_id=FIPAMessage.protocol_id, - message=FIPASerializer().encode(cfp_bytes))) + message=FIPASerializer().encode(cfp_message))) envelope = self.multiplexer2.get(block=True, timeout=5.0) - expected_cfp_bytes = FIPASerializer().decode(envelope.message) - assert expected_cfp_bytes == cfp_bytes + expected_cfp_message = FIPASerializer().decode(envelope.message) + expected_cfp_message.counterparty = self.crypto2.address + + assert expected_cfp_message == cfp_message cfp_none = FIPAMessage(message_id=0, dialogue_reference=(str(0), ''), target=0, performative=FIPAMessage.Performative.CFP, query=None) - self.multiplexer1.put(Envelope(to=self.crypto2.public_key, sender=self.crypto1.public_key, + cfp_none.counterparty = self.crypto2.address + self.multiplexer1.put(Envelope(to=self.crypto2.address, sender=self.crypto1.address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(cfp_none))) envelope = self.multiplexer2.get(block=True, timeout=5.0) expected_cfp_none = FIPASerializer().decode(envelope.message) + expected_cfp_none.counterparty = self.crypto2.address assert expected_cfp_none == cfp_none def test_propose(self): """Test that a Propose can be sent correctly.""" propose_empty = FIPAMessage(message_id=0, dialogue_reference=(str(0), ''), target=0, performative=FIPAMessage.Performative.PROPOSE, proposal=[]) - self.multiplexer1.put(Envelope(to=self.crypto2.public_key, sender=self.crypto1.public_key, + propose_empty.counterparty = self.crypto2.address + self.multiplexer1.put(Envelope(to=self.crypto2.address, sender=self.crypto1.address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(propose_empty))) envelope = self.multiplexer2.get(block=True, timeout=2.0) expected_propose_empty = FIPASerializer().decode(envelope.message) + expected_propose_empty.counterparty = self.crypto2.address assert expected_propose_empty == propose_empty propose_descriptions = FIPAMessage(message_id=0, @@ -351,20 +356,25 @@ def test_propose(self): target=0, performative=FIPAMessage.Performative.PROPOSE, proposal=[Description({"foo": "bar"}, DataModel("foobar", [Attribute("foo", str, True)]))]) - self.multiplexer1.put(Envelope(to=self.crypto2.public_key, sender=self.crypto1.public_key, + + propose_descriptions.counterparty = self.crypto2.address + self.multiplexer1.put(Envelope(to=self.crypto2.address, sender=self.crypto1.address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(propose_descriptions))) envelope = self.multiplexer2.get(block=True, timeout=2.0) expected_propose_descriptions = FIPASerializer().decode(envelope.message) + expected_propose_descriptions.counterparty = self.crypto2.address assert expected_propose_descriptions == propose_descriptions def test_accept(self): """Test that an Accept can be sent correctly.""" accept = FIPAMessage(message_id=0, dialogue_reference=(str(0), ''), target=0, performative=FIPAMessage.Performative.ACCEPT) - self.multiplexer1.put(Envelope(to=self.crypto2.public_key, sender=self.crypto1.public_key, + accept.counterparty = self.crypto2.address + self.multiplexer1.put(Envelope(to=self.crypto2.address, sender=self.crypto1.address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(accept))) envelope = self.multiplexer2.get(block=True, timeout=2.0) expected_accept = FIPASerializer().decode(envelope.message) + expected_accept.counterparty = self.crypto2.address assert expected_accept == accept def test_match_accept(self): @@ -372,51 +382,59 @@ def test_match_accept(self): # NOTE since the OEF SDK doesn't support the match accept, we have to use a fixed message id! match_accept = FIPAMessage(message_id=4, dialogue_reference=(str(0), ''), target=3, performative=FIPAMessage.Performative.MATCH_ACCEPT) - self.multiplexer1.put(Envelope(to=self.crypto2.public_key, sender=self.crypto1.public_key, + match_accept.counterparty = self.crypto2.address + self.multiplexer1.put(Envelope(to=self.crypto2.address, sender=self.crypto1.address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(match_accept))) envelope = self.multiplexer2.get(block=True, timeout=2.0) expected_match_accept = FIPASerializer().decode(envelope.message) + expected_match_accept.counterparty = self.crypto2.address assert expected_match_accept == match_accept def test_decline(self): """Test that a Decline can be sent correctly.""" decline = FIPAMessage(message_id=0, dialogue_reference=(str(0), ''), target=0, performative=FIPAMessage.Performative.DECLINE) - self.multiplexer1.put(Envelope(to=self.crypto2.public_key, sender=self.crypto1.public_key, + decline.counterparty = self.crypto2.address + self.multiplexer1.put(Envelope(to=self.crypto2.address, sender=self.crypto1.address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(decline))) envelope = self.multiplexer2.get(block=True, timeout=2.0) expected_decline = FIPASerializer().decode(envelope.message) + expected_decline.counterparty = self.crypto2.address assert expected_decline == decline - def test_match_accept_w_address(self): - """Test that a match accept with address can be sent correctly.""" - match_accept_w_address = FIPAMessage(message_id=0, - dialogue_reference=(str(0), ''), - target=0, - performative=FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS, - address='my_address') - self.multiplexer1.put(Envelope(to=self.crypto2.public_key, - sender=self.crypto1.public_key, + def test_match_accept_w_inform(self): + """Test that a match accept with inform can be sent correctly.""" + match_accept_w_inform = FIPAMessage(message_id=0, + dialogue_reference=(str(0), ''), + target=0, + performative=FIPAMessage.Performative.MATCH_ACCEPT_W_INFORM, + info={"address": "my_address"}) + match_accept_w_inform.counterparty = self.crypto2.address + self.multiplexer1.put(Envelope(to=self.crypto2.address, + sender=self.crypto1.address, protocol_id=FIPAMessage.protocol_id, - message=FIPASerializer().encode(match_accept_w_address))) + message=FIPASerializer().encode(match_accept_w_inform))) envelope = self.multiplexer2.get(block=True, timeout=2.0) - returned_match_accept_w_address = FIPASerializer().decode(envelope.message) - assert returned_match_accept_w_address == match_accept_w_address + returned_match_accept_w_inform = FIPASerializer().decode(envelope.message) + returned_match_accept_w_inform.counterparty = self.crypto2.address + assert returned_match_accept_w_inform == match_accept_w_inform - def test_accept_w_address(self): + def test_accept_w_inform(self): """Test that an accept with address can be sent correctly.""" - accept_w_address = FIPAMessage(message_id=0, - dialogue_reference=(str(0), ''), - target=0, - performative=FIPAMessage.Performative.ACCEPT_W_ADDRESS, - address='my_address') - self.multiplexer1.put(Envelope(to=self.crypto2.public_key, - sender=self.crypto1.public_key, + accept_w_inform = FIPAMessage(message_id=0, + dialogue_reference=(str(0), ''), + target=0, + performative=FIPAMessage.Performative.ACCEPT_W_INFORM, + info={"address": "my_address"}) + accept_w_inform.counterparty = self.crypto2.address + self.multiplexer1.put(Envelope(to=self.crypto2.address, + sender=self.crypto1.address, protocol_id=FIPAMessage.protocol_id, - message=FIPASerializer().encode(accept_w_address))) + message=FIPASerializer().encode(accept_w_inform))) envelope = self.multiplexer2.get(block=True, timeout=2.0) - returned_accept_w_address = FIPASerializer().decode(envelope.message) - assert returned_accept_w_address == accept_w_address + returned_accept_w_inform = FIPASerializer().decode(envelope.message) + returned_accept_w_inform.counterparty = self.crypto2.address + assert returned_accept_w_inform == accept_w_inform def test_inform(self): """Test that an inform can be sent correctly.""" @@ -425,13 +443,15 @@ def test_inform(self): dialogue_reference=(str(0), ''), target=0, performative=FIPAMessage.Performative.INFORM, - json_data=payload) - self.multiplexer1.put(Envelope(to=self.crypto2.public_key, - sender=self.crypto1.public_key, + info=payload) + inform.counterparty = self.crypto2.address + self.multiplexer1.put(Envelope(to=self.crypto2.address, + sender=self.crypto1.address, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(inform))) envelope = self.multiplexer2.get(block=True, timeout=2.0) returned_inform = FIPASerializer().decode(envelope.message) + returned_inform.counterparty = self.crypto2.address assert returned_inform == inform def test_serialisation_fipa(self): @@ -443,7 +463,7 @@ def test_serialisation_fipa(self): dialogue_reference=(str(0), ''), target=1, query=None) - with mock.patch("aea.protocols.fipa.message.FIPAMessage.Performative") as mock_performative_enum: + with mock.patch("packages.protocols.fipa.message.FIPAMessage.Performative") as mock_performative_enum: mock_performative_enum.CFP.value = "unknown" FIPASerializer().encode(msg), "Raises Value Error" with pytest.raises(ValueError): @@ -475,7 +495,7 @@ def test_serialisation_fipa(self): target=0, performative=FIPAMessage.Performative.CFP, query=b"hello") - with mock.patch("aea.protocols.fipa.message.FIPAMessage.Performative") as mock_performative_enum: + with mock.patch("packages.protocols.fipa.message.FIPAMessage.Performative") as mock_performative_enum: mock_performative_enum.CFP.value = "unknown" fipa_msg = fipa_pb2.FIPAMessage() fipa_msg.message_id = cfp_msg.get("message_id") @@ -512,12 +532,10 @@ def test_on_dialogue_error(self): def test_send(self): """Test the send method.""" - envelope = Envelope(to="receiver", sender="me", protocol_id="tac", message=b'Hello') - self.multiplexer1.put(envelope) - self.multiplexer1.get(block=True, timeout=5.0) - - envelope = Envelope(to="receiver", sender="me", protocol_id="unknown", message=b'Hello') + envelope = Envelope(to=DEFAULT_OEF, sender="me", protocol_id="tac", message=b'Hello') self.multiplexer1.put(envelope) + received_envelope = self.multiplexer1.get(block=True, timeout=5.0) + assert received_envelope is not None @classmethod def teardown_class(cls): @@ -536,7 +554,7 @@ def _start_oef_node(self, network_node): def test_connection(self): """Test that an OEF connection can be established to the OEF.""" crypto = DefaultCrypto() - connection = OEFConnection(crypto.public_key, oef_addr="127.0.0.1", oef_port=10000) + connection = OEFConnection(crypto.address, oef_addr="127.0.0.1", oef_port=10000) multiplexer = Multiplexer([connection]) multiplexer.connect() multiplexer.disconnect() @@ -545,14 +563,14 @@ def test_connection(self): # @pytest.mark.asyncio # async def test_oef_connect(self): # """Test the OEFConnection.""" - # con = OEFConnection(public_key="pk", oef_addr="this_is_not_an_address") + # con = OEFConnection(address="pk", oef_addr="this_is_not_an_address") # assert not con.connection_status.is_connected # with pytest.raises(ConnectionError): # await con.connect() def test_oef_from_config(self): """Test the Connection from config File.""" - con = OEFConnection.from_config(public_key="pk", connection_configuration=ConnectionConfig()) + con = OEFConnection.from_config(address="pk", connection_configuration=ConnectionConfig()) assert not con.connection_status.is_connected, "We are connected..." @@ -679,24 +697,23 @@ async def test_send_oef_message(network_node): """Test the send oef message.""" private_key_pem_path = os.path.join(CUR_PATH, "data", "priv.pem") wallet = Wallet({'default': private_key_pem_path}) - public_key = wallet.public_keys['default'] - oef_connection = OEFConnection(public_key=public_key, oef_addr="127.0.0.1", oef_port=10000) + address = wallet.addresses['default'] + oef_connection = OEFConnection(address=address, oef_addr="127.0.0.1", oef_port=10000) oef_connection.loop = asyncio.get_event_loop() await oef_connection.connect() - - msg = OEFMessage(oef_type=OEFMessage.Type.OEF_ERROR, id=0, + msg = OEFMessage(type=OEFMessage.Type.OEF_ERROR, id=0, operation=OEFMessage.OEFErrorOperation.SEARCH_AGENTS) msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to=DEFAULT_OEF, sender=public_key, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=DEFAULT_OEF, sender=address, protocol_id=OEFMessage.protocol_id, message=msg_bytes) with pytest.raises(ValueError): await oef_connection.send(envelope) data_model = DataModel("foobar", attributes=[]) query = Query(constraints=[], model=data_model) - msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_AGENTS, id=0, query=query) + msg = OEFMessage(type=OEFMessage.Type.SEARCH_AGENTS, id=0, query=query) msg_bytes = OEFSerializer().encode(msg) - envelope = Envelope(to="recipient", sender=public_key, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + envelope = Envelope(to=DEFAULT_OEF, sender=address, protocol_id=OEFMessage.protocol_id, message=msg_bytes) await oef_connection.send(envelope) await oef_connection.disconnect() @@ -706,11 +723,11 @@ async def test_cancelled_receive(network_node): """Test the case when a receive request is cancelled.""" private_key_pem_path = os.path.join(CUR_PATH, "data", "priv.pem") wallet = Wallet({'default': private_key_pem_path}) - oef_connection = OEFConnection(public_key=wallet.public_keys['default'], oef_addr="127.0.0.1", oef_port=10000) + oef_connection = OEFConnection(address=wallet.addresses['default'], oef_addr="127.0.0.1", oef_port=10000) oef_connection.loop = asyncio.get_event_loop() await oef_connection.connect() - patch = unittest.mock.patch.object(aea.connections.oef.connection.logger, 'debug') + patch = unittest.mock.patch.object(packages.connections.oef.connection.logger, 'debug') mocked_logger_debug = patch.__enter__() async def receive(): @@ -730,7 +747,7 @@ async def test_exception_during_receive(network_node): """Test the case when there is an exception during a receive request.""" private_key_pem_path = os.path.join(CUR_PATH, "data", "priv.pem") wallet = Wallet({'default': private_key_pem_path}) - oef_connection = OEFConnection(public_key=wallet.public_keys['default'], oef_addr="127.0.0.1", oef_port=10000) + oef_connection = OEFConnection(address=wallet.addresses['default'], oef_addr="127.0.0.1", oef_port=10000) oef_connection.loop = asyncio.get_event_loop() await oef_connection.connect() @@ -747,10 +764,10 @@ async def test_cannot_connect_to_oef(): """Test the case when we can't connect to the OEF.""" private_key_pem_path = os.path.join(CUR_PATH, "data", "priv.pem") wallet = Wallet({'default': private_key_pem_path}) - oef_connection = OEFConnection(public_key=wallet.public_keys['default'], oef_addr="a_fake_address", oef_port=10000) + oef_connection = OEFConnection(address=wallet.addresses['default'], oef_addr="a_fake_address", oef_port=10000) oef_connection.loop = asyncio.get_event_loop() - patch = unittest.mock.patch.object(aea.connections.oef.connection.logger, 'warning') + patch = unittest.mock.patch.object(packages.connections.oef.connection.logger, 'warning') mocked_logger_warning = patch.__enter__() async def try_to_connect(): @@ -768,7 +785,7 @@ async def test_connecting_twice_is_ok(network_node): """Test that calling 'connect' twice works as expected.""" private_key_pem_path = os.path.join(CUR_PATH, "data", "priv.pem") wallet = Wallet({'default': private_key_pem_path}) - oef_connection = OEFConnection(public_key=wallet.public_keys['default'], oef_addr="127.0.0.1", oef_port=10000) + oef_connection = OEFConnection(address=wallet.addresses['default'], oef_addr="127.0.0.1", oef_port=10000) oef_connection.loop = asyncio.get_event_loop() assert not oef_connection.connection_status.is_connected diff --git a/tests/test_connections/test_oef/test_models.py b/tests/test_packages/test_connections/test_oef/test_models.py similarity index 97% rename from tests/test_connections/test_oef/test_models.py rename to tests/test_packages/test_connections/test_oef/test_models.py index 6f7274e720..1161067aad 100644 --- a/tests/test_connections/test_oef/test_models.py +++ b/tests/test_packages/test_connections/test_oef/test_models.py @@ -23,8 +23,8 @@ import pytest -from aea.connections.oef.connection import OEFObjectTranslator -from aea.protocols.oef.models import Attribute, DataModel, Description, Query, And, Or, Not, Constraint, ConstraintType +from aea.helpers.search.models import Attribute, DataModel, Description, Query, And, Or, Not, Constraint, ConstraintType +from packages.connections.oef.connection import OEFObjectTranslator class TestTranslator: @@ -176,7 +176,7 @@ def test_validity(self): m_constraint.check("HelloWorld") m_constraint = ConstraintType("==", 3) - with mock.patch("aea.protocols.oef.models.ConstraintTypes") as mocked_types: + with mock.patch("aea.helpers.search.models.ConstraintTypes") as mocked_types: mocked_types.EQUAL.value = "unknown" assert not m_constraint._check_validity(), "My constraint must not be valid" diff --git a/tests/test_connections/test_oef/test_oef_serializer.py b/tests/test_packages/test_connections/test_oef/test_oef_serializer.py similarity index 79% rename from tests/test_connections/test_oef/test_oef_serializer.py rename to tests/test_packages/test_connections/test_oef/test_oef_serializer.py index 52141c564b..a511190e11 100644 --- a/tests/test_connections/test_oef/test_oef_serializer.py +++ b/tests/test_packages/test_connections/test_oef/test_oef_serializer.py @@ -19,15 +19,15 @@ """This test module contains the tests for the OEF serializer.""" -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.serialization import OEFSerializer -from aea.protocols.oef.models import Attribute, DataModel, Description +from aea.helpers.search.models import Attribute, DataModel, Description +from packages.protocols.oef.message import OEFMessage +from packages.protocols.oef.serialization import OEFSerializer def test_oef_serialization(): """Testing the serialization of the OEF.""" foo_datamodel = DataModel("foo", [Attribute("bar", int, True, "A bar attribute.")]) desc = Description({"bar": 1}, data_model=foo_datamodel) - msg = OEFMessage(oef_type=OEFMessage.Type.REGISTER_SERVICE, id=1, service_description=desc, service_id="") + msg = OEFMessage(type=OEFMessage.Type.REGISTER_SERVICE, id=1, service_description=desc, service_id="") msg_bytes = OEFSerializer().encode(msg) assert len(msg_bytes) > 0 diff --git a/tests/test_packages/test_connections/test_p2p.py b/tests/test_packages/test_connections/test_p2p.py new file mode 100644 index 0000000000..829a3973e7 --- /dev/null +++ b/tests/test_packages/test_connections/test_p2p.py @@ -0,0 +1,121 @@ +# -*- 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. +# +# ------------------------------------------------------------------------------ + +"""Peer to Peer connection and channel.""" +import asyncio +import logging +from unittest import mock +from unittest.mock import MagicMock + +import fetch.p2p.api.http_calls +import pytest +from fetchai.ledger.crypto import entity + +from aea.configurations.base import ConnectionConfig +from aea.mail.base import Envelope +from packages.connections.p2p.connection import PeerToPeerConnection + +logger = logging.getLogger(__name__) + + +@pytest.mark.asyncio +class TestP2p: + """Contains the test for the p2p connection.""" + + @classmethod + def setup_class(cls): + """Initialise the class.""" + cls.address = "127.0.0.1" + cls.port = 8000 + m_fet_key = "6d56fd47e98465824aa85dfe620ad3dbf092b772abc6c6a182e458b5c56ad13b" + cls.ent = entity.Entity.from_hex(m_fet_key) + cls.p2p_connection = PeerToPeerConnection(address=cls.ent.public_key_hex, + provider_addr=cls.address, + provider_port=cls.port) + cls.p2p_connection.loop = asyncio.get_event_loop() + + async def test_initialization(self): + """Test the initialisation of the class.""" + assert self.p2p_connection.address == self.ent.public_key_hex + + async def test_connection(self): + """Test the connection and disconnection from the p2p connection.""" + with mock.patch.object(fetch.p2p.api.http_calls.HTTPCalls, "get_messages", return_value=[]): + with mock.patch.object(fetch.p2p.api.http_calls.HTTPCalls, "register", return_value={'status': 'OK'}): + await self.p2p_connection.connect() + + async def test_send(self): + """Test the send functionality of the p2p connection.""" + envelope = Envelope(to="receiver", sender="sender", protocol_id="protocol", message=b"Hello") + with mock.patch.object(fetch.p2p.api.http_calls.HTTPCalls, "get_messages", return_value=[]): + with mock.patch.object(fetch.p2p.api.http_calls.HTTPCalls, "send_message", return_value={'status': 'OK'}): + await self.p2p_connection.send(envelope=envelope) + # TODO: Consider returning the response from the server in order to be able to assert that the message send! + assert True + + async def test_disconnect(self): + """Test the connection and disconnection from the p2p connection.""" + with mock.patch.object(fetch.p2p.api.http_calls.HTTPCalls, "unregister", return_value={'status': 'OK'}): + await self.p2p_connection.disconnect() + assert self.p2p_connection.connection_status.is_connected is False + + +@pytest.mark.asyncio +async def test_p2p_receive(): + """Test receive from p2p connection.""" + address = "127.0.0.1" + port = 8000 + m_fet_key = "6d56fd47e98465824aa85dfe620ad3dbf092b772abc6c6a182e458b5c56ad13b" + ent = entity.Entity.from_hex(m_fet_key) + p2p_connection = PeerToPeerConnection(address=ent.public_key_hex, provider_addr=address, provider_port=port) + p2p_connection.loop = asyncio.get_event_loop() + + fake_get_messages_empty = MagicMock(return_value=[]) + + s_msg = {"FROM": {"NODE_ADDRESS": "node_address", + "SENDER_ADDRESS": "sender_address"}, + "TO": {"NODE_ADDRESS": "node_address", + "RECEIVER_ADDRESS": "receiver_address"}, + "PROTOCOL": "protocol", + "CONTEXT": "context", + "PAYLOAD": "payload"} + messages = [s_msg] + + with mock.patch.object(fetch.p2p.api.http_calls.HTTPCalls, "get_messages", return_value=[]): + with mock.patch.object(fetch.p2p.api.http_calls.HTTPCalls, "register", return_value={'status': 'OK'}): + await p2p_connection.connect() + assert p2p_connection.connection_status.is_connected is True + + with mock.patch.object(fetch.p2p.api.http_calls.HTTPCalls, "get_messages", return_value=messages) as mock_receive: + p2p_connection.channel._httpCall.get_messages = mock_receive + await asyncio.sleep(1.0) + envelope = await p2p_connection.receive() + assert envelope is not None + + with mock.patch.object(fetch.p2p.api.http_calls.HTTPCalls, "unregister", return_value={'status': 'OK'}): + p2p_connection.channel._httpCall.get_messages = fake_get_messages_empty + await p2p_connection.disconnect() + assert p2p_connection.connection_status.is_connected is False + + +def test_p2p_from_config(): + """Test the Connection from config File.""" + con = PeerToPeerConnection.from_config(address="pk", connection_configuration=ConnectionConfig()) + assert not con.connection_status.is_connected, "We are connected..." diff --git a/tests/test_connections/test_tcp/__init__.py b/tests/test_packages/test_connections/test_tcp/__init__.py similarity index 100% rename from tests/test_connections/test_tcp/__init__.py rename to tests/test_packages/test_connections/test_tcp/__init__.py diff --git a/tests/test_connections/test_tcp/test_base.py b/tests/test_packages/test_connections/test_tcp/test_base.py similarity index 68% rename from tests/test_connections/test_tcp/test_base.py rename to tests/test_packages/test_connections/test_tcp/test_base.py index f4ee859135..65631fbe29 100644 --- a/tests/test_connections/test_tcp/test_base.py +++ b/tests/test_packages/test_connections/test_tcp/test_base.py @@ -21,29 +21,29 @@ import asyncio from asyncio import CancelledError -import aea -import aea.connections.tcp.base +import packages +# import packages.connections.tcp.base import pytest import unittest.mock -from aea.connections.tcp.tcp_client import TCPClientConnection -from aea.connections.tcp.tcp_server import TCPServerConnection from aea.mail.base import Envelope -from tests.conftest import get_unused_tcp_port +from packages.connections.tcp.tcp_client import TCPClientConnection +from packages.connections.tcp.tcp_server import TCPServerConnection +from ....conftest import get_unused_tcp_port @pytest.mark.asyncio async def test_connect_twice(): """Test that connecting twice the tcp connection works correctly.""" port = get_unused_tcp_port() - tcp_connection = TCPServerConnection("public_key", "127.0.0.1", port) + tcp_connection = TCPServerConnection("address", "127.0.0.1", port) loop = asyncio.get_event_loop() tcp_connection.loop = loop await tcp_connection.connect() await asyncio.sleep(0.1) - with unittest.mock.patch.object(aea.connections.tcp.base.logger, "warning") as mock_logger_warning: + with unittest.mock.patch.object(packages.connections.tcp.base.logger, "warning") as mock_logger_warning: await tcp_connection.connect() mock_logger_warning.assert_called_with("Connection already set up.") @@ -54,12 +54,12 @@ async def test_connect_twice(): async def test_connect_raises_exception(): """Test the case that a connection attempt raises an exception.""" port = get_unused_tcp_port() - tcp_connection = TCPServerConnection("public_key", "127.0.0.1", port) + tcp_connection = TCPServerConnection("address", "127.0.0.1", port) loop = asyncio.get_event_loop() tcp_connection.loop = loop - with unittest.mock.patch.object(aea.connections.tcp.base.logger, "error") as mock_logger_error: + with unittest.mock.patch.object(packages.connections.tcp.base.logger, "error") as mock_logger_error: with unittest.mock.patch.object(tcp_connection, "setup", side_effect=Exception("error during setup")): await tcp_connection.connect() mock_logger_error.assert_called_with("error during setup") @@ -69,9 +69,9 @@ async def test_connect_raises_exception(): async def test_disconnect_when_already_disconnected(): """Test that disconnecting a connection already disconnected works correctly.""" port = get_unused_tcp_port() - tcp_connection = TCPServerConnection("public_key", "127.0.0.1", port) + tcp_connection = TCPServerConnection("address", "127.0.0.1", port) - with unittest.mock.patch.object(aea.connections.tcp.base.logger, "warning") as mock_logger_warning: + with unittest.mock.patch.object(packages.connections.tcp.base.logger, "warning") as mock_logger_warning: await tcp_connection.disconnect() mock_logger_warning.assert_called_with("Connection already disconnected.") @@ -79,27 +79,27 @@ async def test_disconnect_when_already_disconnected(): @pytest.mark.asyncio async def test_send_to_unknown_destination(): """Test that a message to an unknown destination logs an error.""" - public_key = "public_key" + address = "address" port = get_unused_tcp_port() - tcp_connection = TCPServerConnection(public_key, "127.0.0.1", port) - envelope = Envelope(to="non_existing_destination", sender="public_key", protocol_id="default", message=b"") - with unittest.mock.patch.object(aea.connections.tcp.base.logger, "error") as mock_logger_error: + tcp_connection = TCPServerConnection(address, "127.0.0.1", port) + envelope = Envelope(to="non_existing_destination", sender="address", protocol_id="default", message=b"") + with unittest.mock.patch.object(packages.connections.tcp.base.logger, "error") as mock_logger_error: await tcp_connection.send(envelope) - mock_logger_error.assert_called_with("[{}]: Cannot send envelope {}".format(public_key, envelope)) + mock_logger_error.assert_called_with("[{}]: Cannot send envelope {}".format(address, envelope)) @pytest.mark.asyncio async def test_send_cancelled(): """Test that cancelling a send works correctly.""" port = get_unused_tcp_port() - tcp_server = TCPServerConnection("public_key_server", "127.0.0.1", port) - tcp_client = TCPClientConnection("public_key_client", "127.0.0.1", port) + tcp_server = TCPServerConnection("address_server", "127.0.0.1", port) + tcp_client = TCPClientConnection("address_client", "127.0.0.1", port) await tcp_server.connect() await tcp_client.connect() with unittest.mock.patch.object(tcp_client._writer, "drain", side_effect=CancelledError): - envelope = Envelope(to="public_key_client", sender="public_key_server", protocol_id="default", message=b"") + envelope = Envelope(to="address_client", sender="address_server", protocol_id="default", message=b"") await tcp_client.send(envelope) await tcp_client.disconnect() diff --git a/tests/test_connections/test_tcp/test_communication.py b/tests/test_packages/test_connections/test_tcp/test_communication.py similarity index 74% rename from tests/test_connections/test_tcp/test_communication.py rename to tests/test_packages/test_connections/test_tcp/test_communication.py index dd36e3e05b..036c45e480 100644 --- a/tests/test_connections/test_tcp/test_communication.py +++ b/tests/test_packages/test_connections/test_tcp/test_communication.py @@ -24,14 +24,14 @@ import pytest -import aea +import packages from aea.configurations.base import ConnectionConfig -from aea.connections.tcp.tcp_client import TCPClientConnection -from aea.connections.tcp.tcp_server import TCPServerConnection from aea.mail.base import Envelope, Multiplexer from aea.protocols.default.message import DefaultMessage from aea.protocols.default.serialization import DefaultSerializer -from tests.conftest import get_unused_tcp_port +from packages.connections.tcp.tcp_client import TCPClientConnection +from packages.connections.tcp.tcp_server import TCPServerConnection +from ....conftest import get_unused_tcp_port class TestTCPCommunication: @@ -43,13 +43,13 @@ def setup_class(cls): cls.host = "127.0.0.1" cls.port = get_unused_tcp_port() - cls.server_pbk = "server_pbk" - cls.client_pbk_1 = "client_pbk_1" - cls.client_pbk_2 = "client_pbk_2" + cls.server_addr = "server_addr" + cls.client_addr_1 = "client_addr_1" + cls.client_addr_2 = "client_addr_2" - cls.server_conn = TCPServerConnection(cls.server_pbk, cls.host, cls.port) - cls.client_conn_1 = TCPClientConnection(cls.client_pbk_1, cls.host, cls.port) - cls.client_conn_2 = TCPClientConnection(cls.client_pbk_2, cls.host, cls.port) + cls.server_conn = TCPServerConnection(cls.server_addr, cls.host, cls.port) + cls.client_conn_1 = TCPClientConnection(cls.client_addr_1, cls.host, cls.port) + cls.client_conn_2 = TCPClientConnection(cls.client_addr_2, cls.host, cls.port) cls.server_multiplexer = Multiplexer([cls.server_conn]) cls.client_1_multiplexer = Multiplexer([cls.client_conn_1]) @@ -73,7 +73,7 @@ def test_communication_client_server(self): """Test that envelopes can be sent from a client to a server.""" msg = DefaultMessage(type=DefaultMessage.Type.BYTES, content=b"hello") msg_bytes = DefaultSerializer().encode(msg) - expected_envelope = Envelope(to=self.server_pbk, sender=self.client_pbk_1, protocol_id=DefaultMessage.protocol_id, message=msg_bytes) + expected_envelope = Envelope(to=self.server_addr, sender=self.client_addr_1, protocol_id=DefaultMessage.protocol_id, message=msg_bytes) self.client_1_multiplexer.put(expected_envelope) actual_envelope = self.server_multiplexer.get(block=True, timeout=5.0) @@ -84,13 +84,13 @@ def test_communication_server_client(self): msg = DefaultMessage(type=DefaultMessage.Type.BYTES, content=b"hello") msg_bytes = DefaultSerializer().encode(msg) - expected_envelope = Envelope(to=self.client_pbk_1, sender=self.server_pbk, protocol_id=DefaultMessage.protocol_id, message=msg_bytes) + expected_envelope = Envelope(to=self.client_addr_1, sender=self.server_addr, protocol_id=DefaultMessage.protocol_id, message=msg_bytes) self.server_multiplexer.put(expected_envelope) actual_envelope = self.client_1_multiplexer.get(block=True, timeout=5.0) assert expected_envelope == actual_envelope - expected_envelope = Envelope(to=self.client_pbk_2, sender=self.server_pbk, protocol_id=DefaultMessage.protocol_id, message=msg_bytes) + expected_envelope = Envelope(to=self.client_addr_2, sender=self.server_addr, protocol_id=DefaultMessage.protocol_id, message=msg_bytes) self.server_multiplexer.put(expected_envelope) actual_envelope = self.client_2_multiplexer.get(block=True, timeout=5.0) @@ -111,18 +111,18 @@ class TestTCPClientConnection: async def test_receive_cancelled(self): """Test that cancelling a receive task works correctly.""" port = get_unused_tcp_port() - tcp_server = TCPServerConnection("public_key_server", "127.0.0.1", port) - tcp_client = TCPClientConnection("public_key_client", "127.0.0.1", port) + tcp_server = TCPServerConnection("address_server", "127.0.0.1", port) + tcp_client = TCPClientConnection("address_client", "127.0.0.1", port) await tcp_server.connect() await tcp_client.connect() - with unittest.mock.patch.object(aea.connections.tcp.tcp_client.logger, "debug") as mock_logger_debug: + with unittest.mock.patch.object(packages.connections.tcp.tcp_client.logger, "debug") as mock_logger_debug: task = asyncio.ensure_future(tcp_client.receive()) await asyncio.sleep(0.1) task.cancel() await asyncio.sleep(0.1) - mock_logger_debug.assert_called_with("[{}] Read cancelled.".format("public_key_client")) + mock_logger_debug.assert_called_with("[{}] Read cancelled.".format("address_client")) assert task.result() is None await tcp_client.disconnect() @@ -132,13 +132,13 @@ async def test_receive_cancelled(self): async def test_receive_raises_struct_error(self): """Test the case when a receive raises a struct error.""" port = get_unused_tcp_port() - tcp_server = TCPServerConnection("public_key_server", "127.0.0.1", port) - tcp_client = TCPClientConnection("public_key_client", "127.0.0.1", port) + tcp_server = TCPServerConnection("address_server", "127.0.0.1", port) + tcp_client = TCPClientConnection("address_client", "127.0.0.1", port) await tcp_server.connect() await tcp_client.connect() - with unittest.mock.patch.object(aea.connections.tcp.tcp_client.logger, "debug") as mock_logger_debug: + with unittest.mock.patch.object(packages.connections.tcp.tcp_client.logger, "debug") as mock_logger_debug: with unittest.mock.patch.object(tcp_client, "_recv", side_effect=struct.error): task = asyncio.ensure_future(tcp_client.receive()) await asyncio.sleep(0.1) @@ -152,8 +152,8 @@ async def test_receive_raises_struct_error(self): async def test_receive_raises_exception(self): """Test the case when a receive raises a generic exception.""" port = get_unused_tcp_port() - tcp_server = TCPServerConnection("public_key_server", "127.0.0.1", port) - tcp_client = TCPClientConnection("public_key_client", "127.0.0.1", port) + tcp_server = TCPServerConnection("address_server", "127.0.0.1", port) + tcp_client = TCPClientConnection("address_client", "127.0.0.1", port) await tcp_server.connect() await tcp_client.connect() @@ -171,7 +171,7 @@ async def test_receive_raises_exception(self): async def test_from_config(self): """Test the creation of the connection from a configuration.""" port = get_unused_tcp_port() - TCPClientConnection.from_config("public_key", ConnectionConfig(host="127.0.0.1", port=port)) + TCPClientConnection.from_config("address", ConnectionConfig(host="127.0.0.1", port=port)) class TestTCPServerConnection: @@ -181,13 +181,13 @@ class TestTCPServerConnection: async def test_receive_raises_exception(self): """Test the case when a receive raises a generic exception.""" port = get_unused_tcp_port() - tcp_server = TCPServerConnection("public_key_server", "127.0.0.1", port) - tcp_client = TCPClientConnection("public_key_client", "127.0.0.1", port) + tcp_server = TCPServerConnection("address_server", "127.0.0.1", port) + tcp_client = TCPClientConnection("address_client", "127.0.0.1", port) await tcp_server.connect() await tcp_client.connect() await asyncio.sleep(0.1) - with unittest.mock.patch.object(aea.connections.tcp.tcp_server.logger, "error") as mock_logger_error: + with unittest.mock.patch.object(packages.connections.tcp.tcp_server.logger, "error") as mock_logger_error: with unittest.mock.patch("asyncio.wait", side_effect=Exception("generic exception")): result = await tcp_server.receive() assert result is None @@ -200,4 +200,4 @@ async def test_receive_raises_exception(self): async def test_from_config(self): """Test the creation of the connection from a configuration.""" port = get_unused_tcp_port() - TCPServerConnection.from_config("public_key", ConnectionConfig(host="127.0.0.1", port=port)) + TCPServerConnection.from_config("address", ConnectionConfig(host="127.0.0.1", port=port)) diff --git a/tests/test_protocols/test_fipa.py b/tests/test_packages/test_protocols/test_fipa.py similarity index 80% rename from tests/test_protocols/test_fipa.py rename to tests/test_packages/test_protocols/test_fipa.py index 87141feb9a..782e91bb8b 100644 --- a/tests/test_protocols/test_fipa.py +++ b/tests/test_packages/test_protocols/test_fipa.py @@ -22,16 +22,17 @@ import pytest +from aea.helpers.search.models import Description, Query, Constraint, ConstraintType from aea.mail.base import Envelope -from aea.protocols.fipa.dialogues import FIPADialogues, FIPADialogue -from aea.protocols.fipa.message import FIPAMessage -from aea.protocols.fipa.serialization import FIPASerializer -from aea.protocols.oef.models import Description, Query, Constraint, ConstraintType +from packages.protocols.fipa.dialogues import FIPADialogues, FIPADialogue +from packages.protocols.fipa.message import FIPAMessage +from packages.protocols.fipa.serialization import FIPASerializer def test_fipa_cfp_serialization(): """Test that the serialization for the 'fipa' protocol works.""" query = Query([Constraint('something', ConstraintType('>', 1))]) + msg = FIPAMessage(message_id=0, dialogue_reference=(str(0), ''), target=0, @@ -65,6 +66,7 @@ def test_fipa_cfp_serialization_bytes(): target=0, performative=FIPAMessage.Performative.CFP, query=query) + msg.counterparty = "sender" msg_bytes = FIPASerializer().encode(msg) envelope = Envelope(to="receiver", sender="sender", @@ -77,10 +79,12 @@ def test_fipa_cfp_serialization_bytes(): assert expected_envelope == actual_envelope actual_msg = FIPASerializer().decode(actual_envelope.message) + actual_msg.counterparty = "sender" expected_msg = msg assert expected_msg == actual_msg deserialised_msg = FIPASerializer().decode(envelope.message) + deserialised_msg.counterparty = "sender" assert msg.get("performative") == deserialised_msg.get("performative") @@ -121,6 +125,7 @@ def test_fipa_accept_serialization(): dialogue_reference=(str(0), ''), target=0, performative=FIPAMessage.Performative.ACCEPT) + msg.counterparty = "sender" msg_bytes = FIPASerializer().encode(msg) envelope = Envelope(to="receiver", sender="sender", @@ -133,6 +138,7 @@ def test_fipa_accept_serialization(): assert expected_envelope == actual_envelope actual_msg = FIPASerializer().decode(actual_envelope.message) + actual_msg.counterparty = "sender" expected_msg = msg assert expected_msg == actual_msg @@ -143,12 +149,12 @@ def test_performative_match_accept(): dialogue_reference=(str(0), ''), target=1, performative=FIPAMessage.Performative.MATCH_ACCEPT) - msg_bytes = FIPASerializer().encode(msg) envelope = Envelope(to="receiver", sender="sender", protocol_id=FIPAMessage.protocol_id, message=msg_bytes) + msg.counterparty = "sender" envelope_bytes = envelope.encode() actual_envelope = Envelope.decode(envelope_bytes) @@ -158,28 +164,28 @@ def test_performative_match_accept(): assert msg.get("performative") == deserialised_msg.get("performative") -def test_performative_not_recognized(): - """Tests an unknown Performative.""" - msg = FIPAMessage( - performative=FIPAMessage.Performative.ACCEPT, - message_id=0, - dialogue_reference=(str(0), ''), - target=1) +# def test_performative_not_recognized(): +# """Tests an unknown Performative.""" +# msg = FIPAMessage( +# performative=FIPAMessage.Performative.ACCEPT, +# message_id=0, +# dialogue_reference=(str(0), ''), +# target=1) - with mock.patch("aea.protocols.fipa.message.FIPAMessage.Performative")\ - as mock_performative_enum: - mock_performative_enum.ACCEPT.value = "unknown" - assert not msg.check_consistency(),\ - "We expect that the check_consistency will return False" +# with mock.patch("packages.protocols.fipa.message.FIPAMessage.Performative")\ +# as mock_performative_enum: +# mock_performative_enum.ACCEPT.value = "unknown" +# assert not msg.check_consistency(),\ +# "We expect that the check_consistency will return False" -def test_performative_accept_with_address(): +def test_performative_accept_with_inform(): """Test the serialization - deserialization of the accept_with_address performative.""" msg = FIPAMessage(message_id=0, dialogue_reference=(str(0), ''), target=1, - performative=FIPAMessage.Performative.ACCEPT_W_ADDRESS, - address="dummy_address") + performative=FIPAMessage.Performative.ACCEPT_W_INFORM, + info={"address": "dummy_address"}) msg_bytes = FIPASerializer().encode(msg) envelope = Envelope(to="receiver", @@ -195,13 +201,13 @@ def test_performative_accept_with_address(): assert msg.get("performative") == deserialised_msg.get("performative") -def test_performative_match_accept_with_address(): +def test_performative_match_accept_with_inform(): """Test the serialization - deserialization of the match_accept_with_address performative.""" msg = FIPAMessage(message_id=0, dialogue_reference=(str(0), ''), target=1, - performative=FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS, - address="dummy_address") + performative=FIPAMessage.Performative.MATCH_ACCEPT_W_INFORM, + info={"address": "dummy_address", "signature": "my_signature"}) msg_bytes = FIPASerializer().encode(msg) envelope = Envelope(to="receiver", @@ -223,7 +229,7 @@ def test_performative_inform(): dialogue_reference=(str(0), ''), target=1, performative=FIPAMessage.Performative.INFORM, - json_data={"foo": "bar"}) + info={"foo": "bar"}) msg_bytes = FIPASerializer().encode(msg) envelope = Envelope(to="receiver", @@ -251,10 +257,10 @@ def test_performative_string_value(): "The str value must be accept" assert str(FIPAMessage.Performative.MATCH_ACCEPT) == "match_accept",\ "The str value must be match_accept" - assert str(FIPAMessage.Performative.ACCEPT_W_ADDRESS) == "accept_w_address", \ - "The str value must be accept_w_address" - assert str(FIPAMessage.Performative.MATCH_ACCEPT_W_ADDRESS) == "match_accept_w_address", \ - "The str value must be match_accept_w_address" + assert str(FIPAMessage.Performative.ACCEPT_W_INFORM) == "accept_w_inform", \ + "The str value must be accept_w_inform" + assert str(FIPAMessage.Performative.MATCH_ACCEPT_W_INFORM) == "match_accept_w_inform", \ + "The str value must be match_accept_w_inform" assert str(FIPAMessage.Performative.INFORM) == "inform", \ "The str value must be inform" @@ -287,7 +293,13 @@ def test_fipa_decoding_unknown_performative(): def test_dialogues(): """Test the dialogues model.""" dialogues = FIPADialogues() - result = dialogues.create_self_initiated(dialogue_opponent_pbk="opponent", dialogue_starter_pbk="starter", is_seller=True) + result = dialogues.create_self_initiated(dialogue_starter_addr="starter", dialogue_opponent_addr="opponent", is_seller=True) assert isinstance(result, FIPADialogue) - result = dialogues.create_opponent_initiated(dialogue_opponent_pbk="opponent", dialogue_reference=(str(0), ''), is_seller=False) + result = dialogues.create_opponent_initiated(dialogue_opponent_addr="opponent", dialogue_reference=(str(0), ''), is_seller=False) assert isinstance(result, FIPADialogue) + assert result.role == FIPADialogue.AgentRole.BUYER + assert dialogues.dialogue_stats is not None + dialogues.dialogue_stats.add_dialogue_endstate(FIPADialogue.EndState.SUCCESSFUL, is_self_initiated=True) + dialogues.dialogue_stats.add_dialogue_endstate(FIPADialogue.EndState.DECLINED_CFP, is_self_initiated=False) + assert dialogues.dialogue_stats.self_initiated == {FIPADialogue.EndState.SUCCESSFUL: 1, FIPADialogue.EndState.DECLINED_PROPOSE: 0, FIPADialogue.EndState.DECLINED_ACCEPT: 0, FIPADialogue.EndState.DECLINED_CFP: 0} + assert dialogues.dialogue_stats.other_initiated == {FIPADialogue.EndState.SUCCESSFUL: 0, FIPADialogue.EndState.DECLINED_PROPOSE: 0, FIPADialogue.EndState.DECLINED_ACCEPT: 0, FIPADialogue.EndState.DECLINED_CFP: 1} diff --git a/tests/test_packages/test_protocols/test_gym.py b/tests/test_packages/test_protocols/test_gym.py index 8001e4e59a..51ade6f2ef 100644 --- a/tests/test_packages/test_protocols/test_gym.py +++ b/tests/test_packages/test_protocols/test_gym.py @@ -18,7 +18,6 @@ # ------------------------------------------------------------------------------ """This module contains the tests of the messages module.""" -from unittest import mock from packages.protocols.gym.message import GymMessage from packages.protocols.gym.serialization import GymSerializer @@ -32,12 +31,6 @@ def test_gym_message_instantiation(): assert GymMessage(performative=GymMessage.Performative.CLOSE) assert str(GymMessage.Performative.CLOSE) == 'close' - msg = GymMessage(performative=GymMessage.Performative.ACT, action='any_action', step_id=1) - with mock.patch('packages.protocols.gym.message.GymMessage.Performative') as mocked_type: - mocked_type.ACT.value = "unknown" - assert not msg.check_consistency(), \ - "Expect the consistency to return False" - def test_gym_serialization(): """Test that the serialization for the 'simple' protocol works for the ERROR message.""" diff --git a/tests/test_packages/test_protocols/test_ml_message.py b/tests/test_packages/test_protocols/test_ml_message.py new file mode 100644 index 0000000000..48f083c8a3 --- /dev/null +++ b/tests/test_packages/test_protocols/test_ml_message.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the tests of the ml_messages module.""" +import logging + +import pytest + +from aea.helpers.search.models import DataModel, Attribute, Constraint, Query, ConstraintType, Description +from packages.protocols.ml_trade.message import MLTradeMessage +from packages.protocols.ml_trade.serialization import MLTradeSerializer + +import numpy as np + +logger = logging.getLogger(__name__) + + +def test_perfomrative_str(): + """Test the str value of each performative.""" + assert str(MLTradeMessage.Performative.CFT) == 'cft' + assert str(MLTradeMessage.Performative.TERMS) == 'terms' + assert str(MLTradeMessage.Performative.ACCEPT) == 'accept' + assert str(MLTradeMessage.Performative.DATA) == 'data' + + +def test_ml_wrong_message_creation(): + """Test the creation of a ml message.""" + with pytest.raises(AssertionError): + MLTradeMessage(performative=MLTradeMessage.Performative.CFT, query="") + + +def test_ml_message_creation(): + """Test the creation of a ml message.""" + dm = DataModel("ml_datamodel", [Attribute("dataset_id", str, True)]) + query = Query([Constraint("dataset_id", ConstraintType("==", "fmnist"))], model=dm) + msg = MLTradeMessage(performative=MLTradeMessage.Performative.CFT, query=query) + msg_bytes = MLTradeSerializer().encode(msg) + recovered_msg = MLTradeSerializer().decode(msg_bytes) + assert recovered_msg == msg + + terms = Description({"batch_size": 5, + "price": 10, + "seller_tx_fee": 5, + "buyer_tx_fee": 2, + "currency_id": "FET", + "ledger_id": "fetch", + "address": "agent1"}) + + msg = MLTradeMessage(performative=MLTradeMessage.Performative.TERMS, terms=terms) + msg_bytes = MLTradeSerializer().encode(msg) + recovered_msg = MLTradeSerializer().decode(msg_bytes) + assert recovered_msg == msg + + tx_digest = "This is the transaction digest." + msg = MLTradeMessage(performative=MLTradeMessage.Performative.ACCEPT, terms=terms, tx_digest=tx_digest) + msg_bytes = MLTradeSerializer().encode(msg) + recovered_msg = MLTradeSerializer().decode(msg_bytes) + assert recovered_msg == msg + + # TODO: Need to change the __eq__ function : Error message is : + # ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all() + data = np.zeros((5, 2)), np.zeros((5, 2)) + msg = MLTradeMessage(performative=MLTradeMessage.Performative.DATA, terms=terms, data=data) + msg_bytes = MLTradeSerializer().encode(msg) + with pytest.raises(ValueError): + recovered_msg = MLTradeSerializer().decode(msg_bytes) + assert recovered_msg == msg diff --git a/tests/test_protocols/test_oef_message.py b/tests/test_packages/test_protocols/test_oef_message.py similarity index 79% rename from tests/test_protocols/test_oef_message.py rename to tests/test_packages/test_protocols/test_oef_message.py index 5532bf0825..e1927d7503 100644 --- a/tests/test_protocols/test_oef_message.py +++ b/tests/test_packages/test_protocols/test_oef_message.py @@ -21,11 +21,11 @@ # from enum import Enum # import base64 # import json -from unittest import mock +# from unittest import mock -from aea.protocols.oef.message import OEFMessage -from aea.protocols.oef.models import DataModel, Attribute, Query, Constraint, ConstraintType, Description -from aea.protocols.oef.serialization import OEFSerializer +from aea.helpers.search.models import DataModel, Attribute, Query, Constraint, ConstraintType, Description +from packages.protocols.oef.message import OEFMessage +from packages.protocols.oef.serialization import OEFSerializer def test_oef_type_string_value(): @@ -55,39 +55,35 @@ def test_oef_message_consistency(): foo_datamodel = DataModel("foo", [Attribute("bar", int, True, "A bar attribute.")]) msg = OEFMessage( - oef_type=OEFMessage.Type.SEARCH_AGENTS, + type=OEFMessage.Type.SEARCH_AGENTS, id=2, query=Query([Constraint("bar", ConstraintType("==", 1))], model=foo_datamodel) ) assert msg.check_consistency(), "We expect the consistency to return TRUE" - with mock.patch("aea.protocols.oef.message.OEFMessage.Type")\ - as mock_type_enum: - mock_type_enum.SEARCH_AGENTS.value = "unknown" - assert not msg.check_consistency(),\ - "Expect the consistency to return False" attribute_foo = Attribute("foo", int, True, "a foo attribute.") attribute_bar = Attribute("bar", str, True, "a bar attribute.") data_model_foobar = DataModel("foobar", [attribute_foo, attribute_bar], "A foobar data model.") description_foobar = Description({"foo": 1, "bar": "baz"}, data_model=data_model_foobar) - msg = OEFMessage(oef_type=OEFMessage.Type.REGISTER_AGENT, + msg = OEFMessage(type=OEFMessage.Type.REGISTER_AGENT, id=0, agent_description=description_foobar, - agent_id="public_key") + agent_id="address") assert msg.check_consistency() - msg = OEFMessage(oef_type=OEFMessage.Type.UNREGISTER_AGENT, id=0, agent_description=description_foobar, - agent_id="public_key") + msg = OEFMessage(type=OEFMessage.Type.UNREGISTER_AGENT, + id=0, + agent_description=description_foobar, + agent_id="address") assert msg.check_consistency() def test_oef_message_oef_error(): """Tests the OEF_ERROR type of message.""" - msg = OEFMessage(oef_type=OEFMessage.Type.OEF_ERROR, id=0, + msg = OEFMessage(type=OEFMessage.Type.OEF_ERROR, id=0, operation=OEFMessage.OEFErrorOperation.SEARCH_AGENTS) - - assert OEFMessage(oef_type=OEFMessage.Type.OEF_ERROR, id=0, + assert OEFMessage(type=OEFMessage.Type.OEF_ERROR, id=0, operation=OEFMessage.OEFErrorOperation.SEARCH_AGENTS),\ "Expects an oef message Error!" msg_bytes = OEFSerializer().encode(msg) @@ -98,9 +94,9 @@ def test_oef_message_oef_error(): "Expected the deserialized_msg to me equals to msg" -def test_oef_message_DialogeError(): +def test_oef_message_dialoge_error(): """Tests the OEFMEssage of type DialogueError.""" - assert OEFMessage(oef_type=OEFMessage.Type.DIALOGUE_ERROR, + assert OEFMessage(type=OEFMessage.Type.DIALOGUE_ERROR, id=0, dialogue_id=1, origin="myKey"),\ diff --git a/tests/test_packages/test_protocols/test_tac.py b/tests/test_packages/test_protocols/test_tac.py index 1a6834df3d..f7a7446dda 100644 --- a/tests/test_packages/test_protocols/test_tac.py +++ b/tests/test_packages/test_protocols/test_tac.py @@ -23,104 +23,106 @@ import pytest from packages.protocols.tac.message import TACMessage -from packages.protocols.tac.serialization import TACSerializer +from packages.protocols.tac.serialization import TACSerializer, _from_dict_to_pairs def test_tac_message_instantiation(): """Test instantiation of the tac message.""" - assert TACMessage(tac_type=TACMessage.Type.REGISTER, + assert TACMessage(type=TACMessage.Type.REGISTER, agent_name='some_name') - assert TACMessage(tac_type=TACMessage.Type.UNREGISTER) - assert TACMessage(tac_type=TACMessage.Type.TRANSACTION, - transaction_id='some_id', - counterparty='some_address', - amount_by_currency={'FET': 10}, - sender_tx_fee=10, - counterparty_tx_fee=10, - quantities_by_good_pbk={'good_1': 0, 'good_2': 10}) - assert TACMessage(tac_type=TACMessage.Type.GET_STATE_UPDATE) - assert TACMessage(tac_type=TACMessage.Type.CANCELLED) - assert TACMessage(tac_type=TACMessage.Type.GAME_DATA, - amount_by_currency={'FET': 10}, - exchange_params_by_currency={'FET': 10.0}, - quantities_by_good_pbk={'good_1': 20, 'good_2': 15}, - utility_params_by_good_pbk={'good_1': 30.0, 'good_2': 50.0}, + assert TACMessage(type=TACMessage.Type.UNREGISTER) + assert TACMessage(type=TACMessage.Type.TRANSACTION, + tx_id='some_id', + tx_sender_addr='some_address', + tx_counterparty_addr='some_other_address', + amount_by_currency_id={'FET': 10}, + tx_sender_fee=10, + tx_counterparty_fee=10, + quantities_by_good_id={'good_1': 0, 'good_2': 10}, + tx_nonce=1, + tx_sender_signature=b'some_signature', + tx_counterparty_signature=b'some_other_signature') + assert TACMessage(type=TACMessage.Type.GET_STATE_UPDATE) + assert TACMessage(type=TACMessage.Type.CANCELLED) + assert TACMessage(type=TACMessage.Type.GAME_DATA, + amount_by_currency_id={'FET': 10}, + exchange_params_by_currency_id={'FET': 10.0}, + quantities_by_good_id={'good_1': 20, 'good_2': 15}, + utility_params_by_good_id={'good_1': 30.0, 'good_2': 50.0}, tx_fee=20, - agent_pbk_to_name={'agent_1': 'Agent one', 'agent_2': 'Agent two'}, - good_pbk_to_name={'good_1': 'First good', 'good_2': 'Second good'}, + agent_addr_to_name={'agent_1': 'Agent one', 'agent_2': 'Agent two'}, + good_id_to_name={'good_1': 'First good', 'good_2': 'Second good'}, version_id='game_version_1') - assert TACMessage(tac_type=TACMessage.Type.TRANSACTION_CONFIRMATION, - transaction_id='some_id', - amount_by_currency={'FET': 10}, - quantities_by_good_pbk={'good_1': 20, 'good_2': 15}) - assert TACMessage(tac_type=TACMessage.Type.TAC_ERROR, + assert TACMessage(type=TACMessage.Type.TRANSACTION_CONFIRMATION, + tx_id='some_id', + amount_by_currency_id={'FET': 10}, + quantities_by_good_id={'good_1': 20, 'good_2': 15}) + assert TACMessage(type=TACMessage.Type.TAC_ERROR, error_code=TACMessage.ErrorCode.GENERIC_ERROR) assert str(TACMessage.Type.REGISTER) == 'register' - msg = TACMessage(tac_type=TACMessage.Type.REGISTER, agent_name='some_name') - with mock.patch('packages.protocols.tac.message.TACMessage.Type') as mocked_type: - mocked_type.REGISTER.value = "unknown" - assert not msg.check_consistency(), \ - "Expect the consistency to return False" - def test_tac_serialization(): """Test that the serialization for the tac message works.""" - msg = TACMessage(tac_type=TACMessage.Type.REGISTER, + msg = TACMessage(type=TACMessage.Type.REGISTER, agent_name='some_name') msg_bytes = TACSerializer().encode(msg) actual_msg = TACSerializer().decode(msg_bytes) expected_msg = msg assert expected_msg == actual_msg - msg = TACMessage(tac_type=TACMessage.Type.UNREGISTER) + msg = TACMessage(type=TACMessage.Type.UNREGISTER) msg_bytes = TACSerializer().encode(msg) actual_msg = TACSerializer().decode(msg_bytes) expected_msg = msg assert expected_msg == actual_msg - msg = TACMessage(tac_type=TACMessage.Type.TRANSACTION, - transaction_id='some_id', - counterparty='some_address', - amount_by_currency={'FET': 10}, - sender_tx_fee=10, - counterparty_tx_fee=10, - quantities_by_good_pbk={'good_1': 0, 'good_2': 10}) + msg = TACMessage(type=TACMessage.Type.TRANSACTION, + tx_id='some_id', + tx_sender_addr='some_address', + tx_counterparty_addr='some_other_address', + amount_by_currency_id={'FET': -10}, + tx_sender_fee=10, + tx_counterparty_fee=10, + quantities_by_good_id={'good_1': 0, 'good_2': 10}, + tx_nonce=1, + tx_sender_signature=b'some_signature', + tx_counterparty_signature=b'some_other_signature') msg_bytes = TACSerializer().encode(msg) actual_msg = TACSerializer().decode(msg_bytes) expected_msg = msg assert expected_msg == actual_msg - msg = TACMessage(tac_type=TACMessage.Type.GET_STATE_UPDATE) + msg = TACMessage(type=TACMessage.Type.GET_STATE_UPDATE) msg_bytes = TACSerializer().encode(msg) actual_msg = TACSerializer().decode(msg_bytes) expected_msg = msg assert expected_msg == actual_msg - msg = TACMessage(tac_type=TACMessage.Type.CANCELLED) + msg = TACMessage(type=TACMessage.Type.CANCELLED) msg_bytes = TACSerializer().encode(msg) actual_msg = TACSerializer().decode(msg_bytes) expected_msg = msg assert expected_msg == actual_msg - msg = TACMessage(tac_type=TACMessage.Type.GAME_DATA, - amount_by_currency={'FET': 10}, - exchange_params_by_currency={'FET': 10.0}, - quantities_by_good_pbk={'good_1': 20, 'good_2': 15}, - utility_params_by_good_pbk={'good_1': 30.0, 'good_2': 50.0}, + msg = TACMessage(type=TACMessage.Type.GAME_DATA, + amount_by_currency_id={'FET': 10}, + exchange_params_by_currency_id={'FET': 10.0}, + quantities_by_good_id={'good_1': 20, 'good_2': 15}, + utility_params_by_good_id={'good_1': 30.0, 'good_2': 50.0}, tx_fee=20, - agent_pbk_to_name={'agent_1': 'Agent one', 'agent_2': 'Agent two'}, - good_pbk_to_name={'good_1': 'First good', 'good_2': 'Second good'}, + agent_addr_to_name={'agent_1': 'Agent one', 'agent_2': 'Agent two'}, + good_id_to_name={'good_1': 'First good', 'good_2': 'Second good'}, version_id='game_version_1') msg_bytes = TACSerializer().encode(msg) actual_msg = TACSerializer().decode(msg_bytes) expected_msg = msg assert expected_msg == actual_msg - msg = TACMessage(tac_type=TACMessage.Type.TRANSACTION_CONFIRMATION, - transaction_id='some_id', - amount_by_currency={'FET': 10}, - quantities_by_good_pbk={'good_1': 20, 'good_2': 15}) + msg = TACMessage(type=TACMessage.Type.TRANSACTION_CONFIRMATION, + tx_id='some_id', + amount_by_currency_id={'FET': 10}, + quantities_by_good_id={'good_1': 20, 'good_2': 15}) msg_bytes = TACSerializer().encode(msg) actual_msg = TACSerializer().decode(msg_bytes) expected_msg = msg @@ -131,9 +133,17 @@ def test_tac_serialization(): mocked_type.TRANSACTION_CONFIRMATION.value = "unknown" TACSerializer().encode(msg) - msg = TACMessage(tac_type=TACMessage.Type.TAC_ERROR, - error_code=TACMessage.ErrorCode.GENERIC_ERROR) + msg = TACMessage(type=TACMessage.Type.TAC_ERROR, + error_code=TACMessage.ErrorCode.GENERIC_ERROR, + info={'msg': "This is info msg."}) msg_bytes = TACSerializer().encode(msg) actual_msg = TACSerializer().decode(msg_bytes) expected_msg = msg assert expected_msg == actual_msg + + +def test_from_dict_to_pairs(): + """Test the helper function _from_dict_to_pairs.""" + with pytest.raises(ValueError): + test_items_dict = {"Test": b'UnsupportedType'} + _from_dict_to_pairs(test_items_dict) diff --git a/tests/test_packages/test_skills/data/connection.yaml b/tests/test_packages/test_skills/data/connection.yaml index 4cb29d512e..e98c95ee86 100644 --- a/tests/test_packages/test_skills/data/connection.yaml +++ b/tests/test_packages/test_skills/data/connection.yaml @@ -1,12 +1,15 @@ name: gym -authors: Fetch.AI Limited +author: fetchai version: 0.1.0 license: Apache 2.0 +fingerprint: "" description: "The gym connection wraps an OpenAI gym." url: "" class_name: GymConnection +protocols: ["gym"] restricted_to_protocols: ["gym"] +excluded_protocols: [] config: env: 'gyms.env.BanditNArmedRandom' dependencies: - - gym + gym: {} diff --git a/tests/test_packages/test_skills/test_carpark.py b/tests/test_packages/test_skills/test_carpark.py index d1dfda05d7..835224a133 100644 --- a/tests/test_packages/test_skills/test_carpark.py +++ b/tests/test_packages/test_skills/test_carpark.py @@ -35,7 +35,7 @@ from aea.cli import cli -from tests.conftest import CLI_LOG_OPTION +from ...conftest import CLI_LOG_OPTION def _read_tty(pid: subprocess.Popen): @@ -89,9 +89,15 @@ def test_carpark(self, pytestconfig): agent_one_dir_path = os.path.join(self.t, self.agent_name_one) os.chdir(agent_one_dir_path) + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "oef"], standalone_mode=False) + assert result.exit_code == 0 + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "skill", "carpark_detection"], standalone_mode=False) assert result.exit_code == 0 + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "install"], standalone_mode=False) + assert result.exit_code == 0 + # Load the agent yaml file and manually insert the things we need yaml_path = os.path.join("skills", "carpark_detection", "skill.yaml") file = open(yaml_path, mode='r') @@ -120,7 +126,9 @@ def test_carpark(self, pytestconfig): sys.executable, '-m', 'aea.cli', - "run" + "run", + '--connections', + 'oef' ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -132,9 +140,15 @@ def test_carpark(self, pytestconfig): agent_two_dir_path = os.path.join(self.t, self.agent_name_two) os.chdir(agent_two_dir_path) + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "oef"], standalone_mode=False) + assert result.exit_code == 0 + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "skill", "carpark_client"], standalone_mode=False) assert result.exit_code == 0 + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "install"], standalone_mode=False) + assert result.exit_code == 0 + # Load the agent yaml file and manually insert the things we need file = open("aea-config.yaml", mode='r') @@ -171,7 +185,9 @@ def test_carpark(self, pytestconfig): sys.executable, '-m', 'aea.cli', - "run" + "run", + '--connections', + 'oef' ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, diff --git a/tests/test_packages/test_skills/test_echo.py b/tests/test_packages/test_skills/test_echo.py index 3788e9ae3d..1b0a44d6c7 100644 --- a/tests/test_packages/test_skills/test_echo.py +++ b/tests/test_packages/test_skills/test_echo.py @@ -38,7 +38,7 @@ from aea.protocols.default.message import DefaultMessage from aea.protocols.default.serialization import DefaultSerializer -from tests.conftest import CLI_LOG_OPTION +from ...conftest import CLI_LOG_OPTION class TestEchoSkill: @@ -78,17 +78,13 @@ def test_echo(self, pytestconfig): # add skills result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "skill", "echo"], standalone_mode=False) assert result.exit_code == 0 - result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "stub"], standalone_mode=False) - assert result.exit_code == 0 # run the agent process = subprocess.Popen([ sys.executable, '-m', 'aea.cli', - "run", - "--connections", - "stub" + "run" ], stdout=subprocess.PIPE, env=os.environ.copy()) diff --git a/tests/test_packages/test_skills/test_gym.py b/tests/test_packages/test_skills/test_gym.py index 53ba8d572f..cc09d50f45 100644 --- a/tests/test_packages/test_skills/test_gym.py +++ b/tests/test_packages/test_skills/test_gym.py @@ -36,7 +36,7 @@ from aea.cli import cli -from tests.conftest import CLI_LOG_OPTION +from ...conftest import CLI_LOG_OPTION class TestGymSkill: @@ -87,7 +87,7 @@ def test_gym(self, pytestconfig): # change number of training steps skill_config_path = Path(self.t, self.agent_name, "skills", "gym", "skill.yaml") skill_config = SkillConfig.from_json(yaml.safe_load(open(skill_config_path))) - skill_config.tasks.read("GymTask").args["nb_steps"] = 100 + skill_config.tasks.read("gym").args["nb_steps"] = 100 yaml.safe_dump(skill_config.json, open(skill_config_path, "w")) process = subprocess.Popen([ diff --git a/tests/test_packages/test_skills/test_ml_skills.py b/tests/test_packages/test_skills/test_ml_skills.py new file mode 100644 index 0000000000..b3940bb597 --- /dev/null +++ b/tests/test_packages/test_skills/test_ml_skills.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the integration test for the weather skills.""" + +import os +import pytest +import shutil +import signal +import subprocess +import sys +import tempfile +import time + +import io +import threading + +from ...common.click_testing import CliRunner + +from aea.cli import cli + +from ...conftest import CLI_LOG_OPTION + + +def _read_tty(pid: subprocess.Popen): + for line in io.TextIOWrapper(pid.stdout, encoding="utf-8"): + print("stdout: " + line.replace("\n", "")) + + +def _read_error(pid: subprocess.Popen): + for line in io.TextIOWrapper(pid.stderr, encoding="utf-8"): + print("stderr: " + line.replace("\n", "")) + + +class TestMLSkills: + """Test that ml skills work.""" + + @pytest.fixture(autouse=True) + def _start_oef_node(self, network_node): + """Start an oef node.""" + + @classmethod + def setup_class(cls): + """Set up the test class.""" + cls.runner = CliRunner() + cls.agent_name_one = "ml_data_provider" + cls.agent_name_two = "ml_model_trainer" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + + @pytest.mark.skipif(sys.version_info == (3, 8), reason="cannot run on 3.8 as tensorflow not installable") + def test_ml_skills(self, pytestconfig): + """Run the ml skills sequence.""" + if pytestconfig.getoption("ci"): + pytest.skip("Skipping the test since it doesn't work in CI.") + # add packages folder + packages_src = os.path.join(self.cwd, 'packages') + packages_dst = os.path.join(os.getcwd(), 'packages') + shutil.copytree(packages_src, packages_dst) + + # Add scripts folder + scripts_src = os.path.join(self.cwd, 'scripts') + scripts_dst = os.path.join(os.getcwd(), 'scripts') + shutil.copytree(scripts_src, scripts_dst) + + # create agent one and agent two + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "create", self.agent_name_one], standalone_mode=False) + assert result.exit_code == 0 + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "create", self.agent_name_two], standalone_mode=False) + assert result.exit_code == 0 + + # add packages for agent one and run it + agent_one_dir_path = os.path.join(self.t, self.agent_name_one) + os.chdir(agent_one_dir_path) + + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "oef"], standalone_mode=False) + assert result.exit_code == 0 + + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "skill", "ml_data_provider"], standalone_mode=False) + assert result.exit_code == 0 + + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "install"], standalone_mode=False) + assert result.exit_code == 0 + + process_one = subprocess.Popen([ + sys.executable, + '-m', + 'aea.cli', + "run", + '--connections', + 'oef' + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=os.environ.copy()) + + os.chdir(self.t) + + # add packages for agent two and run it + agent_two_dir_path = os.path.join(self.t, self.agent_name_two) + os.chdir(agent_two_dir_path) + + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "oef"], standalone_mode=False) + assert result.exit_code == 0 + + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "skill", "ml_train"], standalone_mode=False) + assert result.exit_code == 0 + + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "install"], standalone_mode=False) + assert result.exit_code == 0 + + # # Load the agent yaml file and manually insert the things we need + # file = open("aea-config.yaml", mode='r') + + # # read all lines at once + # whole_file = file.read() + + # # add in the ledger address + # find_text = "ledger_apis: []" + # replace_text = """ledger_apis: + # - ledger_api: + # addr: alpha.fetch-ai.com + # ledger: fetchai + # port: 80""" + + # whole_file = whole_file.replace(find_text, replace_text) + + # # close the file + # file.close() + + # with open("aea-config.yaml", 'w') as f: + # f.write(whole_file) + + # # Generate the private keys + # result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "generate-key", "fetchai"], standalone_mode=False) + # assert result.exit_code == 0 + + # # Add some funds to the weather station + # os.chdir(os.path.join(scripts_dst, "../")) + # result = subprocess.call(["python", "./scripts/fetchai_wealth_generation.py", "--private-key", os.path.join("./", self.agent_name_two, "fet_private_key.txt"), "--amount", "10000000", "--addr", "alpha.fetch-ai.com", "--port", "80"]) + # assert result == 0 + + os.chdir(agent_two_dir_path) + process_two = subprocess.Popen([ + sys.executable, + '-m', + 'aea.cli', + "run", + '--connections', + 'oef' + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=os.environ.copy()) + + tty_read_thread = threading.Thread(target=_read_tty, args=(process_one, )) + tty_read_thread.start() + + error_read_thread = threading.Thread(target=_read_error, args=(process_one, )) + error_read_thread.start() + + tty_read_thread = threading.Thread(target=_read_tty, args=(process_two, )) + tty_read_thread.start() + + error_read_thread = threading.Thread(target=_read_error, args=(process_two, )) + error_read_thread.start() + + time.sleep(60) + process_one.send_signal(signal.SIGINT) + process_two.send_signal(signal.SIGINT) + + process_one.wait(timeout=60) + process_two.wait(timeout=60) + + assert process_one.returncode == 0 + assert process_two.returncode == 0 + + poll_one = process_one.poll() + if poll_one is None: + process_one.terminate() + process_one.wait(2) + + poll_two = process_two.poll() + if poll_two is None: + process_two.terminate() + process_two.wait(2) + + os.chdir(self.t) + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "delete", self.agent_name_one], standalone_mode=False) + assert result.exit_code == 0 + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "delete", self.agent_name_two], standalone_mode=False) + assert result.exit_code == 0 + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass diff --git a/tests/test_packages/test_skills/test_weather.py b/tests/test_packages/test_skills/test_weather.py index a5cc78f29f..fb005954c4 100644 --- a/tests/test_packages/test_skills/test_weather.py +++ b/tests/test_packages/test_skills/test_weather.py @@ -32,7 +32,7 @@ from aea.cli import cli -from tests.conftest import CLI_LOG_OPTION +from ...conftest import CLI_LOG_OPTION class TestWeatherSkills: @@ -71,14 +71,22 @@ def test_weather(self, pytestconfig): agent_one_dir_path = os.path.join(self.t, self.agent_name_one) os.chdir(agent_one_dir_path) + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "oef"], standalone_mode=False) + assert result.exit_code == 0 + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "skill", "weather_station"], standalone_mode=False) assert result.exit_code == 0 + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "install"], standalone_mode=False) + assert result.exit_code == 0 + process_one = subprocess.Popen([ sys.executable, '-m', 'aea.cli', - "run" + "run", + '--connections', + 'oef' ], stdout=subprocess.PIPE, env=os.environ.copy()) @@ -89,14 +97,22 @@ def test_weather(self, pytestconfig): agent_two_dir_path = os.path.join(self.t, self.agent_name_two) os.chdir(agent_two_dir_path) + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "oef"], standalone_mode=False) + assert result.exit_code == 0 + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "skill", "weather_client"], standalone_mode=False) assert result.exit_code == 0 + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "install"], standalone_mode=False) + assert result.exit_code == 0 + process_two = subprocess.Popen([ sys.executable, '-m', 'aea.cli', - "run" + "run", + '--connections', + 'oef' ], stdout=subprocess.PIPE, env=os.environ.copy()) diff --git a/tests/test_packages/test_skills/test_weather_ledger.py b/tests/test_packages/test_skills/test_weather_ledger.py index 95ad811d32..51a51d987b 100644 --- a/tests/test_packages/test_skills/test_weather_ledger.py +++ b/tests/test_packages/test_skills/test_weather_ledger.py @@ -35,7 +35,7 @@ from aea.cli import cli -from tests.conftest import CLI_LOG_OPTION +from ...conftest import CLI_LOG_OPTION def _read_tty(pid: subprocess.Popen): @@ -89,14 +89,22 @@ def test_weather(self, pytestconfig): agent_one_dir_path = os.path.join(self.t, self.agent_name_one) os.chdir(agent_one_dir_path) + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "oef"], standalone_mode=False) + assert result.exit_code == 0 + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "skill", "weather_station_ledger"], standalone_mode=False) assert result.exit_code == 0 + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "install"], standalone_mode=False) + assert result.exit_code == 0 + process_one = subprocess.Popen([ sys.executable, '-m', 'aea.cli', - "run" + "run", + '--connections', + 'oef' ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -108,9 +116,15 @@ def test_weather(self, pytestconfig): agent_two_dir_path = os.path.join(self.t, self.agent_name_two) os.chdir(agent_two_dir_path) + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "connection", "oef"], standalone_mode=False) + assert result.exit_code == 0 + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "add", "skill", "weather_client_ledger"], standalone_mode=False) assert result.exit_code == 0 + result = self.runner.invoke(cli, [*CLI_LOG_OPTION, "install"], standalone_mode=False) + assert result.exit_code == 0 + # Load the agent yaml file and manually insert the things we need file = open("aea-config.yaml", mode='r') @@ -147,7 +161,9 @@ def test_weather(self, pytestconfig): sys.executable, '-m', 'aea.cli', - "run" + "run", + '--connections', + 'oef' ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, diff --git a/tests/test_protocols/test_default.py b/tests/test_protocols/test_default.py index b43c6013cc..64cbba54b7 100644 --- a/tests/test_protocols/test_default.py +++ b/tests/test_protocols/test_default.py @@ -21,7 +21,6 @@ import base64 import json -from typing import cast from unittest import mock import pytest @@ -47,7 +46,7 @@ def test_default_bytes_serialization(): def test_default_error_serialization(): """Test that the serialization for the 'simple' protocol works for the ERROR message.""" - msg = DefaultMessage(type=DefaultMessage.Type.ERROR, error_code=-10001, error_msg="An error", error_data='Some data') + msg = DefaultMessage(type=DefaultMessage.Type.ERROR, error_code=-10001, error_msg="An error", error_data={'error': 'Some data'}) msg_bytes = DefaultSerializer().encode(msg) actual_msg = DefaultSerializer().decode(msg_bytes) expected_msg = msg @@ -55,13 +54,12 @@ def test_default_error_serialization(): msg = DefaultMessage(type=DefaultMessage.Type.BYTES, content=b"hello") with pytest.raises(ValueError): - with mock.patch("aea.protocols.default.message.DefaultMessage.Type")\ + with mock.patch("aea.protocols.default.message.DefaultMessage.Type") \ as mock_type_enum: mock_type_enum.BYTES.value = "unknown" body = {} # Dict[str, Any] - msg_type = DefaultMessage.Type(msg.get("type")) - body["type"] = str(msg_type.value) - content = cast(bytes, msg.get("content")) + body["type"] = str(msg.type.value) + content = msg.content body["content"] = base64.b64encode(content).decode("utf-8") bytes_msg = json.dumps(body).encode("utf-8") returned_msg = DefaultSerializer().decode(bytes_msg) diff --git a/tests/test_protocols/test_scaffold.py b/tests/test_protocols/test_scaffold.py index 810d1edc5f..c26fece538 100644 --- a/tests/test_protocols/test_scaffold.py +++ b/tests/test_protocols/test_scaffold.py @@ -26,5 +26,5 @@ def test_scaffold_message(): """Testing the creation of a scaffold message.""" with pytest.raises(NotImplementedError): - msg = MyScaffoldMessage() + msg = MyScaffoldMessage(performative='') assert not msg.check_consistency(), "Not Implemented Error" diff --git a/tests/test_registries.py b/tests/test_registries.py index 491e6a8983..a227d80424 100644 --- a/tests/test_registries.py +++ b/tests/test_registries.py @@ -210,22 +210,26 @@ def test_task_registry(self): def test_skill_loading(self): """Test that the skills have been loaded correctly.""" dummy_skill = self.resources.get_skill("dummy") - error_skill_context = dummy_skill.skill_context + skill_context = dummy_skill.skill_context handlers = dummy_skill.handlers behaviours = dummy_skill.behaviours tasks = dummy_skill.tasks shared_classes = dummy_skill.shared_classes - assert handlers == error_skill_context.handlers - assert behaviours == error_skill_context.behaviours - assert tasks == error_skill_context.tasks - assert getattr(error_skill_context, "agent_name") == self.agent_name + assert len(handlers) == len(skill_context.handlers.__dict__) + assert len(behaviours) == len(skill_context.behaviours.__dict__) + assert len(tasks) == len(skill_context.tasks.__dict__) - assert handlers[0].context == dummy_skill.skill_context - assert behaviours[0].context == dummy_skill.skill_context - assert tasks[0].context == dummy_skill.skill_context - assert shared_classes[0].context == dummy_skill.skill_context + assert handlers["dummy"] == skill_context.handlers.dummy + assert behaviours["dummy"] == skill_context.behaviours.dummy + assert tasks["dummy"] == skill_context.tasks.dummy + assert shared_classes["dummy"] == skill_context.dummy + + assert handlers["dummy"].context == dummy_skill.skill_context + assert behaviours["dummy"].context == dummy_skill.skill_context + assert tasks["dummy"].context == dummy_skill.skill_context + assert shared_classes["dummy"].context == dummy_skill.skill_context def test_handler_configuration_loading(self): """Test that the handler configurations are loaded correctly.""" @@ -265,7 +269,7 @@ def test_shared_class_configuration_loading(self): """Test that the shared class configurations are loaded correctly.""" dummy_skill = self.resources.get_skill("dummy") assert len(dummy_skill.shared_classes) == 1 - dummy_shared_class = dummy_skill.shared_classes[0] + dummy_shared_class = dummy_skill.shared_classes["dummy"] assert dummy_shared_class.config == { "shared_class_arg_1": 1, @@ -303,22 +307,24 @@ def setup_class(cls): def test_handle_internal_messages(self): """Test that the internal messages are handled.""" self.aea.setup() - t = TransactionMessage(performative=TransactionMessage.Performative.ACCEPT, - skill_id="dummy", - transaction_id="transaction0", - sender="pk1", - counterparty="pk2", - is_sender_buyer=True, - currency_pbk="Unknown", - amount=2, - sender_tx_fee=0, - counterparty_tx_fee=0, - quantities_by_good_pbk={"Unknown": 10}) + t = TransactionMessage(performative=TransactionMessage.Performative.SUCCESSFUL_SETTLEMENT, + tx_id="transaction0", + skill_callback_ids=["internal", "dummy"], + tx_sender_addr="pk1", + tx_counterparty_addr="pk2", + tx_amount_by_currency_id={"FET": 2}, + tx_sender_fee=0, + tx_counterparty_fee=0, + tx_quantities_by_good_id={"Unknown": 10}, + ledger_id="fetchai", + info={}, + tx_digest='some_tx_digest') self.aea.decision_maker.message_out_queue.put(t) self.aea.filter.handle_internal_messages() internal_handler = self.aea.resources.handler_registry.fetch_by_skill("internal", "dummy") assert len(internal_handler.handled_internal_messages) == 1 + self.aea.teardown() @classmethod def teardown_class(cls): diff --git a/tests/test_skills/test_base.py b/tests/test_skills/test_base.py index c31f764e1c..d3fd8fe39d 100644 --- a/tests/test_skills/test_base.py +++ b/tests/test_skills/test_base.py @@ -32,7 +32,7 @@ from aea.crypto.ledger_apis import LedgerApis from aea.decision_maker.base import OwnershipState, Preferences, GoalPursuitReadiness from aea.skills.base import SkillContext, Skill -from tests.conftest import CUR_PATH, DummyConnection +from ..conftest import CUR_PATH, DummyConnection def test_agent_context_ledger_apis(): @@ -148,7 +148,7 @@ def test_missing_handler(self): """Test that when parsing a skill and an handler is missing, we behave correctly.""" Path(self.t, "handlers.py").write_text("") Skill.from_dir(self.t, self.agent_context) - self.mocked_logger_warning.assert_called_with("Handler 'DummyHandler' cannot be found.") + self.mocked_logger_warning.assert_called_with("Handler 'DummyInternalHandler' cannot be found.") def test_missing_behaviour(self): """Test that when parsing a skill and a behaviour is missing, we behave correctly.""" diff --git a/tests/test_skills/test_behaviours.py b/tests/test_skills/test_behaviours.py new file mode 100644 index 0000000000..7f1359be34 --- /dev/null +++ b/tests/test_skills/test_behaviours.py @@ -0,0 +1,113 @@ +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the tests for the behaviours.""" +from aea.skills.behaviours import SequenceBehaviour, OneShotBehaviour, FSMBehaviour, State + + +def test_sequence_behaviour(): + """Test the sequence behaviour.""" + outputs = [] + + class MySequenceBehaviour(SequenceBehaviour): + + def setup(self) -> None: + pass + + def teardown(self) -> None: + pass + + class SimpleOneShotBehaviour(OneShotBehaviour): + + def __init__(self, id, **kwargs): + super().__init__(**kwargs) + self.id = id + + def setup(self) -> None: + pass + + def teardown(self) -> None: + pass + + def act(self) -> None: + outputs.append(self.id) + + # TODO let the initialization of a behaviour action from constructor + a = SimpleOneShotBehaviour("a", skill_context=None) + b = SimpleOneShotBehaviour("b", skill_context=None) + c = SimpleOneShotBehaviour("c", skill_context=None) + sequence = MySequenceBehaviour([a, b, c], skill_context=None) + + max_iterations = 10 + i = 0 + while not sequence.done() and i < max_iterations: + sequence.act() + i += 1 + + assert outputs == ["a", "b", "c"] + + +def test_fms_behaviour(): + """Test the finite-state machine behaviour.""" + outputs = [] + + class MyFSMBehaviour(FSMBehaviour): + + def setup(self) -> None: + pass + + def teardown(self) -> None: + pass + + class SimpleOneShotBehaviour(State): + + def __init__(self, id: int, **kwargs): + super().__init__(**kwargs) + self.id = id + self.executed = False + + def setup(self) -> None: + pass + + def teardown(self) -> None: + pass + + def act(self) -> None: + outputs.append(self.id) + self.next_state = str(self.id + 1) + self.executed = True + + def done(self) -> bool: + return self.executed + + # TODO let the initialization of a behaviour action from constructor + a = SimpleOneShotBehaviour(0, skill_context=None) + b = SimpleOneShotBehaviour(1, skill_context=None) + c = SimpleOneShotBehaviour(2, skill_context=None) + fsm = MyFSMBehaviour(skill_context=None) + fsm.register_state(str(a.id), a, initial=True) + fsm.register_state(str(b.id), b) + fsm.register_state(str(c.id), c) + + max_iterations = 10 + i = 0 + while not fsm.done() and i < max_iterations: + fsm.act() + i += 1 + + assert outputs == [0, 1, 2] diff --git a/tests/test_skills/test_error.py b/tests/test_skills/test_error.py index e11220f5a2..64128dbe57 100644 --- a/tests/test_skills/test_error.py +++ b/tests/test_skills/test_error.py @@ -23,20 +23,20 @@ from threading import Thread from aea.aea import AEA -from aea.connections.local.connection import LocalNode from aea.crypto.wallet import Wallet from aea.crypto.ledger_apis import LedgerApis from aea.mail.base import Envelope from aea.protocols.default.message import DefaultMessage from aea.protocols.default.serialization import DefaultSerializer -from aea.protocols.fipa.message import FIPAMessage -from aea.protocols.fipa.serialization import FIPASerializer -from aea.protocols.oef.message import OEFMessage from aea.registries.base import Resources from aea.skills.base import SkillContext from aea.skills.error.behaviours import ErrorBehaviour from aea.skills.error.handlers import ErrorHandler from aea.skills.error.tasks import ErrorTask +from packages.connections.local.connection import LocalNode +from packages.protocols.fipa.message import FIPAMessage +from packages.protocols.fipa.serialization import FIPASerializer +from packages.protocols.oef.message import OEFMessage from ..conftest import CUR_PATH, DummyConnection @@ -51,7 +51,7 @@ def setup_class(cls): cls.wallet = Wallet({'default': private_key_pem_path}) cls.ledger_apis = LedgerApis({}) cls.agent_name = "Agent0" - cls.public_key = cls.wallet.public_keys['default'] + cls.address = cls.wallet.addresses['default'] cls.connection = DummyConnection() cls.connections = [cls.connection] @@ -67,30 +67,28 @@ def setup_class(cls): def test_error_handler_handle(self): """Test the handle function.""" msg = FIPAMessage(message_id=0, dialogue_reference=(str(0), ''), target=0, performative=FIPAMessage.Performative.ACCEPT) - msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=self.public_key, - protocol_id=FIPAMessage.protocol_id, message=msg_bytes) - self.my_error_handler.handle(message=msg, sender=envelope.sender) + msg.counterparty = "a_counterparty" + self.my_error_handler.handle(message=msg) def test_error_skill_unsupported_protocol(self): """Test the unsupported error message.""" msg = FIPAMessage(message_id=0, dialogue_reference=(str(0), ''), target=0, performative=FIPAMessage.Performative.ACCEPT) msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=self.public_key, + envelope = Envelope(to=self.address, sender=self.address, protocol_id=FIPAMessage.protocol_id, message=msg_bytes) self.my_error_handler.send_unsupported_protocol(envelope) envelope = self.my_aea.inbox.get(block=True, timeout=1.0) msg = DefaultSerializer().decode(envelope.message) - assert msg.get("type") == DefaultMessage.Type.ERROR - assert msg.get("error_code") == DefaultMessage.ErrorCode.UNSUPPORTED_PROTOCOL.value + assert msg.type == DefaultMessage.Type.ERROR + assert msg.error_code == DefaultMessage.ErrorCode.UNSUPPORTED_PROTOCOL def test_error_decoding_error(self): """Test the decoding error.""" msg = FIPAMessage(message_id=0, dialogue_reference=(str(0), ''), target=0, performative=FIPAMessage.Performative.ACCEPT) msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=self.public_key, + envelope = Envelope(to=self.address, sender=self.address, protocol_id=DefaultMessage.protocol_id, message=msg_bytes) self.my_error_handler.send_decoding_error(envelope) @@ -104,7 +102,7 @@ def test_error_invalid_message(self): """Test the invalid message.""" msg = FIPAMessage(message_id=0, dialogue_reference=(str(0), ''), target=0, performative=FIPAMessage.Performative.ACCEPT) msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=self.public_key, + envelope = Envelope(to=self.address, sender=self.address, protocol_id=OEFMessage.protocol_id, message=msg_bytes) self.my_error_handler.send_invalid_message(envelope) @@ -118,7 +116,7 @@ def test_error_unsupported_skill(self): """Test the unsupported skill.""" msg = FIPAMessage(message_id=0, dialogue_reference=(str(0), ''), target=0, performative=FIPAMessage.Performative.ACCEPT) msg_bytes = FIPASerializer().encode(msg) - envelope = Envelope(to=self.public_key, sender=self.public_key, + envelope = Envelope(to=self.address, sender=self.address, protocol_id=DefaultMessage.protocol_id, message=msg_bytes) self.my_error_handler.send_unsupported_skill(envelope=envelope) diff --git a/tox.ini b/tox.ini index fdc7a0a77a..787b80efc2 100644 --- a/tox.ini +++ b/tox.ini @@ -5,30 +5,34 @@ ignore_basepython_conflict = True [testenv:py38] basepython = python3.8 -whitelist_externals = pipenv, pip, pytest, gym, numpy deps = + pipenv pytest pytest-cov pytest-asyncio Cython docker + oef==0.8.1 + colorlog gym numpy commands = pip install git+https://github.com/pytoolz/cytoolz.git#egg=cytoolz==0.10.1.dev0 pip install -e .[all] + pip install -i https://test.pypi.org/simple/ fetch-p2p-api==0.0.1 pytest --doctest-modules aea packages/protocols packages/connections tests/ --cov-report=html --cov-report=term --cov=aea --cov=packages/protocols --cov=packages/connections {posargs} [testenv:py37] basepython = python3.7 -whitelist_externals = pipenv, pip, pytest, gym, numpy deps = + pipenv pytest pytest-cov pytest-asyncio gym numpy + tensorflow commands = pipenv install @@ -37,13 +41,14 @@ commands = [testenv:py36] basepython = python3.6 -whitelist_externals = pipenv, pip, pytest, gym, numpy deps = + pipenv pytest pytest-cov pytest-asyncio gym numpy + tensorflow commands = pipenv install