diff --git a/HISTORY.rst b/HISTORY.rst index c231fc9b..6accece0 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -14,4 +14,12 @@ Release History 0.1.2 (2019-07-08) ------------------- -- Minor updates, many bug fixes, no breaking changes. \ No newline at end of file +- Minor updates, many bug fixes, no breaking changes. + +0.1.3 (2019-08-09) +------------------- + +- Updated to OEF SDK version 0.6 +- Improved documentation by adding detailed guides +- Added more tools/features to support the developer (e.g. the launcher app) + diff --git a/LICENSE b/LICENSE index 261eeb9e..a8c7d736 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 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. diff --git a/MANIFEST.in b/MANIFEST.in index 047a4324..47eb1645 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,3 +8,4 @@ recursive-include templates * recursive-include sandbox * recursive-include simulation * recursive-include scripts * +recursive-include oef_search_pluto_scripts * diff --git a/Pipfile b/Pipfile index 034fcbdf..d288db05 100644 --- a/Pipfile +++ b/Pipfile @@ -3,12 +3,18 @@ name = "pypi" url = "https://pypi.org/simple" verify_ssl = true +[[source]] +url = "https://test.pypi.org/simple" +verify_ssl = true +name = "test-pypi" + [dev-packages] flake8 = "*" ipython = "*" jupyter = "*" pytest = "*" tox = "*" +tox-pipenv = "*" pytest-cov = "*" docker = "*" flake8-docstrings = "*" @@ -20,11 +26,14 @@ python-dateutil = "*" visdom = "*" cryptography = "*" base58 = "*" -oef = "==0.4.0" +fetchai-ledger-api = {git = "https://github.com/fetchai/ledger-api-py.git", +oef = {version="*", index="test-pypi"} sphinxcontrib-mermaid = "*" sphinxcontrib-apidoc = "*" sphinx = "*" nbsphinx = "*" +flask-restful = "*" +wtforms = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 20d17c6e..65d8f67a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "69b528c350c986cf7a155f251999f650b4b52b836998ed4d63faeda7c65a13d1" + "sha256": "6117d1728abb2c7068f63e101220ef6089d74955f3efd21ae650b812e279bdd6" }, "pipfile-spec": 6, "requires": { @@ -12,6 +12,11 @@ "name": "pypi", "url": "https://pypi.org/simple", "verify_ssl": true + }, + { + "name": "test-pypi", + "url": "https://test.pypi.org/simple", + "verify_ssl": true } ] }, @@ -23,6 +28,13 @@ ], "version": "==0.7.12" }, + "aniso8601": { + "hashes": [ + "sha256:513d2b6637b7853806ae79ffaca6f3e8754bdd547048f5ccc1420aec4b714f1e", + "sha256:d10a4bf949f619f719b227ef5386e31f49a2b6d453004b21f02661ccc8670c7b" + ], + "version": "==7.0.0" + }, "asn1crypto": { "hashes": [ "sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87", @@ -107,6 +119,20 @@ ], "version": "==3.0.4" }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, + "colorlog": { + "hashes": [ + "sha256:3cf31b25cbc8f86ec01fef582ef3b840950dea414084ed19ab922c8b493f9b42", + "sha256:450f52ea2a2b6ebb308f034ea9a9b15cea51e65650593dca1da3eb792e4e4981" + ], + "version": "==4.0.2" + }, "cryptography": { "hashes": [ "sha256:24b61e5fcb506424d3ec4e18bca995833839bf13c59fc43e530e488f28d46b8c", @@ -152,11 +178,11 @@ }, "docutils": { "hashes": [ - "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", - "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", - "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" + "sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0", + "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", + "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99" ], - "version": "==0.14" + "version": "==0.15.2" }, "entrypoints": { "hashes": [ @@ -165,6 +191,32 @@ ], "version": "==0.3" }, + "fetchai-ledger-api": { + "git": "https://github.com/fetchai/ledger-api-py.git", + "ref": "a5f513a9247524ee767e1dd1519aad1a1f4383e3" + }, + "flask": { + "hashes": [ + "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", + "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" + ], + "version": "==1.1.1" + }, + "flask-restful": { + "hashes": [ + "sha256:ecd620c5cc29f663627f99e04f17d1f16d095c83dc1d618426e2ad68b03092f8", + "sha256:f8240ec12349afe8df1db168ea7c336c4e5b0271a36982bff7394f93275f2ca9" + ], + "index": "pypi", + "version": "==0.3.7" + }, + "graphviz": { + "hashes": [ + "sha256:6d0f69c107cfdc9bd1df3763fad99569bbcba29d0c52ffcbc6f266621d8bf709", + "sha256:914b8b124942d82e3e1dcef499c9fe77c10acd3d18a1cfeeb2b9de05f6d24805" + ], + "version": "==0.11.1" + }, "idna": { "hashes": [ "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", @@ -186,6 +238,13 @@ ], "version": "==0.2.0" }, + "itsdangerous": { + "hashes": [ + "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", + "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" + ], + "version": "==1.1.0" + }, "jinja2": { "hashes": [ "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", @@ -195,10 +254,10 @@ }, "jsonschema": { "hashes": [ - "sha256:0c0a81564f181de3212efa2d17de1910f8732fa1b71c42266d983cd74304e20d", - "sha256:a5f6559964a3851f59040d3b961de5e68e70971afb88ba519d27e6a039efff1a" + "sha256:5f9c0a719ca2ce14c5de2fd350a64fd2d13e8539db29836a86adc990bb1a068f", + "sha256:8d4a2b7b6c2237e0199c8ea1a6d3e05bf118e289ae2b9d7ba444182a2959560d" ], - "version": "==3.0.1" + "version": "==3.0.2" }, "jupyter-core": { "hashes": [ @@ -319,47 +378,40 @@ }, "numpy": { "hashes": [ - "sha256:0778076e764e146d3078b17c24c4d89e0ecd4ac5401beff8e1c87879043a0633", - "sha256:141c7102f20abe6cf0d54c4ced8d565b86df4d3077ba2343b61a6db996cefec7", - "sha256:14270a1ee8917d11e7753fb54fc7ffd1934f4d529235beec0b275e2ccf00333b", - "sha256:27e11c7a8ec9d5838bc59f809bfa86efc8a4fd02e58960fa9c49d998e14332d5", - "sha256:2a04dda79606f3d2f760384c38ccd3d5b9bb79d4c8126b67aff5eb09a253763e", - "sha256:3c26010c1b51e1224a3ca6b8df807de6e95128b0908c7e34f190e7775455b0ca", - "sha256:52c40f1a4262c896420c6ea1c6fda62cf67070e3947e3307f5562bd783a90336", - "sha256:6e4f8d9e8aa79321657079b9ac03f3cf3fd067bf31c1cca4f56d49543f4356a5", - "sha256:7242be12a58fec245ee9734e625964b97cf7e3f2f7d016603f9e56660ce479c7", - "sha256:7dc253b542bfd4b4eb88d9dbae4ca079e7bf2e2afd819ee18891a43db66c60c7", - "sha256:94f5bd885f67bbb25c82d80184abbf7ce4f6c3c3a41fbaa4182f034bba803e69", - "sha256:a89e188daa119ffa0d03ce5123dee3f8ffd5115c896c2a9d4f0dbb3d8b95bfa3", - "sha256:ad3399da9b0ca36e2f24de72f67ab2854a62e623274607e37e0ce5f5d5fa9166", - "sha256:b0348be89275fd1d4c44ffa39530c41a21062f52299b1e3ee7d1c61f060044b8", - "sha256:b5554368e4ede1856121b0dfa35ce71768102e4aa55e526cb8de7f374ff78722", - "sha256:cbddc56b2502d3f87fda4f98d948eb5b11f36ff3902e17cb6cc44727f2200525", - "sha256:d79f18f41751725c56eceab2a886f021d70fd70a6188fd386e29a045945ffc10", - "sha256:dc2ca26a19ab32dc475dbad9dfe723d3a64c835f4c23f625c2b6566ca32b9f29", - "sha256:dd9bcd4f294eb0633bb33d1a74febdd2b9018b8b8ed325f861fffcd2c7660bb8", - "sha256:e8baab1bc7c9152715844f1faca6744f2416929de10d7639ed49555a85549f52", - "sha256:ec31fe12668af687b99acf1567399632a7c47b0e17cfb9ae47c098644ef36797", - "sha256:f12b4f7e2d8f9da3141564e6737d79016fe5336cc92de6814eba579744f65b0a", - "sha256:f58ac38d5ca045a377b3b377c84df8175ab992c970a53332fa8ac2373df44ff7" + "sha256:03e311b0a4c9f5755da7d52161280c6a78406c7be5c5cc7facfbcebb641efb7e", + "sha256:0cdd229a53d2720d21175012ab0599665f8c9588b3b8ffa6095dd7b90f0691dd", + "sha256:312bb18e95218bedc3563f26fcc9c1c6bfaaf9d453d15942c0839acdd7e4c473", + "sha256:464b1c48baf49e8505b1bb754c47a013d2c305c5b14269b5c85ea0625b6a988a", + "sha256:5adfde7bd3ee4864536e230bcab1c673f866736698724d5d28c11a4d63672658", + "sha256:7724e9e31ee72389d522b88c0d4201f24edc34277999701ccd4a5392e7d8af61", + "sha256:8d36f7c53ae741e23f54793ffefb2912340b800476eb0a831c6eb602e204c5c4", + "sha256:910d2272403c2ea8a52d9159827dc9f7c27fb4b263749dca884e2e4a8af3b302", + "sha256:951fefe2fb73f84c620bec4e001e80a80ddaa1b84dce244ded7f1e0cbe0ed34a", + "sha256:9588c6b4157f493edeb9378788dcd02cb9e6a6aeaa518b511a1c79d06cbd8094", + "sha256:9ce8300950f2f1d29d0e49c28ebfff0d2f1e2a7444830fbb0b913c7c08f31511", + "sha256:be39cca66cc6806652da97103605c7b65ee4442c638f04ff064a7efd9a81d50a", + "sha256:c3ab2d835b95ccb59d11dfcd56eb0480daea57cdf95d686d22eff35584bc4554", + "sha256:eb0fc4a492cb896346c9e2c7a22eae3e766d407df3eb20f4ce027f23f76e4c54", + "sha256:ec0c56eae6cee6299f41e780a0280318a93db519bbb2906103c43f3e2be1206c", + "sha256:f4e4612de60a4f1c4d06c8c2857cdcb2b8b5289189a12053f37d3f41f06c60d0" ], "index": "pypi", - "version": "==1.16.4" + "version": "==1.17.0" }, "oef": { "hashes": [ - "sha256:8ba474ba738d2ecd81c6db77ff3f1e1ecb0c3e572f8271bd57398487fa17df26", - "sha256:c97416494efded1994a0baf93a66935d86e82e70b652c0e44532aab1f5d84915" + "sha256:1944b11632c4af841e373f52e3e96941821f6b4bb4dbc430f790c1b436880618", + "sha256:cab9e251bfd1476f332de5046a83e0c01d0ce75ddfefb1bd614359a2b904d726" ], - "index": "pypi", - "version": "==0.4.0" + "index": "test-pypi", + "version": "==0.6.0" }, "packaging": { "hashes": [ - "sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af", - "sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3" + "sha256:a7ac867b97fdc07ee80a8058fe4435ccd274ecc3b0ed61d852d7d53055528cf9", + "sha256:c491ca87294da7cc01902edbe30a5bc6c4c28172b5138ab4e4aa1b9d7bfaeafe" ], - "version": "==19.0" + "version": "==19.1" }, "pandocfilters": { "hashes": [ @@ -369,10 +421,10 @@ }, "pbr": { "hashes": [ - "sha256:9181e2a34d80f07a359ff1d0504fad3a47e00e1cf2c475b0aa7dcb030af54c40", - "sha256:94bdc84da376b3dd5061aa0c3b6faffe943ee2e56fa4ff9bd63e1643932f34fc" + "sha256:56e52299170b9492513c64be44736d27a512fa7e606f21942160b68ce510b4bc", + "sha256:9b321c204a88d8ab5082699469f52cc94c5da45c51f114113d01b3d993c24cdf" ], - "version": "==5.3.1" + "version": "==5.4.2" }, "pillow": { "hashes": [ @@ -407,26 +459,28 @@ }, "protobuf": { "hashes": [ - "sha256:03f43eac9d5b651f976e91cf46a25b75e5779d98f0f4114b0abfed83376d75f8", - "sha256:0c94b21e6de01362f91a86b372555d22a60b59708599ca9d5032ae9fdf8e3538", - "sha256:2d2a9f30f61f4063fadd7fb68a2510a6939b43c0d6ceeec5c4704f22225da28e", - "sha256:34a0b05fca061e4abb77dd180209f68d8637115ff319f51e28a6a9382d69853a", - "sha256:358710fd0db25372edcf1150fa691f48376a134a6c69ce29f38f185eea7699e6", - "sha256:41e47198b94c27ba05a08b4a95160656105745c462af574e4bcb0807164065c0", - "sha256:8c61cc8a76e9d381c665aecc5105fa0f1878cf7db8b5cd17202603bcb386d0fc", - "sha256:a6eebc4db759e58fdac02efcd3028b811effac881d8a5bad1996e4e8ee6acb47", - "sha256:a9c12f7c98093da0a46ba76ec40ace725daa1ac4038c41e4b1466afb5c45bb01", - "sha256:cb95068492ba0859b8c9e61fa8ba206a83c64e5d0916fb4543700b2e2b214115", - "sha256:cd98476ce7bb4dcd6a7b101f5eecdc073dafea19f311e36eb8fba1a349346277", - "sha256:ce64cfbea18c535176bdaa10ba740c0fc4c6d998a3f511c17bedb0ae4b3b167c", - "sha256:dcbb59eac73fd454e8f2c5fba9e3d3320fd4707ed6a9d3ea3717924a6f0903ea", - "sha256:dd67f34458ae716029e2a71ede998e9092493b62a519236ca52e3c5202096c87", - "sha256:e3c96056eb5b7284a20e256cb0bf783c8f36ad82a4ae5434a7b7cd02384144a7", - "sha256:f612d584d7a27e2f39e7b17878430a959c1bc09a74ba09db096b468558e5e126", - "sha256:f6de8a7d6122297b81566e5bd4df37fd5d62bec14f8f90ebff8ede1c9726cd0a", - "sha256:fa529d9261682b24c2aaa683667253175c9acebe0a31105394b221090da75832" - ], - "version": "==3.8.0" + "sha256:05c36022fef3c7d3562ac22402965c0c2b9fe8421f459bb377323598996e407f", + "sha256:139b7eadcca0a861d60b523cb37d9475505e0dfb07972436b15407c2b968d87e", + "sha256:15f683006cb77fb849b1f561e509b03dd2b7dcc749086b8dd1831090d0ba4740", + "sha256:2ad566b7b7cdd8717c7af1825e19f09e8fef2787b77fcb979588944657679604", + "sha256:35cfcf97642ef62108e10a9431c77733ec7eaab8e32fe4653de20403429907cb", + "sha256:387822859ecdd012fdc25ec879f7f487da6e1d5b1ae6115e227e6be208836f71", + "sha256:4df14cbe1e7134afcfdbb9f058949e31c466de27d9b2f7fb4da9e0b67231b538", + "sha256:573e3fe6582e0ec4e0ed6576127893f0824c48f01bce48e10114f04f3e953af8", + "sha256:586c4ca37a7146d4822c700059f150ac3445ce0aef6f3ea258640838bb892dc2", + "sha256:58b11e530e954d29ab3180c48dc558a409f705bf16739fd4e0d3e07924ad7add", + "sha256:63c8c98ccb8c95f41c18fb829aeeab21c6249adee4ed75354125bdc44488f30e", + "sha256:72edcbacd0c73eef507d2ff1af99a6c27df18e66a3ff4351e401182e4de62b03", + "sha256:83dc8a561b3b954fd7002c690bb83278b8d1742a1e28abba9aaef28b0c8b437d", + "sha256:913171ecc84c2726b86574e40549a0ea619d569657c5a5ff782a3be7d81401a5", + "sha256:aabb7c741d3416671c3e6fe7c52970a226e6a8274417a97d7d795f953fadef36", + "sha256:b3452bbda12b1cbe2187d416779de07b2ab4c497d83a050e43c344778763721d", + "sha256:c5d5b8d4a9212338297fa1fa44589f69b470c0ba1d38168b432d577176b386a8", + "sha256:d7ec7dda851e7f259ff8fdd844f88a7b01496ea2fc907569a01ec7b9f28527f4", + "sha256:d86ee389c2c4fc3cebabb8ce83a8e97b6b3b5dc727b7419c1ccdc7b6e545a233", + "sha256:f2db8c754de788ab8be5e108e1e967c774c0942342b4f8aaaf14063889a6cfdc" + ], + "version": "==3.9.0" }, "pycparser": { "hashes": [ @@ -443,16 +497,16 @@ }, "pyparsing": { "hashes": [ - "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a", - "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03" + "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", + "sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4" ], - "version": "==2.4.0" + "version": "==2.4.2" }, "pyrsistent": { "hashes": [ - "sha256:16692ee739d42cf5e39cef8d27649a8c1fdb7aa99887098f1460057c5eb75c3a" + "sha256:34b47fa169d6006b32e99d4b3c4031f155e6e68ebcc107d6454852e8e0ee6533" ], - "version": "==0.15.2" + "version": "==0.15.4" }, "python-dateutil": { "hashes": [ @@ -464,10 +518,10 @@ }, "pytz": { "hashes": [ - "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", - "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141" + "sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32", + "sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7" ], - "version": "==2019.1" + "version": "==2019.2" }, "pyzmq": { "hashes": [ @@ -614,6 +668,7 @@ }, "torchfile": { "hashes": [ + "sha256:1955f516824481333f4b5a84367330b9dc6bb4670595419e58078796a06d8a23", "sha256:a53dfe134b737845a9f2cb24fe0585317874f965932cebdb0439d13c8da4136e" ], "version": "==0.1.0" @@ -664,9 +719,32 @@ "sha256:1fd5520878b68b84b5748bb30e592b10d0a91529d5383f74f4964e72b297fd3a" ], "version": "==0.56.0" + }, + "werkzeug": { + "hashes": [ + "sha256:87ae4e5b5366da2347eb3116c0e6c681a0e939a33b2805e2c0cbd282664932c4", + "sha256:a13b74dd3c45f758d4ebdb224be8f1ab8ef58b3c0ffc1783a8c7d9f4f50227e6" + ], + "version": "==0.15.5" + }, + "wtforms": { + "hashes": [ + "sha256:0cdbac3e7f6878086c334aa25dc5a33869a3954e9d1e015130d65a69309b3b61", + "sha256:e3ee092c827582c50877cdbd49e9ce6d2c5c1f6561f849b3b068c1b8029626f1" + ], + "index": "pypi", + "version": "==2.2.1" } }, "develop": { + "appnope": { + "hashes": [ + "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", + "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71" + ], + "markers": "sys_platform == 'darwin'", + "version": "==0.1.0" + }, "atomicwrites": { "hashes": [ "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", @@ -711,39 +789,40 @@ }, "coverage": { "hashes": [ - "sha256:3684fabf6b87a369017756b551cef29e505cb155ddb892a7a29277b978da88b9", - "sha256:39e088da9b284f1bd17c750ac672103779f7954ce6125fd4382134ac8d152d74", - "sha256:3c205bc11cc4fcc57b761c2da73b9b72a59f8d5ca89979afb0c1c6f9e53c7390", - "sha256:465ce53a8c0f3a7950dfb836438442f833cf6663d407f37d8c52fe7b6e56d7e8", - "sha256:48020e343fc40f72a442c8a1334284620f81295256a6b6ca6d8aa1350c763bbe", - "sha256:5296fc86ab612ec12394565c500b412a43b328b3907c0d14358950d06fd83baf", - "sha256:5f61bed2f7d9b6a9ab935150a6b23d7f84b8055524e7be7715b6513f3328138e", - "sha256:68a43a9f9f83693ce0414d17e019daee7ab3f7113a70c79a3dd4c2f704e4d741", - "sha256:6b8033d47fe22506856fe450470ccb1d8ba1ffb8463494a15cfc96392a288c09", - "sha256:7ad7536066b28863e5835e8cfeaa794b7fe352d99a8cded9f43d1161be8e9fbd", - "sha256:7bacb89ccf4bedb30b277e96e4cc68cd1369ca6841bde7b005191b54d3dd1034", - "sha256:839dc7c36501254e14331bcb98b27002aa415e4af7ea039d9009409b9d2d5420", - "sha256:8f9a95b66969cdea53ec992ecea5406c5bd99c9221f539bca1e8406b200ae98c", - "sha256:932c03d2d565f75961ba1d3cec41ddde00e162c5b46d03f7423edcb807734eab", - "sha256:988529edadc49039d205e0aa6ce049c5ccda4acb2d6c3c5c550c17e8c02c05ba", - "sha256:998d7e73548fe395eeb294495a04d38942edb66d1fa61eb70418871bc621227e", - "sha256:9de60893fb447d1e797f6bf08fdf0dbcda0c1e34c1b06c92bd3a363c0ea8c609", - "sha256:9e80d45d0c7fcee54e22771db7f1b0b126fb4a6c0a2e5afa72f66827207ff2f2", - "sha256:a545a3dfe5082dc8e8c3eb7f8a2cf4f2870902ff1860bd99b6198cfd1f9d1f49", - "sha256:a5d8f29e5ec661143621a8f4de51adfb300d7a476224156a39a392254f70687b", - "sha256:aca06bfba4759bbdb09bf52ebb15ae20268ee1f6747417837926fae990ebc41d", - "sha256:bb23b7a6fd666e551a3094ab896a57809e010059540ad20acbeec03a154224ce", - "sha256:bfd1d0ae7e292105f29d7deaa9d8f2916ed8553ab9d5f39ec65bcf5deadff3f9", - "sha256:c62ca0a38958f541a73cf86acdab020c2091631c137bd359c4f5bddde7b75fd4", - "sha256:c709d8bda72cf4cd348ccec2a4881f2c5848fd72903c185f363d361b2737f773", - "sha256:c968a6aa7e0b56ecbd28531ddf439c2ec103610d3e2bf3b75b813304f8cb7723", - "sha256:df785d8cb80539d0b55fd47183264b7002077859028dfe3070cf6359bf8b2d9c", - "sha256:f406628ca51e0ae90ae76ea8398677a921b36f0bd71aab2099dfed08abd0322f", - "sha256:f46087bbd95ebae244a0eda01a618aff11ec7a069b15a3ef8f6b520db523dcf1", - "sha256:f8019c5279eb32360ca03e9fac40a12667715546eed5c5eb59eb381f2f501260", - "sha256:fc5f4d209733750afd2714e9109816a29500718b32dd9a5db01c0cb3a019b96a" - ], - "version": "==4.5.3" + "sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", + "sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", + "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", + "sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", + "sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", + "sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", + "sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", + "sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", + "sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", + "sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", + "sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", + "sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", + "sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", + "sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", + "sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", + "sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", + "sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", + "sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", + "sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", + "sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", + "sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", + "sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", + "sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", + "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", + "sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", + "sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", + "sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", + "sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", + "sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", + "sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", + "sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", + "sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025" + ], + "version": "==4.5.4" }, "decorator": { "hashes": [ @@ -776,6 +855,7 @@ }, "filelock": { "hashes": [ + "sha256:0abccd31bbba7275d0c28a07eceedbedd9b892b8c479a2c805732ca8fe3bc167", "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836" ], @@ -783,19 +863,19 @@ }, "flake8": { "hashes": [ - "sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661", - "sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8" + "sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", + "sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696" ], "index": "pypi", - "version": "==3.7.7" + "version": "==3.7.8" }, "flake8-docstrings": { "hashes": [ - "sha256:4e0ce1476b64e6291520e5570cf12b05016dd4e8ae454b8a8a9a48bc5f84e1cd", - "sha256:8436396b5ecad51a122a2c99ba26e5b4e623bf6e913b0fea0cb6c2c4050f91eb" + "sha256:3ad372b641f4c8e70c7465f067aed4ff8bf1e9347fce14f9eb71ed816db36257", + "sha256:d8d72ccd5807c1ab9ff1466cb9bece0c4d94b8669e9bc4f472abc80dbc5d399e" ], "index": "pypi", - "version": "==1.3.0" + "version": "==1.3.1" }, "flake8-polyfill": { "hashes": [ @@ -813,10 +893,10 @@ }, "importlib-metadata": { "hashes": [ - "sha256:6dfd58dfe281e8d240937776065dd3624ad5469c835248219bd16cf2e12dbeb7", - "sha256:cb6ee23b46173539939964df59d3d72c3e0c1b5d54b84f1d8a7e912fe43612db" + "sha256:23d3d873e008a513952355379d93cbcab874c58f4f034ff657c7a87422fa64e8", + "sha256:80d2de76188eabfbfcf27e6a37342c2827801e59c4cc14b0371c56fed43820e3" ], - "version": "==0.18" + "version": "==0.19" }, "ipykernel": { "hashes": [ @@ -827,11 +907,11 @@ }, "ipython": { "hashes": [ - "sha256:11067ab11d98b1e6c7f0993506f7a5f8a91af420f7e82be6575fcb7a6ca372a0", - "sha256:60bc55c2c1d287161191cc2469e73c116d9b634cff25fe214a43cba7cec94c79" + "sha256:1d3a1692921e932751bc1a1f7bb96dc38671eeefdc66ed33ee4cbc57e92a410e", + "sha256:537cd0176ff6abd06ef3e23f2d0c4c2c8a4d9277b7451544c6cbf56d1c79a83d" ], "index": "pypi", - "version": "==7.6.1" + "version": "==7.7.0" }, "ipython-genutils": { "hashes": [ @@ -842,17 +922,17 @@ }, "ipywidgets": { "hashes": [ - "sha256:cb263c6974aca902d00a435711823bb4aaf6614a5f997f517e15fa84151e8fa2", - "sha256:eab6060f20f7f10d91f6efc8d33f9fd22133406980fcaee2738d836a910402f4" + "sha256:13ffeca438e0c0f91ae583dc22f50379b9d6b28390ac7be8b757140e9a771516", + "sha256:e945f6e02854a74994c596d9db83444a1850c01648f1574adf144fbbabe05c97" ], - "version": "==7.5.0" + "version": "==7.5.1" }, "jedi": { "hashes": [ - "sha256:49ccb782651bb6f7009810d17a3316f8867dde31654c750506970742e18b553d", - "sha256:79d0f6595f3846dffcbe667cc6dc821b96e5baa8add125176c31a3917eb19d58" + "sha256:53c850f1a7d3cfcd306cc513e2450a54bdf5cacd7604b74e42dd1f0758eaaf36", + "sha256:e07457174ef7cb2342ff94fa56484fe41cec7ef69b0059f01d3f812379cb6f7c" ], - "version": "==0.14.0" + "version": "==0.14.1" }, "jinja2": { "hashes": [ @@ -863,10 +943,10 @@ }, "jsonschema": { "hashes": [ - "sha256:0c0a81564f181de3212efa2d17de1910f8732fa1b71c42266d983cd74304e20d", - "sha256:a5f6559964a3851f59040d3b961de5e68e70971afb88ba519d27e6a039efff1a" + "sha256:5f9c0a719ca2ce14c5de2fd350a64fd2d13e8539db29836a86adc990bb1a068f", + "sha256:8d4a2b7b6c2237e0199c8ea1a6d3e05bf118e289ae2b9d7ba444182a2959560d" ], - "version": "==3.0.1" + "version": "==3.0.2" }, "jupyter": { "hashes": [ @@ -879,10 +959,10 @@ }, "jupyter-client": { "hashes": [ - "sha256:b5f9cb06105c1d2d30719db5ffb3ea67da60919fb68deaefa583deccd8813551", - "sha256:c44411eb1463ed77548bc2d5ec0d744c9b81c4a542d9637c7a52824e2121b987" + "sha256:73a809a2964afa07adcc1521537fddb58c2ffbb7e84d53dc5901cf80480465b3", + "sha256:98e8af5edff5d24e4d31e73bc21043130ae9d955a91aa93fc0bc3b1d0f7b5880" ], - "version": "==5.2.4" + "version": "==5.3.1" }, "jupyter-console": { "hashes": [ @@ -947,10 +1027,10 @@ }, "more-itertools": { "hashes": [ - "sha256:3ad685ff8512bf6dc5a8b82ebf73543999b657eded8c11803d9ba6b648986f4d", - "sha256:8bb43d1f51ecef60d81854af61a3a880555a14643691cc4b64a6ee269c78f09a" + "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", + "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" ], - "version": "==7.1.0" + "version": "==7.2.0" }, "nbconvert": { "hashes": [ @@ -968,17 +1048,17 @@ }, "notebook": { "hashes": [ - "sha256:573e0ae650c5d76b18b6e564ba6d21bf321d00847de1d215b418acb64f056eb8", - "sha256:f64fa6624d2323fbef6210a621817d6505a45d0d4a9367f1843b20a38a4666ee" + "sha256:0be97e939cec73cde37fc4d2a606a6f497a9addf3afcf61a09a21b0c35e699c5", + "sha256:5c16dbf4fa824db19de43637ebfb24bcbd3b4f646e5d6a0414ed3a376d6bc951" ], - "version": "==5.7.8" + "version": "==6.0.0" }, "packaging": { "hashes": [ - "sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af", - "sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3" + "sha256:a7ac867b97fdc07ee80a8058fe4435ccd274ecc3b0ed61d852d7d53055528cf9", + "sha256:c491ca87294da7cc01902edbe30a5bc6c4c28172b5138ab4e4aa1b9d7bfaeafe" ], - "version": "==19.0" + "version": "==19.1" }, "pandocfilters": { "hashes": [ @@ -988,10 +1068,10 @@ }, "parso": { "hashes": [ - "sha256:5052bb33be034cba784193e74b1cde6ebf29ae8b8c1e4ad94df0c4209bfc4826", - "sha256:db5881df1643bf3e66c097bfd8935cf03eae73f4cb61ae4433c9ea4fb6613446" + "sha256:63854233e1fadb5da97f2744b6b24346d2750b85965e7e399bec1620232797dc", + "sha256:666b0ee4a7a1220f65d367617f2cd3ffddff3e205f3f16a0284df30e774c2a9c" ], - "version": "==0.5.0" + "version": "==0.5.1" }, "pexpect": { "hashes": [ @@ -1008,6 +1088,14 @@ ], "version": "==0.7.5" }, + "pipenv": { + "hashes": [ + "sha256:56ad5f5cb48f1e58878e14525a6e3129d4306049cb76d2f6a3e95df0d5fc6330", + "sha256:7df8e33a2387de6f537836f48ac6fcd94eda6ed9ba3d5e3fd52e35b5bc7ff49e", + "sha256:a673e606e8452185e9817a987572b55360f4d28b50831ef3b42ac3cab3fee846" + ], + "version": "==2018.11.26" + }, "pluggy": { "hashes": [ "sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc", @@ -1053,11 +1141,9 @@ }, "pydocstyle": { "hashes": [ - "sha256:2258f9b0df68b97bf3a6c29003edc5238ff8879f1efb6f1999988d934e432bd8", - "sha256:5741c85e408f9e0ddf873611085e819b809fca90b619f5fd7f34bd4959da3dd4", - "sha256:ed79d4ec5e92655eccc21eb0c6cf512e69512b4a97d215ace46d17e4990f2039" + "sha256:58c421dd605eec0bce65df8b8e5371bb7ae421582cdf0ba8d9435ac5b0ffc36a" ], - "version": "==3.0.0" + "version": "==4.0.0" }, "pyflakes": { "hashes": [ @@ -1075,24 +1161,24 @@ }, "pyparsing": { "hashes": [ - "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a", - "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03" + "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", + "sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4" ], - "version": "==2.4.0" + "version": "==2.4.2" }, "pyrsistent": { "hashes": [ - "sha256:16692ee739d42cf5e39cef8d27649a8c1fdb7aa99887098f1460057c5eb75c3a" + "sha256:34b47fa169d6006b32e99d4b3c4031f155e6e68ebcc107d6454852e8e0ee6533" ], - "version": "==0.15.2" + "version": "==0.15.4" }, "pytest": { "hashes": [ - "sha256:2878de8ae1c79a62c012da6186b88ff0562ea96ce29c4208d2a9b11d9f607df1", - "sha256:95b700cf21ed5b7e91bce7a6b5a573b2e3ef7b3643d00f681d8f9c4672f9fbdf" + "sha256:6ef6d06de77ce2961156013e9dff62f1b2688aa04d0dc244299fe7d67e09370d", + "sha256:a736fed91c12681a7b34617c8fcefe39ea04599ca72c608751c31d89579a3f77" ], "index": "pypi", - "version": "==5.0.0" + "version": "==5.0.1" }, "pytest-cov": { "hashes": [ @@ -1142,10 +1228,10 @@ }, "qtconsole": { "hashes": [ - "sha256:4af84facdd6f00a6b9b2927255f717bb23ae4b7a20ba1d9ef0a5a5a8dbe01ae2", - "sha256:60d61d93f7d67ba2b265c6d599d413ffec21202fec999a952f658ff3a73d252b" + "sha256:6a85456af7a98b0f554d140922b7b6a219757b039adb2b95e847cf115eaa20ae", + "sha256:767eb9ec3f9943bc84270198b5ff95d2d86d68d6b57792fafa4df4fc6b16cd7c" ], - "version": "==4.5.1" + "version": "==4.5.2" }, "requests": { "hashes": [ @@ -1215,6 +1301,14 @@ "index": "pypi", "version": "==3.13.2" }, + "tox-pipenv": { + "hashes": [ + "sha256:11342d2953d5be105b9530389191002fc7f9b5a78150d94b19acf87b3ad668dc", + "sha256:8c82aea4a64db248246d171bffc0e831773432e76e47c25c2fb9a37354e71501" + ], + "index": "pypi", + "version": "==1.9.0" + }, "traitlets": { "hashes": [ "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", @@ -1231,10 +1325,17 @@ }, "virtualenv": { "hashes": [ - "sha256:b7335cddd9260a3dd214b73a2521ffc09647bde3e9457fcca31dc3be3999d04a", - "sha256:d28ca64c0f3f125f59cabf13e0a150e1c68e5eea60983cc4395d88c584495783" + "sha256:6cb2e4c18d22dbbe283d0a0c31bb7d90771a606b2cb3415323eea008eaee6a9d", + "sha256:909fe0d3f7c9151b2df0a2cb53e55bdb7b0d61469353ff7a49fd47b0f0ab9285" ], - "version": "==16.6.1" + "version": "==16.7.2" + }, + "virtualenv-clone": { + "hashes": [ + "sha256:532f789a5c88adf339506e3ca03326f20ee82fd08ee5586b44dc859b5b4468c5", + "sha256:c88ae171a11b087ea2513f260cdac9232461d8e9369bcd1dc143fc399d220557" + ], + "version": "==0.5.3" }, "wcwidth": { "hashes": [ @@ -1259,17 +1360,17 @@ }, "widgetsnbextension": { "hashes": [ - "sha256:120f85acc3976450220b03b8933ce48678e518905cca69fc3c856ea5a0144196", - "sha256:8c9b4d73e388f2484296be18432d3cc0b8d59de243079a0db16a56c5571e1f86" + "sha256:079f87d87270bce047512400efd70238820751a11d2d8cb137a5a5bdbaf255c7", + "sha256:bd314f8ceb488571a5ffea6cc5b9fc6cba0adaf88a9d2386b93a489751938bcd" ], - "version": "==3.5.0" + "version": "==3.5.1" }, "zipp": { "hashes": [ - "sha256:8c1019c6aad13642199fbe458275ad6a84907634cc9f0989877ccc4a2840139d", - "sha256:ca943a7e809cc12257001ccfb99e3563da9af99d52f261725e96dfe0f9275bc3" + "sha256:4970c3758f4e89a7857a973b1e2a5d75bcdc47794442f2e2dd4fe8e0466e809a", + "sha256:8a5712cfd3bb4248015eb3b0b3c54a5f6ee3f2425963ef2a0125b8bc40aafaec" ], - "version": "==0.5.1" + "version": "==0.5.2" } } } diff --git a/README.md b/README.md index 6e9f4ba8..7de71e38 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,17 @@ This repository contains submodules. Clone with recursive strategy: git clone git@github.com:fetchai/agents-tac.git --recursive && cd agents-tac -## Quick Start +## Quick Start: + +- [x] You have followed the steps under 'Dependencies' and 'Preliminaries' below +- [x] You have entered the virtual environment and launched the script: + + pipenv shell + python scripts/launch.py + +The controller GUI at http://localhost:8097 provides real time insights. + +## Step by step: - [x] You have followed the steps under 'Dependencies' and 'Preliminaries' below - [x] In one terminal, you have built the sandbox and then launched it: @@ -19,7 +29,7 @@ This repository contains submodules. Clone with recursive strategy: - [x] In another terminal, you have entered the virtual environment and connected a template agent to the sandbox: pipenv shell - python3 templates/v1/basic.py --name my_agent --gui + python templates/v1/basic.py --name my_agent --gui The sandbox is starting up:

@@ -88,7 +98,7 @@ The [competition sandbox](../master/sandbox) provides the code to build the dock - Install the package: - python3 setup.py install + python setup.py install ## Contribute @@ -97,6 +107,10 @@ The following dependency is only relevant if you intend to contribute to the rep The following steps are only relevant if you intend to contribute to the repository. They are not required for agent development. +- Clear cache + + pipenv --clear + - Install development dependencies: pipenv install --dev @@ -119,7 +133,7 @@ The following steps are only relevant if you intend to contribute to the reposit - We recommend you use the latest OEF build: - python3 oef_search_pluto_scripts/launch.py -c ./oef_search_pluto_scripts/launch_config_latest.json + python oef_search_pluto_scripts/launch.py -c ./oef_search_pluto_scripts/launch_config_latest.json ## Resources diff --git a/docker-tac-develop/Dockerfile b/docker-tac-develop/Dockerfile index 0be5e5a3..e3e26a06 100644 --- a/docker-tac-develop/Dockerfile +++ b/docker-tac-develop/Dockerfile @@ -51,11 +51,6 @@ RUN sudo apt install python3.7-dev -y RUN sudo apt install python3-pip -y RUN sudo pip install pipenv -## Docs dependencies: -#RUN apt install pandoc -y -## python package to parse plantuml files. docs: https://pythonhosted.org/plantuml/ -#RUN apt install plantuml -y - RUN sudo mkdir /build WORKDIR /build COPY . /build @@ -63,7 +58,7 @@ COPY . /build RUN sudo make clean RUN pipenv --python python3.7 -RUN pipenv install --dev --skip-lock -RUN pipenv run pip3 install . --no-cache-dir --force +RUN pipenv install --dev +RUN pipenv run pip3 install . ENTRYPOINT [] diff --git a/docs/README.md b/docs/README.md index 24226f37..d19da8f2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,10 +1,15 @@ # TAC Documentation +## Dependency + +This requires `pandoc` to be installed locally. See [here](https://pandoc.org/installing.html) for instructions. + ## Build - Before getting started, check that: - [x] You have followed the steps under 'Dependencies' and 'Preliminaries' on root readme. + - [x] You have installed the development dependencies as described on root readme. - Activate the virtual environment: diff --git a/docs/conf.py b/docs/conf.py index de9b890b..a68180ec 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,14 +31,15 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.mathjax', - 'sphinx.ext.githubpages', - 'nbsphinx', - 'sphinxcontrib.mermaid', - 'sphinx.ext.todo', - 'sphinx.ext.autodoc', - 'sphinx.ext.autosectionlabel', - 'sphinx.ext.intersphinx' +extensions = [ + 'sphinx.ext.mathjax', + 'sphinx.ext.githubpages', + 'nbsphinx', + 'sphinxcontrib.mermaid', + 'sphinx.ext.todo', + 'sphinx.ext.autodoc', + 'sphinx.ext.autosectionlabel', + 'sphinx.ext.intersphinx', ] # autodoc conf @@ -67,14 +68,15 @@ copyright = '2019, Fetch.AI' author = 'Fetch.AI' +from tac import __version__ # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.1.2' +version = __version__ # The full version, including alpha/beta/rc tags. -release = '0.1.2' +release = __version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -112,7 +114,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +# html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. diff --git a/docs/diagrams/agent-state-management-guide/summary.mmd b/docs/diagrams/agent-state-management-guide/summary.mmd new file mode 100644 index 00000000..ed5de534 --- /dev/null +++ b/docs/diagrams/agent-state-management-guide/summary.mmd @@ -0,0 +1,45 @@ +sequenceDiagram + + participant Agent_1 + + participant Agent_2 + + participant Controller + + activate Controller + + Agent_1->>Agent_2: (1) send_cfp(1, 1, "agent_2_pbk", 0, query) + + Agent_2->>Agent_2: (2) get_proposal() + Agent_2->>Agent_2: (3) add_pending_proposal() + + Agent_2->>Agent_1: (4) send_propose(2, 1, "agent_1_pbk", 1, proposals) + + Agent_1->>Agent_1: (5) is_profitable_transaction() + Agent_1->>Agent_1: (6) add_locked_tx() + Agent_1->>Agent_1: (7) add_pending_initial_acceptance() + + Agent_1->>Agent_2: (8) send_accept(3, 1, "agent_2_pbk", 2) + + Agent_2->>Agent_2: (9) pop_pending_proposal() + Agent_2->>Agent_2: (10) is_profitable_transaction() + Agent_2->>Agent_2: (11) add_locked_tx() + + Agent_2->>Agent_1: (12) send_accept(4, 1, "agent_1_pbk", 3) + Agent_2->>Controller: (13) send_message(4, 1, "controller_pbk", transaction) + + Agent_1->>Agent_1: (14) pop_pending_initial_acceptance() + + Agent_1->>Controller: (15) send_message(5, 1, "controller_pbk", transaction) + + Controller->>Agent_1: (16) TransactionConfirmation(transaction) + + Agent_1->>Agent_1: (17) pop_locked_tx() + Agent_1->>Agent_1: (18) agent_state.update() + + Controller->>Agent_2: (19) TransactionConfirmation(transaction) + + Agent_2->>Agent_2: (20) pop_locked_tx() + Agent_2->>Agent_2: (21) agent_state.update() + + deactivate Controller diff --git a/docs/_static/diagrams/controller_setup.mmd b/docs/diagrams/controller_setup.mmd similarity index 100% rename from docs/_static/diagrams/controller_setup.mmd rename to docs/diagrams/controller_setup.mmd diff --git a/docs/_static/diagrams/controller_setup.uml b/docs/diagrams/controller_setup.uml similarity index 100% rename from docs/_static/diagrams/controller_setup.uml rename to docs/diagrams/controller_setup.uml diff --git a/docs/_static/diagrams/fipa_negotiation_1.mmd b/docs/diagrams/fipa_negotiation_1.mmd similarity index 100% rename from docs/_static/diagrams/fipa_negotiation_1.mmd rename to docs/diagrams/fipa_negotiation_1.mmd diff --git a/docs/_static/diagrams/fipa_negotiation_2.mmd b/docs/diagrams/fipa_negotiation_2.mmd similarity index 100% rename from docs/_static/diagrams/fipa_negotiation_2.mmd rename to docs/diagrams/fipa_negotiation_2.mmd diff --git a/docs/_static/diagrams/fipa_negotiation_3.mmd b/docs/diagrams/fipa_negotiation_3.mmd similarity index 100% rename from docs/_static/diagrams/fipa_negotiation_3.mmd rename to docs/diagrams/fipa_negotiation_3.mmd diff --git a/docs/_static/diagrams/fipa_negotiation_4.mmd b/docs/diagrams/fipa_negotiation_4.mmd similarity index 100% rename from docs/_static/diagrams/fipa_negotiation_4.mmd rename to docs/diagrams/fipa_negotiation_4.mmd diff --git a/docs/diagrams/negotiation-guide/summary.mmd b/docs/diagrams/negotiation-guide/summary.mmd new file mode 100644 index 00000000..398d340f --- /dev/null +++ b/docs/diagrams/negotiation-guide/summary.mmd @@ -0,0 +1,41 @@ +sequenceDiagram + + participant Agent_1 + + participant Agent_2 + + participant Controller + + participant OEF + + + + activate Controller + + Agent_1->>Agent_1: (1) get_service_description() + Agent_1->>OEF: (2) register_service(description) + + Agent_2->>Agent_2: (3) get_service_description() + Agent_2->>OEF: (4) register_service(description) + + Agent_1->>Agent_1: (5) build_services_query() + Agent_1->>OEF: (6) search_services(query) + + OEF->>Agent_1: (7) search_results(agents) + + Agent_1->>Agent_2: (8) send_cfp(1, 1, "agent_2_pbk", 0, query) + Agent_2->>Agent_2: (9) get_proposal() + + Agent_2->>Agent_1: (10) send_propose(2, 1, "agent_1_pbk", 1, proposals) + + Agent_1->>Agent_2: (11) send_accept(3, 1, "agent_2_pbk", 2) + + Agent_2->>Agent_1: (13) send_accept(4, 1, "agent_1_pbk", 3) + Agent_2->>Controller: (14) send_message(4, 1, "controller_pbk", transaction) + + Agent_1->>Controller: (12) send_message(5, 1, "controller_pbk", transaction) + + Controller->>Agent_1: (15) TransactionConfirmation(transaction) + Controller->>Agent_2: (16) TransactionConfirmation(transaction) + + deactivate Controller diff --git a/docs/_static/diagrams/register_to_tac.mmd b/docs/diagrams/register_to_tac.mmd similarity index 100% rename from docs/_static/diagrams/register_to_tac.mmd rename to docs/diagrams/register_to_tac.mmd diff --git a/docs/_static/diagrams/register_to_tac.uml b/docs/diagrams/register_to_tac.uml similarity index 100% rename from docs/_static/diagrams/register_to_tac.uml rename to docs/diagrams/register_to_tac.uml diff --git a/docs/_static/diagrams/registration.mmd b/docs/diagrams/registration.mmd similarity index 100% rename from docs/_static/diagrams/registration.mmd rename to docs/diagrams/registration.mmd diff --git a/docs/_static/diagrams/registration.uml b/docs/diagrams/registration.uml similarity index 100% rename from docs/_static/diagrams/registration.uml rename to docs/diagrams/registration.uml diff --git a/docs/_static/diagrams/search_controller.mmd b/docs/diagrams/search_controller.mmd similarity index 100% rename from docs/_static/diagrams/search_controller.mmd rename to docs/diagrams/search_controller.mmd diff --git a/docs/_static/diagrams/search_controller.uml b/docs/diagrams/search_controller.uml similarity index 100% rename from docs/_static/diagrams/search_controller.uml rename to docs/diagrams/search_controller.uml diff --git a/docs/index.rst b/docs/index.rst index 6b309adc..c6a47ae1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,6 +16,8 @@ Welcome to TAC's documentation! sections/controller_protocol sections/baseline_agent sections/develop_agent + sections/negotiation_guide + sections/agent_state_management_guide sections/glossary reference/api/tac diff --git a/docs/publish_to_ghpages.sh b/docs/publish_to_ghpages.sh new file mode 100755 index 00000000..429ca4c7 --- /dev/null +++ b/docs/publish_to_ghpages.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +if [ -z $1 ] +then + echo "Please provide a tag argument." + exit 1; +fi + +STATUS="$(git status)" + +if ! [[ $STATUS == *"nothing to commit, working tree clean"* ]] +then + echo "The working directory is dirty. Please commit any pending changes." + exit 1; +fi + +set -e + +echo "Deleting old publication" +rm -rf _build + +echo "Cloning gh-pages branch" +mkdir _build -p +cd _build +git clone --single-branch --branch gh-pages git@github.com:fetchai/agents-tac.git html +cd .. + +echo "Building docs" +sphinx-apidoc -o reference/api/ ../tac/ +make html +cd _build/html + +echo "Pushing to gh-pages branch" +git add --all && git commit -m "Publish docs ($1)" +git tag $1 +git push origin gh-pages + +echo "Delete local repo" +cd ../../ +make clean diff --git a/docs/reference/api/tac.agents.v1.base.rst b/docs/reference/api/tac.agents.v1.base.rst index ee1a44a5..6e7f277d 100644 --- a/docs/reference/api/tac.agents.v1.base.rst +++ b/docs/reference/api/tac.agents.v1.base.rst @@ -52,14 +52,6 @@ tac.agents.v1.base.interfaces module :undoc-members: :show-inheritance: -tac.agents.v1.base.lock\_manager module ---------------------------------------- - -.. automodule:: tac.agents.v1.base.lock_manager - :members: - :undoc-members: - :show-inheritance: - tac.agents.v1.base.negotiation\_behaviours module ------------------------------------------------- @@ -100,6 +92,14 @@ tac.agents.v1.base.strategy module :undoc-members: :show-inheritance: +tac.agents.v1.base.transaction\_manager module +---------------------------------------------- + +.. automodule:: tac.agents.v1.base.transaction_manager + :members: + :undoc-members: + :show-inheritance: + Module contents --------------- diff --git a/docs/reference/api/tac.gui.dashboards.rst b/docs/reference/api/tac.gui.dashboards.rst index 8ff4b40a..fe6c0387 100644 --- a/docs/reference/api/tac.gui.dashboards.rst +++ b/docs/reference/api/tac.gui.dashboards.rst @@ -28,6 +28,14 @@ tac.gui.dashboards.controller module :undoc-members: :show-inheritance: +tac.gui.dashboards.leaderboard module +------------------------------------- + +.. automodule:: tac.gui.dashboards.leaderboard + :members: + :undoc-members: + :show-inheritance: + Module contents --------------- diff --git a/docs/reference/api/tac.gui.launcher.api.resources.rst b/docs/reference/api/tac.gui.launcher.api.resources.rst new file mode 100644 index 00000000..2e6c3bfa --- /dev/null +++ b/docs/reference/api/tac.gui.launcher.api.resources.rst @@ -0,0 +1,30 @@ +tac.gui.launcher.api.resources package +====================================== + +Submodules +---------- + +tac.gui.launcher.api.resources.agents module +-------------------------------------------- + +.. automodule:: tac.gui.launcher.api.resources.agents + :members: + :undoc-members: + :show-inheritance: + +tac.gui.launcher.api.resources.sandboxes module +----------------------------------------------- + +.. automodule:: tac.gui.launcher.api.resources.sandboxes + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: tac.gui.launcher.api.resources + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/reference/api/tac.gui.launcher.api.rst b/docs/reference/api/tac.gui.launcher.api.rst new file mode 100644 index 00000000..a9d5df5d --- /dev/null +++ b/docs/reference/api/tac.gui.launcher.api.rst @@ -0,0 +1,17 @@ +tac.gui.launcher.api package +============================ + +Subpackages +----------- + +.. toctree:: + + tac.gui.launcher.api.resources + +Module contents +--------------- + +.. automodule:: tac.gui.launcher.api + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/reference/api/tac.gui.launcher.forms.rst b/docs/reference/api/tac.gui.launcher.forms.rst new file mode 100644 index 00000000..8ff3207e --- /dev/null +++ b/docs/reference/api/tac.gui.launcher.forms.rst @@ -0,0 +1,30 @@ +tac.gui.launcher.forms package +============================== + +Submodules +---------- + +tac.gui.launcher.forms.agent module +----------------------------------- + +.. automodule:: tac.gui.launcher.forms.agent + :members: + :undoc-members: + :show-inheritance: + +tac.gui.launcher.forms.sandbox module +------------------------------------- + +.. automodule:: tac.gui.launcher.forms.sandbox + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: tac.gui.launcher.forms + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/reference/api/tac.gui.launcher.rst b/docs/reference/api/tac.gui.launcher.rst new file mode 100644 index 00000000..5bcb891e --- /dev/null +++ b/docs/reference/api/tac.gui.launcher.rst @@ -0,0 +1,46 @@ +tac.gui.launcher package +======================== + +Subpackages +----------- + +.. toctree:: + + tac.gui.launcher.api + tac.gui.launcher.forms + +Submodules +---------- + +tac.gui.launcher.app module +--------------------------- + +.. automodule:: tac.gui.launcher.app + :members: + :undoc-members: + :show-inheritance: + +tac.gui.launcher.config module +------------------------------ + +.. automodule:: tac.gui.launcher.config + :members: + :undoc-members: + :show-inheritance: + +tac.gui.launcher.home module +---------------------------- + +.. automodule:: tac.gui.launcher.home + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: tac.gui.launcher + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/reference/api/tac.gui.rst b/docs/reference/api/tac.gui.rst index 2f75db79..e9c31ed9 100644 --- a/docs/reference/api/tac.gui.rst +++ b/docs/reference/api/tac.gui.rst @@ -7,6 +7,7 @@ Subpackages .. toctree:: tac.gui.dashboards + tac.gui.launcher Submodules ---------- diff --git a/docs/reference/api/tac.platform.rst b/docs/reference/api/tac.platform.rst index 8c7077f6..21a6387a 100644 --- a/docs/reference/api/tac.platform.rst +++ b/docs/reference/api/tac.platform.rst @@ -28,6 +28,14 @@ tac.platform.protocol module :undoc-members: :show-inheritance: +tac.platform.simulation module +------------------------------ + +.. automodule:: tac.platform.simulation + :members: + :undoc-members: + :show-inheritance: + tac.platform.stats module ------------------------- diff --git a/docs/sections/agent_state_management_guide.ipynb b/docs/sections/agent_state_management_guide.ipynb new file mode 100644 index 00000000..03b0e63c --- /dev/null +++ b/docs/sections/agent_state_management_guide.ipynb @@ -0,0 +1,87 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "collapsed": true, + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "\n", + "# Step-by-step agent state management\n", + "\n", + "\n", + "In this section, you will see how the agent state is managed in our framework.\n", + "\n", + "## The big picture\n", + "\n", + "\n", + "The following diagram gives you a high-level understanding of how the agent state management works for a generic negotiation.\n", + "\n", + ".. mermaid:: ../diagrams/agent-state-management-guide/summary.mmd\n", + "\n", + "Let's see step by step what happens:\n", + "\n", + "1. `Agent_1` sends a `CFP` to `Agent_2`, meaning that she wants to start a negotiation. The CFP contains a reference to the goods which `Agent_1` is interested in and whether `Agent_1` is a buyer or seller of these goods.\n", + "\n", + "2. Assuming `Agent_2` does not decline the CFP, `Agent_2` calls `get_proposals()` to generate a list of proposal for answering to `Agent_1`. When generating the proposals `Agent_2` applies all locked transactions - we discuss below when a transaction becomes locked - to the current agent state to generate a \"forward looking agent state\" (i.e. the state the agent will be in when all locked transactions have been settled by the `Controller`). This ensures `Agent_2` takes into account all the transactions she has committed to.\n", + "3. `Agent_2` calls `store_proposals()` to store the proposals as `pending_proposals`.\n", + "4. `Agent_2` replies with a `Propose` message which includes the proposals as an answer to the `CFP` (note, currently the list of proposals includes exactly one proposal).\n", + "\n", + "5. `Agent_1` translates the proposal into a transaction and calls `is_profitable_transaction()` to identify whether the transaction is profitable.\n", + "6. Assuming the proposal is profitable, `Agent_1` calls `add_locked_tx()` to add the transaction to the list of locked transactions.\n", + "7. `Agent_1` calls `add_pending_initial_acceptance()` to add the transaction to the list of pending initial acceptances. This helps the agent identify whether an incoming `Accept` from `Agent_2` is a matching accept or an initial accept.\n", + "8. `Agent_1` sends an `Accept` message to `Agent_2`, meaning that she accepts the proposal.\n", + "\n", + "9. `Agent_2` calls `pop_pending_proposal()` to recover the proposal - in the form of a transaction - made to `Agent_1` and referenced in the acceptance message.\n", + "10. `Agent_2` calls `is_profitable_transaction()` to identify whether the transaction is profitable. This proposal was created by `Agent_2`, however in the meanwhile `Agent_2` might have a different agent state which could render this proposal no longer profitable.\n", + "11. Assuming the proposal is still profitable, `Agent_2` calls `add_locked_tx()` to add the transaction to the list of locked transactions.\n", + "12. `Agent_2` sends an `Accept` message to `Agent_1`, meaning that she \"match-accepts\" the proposal.\n", + "13. `Agent_2` sends a Transaction request to the `Controller`.\n", + "\n", + "14. `Agent_1` calls `pop_pending_initial_acceptance()` to \n", + "15. `Agent_1` sends a Transaction request to the `Controller` (analogous to step 13).\n", + "\n", + "16. The `Controller` notifies `Agent_1` that the transaction has been confirmed.\n", + "17. `Agent_1` calls `pop_locked_tx()` to remove the transaction from the locked transaction list.\n", + "18. `Agent_1` calls `agent_state.update()` to update its state.\n", + "\n", + "19. The `Controller` notifies `Agent_2` that the transaction has been confirmed.\n", + "20. `Agent_2` calls `pop_locked_tx()` to remove the transaction from the locked transaction list (analogous to step 17).\n", + "21. `Agent_2` calls `agent_state.update()` to update its state (analogous to step 18)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + }, + "pycharm": { + "stem_cell": { + "cell_type": "raw", + "metadata": { + "collapsed": false + }, + "source": [] + } + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/docs/sections/baseline_agent.rst b/docs/sections/baseline_agent.rst index 86c8d5f3..672597d6 100644 --- a/docs/sections/baseline_agent.rst +++ b/docs/sections/baseline_agent.rst @@ -112,21 +112,21 @@ Negotiation The :class:`~tac.agents.v1.base.participant_agent.ParticipantAgent` implements the FIPA negotiation protocol in :class:`~tac.agents.v1.base.negotiation_behaviours.FIPABehaviour`. A FIPA negotiation starts with a call for proposal (:class:`~oef.messages.CFP`) which contains a :class:`~oef.query.Query` referencing the services which are demanded or supplied by the sending agent. The receiving agent then responds, if it implements the FIPA negotiation protocol, with a suitable proposal (:class:`~oef.messages.Propose`) which contains a list of :class:`~oef.schema.Description` objects (think individual proposals). The first agent responds to the proposal with either a :class:`~oef.messages.Decline` or an :class:`~oef.messages.Accept`. Assuming the agent accepts, it will also send the :class:`~tac.platform.protocol.Transaction` to the :class:`~tac.platform.controller.ControllerAgent`. Finally, the second agent can close the negotiation by responding with a matching :class:`~oef.messages.Accept` and a submission of the :class:`~tac.platform.protocol.Transaction` to the :class:`~tac.platform.controller.ControllerAgent`. The controller only settles a transaction if it receives matching transactions from each one of the two trading parties referenced in the transaction. -.. mermaid:: ../_static/diagrams/fipa_negotiation_1.mmd +.. mermaid:: ../diagrams/fipa_negotiation_1.mmd :align: center :caption: A successful FIPA negotiation between two agents. Trade can break down at various stages in the negotiation due to the :class:`~tac.agents.v1.base.strategy.Strategy` employed by the agents: -.. mermaid:: ../_static/diagrams/fipa_negotiation_2.mmd +.. mermaid:: ../diagrams/fipa_negotiation_2.mmd :align: center :caption: An unsuccessful FIPA negotiation between two agents breaking down after initial accept. -.. mermaid:: ../_static/diagrams/fipa_negotiation_3.mmd +.. mermaid:: ../diagrams/fipa_negotiation_3.mmd :align: center :caption: An unsuccessful FIPA negotiation between two agents breaking down after proposal. -.. mermaid:: ../_static/diagrams/fipa_negotiation_4.mmd +.. mermaid:: ../diagrams/fipa_negotiation_4.mmd :align: center :caption: An unsuccessful FIPA negotiation between two agents breaking down after cfp. diff --git a/docs/sections/develop_agent.rst b/docs/sections/develop_agent.rst index 84e457c9..763c5aae 100644 --- a/docs/sections/develop_agent.rst +++ b/docs/sections/develop_agent.rst @@ -6,6 +6,22 @@ Developing Your Own Agent In this section we describe a number of approaches you could take to develop your own agent. +Familiarize yourself with Sandbox and Playground +------------------------------------------------ + +To launch the sandbox from root directory run the launchscript: + +`python scripts/launch.py` + +This lets you explore the competition setup and how the agents trade. + +To launch the playground from root directory run: + +`python sandbox/playground.py` + +This lets you explore the agent and mailbox interface. + + Basic: Tuning the Agent's Parameters ------------------------------------ @@ -30,6 +46,10 @@ To evaluate changes in parameters on agent performance you can run your agent ag .. _sandbox readme: https://github.com/fetchai/agents-tac/blob/master/sandbox/README.md +Alternatively, you can use our Sandbox Launch App to do a grid parameter search for a population of agents. The Sandbox Launch App can be launched via executing the following command from root directory: + +`python tac/gui/panel/app.py` + Advanced: Changing the Agent's Strategy --------------------------------------- diff --git a/docs/sections/negotiation_guide.ipynb b/docs/sections/negotiation_guide.ipynb new file mode 100644 index 00000000..af79234c --- /dev/null +++ b/docs/sections/negotiation_guide.ipynb @@ -0,0 +1,443 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "collapsed": true, + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "\n", + "# Step-by-step agent negotiation\n", + "\n", + "\n", + "In this section, you will see how the negotiation works in our framework.\n", + "\n", + "## The big picture\n", + "\n", + "\n", + "The following diagram gives you a high-level understanding on how a generic negotiation works.\n", + "\n", + ".. mermaid:: ../diagrams/negotiation-guide/summary.mmd\n", + "\n", + "Let's see step by step what happens:\n", + "\n", + "1. `Agent_1` calls `get_service_description(is_supply)` to generate the service description. `is_supply` is a flag to \n", + "switch between registering goods which the agent supplies (`is_supply` is `True`, the agent is in a seller role for these goods) \n", + "and registering goods which the agent demands (`is_supply` is `False`, the agent is in a buyer role for these goods).\n", + "2. `Agent_1` sends a `register_service` request to the OEF node, to register her services (the goods she supplies/demands) on the OEF.\n", + "3. Analogous to (1), but for `Agent_2`\n", + "4. Analogous to (2), but for `Agent_2`\n", + "5. `Agent_1` calls `build_services_query(is_searching_for_sellers)` to generate a `query` for the OEF.\n", + "`is_searching_for_sellers` is a flag to switch between searching for sellers and searching for buyers of the goods referenced in the query. \n", + "If the agent is searching for sellers than the agent is in the buyer role, similarly when searching for buyers the agent is in a seller role.\n", + "6. `Agent_1` send a `search_service` request with the `query` previously generated.\n", + "7. The OEF node returns a search result with the list of agent ids matching the `query`\n", + "8. `Agent_1` finds `Agent_2`, so `Agent_1` sends a `CFP` to `Agent_2`, meaning that she wants to start a negotiation.\n", + "The CFP contains a reference to the goods which Agent_1 is interested in and whether Agent_1 is a buyer or seller \n", + "of these goods, both in the form of the query.\n", + "9. `Agent_2` calls `get_proposal()` to generate a proposal for answering `Agent_1`\n", + "10. `Agent_2` replies with a `Propose` message as an answer for the `CFP`.\n", + "11. `Agent_1` sends an `Accept` message to `Agent_2`, meaning that she accepts the proposal.\n", + "12. `Agent_2` replies with a _matched accept_ to `Agent_1`, meaning that she confirms definitively the transaction. \n", + "13. `Agent_2` sends a Transaction request to the `Controller` (analogous to step 12).\n", + "14. `Agent_1` sends a Transaction request to the `Controller`.\n", + "15. The `Controller` notifies `Agent_1` that the transaction has been confirmed.\n", + "15. The `Controller` notifies `Agent_2` that the transaction has been confirmed.\n", + "\n", + "Notice that this is the behaviour of the `BaselineAgent`. By modifying the default strategy, you can change\n", + "the behaviour in steps 1 (or 3), 5 and 9. The other methods are handled by our implementation of the FIPA negotiation protocol.\n", + "\n", + "## Analyzing the APIs\n", + "\n", + "In the following, we're going to describe the steps listed before, but more in detail, using code examples from the framework.\n", + "\n", + "### Instantiate an agent\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "pycharm": { + "is_executing": false, + "name": "#%% \n" + } + }, + "outputs": [], + "source": [ + "from tac.agents.v1.examples.baseline import BaselineAgent\n", + "from tac.agents.v1.examples.strategy import BaselineStrategy\n", + "\n", + "strategy = BaselineStrategy()\n", + "agent = BaselineAgent(name=\"tac_agent\", oef_addr=\"127.0.0.1\", oef_port=10000, strategy=strategy)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "\n", + "\n", + "### Registration\n", + "\n", + "This part covers the steps 1-4. That is, when the agents build their own description and register their service to the OEF.\n", + "This step allows the agents to be found via search queries, and hence increasing the probability to be found by other agents. \n", + "\n", + "#### The `get_service_description(is_supply)` method\n", + "\n", + "This method generates a `Description` object of the [Python OEF SDK](https://github.com/fetchai/oef-sdk-python.git) \n", + "(check the documentation [here](http://oef-sdk-docs.fetch.ai/oef.html#oef.schema.Description)).\n", + "It is basically a data structure that holds a dictionary objects, mapping from attribute names (strings) to some values.\n", + "Moreover, it might refer to a [DataModel](http://oef-sdk-docs.fetch.ai/oef.html#oef.schema.DataModel) object, which defines\n", + "the abstract structure that a `Description` object should have. You can think of them in terms of the relational database\n", + "domain: a `DataModel` object corresponds to an SQL Table, whereas a `Description` object correspond to a row of that table. \n", + "\n", + "The method is used in steps 1 and 3 by `Agent_1` and `Agent_2`, respectively.\n", + "\n", + "In the context of TAC, the `Description` for service registration looks like the following:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "is_executing": false, + "name": "#%% \n" + } + }, + "outputs": [], + "source": [ + "from oef.schema import Description\n", + "\n", + "\n", + "description = Description({\n", + " 'tac_good_0_pbk': 1, \n", + " 'tac_good_1_pbk': 1, \n", + " 'tac_good_2_pbk': 1, \n", + " 'tac_good_3_pbk': 1, \n", + " 'tac_good_4_pbk': 1, \n", + " 'tac_good_5_pbk': 1, \n", + " 'tac_good_6_pbk': 1, \n", + " 'tac_good_7_pbk': 1, \n", + " 'tac_good_8_pbk': 1, \n", + " 'tac_good_9_pbk': 1\n", + "}, data_model=None)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "\n", + "The argument `data_model` is set to `None`, but in the framework it is properly set depending on the context \n", + "That is, when we refer to a description of an agent in the seller role, we use the `\"tac_supply\"` data model \n", + "(the agent supplies goods), whereas in the case of a description of an agent in the buyer role, we use the \n", + "`\"tac_demand\"` data model (the agent demands goods).\n", + " \n", + "\n", + "The attribute names `tac_good_X_pbk` is the name given to each tradable good. \n", + "Notice that the keys are automatically generated, depending on the number of goods in the game. \n", + "\n", + "Depending on the value of the flag `is_supply`, the generated description contains different quantities for each good:\n", + "\n", + "- If `is_supply` is `True`, then the quantities good are generated by the method `Strategy.supplied_good_quantities(current_holdings)`\n", + " and have to be interpreted as the amount of each good the agent is willing to sell;\n", + "- If `is_supply` is `False`, then the quantities good are generated by the method `Strategy.demanded_good_quantities(current_holdings)` \n", + "and have to be interpreted as the amount of each good the agent is willing to buy; \n", + "\n", + "Notice that `supplied_good_quantities` and `demanded_good_quantities` are user-defined method to be implemented \n", + "in the `Strategy` object, which defines the agent's behaviour.\n", + "\n", + "Here you can see the output of `BaselineStrategy.supplied_good_quantities` \n", + "and `BaselineStrategy.demanded_good_quantities`\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "is_executing": false, + "name": "#%% \n" + } + }, + "outputs": [], + "source": [ + "from tac.agents.v1.examples.strategy import BaselineStrategy\n", + "baseline_strategy = BaselineStrategy()\n", + "\n", + "current_holdings = [2, 3, 4, 1]\n", + "\n", + "supplied_good_quantities = baseline_strategy.supplied_good_quantities(current_holdings)\n", + "demanded_good_quantities = baseline_strategy.demanded_good_quantities(current_holdings)\n", + "\n", + "print(\"Supplied quantities: \", supplied_good_quantities)\n", + "print(\"Demanded quantities: \", demanded_good_quantities)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "\n", + "The baseline supplied quantities are the current holdings minus `1`. This is because\n", + "the first quantity is the most valuable one in terms of utility, \n", + "due to the logarithmic shape of the [Cobb-Douglas utility function](https://en.wikipedia.org/wiki/Cobb%E2%80%93Douglas_production_function#Cobb%E2%80%93Douglas_utilities)\n", + "\n", + "The baseline demanded quantities are just `1` for every good. this is because every good instance is going to be providing additional utility to the agent,\n", + " due to the ever-increasing utility function.\n", + "\n", + "However, the baseline strategy is relatively simple and naive, so you might think to more complex and/or dynamic computation\n", + "of supplied/demanded quantities, which affects the your agent's behaviour during the whole competition. \n", + "\n", + "#### The `register_service(description)` method\n", + "\n", + "The `register_service(description)` method is implemented the OEF Python SDK.\n", + "You can find the informal introduction to the [registering](https://docs.fetch.ai/oef/registering/) and \n", + "[advertising](https://docs.fetch.ai/oef/registering/) processes, and the reference documentation \n", + "of the API [here](http://oef-sdk-docs.fetch.ai/oef.html#oef.agents.Agent.register_service).\n", + "\n", + "The method is used in steps 2 and 4 by `Agent_1` and `Agent_2` respectively.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "### Searching\n", + "\n", + "This part covers the steps 5-7 of the diagram.\n", + "\n", + "The searching/advertising features of the OEF platform are crucial in the TAC, since they allow the discovery \n", + "of potential sellers or buyers. \n", + "\n", + "#### The `build_services_query(is_searching_for_sellers)` method\n", + "\n", + "The `build_services_query(is_searching_for_sellers)` method returns a \n", + "[Query](http://oef-sdk-docs.fetch.ai/oef.html#oef.schema.Query) object that is used for searching, on the OEF platform, \n", + "potential agents to negotiate with.\n", + "The method takes in input the flag `is_searching_for_sellers` that determines whether the generated query should\n", + "search for buyer or sellers.\n", + "\n", + "More detail and code examples about how to build a query in the OEF Python SDK \n", + "can be found [here](https://docs.fetch.ai/oef/searching/) \n", + "\n", + "\n", + "Depending on the value of the flag `is_searching_for_sellers`, the generated description contains different quantities for each good:\n", + "\n", + "- If `is_searching_for_sellers` is `True`, then the good public keys are generated by the method `Strategy.demanded_good_pbks(good_pbks, current_holdings)`\n", + " and have to be interpreted as the goods the agent is willing to buy;\n", + "- If `is_searching_for_sellers` is `False`, then the good public keys are generated by the method `Strategy.supplied_good_pbks(good_pbks, current_holdings)` \n", + "and have to be interpreted as the goods the agent is willing to sell; \n", + "\n", + "Notice that `demanded_good_pbks` and `supplied_good_pbks` are user-defined method to be implemented \n", + "in the `Strategy` object, which defines the agent's behaviour.\n", + "\n", + "Here you can see the output of `BaselineStrategy.supplied_good_pbks` \n", + "and `BaselineStrategy.demanded_good_pbks`\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "is_executing": false, + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "\n", + "from tac.agents.v1.examples.strategy import BaselineStrategy\n", + "baseline_strategy = BaselineStrategy()\n", + "\n", + "good_pbks = [\"tac_good_0_pbk\", \"tac_good_1_pbk\", \"tac_good_2_pbk\", \"tac_good_3_pbk\"]\n", + "current_holdings = [2, 3, 4, 1]\n", + "\n", + "supplied_good_pbks = baseline_strategy.supplied_good_pbks(good_pbks, current_holdings)\n", + "demanded_good_pbks = baseline_strategy.demanded_good_pbks(good_pbks, current_holdings)\n", + "\n", + "print(\"Supplied good public keys: \", supplied_good_pbks)\n", + "print(\"Demanded good public keys: \", demanded_good_pbks)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "\n", + "As you can notice, the baseline supplied goods are the ones for which the holdings are strictly greater than `1`,\n", + "whereas the baseline demanded goods are all the goods. \n", + "\n", + "You can control what goods your agent is looking for during the competition by modifying those methods. \n", + "\n", + "#### The `search_services(search_id, query)` method\n", + "\n", + "The [`search_services(search_id, query)`](http://oef-sdk-docs.fetch.ai/oef.html#oef.agents.Agent.search_services) method is used send a search request to the OEF node.\n", + "The OEF node will search for registered agents in the service directory, and the ones whose description matches the `query`\n", + "will be included in the search result (see below).\n", + "\n", + "For further details, look [here](https://docs.fetch.ai/oef/searching/).\n", + "\n", + "#### The `on_search_result(agents)` method\n", + "\n", + "The [`on_search_result(agents)`](http://oef-sdk-docs.fetch.ai/oef.html?#oef.agents.Agent.on_search_result) \n", + "method is a callback that it is called when the agent receives a search result from the\n", + "OEF node.\n", + "\n", + "It contains a list of agent identifiers that satisfy the search criteria of the corresponding search request.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "\n", + "### Negotiation\n", + "\n", + "This part covers the steps 8-14 of the diagram.\n", + "\n", + "Further details of a generic negotiation in the OEF platform can be found [here](https://docs.fetch.ai/oef/negotiating/)\n", + "and [here](https://fetchai.github.io/oef-sdk-python/user/communication-protocols.html#using-fipa-for-negotiation)\n", + "\n", + "#### Call for proposals\n", + "\n", + "The message that initiates a negotiation is called \"Call for proposals\", or `CFP`. A `CFP` message contains a query\n", + "object which defines what the agent is looking for.\n", + "\n", + "#### The `get_proposals()` method and Propose message\n", + "\n", + "The `Strategy.get_proposals()` method defines how an agent replies to the incoming CFPs. The output of this method\n", + "is a list of `Description` objects.\n", + "\n", + "Here's an example of output:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "is_executing": false, + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "from tac.agents.v1.examples.strategy import BaselineStrategy\n", + "baseline_strategy = BaselineStrategy()\n", + "proposals = baseline_strategy.get_proposals(\n", + " good_pbks=[\"tac_good_0_pbk\", \"tac_good_1_pbk\"],\n", + " current_holdings=[2, 2],\n", + " utility_params=[0.4, 0.6],\n", + " tx_fee=0.1,\n", + " is_seller=True,\n", + " world_state=False\n", + ")\n", + "\n", + "print(proposals[0].values)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "\n", + "The values of the `Description` dictionary are the good quantities plus a field `\"price\"` that \n", + "specifies the price of the set of goods proposed. \n", + "\n", + "The generated proposals in step 9 are then sent in a `Propose` message to the agent that initiated the negotiation (step 10).\n", + "\n", + "Notice that `get_proposals()` is an abstract method of the `Strategy` object, which hence it's another way to \n", + "modify the behaviour of the agent.\n", + "\n", + "\n", + "#### The Accept message\n", + "\n", + "If the proposal is profitable, the agent that receives a `Propose` (in the example `Agent_1`) \n", + "can reply with an `Accept` message, which means that she accepts the offer (step 11)\n", + "\n", + "\n", + "#### Transaction request\n", + "\n", + "Alongside the `Accept` message, the agent also sends a transaction request to the `Controller` agent (step 12).\n", + "The controller then waits until also the counterparty sends the request for the same transaction.\n", + "\n", + "#### The Matched Accept\n", + "\n", + "when the other agent (in the example, `Agent_2`) receives an `Accept`, she replies with another accept, that we call\n", + "\"matched accept\" (step 13). That is, a notification for `Agent_1` that she acknowledged the `Agent_1`'s acceptance.\n", + "\n", + "At the same time, `Agent_2` also sends a transaction request in step 14 (analogous to step 12).\n", + "\n", + "\n", + "#### Transaction confirmations\n", + "\n", + "Once the `Controller` received the transaction requests from both the involved parties, he stores the transaction\n", + "in the ledger and sends back a `TransactionConfirmation` message to both the agents to let them update their internal\n", + "state. \n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + }, + "pycharm": { + "stem_cell": { + "cell_type": "raw", + "metadata": { + "collapsed": false + }, + "source": [] + } + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/docs/sections/registration_phase.rst b/docs/sections/registration_phase.rst index 8475f03c..9b0a2132 100644 --- a/docs/sections/registration_phase.rst +++ b/docs/sections/registration_phase.rst @@ -50,7 +50,7 @@ The controller agent will wait for *registration_timeout*. At the end, if there participant, it will start the competition. Otherwise, it will send back a "Cancelled" message to every registered participant -.. mermaid:: ../_static/diagrams/controller_setup.mmd +.. mermaid:: ../diagrams/controller_setup.mmd :align: center :caption: The setup of the controller agent. @@ -78,7 +78,7 @@ In order to complete a registration, a trading agent should do the following ste Search for controller agent ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. mermaid:: ../_static/diagrams/search_controller.mmd +.. mermaid:: ../diagrams/search_controller.mmd :align: center :caption: TAC Agent search for controller agents. @@ -92,7 +92,7 @@ The message :class:`~tac.platform.protocol.Register` is an empty message. In ord the registration, the agent can unregister from the competition by sending the :class:`~tac.platform.protocol.Unregister` message. -.. mermaid:: ../_static/diagrams/register_to_tac.mmd +.. mermaid:: ../diagrams/register_to_tac.mmd :align: center :caption: an agent registers to TAC. @@ -120,6 +120,6 @@ Summary In the following, a transition diagram that sumarize the *registration phase*: -.. mermaid:: ../_static/diagrams/registration.mmd +.. mermaid:: ../diagrams/registration.mmd :align: center :caption: The transition diagram for the registration phase. diff --git a/notebooks/CompetitiveEquilibriumGenerator.ipynb b/notebooks/CompetitiveEquilibriumGenerator.ipynb index 68ac86a5..2bb96e35 100644 --- a/notebooks/CompetitiveEquilibriumGenerator.ipynb +++ b/notebooks/CompetitiveEquilibriumGenerator.ipynb @@ -181,7 +181,7 @@ "source": [ "There exists a simple algorithm to find the competitive equilibrium:\n", "\n", - "- 1. solve the optimization problem for each consumer to get their excess demand as a function of prices and endowments: $z_j^{i,\\ast}\\left(\\mathbf{p},\\mathbf{e}^i\\right) = x_j^{i,\\ast}\\left(\\mathbf{p},\\mathbf{e}^i, c^i\\right) - e_j^i$ for each $j \\in G$ and $z_f^{i,\\ast}\\left(\\mathbf{p},\\mathbf{e}^i, c^i\\right) = f^{i,\\ast}\\left(\\mathbf{p},\\mathbf{e}^i, c^i\\right) - m^i$\n", + "- 1. solve the optimization problem for each consumer to get their excess demand as a function of prices and endowments: $z_j^{i,\\ast}\\left(\\mathbf{p},\\mathbf{e}^i\\right) = x_j^{i,\\ast}\\left(\\mathbf{p},\\mathbf{e}^i, c^i\\right) - e_j^i$ for each $j \\in G$ and $z_f^{i,\\ast}\\left(\\mathbf{p},\\mathbf{e}^i, c^i\\right) = f^{i,\\ast}\\left(\\mathbf{p},\\mathbf{e}^i, c^i\\right) - c^i$\n", "- 2. find a price s.t. net demand for each good in economy is zero: $\\sum_{i \\in I} z_j^{i,\\ast}\\left(\\mathbf{p},\\mathbf{e}^i, c^i\\right) = 0$ for each $j \\in G$ and $\\sum_{i \\in I} z_f^{i,\\ast}\\left(\\mathbf{p},\\mathbf{e}^i, c^i\\right) = 0$" ] }, @@ -261,7 +261,7 @@ "for $j \\in G$ and \n", "$\n", "\\begin{equation}\n", - "f^{i,\\ast}(\\mathbf{p}, \\mathbf{e}^i) = \\sum_{k \\in G} p_j e^i_j - t\n", + "z_f^{i,\\ast}(\\mathbf{p}, \\mathbf{e}^i) = \\sum_{j \\in G} p_j e^i_j - t\n", "\\end{equation}\n", "$" ] @@ -290,6 +290,79 @@ "$" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Our Game (with money and shift param)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We take Option 3 and we slightly adjust the utility function with a scalling factor $t$ and a shift param $\\alpha$:\n", + "$\n", + "\\begin{equation}\n", + "u(f^i, \\mathbf{x}^i, \\mathbf{s}^i) = f^i + \\sum_{j \\in G} t * s^i_j \\ln \\left(x^i_j + \\alpha\\right) \n", + "\\end{equation}\n", + "$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then it can be shown that the agent's demand functions are\n", + "$\n", + "\\begin{equation}\n", + "x^{i,\\ast}_j(\\mathbf{p}, \\mathbf{e}^i) = \\frac{s_j^{i} * t}{p_j} - \\alpha\n", + "\\end{equation}\n", + "$\n", + "for $j \\in G$ and \n", + "$\n", + "\\begin{equation}\n", + "f^{i,\\ast}(\\mathbf{p}, \\mathbf{e}^i) = \\sum_{j \\in G} p_j \\left(e^i_j + \\alpha \\right) + c^i - t\n", + "\\end{equation}\n", + "$\n", + "excess demand functions are\n", + "$\n", + "\\begin{equation}\n", + "z^{i,\\ast}_j(\\mathbf{p}, \\mathbf{e}^i) = \\frac{s_j^{i} * t}{p_j} - \\alpha - e_j^i\n", + "\\end{equation}\n", + "$\n", + "for $j \\in G$ and \n", + "$\n", + "\\begin{equation}\n", + "z_f^{i,\\ast}(\\mathbf{p}, \\mathbf{e}^i) = \\sum_{j \\in G} p_j \\left(e^i_j + \\alpha \\right) - t\n", + "\\end{equation}\n", + "$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And therefore\n", + "$\n", + "\\begin{equation}\n", + "p_j = t\\frac{\\sum_{i \\in I} s^i_j}{n\\alpha + \\sum_{i \\in I} e^i_j},\n", + "\\end{equation}\n", + "$\n", + "and\n", + "$\n", + "\\begin{equation}\n", + "x_j^i = \\frac{s^i_j \\left(n\\alpha + \\sum_{i \\in I} e^i_j\\right) }{\\sum_{i \\in I} s^i_j} - \\alpha,\n", + "\\end{equation}\n", + "$\n", + "and\n", + "$\n", + "\\begin{equation}\n", + "f^i = t\\sum_{j \\in G}\\frac{\\sum_{i \\in I} s^i_j}{n\\alpha + \\sum_{i \\in I} e^i_j}\\left(e_j^i + \\alpha \\right) +c^i - t ,\n", + "\\end{equation}\n", + "$" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -417,7 +490,8 @@ "initial_money_endowment = 200\n", "uniform_lower_bound_factor = 2\n", "uniform_upper_bound_factor = 10\n", - "scaling_factor = 100.0" + "scaling_factor = 100.0\n", + "shift = 1" ] }, { @@ -430,16 +504,16 @@ "output_type": "stream", "text": [ "Utility function params shape: 10 x 7\n", - "Utility function params: [[0.1857, 0.0716, 0.122, 0.1034, 0.1485, 0.1645, 0.2043],\n", - " [0.1439, 0.2099, 0.2146, 0.0189, 0.158, 0.2028, 0.0519],\n", - " [0.0025, 0.2405, 0.1443, 0.1949, 0.1544, 0.0684, 0.195],\n", - " [0.0842, 0.1789, 0.1439, 0.2035, 0.1544, 0.1614, 0.0737],\n", - " [0.1818, 0.0801, 0.0952, 0.1039, 0.1558, 0.197, 0.1862],\n", - " [0.1017, 0.0424, 0.0706, 0.0593, 0.2768, 0.2401, 0.2091],\n", - " [0.1492, 0.1051, 0.2441, 0.1119, 0.0034, 0.2915, 0.0948],\n", - " [0.1929, 0.1039, 0.1395, 0.2433, 0.0208, 0.2255, 0.0741],\n", - " [0.1761, 0.1433, 0.1821, 0.1224, 0.0746, 0.2567, 0.0448],\n", - " [0.2111, 0.0022, 0.1911, 0.1778, 0.2022, 0.04, 0.1756]]\n" + "Utility function params: [[0.1421, 0.1448, 0.1727, 0.0418, 0.0724, 0.2786, 0.1476],\n", + " [0.0833, 0.31, 0.1667, 0.2433, 0.03, 0.0633, 0.1034],\n", + " [0.0364, 0.3318, 0.2227, 0.1773, 0.0727, 0.1182, 0.0409],\n", + " [0.0854, 0.2313, 0.2349, 0.1246, 0.0961, 0.0498, 0.1779],\n", + " [0.0446, 0.0891, 0.1866, 0.2423, 0.195, 0.1922, 0.0502],\n", + " [0.047, 0.1436, 0.1123, 0.2376, 0.0392, 0.2637, 0.1566],\n", + " [0.0148, 0.1852, 0.1333, 0.2481, 0.0778, 0.063, 0.2778],\n", + " [0.1341, 0.1976, 0.1317, 0.0537, 0.0878, 0.2146, 0.1805],\n", + " [0.0932, 0.1695, 0.1737, 0.0805, 0.1949, 0.1992, 0.089],\n", + " [0.1919, 0.1967, 0.1919, 0.1493, 0.1374, 0.1185, 0.0143]]\n" ] } ], @@ -482,23 +556,24 @@ "name": "stdout", "output_type": "stream", "text": [ - "Utility of first agent from random bundle: 1.2629093134523766, [2, 2, 5, 6, 5, 3, 4]\n" + "Utility of first agent from random bundle: 1.6234039411451944, [2, 9, 10, 8, 2, 3, 2]\n" ] } ], "source": [ - "def utility(utility_function_params: List[float], good_bundle: List[int]) -> float:\n", + "def utility(utility_function_params: List[float], good_bundle: List[int], shift: int) -> float:\n", " \"\"\"\n", " Compute agent's utility given her utilit function params and a good bundle.\n", " :param utility_function_params: utility function params of the agent\n", " :param good_bundle: a bundle of goods with the quantitity for each good\n", + " :param shift: the shift parameter\n", " :return: utility value\n", " \"\"\"\n", - " goodwise_utility = [param * math.log(quantity) for param, quantity in zip(utility_function_params, good_bundle)]\n", + " goodwise_utility = [param * math.log(quantity + shift) for param, quantity in zip(utility_function_params, good_bundle)]\n", " return sum(goodwise_utility)\n", "\n", "random_bundle = [random.randint(1,10) for _ in range(nb_goods)]\n", - "utility = utility(utility_function_params[0], random_bundle)\n", + "utility = utility(utility_function_params[0], random_bundle, shift)\n", "print(\"Utility of first agent from random bundle: \", str(utility) + ', ' + pprint.pformat(random_bundle)) " ] }, @@ -512,16 +587,16 @@ "output_type": "stream", "text": [ "Endowments: \n", - "[[ 5 9 3 4 6 6 6]\n", - " [ 5 6 3 7 5 3 5]\n", - " [ 4 12 1 4 6 3 3]\n", - " [ 4 9 7 4 3 4 1]\n", - " [ 6 11 4 3 6 3 3]\n", - " [ 3 10 4 4 4 5 4]\n", - " [ 4 9 2 2 10 2 3]\n", - " [ 5 12 5 3 6 5 8]\n", - " [ 5 8 3 3 8 5 3]\n", - " [ 9 9 3 1 6 1 2]]\n" + "[[ 7 2 10 11 6 7 2]\n", + " [ 6 5 6 12 4 12 3]\n", + " [ 9 6 7 8 3 7 3]\n", + " [11 3 10 7 3 9 7]\n", + " [11 3 15 9 7 12 6]\n", + " [ 5 7 7 11 1 8 1]\n", + " [ 7 2 4 8 8 12 2]\n", + " [ 8 8 10 11 2 7 3]\n", + " [ 6 10 11 12 3 8 2]\n", + " [13 4 8 10 4 9 6]]\n" ] } ], @@ -570,7 +645,7 @@ "metadata": {}, "outputs": [], "source": [ - "def _compute_competitive_equilibrium_money(endowments: List[List[int]], utility_function_params: List[List[float]], scaling_factor: float, initial_money_endowment: float) -> (List[float], List[int]):\n", + "def _compute_competitive_equilibrium_money(endowments: List[List[int]], utility_function_params: List[List[float]], scaling_factor: float, initial_money_endowment: float, shift: int) -> (List[float], List[int]):\n", " \"\"\"\n", " Computes the competitive equilibrium prices and allocation, assuming money.\n", " \"\"\"\n", @@ -578,9 +653,9 @@ " scaled_utility_function_params_a = np.array(utility_function_params, dtype=np.float) * scaling_factor\n", " endowments_by_good = np.sum(endowments_a, axis=0)\n", " scaled_params_by_good = np.sum(scaled_utility_function_params_a, axis=0)\n", - " eq_prices = np.divide(scaled_params_by_good, endowments_by_good)\n", - " eq_allocation = np.divide(scaled_utility_function_params_a, eq_prices)\n", - " eq_money = np.transpose(np.dot(eq_prices, np.transpose(endowments_a))) + initial_money_endowment - scaling_factor\n", + " eq_prices = np.divide(scaled_params_by_good, shift*len(endowments) + endowments_by_good)\n", + " eq_allocation = np.divide(scaled_utility_function_params_a, eq_prices) - shift\n", + " eq_money = np.transpose(np.dot(eq_prices, np.transpose(endowments_a + shift))) + initial_money_endowment - scaling_factor\n", " return eq_prices, eq_allocation, eq_money\n", " " ] @@ -593,8 +668,8 @@ { "data": { "text/plain": [ - "array([2.8582 , 1.23989474, 4.42114286, 3.82657143, 2.24816667,\n", - " 4.99432432, 3.44605263])" + "array([0.93849462, 3.33266667, 1.76173469, 1.46651376, 1.9672549 ,\n", + " 1.54564356, 2.75155556])" ] }, "execution_count": 6, @@ -603,7 +678,7 @@ } ], "source": [ - "eq_prices, eq_allocation, eq_money = _compute_competitive_equilibrium_money(endowments,utility_function_params, scaling_factor, initial_money_endowment)\n", + "eq_prices, eq_allocation, eq_money = _compute_competitive_equilibrium_money(endowments,utility_function_params, scaling_factor, initial_money_endowment, shift)\n", "eq_prices" ] }, @@ -615,26 +690,26 @@ { "data": { "text/plain": [ - "array([[ 6.49709607, 5.77468376, 2.75946749, 2.70215784, 6.60538216,\n", - " 3.29373884, 5.92852234],\n", - " [ 5.03463718, 16.92885644, 4.85394856, 0.49391473, 7.0279487 ,\n", - " 4.06060934, 1.50607102],\n", - " [ 0.08746764, 19.39680788, 3.26386196, 5.09333234, 6.86781822,\n", - " 1.36955463, 5.65864834],\n", - " [ 2.94591001, 14.4286442 , 3.25481453, 5.31807661, 6.86781822,\n", - " 3.23166838, 2.13867889],\n", - " [ 6.36064656, 6.46022583, 2.15328939, 2.71522437, 6.93009119,\n", - " 3.94447752, 5.4032837 ],\n", - " [ 3.55818347, 3.41964513, 1.59687217, 1.54969014, 12.31225443,\n", - " 4.80745711, 6.06781214],\n", - " [ 5.22006857, 8.47652602, 5.52119685, 2.92428881, 0.15123434,\n", - " 5.83662536, 2.75097365],\n", - " [ 6.74900287, 8.37974361, 3.15529275, 6.35817218, 0.92519831,\n", - " 4.51512528, 2.15028637],\n", - " [ 6.16122035, 11.55743272, 4.11884451, 3.19868588, 3.31825932,\n", - " 5.13983441, 1.30003818],\n", - " [ 7.38576727, 0.17743442, 4.32241179, 4.6464571 , 8.99399511,\n", - " 0.80090914, 5.09568538]])" + "array([[14.14126948, 3.34486897, 8.80283811, 1.85029715, 2.68025516,\n", + " 17.02485427, 4.36423841],\n", + " [ 7.87591659, 8.30186037, 8.4622647 , 15.59036597, 0.52496761,\n", + " 3.09538146, 2.75787433],\n", + " [ 2.87855179, 8.9559912 , 11.6409499 , 11.08989678, 2.69550483,\n", + " 6.64729998, 0.48643192],\n", + " [ 8.09967919, 5.94038808, 12.33344917, 7.49634032, 3.88497957,\n", + " 2.22195888, 5.46543369],\n", + " [ 3.75229148, 1.67353471, 9.59183319, 15.52217704, 8.91228944,\n", + " 11.43494971, 0.82442255],\n", + " [ 4.00802016, 3.30886177, 5.37439907, 15.20168908, 0.99262434,\n", + " 16.06085453, 4.69132612],\n", + " [ 0.57699358, 4.55711142, 6.56640602, 15.91767282, 2.95474933,\n", + " 3.07597207, 9.09610725],\n", + " [13.28884051, 4.92918584, 6.47558645, 2.66174539, 3.46307186,\n", + " 12.88418423, 5.5599257 ],\n", + " [ 8.93079743, 4.0860172 , 8.85960035, 4.48920863, 8.90720622,\n", + " 11.8878355 , 2.234534 ],\n", + " [19.44763978, 4.90218044, 9.89267304, 9.18060682, 5.98435164,\n", + " 6.66670937, -0.48029398]])" ] }, "execution_count": 7, @@ -654,9 +729,9 @@ { "data": { "text/plain": [ - "array([218.15102865, 205.23386646, 184.84909628, 199.01398827,\n", - " 198.76245869, 201.71290333, 181.89575441, 228.78420809,\n", - " 202.2484136 , 179.34828222])" + "array([188.87380325, 198.89814719, 191.24650465, 201.04169357,\n", + " 222.53778931, 187.33275676, 185.56658143, 204.69083383,\n", + " 213.46876928, 206.34312073])" ] }, "execution_count": 8, @@ -672,18 +747,29 @@ "cell_type": "code", "execution_count": 9, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[83 50 88 99 41 91 35]\n", + "[83. 50. 88. 99. 41. 91. 35.]\n" + ] + } + ], "source": [ - "def _test_compute_competitive_equilibrium_money(endowments: List[List[int]], utility_function_params: List[List[float]], scaling_factor: float, initial_money_endowment: float) -> None:\n", + "def _test_compute_competitive_equilibrium_money(endowments: List[List[int]], utility_function_params: List[List[float]], scaling_factor: float, initial_money_endowment: float, shift: int) -> None:\n", " \"\"\"\n", " \"\"\"\n", " endowments_by_good = np.sum(np.array(endowments, dtype=np.int), axis=0)\n", - " eq_prices, eq_allocation, eq_money = _compute_competitive_equilibrium_money(endowments,utility_function_params, scaling_factor, initial_money_endowment)\n", + " eq_prices, eq_allocation, eq_money = _compute_competitive_equilibrium_money(endowments,utility_function_params, scaling_factor, initial_money_endowment, shift)\n", " eq_allocation_by_good = np.sum(eq_allocation, axis=0)\n", + " print(endowments_by_good)\n", + " print(eq_allocation_by_good)\n", " assert(np.allclose(endowments_by_good, eq_allocation_by_good))\n", " assert(np.allclose(len(endowments) * initial_money_endowment, np.sum(eq_money)))\n", "\n", - "_test_compute_competitive_equilibrium_money(endowments, utility_function_params, scaling_factor, initial_money_endowment)" + "_test_compute_competitive_equilibrium_money(endowments, utility_function_params, scaling_factor, initial_money_endowment, shift)" ] }, { @@ -703,32 +789,32 @@ "output_type": "stream", "text": [ "Equilibrium price vector: [1.0,\n", - " 0.4250944750428704,\n", - " 1.487911893695501,\n", - " 1.3163183865891492,\n", - " 0.75929694518813,\n", - " 1.7500064971830491,\n", - " 1.1643177419391881]\n", - "Equilibrium allocation: array([[ 7.53878613, 6.83781082, 3.32868067, 3.18896028, 7.93970416,\n", - " 3.81606497, 7.12338423],\n", - " [ 5.19430422, 17.82352361, 5.20617893, 0.51828357, 7.51124663,\n", - " 4.18306754, 1.60902366],\n", - " [ 0.07288268, 16.49354342, 2.82731022, 4.31653473, 5.92816102,\n", - " 1.13946447, 4.88255837],\n", - " [ 2.85848405, 14.28724727, 3.283275 , 5.24840266, 6.90334097,\n", - " 3.13103232, 2.14891706],\n", - " [ 6.15854706, 6.38309695, 2.1674252 , 2.67386197, 6.95088632,\n", - " 3.8133876 , 5.4174205 ],\n", - " [ 3.55058672, 3.48224695, 1.65655804, 1.57279789, 12.72722155,\n", - " 4.78995761, 6.26991551],\n", - " [ 4.18062078, 6.92770215, 4.59687311, 2.38199634, 0.1254698 ,\n", - " 4.66735516, 2.28143854],\n", - " [ 8.50881985, 10.78120213, 4.13555779, 8.1530154 , 1.20833897,\n", - " 5.68386804, 2.80726748],\n", - " [ 6.18653527, 11.84264927, 4.29952886, 3.26669515, 3.45155909,\n", - " 5.15316852, 1.35174458],\n", - " [ 5.75043324, 0.14097742, 3.49861218, 3.679452 , 7.25407149,\n", - " 0.62263376, 4.10833007]])\n" + " 3.678034340829961,\n", + " 1.8488358628250527,\n", + " 1.498334153356826,\n", + " 2.4285842534879545,\n", + " 1.6042611644048088,\n", + " 3.161736002015897]\n", + "Equilibrium allocation: array([[11.57417412, 3.206629 , 7.60834004, 2.27228884, 2.4281825 ,\n", + " 14.1449682 , 3.80239024],\n", + " [ 7.65642585, 7.74688294, 8.28740041, 14.92500582, 1.1354007 ,\n", + " 3.62668402, 3.00591028],\n", + " [ 3.0575097 , 7.57751948, 10.11785195, 9.93954528, 2.51447716,\n", + " 6.18883446, 1.08658615],\n", + " [ 8.10164658, 5.96589048, 12.05313673, 7.88905142, 3.75392539,\n", + " 2.94489419, 5.33784264],\n", + " [ 5.28390865, 2.8700031 , 11.9573165 , 19.15866207, 9.5126579 ,\n", + " 14.19380241, 1.88104227],\n", + " [ 3.6939263 , 3.0685205 , 4.77388297, 12.46315873, 1.26859578,\n", + " 12.91889678, 3.89275008],\n", + " [ 1.16537221, 3.96486262, 5.67720705, 13.03830171, 2.52248948,\n", + " 3.09220601, 6.91846229],\n", + " [13.13730949, 5.26318716, 6.97854802, 3.51110049, 3.5417589 ,\n", + " 13.10485743, 5.59279956],\n", + " [10.02280832, 4.95595592, 10.10357278, 5.77777618, 8.63042488,\n", + " 13.35327449, 3.02717769],\n", + " [19.30691878, 5.3805488 , 10.44274355, 10.02510947, 5.69208731,\n", + " 7.43158201, 0.4550388 ]])\n" ] } ], @@ -809,13 +895,6 @@ "_test_compute_competitive_equilibrium(endowments,utility_function_params)" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, { "cell_type": "code", "execution_count": null, @@ -840,7 +919,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.2" + "version": "3.7.3" } }, "nbformat": 4, diff --git a/oef_search_pluto_scripts/launch.py b/oef_search_pluto_scripts/launch.py index be24e448..48368f5a 100755 --- a/oef_search_pluto_scripts/launch.py +++ b/oef_search_pluto_scripts/launch.py @@ -14,8 +14,12 @@ def run(cmd): return c.returncode -def fail(*x): +def error(*x): print(''.join([str(xx) for xx in x]), file=sys.stderr) + + +def fail(*x): + error(x) exit(1) @@ -31,7 +35,7 @@ def pull_image(run_sudo, img): ] r = run(c) if r != 0: - fail("can't pull " + img) + error("can't pull " + img) def parse_command(j): diff --git a/oef_search_pluto_scripts/launch_config_ci.json b/oef_search_pluto_scripts/launch_config_ci.json index 8c67e9cd..51996ed6 100644 --- a/oef_search_pluto_scripts/launch_config_ci.json +++ b/oef_search_pluto_scripts/launch_config_ci.json @@ -13,9 +13,7 @@ ], "cmd": { "oef-search": { - "positional_args": ["run_mode", "run_sh_in_the_end"], - "run_mode": "node", - "run_sh_in_the_end": "no_sh", + "positional_args": ["config_file"], "config_file": "/config/node_config_latest.json" } } diff --git a/oef_search_pluto_scripts/launch_config_latest.json b/oef_search_pluto_scripts/launch_config_latest.json index d0de648a..e4db0d0f 100644 --- a/oef_search_pluto_scripts/launch_config_latest.json +++ b/oef_search_pluto_scripts/launch_config_latest.json @@ -10,14 +10,14 @@ "-p", "10000:10000", "-p", - "40000:40000" + "40000:40000", + "-p", + "7500:7500" ] ], "cmd": { "oef-search": { - "positional_args": ["run_mode", "run_sh_in_the_end"], - "run_mode": "node", - "run_sh_in_the_end": "no_sh", + "positional_args": ["config_file"], "config_file": "/config/node_config_latest.json" } } diff --git a/oef_search_pluto_scripts/node_config_latest.json b/oef_search_pluto_scripts/node_config_latest.json index 8a85c28b..4ffd60f6 100644 --- a/oef_search_pluto_scripts/node_config_latest.json +++ b/oef_search_pluto_scripts/node_config_latest.json @@ -5,19 +5,32 @@ "search_broadcast_cache_lifetime_sec": 6, "director_port": 40000, "html_dir": "api/src/resources/website", + "prometheus_api_path": "/metrics", + "prometheus_log_file": "./fetch-logs/core-vars.txt", + "search_prometheus_log_file": "./fetch-logs/search-vars.txt", "http_port": 7500, - "log_dir": "/logs", - "ssl_certificate": "/app/server.pem", + "log_dir": "./fetch-logs/", + "search_log": "/logs/search.log", + "core_log": "/logs/core.log", + "ssl_certificate": "", "core": { - "core_binary": "fetch_teams/OEFCore", + "core_binary": "/oef-mt-core/bazel-bin/mt-core/main/src/cpp/app", "config": { "core_key": "CoreKey", "core_uri": "tcp://127.0.0.1:10000", + "ws_uri": "", + "ssl_uri": "ssl://127.0.0.1:15000", + "white_list_file":"", + "core_cert_pk_file": "/app/ssl/core.pem", + "core_pubkey_file": "/app/ssl/core_pub.pem", + "tmp_dh_file": "/app/ssl/dh2048.pem", "search_uri": "tcp://127.0.0.1:20000", "tasks_thread_count": 10, "comms_thread_count": 10, "karma_policy": { - } + }, + "prometheus_log_interval": 3, + "prometheus_log_file": "/logs/core-vars.txt" } }, "search_peers": ["KEY:HOST:PORT"], @@ -37,6 +50,7 @@ "fields": ["bootstrap:private-key"] }, "search_config": { + "search_prometheus_log_file": "/logs/search-vars.txt", "daps": { "network_search": { "class": "DapERNetwork", diff --git a/sandbox/.env b/sandbox/.env index bac9511b..8d375df5 100644 --- a/sandbox/.env +++ b/sandbox/.env @@ -13,4 +13,7 @@ REGISTRATION_TIMEOUT=30 INACTIVITY_TIMEOUT=180 COMPETITION_TIMEOUT=300 SEED=42 -WHITELIST= \ No newline at end of file +WHITELIST= +REGISTER_AS=both +SEARCH_FOR=both +PENDING_TRANSACTION_TIMEOUT=120 \ No newline at end of file diff --git a/sandbox/README.md b/sandbox/README.md index 43c6a327..b7f686bf 100644 --- a/sandbox/README.md +++ b/sandbox/README.md @@ -29,7 +29,6 @@ To configure the execution of TAC, you can tune the following parameters: - `NB_GOODS` is the number of types of goods available for trade in the competition. - `NB_BASELINE_AGENTS` is the number of baseline agents spawned in the TAC instance. - `OEF_ADDR` and `OEF_PORT` allow you to specify a different OEF Node to use for the sandbox. -- `SERVICE_REGISTRATION_STRATEGY` indicates whether the baseline agent registers supply, demand or both services on the oef. - `DATA_OUTPUT_DIR` is the output directory to use for storing simulation data in `${DATA_OUTPUT_DIR}/${EXPERIMENT_ID}`. - `EXPERIMENT_ID` is the name to give to the simulation. - `LOWER_BOUND_FACTOR` is the lower bound factor of a uniform distribution used for generating good instances. @@ -39,7 +38,10 @@ To configure the execution of TAC, you can tune the following parameters: - `INACTIVITY_TIMEOUT` is the amount of time (in seconds) to wait during inactivity until the termination of the competition. - `COMPETITION_TIMEOUT` is the amount of time (in seconds) to wait from the start of the competition until the termination of the competition. - `SEED` is the seed for the random module. - +- `WHITELIST` is the public keys permitted by the controller for the competition. +- `REGISTER_AS` indicates whether the baseline agent registers supply, demand or both services on the oef. +- `SEARCH_FOR` indicates whether the baseline agent searches supply, demand or both services on the oef. +- `PENDING_TRANSACTION_TIMEOUT` is the amount of time an in-flight transaction is kept in the transaction manager. Specify the values in the [`.env`](.env) file. @@ -75,7 +77,7 @@ If you want to include your own agents, set `NB_AGENTS` to a number equal to `NB - Connect your agents to `localhost:10000`, e.g.: ``` -python3 ../templates/v1/*.py +python ../templates/v1/*.py ``` Be careful with the values of `NB_AGENTS` and `NB_BASELINE_AGENTS`: @@ -86,7 +88,7 @@ Be careful with the values of `NB_AGENTS` and `NB_BASELINE_AGENTS`: To run the sandbox multiple times, use the script `run_iterated_games.py`: - python3 run_iterated_games.py --config config.json + python run_iterated_games.py --config config.json Usage: ``` diff --git a/sandbox/docker-compose.yml b/sandbox/docker-compose.yml index be0a12eb..d515c800 100644 --- a/sandbox/docker-compose.yml +++ b/sandbox/docker-compose.yml @@ -6,6 +6,7 @@ services: - "10000:10000" - "20000:20000" - "40000:40000" + - "7500:7500" networks: main_net: ipv4_address: 172.28.1.1 @@ -15,9 +16,6 @@ services: - "../oef_search_pluto_scripts:/config" - "../data/oef-logs:/logs/" command: - - "node" - - "no_sh" - - "--config_file" - "/config/node_config_latest.json" healthcheck: interval: 10s @@ -34,6 +32,8 @@ services: image: "hypnosapos/visdom:latest" ports: - "8097:8097" + logging: + driver: none networks: main_net: ipv4_address: 172.28.1.2 @@ -90,7 +90,13 @@ services: - "${COMPETITION_TIMEOUT}" - "--services-interval" - "${SERVICES_INTERVAL}" - - "--gui" + - "--pending-transaction-timeout" + - "${PENDING_TRANSACTION_TIMEOUT}" + - "--register-as" + - "${REGISTER_AS}" + - "--search-for" + - "${SEARCH_FOR}" + - "--dashboard" - "--visdom-addr" - "172.28.1.2" - "--visdom-port" diff --git a/sandbox/playground.py b/sandbox/playground.py new file mode 100644 index 00000000..c63c5d30 --- /dev/null +++ b/sandbox/playground.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +# -*- 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. +# +# ------------------------------------------------------------------------------ + +"""Play ground to spin up an agent and interact with it.""" + +import docker +import inspect +import json +import pdb +import os +import re +import subprocess +import time + +from typing import Dict, Optional + +from oef.messages import CFP, Message +from oef.uri import Context + +from tac.agents.v1.base.dialogues import Dialogue +from tac.agents.v1.examples.baseline import BaselineAgent +from tac.agents.v1.examples.strategy import BaselineStrategy +from tac.platform.protocol import GameData + +CUR_PATH = inspect.getfile(inspect.currentframe()) +ROOT_DIR = os.path.join(os.path.dirname(CUR_PATH), "..") + + +def kill_oef(): + """Kill any running OEF instance.""" + client = docker.from_env() + for container in client.containers.list(): + if any(re.match("fetchai/oef-search", tag) for tag in container.image.tags): + print("Stopping existing OEF Node...") + container.stop() + + +def launch_oef(): + """Launch an OEF node instance.""" + script_path = os.path.join("oef_search_pluto_scripts", "launch.py") + configuration_file_path = os.path.join("oef_search_pluto_scripts", "launch_config_latest.json") + print("Launching new OEF Node...") + subprocess.Popen(["python3", script_path, "-c", configuration_file_path, "--background"], + stdout=subprocess.PIPE, env=os.environ, cwd=ROOT_DIR) + + # Wait for OEF + print("Waiting for the OEF to be operative...") + wait_for_oef = subprocess.Popen([ + os.path.join("sandbox", "wait-for-oef.sh"), + "127.0.0.1", + "10000", + ":" + ], env=os.environ, cwd=ROOT_DIR) + + wait_for_oef.wait(30) + + +if __name__ == '__main__': + + kill_oef() + launch_oef() + try: + # Create an agent + # Creating an agent is straightforward. You simply import the `BaselineAgent` and `BaselineStrategy` and instantiate them. + strategy = BaselineStrategy() + agent_one = BaselineAgent(name='agent_one', oef_addr='127.0.0.1', oef_port=10000, strategy=strategy) + agent_two = BaselineAgent(name='agent_two', oef_addr='127.0.0.1', oef_port=10000, strategy=strategy) + + # Feed the agent some game data + # To start, we require some game data to feed to the agent: + money = 100.0 + initial_endowments = [2, 2, 2, 2] + utility_params_one = [50.0, 25.0, 20.0, 5.0] + utility_params_two = [5.0, 15.0, 30.0, 50.0] + nb_agents = 2 + nb_goods = 4 + tx_fee = 0.01 + agent_pbk_to_name = {agent_one.crypto.public_key: agent_one.name, agent_two.crypto.public_key: agent_two.name} + good_pbk_to_name = {'good_1_pbk': 'good_1', 'good_2_pbk': 'good_2', 'good_3_pbk': 'good_3', 'good_4_pbk': 'good_4'} + + game_data_one = GameData(agent_one.crypto.public_key, + agent_one.crypto, + money, + initial_endowments, + utility_params_one, + nb_agents, + nb_goods, + tx_fee, + agent_pbk_to_name, + good_pbk_to_name) + agent_one.game_instance.init(game_data_one, agent_one.crypto.public_key) + + game_data_two = GameData(agent_two.crypto.public_key, + agent_two.crypto, + money, + initial_endowments, + utility_params_one, + nb_agents, + nb_goods, + tx_fee, + agent_pbk_to_name, + good_pbk_to_name) + agent_two.game_instance.init(game_data_two, agent_two.crypto.public_key) + + # Set the debugger + print("Setting debugger ... To continue enter: c + Enter") + pdb.set_trace() + + # agent_one initiates a dialogue + is_seller = True + dialogue = agent_one.game_instance.dialogues.create_self_initiated(agent_two.crypto.public_key, agent_one.crypto.public_key, is_seller) # type: Dialogue + + # agent_one creates a CFP and enqueues it in the outbox + starting_message_id = 1 + starting_message_target = 0 + services = agent_one.game_instance.build_services_dict(is_supply=is_seller) # type: Dict + cfp = CFP(starting_message_id, dialogue.dialogue_label.dialogue_id, agent_two.crypto.public_key, starting_message_target, json.dumps(services).encode('utf-8'), Context()) + dialogue.outgoing_extend([cfp]) + agent_one.out_box.out_queue.put(cfp) + + # Send the messages in the outbox + agent_one.out_box.send_nowait() + + # Check the message arrived in the inbox of agent_two + checks = 0 + while checks < 10: + if agent_two.in_box.is_in_queue_empty(): + # Wait a bit + print("Sleeping for 1 second ...") + time.sleep(1.0) + checks += 1 + else: + checks = 10 + msg = agent_two.in_box.get_no_wait() # type: Optional[Message] + print("The msg is a CFP: {}".format(isinstance(msg, CFP))) + + # Set the debugger + print("Setting debugger ... To continue enter: c + Enter") + pdb.set_trace() + + finally: + kill_oef() diff --git a/sandbox/run_iterated_games.py b/sandbox/run_iterated_games.py index 3fd45ffc..9cb23d10 100644 --- a/sandbox/run_iterated_games.py +++ b/sandbox/run_iterated_games.py @@ -188,10 +188,10 @@ def compute_aggregate_scores(all_game_stats: List[GameStats]) -> Dict[str, float """ result = defaultdict(lambda: 0) for game_stats in all_game_stats: - agent_names = game_stats.game.configuration.agent_names - agent_scores = game_stats.game.get_scores() - for name, score in zip(agent_names, agent_scores): - result[name] += score + agent_pbk_to_scores = game_stats.game.get_scores() + for pbk, score in agent_pbk_to_scores.items(): + agent_name = game_stats.game.configuration.agent_pbk_to_name[pbk] + result[agent_name] += score return result diff --git a/scripts/generate_private_key.py b/scripts/generate_private_key.py index ac6d79ff..7618d979 100644 --- a/scripts/generate_private_key.py +++ b/scripts/generate_private_key.py @@ -21,6 +21,7 @@ """ Generate a private key to be used for the Trading Agent Competition. + It prints the key in PEM format to the specified file. """ diff --git a/scripts/launch.py b/scripts/launch.py new file mode 100644 index 00000000..fafe2642 --- /dev/null +++ b/scripts/launch.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# -*- 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. +# +# ------------------------------------------------------------------------------ + +"""Start a sandbox.""" + +import inspect +import os +import re +import subprocess +from typing import Optional + +import docker + +import tac.agents.v1.examples.baseline + +CUR_PATH = inspect.getfile(inspect.currentframe()) +ROOT_DIR = os.path.join(os.path.dirname(CUR_PATH), "..") + + +class Sandbox: + """Class to manage the sandbox.""" + + def _build_sandbox(self): + """Build sandbox.""" + sandbox_build_process = None # type: Optional[subprocess.Popen] + sandbox_build_process = subprocess.Popen(["docker-compose", "build"], + env=os.environ, + cwd=os.path.join(ROOT_DIR, "sandbox")) + sandbox_build_process.wait() + + def _stop_oef_search_images(self): + """Stop any running OEF nodes.""" + client = docker.from_env() + for container in client.containers.list(): + if any(re.match("fetchai/oef-search", tag) for tag in container.image.tags): + print("Stopping existing OEF Node...") + container.stop() + + def __enter__(self): + """Define what the context manager should do at the beginning of the block.""" + self._stop_oef_search_images() + self._build_sandbox() + + print("Launching sandbox...") + self.sandbox_process = subprocess.Popen(["docker-compose", "up", "--abort-on-container-exit"], + env=os.environ, + cwd=os.path.join(ROOT_DIR, "sandbox")) + + def __exit__(self, exc_type, exc_val, exc_tb): + """Define what the context manager should do after the block has been executed.""" + print("Terminating sandbox...") + self.sandbox_process.terminate() + + +def wait_for_oef(): + """Wait for the OEF to come live.""" + print("Waiting for the OEF to be operative...") + wait_for_oef = subprocess.Popen([ + os.path.join("sandbox", "wait-for-oef.sh"), + "127.0.0.1", + "10000", + ":" + ], env=os.environ, cwd=ROOT_DIR) + + wait_for_oef.wait(30) + + +if __name__ == '__main__': + + with Sandbox(): + wait_for_oef() + tac.agents.v1.examples.baseline.main(name="my_agent", dashboard=True) diff --git a/scripts/launch_alt.py b/scripts/launch_alt.py new file mode 100644 index 00000000..a199a10c --- /dev/null +++ b/scripts/launch_alt.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# -*- 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. +# +# ------------------------------------------------------------------------------ + +"""Start a Visdom server, an OEF node instance, and run the simulation script.""" + +import inspect +import os +import platform +import re +import subprocess +import sys + +import docker + +import tac +from tac.platform.simulation import parse_arguments, build_simulation_parameters + +CUR_PATH = inspect.getfile(inspect.currentframe()) +ROOT_DIR = os.path.join(os.path.dirname(CUR_PATH), "..") + + +class VisdomServer: + """Class to manage the visdom server.""" + + def __enter__(self): + """Define what the context manager should do at the beginning of the block.""" + if platform.system() == 'Darwin': + # This is required due to a bug in mac os Mojave + print("Setting environment var...") + os.environ["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"] = "YES" + print("Starting Visdom server...") + self.proc = subprocess.Popen(["python3", "-m", "visdom.server"], env=os.environ, cwd=ROOT_DIR) + + def __exit__(self, exc_type, exc_val, exc_tb): + """Define what the context manager should do after the block has been executed.""" + print("Terminating Visdom server...") + self.proc.terminate() + + +class OEFNode: + """Class to manage the OEF node.""" + + def _stop_oef_search_images(self): + """Stop any running OEF nodes.""" + client = docker.from_env() + for container in client.containers.list(): + if any(re.match("fetchai/oef-search", tag) for tag in container.image.tags): + print("Stopping existing OEF Node...") + container.stop() + + def _wait_for_oef(self): + """Wait for the OEF to come live.""" + print("Waiting for the OEF to be operative...") + wait_for_oef = subprocess.Popen([ + os.path.join("sandbox", "wait-for-oef.sh"), + "127.0.0.1", + "10000", + ":" + ], env=os.environ, cwd=ROOT_DIR) + + wait_for_oef.wait(30) + + def __enter__(self): + """Define what the context manager should do at the beginning of the block.""" + self._stop_oef_search_images() + script_path = os.path.join("oef_search_pluto_scripts", "launch.py") + configuration_file_path = os.path.join("oef_search_pluto_scripts", "launch_config_latest.json") + print("Launching new OEF Node...") + self.oef_process = subprocess.Popen(["python3", script_path, "-c", configuration_file_path, "--background"], + stdout=subprocess.PIPE, env=os.environ, cwd=ROOT_DIR) + self._wait_for_oef() + self.id = self._get_image_id() + print("ID: ", self.id) + + def __exit__(self, exc_type, exc_val, exc_tb): + """Define what the context manager should do after the block has been executed.""" + print("Stopping OEF Node...") + p = subprocess.Popen(["docker", "stop", self.id], env=os.environ) + p.wait() + + def _get_image_id(self): + output = self.oef_process.communicate()[0] + id = output.splitlines()[-1].decode("utf-8") + return id + + +if __name__ == '__main__': + sys.argv += ['--dashboard'] + args = parse_arguments() + simulation_params = build_simulation_parameters(args) + + with VisdomServer(), OEFNode(): + tac.platform.simulation.run(simulation_params) diff --git a/setup.py b/setup.py index 60170f56..7dc847f7 100644 --- a/setup.py +++ b/setup.py @@ -103,7 +103,7 @@ def _fix_import_statements_in_protobuf_module(self, filename): author=about['__author__'], url=about['__url__'], long_description=readme, - packages=find_packages(), + packages=find_packages(include=["tac"]), cmdclass={ 'protoc': protoc, }, @@ -120,17 +120,25 @@ def _fix_import_statements_in_protobuf_module(self, filename): "numpy", "matplotlib", "flask", + "flask_restful", + "wtforms", "python-dateutil", "visdom", "cryptography", + "fetchai-ledger-api @ git+https://github.com/fetchai/ledger-api-py.git#egg=fetchai-ledger-api", "base58" ], tests_require=["tox"], - entry_points={ - 'console_scripts': ["tac=tac.__main__:main"], - }, zip_safe=False, include_package_data=True, + data_files=[ + ("sandbox", ["sandbox/docker-compose.yml", "sandbox/config.json", "sandbox/.env"] + + glob.glob("sandbox/*.py") + + glob.glob("sandbox/*.sh")), + ("templates/v1", glob.glob("templates/v1/*.py")), + ("simulation/v1", glob.glob("simulation/v1/*")), + ("oef_search_pluto_scripts", glob.glob("oef_search_pluto_scripts/*.py") + glob.glob("oef_search_pluto_scripts/*.json")) + ], license=about['__license__'], ) diff --git a/simulation/README.md b/simulation/README.md index 98e75ee8..4d5400cf 100644 --- a/simulation/README.md +++ b/simulation/README.md @@ -9,19 +9,27 @@ This tutorial shows how to simulate a TAC. - [x] You have followed the steps under 'Dependencies' and 'Preliminaries' on root readme. - [x] You are connected to the internet (to pull the latest docker images). + +## Quickstart + +Simply run: + + python scripts/launch_alt.py + +## Manual + - First, ensure that you are running an OEF Node on `localhost`, using this command (make sure all docker containers are stopped `docker stop $(docker ps -q)`): -``` -python3 oef_search_pluto_scripts/launch.py -c ./oef_search_pluto_scripts/launch_config.json -``` + python oef_search_pluto_scripts/launch.py -c ./oef_search_pluto_scripts/launch_config_latest.json + - Second, (in a new terminal window, from root and in shell) start a `visdom` server: python -m visdom.server -- Third, (in a new terminal window, from root and in shell) run the simulation example with the gui flag to visualize data in realtime: +- Third, (in a new terminal window, from root and in shell) run the simulation example with the dashboard flag to visualize data in realtime: - python simulation/v1/tac_agent_spawner.py --gui + python simulation/v1/tac_agent_spawner.py --dashboard - Finally, lean back and watch the competition on `http://localhost:8097` in your browser (you might have to select the right environment `tac && tac_controller` and deselect `main` in the visdom browser tab). @@ -51,7 +59,7 @@ For a full list, do `python simulation/tac_agent_spawner.py -h` - `--competition-timeout` is the amount of time (in seconds) to wait from the start of the competition until the termination of the competition. - `--visdom-addr` is the TCP/IP address of the Visdom server - `--visdom-port` is the TCP/IP port of the Visdom server -- `--gui` is a flag to specify that the gui is live and expecting an event stream. +- `--dashboard` is a flag to specify that the dashboard is live and expecting an event stream. - `--seed` is the seed for the random module. Example: @@ -75,10 +83,10 @@ Example: --registration-timeout 10 --inactivity-timeout 60 --competition-timeout 240 - --gui + --dashboard --seed 42 -It generates a `game.json` file in the `${data_output_dir}/${experiment_id}` that can be inspected with a GUI (see `tac/gui`). +It generates a `game.json` file in the `${data_output_dir}/${experiment_id}` that can be inspected with a dashboard (see `tac/gui`). ### Scalability diff --git a/simulation/v1/tac_agent_spawner.py b/simulation/v1/tac_agent_spawner.py index 84462d1e..0d6a6966 100644 --- a/simulation/v1/tac_agent_spawner.py +++ b/simulation/v1/tac_agent_spawner.py @@ -19,176 +19,10 @@ # ------------------------------------------------------------------------------ """Spawn several TAC agents.""" -import argparse -import datetime -import logging -import math -import multiprocessing -import pprint -import random -import signal -import time -from typing import Optional, List - -from tac.agents.v1.examples.baseline import main as baseline_main -from tac.platform.controller import main as controller_main - -logger = logging.getLogger("tac") - - -def parse_arguments(): - """Arguments parsing.""" - parser = argparse.ArgumentParser("tac_agent_spawner") - parser.add_argument("--nb-agents", type=int, default=10, help="(minimum) number of TAC agent to wait for the competition.") - parser.add_argument("--nb-goods", type=int, default=10, help="Number of TAC agent to run.") - parser.add_argument("--money-endowment", type=int, default=200, help="Initial amount of money.") - parser.add_argument("--base-good-endowment", default=2, type=int, help="The base amount of per good instances every agent receives.") - parser.add_argument("--lower-bound-factor", default=0, type=int, help="The lower bound factor of a uniform distribution.") - parser.add_argument("--upper-bound-factor", default=0, type=int, help="The upper bound factor of a uniform distribution.") - parser.add_argument("--tx-fee", default=0.1, type=float, help="The transaction fee.") - parser.add_argument("--oef-addr", default="127.0.0.1", help="TCP/IP address of the OEF Agent") - parser.add_argument("--oef-port", default=10000, help="TCP/IP port of the OEF Agent") - parser.add_argument("--nb-baseline-agents", type=int, default=10, help="Number of baseline agent to run. Defaults to the number of agents of the competition.") - parser.add_argument("--start-time", default=str(datetime.datetime.now() + datetime.timedelta(0, 10)), type=str, help="The start time for the competition (in UTC format).") - parser.add_argument("--registration-timeout", default=10, type=int, help="The amount of time (in seconds) to wait for agents to register before attempting to start the competition.") - parser.add_argument("--inactivity-timeout", default=60, type=int, help="The amount of time (in seconds) to wait during inactivity until the termination of the competition.") - parser.add_argument("--competition-timeout", default=240, type=int, help="The amount of time (in seconds) to wait from the start of the competition until the termination of the competition.") - parser.add_argument("--services-interval", default=5, type=int, help="The amount of time (in seconds) the baseline agents wait until it updates services again.") - parser.add_argument("--pending-transaction-timeout", default=120, type=int, help="The amount of time (in seconds) the baseline agents wait until the transaction confirmation.") - parser.add_argument("--register-as", choices=['seller', 'buyer', 'both'], default='both', help="The string indicates whether the baseline agent registers as seller, buyer or both on the oef.") - parser.add_argument("--search-for", choices=['sellers', 'buyers', 'both'], default='both', help="The string indicates whether the baseline agent searches for sellers, buyers or both on the oef.") - parser.add_argument("--gui", action="store_true", help="Enable the GUI.") - parser.add_argument("--data-output-dir", default="data", help="The output directory for the simulation data.") - parser.add_argument("--experiment-id", default=None, help="The experiment ID.") - parser.add_argument("--visdom-addr", default="localhost", help="TCP/IP address of the Visdom server") - parser.add_argument("--visdom-port", default=8097, help="TCP/IP port of the Visdom server") - parser.add_argument("--seed", default=42, help="The random seed of the simulation.") - parser.add_argument("--whitelist-file", nargs="?", default=None, type=str, help="The file that contains the list of agent names to be whitelisted.") - - arguments = parser.parse_args() - logger.debug("Arguments: {}".format(pprint.pformat(arguments.__dict__))) - - return arguments - - -def _make_id(agent_id: int, is_world_modeling: bool, nb_agents: int) -> str: - """ - Make the name for baseline agents from an integer identifier. - - E.g.: - - >>> _make_id(2, 10) - 'agent_2' - >>> _make_id(2, 100) - 'agent_02' - >>> _make_id(2, 101) - 'agent_002' - - :param agent_id: the agent id. - :param is_world_modeling: the boolean indicated whether the baseline agent models the world around her or not. - :param nb_agents: the overall number of agents. - :return: the formatted name. - :return: the string associated to the integer id. - """ - max_number_of_digits = math.ceil(math.log10(nb_agents)) - if is_world_modeling: - string_format = "tac_agent_{:0" + str(max_number_of_digits) + "}_wm" - else: - string_format = "tac_agent_{:0" + str(max_number_of_digits) + "}" - result = string_format.format(agent_id) - return result - - -def spawn_controller_agent(arguments): - """Spawn a controller agent.""" - result = multiprocessing.Process(target=controller_main, kwargs=dict( - name="tac_controller", - nb_agents=arguments.nb_agents, - nb_goods=arguments.nb_goods, - money_endowment=arguments.money_endowment, - base_good_endowment=arguments.base_good_endowment, - lower_bound_factor=arguments.lower_bound_factor, - upper_bound_factor=arguments.upper_bound_factor, - tx_fee=arguments.tx_fee, - oef_addr=arguments.oef_addr, - oef_port=arguments.oef_port, - start_time=arguments.start_time, - registration_timeout=arguments.registration_timeout, - inactivity_timeout=arguments.inactivity_timeout, - competition_timeout=arguments.competition_timeout, - whitelist_file=arguments.whitelist_file, - verbose=True, - gui=arguments.gui, - visdom_addr=arguments.visdom_addr, - visdom_port=arguments.visdom_port, - data_output_dir=arguments.data_output_dir, - experiment_id=arguments.experiment_id, - seed=arguments.seed, - version=1, - )) - result.start() - return result - - -def run_baseline_agent(**kwargs) -> None: - """Run a baseline agent.""" - # give the time to the controller to connect to the OEF - time.sleep(5.0) - baseline_main(**kwargs) - - -def spawn_baseline_agents(arguments) -> List[multiprocessing.Process]: - """Spawn baseline agents.""" - fraction_world_modeling = 0.1 - nb_baseline_agents_world_modeling = round(arguments.nb_baseline_agents * fraction_world_modeling) - - threads = [multiprocessing.Process(target=run_baseline_agent, kwargs=dict( - name=_make_id(i, i < nb_baseline_agents_world_modeling, arguments.nb_agents), - oef_addr=arguments.oef_addr, - oef_port=arguments.oef_port, - register_as=arguments.register_as, - search_for=arguments.search_for, - is_world_modeling=i < nb_baseline_agents_world_modeling, - services_interval=arguments.services_interval, - pending_transaction_timeout=arguments.pending_transaction_timeout, - gui=arguments.gui, - visdom_addr=arguments.visdom_addr, - visdom_port=arguments.visdom_port)) for i in range(arguments.nb_agents)] - - def signal_handler(sig, frame): - """Filter the SIGINT from the parent process.""" - - original_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN) - signal.signal(signal.SIGINT, signal_handler) - for t in threads: - t.start() - signal.signal(signal.SIGINT, original_sigint_handler) - return threads +from tac.platform.simulation import parse_arguments, build_simulation_parameters, run if __name__ == '__main__': arguments = parse_arguments() - random.seed(arguments.seed) - - controller_thread = None # type: Optional[multiprocessing.Process] - baseline_threads = [] # type: List[multiprocessing.Process] - - try: - - controller_thread = spawn_controller_agent(arguments) - baseline_threads = spawn_baseline_agents(arguments) - controller_thread.join() - - except KeyboardInterrupt: - logger.debug("Simulation interrupted...") - except Exception: - logger.exception("Unexpected exception.") - exit(-1) - finally: - if controller_thread is not None: - controller_thread.join(timeout=5) - controller_thread.terminate() - - for t in baseline_threads: - t.join(timeout=5) - t.terminate() + simulation_parameters = build_simulation_parameters(arguments) + run(simulation_parameters) diff --git a/tac/README.md b/tac/README.md index 98a5620c..5c236b49 100644 --- a/tac/README.md +++ b/tac/README.md @@ -1,9 +1,9 @@ # tac Framework for the Trading Agents Competition -## Available agents +## Available modules - `agents`: the agent framework and sample implementations; -- `gui`: the gui to monitor a competition; +- `gui`: the gui modules; - `helpers`: supporting modules; - `platform`: the competition framework; \ No newline at end of file diff --git a/tac/__init__.py b/tac/__init__.py index 53884850..f6c941d0 100644 --- a/tac/__init__.py +++ b/tac/__init__.py @@ -19,9 +19,11 @@ # ------------------------------------------------------------------------------ """Contains the TAC package.""" +import inspect +import os from tac.__version__ import __title__, __description__, __url__, __version__ -from tac.__version__ import __build__, __author__, __license__, __copyright__ +from tac.__version__ import __author__, __license__, __copyright__ import logging @@ -33,3 +35,6 @@ handler.setFormatter(formatter) logger.addHandler(handler) logger.propagate = False + +ROOT_DIR = os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), "..") + diff --git a/tac/__version__.py b/tac/__version__.py index 72ccacf7..d346aab8 100644 --- a/tac/__version__.py +++ b/tac/__version__.py @@ -22,9 +22,8 @@ __title__ = 'tac' __description__ = 'Trading Agent Competition agents' -__url__ = 'https://github.com/uvue-git/tac-agents.git' -__version__ = '0.1.2' -__build__ = 0x000100 +__url__ = 'https://github.com/fetchai/agents-tac.git' +__version__ = '0.1.3' __author__ = 'Fetch.AI Limited' __license__ = 'Apache 2.0' -__copyright__ = '2018 Fetch.AI Limited' +__copyright__ = '2019 Fetch.AI Limited' diff --git a/tac/agents/v1/agent.py b/tac/agents/v1/agent.py index 4e63a3e8..ffbece72 100644 --- a/tac/agents/v1/agent.py +++ b/tac/agents/v1/agent.py @@ -105,6 +105,7 @@ def run_main_loop(self) -> None: self.act() time.sleep(self._timeout) self.react() + self.update() self.stop() logger.debug("[{}]: Exiting main loop...".format(self.name)) @@ -134,3 +135,10 @@ def react(self) -> None: :return: None """ + + @abstractmethod + def update(self) -> None: + """Update the current state of the agent. + + :return None + """ diff --git a/tac/agents/v1/base/game_instance.py b/tac/agents/v1/base/game_instance.py index fb0135f4..82215dca 100644 --- a/tac/agents/v1/base/game_instance.py +++ b/tac/agents/v1/base/game_instance.py @@ -22,21 +22,21 @@ import datetime from enum import Enum -from typing import List, Optional, Set +import random +from typing import List, Optional, Set, Tuple, Dict, Union -from oef.messages import CFP_TYPES from oef.query import Query from oef.schema import Description from tac.agents.v1.mail import MailStats -from tac.agents.v1.base.dialogues import Dialogues -from tac.agents.v1.base.lock_manager import LockManager +from tac.agents.v1.base.dialogues import Dialogues, Dialogue +from tac.agents.v1.base.transaction_manager import TransactionManager from tac.agents.v1.base.strategy import Strategy from tac.agents.v1.base.stats_manager import StatsManager from tac.gui.dashboards.agent import AgentDashboard from tac.platform.game import AgentState, WorldState, GameConfiguration -from tac.helpers.misc import build_query, get_goods_quantities_description -from tac.platform.protocol import GameData, StateUpdate +from tac.helpers.misc import build_dict, build_query, get_goods_quantities_description +from tac.platform.protocol import GameData, StateUpdate, Transaction class GamePhase(Enum): @@ -116,8 +116,7 @@ def __init__(self, agent_name: str, self.goods_supplied_description = None self.goods_demanded_description = None - self.lock_manager = LockManager(agent_name, pending_transaction_timeout=pending_transaction_timeout) - self.lock_manager.start() + self.transaction_manager = TransactionManager(agent_name, pending_transaction_timeout=pending_transaction_timeout) self.stats_manager = StatsManager(mail_stats, dashboard) @@ -237,6 +236,31 @@ def is_time_to_search_services(self) -> bool: self._last_search_time = now return result + def is_profitable_transaction(self, transaction: Transaction, dialogue: Dialogue) -> Tuple[bool, str]: + """ + Check if a transaction is profitable. + + Is it a profitable transaction? + - apply all the locks for role. + - check if the transaction is consistent with the locks (enough money/holdings) + - check that we gain score. + + :param transaction: the transaction + :param dialogue: the dialogue + + :return: True if the transaction is good (as stated above), False otherwise. + """ + state_after_locks = self.state_after_locks(dialogue.is_seller) + + if not state_after_locks.check_transaction_is_consistent(transaction, self.game_configuration.tx_fee): + message = "[{}]: the proposed transaction is not consistent with the state after locks.".format(self.agent_name) + return False, message + proposal_delta_score = state_after_locks.get_score_diff_from_transaction(transaction, self.game_configuration.tx_fee) + + result = self.strategy.is_acceptable_proposal(proposal_delta_score) + message = "[{}]: is good proposal for {}? {}: tx_id={}, delta_score={}, amount={}".format(self.agent_name, dialogue.role, result, transaction.transaction_id, proposal_delta_score, transaction.amount) + return result, message + def get_service_description(self, is_supply: bool) -> Description: """ Get the description of the supplied goods (as a seller), or the demanded goods (as a buyer). @@ -267,6 +291,37 @@ def build_services_query(self, is_searching_for_sellers: bool) -> Optional[Query res = None if len(good_pbks) == 0 else build_query(good_pbks, is_searching_for_sellers) return res + def build_services_dict(self, is_supply: bool) -> Optional[Dict[str, Union[str, List]]]: + """ + Build a dictionary containing the services demanded/supplied. + + :param is_supply: Boolean indicating whether the services are demanded or supplied. + + :return: a Dict. + """ + good_pbks = self.get_goods_pbks(is_supply=is_supply) + + res = None if len(good_pbks) == 0 else build_dict(good_pbks, is_supply) + return res + + def is_matching(self, cfp_services: Dict[str, Union[bool, List]], goods_description: Description) -> bool: + """ + Check for a match between the CFP services and the goods description. + + :param cfp_services: the services associated with the cfp. + :param goods_description: a description of the goods. + + :return: Bool + """ + services = cfp_services['services'] + if cfp_services['description'] is goods_description.data_model.name: + # The call for proposal description and the goods model name cannot be the same for trading agent pairs. + return False + for good_pbk in goods_description.data_model.attributes_by_name.keys(): + if good_pbk not in services: continue + return True + return False + def get_goods_pbks(self, is_supply: bool) -> Set[str]: """ Wrap the function which determines supplied and demanded good public keys. @@ -301,18 +356,18 @@ def state_after_locks(self, is_seller: bool) -> AgentState: :return: the agent state with the locks applied to current state """ - transactions = list(self.lock_manager.locks_as_seller.values()) if is_seller \ - else list(self.lock_manager.locks_as_buyer.values()) + transactions = list(self.transaction_manager.locked_txs_as_seller.values()) if is_seller \ + else list(self.transaction_manager.locked_txs_as_buyer.values()) state_after_locks = self._agent_state.apply(transactions, self.game_configuration.tx_fee) return state_after_locks - def get_proposals(self, query: CFP_TYPES, is_seller: bool) -> List[Description]: + def generate_proposal(self, cfp_services: Dict[str, Union[bool, List]], is_seller: bool) -> Optional[List[Description]]: """ Wrap the function which generates proposals from a seller or buyer. If there are locks as seller, it applies them. - :param query: the query associated with the cfp. + :param cfp_services: the query associated with the cfp. :param is_seller: Boolean indicating the role of the agent. :return: a list of descriptions @@ -321,13 +376,14 @@ def get_proposals(self, query: CFP_TYPES, is_seller: bool) -> List[Description]: candidate_proposals = self.strategy.get_proposals(self.game_configuration.good_pbks, state_after_locks.current_holdings, state_after_locks.utility_params, self.game_configuration.tx_fee, is_seller, self._world_state) proposals = [] for proposal in candidate_proposals: - if not query.check(proposal): continue + if not self.is_matching(cfp_services, proposal): continue + if not proposal.values["price"] > 0: continue proposals.append(proposal) - if proposals == []: - proposals.append(candidate_proposals[0]) # TODO remove this - return proposals + if not proposals: + return None + else: + return random.choice(proposals) def stop(self): """Stop the services attached to the game instance.""" - self.lock_manager.stop() self.stats_manager.stop() diff --git a/tac/agents/v1/base/handlers.py b/tac/agents/v1/base/handlers.py index 4b69d615..bce232c2 100644 --- a/tac/agents/v1/base/handlers.py +++ b/tac/agents/v1/base/handlers.py @@ -110,7 +110,7 @@ def handle_controller_message(self, msg: ControllerMessage) -> None: :return: None """ - response = Response.from_pb(msg.msg, msg.destination, self.crypto) # TODO this is already created once above! + response = Response.from_pb(msg.msg, msg.destination, self.crypto) logger.debug("[{}]: Handling controller response. type={}".format(self.agent_name, type(response))) try: if msg.destination != self.game_instance.controller_pbk: diff --git a/tac/agents/v1/base/lock_manager.py b/tac/agents/v1/base/lock_manager.py deleted file mode 100644 index 500eac68..00000000 --- a/tac/agents/v1/base/lock_manager.py +++ /dev/null @@ -1,261 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains a class to manage locked agent states.""" - -import datetime -import logging -import time -from collections import defaultdict, deque -from threading import Thread -from typing import Dict, Tuple, Deque, List - -from oef.schema import Description - -from tac.agents.v1.base.dialogues import DialogueLabel, Dialogue -from tac.helpers.crypto import Crypto -from tac.agents.v1.base.helpers import generate_transaction_id -from tac.platform.protocol import Transaction - -logger = logging.getLogger(__name__) - -MESSAGE_ID = int -TRANSACTION_ID = str - - -class LockManager(object): - """Class to handle pending proposals/acceptances and locks.""" - - def __init__(self, agent_name: str, pending_transaction_timeout: int = 30, task_timeout: float = 2.0) -> None: - """ - Initialize a LockManager. - - :param agent_name: The name of the agent the manager refers to. - :param pending_transaction_timeout: seconds to wait before a transaction/message can be removed from any pool. - :param task_timeout: seconds to sleep for the task - - :return: None - """ - self.agent_name = agent_name - - self.pending_tx_proposals = defaultdict(lambda: {}) # type: Dict[DialogueLabel, Dict[MESSAGE_ID, Transaction]] - self.pending_tx_acceptances = defaultdict(lambda: {}) # type: Dict[DialogueLabel, Dict[MESSAGE_ID, Transaction]] - - self.locks = {} # type: Dict[TRANSACTION_ID, Transaction] - self.locks_as_buyer = {} # type: Dict[TRANSACTION_ID, Transaction] - self.locks_as_seller = {} # type: Dict[TRANSACTION_ID, Transaction] - - self.pending_transaction_timeout = pending_transaction_timeout - self._cleanup_locks_task = None - self._cleanup_locks_task_is_running = False - self._cleanup_locks_task_timeout = task_timeout - - self._last_update_for_transactions = deque() # type: Deque[Tuple[datetime.datetime, TRANSACTION_ID]] - - def cleanup_locks_job(self) -> None: - """ - Periodically check for transactions in one of the pending pools. - - If they have been there for too much time, remove them. - - :return: None - """ - while self._cleanup_locks_task_is_running: - time.sleep(self._cleanup_locks_task_timeout) - self._cleanup_pending_transactions() - - 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. - - :return: None - """ - queue = self._last_update_for_transactions - timeout = datetime.timedelta(0, self.pending_transaction_timeout) - - if len(queue) == 0: - return - - next_date, next_item = queue[0] - - while datetime.datetime.now() - next_date > timeout: - - # remove the element from the queue - queue.popleft() - - # extract dialogue label and message id - transaction_id = next_item - logger.debug("[{}]: Removing transaction: {}".format(self.agent_name, transaction_id)) - - # remove (safely) the associated pending proposal (if present) - self.locks.pop(transaction_id, None) - self.locks_as_buyer.pop(transaction_id, None) - self.locks_as_seller.pop(transaction_id, None) - - # check the next transaction, if present - if len(queue) == 0: - break - next_date, next_item = queue[0] - - def _register_transaction_with_time(self, transaction_id: str) -> None: - """ - Register a transaction with a creation datetime. - - :param transaction_id: the transaction id - - :return: None - """ - now = datetime.datetime.now() - self._last_update_for_transactions.append((now, transaction_id)) - - def add_pending_proposal(self, dialogue: Dialogue, proposal_id: int, transaction: Transaction) -> None: - """ - Add a proposal (in the form of a transaction) to the pending list. - - :param dialogue: the dialogue associated with the proposal - :param proposal_id: the message id of the proposal - :param transaction: the transaction - :raise AssertionError: if the pending proposal is already present. - - :return: None - """ - assert dialogue.dialogue_label not in self.pending_tx_proposals and proposal_id not in self.pending_tx_proposals[dialogue.dialogue_label] - self.pending_tx_proposals[dialogue.dialogue_label][proposal_id] = transaction - - def pop_pending_proposal(self, dialogue: Dialogue, proposal_id: int) -> Transaction: - """ - Remove a proposal (in the form of a transaction) from the pending list. - - :param dialogue: the dialogue associated with the proposal - :param proposal_id: the message id of the proposal - :raise AssertionError: if the pending proposal is not present. - - :return: the transaction - """ - assert dialogue.dialogue_label in self.pending_tx_proposals and proposal_id in self.pending_tx_proposals[dialogue.dialogue_label] - transaction = self.pending_tx_proposals[dialogue.dialogue_label].pop(proposal_id) - return transaction - - def add_pending_initial_acceptance(self, dialogue: Dialogue, proposal_id: int, transaction: Transaction) -> None: - """ - Add an acceptance (in the form of a transaction) to the pending list. - - :param dialogue: the dialogue associated with the proposal - :param proposal_id: the message id of the proposal - :param transaction: the transaction - :raise AssertionError: if the pending acceptance is already present. - - :return: None - """ - assert dialogue.dialogue_label not in self.pending_tx_acceptances and proposal_id not in self.pending_tx_acceptances[dialogue.dialogue_label] - self.pending_tx_acceptances[dialogue.dialogue_label][proposal_id] = transaction - - def pop_pending_initial_acceptance(self, dialogue: Dialogue, proposal_id: int) -> Transaction: - """ - Remove an acceptance (in the form of a transaction) from the pending list. - - :param dialogue: the dialogue associated with the proposal - :param proposal_id: the message id of the proposal - :raise AssertionError: if the pending acceptance is not present. - - :return: the transaction - """ - assert dialogue.dialogue_label in self.pending_tx_acceptances and proposal_id in self.pending_tx_acceptances[dialogue.dialogue_label] - transaction = self.pending_tx_acceptances[dialogue.dialogue_label].pop(proposal_id) - return transaction - - def add_lock(self, transaction: Transaction, as_seller: bool) -> None: - """ - Add a lock (in the form of a transaction). - - :param transaction: the transaction - :param as_seller: whether the agent is a seller or not - :raise AssertionError: if the transaction is already present. - - :return: None - """ - transaction_id = transaction.transaction_id - assert transaction_id not in self.locks - self._register_transaction_with_time(transaction_id) - self.locks[transaction_id] = transaction - if as_seller: - self.locks_as_seller[transaction_id] = transaction - else: - self.locks_as_buyer[transaction_id] = transaction - - def pop_lock(self, transaction_id: str) -> Transaction: - """ - Remove a lock (in the form of a transaction). - - :param transaction_id: the transaction id - :raise AssertionError: if the transaction with the given transaction id has not been found. - - :return: the transaction - """ - assert transaction_id in self.locks - transaction = self.locks.pop(transaction_id) - self.locks_as_buyer.pop(transaction_id, None) - self.locks_as_seller.pop(transaction_id, None) - return transaction - - def start(self) -> None: - """ - Start the lock manager. - - :return: None - """ - if not self._cleanup_locks_task_is_running: - self._cleanup_locks_task_is_running = True - self._cleanup_locks_task = Thread(target=self.cleanup_locks_job) - self._cleanup_locks_task.start() - - def stop(self) -> None: - """ - Stop the lock manager. - - :return: None - """ - if self._cleanup_locks_task_is_running: - self._cleanup_locks_task_is_running = False - self._cleanup_locks_task.join() - - def store_proposals(self, proposals: List[Description], new_msg_id: int, dialogue: Dialogue, origin: str, is_seller: bool, crypto: Crypto) -> None: - """ - Store proposals as pending transactions. - - :param proposals: the list of proposals - :param new_msg_id: the new message id - :param dialogue: the dialogue - :param origin: the public key of the message sender. - :param is_seller: Boolean indicating the role of the agent - :param crypto: the crypto object - - :return: None - """ - for proposal in proposals: - proposal_id = new_msg_id # TODO fix if more than one proposal! - transaction_id = generate_transaction_id(crypto.public_key, origin, dialogue.dialogue_label, is_seller) # TODO fix if more than one proposal! - transaction = Transaction.from_proposal(proposal=proposal, - transaction_id=transaction_id, - is_sender_buyer=not is_seller, - counterparty=origin, - sender=crypto.public_key, - crypto=crypto) - self.add_pending_proposal(dialogue, proposal_id, transaction) diff --git a/tac/agents/v1/base/negotiation_behaviours.py b/tac/agents/v1/base/negotiation_behaviours.py index 2ed97aa1..16a39b90 100644 --- a/tac/agents/v1/base/negotiation_behaviours.py +++ b/tac/agents/v1/base/negotiation_behaviours.py @@ -20,13 +20,13 @@ """This module contains a class which implements the FIPA protocol for the TAC.""" +import json import logging import pprint -import random from typing import Union, List from oef.messages import CFP, Decline, Propose, Accept -from oef.utils import Context +from oef.uri import Context from tac.agents.v1.base.game_instance import GameInstance from tac.agents.v1.base.dialogues import Dialogue @@ -84,8 +84,18 @@ def on_cfp(self, cfp: CFP, dialogue: Dialogue) -> Union[Propose, Decline]: """ goods_description = self.game_instance.get_service_description(is_supply=dialogue.is_seller) new_msg_id = cfp.msg_id + 1 - if not cfp.query.check(goods_description): + decline = False + cfp_services = json.loads(cfp.query.decode('utf-8')) + if not self.game_instance.is_matching(cfp_services, goods_description): + decline = True logger.debug("[{}]: Current holdings do not satisfy CFP query.".format(self.agent_name)) + else: + proposal = self.game_instance.generate_proposal(cfp_services, dialogue.is_seller) + if proposal is None: + decline = True + logger.debug("[{}]: Current strategy does not generate proposal that satisfies CFP query.".format(self.agent_name)) + + if decline: logger.debug("[{}]: sending to {} a Decline{}".format(self.agent_name, cfp.destination, pprint.pformat({ "msg_id": new_msg_id, @@ -96,17 +106,23 @@ def on_cfp(self, cfp: CFP, dialogue: Dialogue) -> Union[Propose, Decline]: response = Decline(new_msg_id, cfp.dialogue_id, cfp.destination, cfp.msg_id, Context()) self.game_instance.stats_manager.add_dialogue_endstate(EndState.DECLINED_CFP, dialogue.is_self_initiated) else: - proposals = [random.choice(self.game_instance.get_proposals(cfp.query, dialogue.is_seller))] - self.game_instance.lock_manager.store_proposals(proposals, new_msg_id, dialogue, cfp.destination, dialogue.is_seller, self.crypto) + transaction_id = generate_transaction_id(self.crypto.public_key, cfp.destination, dialogue.dialogue_label, dialogue.is_seller) + transaction = Transaction.from_proposal(proposal=proposal, + transaction_id=transaction_id, + is_sender_buyer=not dialogue.is_seller, + counterparty=cfp.destination, + sender=self.crypto.public_key, + crypto=self.crypto) + self.game_instance.transaction_manager.add_pending_proposal(dialogue.dialogue_label, new_msg_id, transaction) logger.debug("[{}]: sending to {} a Propose{}".format(self.agent_name, cfp.destination, pprint.pformat({ "msg_id": new_msg_id, "dialogue_id": cfp.dialogue_id, "origin": cfp.destination, "target": cfp.msg_id, - "propose": proposals[0].values # TODO fix if more than one proposal! + "propose": proposal.values }))) - response = Propose(new_msg_id, cfp.dialogue_id, cfp.destination, cfp.msg_id, proposals, Context()) + response = Propose(new_msg_id, cfp.dialogue_id, cfp.destination, cfp.msg_id, [proposal], Context()) return response def on_propose(self, propose: Propose, dialogue: Dialogue) -> Union[Accept, Decline]: @@ -121,17 +137,19 @@ def on_propose(self, propose: Propose, dialogue: Dialogue) -> Union[Accept, Decl logger.debug("[{}]: on propose as {}.".format(self.agent_name, dialogue.role)) proposal = propose.proposals[0] transaction_id = generate_transaction_id(self.crypto.public_key, propose.destination, dialogue.dialogue_label, dialogue.is_seller) - transaction = Transaction.from_proposal(proposal, - transaction_id, + transaction = Transaction.from_proposal(proposal=proposal, + transaction_id=transaction_id, is_sender_buyer=not dialogue.is_seller, counterparty=propose.destination, sender=self.crypto.public_key, crypto=self.crypto) new_msg_id = propose.msg_id + 1 - if self._is_profitable_transaction(transaction, dialogue): + is_profitable_transaction, message = self.game_instance.is_profitable_transaction(transaction, dialogue) + logger.debug(message) + if is_profitable_transaction: logger.debug("[{}]: Accepting propose (as {}).".format(self.agent_name, dialogue.role)) - self.game_instance.lock_manager.add_lock(transaction, as_seller=dialogue.is_seller) - self.game_instance.lock_manager.add_pending_initial_acceptance(dialogue, new_msg_id, transaction) + self.game_instance.transaction_manager.add_locked_tx(transaction, as_seller=dialogue.is_seller) + self.game_instance.transaction_manager.add_pending_initial_acceptance(dialogue.dialogue_label, new_msg_id, transaction) result = Accept(new_msg_id, propose.dialogue_id, propose.destination, propose.msg_id, Context()) else: logger.debug("[{}]: Declining propose (as {})".format(self.agent_name, dialogue.role)) @@ -139,34 +157,6 @@ def on_propose(self, propose: Propose, dialogue: Dialogue) -> Union[Accept, Decl self.game_instance.stats_manager.add_dialogue_endstate(EndState.DECLINED_PROPOSE, dialogue.is_self_initiated) return result - def _is_profitable_transaction(self, transaction: Transaction, dialogue: Dialogue) -> bool: - """ - Check if a transaction is profitable. - - Is it a profitable transaction? - - apply all the locks for role. - - check if the transaction is consistent with the locks (enough money/holdings) - - check that we gain score. - - :param transaction: the transaction - :param dialogue: the dialogue - - :return: True if the transaction is good (as stated above), False otherwise. - """ - state_after_locks = self.game_instance.state_after_locks(dialogue.is_seller) - - if not state_after_locks.check_transaction_is_consistent(transaction, self.game_instance.game_configuration.tx_fee): - logger.debug("[{}]: the proposed transaction is not consistent with the state after locks.".format(self.agent_name)) - return False - proposal_delta_score = state_after_locks.get_score_diff_from_transaction(transaction, self.game_instance.game_configuration.tx_fee) - - result = self.game_instance.strategy.is_acceptable_proposal(proposal_delta_score) - logger.debug("[{}]: is good proposal for {}? {}: tx_id={}, " - "delta_score={}, amount={}" - .format(self.agent_name, dialogue.role, result, transaction.transaction_id, - proposal_delta_score, transaction.amount)) - return result - def on_decline(self, decline: Decline, dialogue: Dialogue) -> None: """ Handle a Decline. @@ -183,13 +173,13 @@ def on_decline(self, decline: Decline, dialogue: Dialogue) -> None: self.game_instance.stats_manager.add_dialogue_endstate(EndState.DECLINED_CFP, dialogue.is_self_initiated) elif decline.target == 2: self.game_instance.stats_manager.add_dialogue_endstate(EndState.DECLINED_PROPOSE, dialogue.is_self_initiated) - transaction = self.game_instance.lock_manager.pop_pending_proposal(dialogue, decline.target) + transaction = self.game_instance.transaction_manager.pop_pending_proposal(dialogue.dialogue_label, decline.target) if self.game_instance.strategy.is_world_modeling: self.game_instance.world_state.update_on_declined_propose(transaction) elif decline.target == 3: self.game_instance.stats_manager.add_dialogue_endstate(EndState.DECLINED_ACCEPT, dialogue.is_self_initiated) - transaction = self.game_instance.lock_manager.pop_pending_initial_acceptance(dialogue, decline.target) - self.game_instance.lock_manager.pop_lock(transaction.transaction_id) + transaction = self.game_instance.transaction_manager.pop_pending_initial_acceptance(dialogue.dialogue_label, decline.target) + self.game_instance.transaction_manager.pop_locked_tx(transaction.transaction_id) return None @@ -205,8 +195,8 @@ def on_accept(self, accept: Accept, dialogue: Dialogue) -> Union[List[Decline], logger.debug("[{}]: on_accept: msg_id={}, dialogue_id={}, origin={}, target={}" .format(self.agent_name, accept.msg_id, accept.dialogue_id, accept.destination, accept.target)) - if dialogue.dialogue_label in self.game_instance.lock_manager.pending_tx_acceptances \ - and accept.target in self.game_instance.lock_manager.pending_tx_acceptances[dialogue.dialogue_label]: + if dialogue.dialogue_label in self.game_instance.transaction_manager.pending_initial_acceptances \ + and accept.target in self.game_instance.transaction_manager.pending_initial_acceptances[dialogue.dialogue_label]: results = self._on_match_accept(accept, dialogue) else: results = self._on_initial_accept(accept, dialogue) @@ -221,14 +211,16 @@ def _on_initial_accept(self, accept: Accept, dialogue: Dialogue) -> Union[List[D :return: a Deline or an Accept and a Transaction (in OutContainer """ - transaction = self.game_instance.lock_manager.pop_pending_proposal(dialogue, accept.target) + transaction = self.game_instance.transaction_manager.pop_pending_proposal(dialogue.dialogue_label, accept.target) new_msg_id = accept.msg_id + 1 results = [] - if self._is_profitable_transaction(transaction, dialogue): + is_profitable_transaction, message = self.game_instance.is_profitable_transaction(transaction, dialogue) + logger.debug(message) + if is_profitable_transaction: if self.game_instance.strategy.is_world_modeling: self.game_instance.world_state.update_on_initial_accept(transaction) logger.debug("[{}]: Locking the current state (as {}).".format(self.agent_name, dialogue.role)) - self.game_instance.lock_manager.add_lock(transaction, as_seller=dialogue.is_seller) + self.game_instance.transaction_manager.add_locked_tx(transaction, as_seller=dialogue.is_seller) results.append(OutContainer(message=transaction.serialize(), message_id=STARTING_MESSAGE_ID, dialogue_id=accept.dialogue_id, destination=self.game_instance.controller_pbk)) results.append(Accept(new_msg_id, accept.dialogue_id, accept.destination, accept.msg_id, Context())) else: @@ -248,6 +240,6 @@ def _on_match_accept(self, accept: Accept, dialogue: Dialogue) -> List[OutContai """ logger.debug("[{}]: on match accept".format(self.agent_name)) results = [] - transaction = self.game_instance.lock_manager.pop_pending_initial_acceptance(dialogue, accept.target) + transaction = self.game_instance.transaction_manager.pop_pending_initial_acceptance(dialogue.dialogue_label, accept.target) results.append(OutContainer(message=transaction.serialize(), message_id=STARTING_MESSAGE_ID, dialogue_id=accept.dialogue_id, destination=self.game_instance.controller_pbk)) return results diff --git a/tac/agents/v1/base/participant_agent.py b/tac/agents/v1/base/participant_agent.py index bcf97771..943c4f56 100644 --- a/tac/agents/v1/base/participant_agent.py +++ b/tac/agents/v1/base/participant_agent.py @@ -126,6 +126,14 @@ def react(self) -> None: self.out_box.send_nowait() + def update(self) -> None: + """ + Update the state of the agent. + + :return: None + """ + self.game_instance.transaction_manager.cleanup_pending_transactions() + def stop(self) -> None: """ Stop the agent. diff --git a/tac/agents/v1/base/reactions.py b/tac/agents/v1/base/reactions.py index 9f5965a5..9802bd39 100644 --- a/tac/agents/v1/base/reactions.py +++ b/tac/agents/v1/base/reactions.py @@ -26,12 +26,13 @@ - DialogueReactions: The DialogueReactions class defines the reactions of an agent in the context of a Dialogue. """ +import json import logging from typing import List, Union from oef.messages import CFP, Propose, Accept, Decline, Message as ByteMessage, SearchResult, OEFErrorMessage, \ DialogueErrorMessage -from oef.utils import Context +from oef.uri import Context from tac.agents.v1.agent import Liveness from tac.agents.v1.base.dialogues import Dialogue @@ -43,7 +44,7 @@ from tac.agents.v1.base.stats_manager import EndState from tac.agents.v1.mail import OutBox, OutContainer from tac.helpers.crypto import Crypto -from tac.helpers.misc import TAC_SUPPLY_DATAMODEL_NAME +from tac.helpers.misc import TAC_DEMAND_DATAMODEL_NAME from tac.platform.protocol import Error, ErrorCode, GameData, TransactionConfirmation, StateUpdate, Register, \ GetStateUpdate @@ -113,12 +114,12 @@ def on_transaction_confirmed(self, tx_confirmation: TransactionConfirmation) -> :return: None """ logger.debug("[{}]: Received transaction confirmation from the controller: transaction_id={}".format(self.agent_name, tx_confirmation.transaction_id)) - if tx_confirmation.transaction_id not in self.game_instance.lock_manager.locks: + if tx_confirmation.transaction_id not in self.game_instance.transaction_manager.locked_txs: logger.debug("[{}]: transaction not found - ask the controller an update of the state.".format(self.agent_name)) self._request_state_update() return - transaction = self.game_instance.lock_manager.pop_lock(tx_confirmation.transaction_id) + transaction = self.game_instance.transaction_manager.pop_locked_tx(tx_confirmation.transaction_id) self.game_instance.agent_state.update(transaction, self.game_instance.game_configuration.tx_fee) dialogue_label = dialogue_label_from_transaction_id(self.crypto.public_key, tx_confirmation.transaction_id) self.game_instance.stats_manager.add_dialogue_endstate(EndState.SUCCESSFUL, self.crypto.public_key == dialogue_label.dialogue_starter_pbk) @@ -167,8 +168,8 @@ def on_tac_error(self, error: Error) -> None: # if error in checking transaction, remove it from the pending transactions. start_idx_of_tx_id = len("Error in checking transaction: ") transaction_id = error.error_msg[start_idx_of_tx_id:] - if transaction_id in self.game_instance.lock_manager.locks: - self.game_instance.lock_manager.pop_lock(transaction_id) + if transaction_id in self.game_instance.transaction_manager.locked_txs: + self.game_instance.transaction_manager.pop_locked_tx(transaction_id) else: logger.warning("[{}]: Received error on unknown transaction id: {}".format(self.agent_name, transaction_id)) pass @@ -295,16 +296,16 @@ def _on_services_search_result(self, agent_pbks: List[str], is_searching_for_sel searched_for = 'sellers' if is_searching_for_sellers else 'buyers' logger.debug("[{}]: Found potential {}: {}".format(self.agent_name, searched_for, agent_pbks)) - query = self.game_instance.build_services_query(is_searching_for_sellers) - if query is None: + services = self.game_instance.build_services_dict(is_supply=not is_searching_for_sellers) + if services is None: response = 'demanding' if is_searching_for_sellers else 'supplying' logger.debug("[{}]: No longer {} any goods...".format(self.agent_name, response)) return for agent_pbk in agent_pbks: dialogue = self.game_instance.dialogues.create_self_initiated(agent_pbk, self.crypto.public_key, not is_searching_for_sellers) - cfp = CFP(STARTING_MESSAGE_ID, dialogue.dialogue_label.dialogue_id, agent_pbk, STARTING_MESSAGE_TARGET, query, Context()) - logger.debug("[{}]: send_cfp_as_{}: msg_id={}, dialogue_id={}, destination={}, target={}, query={}" - .format(self.agent_name, dialogue.role, cfp.msg_id, cfp.dialogue_id, cfp.destination, cfp.target, query)) + cfp = CFP(STARTING_MESSAGE_ID, dialogue.dialogue_label.dialogue_id, agent_pbk, STARTING_MESSAGE_TARGET, json.dumps(services).encode('utf-8'), Context()) + logger.debug("[{}]: send_cfp_as_{}: msg_id={}, dialogue_id={}, destination={}, target={}, services={}" + .format(self.agent_name, dialogue.role, cfp.msg_id, cfp.dialogue_id, cfp.destination, cfp.target, services)) dialogue.outgoing_extend([cfp]) self.out_box.out_queue.put(cfp) @@ -366,7 +367,8 @@ def on_new_dialogue(self, msg: AgentMessage) -> None: :return: None """ - is_seller = msg.query.model.name == TAC_SUPPLY_DATAMODEL_NAME + services = json.loads(msg.query.decode('utf-8')) + is_seller = services['description'] == TAC_DEMAND_DATAMODEL_NAME dialogue = self.dialogues.create_opponent_initiated(msg.destination, msg.dialogue_id, is_seller) logger.debug("[{}]: saving dialogue (as {}): dialogue_id={}".format(self.agent_name, dialogue.role, dialogue.dialogue_label.dialogue_id)) results = self._handle(msg, dialogue) diff --git a/tac/agents/v1/base/transaction_manager.py b/tac/agents/v1/base/transaction_manager.py new file mode 100644 index 00000000..31918a58 --- /dev/null +++ b/tac/agents/v1/base/transaction_manager.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains a class to manage transactions the agent has committed to at varying degrees.""" + +import datetime +import logging +from collections import defaultdict, deque +from typing import Dict, Tuple, Deque + +from tac.agents.v1.base.dialogues import DialogueLabel +from tac.platform.protocol import Transaction + +logger = logging.getLogger(__name__) + +MESSAGE_ID = int +TRANSACTION_ID = str + + +class TransactionManager(object): + """Class to handle pending transaction proposals/acceptances and locked transactions.""" + + def __init__(self, agent_name: str, pending_transaction_timeout: int = 30) -> None: + """ + Initialize a TransactionManager. + + :param agent_name: The name of the agent the manager refers to. + :param pending_transaction_timeout: seconds to wait before a transaction/message can be removed from any pool. + + :return: None + """ + self.agent_name = agent_name + + self.pending_proposals = defaultdict(lambda: {}) # type: Dict[DialogueLabel, Dict[MESSAGE_ID, Transaction]] + self.pending_initial_acceptances = defaultdict(lambda: {}) # type: Dict[DialogueLabel, Dict[MESSAGE_ID, Transaction]] + + self.locked_txs = {} # type: Dict[TRANSACTION_ID, Transaction] + self.locked_txs_as_buyer = {} # type: Dict[TRANSACTION_ID, Transaction] + self.locked_txs_as_seller = {} # type: Dict[TRANSACTION_ID, Transaction] + + self.pending_transaction_timeout = pending_transaction_timeout + + self._last_update_for_transactions = deque() # type: Deque[Tuple[datetime.datetime, TRANSACTION_ID]] + + 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. + + :return: None + """ + queue = self._last_update_for_transactions + timeout = datetime.timedelta(0, self.pending_transaction_timeout) + + if len(queue) == 0: + return + + next_date, next_item = queue[0] + + while datetime.datetime.now() - next_date > timeout: + + # remove the element from the queue + queue.popleft() + + # extract dialogue label and message id + transaction_id = next_item + logger.debug("[{}]: Removing transaction: {}".format(self.agent_name, transaction_id)) + + # remove (safely) the associated pending proposal (if present) + self.locked_txs.pop(transaction_id, None) + self.locked_txs_as_buyer.pop(transaction_id, None) + self.locked_txs_as_seller.pop(transaction_id, None) + + # check the next transaction, if present + if len(queue) == 0: + break + next_date, next_item = queue[0] + + def _register_transaction_with_time(self, transaction_id: str) -> None: + """ + Register a transaction with a creation datetime. + + :param transaction_id: the transaction id + + :return: None + """ + now = datetime.datetime.now() + self._last_update_for_transactions.append((now, transaction_id)) + + def add_pending_proposal(self, dialogue_label: DialogueLabel, proposal_id: int, transaction: Transaction) -> None: + """ + Add a proposal (in the form of a transaction) to the pending list. + + :param dialogue_label: the dialogue label associated with the proposal + :param proposal_id: the message id of the proposal + :param transaction: the transaction + :raise AssertionError: if the pending proposal is already present. + + :return: None + """ + assert dialogue_label not in self.pending_proposals and proposal_id not in self.pending_proposals[dialogue_label] + self.pending_proposals[dialogue_label][proposal_id] = transaction + + def pop_pending_proposal(self, dialogue_label: DialogueLabel, proposal_id: int) -> Transaction: + """ + Remove a proposal (in the form of a transaction) from the pending list. + + :param dialogue_label: the dialogue label associated with the proposal + :param proposal_id: the message id of the proposal + :raise AssertionError: if the pending proposal is not present. + + :return: the transaction + """ + assert dialogue_label in self.pending_proposals and proposal_id in self.pending_proposals[dialogue_label] + transaction = self.pending_proposals[dialogue_label].pop(proposal_id) + return transaction + + def add_pending_initial_acceptance(self, dialogue_label: DialogueLabel, proposal_id: int, transaction: Transaction) -> None: + """ + Add an acceptance (in the form of a transaction) to the pending list. + + :param dialogue_label: the dialogue label associated with the proposal + :param proposal_id: the message id of the proposal + :param transaction: the transaction + :raise AssertionError: if the pending acceptance is already present. + + :return: None + """ + assert dialogue_label not in self.pending_initial_acceptances and proposal_id not in self.pending_initial_acceptances[dialogue_label] + self.pending_initial_acceptances[dialogue_label][proposal_id] = transaction + + def pop_pending_initial_acceptance(self, dialogue_label: DialogueLabel, proposal_id: int) -> Transaction: + """ + Remove an acceptance (in the form of a transaction) from the pending list. + + :param dialogue_label: the dialogue label associated with the proposal + :param proposal_id: the message id of the proposal + :raise AssertionError: if the pending acceptance is not present. + + :return: the transaction + """ + assert dialogue_label in self.pending_initial_acceptances and proposal_id in self.pending_initial_acceptances[dialogue_label] + transaction = self.pending_initial_acceptances[dialogue_label].pop(proposal_id) + return transaction + + def add_locked_tx(self, transaction: Transaction, as_seller: bool) -> None: + """ + Add a lock (in the form of a transaction). + + :param transaction: the transaction + :param as_seller: whether the agent is a seller or not + :raise AssertionError: if the transaction is already present. + + :return: None + """ + transaction_id = transaction.transaction_id + assert transaction_id not in self.locked_txs + self._register_transaction_with_time(transaction_id) + self.locked_txs[transaction_id] = transaction + if as_seller: + self.locked_txs_as_seller[transaction_id] = transaction + else: + self.locked_txs_as_buyer[transaction_id] = transaction + + def pop_locked_tx(self, transaction_id: str) -> Transaction: + """ + Remove a lock (in the form of a transaction). + + :param transaction_id: the transaction id + :raise AssertionError: if the transaction with the given transaction id has not been found. + + :return: the transaction + """ + assert transaction_id in self.locked_txs + transaction = self.locked_txs.pop(transaction_id) + self.locked_txs_as_buyer.pop(transaction_id, None) + self.locked_txs_as_seller.pop(transaction_id, None) + return transaction diff --git a/tac/agents/v1/examples/baseline.py b/tac/agents/v1/examples/baseline.py index 5edbee85..37408215 100644 --- a/tac/agents/v1/examples/baseline.py +++ b/tac/agents/v1/examples/baseline.py @@ -73,7 +73,7 @@ def _parse_arguments(): parser.add_argument("--pending-transaction-timeout", type=int, default=30, help="The timeout in seconds to wait for pending transaction/negotiations.") parser.add_argument("--private-key-pem", type=str, default=None, help="Path to a file containing a private key in PEM format.") parser.add_argument("--rejoin", action="store_true", default=False, help="Whether the agent is joining a running TAC.") - parser.add_argument("--gui", action="store_true", help="Show the GUI.") + parser.add_argument("--dashboard", action="store_true", help="Show the agent dashboard.") parser.add_argument("--visdom_addr", type=str, default="localhost", help="Address of the Visdom server.") parser.add_argument("--visdom_port", type=int, default=8097, help="Port of the Visdom server.") return parser.parse_args() @@ -92,7 +92,7 @@ def main( pending_transaction_timeout: int = 30, private_key_pem: Optional[str] = None, rejoin: bool = False, - gui: bool = False, + dashboard: bool = False, visdom_addr: str = "127.0.0.1", visdom_port: int = 8097, ): @@ -102,15 +102,15 @@ def main( Main entrypoint for starting a baseline agent. Please run the module with hte '--help flag' to get more details about the parameters. """ - if gui: - dashboard = AgentDashboard(agent_name=name, env_name=name, visdom_addr=visdom_addr, visdom_port=visdom_port) + if dashboard: + agent_dashboard = AgentDashboard(agent_name=name, env_name=name, visdom_addr=visdom_addr, visdom_port=visdom_port) else: - dashboard = None + agent_dashboard = None strategy = BaselineStrategy(register_as=RegisterAs(register_as), search_for=SearchFor(search_for), is_world_modeling=is_world_modeling) agent = BaselineAgent(name=name, oef_addr=oef_addr, oef_port=oef_port, strategy=strategy, agent_timeout=agent_timeout, max_reactions=max_reactions, services_interval=services_interval, - pending_transaction_timeout=pending_transaction_timeout, dashboard=dashboard, + pending_transaction_timeout=pending_transaction_timeout, dashboard=agent_dashboard, private_key_pem=private_key_pem) try: diff --git a/tac/agents/v1/examples/strategy.py b/tac/agents/v1/examples/strategy.py index f7245821..a843b4ec 100644 --- a/tac/agents/v1/examples/strategy.py +++ b/tac/agents/v1/examples/strategy.py @@ -117,5 +117,6 @@ def get_proposals(self, good_pbks: List[str], current_holdings: List[int], utili desc.values["price"] = round(marginal_utility_from_delta_holdings, 2) + share_of_tx_fee + rounding_adjustment else: desc.values["price"] = round(marginal_utility_from_delta_holdings, 2) - share_of_tx_fee - rounding_adjustment + if not desc.values["price"] > 0: continue proposals.append(desc) return proposals diff --git a/tac/agents/v1/mail.py b/tac/agents/v1/mail.py index 30c697c2..a3e08a93 100644 --- a/tac/agents/v1/mail.py +++ b/tac/agents/v1/mail.py @@ -30,6 +30,7 @@ import asyncio import datetime import logging +import time from queue import Queue, Empty from threading import Thread from typing import List, Optional, Any, Union, Dict @@ -39,7 +40,7 @@ SearchResult, OEFErrorOperation, OEFErrorMessage, DialogueErrorMessage from oef.query import Query from oef.schema import Description -from oef.utils import Context +from oef.uri import Context logger = logging.getLogger(__name__) @@ -59,10 +60,16 @@ def __init__(self) -> None: :return: None """ + self._search_count = 0 self._search_start_time = {} # type: Dict[int, datetime.datetime] self._search_timedelta = {} # type: Dict[int, float] self._search_result_counts = {} # type: Dict[int, int] + @property + def search_count(self) -> int: + """Get the search count.""" + return self._search_count + def search_start(self, search_id: int) -> None: """ Add a search id and start time. @@ -72,6 +79,7 @@ def search_start(self, search_id: int) -> None: :return: None """ assert search_id not in self._search_start_time + self._search_count += 1 self._search_start_time[search_id] = datetime.datetime.now() def search_end(self, search_id: int, nb_search_results: int) -> None: @@ -166,6 +174,24 @@ def is_running(self) -> bool: """Check whether the mailbox is running.""" return self._mail_box_thread is None + def connect(self) -> bool: + """ + Connect to the OEF Node. If it fails, then sleep for 3 seconds and try to reconnect again. + + :return: True if the connection has been established successfully, False otherwise. + """ + success = False + + while not success: + try: + success = super().connect() + except ConnectionError: + logger.error("Problems when connecting to the OEF. Retrying in 3 seconds...") + success = False + time.sleep(3.0) + + return success + def start(self) -> None: """ Start the mailbox. diff --git a/tac/gui/README.md b/tac/gui/README.md index 56a033f2..ab789d73 100644 --- a/tac/gui/README.md +++ b/tac/gui/README.md @@ -1,6 +1,6 @@ ## GUIs -This package contains GUI tools to interact with the TAC project (e.g. data visualization). +This package contains GUI tools to interact with the TAC project (e.g. data visualization & agent launcher). ## Recommended Visualization: @@ -14,6 +14,13 @@ Set explicit experiment id `experiment_id` and then run Here `#{data_output_dir}/#{experiment_id}` is the path to the folder containing the `game.json` file. + +### To visualize the leaderboard after a full TAC + +Assuming the output of `sandbox/run_iterated_games.py` is in `sandbox/data`, do: + + python tac/gui/dashboards/leaderboard.py --datadir sandbox/data + ## Alternative Visualization This displays static information. diff --git a/tac/gui/__init__.py b/tac/gui/__init__.py index d7c3c2f3..de96012a 100644 --- a/tac/gui/__init__.py +++ b/tac/gui/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- + # ------------------------------------------------------------------------------ # # Copyright 2018-2019 Fetch.AI Limited diff --git a/tac/gui/dashboards/__init__.py b/tac/gui/dashboards/__init__.py index 8b41b63a..15392bd5 100644 --- a/tac/gui/dashboards/__init__.py +++ b/tac/gui/dashboards/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- + # ------------------------------------------------------------------------------ # # Copyright 2018-2019 Fetch.AI Limited diff --git a/tac/gui/dashboards/agent.py b/tac/gui/dashboards/agent.py index b4389017..45028f58 100644 --- a/tac/gui/dashboards/agent.py +++ b/tac/gui/dashboards/agent.py @@ -1,5 +1,23 @@ # -*- 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. +# +# ------------------------------------------------------------------------------ + """Module containing the agent dashboard and related classes.""" import inspect @@ -129,6 +147,16 @@ def _update_balance(self, agent_state: AgentState, append: bool = True) -> None: xlabel="Transactions", ylabel="Score")) + def _update_search_count(self, stats_manager: StatsManager, append: bool = True) -> None: + + window_name = "{}_search_count".format(self.env_name) + self.viz.line(X=[self._update_nb_stats_manager], Y=[stats_manager.mail_stats.search_count], update="append" if append else "replace", + env=self.env_name, win=window_name, + opts=dict( + title="{}'s Search Count".format(repr(self.agent_name)), + xlabel="Ticks", + ylabel="Search Count")) + def _update_avg_search_time(self, stats_manager: StatsManager, append: bool = True) -> None: window_name = "{}_avg_search_time".format(self.env_name) @@ -187,6 +215,7 @@ def update_from_stats_manager(self, stats_manager: StatsManager, append: bool = raise Exception("Dashboard not running, update not allowed.") self._update_nb_stats_manager += 1 + self._update_search_count(stats_manager, append=append) self._update_avg_search_time(stats_manager, append=append) self._update_avg_search_result_counts(stats_manager, append=append) self._update_negotiation_metrics_self(stats_manager, append=append) diff --git a/tac/gui/dashboards/base.py b/tac/gui/dashboards/base.py index 8995a981..5a102950 100644 --- a/tac/gui/dashboards/base.py +++ b/tac/gui/dashboards/base.py @@ -1,5 +1,23 @@ # -*- 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. +# +# ------------------------------------------------------------------------------ + """Module wrapping the visdom dashboard initialization.""" import inspect diff --git a/tac/gui/dashboards/controller.py b/tac/gui/dashboards/controller.py index 87ed0656..5b7e331c 100644 --- a/tac/gui/dashboards/controller.py +++ b/tac/gui/dashboards/controller.py @@ -1,5 +1,24 @@ # -*- 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. +# +# ------------------------------------------------------------------------------ + + """Module containing the controller dashboard and related classes.""" import argparse diff --git a/tac/gui/dashboards/leaderboard.py b/tac/gui/dashboards/leaderboard.py index c43de79a..4afd21c4 100644 --- a/tac/gui/dashboards/leaderboard.py +++ b/tac/gui/dashboards/leaderboard.py @@ -1,5 +1,23 @@ # -*- 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. +# +# ------------------------------------------------------------------------------ + """Module containing the controller dashboard and related classes.""" import argparse @@ -9,6 +27,7 @@ from typing import Optional, Dict, List from tac.gui.dashboards.base import start_visdom_server, Dashboard +from tac.helpers.crypto import Crypto from tac.platform.game import Game from tac.platform.stats import GameStats @@ -60,7 +79,7 @@ def _load(self) -> List[GameStats]: for game_dir in game_dirs: game_data_json_filepath = os.path.join(self.competition_directory, game_dir, "game.json") game_data = json.load(open(game_data_json_filepath)) - game = Game.from_dict(game_data) + game = Game.from_dict(game_data, Crypto()) game_stats = GameStats(game) result.append(game_stats) diff --git a/tac/gui/launcher/__init__.py b/tac/gui/launcher/__init__.py new file mode 100644 index 00000000..5b95120c --- /dev/null +++ b/tac/gui/launcher/__init__.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +""" +Implement a Flask-based server for controlling the simulation. + +In particular, it provides REST methods to start/stop a sandbox and an agent, alongside a GUI to let the + user to easily change the parameters. +""" + +import docker +import logging +import os +import re +from queue import Empty +from threading import Thread + +from flask import Flask + +from tac.gui.launcher import home, api +from tac.gui.launcher.api.resources.sandboxes import sandbox_queue, SandboxRunner + +logger = logging.getLogger(__name__) + + +class CustomFlask(Flask): + """Wrapper of the Flask app.""" + + def __init__(self, *args, **kwargs): + """Initialize our wrapper.""" + super().__init__(*args, **kwargs) + + self.running = False + self.sandbox_runner_thread = Thread(target=self.run_sandbox_queue) + + def run_sandbox_queue(self): + """Consume elements from the sandbox queue.""" + while self.running: + logger.debug("Waiting for sandbox to execute...") + try: + sandbox_runner = sandbox_queue.get(timeout=5.0) # type: SandboxRunner + logger.debug("Launching the sandbox with id: {}".format(sandbox_runner.id)) + sandbox_runner() + logger.debug("Waiting until it completes.") + sandbox_runner.wait() + logger.debug("Sandbox with ID={} has been completed.".format(sandbox_runner.id)) + except Empty: + pass + logger.debug("Exiting from the job loop...") + + def setup(self): + """Set up resources before running the main app.""" + logger.debug("Setup method called.") + kill_any_running_oef() + self.running = True + self.sandbox_runner_thread.start() + + def run(self, *args, **kwargs): + """Wrap the run method to hide setup and teardown operations to the user.""" + try: + self.setup() + super().run(*args, **kwargs) + finally: + self.teardown() + + def teardown(self): + """Teardown the allocated resources.""" + logger.debug("Teardown method called.") + self.running = False + self.sandbox_runner_thread.join() + + +def kill_any_running_oef(): + """Kill any running OEF instance.""" + client = docker.from_env() + for container in client.containers.list(): + if any(re.match("fetchai/oef-search", tag) for tag in container.image.tags): + logger.debug("Stopping existing OEF Node...") + container.stop() + + +def create_app(test_config=None): + """Create and configure an instance of the Flask application.""" + app = CustomFlask(__name__, instance_relative_config=True) + + if test_config is None: + # load the instance config, if it exists, when not testing + app.config.from_pyfile("config.py", silent=True) + else: + # load the test config if passed in + app.config.update(test_config) + + # ensure the instance folder exists + try: + os.makedirs(app.instance_path) + except OSError: + pass + + # register api endpoints + api.create_api(app) + + # register home pages + app.register_blueprint(home.bp) + + return app diff --git a/tac/gui/launcher/api/__init__.py b/tac/gui/launcher/api/__init__.py new file mode 100644 index 00000000..37850ab3 --- /dev/null +++ b/tac/gui/launcher/api/__init__.py @@ -0,0 +1,34 @@ +# -*- 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. +# +# ------------------------------------------------------------------------------ + +"""Define the REST APIs for the launcher app.""" +from flask_restful import Api + +from .resources.sandboxes import SandboxList, Sandbox +from .resources.agents import Agent + + +def create_api(app): + """Wrap the Flask app with the Flask-RESTful Api object.""" + api = Api(app, prefix='/api') + + api.add_resource(SandboxList, "/sandboxes") + api.add_resource(Sandbox, "/sandboxes/") + api.add_resource(Agent, "/agent") diff --git a/tac/gui/launcher/api/resources/__init__.py b/tac/gui/launcher/api/resources/__init__.py new file mode 100644 index 00000000..051134c6 --- /dev/null +++ b/tac/gui/launcher/api/resources/__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. +# +# ------------------------------------------------------------------------------ + +"""Define all the REST resources for the launcher APIs.""" diff --git a/tac/gui/launcher/api/resources/agents.py b/tac/gui/launcher/api/resources/agents.py new file mode 100644 index 00000000..77ec5293 --- /dev/null +++ b/tac/gui/launcher/api/resources/agents.py @@ -0,0 +1,175 @@ +# -*- 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. +# +# ------------------------------------------------------------------------------ + +"""Implement the agent resource and other utility classes.""" + +import logging +import os +import subprocess +from enum import Enum +from typing import Dict, Any, Optional + +from flask_restful import Resource, reqparse + +from tac import ROOT_DIR + +logger = logging.getLogger(__name__) + +parser = reqparse.RequestParser() +parser.add_argument("name", default="my_baseline_agent", help="Name of the agent.") +parser.add_argument("agent_timeout", type=float, default=1.0, help="The time in (fractions of) seconds to time out an agent between act and react.") +parser.add_argument("max_reactions", type=int, default=100, help="The maximum number of reactions (messages processed) per call to react.") +parser.add_argument("register_as", choices=['seller', 'buyer', 'both'], default='both', help="The string indicates whether the baseline agent registers as seller, buyer or both on the oef.") +parser.add_argument("search_for", choices=['sellers', 'buyers', 'both'], default='both', help="The string indicates whether the baseline agent searches for sellers, buyers or both on the oef.") +parser.add_argument("is_world_modeling", type=bool, default=False, help="Whether the agent uses a workd model or not.") +parser.add_argument("services_interval", type=int, default=5, help="The number of seconds to wait before doing another search.") +parser.add_argument("pending_transaction_timeout", type=int, default=30, help="The timeout in seconds to wait for pending transaction/negotiations.") +parser.add_argument("private_key_pem", default=None, help="Path to a file containing a private key in PEM format.") +parser.add_argument("rejoin", type=bool, default=False, help="Whether the agent is joining a running TAC.") + +current_agent = None # type: Optional[AgentRunner] + + +class AgentState(Enum): + """The state of execution of an agent.""" + + NOT_STARTED = "not_started" + RUNNING = "running" + FINISHED = "finished" + FAILED = "failed" + + +class AgentRunner: + """Wrapper class to track the execution of an agent script.""" + + def __init__(self, id: int, params: Dict[str, Any]): + """ + Initialize the agent runner. + + :param id: an identifier for the object. + :param params: the parameters of the agent script. + """ + self.id = id + self.params = params + + self.process = None # type: Optional[subprocess.Popen] + + def __call__(self): + """Launch the agent script.""" + if self.status != AgentState.NOT_STARTED: + return + + args = ["--name", str(self.params["name"]), + "--agent-timeout", str(self.params["agent_timeout"]), + "--max-reactions", str(self.params["max_reactions"]), + "--register-as", str(self.params["register_as"]), + "--search-for", str(self.params["search_for"]), + "--services-interval", str(self.params["services_interval"]), + "--pending-transaction-timeout", str(self.params["pending_transaction_timeout"])] + + if self.params["is_world_modeling"]: + args.append("--is-world-modeling") + if self.params["rejoin"]: + args.append("--rejoin") + if self.params["private_key_pem"] is not None: + args.append("--private-key-pem") + args.append(self.params["--private-key-pem"]) + + self.process = subprocess.Popen([ + "python3", + os.path.join(ROOT_DIR, "templates", "v1", "basic.py"), + *args, + "--dashboard", + "--visdom-addr", "127.0.0.1", + "--visdom-port", "8097", + ], stdout=subprocess.PIPE) + + @property + def status(self) -> AgentState: + """Return the state of the execution.""" + if self.process is None: + return AgentState.NOT_STARTED + returncode = self.process.poll() + if returncode is None: + return AgentState.RUNNING + elif returncode == 0: + return AgentState.FINISHED + elif returncode > 0: + return AgentState.FAILED + + def to_dict(self): + """Serialize the object into a dictionary.""" + return { + "id": self.id, + "status": self.status.value, + "params": self.params + } + + def stop(self): + """Stop the execution of the sandbox.""" + try: + self.process.terminate() + self.process.wait() + return True + except Exception: + raise + + +class Agent(Resource): + """The agent REST resource.""" + + def get(self): + """Get the current instance of the agent.""" + global current_agent + if current_agent is not None: + return current_agent.to_dict(), 200 + else: + return None, 200 + + def post(self): + """Create an agent instance.""" + global current_agent + if current_agent is not None and current_agent.status == AgentState.RUNNING: + # a sandbox is already running + return None, 400 + + # parse the arguments + args = parser.parse_args(strict=True) + + # create the agent runner wrapper + agent_runner = AgentRunner(0, args) + + # save the created simulation to the global state + current_agent = agent_runner + + # run the simulation + agent_runner() + + return agent_runner.to_dict(), 202 + + def delete(self): + """Delete the current agent instance.""" + global current_agent + if current_agent is None: + return None, 400 + else: + current_agent.stop() + current_agent = None + return {}, 204 diff --git a/tac/gui/launcher/api/resources/sandboxes.py b/tac/gui/launcher/api/resources/sandboxes.py new file mode 100644 index 00000000..a734c130 --- /dev/null +++ b/tac/gui/launcher/api/resources/sandboxes.py @@ -0,0 +1,215 @@ +# -*- 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. +# +# ------------------------------------------------------------------------------ + +"""Implement the sandbox resource and other utility classes.""" + +# import datetime +import logging +import os +import subprocess +from enum import Enum +from queue import Queue +from typing import Dict, Any, Optional + +from flask_restful import Resource, reqparse + +from tac import ROOT_DIR + +logger = logging.getLogger(__name__) + +parser = reqparse.RequestParser() +parser.add_argument("nb_agents", type=int, default=10, help="(minimum) number of TAC agent to wait for the competition.") +parser.add_argument("nb_goods", type=int, default=10, help="Number of TAC agent to run.") +parser.add_argument("money_endowment", type=int, default=200, help="Initial amount of money.") +parser.add_argument("base_good_endowment", default=2, type=int, help="The base amount of per good instances every agent receives.") +parser.add_argument("lower_bound_factor", default=0, type=int, help="The lower bound factor of a uniform distribution.") +parser.add_argument("upper_bound_factor", default=0, type=int, help="The upper bound factor of a uniform distribution.") +parser.add_argument("tx_fee", default=0.1, type=float, help="The transaction fee.") +# parser.add_argument("start_time", default=str(datetime.datetime.now() + datetime.timedelta(0, 10)), type=str, help="The start time for the competition (in UTC format).") +parser.add_argument("registration_timeout", default=10, type=int, help="The amount of time (in seconds) to wait for agents to register before attempting to start the competition.") +parser.add_argument("inactivity_timeout", default=60, type=int, help="The amount of time (in seconds) to wait during inactivity until the termination of the competition.") +parser.add_argument("competition_timeout", default=240, type=int, help="The amount of time (in seconds) to wait from the start of the competition until the termination of the competition.") +parser.add_argument("nb_baseline_agents", type=int, default=10, help="Number of baseline agent to run. Defaults to the number of agents of the competition.") +parser.add_argument("data_output_dir", default="data", help="The output directory for the simulation data.") +parser.add_argument("experiment_id", default=None, help="The experiment ID.") +parser.add_argument("seed", default=42, help="The random seed of the simulation.") +parser.add_argument("whitelist_file", default="", type=str, help="The file that contains the list of agent names to be whitelisted.") +parser.add_argument("services_interval", default=5, type=int, help="The amount of time (in seconds) the baseline agents wait until it updates services again.") +parser.add_argument("pending_transaction_timeout", default=120, type=int, help="The amount of time (in seconds) the baseline agents wait until the transaction confirmation.") +parser.add_argument("register_as", choices=['seller', 'buyer', 'both'], default='both', help="The string indicates whether the baseline agent registers as seller, buyer or both on the oef.") +parser.add_argument("search_for", choices=['sellers', 'buyers', 'both'], default='both', help="The string indicates whether the baseline agent searches for sellers, buyers or both on the oef.") + +sandboxes = {} # type: Dict[int, SandboxRunner] +sandbox_queue = Queue() + + +class SandboxState(Enum): + """The state of execution of a sandbox.""" + + NOT_STARTED = "Not started yet" + RUNNING = "Running" + FINISHED = "Finished" + FAILED = "Failed" + + +class SandboxRunner: + """Wrapper class to track the execution of a sandbox.""" + + def __init__(self, id: int, params: Dict[str, Any]): + """ + Initialize the sandbox runner. + + :param id: an identifier for the object. + :param params: the parameters of the simulation. + """ + self.id = id + self.params = params + + self.process = None # type: Optional[subprocess.Popen] + + def __call__(self): + """Launch the sandbox.""" + if self.status != SandboxState.NOT_STARTED: + return + + args = self.params + env = { + "NB_AGENTS": str(args["nb_agents"]), + "NB_GOODS": str(args["nb_goods"]), + "NB_BASELINE_AGENTS": str(args["nb_baseline_agents"]), + "SERVICES_INTERVAL": str(args["services_interval"]), + "REGISTER_AS": str(args["register_as"]), + "SEARCH_FOR": str(args["search_for"]), + "PENDING_TRANSACTION_TIMEOUT": str(args["pending_transaction_timeout"]), + "OEF_ADDR": "172.28.1.1", + "OEF_PORT": "10000", + "DATA_OUTPUT_DIR": str(args["data_output_dir"]), + "EXPERIMENT_ID": str(args["experiment_id"]), + "LOWER_BOUND_FACTOR": str(args["lower_bound_factor"]), + "UPPER_BOUND_FACTOR": str(args["upper_bound_factor"]), + "TX_FEE": str(args["tx_fee"]), + "REGISTRATION_TIMEOUT": str(args["registration_timeout"]), + "INACTIVITY_TIMEOUT": str(args["inactivity_timeout"]), + "COMPETITION_TIMEOUT": str(args["competition_timeout"]), + "SEED": str(args["seed"]), + "WHITELIST": str(args["whitelist_file"]), + **os.environ + } + self.process = subprocess.Popen([ + "docker-compose", + "-f", + os.path.join(ROOT_DIR, "sandbox", "docker-compose.yml"), + "up", + "--abort-on-container-exit"], + env=env) + + @property + def status(self) -> SandboxState: + """Return the state of the execution.""" + if self.process is None: + return SandboxState.NOT_STARTED + returncode = self.process.poll() + if returncode is None: + return SandboxState.RUNNING + elif returncode == 0: + return SandboxState.FINISHED + elif returncode > 0: + return SandboxState.FAILED + + def to_dict(self) -> Dict[str, Any]: + """Serialize the object into a dictionary.""" + return { + "id": self.id, + "status": self.status.value, + "params": self.params + } + + def stop(self) -> None: + """Stop the execution of the sandbox.""" + if self.process is None: + return + try: + self.process.terminate() + self.process.wait() + return + except Exception: + raise + + def wait(self): + """Wait for the completion of the sandbox.""" + return self.process.wait() + + +class Sandbox(Resource): + """The sandbox REST resource.""" + + def get(self, sandbox_id): + """Get the current instance of the sandbox.""" + if sandbox_id in sandboxes: + return sandboxes[sandbox_id].to_dict(), 200 + else: + return None, 404 + + def delete(self, sandbox_id): + """Delete the current sandbox instance.""" + if sandbox_id not in sandboxes: + return None, 404 + else: + sandbox = sandboxes[sandbox_id] + sandbox.stop() + return {}, 204 + + +class SandboxList(Resource): + """Resource to handle sandboxes.""" + + def get(self): + """Get all the sandboxes.""" + return {sandbox_id: sandbox.to_dict() for sandbox_id, sandbox in sandboxes.items()} + + def post(self): + """Create a sandbox instance.""" + # parse the arguments + args = parser.parse_args() + logger.debug("Args: \n{}".format(str(args))) + sandbox_id = len(sandboxes) + args = self._post_args_preprocessing(args, sandbox_id) + + # create the simulation runner wrapper + simulation_runner = SandboxRunner(sandbox_id, args) + + # save the created simulation to the global state + sandboxes[sandbox_id] = simulation_runner + + global sandbox_queue + sandbox_queue.put(simulation_runner) + return simulation_runner.to_dict(), 202 + + def _post_args_preprocessing(self, args, sandbox_id): + """Process the arguments of the POST request on /api/sandbox.""" + if args["data_output_dir"] == "": + args["data_output_dir"] = "./data" + if args["experiment_id"] == "" or args["experiment_id"] is None: + args["experiment_id"] = "./experiment-{}".format(sandbox_id) + # if args["start_time"] == "": + # args["start_time"] = str(datetime.datetime.now()) + # else: + # args["start_time"] = str(datetime.datetime.strptime(args["start_time"], "%m/%d/%Y %I:%M %p")) + return args diff --git a/tac/gui/launcher/app.py b/tac/gui/launcher/app.py new file mode 100644 index 00000000..ca87864c --- /dev/null +++ b/tac/gui/launcher/app.py @@ -0,0 +1,27 @@ +# -*- 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. +# +# ------------------------------------------------------------------------------ + +"""The main entrypoint for the launcher app.""" + +from tac.gui.launcher import create_app + +if __name__ == '__main__': + app = create_app() + app.run("127.0.0.1", 5000, debug=True, use_reloader=False) diff --git a/tac/gui/launcher/config.py b/tac/gui/launcher/config.py new file mode 100644 index 00000000..34e7143a --- /dev/null +++ b/tac/gui/launcher/config.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. +# +# ------------------------------------------------------------------------------ + +"""Configuration file for the Flask server.""" diff --git a/tac/gui/launcher/forms/__init__.py b/tac/gui/launcher/forms/__init__.py new file mode 100644 index 00000000..16f8cb40 --- /dev/null +++ b/tac/gui/launcher/forms/__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. +# +# ------------------------------------------------------------------------------ + +"""Implement forms to display in the launcher GUI.""" diff --git a/tac/gui/launcher/forms/agent.py b/tac/gui/launcher/forms/agent.py new file mode 100644 index 00000000..2db21f93 --- /dev/null +++ b/tac/gui/launcher/forms/agent.py @@ -0,0 +1,39 @@ +# -*- 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. +# +# ------------------------------------------------------------------------------ + +"""Implement the form for agent parameters.""" + +import wtforms +from wtforms import Form, StringField, IntegerField, FloatField, widgets, FileField + + +class AgentForm(Form): + """The form to set the agent parameters.""" + + name = StringField("Agent name", default="my_baseline_agent", validators=[wtforms.validators.Length(min=1)]) + agent_timeout = FloatField("Agent timeout", default=1.0, validators=[wtforms.validators.NumberRange(min=0.0)]) + max_reactions = IntegerField("Max reactions", default=100, widget=widgets.Input(input_type="number"), validators=[wtforms.validators.NumberRange(min=0.0)]) + register_as = wtforms.SelectField("Register as", choices=[("buyer", "buyer"), ("seller", "seller"), ("both", "both")], default="both") + search_for = wtforms.SelectField("Search for", choices=[("buyers", "buyers"), ("sellers", "sellers"), ("both", "both")], default="both") + is_world_modeling = wtforms.BooleanField("Is world modeling?", default=False) + services_interval = IntegerField('Services interval', default=5, widget=widgets.Input(input_type="number"), validators=[wtforms.validators.NumberRange(min=2, message="At least two baseline agents.")],) + pending_transaction_timeout = IntegerField("Pending transaction timeout", default=30, validators=[wtforms.validators.NumberRange(min=0.0)]) + private_key_pem = FileField("Private key PEM file path", default=None, validators=[wtforms.validators.Optional]) + rejoin = wtforms.BooleanField("Is rejoining?", default=False) diff --git a/tac/gui/launcher/forms/sandbox.py b/tac/gui/launcher/forms/sandbox.py new file mode 100644 index 00000000..06a426f5 --- /dev/null +++ b/tac/gui/launcher/forms/sandbox.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Implement the form for sandbox parameters.""" + +# from datetime import datetime +import wtforms +from wtforms import Form, IntegerField, FileField, widgets, FloatField # , DateTimeField, StringField + + +class SandboxForm(Form): + """The form to set the sandbox parameters.""" + + nb_agents = IntegerField('No. Agents', default=5, widget=widgets.Input(input_type="number"), validators=[wtforms.validators.NumberRange(min=2, message="At least two agents.")]) + nb_goods = IntegerField('No. Goods', default=5, widget=widgets.Input(input_type="number"), validators=[wtforms.validators.NumberRange(min=2, message="At least two goods.")],) + nb_baseline_agents = IntegerField('No. Baseline Agents', default=5, widget=widgets.Input(input_type="number"), validators=[wtforms.validators.NumberRange(min=2, message="At least two baseline agents.")],) + services_interval = IntegerField('Services interval', default=5, widget=widgets.Input(input_type="number"), validators=[wtforms.validators.NumberRange(min=2, message="At least two baseline agents.")],) + # data_output_dir = FileField("Data output directory", default="./data") + # experiment_id = StringField("Experiment ID", [wtforms.validators.Required()], default="exp_1") + lower_bound_factor = IntegerField('Lower bound factor', default=0, widget=widgets.Input(input_type="number"), validators=[wtforms.validators.NumberRange(min=0, message="Must be non-negative")],) + upper_bound_factor = IntegerField('Upper bound factor', default=0, widget=widgets.Input(input_type="number"), validators=[wtforms.validators.NumberRange(min=0, message="Must be non-negative")],) + tx_fee = FloatField("Transaction fee", default=0.1, validators=[wtforms.validators.NumberRange(min=0, message="Must be non-negative")],) + registration_timeout = IntegerField("Registration timeout", default=10, validators=[wtforms.validators.NumberRange(min=0, message="Must be non-negative")]) + inactivity_timeout = IntegerField("Inactivity timeout", default=60, validators=[wtforms.validators.NumberRange(min=0, message="Must be non-negative")]) + competition_timeout = IntegerField("Competition timeout", default=240, validators=[wtforms.validators.NumberRange(min=0, message="Must be non-negative")]) + # start_time = DateTimeField("Start time", id='datepick', validators=[wtforms.validators.Required()]) + seed = IntegerField("Seed", default=42) + whitelist_file = FileField("Whitelist file", default=None, validators=[wtforms.validators.Optional]) diff --git a/tac/gui/launcher/home.py b/tac/gui/launcher/home.py new file mode 100644 index 00000000..e897e592 --- /dev/null +++ b/tac/gui/launcher/home.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. +# +# ------------------------------------------------------------------------------ + +"""Implement the basic Flask blueprint for the common web pages (e.g. the index page).""" + +from flask import Blueprint, render_template, redirect + +from tac.gui.launcher.forms.sandbox import SandboxForm +from tac.gui.launcher.forms.agent import AgentForm + +bp = Blueprint("home", __name__, url_prefix="/") + + +@bp.route("/", methods=["GET"]) +def index(): + """Render the index page of the launcher app.""" + return redirect("/launcher", code=302) + + +@bp.route("/launcher", methods=["GET"]) +def launcher(): + """Render the launcher page.""" + sandbox_form = SandboxForm() + agent_form = AgentForm() + return render_template("launcher.html", form_sandbox=sandbox_form, form_agent=agent_form) + + +@bp.route("/grid-search", methods=["GET"]) +def grid_search(): + """Render the grid search page.""" + sandbox_form = SandboxForm() + return render_template("grid_search.html", form_sandbox=sandbox_form) diff --git a/tac/gui/launcher/static/css/main.css b/tac/gui/launcher/static/css/main.css new file mode 100644 index 00000000..968fedd4 --- /dev/null +++ b/tac/gui/launcher/static/css/main.css @@ -0,0 +1,70 @@ +html { + position: relative; + min-height: 100%; +} +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + /* Set the fixed height of the footer here */ + height: 60px; + line-height: 60px; /* Vertically center the text there */ + background-color: #f5f5f5; +} + + +/* Custom page CSS +-------------------------------------------------- */ + +body > .container { + padding: 15px 15px 15px 15px; +} + +.footer > .container { + padding-right: 15px; + padding-left: 15px; +} + +code { + font-size: 80%; +} + +th { + padding:5px +} + + +.card { + position: relative; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + min-width: 0; + word-wrap: break-word; + background-color: #fff; + background-clip: border-box; + border: 1px solid rgba(0,0,0,.125); + border-radius: .25rem; +} + +.card-header { + padding: .75rem 1.25rem; + margin-bottom: 0; + background-color: rgba(0, 0, 0, .03); + border-bottom: 1px solid rgba(0, 0, 0, .125); +} + +.card-body { + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + padding: 1.25rem; +} diff --git a/tac/gui/launcher/static/js/grid_search.js b/tac/gui/launcher/static/js/grid_search.js new file mode 100644 index 00000000..e2daa728 --- /dev/null +++ b/tac/gui/launcher/static/js/grid_search.js @@ -0,0 +1,91 @@ +(function() { + + let firstCard = $("#card-0"); + let cardHtmlTemplate = firstCard.html(); + let sandboxes = {}; + let accordion = $("#accordion"); + + class SandboxCard { + + constructor(id, jqueryObj){ + this.id = id; + this.jqueryObj = jqueryObj; + this.trashBtn = this.jqueryObj.find("button[name='btn-remove-item']"); + this.stopBtn = this.jqueryObj.find("button[name='btn-stop-item']"); + + this.setup(); + + } + + setup() { + let jqueryObj = this.jqueryObj; + let id = this.id; + + let remove = function(){ + jqueryObj.remove(); + delete sandboxes[id]; + }; + + let stop = function(){ + let XHR = new XMLHttpRequest(); + XHR.open("DELETE", "/api/sandboxes/" + id, true); + XHR.send(); + }; + + this.trashBtn.on('click', remove); + this.stopBtn.on('click', stop); + } + + static fromTemplate(id){ + let card = $("

"); + card.html(cardHtmlTemplate + .replace(/collapse-0/g, "collapse-" + id) + .replace(/heading-0/g, "heading-"+id) + .replace(/Sandbox 1/, "Sandbox " + (id + 1))); + card.addClass("card"); + card.attr("id", "card-" + id); + + accordion.append(card); + let jqueryObj = $('#card-'+id); + return new SandboxCard(id, jqueryObj); + } + } + + function buildJSONFromSandboxFormList(){ + let result = []; + for (let sandboxId in sandboxes){ + let inputs = sandboxes[sandboxId].jqueryObj.find('.card-body :input'); + let values = {}; + inputs.each(function() { + values[this.name] = $(this).val(); + }); + result.push(values); + } + return result + } + + $("#btn-add-sandbox").on("click", function(){ + let nbCards = Object.keys(sandboxes).length; + let card = SandboxCard.fromTemplate(nbCards); + sandboxes[card.id] = card; + }); + + $("#btn-submit-gridsearch").on("click", function() { + let sandboxObjectList = buildJSONFromSandboxFormList(); + for (let i = 0; i < sandboxObjectList.length; i++) { + console.log("POST /api/sandboxes for Sandbox " + i); + let XHR = new XMLHttpRequest(); + XHR.addEventListener("error", function (event) { + console.log("Error on request " + i + " event: " + event) + }); + XHR.open("POST", "/api/sandboxes", true); + XHR.send(sandboxObjectList[i]); + + } + }); + + (function main(){ + sandboxes[0] = new SandboxCard(0, firstCard); + })(); + +})(); diff --git a/tac/gui/launcher/static/js/launcher.js b/tac/gui/launcher/static/js/launcher.js new file mode 100644 index 00000000..56187bb3 --- /dev/null +++ b/tac/gui/launcher/static/js/launcher.js @@ -0,0 +1,176 @@ +(function () { + + // the ID of the current sandbox running/finished/stopped. 'null' if no sandbox has been started yet. + let currentSandboxID = null; + + let configureSandboxForm = function () { + + let form = document.getElementById("form-sandbox"); + let startBtn = document.getElementById("btn-start-sandbox"); + let stopBtn = document.getElementById("btn-stop-sandbox"); + let statusBtn = document.getElementById("btn-info-sandbox"); + + stopBtn.disabled = true; + + form.addEventListener("submit", function (ev) { + ev.preventDefault(); + + let clickedBtnId = ev.target.target; + if (clickedBtnId === startBtn.id) { + // start sandbox button clicked + startSandbox(); + } else if (clickedBtnId === stopBtn.id) { + // stop sandbox button clicked + stopSandbox(); + } + + }); + + let startSandbox = function () { + let XHR = new XMLHttpRequest(); + + // Bind the FormData object and the form element + let FD = new FormData(form); + + // Define what happens on successful data submission + XHR.addEventListener("load", function (event) { + startBtn.disabled = true; + stopBtn.disabled = false; + }); + + // Define what happens in case of error + XHR.addEventListener("error", function (event) { + startBtn.disabled = false; + stopBtn.disabled = true; + }); + + XHR.open("POST", "/api/sandboxes", false); + XHR.send(FD); + let jsonResponse = JSON.parse(XHR.response); + console.log("ID=" + jsonResponse["id"]); + currentSandboxID = jsonResponse["id"]; + return XHR.response; + }; + + let stopSandbox = function () { + let XHR = new XMLHttpRequest(); + + // Bind the FormData object and the form element + let FD = new FormData(form); + + // Define what happens on successful data submission + XHR.addEventListener("load", function (event) { + startBtn.disabled = false; + stopBtn.disabled = true; + }); + + // Define what happens in case of error + XHR.addEventListener("error", function (event) { + alert('ERROR: could not stop sandbox.'); + startBtn.disabled = true; + stopBtn.disabled = false; + }); + + XHR.open("DELETE", "/api/sandboxes/" + currentSandboxID, true); + XHR.send(FD); + return XHR.responseText; + }; + + let getSandboxStatus = function () { + console.log("getSandboxStatus called for ID=", currentSandboxID); + if (currentSandboxID != null) { + let XHR = new XMLHttpRequest(); + XHR.onreadystatechange = function () { + let jsonResponse = JSON.parse(XHR.response); + if (this.readyState == 4 && this.status == 200) { + statusBtn.innerHTML = jsonResponse["status"]; + } + }; + XHR.open("GET", "/api/sandboxes/" + currentSandboxID, true); + XHR.send(); + } + setTimeout(getSandboxStatus, 1000); + }; + getSandboxStatus(); + }; + let configureAgentForm = function () { + let form = document.getElementById("form-agent"); + let startBtn = document.getElementById("btn-start-agent"); + let stopBtn = document.getElementById("btn-stop-agent"); + + stopBtn.disabled = true; + + form.addEventListener("submit", function (ev) { + ev.preventDefault(); + + let clickedBtnId = ev.target.target; + if (clickedBtnId === startBtn.id) { + // start agent button clicked + startAgent(); + } else if (clickedBtnId === stopBtn.id) { + // stop sandbox button clicked + stopAgent(); + } + + }); + + let startAgent = function () { + let XHR = new XMLHttpRequest(); + + // Bind the FormData object and the form element + let FD = new FormData(form); + + // Define what happens on successful data submission + XHR.addEventListener("load", function (event) { + startBtn.disabled = true; + stopBtn.disabled = false; + }); + + // Define what happens in case of error + XHR.addEventListener("error", function (event) { + alert('ERROR: could not start agent.'); + startBtn.disabled = false; + stopBtn.disabled = true; + }); + + XHR.open("POST", "/api/agent", true); + XHR.send(FD); + return XHR.responseText; + }; + + let stopAgent = function () { + let XHR = new XMLHttpRequest(); + + // Bind the FormData object and the form element + let FD = new FormData(form); + + // Define what happens on successful data submission + XHR.addEventListener("load", function (event) { + startBtn.disabled = false; + stopBtn.disabled = true; + }); + + // Define what happens in case of error + XHR.addEventListener("error", function (event) { + alert('ERROR: could not stop agent.'); + startBtn.disabled = true; + stopBtn.disabled = false; + }); + + XHR.open("DELETE", "/api/agent", true); + XHR.send(FD); + return XHR.responseText; + }; + + }; + + + window.addEventListener("load", function () { + configureSandboxForm(); + configureAgentForm(); + }); + + +})(); + + diff --git a/tac/gui/launcher/templates/base.html b/tac/gui/launcher/templates/base.html new file mode 100644 index 00000000..6350a984 --- /dev/null +++ b/tac/gui/launcher/templates/base.html @@ -0,0 +1,44 @@ + + + + TAC + + + + + + + + + + + + + + + + + + {% include "includes/navbar.html" %} + {% block content %}{% endblock %} + + + + + + + + + + + {% block scripts %}{% endblock %} + + diff --git a/tac/gui/launcher/templates/grid_search.html b/tac/gui/launcher/templates/grid_search.html new file mode 100644 index 00000000..36a8effb --- /dev/null +++ b/tac/gui/launcher/templates/grid_search.html @@ -0,0 +1,63 @@ +{% extends "base.html" %} + +{% block content %} +
+

Sandbox list

+
+
+ + +
+
+
+
+ +
+ +
+
+
+ + + + + + +
+
+
+
+ + + {% for items_batched in form_sandbox._fields.items()|batch(3) %} + + {% for name, field in items_batched %} + + {% endfor %} + + {% endfor %} + +
+ {{ field.label }}
+ {{ field }} +
+
+
+
+ +
+ +
+ +
+ +{% endblock %} +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/tac/gui/launcher/templates/includes/navbar.html b/tac/gui/launcher/templates/includes/navbar.html new file mode 100644 index 00000000..9bc2ed1b --- /dev/null +++ b/tac/gui/launcher/templates/includes/navbar.html @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/tac/gui/launcher/templates/launcher.html b/tac/gui/launcher/templates/launcher.html new file mode 100644 index 00000000..c82a67bc --- /dev/null +++ b/tac/gui/launcher/templates/launcher.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+

Sandbox Configurations

+
+ {% for name, field in form_sandbox._fields.items() %} + {{ field.label }} + {{ field }}
+ {% endfor %} +
+ + + +
+
+
+
+

Agent Configurations

+
+ {% for name, field in form_agent._fields.items() %} + {{ field.label }} + {{ field }}
+ {% endfor %} + +
+ + + + + +
+
+
+
+
+{% endblock %} +{% block scripts %} + +{% endblock %} diff --git a/tac/helpers/misc.py b/tac/helpers/misc.py index cd230b13..1a8d74ad 100644 --- a/tac/helpers/misc.py +++ b/tac/helpers/misc.py @@ -22,7 +22,7 @@ import logging import random -from typing import List, Set, Dict, Tuple +from typing import List, Set, Dict, Tuple, Union import math import numpy as np @@ -33,6 +33,7 @@ logger = logging.getLogger("tac") TAC_SUPPLY_DATAMODEL_NAME = "tac_supply" TAC_DEMAND_DATAMODEL_NAME = "tac_demand" +QUANTITY_SHIFT = 1 # Any non-negative integer is fine. class TacError(Exception): @@ -160,7 +161,7 @@ def generate_money_endowments(nb_agents: int, money_endowment: int) -> List[int] return [money_endowment] * nb_agents -def generate_equilibrium_prices_and_holdings(endowments: List[List[int]], utility_function_params: List[List[float]], money_endowment: float, scaling_factor: float) -> Tuple[List[float], List[List[float]], List[float]]: +def generate_equilibrium_prices_and_holdings(endowments: List[List[int]], utility_function_params: List[List[float]], money_endowment: float, scaling_factor: float, quantity_shift: int = QUANTITY_SHIFT) -> Tuple[List[float], List[List[float]], List[float]]: """ Compute the competitive equilibrium prices and allocation. @@ -168,27 +169,29 @@ def generate_equilibrium_prices_and_holdings(endowments: List[List[int]], utilit :param utility_function_params: utility function params of the agents (already scaled) :param money_endowment: money endowment per agent. :param scaling_factor: a scaling factor for all the utility params generated. + :param quantity_shift: a factor to shift the quantities in the utility function (to ensure the natural logarithm can be used on the entire range of quantities) :return: the lists of equilibrium prices, equilibrium good holdings and equilibrium money holdings """ endowments_a = np.array(endowments, dtype=np.int) scaled_utility_function_params_a = np.array(utility_function_params, dtype=np.float) # note, they are already scaled endowments_by_good = np.sum(endowments_a, axis=0) scaled_params_by_good = np.sum(scaled_utility_function_params_a, axis=0) - eq_prices = np.divide(scaled_params_by_good, endowments_by_good) - eq_good_holdings = np.divide(scaled_utility_function_params_a, eq_prices) - eq_money_holdings = np.transpose(np.dot(eq_prices, np.transpose(endowments_a))) + money_endowment - scaling_factor + eq_prices = np.divide(scaled_params_by_good, quantity_shift * len(endowments) + endowments_by_good) + eq_good_holdings = np.divide(scaled_utility_function_params_a, eq_prices) - quantity_shift + eq_money_holdings = np.transpose(np.dot(eq_prices, np.transpose(endowments_a + quantity_shift))) + money_endowment - scaling_factor return eq_prices.tolist(), eq_good_holdings.tolist(), eq_money_holdings.tolist() -def logarithmic_utility(utility_function_params: List[float], good_bundle: List[int]) -> float: +def logarithmic_utility(utility_function_params: List[float], good_bundle: List[int], quantity_shift: int = QUANTITY_SHIFT) -> float: """ Compute agent's utility given her utility function params and a good bundle. :param utility_function_params: utility function params of the agent :param good_bundle: a bundle of goods with the quantity for each good + :param quantity_shift: a 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 """ - goodwise_utility = [param * math.log(quantity) if quantity > 0 else -10000 + goodwise_utility = [param * math.log(quantity + quantity_shift) if quantity + quantity_shift > 0 else -10000 for param, quantity in zip(utility_function_params, good_bundle)] return sum(goodwise_utility) @@ -293,6 +296,20 @@ def build_query(good_pbks: Set[str], is_searching_for_sellers: bool) -> Query: return query +def build_dict(good_pbks: Set[str], is_supply: bool) -> Dict[str, Union[str, List]]: + """ + Build supply or demand services dictionary. + + :param good_pbks: the good public keys to put in the query + :param is_supply: Boolean indicating whether the services are for supply or demand. + + :return: the dictionary + """ + description = TAC_SUPPLY_DATAMODEL_NAME if is_supply else TAC_DEMAND_DATAMODEL_NAME + result = {'description': description, 'services': list(good_pbks)} + return result + + def generate_good_pbk_to_name(nb_goods: int) -> Dict[str, str]: """ Generate public keys for things. diff --git a/tac/platform/controller.py b/tac/platform/controller.py index 6aeb24bd..28d4d7d0 100644 --- a/tac/platform/controller.py +++ b/tac/platform/controller.py @@ -46,8 +46,7 @@ from abc import ABC, abstractmethod from collections import defaultdict from threading import Thread -from typing import Any, Dict, Type, List -from typing import Optional, Set +from typing import Any, Dict, Type, List, Union, Optional, Set import dateutil from oef.agents import OEFAgent @@ -323,10 +322,6 @@ def handle(self, tx: Transaction) -> Optional[Response]: return self._handle_invalid_transaction(tx) # if transaction arrives second time then process it else: - # TODO how to handle failures in matching transaction? - # that is, should the pending txs be removed from the pool? - # if yes, should the senders be notified and how? - # don't care for now, because assuming only (properly implemented) baseline agents. pending_tx = self._pending_transaction_requests.pop(tx.transaction_id) if tx.matches(pending_tx): if self.controller_agent.game_handler.current_game.is_transaction_valid(tx): @@ -354,7 +349,7 @@ def _handle_valid_transaction(self, tx: Transaction) -> None: # update the game state. self.controller_agent.game_handler.current_game.settle_transaction(tx) - # update the GUI monitor + # update the dashboard monitor self.controller_agent.monitor.update() # send the transaction confirmation. @@ -607,7 +602,6 @@ def handle_registration_phase(self) -> bool: else: logger.debug("[{}]: Not enough agents to start TAC. Registered agents: {}, minimum number of agents: {}." .format(self.controller_agent.name, nb_reg_agents, min_nb_agents)) - self.notify_tac_cancelled() self.controller_agent.terminate() return False @@ -638,7 +632,7 @@ def __init__(self, name: str = "controller", :param oef_addr: the OEF address. :param oef_port: the OEF listening port. :param version: the version of the TAC controller. - :param monitor: the GUI monitor. If None, defaults to a null (dummy) monitor. + :param monitor: the dashboard monitor. If None, defaults to a null (dummy) monitor. """ self.name = name self.crypto = Crypto() @@ -840,7 +834,7 @@ def _parse_arguments(): parser.add_argument("--competition-timeout", default=240, type=int, help="The amount of time (in seconds) to wait from the start of the competition until the termination of the competition.") parser.add_argument("--whitelist-file", default=None, type=str, help="The file that contains the list of agent names to be whitelisted.") parser.add_argument("--verbose", default=False, action="store_true", help="Log debug messages.") - parser.add_argument("--gui", action="store_true", help="Show the GUI.") + parser.add_argument("--dashboard", action="store_true", help="Show the agent dashboard.") parser.add_argument("--visdom-addr", default="localhost", help="TCP/IP address of the Visdom server.") parser.add_argument("--visdom-port", default=8097, help="TCP/IP port of the Visdom server.") parser.add_argument("--data-output-dir", default="data", help="The output directory for the simulation data.") @@ -862,13 +856,13 @@ def main( tx_fee: float = 1.0, oef_addr: str = "127.0.0.1", oef_port: int = 10000, - start_time: str = str(datetime.datetime.now() + datetime.timedelta(0, 10)), + start_time: Union[str, datetime.datetime] = str(datetime.datetime.now() + datetime.timedelta(0, 10)), registration_timeout: int = 10, inactivity_timeout: int = 60, competition_timeout: int = 240, whitelist_file: Optional[str] = None, verbose: bool = False, - gui: bool = False, + dashboard: bool = False, visdom_addr: str = "localhost", visdom_port: int = 8097, data_output_dir: str = "data", @@ -886,7 +880,7 @@ def main( else: logger.setLevel(logging.INFO) - monitor = VisdomMonitor(visdom_addr=visdom_addr, visdom_port=visdom_port) if gui else NullMonitor() + monitor = VisdomMonitor(visdom_addr=visdom_addr, visdom_port=visdom_port) if dashboard else NullMonitor() try: @@ -905,7 +899,7 @@ def main( base_good_endowment=base_good_endowment, lower_bound_factor=lower_bound_factor, upper_bound_factor=upper_bound_factor, - start_time=dateutil.parser.parse(start_time), + start_time=dateutil.parser.parse(start_time) if type(start_time) == str else start_time, registration_timeout=registration_timeout, competition_timeout=competition_timeout, inactivity_timeout=inactivity_timeout, diff --git a/tac/platform/game.py b/tac/platform/game.py index 19958b24..34f78b4e 100644 --- a/tac/platform/game.py +++ b/tac/platform/game.py @@ -329,7 +329,7 @@ class Game: Get the scores: >>> game.get_scores() - {'tac_agent_0_pbk': 20.0, 'tac_agent_1_pbk': 26.931471805599454, 'tac_agent_2_pbk': 40.79441541679836} + {'tac_agent_0_pbk': 89.31471805599453, 'tac_agent_1_pbk': 93.36936913707618, 'tac_agent_2_pbk': 101.47867129923947} """ def __init__(self, configuration: GameConfiguration, initialization: GameInitialization): @@ -605,9 +605,8 @@ def get_holdings_summary(self) -> str: :return: a string representing the holdings for every agent. """ result = "" - # TODO > assuming agent_names ordering is consistent with agent_pbks ordering - for agent_name, agent_state in zip(self.configuration.agent_names, self.agent_states.values()): - result = result + agent_name + " " + str(agent_state._current_holdings) + "\n" + for agent_pbk, agent_state in self.agent_states.items(): + result = result + self.configuration.agent_pbk_to_name[agent_pbk] + " " + str(agent_state._current_holdings) + "\n" return result def get_equilibrium_summary(self) -> str: @@ -666,7 +665,6 @@ def __init__(self, money: float, endowment: Endowment, utility_params: UtilityPa """ assert len(endowment) == len(utility_params) self.balance = money - # TODO: fix notation to utility_params self._utility_params = copy.copy(utility_params) self._current_holdings = copy.copy(endowment) @@ -680,7 +678,6 @@ def utility_params(self) -> UtilityParams: """Get utility parameter for each good.""" return copy.copy(self._utility_params) - # TODO: potentially move the next three methods out as separate utilities; separate state (data) from member functions def get_score(self) -> float: """ Compute the score of the current state. @@ -725,7 +722,6 @@ def check_transaction_is_consistent(self, tx: Transaction, tx_fee: float) -> boo result = result and (self._current_holdings[good_id] >= quantity) return result - # TODO: think about potentially taking apply and update out (simplifies not having to worry about changing state from within the class) def apply(self, transactions: List[Transaction], tx_fee: float) -> 'AgentState': """ Apply a list of transactions to the current state. diff --git a/tac/platform/simulation.py b/tac/platform/simulation.py new file mode 100644 index 00000000..4b7af7f0 --- /dev/null +++ b/tac/platform/simulation.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python3 +# -*- 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 implements a TAC simulation. + +It spawn a controller agent that handles the competition and +several baseline agents that will participate to the competition. + +It requires an OEF node running and a Visdom server, if the visualization is desired. + +You can also run it as a script. To check the available arguments: + + python3 -m tac.platform.simulation -h + +""" + +import argparse +import datetime +import logging +import math +import multiprocessing +import pprint +import random +import time +from typing import Optional, List + +import dateutil + +from tac.agents.v1.base.strategy import RegisterAs, SearchFor +from tac.agents.v1.examples.baseline import main as baseline_main +from tac.platform.controller import TACParameters +from tac.platform.controller import main as controller_main + +logger = logging.getLogger(__name__) + + +class SimulationParams: + """Class to hold simulation parameters.""" + + def __init__(self, + oef_addr: str = "localhost", + oef_port: int = 10000, + nb_baseline_agents: int = 5, + register_as: RegisterAs = RegisterAs.BOTH, + search_for: SearchFor = SearchFor.BOTH, + services_interval: int = 5, + pending_transaction_timeout: int = 120, + verbose: bool = False, + dashboard: bool = False, + visdom_addr: str = "localhost", + visdom_port: int = 8097, + data_output_dir: Optional[str] = "data", + experiment_id: int = None, + seed: int = 42, + tac_parameters: Optional[TACParameters] = None): + """ + Initialize a SimulationParams class. + + :param oef_addr: the IP address of the OEF. + :param oef_port: the port of the OEF. + :param nb_baseline_agents: the number of baseline agents to spawn. + :param register_as: the registration policy the agents will follow. + :param search_for: the search policy the agents will follow. + :param services_interval: The amount of time (in seconds) the baseline agents wait until it updates services again. + :param pending_transaction_timeout: The amount of time (in seconds) the baseline agents wait until the transaction confirmation. + :param verbose: control the verbosity of the simulation. + :param dashboard: enable the Visdom visualization. + :param visdom_addr: the IP address of the Visdom server + :param visdom_port: the port of the Visdom server. + :param data_output_dir: the path to the output directory. + :param experiment_id: the name of the experiment. + :param seed: the random seed. + :param tac_parameters: the parameters for the TAC. + """ + self.tac_parameters = tac_parameters if tac_parameters is not None else TACParameters() + self.oef_addr = oef_addr + self.oef_port = oef_port + self.nb_baseline_agents = nb_baseline_agents + self.register_as = register_as + self.search_for = search_for + self.services_interval = services_interval + self.pending_transaction_timeout = pending_transaction_timeout + self.verbose = verbose + self.dashboard = dashboard + self.visdom_addr = visdom_addr + self.visdom_port = visdom_port + self.data_output_dir = data_output_dir + self.experiment_id = experiment_id + self.seed = seed + + +def _make_id(agent_id: int, is_world_modeling: bool, nb_agents: int) -> str: + """ + Make the name for baseline agents from an integer identifier. + + E.g.: + + >>> _make_id(2, False, 10) + 'tac_agent_2' + >>> _make_id(2, False, 100) + 'tac_agent_02' + >>> _make_id(2, False, 101) + 'tac_agent_002' + + :param agent_id: the agent id. + :param is_world_modeling: the boolean indicated whether the baseline agent models the world around her or not. + :param nb_agents: the overall number of agents. + :return: the formatted name. + :return: the string associated to the integer id. + """ + max_number_of_digits = math.ceil(math.log10(nb_agents)) + if is_world_modeling: + string_format = "tac_agent_{:0" + str(max_number_of_digits) + "}_wm" + else: + string_format = "tac_agent_{:0" + str(max_number_of_digits) + "}" + result = string_format.format(agent_id) + return result + + +def spawn_controller_agent(params: SimulationParams): + """Spawn a controller agent.""" + result = multiprocessing.Process(target=controller_main, kwargs=dict( + name="tac_controller", + nb_agents=params.tac_parameters.min_nb_agents, + nb_goods=params.tac_parameters.nb_goods, + money_endowment=params.tac_parameters.money_endowment, + base_good_endowment=params.tac_parameters.base_good_endowment, + lower_bound_factor=params.tac_parameters.lower_bound_factor, + upper_bound_factor=params.tac_parameters.upper_bound_factor, + tx_fee=params.tac_parameters.tx_fee, + oef_addr=params.oef_addr, + oef_port=params.oef_port, + start_time=params.tac_parameters.start_time, + registration_timeout=params.tac_parameters.registration_timeout, + inactivity_timeout=params.tac_parameters.inactivity_timeout, + competition_timeout=params.tac_parameters.competition_timeout, + whitelist_file=params.tac_parameters.whitelist, + verbose=True, + dashboard=params.dashboard, + visdom_addr=params.visdom_addr, + visdom_port=params.visdom_port, + data_output_dir=params.data_output_dir, + experiment_id=params.experiment_id, + seed=params.seed, + version=1, + )) + result.start() + return result + + +def run_baseline_agent(**kwargs) -> None: + """Run a baseline agent.""" + # give the time to the controller to connect to the OEF + time.sleep(5.0) + baseline_main(**kwargs) + + +def spawn_baseline_agents(params: SimulationParams) -> List[multiprocessing.Process]: + """Spawn baseline agents.""" + fraction_world_modeling = 0.1 + nb_baseline_agents_world_modeling = round(params.nb_baseline_agents * fraction_world_modeling) + + threads = [multiprocessing.Process(target=run_baseline_agent, kwargs=dict( + name=_make_id(i, i < nb_baseline_agents_world_modeling, params.nb_baseline_agents), + oef_addr=params.oef_addr, + oef_port=params.oef_port, + register_as=params.register_as, + search_for=params.search_for, + is_world_modeling=i < nb_baseline_agents_world_modeling, + services_interval=params.services_interval, + pending_transaction_timeout=params.pending_transaction_timeout, + dashboard=params.dashboard, + visdom_addr=params.visdom_addr, + visdom_port=params.visdom_port)) for i in range(params.nb_baseline_agents)] + + for t in threads: + t.start() + + return threads + + +def parse_arguments(): + """Arguments parsing.""" + parser = argparse.ArgumentParser("tac_agent_spawner") + parser.add_argument("--nb-agents", type=int, default=10, help="(minimum) number of TAC agent to wait for the competition.") + parser.add_argument("--nb-goods", type=int, default=10, help="Number of TAC agent to run.") + parser.add_argument("--money-endowment", type=int, default=200, help="Initial amount of money.") + parser.add_argument("--base-good-endowment", default=2, type=int, help="The base amount of per good instances every agent receives.") + parser.add_argument("--lower-bound-factor", default=0, type=int, help="The lower bound factor of a uniform distribution.") + parser.add_argument("--upper-bound-factor", default=0, type=int, help="The upper bound factor of a uniform distribution.") + parser.add_argument("--tx-fee", default=0.1, type=float, help="The transaction fee.") + parser.add_argument("--oef-addr", default="127.0.0.1", help="TCP/IP address of the OEF Agent") + parser.add_argument("--oef-port", default=10000, help="TCP/IP port of the OEF Agent") + parser.add_argument("--nb-baseline-agents", type=int, default=10, help="Number of baseline agent to run. Defaults to the number of agents of the competition.") + parser.add_argument("--start-time", default=str(datetime.datetime.now() + datetime.timedelta(0, 10)), type=str, help="The start time for the competition (in UTC format).") + parser.add_argument("--registration-timeout", default=10, type=int, help="The amount of time (in seconds) to wait for agents to register before attempting to start the competition.") + parser.add_argument("--inactivity-timeout", default=60, type=int, help="The amount of time (in seconds) to wait during inactivity until the termination of the competition.") + parser.add_argument("--competition-timeout", default=240, type=int, help="The amount of time (in seconds) to wait from the start of the competition until the termination of the competition.") + parser.add_argument("--services-interval", default=5, type=int, help="The amount of time (in seconds) the baseline agents wait until it updates services again.") + parser.add_argument("--pending-transaction-timeout", default=120, type=int, help="The amount of time (in seconds) the baseline agents wait until the transaction confirmation.") + parser.add_argument("--register-as", choices=['seller', 'buyer', 'both'], default='both', help="The string indicates whether the baseline agent registers as seller, buyer or both on the oef.") + parser.add_argument("--search-for", choices=['sellers', 'buyers', 'both'], default='both', help="The string indicates whether the baseline agent searches for sellers, buyers or both on the oef.") + parser.add_argument("--dashboard", action="store_true", help="Enable the agent dashboard.") + parser.add_argument("--data-output-dir", default="data", help="The output directory for the simulation data.") + parser.add_argument("--experiment-id", default=None, help="The experiment ID.") + parser.add_argument("--visdom-addr", default="localhost", help="TCP/IP address of the Visdom server") + parser.add_argument("--visdom-port", default=8097, help="TCP/IP port of the Visdom server") + parser.add_argument("--seed", default=42, help="The random seed of the simulation.") + parser.add_argument("--whitelist-file", nargs="?", default=None, type=str, help="The file that contains the list of agent names to be whitelisted.") + + arguments = parser.parse_args() + logger.debug("Arguments: {}".format(pprint.pformat(arguments.__dict__))) + + return arguments + + +def build_simulation_parameters(arguments: argparse.Namespace) -> SimulationParams: + """From argparse output, build an instance of SimulationParams.""" + tac_parameters = TACParameters( + min_nb_agents=arguments.nb_agents, + money_endowment=arguments.money_endowment, + nb_goods=arguments.nb_goods, + tx_fee=arguments.tx_fee, + base_good_endowment=arguments.base_good_endowment, + lower_bound_factor=arguments.lower_bound_factor, + upper_bound_factor=arguments.upper_bound_factor, + start_time=dateutil.parser.parse(arguments.start_time), + registration_timeout=arguments.registration_timeout, + competition_timeout=arguments.competition_timeout, + inactivity_timeout=arguments.inactivity_timeout, + whitelist=arguments.whitelist_file + ) + + simulation_params = SimulationParams( + oef_addr=arguments.oef_addr, + oef_port=arguments.oef_port, + nb_baseline_agents=arguments.nb_baseline_agents, + dashboard=arguments.dashboard, + visdom_addr=arguments.visdom_addr, + visdom_port=arguments.visdom_port, + data_output_dir=arguments.data_output_dir, + experiment_id=arguments.experiment_id, + seed=arguments.seed, + tac_parameters=tac_parameters + ) + + return simulation_params + + +def run(params: SimulationParams): + """Run the simulation.""" + random.seed(params.seed) + + controller_thread = None # type: Optional[multiprocessing.Process] + baseline_threads = [] # type: List[multiprocessing.Process] + + try: + + controller_thread = spawn_controller_agent(params) + baseline_threads = spawn_baseline_agents(params) + controller_thread.join() + + except KeyboardInterrupt: + logger.debug("Simulation interrupted...") + except Exception: + logger.exception("Unexpected exception.") + exit(-1) + finally: + if controller_thread is not None: + controller_thread.join(timeout=5) + controller_thread.terminate() + + for t in baseline_threads: + t.join(timeout=5) + t.terminate() + + +if __name__ == '__main__': + arguments = parse_arguments() + simulation_parameters = build_simulation_parameters(arguments) + run(simulation_parameters) diff --git a/templates/README.md b/templates/README.md index 0dc098dd..8cce54c7 100644 --- a/templates/README.md +++ b/templates/README.md @@ -13,9 +13,8 @@ Check out the [package documentation](../../master/docs) to learn more about the To test your agent run it against baseline agents in the sandbox. Follow the steps 1.-3. in sandbox readme, then start your own agent: -``` -python3 templates/v1/basic.py --name basic1 --gui -``` + python templates/v1/basic.py --name basic1 --dashboard + The following additional parameters can be used to tune the agent: @@ -32,34 +31,30 @@ If you want to use the same cryptographic key, you can follow these steps: - Generate a private key: - python3 scripts/generate_private_key.py private_key.pem + python scripts/generate_private_key.py private_key.pem - Every time you run your agent, add the parameter `--private-key-pem ` to your command: - python3 templates/v1/basic.py --name basic1 --gui --private-key-pem private_key.pem + python templates/v1/basic.py --name basic1 --dashboard --private-key-pem private_key.pem ## Testing manually (not recommended) - First, start the oef: -``` -python3 oef_search_pluto_scripts/launch.py -c ./oef_search_pluto_scripts/launch_config.json -``` + + python oef_search_pluto_scripts/launch.py -c ./oef_search_pluto_scripts/launch_config.json - Second, start the visdom server in shell: -``` -python3 -m visdom.server -``` + + python -m visdom.server - Third, tart the controller, followed by two agents in separate terminals. -``` -python3 tac/platform/controller.py --verbose --registration-timeout 20 --nb-agents 2 --tx-fee 0.0 --gui -``` -``` -python3 templates/v1/basic.py --name basic0 --gui -``` -``` -python3 templates/v1/basic.py --name basic1 --gui -``` + + python tac/platform/controller.py --verbose --registration-timeout 20 --nb-agents 2 --tx-fee 0.0 --dashboard + + python templates/v1/basic.py --name basic0 --dashboard + + templates/v1/basic.py --name basic1 --dashboard + The following parameters can be used to tune the agent: diff --git a/templates/__init__.py b/templates/__init__.py new file mode 100644 index 00000000..8ec49cc9 --- /dev/null +++ b/templates/__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. +# +# ------------------------------------------------------------------------------ + +"""Contains the TAC v1 templates.""" diff --git a/templates/v1/__init__.py b/templates/v1/__init__.py new file mode 100644 index 00000000..50e029e1 --- /dev/null +++ b/templates/v1/__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. +# +# ------------------------------------------------------------------------------ + +"""Contains the TAC templates.""" diff --git a/templates/v1/advanced.py b/templates/v1/advanced.py index 2b7ca226..4cd5a0a3 100644 --- a/templates/v1/advanced.py +++ b/templates/v1/advanced.py @@ -50,9 +50,9 @@ def parse_arguments(): parser.add_argument("--pending-transaction-timeout", type=int, default=30, help="The timeout in seconds to wait for pending transaction/negotiations.") parser.add_argument("--private-key-pem", default=None, help="Path to a file containing a private key in PEM format.") parser.add_argument("--rejoin", action="store_true", default=False, help="Whether the agent is joining a running TAC.") - parser.add_argument("--gui", action="store_true", help="Show the GUI.") - parser.add_argument("--visdom_addr", type=str, default="localhost", help="IP address to the Visdom server") - parser.add_argument("--visdom_port", type=int, default=8097, help="Port of the Visdom server") + parser.add_argument("--dashboard", action="store_true", help="Show the agent dashboard.") + parser.add_argument("--visdom-addr", type=str, default="localhost", help="IP address to the Visdom server") + parser.add_argument("--visdom-port", type=int, default=8097, help="Port of the Visdom server") return parser.parse_args() @@ -121,15 +121,15 @@ def main(): """Run the script.""" args = parse_arguments() - if args.gui: - dashboard = AgentDashboard(agent_name=args.name, env_name=args.name) + if args.dashboard: + agent_dashboard = AgentDashboard(agent_name=args.name, env_name=args.name) else: - dashboard = None + agent_dashboard = None strategy = MyStrategy(register_as=RegisterAs(args.register_as), search_for=SearchFor(args.search_for), is_world_modeling=args.is_world_modeling) agent = BaselineAgent(name=args.name, oef_addr=args.oef_addr, oef_port=args.oef_port, agent_timeout=args.agent_timeout, strategy=strategy, max_reactions=args.max_reactions, services_interval=args.services_interval, pending_transaction_timeout=args.pending_transaction_timeout, - dashboard=dashboard, private_key_pem=args.private_key_pem) + dashboard=agent_dashboard, private_key_pem=args.private_key_pem) try: agent.start(rejoin=args.rejoin) diff --git a/templates/v1/basic.py b/templates/v1/basic.py index 67468c56..e01f4bfa 100644 --- a/templates/v1/basic.py +++ b/templates/v1/basic.py @@ -47,9 +47,9 @@ def parse_arguments(): parser.add_argument("--pending-transaction-timeout", type=int, default=30, help="The timeout in seconds to wait for pending transaction/negotiations.") parser.add_argument("--private-key-pem", default=None, help="Path to a file containing a private key in PEM format.") parser.add_argument("--rejoin", action="store_true", default=False, help="Whether the agent is joining a running TAC.") - parser.add_argument("--gui", action="store_true", help="Show the GUI.") - parser.add_argument("--visdom_addr", type=str, default="localhost", help="IP address to the Visdom server") - parser.add_argument("--visdom_port", type=int, default=8097, help="Port of the Visdom server") + parser.add_argument("--dashboard", action="store_true", help="Show the agent dashboard.") + parser.add_argument("--visdom-addr", type=str, default="localhost", help="IP address to the Visdom server") + parser.add_argument("--visdom-port", type=int, default=8097, help="Port of the Visdom server") return parser.parse_args() @@ -58,15 +58,15 @@ def main(): """Run the script.""" args = parse_arguments() - if args.gui: - dashboard = AgentDashboard(agent_name=args.name, env_name=args.name) + if args.dashboard: + agent_dashboard = AgentDashboard(agent_name=args.name, env_name=args.name) else: - dashboard = None + agent_dashboard = None strategy = BaselineStrategy(register_as=RegisterAs(args.register_as), search_for=SearchFor(args.search_for), is_world_modeling=args.is_world_modeling) agent = BaselineAgent(name=args.name, oef_addr=args.oef_addr, oef_port=args.oef_port, agent_timeout=args.agent_timeout, strategy=strategy, max_reactions=args.max_reactions, services_interval=args.services_interval, pending_transaction_timeout=args.pending_transaction_timeout, - dashboard=dashboard, private_key_pem=args.private_key_pem) + dashboard=agent_dashboard, private_key_pem=args.private_key_pem) try: agent.start(rejoin=args.rejoin) diff --git a/tests/conftest.py b/tests/conftest.py index 5f436b61..6296ba77 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -86,7 +86,7 @@ def _create_oef_docker_image(oef_addr_, oef_port_) -> Container: '{}/tcp'.format(oef_port_): ("0.0.0.0", oef_port_)} volumes = {ROOT_DIR + '/oef_search_pluto_scripts': {'bind': '/config', 'mode': 'rw'}, ROOT_DIR + '/data/oef-logs': {'bind': '/logs', 'mode': 'rw'}} c = client.containers.run("fetchai/oef-search:latest", - "node no_sh --config_file /config/node_config_latest.json", + "/config/node_config_latest.json", detach=True, ports=ports, volumes=volumes) return c diff --git a/tox.ini b/tox.ini index fe7b63c6..73fb5de8 100644 --- a/tox.ini +++ b/tox.ini @@ -4,13 +4,14 @@ skipsdist = True [testenv] basepython = python3.7 +whitelist_externals = pipenv deps = pytest pytest-cov docker commands = - pip install . + pipenv install pytest --doctest-modules tac tests/ --cov-report=html --cov-report=term --cov=tac {posargs} [testenv:flake8] @@ -18,4 +19,4 @@ basepython = python3.7 deps = flake8 flake8-docstrings pydocstyle==3.0.0 -commands = flake8 tac simulation sandbox templates tests --exclude=tac/gui/static,tac/gui/templates,.md,tac/tac_pb2.py,tac/gui/.visdom_env,tac/__init__.py --ignore=E501,E701 \ No newline at end of file +commands = flake8 tac simulation sandbox scripts templates tests --exclude=tac/gui/static,tac/gui/templates,.md,tac/tac_pb2.py,tac/gui/.visdom_env,tac/__init__.py --ignore=E501,E701 \ No newline at end of file