diff --git a/.gitignore b/.gitignore
index 82f5ee3cbe..8a0b06a480 100644
--- a/.gitignore
+++ b/.gitignore
@@ -111,3 +111,6 @@ venv.bak/
data/*
temp_private_key.pem
.idea/
+
+input_file
+output_file
diff --git a/HISTORY.rst b/HISTORY.rst
index f38d4abda1..372a8f902e 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -110,3 +110,18 @@ Release History
- Updates oef connection to re-establish dropped connections
- Updates the car park agent
- Multiple additional minor fixes and changes
+
+0.1.14 (2019-11-29)
+-------------------
+
+- Removes dependency on OEF SDK's FIPA API
+- Replaces dialogue id with dialogue references
+- Improves CLI logging and list/search command output
+- Introduces multiplexer and removes mailbox
+- Adds much improved tac skills
+- Adds support for CLI integration with registry
+- Increases test coverage to 99%
+- Introduces integration tests for skills and examples
+- Adds support to run multiple connections from CLI
+- Updates the docs and adds uml diagrams
+- Multiple additional minor fixes and changes
diff --git a/Makefile b/Makefile
index e193a79fbd..5eda1c9440 100644
--- a/Makefile
+++ b/Makefile
@@ -23,6 +23,8 @@ clean-test:
rm -fr .hypothesis
rm -fr .pytest_cache
rm -fr .mypy_cache/
+ rm -fr input_file
+ rm -fr output_file
find . -name 'log.txt' -exec rm -fr {} +
find . -name 'log.*.txt' -exec rm -fr {} +
diff --git a/Pipfile b/Pipfile
index 92254b76a3..ba1f844bc6 100644
--- a/Pipfile
+++ b/Pipfile
@@ -19,8 +19,14 @@ pytest-cov = "*"
mypy = "*"
mkdocs = "*"
mkdocs-material = "*"
+bs4 = "*"
pymdown-extensions = "*"
pygments = "*"
+pytest-asyncio = "*"
+gym = "*"
+numpy = "*"
+scikit-image = "*"
+mkdocs-mermaid-plugin = {editable = true,git = "https://github.com/pugong/mkdocs-mermaid-plugin.git"}
[packages]
cryptography = "*"
diff --git a/Pipfile.lock b/Pipfile.lock
index 1f9e858add..89bc838347 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "8ab3d8accc84f8a69a75f0264cb346c03fb35358281f88999a1bca9fe570420a"
+ "sha256": "54ca472b4d79c59a127c1f10ef626afad67fbfb834d3876a7c03fadac3b2e421"
},
"pipfile-spec": 6,
"requires": {
@@ -53,10 +53,10 @@
},
"certifi": {
"hashes": [
- "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50",
- "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"
+ "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
+ "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
],
- "version": "==2019.9.11"
+ "version": "==2019.11.28"
},
"cffi": {
"hashes": [
@@ -71,6 +71,7 @@
"sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97",
"sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43",
"sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db",
+ "sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3",
"sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b",
"sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579",
"sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346",
@@ -164,7 +165,6 @@
"hashes": [
"sha256:82f5bba81d73a5a6b06f2a3553ff9003d865952fcb32e1df192378dd944d8a5c"
],
- "markers": "implementation_name == 'cpython'",
"version": "==0.10.1"
},
"docker": {
@@ -177,10 +177,10 @@
},
"ecdsa": {
"hashes": [
- "sha256:163c80b064a763ea733870feb96f9dd9b92216cfcacd374837af18e4e8ec3d4d",
- "sha256:9814e700890991abeceeb2242586024d4758c8fc18445b194a49bd62d85861db"
+ "sha256:64c613005f13efec6541bb0a33290d0d03c27abab5f15fbab20fb0ee162bdd8e",
+ "sha256:e108a5fe92c67639abae3260e43561af914e7fd0d27bae6d2ec1312ae7934dfe"
],
- "version": "==0.13.3"
+ "version": "==0.14.1"
},
"eth-abi": {
"hashes": [
@@ -230,17 +230,17 @@
},
"eth-typing": {
"hashes": [
- "sha256:abcfd279ad708a6fe9daf2af2bebb47d3da0faa0bbcb3765294506025fe9dd8b",
- "sha256:dab078dceb2b8687c9b94209e7a90da0fba5db075d132db997d9da9e2ead3465"
+ "sha256:2f3e1f891226148898b219bd94674a9af06c2d75d8cdd8c6722227b472cbd4d4",
+ "sha256:cf9e5e9fb62cfeb1027823328569315166851c65c5774604d801b6b926ff65bc"
],
- "version": "==2.2.0"
+ "version": "==2.2.1"
},
"eth-utils": {
"hashes": [
- "sha256:88a10ea8824042c589bbcec1516f363e96a1fbf52d0a8c8c761893a41cab8656",
- "sha256:9ce4658025b8d063ddb5d781e53639725faf1f3f96a9861095f2e874e4a9fbda"
+ "sha256:dc0c83618c0d25d509fde8b48cc40bd4c030788281a298a7fa78900c53199f72",
+ "sha256:f9ee3178db30a1bcff80c73915bde18b80219e944587512cea0101eaf71f35fc"
],
- "version": "==1.8.0"
+ "version": "==1.8.1"
},
"fetchai-ledger-api": {
"hashes": [
@@ -260,10 +260,10 @@
},
"graphviz": {
"hashes": [
- "sha256:dc08677f37c65a4a480f00df4bd0d19a0a103c06aad95f21a37f0b7fd440de81",
- "sha256:df54c2e0d2c8df6aee3397eb44de186d94e2a0610f4052649bfbb26d03d56850"
+ "sha256:241fb099e32b8e8c2acca747211c8237e40c0b89f24b1622860075d59f4c4b25",
+ "sha256:60acbeee346e8c14555821eab57dbf68a169e6c10bce40e83c1bf44f63a62a01"
],
- "version": "==0.13"
+ "version": "==0.13.2"
},
"hexbytes": {
"hashes": [
@@ -284,6 +284,7 @@
"sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26",
"sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"
],
+ "markers": "python_version < '3.8'",
"version": "==0.23"
},
"inflection": {
@@ -315,11 +316,11 @@
},
"jsonschema": {
"hashes": [
- "sha256:2fa0684276b6333ff3c0b1b27081f4b2305f0a36cf702a23db50edb141893c3f",
- "sha256:94c0a13b4a0616458b42529091624e66700a17f847453e52279e35509a5b7631"
+ "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163",
+ "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"
],
"index": "pypi",
- "version": "==3.1.1"
+ "version": "==3.2.0"
},
"lru-dict": {
"hashes": [
@@ -439,27 +440,27 @@
},
"protobuf": {
"hashes": [
- "sha256:125713564d8cfed7610e52444c9769b8dcb0b55e25cc7841f2290ee7bc86636f",
- "sha256:1accdb7a47e51503be64d9a57543964ba674edac103215576399d2d0e34eac77",
- "sha256:27003d12d4f68e3cbea9eb67427cab3bfddd47ff90670cb367fcd7a3a89b9657",
- "sha256:3264f3c431a631b0b31e9db2ae8c927b79fc1a7b1b06b31e8e5bcf2af91fe896",
- "sha256:3c5ab0f5c71ca5af27143e60613729e3488bb45f6d3f143dc918a20af8bab0bf",
- "sha256:45dcf8758873e3f69feab075e5f3177270739f146255225474ee0b90429adef6",
- "sha256:56a77d61a91186cc5676d8e11b36a5feb513873e4ae88d2ee5cf530d52bbcd3b",
- "sha256:5984e4947bbcef5bd849d6244aec507d31786f2dd3344139adc1489fb403b300",
- "sha256:6b0441da73796dd00821763bb4119674eaf252776beb50ae3883bed179a60b2a",
- "sha256:6f6677c5ade94d4fe75a912926d6796d5c71a2a90c2aeefe0d6f211d75c74789",
- "sha256:84a825a9418d7196e2acc48f8746cf1ee75877ed2f30433ab92a133f3eaf8fbe",
- "sha256:b44b373027a0c339cbca5be9c69723363a0989ccfbfca6e02ad5e01377fa13ee",
- "sha256:b842c34fe043ccf78b4a6cf1019d7b80113707d68c88842d061fa2b8fb6ddedc",
- "sha256:ca33d2f09dae149a1dcf942d2d825ebb06343b77b437198c9e2ef115cf5d5bc1",
- "sha256:db83b5c12c0cd30150bb568e6feb2435c49ce4e68fe2d7b903113f0e221e58fe",
- "sha256:e7466c95d49a0ad978444e9c4c61aa62f6da7336aecf2861b6d79d6d398928c3",
- "sha256:f50f3b1c5c1c1334ca7ce9cad5992f098f460ffd6388a3cabad10b66c2006b09",
- "sha256:f99f127909731cafb841c52f9216e447d3e4afb99b17bebfad327a75aee206de"
+ "sha256:0ba5d7626dbc4ce78971c3e62ec37f84c8139ea7008c008660d3312cf11e0db8",
+ "sha256:189b706f72e8b7ddc965168a79ff296ca5b7bdd95b5b05208afb9818a681c712",
+ "sha256:3017454b4b3ce4e4b2dc07f1e21e1bd6a41ad11b638997c2d7d493a656b4fd2c",
+ "sha256:340965444aafc7aac7e3586e930f5b3f8347ca9b350afab60bac84dcc0b94437",
+ "sha256:44fbc7b1786ab975ec9eba9da765398d58ec705d1c8e856b0523b8f9c1c53cf7",
+ "sha256:48d96b559fab3063feaebd316352e3418424629d59b77dbcb96ecc4c594d7f5f",
+ "sha256:5e32923c7896c49b1d3a327fe25a76363d200acdfa97844f5647f1bf9f298da8",
+ "sha256:6662442fbf22796dbd942bb15b664d70dcc25ae28d371b7e4ca6261e9bc495b7",
+ "sha256:6bb5d999faceee281bc4a2fc77866c61af7be4b7e5efadc930c42f234a99cafd",
+ "sha256:83b38b7b61b7c60af0fa03a71c27c4232117453a62ccf69a511284793a400751",
+ "sha256:90c22f4fd4e01279efc4e4911dafe308f35fcc4310bcb89bcee4d3ca20210d20",
+ "sha256:97b08853b9bb71512ed52381f05cf2d4179f4234825b505d8f8d2bb9d9429939",
+ "sha256:aef47082114428b47db73876ecb7751802548830ce5c95dba7ebe24d5e196d7c",
+ "sha256:b89ed3ba88ea5ec8b2c704a5ae747c9038ee1faff277fcddac75f850e645f7e1",
+ "sha256:be5afc2e1f5c320bd4a38e73d8b02c67d72dbee370a004732c923c7c8a472f72",
+ "sha256:ce8e1070dcff0c1149207ab2ee8c88738a5118e96fbf0fa4691659c83f5bb81f",
+ "sha256:d1c18853c7ad3c8e34edfafc6488fc24f4221c15b516c14796032cc53f8cde94",
+ "sha256:f4370d0e3d6e1ac2f80911651691ac540901f661b372036ea72637546ba98202"
],
"index": "pypi",
- "version": "==3.10.0"
+ "version": "==3.11.0"
},
"pycparser": {
"hashes": [
@@ -469,46 +470,46 @@
},
"pycryptodome": {
"hashes": [
- "sha256:0aa49f3fa110f8dc090bad1671a768cc17d3d3bd01566641ffc0d10d0fec8d49",
- "sha256:0fafd3c4fb76c6992f34bf2d074f582f388e3b8062b8ba5d65b020634cc221e6",
- "sha256:17eb9bd5d30a71b0c8a832e3e9cd2b7723f99907c38dc5dd23e59e8c368a70e2",
- "sha256:2776255d5c748782f095ec422d42da2eadd8392ac9de7da23db4aed4231272bd",
- "sha256:3500826dc3b9a8fdb762bebe551106081a6bdecd4181a3d1bd0206e48bba8974",
- "sha256:3aa0d30326dcdef24c632d5c03b8e4d379c6ae0645082b27dd69ea816bb97ecb",
- "sha256:3c7769bdadcc4809508e71997008912cc6d94fd7b5b1f3ef121683ebcac71d81",
- "sha256:3e8c97a38dac6dafd180b4696a522b1581dd1a8e0ea60763458be547bac97361",
- "sha256:5aca5125a46e458b308b5571ce8fe36d2229f161aa7db27b3ecacded70c6aa8b",
- "sha256:62beb75f0688f406946312bfef8923d8ab23f5b8013acded931413625299d317",
- "sha256:7725643de3c884a9945a086670787dce637037f32c5c2df7fd602bd5967f3486",
- "sha256:872191a02a0c2a3b98dc75c62b32912b220a8ae5ff6ac9e39868f903f55dd6a4",
- "sha256:8c501e80960d12328d49e1d409daf426f29364a37c602f257c99509999654650",
- "sha256:9512638bfef8ffc94c62751965a4733c3792104dc84771ba54ce0f80f49134df",
- "sha256:962043051afa7a5ab071b0d8996dc00e564327a18566d3e574a39cb6e097b462",
- "sha256:9db72b18b30902a83fa57b0d7dae4ce24f85186695e3bea0d423f1ec7c5b3fbe",
- "sha256:9ffd4f0bfb5949dfa0e5cedef836364f18da0deb2fba04671607fb3b59b29112",
- "sha256:a26819f693cf5fc0a2373a3e4b91c38e359cad9f00020a885b667c77f28738d5",
- "sha256:a3efc575a53511c48361d933e12e07c2eb940db1afda0995285176c372ab7352",
- "sha256:ababd6685b9d94729a851a0615482156afdacbeaabeea60f67961db0e975b1af",
- "sha256:b0e9c8c270cd3f8c73b53139f0708f257189a00bbc898be6d3f03995e5f7edc2",
- "sha256:b74173b13c221ee96b608212b9adc2c459a73d3632f04490df42e4f07e7041e6",
- "sha256:bed297f75ba19cefe2d10beb4959f4f8cb62c2560a3998ad87479485098ee939",
- "sha256:c639f09e8ce8ad5af9884233f952ade4b73a11b7d41d3b9bb7d4e64d9e1df164",
- "sha256:c7bc308be67288af1cd44668d59e36356f0ce518337899079ddb0235bd55db79",
- "sha256:cca152dcebc318833ba70499190ce17ee81b525404e2a7548c77f52b439306a7",
- "sha256:d5261d22bc3a54db26f11dabcda14bbaab72080977e083d795b4b1d1b510c774",
- "sha256:d81111e3da7fc9eee825ba7d8a68b3c1464f41110ef98a7280e0c7fb82c91e73",
- "sha256:d95fafa899abb9f82e55ff43f423e100784312b43932514f2c05d41cbb20323e",
- "sha256:de411a64d4105d4424441833bd25943208e58c846abf981bba5bbeeba88a49c3",
- "sha256:e02c7b3d05b88ff1a236e49a252b2bf8444d3a1d04a056784af766c0909eba36",
- "sha256:fbafe9b01b717e0bfbc83cd740ff5bf5cdd3f208815be470ea203942b899bbdf"
- ],
- "version": "==3.9.1"
+ "sha256:042ae873baadd0c33b4d699a5c5b976ade3233a979d972f98ca82314632d868c",
+ "sha256:0502876279772b1384b660ccc91563d04490d562799d8e2e06b411e2d81128a9",
+ "sha256:2de33ed0a95855735d5a0fc0c39603314df9e78ee8bbf0baa9692fb46b3b8bbb",
+ "sha256:319e568baf86620b419d53063b18c216abf924875966efdfe06891b987196a45",
+ "sha256:4372ec7518727172e1605c0843cdc5375d4771e447b8148c787b860260aae151",
+ "sha256:48821950ffb9c836858d8fa09d7840b6df52eadd387a3c5acece55cb387743f9",
+ "sha256:4b9533d4166ca07abdd49ce9d516666b1df944997fe135d4b21ac376aa624aff",
+ "sha256:54456cf85130e01674d21fb1ab89ffccacb138a8ade88d72fa2b0ac898d2798b",
+ "sha256:56fdd0e425f1b8fd3a00b6d96351f86226674974814c50534864d0124d48871f",
+ "sha256:57b1b707363490c495ad0eeb38bd1b0e1697c497af25fad78d3a1ebf0477fd5b",
+ "sha256:5c485ed6e9718ebcaa81138fa70ace9c563d202b56a8cee119b4085b023931f5",
+ "sha256:63c103a22cbe9752f6ea9f1a0de129995bad91c4d03a66c67cffcf6ee0c9f1e1",
+ "sha256:68fab8455efcbfe87c5d75015476f9b606227ffe244d57bfd66269451706e899",
+ "sha256:6c2720696b10ae356040e888bde1239b8957fe18885ccf5e7b4e8dec882f0856",
+ "sha256:72166c2ac520a5dbd2d90208b9c279161ec0861662a621892bd52fb6ca13ab91",
+ "sha256:7c52308ac5b834331b2f107a490b2c27de024a229b61df4cdc5c131d563dfe98",
+ "sha256:87d8d85b4792ca5e730fb7a519fbc3ed976c59dcf79c5204589c59afd56b9926",
+ "sha256:896e9b6fd0762aa07b203c993fbbee7a1f1a4674c6886afd7bfa86f3d1be98a8",
+ "sha256:8a799bea3c6617736e914a2e77c409f52893d382f619f088f8a80e2e21f573c1",
+ "sha256:9d9945ac8375d5d8e60bd2a2e1df5882eaa315522eedf3ca868b1546dfa34eba",
+ "sha256:9ef966c727de942de3e41aa8462c4b7b4bca70f19af5a3f99e31376589c11aac",
+ "sha256:a168e73879619b467072509a223282a02c8047d932a48b74fbd498f27224aa04",
+ "sha256:a30f501bbb32e01a49ef9e09ca1260e5ab49bf33a257080ec553e08997acc487",
+ "sha256:a8ca2450394d3699c9f15ef25e8de9a24b401933716a1e39d37fa01f5fe3c58b",
+ "sha256:aec4d42deb836b8fb3ba32f2ba1ef0d33dd3dc9d430b1479ee7a914490d15b5e",
+ "sha256:b4af098f2a50f8d048ab12cabb59456585c0acf43d90ee79782d2d6d0ed59dba",
+ "sha256:b55c60c321ac91945c60a40ac9896ac7a3d432bb3e8c14006dfd82ad5871c331",
+ "sha256:c53348358408d94869059e16fba5ff3bef8c52c25b18421472aba272b9bb450f",
+ "sha256:cbfd97f9e060f0d30245cd29fa267a9a84de9da97559366fca0a3f7655acc63f",
+ "sha256:d3fe3f33ad52bf0c19ee6344b695ba44ffbfa16f3c29ca61116b48d97bd970fb",
+ "sha256:e3a79a30d15d9c7c284a7734036ee8abdb5ca3a6f5774d293cdc9e1358c1dc10",
+ "sha256:eec0689509389f19875f66ae8dedd59f982240cdab31b9f78a8dc266011df93a"
+ ],
+ "version": "==3.9.4"
},
"pyrsistent": {
"hashes": [
- "sha256:eb6545dbeb1aa69ab1fb4809bfbf5a8705e44d92ef8fc7c2361682a47c46c778"
+ "sha256:f3b280d030afb652f79d67c5586157c5c1355c9a58dfc7940566e28d28f3df1b"
],
- "version": "==0.15.5"
+ "version": "==0.15.6"
},
"python-dotenv": {
"hashes": [
@@ -566,10 +567,10 @@
},
"urllib3": {
"hashes": [
- "sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",
- "sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"
+ "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293",
+ "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"
],
- "version": "==1.25.6"
+ "version": "==1.25.7"
},
"varint": {
"hashes": [
@@ -641,13 +642,6 @@
}
},
"develop": {
- "atomicwrites": {
- "hashes": [
- "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
- "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
- ],
- "version": "==1.3.0"
- },
"attrs": {
"hashes": [
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
@@ -655,12 +649,28 @@
],
"version": "==19.3.0"
},
+ "beautifulsoup4": {
+ "hashes": [
+ "sha256:5279c36b4b2ec2cb4298d723791467e3000e5384a43ea0cdf5d45207c7e97169",
+ "sha256:6135db2ba678168c07950f9a16c4031822c6f4aec75a65e0a97bc5ca09789931",
+ "sha256:6df940fdfaa9dec142736cfd93db5e6409d5fa2d36c3fa7d360c5f8f88d582d1",
+ "sha256:dcdef580e18a76d54002088602eba453eec38ebbcafafeaabd8cab12b6155d57"
+ ],
+ "version": "==4.8.1"
+ },
+ "bs4": {
+ "hashes": [
+ "sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a"
+ ],
+ "index": "pypi",
+ "version": "==0.0.1"
+ },
"certifi": {
"hashes": [
- "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50",
- "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"
+ "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
+ "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
],
- "version": "==2019.9.11"
+ "version": "==2019.11.28"
},
"click": {
"hashes": [
@@ -670,6 +680,13 @@
"index": "pypi",
"version": "==7.0"
},
+ "cloudpickle": {
+ "hashes": [
+ "sha256:922401d7140e133253ff5fab4faa4a1166416066453a783b00b507dca93f8859",
+ "sha256:f3ef2c9d438f1553ce7795afb18c1f190d8146132496169ef6aa9b7b65caa4c3"
+ ],
+ "version": "==1.2.2"
+ },
"coverage": {
"hashes": [
"sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6",
@@ -707,6 +724,20 @@
],
"version": "==4.5.4"
},
+ "cycler": {
+ "hashes": [
+ "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d",
+ "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8"
+ ],
+ "version": "==0.10.0"
+ },
+ "decorator": {
+ "hashes": [
+ "sha256:54c38050039232e1db4ad7375cfce6748d7b41c29e95a081c8a6d2c30364a2ce",
+ "sha256:5d19b92a3c8f7f101c8dd86afd86b0f061a8ce4540ab8cd401fa2542756bce6d"
+ ],
+ "version": "==4.4.1"
+ },
"entrypoints": {
"hashes": [
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
@@ -738,17 +769,38 @@
"index": "pypi",
"version": "==1.5.0"
},
+ "future": {
+ "hashes": [
+ "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
+ ],
+ "version": "==0.18.2"
+ },
+ "gym": {
+ "hashes": [
+ "sha256:3b930cbe1c76bbd30455b5e82ba723dea94159a5f988e927f443324bf7cc7ddd"
+ ],
+ "index": "pypi",
+ "version": "==0.15.4"
+ },
"htmlmin": {
"hashes": [
"sha256:50c1ef4630374a5d723900096a961cff426dff46b48f34d194a81bbe14eca178"
],
"version": "==0.1.12"
},
+ "imageio": {
+ "hashes": [
+ "sha256:c9763e5c187ecf74091c845626b0bdcc6130a20a0de7a86ae0108e2b5335ed3f",
+ "sha256:f44eb231b9df485874f2ffd22dfd0c3c711e7de076516b9374edea5c65bc67ae"
+ ],
+ "version": "==2.6.1"
+ },
"importlib-metadata": {
"hashes": [
"sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26",
"sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"
],
+ "markers": "python_version < '3.8'",
"version": "==0.23"
},
"jinja2": {
@@ -764,6 +816,48 @@
],
"version": "==2.2.2"
},
+ "kiwisolver": {
+ "hashes": [
+ "sha256:05b5b061e09f60f56244adc885c4a7867da25ca387376b02c1efc29cc16bcd0f",
+ "sha256:210d8c39d01758d76c2b9a693567e1657ec661229bc32eac30761fa79b2474b0",
+ "sha256:26f4fbd6f5e1dabff70a9ba0d2c4bd30761086454aa30dddc5b52764ee4852b7",
+ "sha256:3b15d56a9cd40c52d7ab763ff0bc700edbb4e1a298dc43715ecccd605002cf11",
+ "sha256:3b2378ad387f49cbb328205bda569b9f87288d6bc1bf4cd683c34523a2341efe",
+ "sha256:400599c0fe58d21522cae0e8b22318e09d9729451b17ee61ba8e1e7c0346565c",
+ "sha256:47b8cb81a7d18dbaf4fed6a61c3cecdb5adec7b4ac292bddb0d016d57e8507d5",
+ "sha256:53eaed412477c836e1b9522c19858a8557d6e595077830146182225613b11a75",
+ "sha256:58e626e1f7dfbb620d08d457325a4cdac65d1809680009f46bf41eaf74ad0187",
+ "sha256:5a52e1b006bfa5be04fe4debbcdd2688432a9af4b207a3f429c74ad625022641",
+ "sha256:5c7ca4e449ac9f99b3b9d4693debb1d6d237d1542dd6a56b3305fe8a9620f883",
+ "sha256:682e54f0ce8f45981878756d7203fd01e188cc6c8b2c5e2cf03675390b4534d5",
+ "sha256:76275ee077772c8dde04fb6c5bc24b91af1bb3e7f4816fd1852f1495a64dad93",
+ "sha256:79bfb2f0bd7cbf9ea256612c9523367e5ec51d7cd616ae20ca2c90f575d839a2",
+ "sha256:7f4dd50874177d2bb060d74769210f3bce1af87a8c7cf5b37d032ebf94f0aca3",
+ "sha256:8944a16020c07b682df861207b7e0efcd2f46c7488619cb55f65882279119389",
+ "sha256:8aa7009437640beb2768bfd06da049bad0df85f47ff18426261acecd1cf00897",
+ "sha256:9105ce82dcc32c73eb53a04c869b6a4bc756b43e4385f76ea7943e827f529e4d",
+ "sha256:933df612c453928f1c6faa9236161a1d999a26cd40abf1dc5d7ebbc6dbfb8fca",
+ "sha256:939f36f21a8c571686eb491acfffa9c7f1ac345087281b412d63ea39ca14ec4a",
+ "sha256:9491578147849b93e70d7c1d23cb1229458f71fc79c51d52dce0809b2ca44eea",
+ "sha256:9733b7f64bd9f807832d673355f79703f81f0b3e52bfce420fc00d8cb28c6a6c",
+ "sha256:a02f6c3e229d0b7220bd74600e9351e18bc0c361b05f29adae0d10599ae0e326",
+ "sha256:a0c0a9f06872330d0dd31b45607197caab3c22777600e88031bfe66799e70bb0",
+ "sha256:aa716b9122307c50686356cfb47bfbc66541868078d0c801341df31dca1232a9",
+ "sha256:acc4df99308111585121db217681f1ce0eecb48d3a828a2f9bbf9773f4937e9e",
+ "sha256:b64916959e4ae0ac78af7c3e8cef4becee0c0e9694ad477b4c6b3a536de6a544",
+ "sha256:d22702cadb86b6fcba0e6b907d9f84a312db9cd6934ee728144ce3018e715ee1",
+ "sha256:d3fcf0819dc3fea58be1fd1ca390851bdb719a549850e708ed858503ff25d995",
+ "sha256:d52e3b1868a4e8fd18b5cb15055c76820df514e26aa84cc02f593d99fef6707f",
+ "sha256:db1a5d3cc4ae943d674718d6c47d2d82488ddd94b93b9e12d24aabdbfe48caee",
+ "sha256:e3a21a720791712ed721c7b95d433e036134de6f18c77dbe96119eaf7aa08004",
+ "sha256:e8bf074363ce2babeb4764d94f8e65efd22e6a7c74860a4f05a6947afc020ff2",
+ "sha256:f16814a4a96dc04bf1da7d53ee8d5b1d6decfc1a92a63349bb15d37b6a263dd9",
+ "sha256:f2b22153870ca5cf2ab9c940d7bc38e8e9089fa0f7e5856ea195e1cf4ff43d5a",
+ "sha256:f790f8b3dff3d53453de6a7b7ddd173d2e020fb160baff578d578065b108a05f",
+ "sha256:fe51b79da0062f8e9d49ed0182a626a7dc7a0cbca0328f612c6ee5e4711c81e4"
+ ],
+ "version": "==1.1.0"
+ },
"livereload": {
"hashes": [
"sha256:78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b",
@@ -811,6 +905,24 @@
],
"version": "==1.1.1"
},
+ "matplotlib": {
+ "hashes": [
+ "sha256:08ccc8922eb4792b91c652d3e6d46b1c99073f1284d1b6705155643e8046463a",
+ "sha256:161dcd807c0c3232f4dcd4a12a382d52004a498174cbfafd40646106c5bcdcc8",
+ "sha256:1f9e885bfa1b148d16f82a6672d043ecf11197f6c71ae222d0546db706e52eb2",
+ "sha256:2d6ab54015a7c0d727c33e36f85f5c5e4172059efdd067f7527f6e5d16ad01aa",
+ "sha256:5d2e408a2813abf664bd79431107543ecb449136912eb55bb312317edecf597e",
+ "sha256:61c8b740a008218eb604de518eb411c4953db0cb725dd0b32adf8a81771cab9e",
+ "sha256:80f10af8378fccc136da40ea6aa4a920767476cdfb3241acb93ef4f0465dbf57",
+ "sha256:819d4860315468b482f38f1afe45a5437f60f03eaede495d5ff89f2eeac89500",
+ "sha256:8cc0e44905c2c8fda5637cad6f311eb9517017515a034247ab93d0cf99f8bb7a",
+ "sha256:8e8e2c2fe3d873108735c6ee9884e6f36f467df4a143136209cff303b183bada",
+ "sha256:98c2ffeab8b79a4e3a0af5dd9939f92980eb6e3fec10f7f313df5f35a84dacab",
+ "sha256:d59bb0e82002ac49f4152963f8a1079e66794a4f454457fd2f0dcc7bf0797d30",
+ "sha256:ee59b7bb9eb75932fe3787e54e61c99b628155b0cedc907864f24723ba55b309"
+ ],
+ "version": "==3.1.2"
+ },
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
@@ -828,11 +940,16 @@
},
"mkdocs-material": {
"hashes": [
- "sha256:a5246b550299d00a135a3f739e70ac6db73d7127480f0fecbda113d0095a674a",
- "sha256:e4a9ac73db7c65fdae1dbd248091e4b0a3f5db3e6bf87a46bb457db013a045e4"
+ "sha256:41e8041e1dba6b6fa6624e21d73c1c700f53e60400429a4c7ee10b769b5136b0",
+ "sha256:e0ccb48f21e671acc2f25e37ff1ea7aba0bcfe799393a7c687a97e07f9cb8528"
],
"index": "pypi",
- "version": "==4.4.3"
+ "version": "==4.5.0"
+ },
+ "mkdocs-mermaid-plugin": {
+ "editable": true,
+ "git": "https://github.com/pugong/mkdocs-mermaid-plugin.git",
+ "ref": "abf14392b0ed0c7022210b32c4e7c9c3e4c5c68a"
},
"mkdocs-minify-plugin": {
"hashes": [
@@ -875,6 +992,71 @@
],
"version": "==0.4.3"
},
+ "networkx": {
+ "hashes": [
+ "sha256:cdfbf698749a5014bf2ed9db4a07a5295df1d3a53bf80bf3cbd61edf9df05fa1",
+ "sha256:f8f4ff0b6f96e4f9b16af6b84622597b5334bf9cae8cf9b2e42e7985d5c95c64"
+ ],
+ "version": "==2.4"
+ },
+ "numpy": {
+ "hashes": [
+ "sha256:0a7a1dd123aecc9f0076934288ceed7fd9a81ba3919f11a855a7887cbe82a02f",
+ "sha256:0c0763787133dfeec19904c22c7e358b231c87ba3206b211652f8cbe1241deb6",
+ "sha256:3d52298d0be333583739f1aec9026f3b09fdfe3ddf7c7028cb16d9d2af1cca7e",
+ "sha256:43bb4b70585f1c2d153e45323a886839f98af8bfa810f7014b20be714c37c447",
+ "sha256:475963c5b9e116c38ad7347e154e5651d05a2286d86455671f5b1eebba5feb76",
+ "sha256:64874913367f18eb3013b16123c9fed113962e75d809fca5b78ebfbb73ed93ba",
+ "sha256:683828e50c339fc9e68720396f2de14253992c495fdddef77a1e17de55f1decc",
+ "sha256:6ca4000c4a6f95a78c33c7dadbb9495c10880be9c89316aa536eac359ab820ae",
+ "sha256:75fd817b7061f6378e4659dd792c84c0b60533e867f83e0d1e52d5d8e53df88c",
+ "sha256:7d81d784bdbed30137aca242ab307f3e65c8d93f4c7b7d8f322110b2e90177f9",
+ "sha256:8d0af8d3664f142414fd5b15cabfd3b6cc3ef242a3c7a7493257025be5a6955f",
+ "sha256:9679831005fb16c6df3dd35d17aa31dc0d4d7573d84f0b44cc481490a65c7725",
+ "sha256:a8f67ebfae9f575d85fa859b54d3bdecaeece74e3274b0b5c5f804d7ca789fe1",
+ "sha256:acbf5c52db4adb366c064d0b7c7899e3e778d89db585feadd23b06b587d64761",
+ "sha256:ada4805ed51f5bcaa3a06d3dd94939351869c095e30a2b54264f5a5004b52170",
+ "sha256:c7354e8f0eca5c110b7e978034cd86ed98a7a5ffcf69ca97535445a595e07b8e",
+ "sha256:e2e9d8c87120ba2c591f60e32736b82b67f72c37ba88a4c23c81b5b8fa49c018",
+ "sha256:e467c57121fe1b78a8f68dd9255fbb3bb3f4f7547c6b9e109f31d14569f490c3",
+ "sha256:ede47b98de79565fcd7f2decb475e2dcc85ee4097743e551fe26cfc7eb3ff143",
+ "sha256:f58913e9227400f1395c7b800503ebfdb0772f1c33ff8cb4d6451c06cabdf316",
+ "sha256:fe39f5fd4103ec4ca3cb8600b19216cd1ff316b4990f4c0b6057ad982c0a34d5"
+ ],
+ "index": "pypi",
+ "version": "==1.17.4"
+ },
+ "opencv-python": {
+ "hashes": [
+ "sha256:04bec0a6d3a00360a7fb769b755ff4489a4ac8291821b785151f63e6d8bb59ea",
+ "sha256:1a2d1801c038f055852bd2379186ca8b19b4ea24afb0b8410293bc802211579b",
+ "sha256:1c7d235faef511aca7669f1aa650897b6c058dfde6412ea3fc58feb0fce78814",
+ "sha256:22c2ee5f97f85903bfb28c056566b2ecaa1d2f804b880ab39ebf94528a402992",
+ "sha256:25127990671dc8bd27ae8b880d7a39f9aae863052a8fbebe8977c6ce8e5fc0c9",
+ "sha256:3cef82b6a1f748d2f4527f5932a86d54ebd10bd89f6cf59b003c36b1015055f7",
+ "sha256:499a0413e7110a934ab56e635252a4c86f8be64de59f94a62318a7b895dc809e",
+ "sha256:5f2cf5a0ab244a0a1dbe5ec426c277b55e06ac6a472ad61be77ef643a238cbd3",
+ "sha256:5fec35916a6b9ce935f2e2806084303fd4e3fbb0c973a8db8f54b5aca54613cb",
+ "sha256:6183c9c7fab4590e0651bc941cde780988c3ad9889bd62de19d581a6f59523ea",
+ "sha256:67a236db8db84d7fb0f6e127f360ce6669350ef324839132e22879ec90588dab",
+ "sha256:6c32d36f52a6e0c02d1ab0bb95223cb4dd5525a7e8292a747116126b3d34c578",
+ "sha256:73a467a78ffd902d2c0265ab6b2e2cdda423d61b3d08685e0c7d0b4572142ff1",
+ "sha256:76de8a247970d150b1672c6646cda91217d562682e713721fc9b9bf1434553c4",
+ "sha256:919d5c3ec1a62258ba8c68b869b1056186e2355c4474739b199c295547e66cc1",
+ "sha256:982d4e80c14356098cde57a6c7d18fe0928a1c3118675bac2252ef38f152e1ab",
+ "sha256:9d025e6bf2989bcbc7744c26d8bd90c2629a92d8de3ba2416f62ce2a94615dd9",
+ "sha256:bb59f98205cd81e29f45eed043cf0f98531486dc0b3f671c9e06fecf08f7ccef",
+ "sha256:c8119248457e909dcd7b598621ed1d139419d69377e8cb4e2b2c49c819de287d",
+ "sha256:ce7b1f25be04b04f2e678b2bf23a975137f77406dcee66a88a2daeb77cda3e76",
+ "sha256:d64428bf59ab4d27620b00a2ad6fea2b4d62016a17849c82a7517ec12db97d55",
+ "sha256:e2ffa3161b8662112f1880734e8b9549d0c9e818e59f652a9d1c5bf31e36586a",
+ "sha256:e6fc00ac42c800fad5fb3927cfb9bf4e60bb3302cb9805f45b826d5d2546119a",
+ "sha256:e793df2e12093b3a01006b5b27f321e306193c7a5c9e2a6c8bf652e1ad2d6a86",
+ "sha256:eae543b3e9253ff702103333aabd87736b5ed5e46ab834d8e0b929f08f494dee",
+ "sha256:f0af656402b73ead2d9f593c2774c04b01e2d0c63e4f99e0dc2f3fde99be22b4"
+ ],
+ "version": "==4.1.2.30"
+ },
"packaging": {
"hashes": [
"sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47",
@@ -889,6 +1071,41 @@
],
"version": "==1.0"
},
+ "pillow": {
+ "hashes": [
+ "sha256:047d9473cf68af50ac85f8ee5d5f21a60f849bc17d348da7fc85711287a75031",
+ "sha256:0f66dc6c8a3cc319561a633b6aa82c44107f12594643efa37210d8c924fc1c71",
+ "sha256:12c9169c4e8fe0a7329e8658c7e488001f6b4c8e88740e76292c2b857af2e94c",
+ "sha256:248cffc168896982f125f5c13e9317c059f74fffdb4152893339f3be62a01340",
+ "sha256:27faf0552bf8c260a5cee21a76e031acaea68babb64daf7e8f2e2540745082aa",
+ "sha256:285edafad9bc60d96978ed24d77cdc0b91dace88e5da8c548ba5937c425bca8b",
+ "sha256:384b12c9aa8ef95558abdcb50aada56d74bc7cc131dd62d28c2d0e4d3aadd573",
+ "sha256:38950b3a707f6cef09cd3cbb142474357ad1a985ceb44d921bdf7b4647b3e13e",
+ "sha256:4aad1b88933fd6dc2846552b89ad0c74ddbba2f0884e2c162aa368374bf5abab",
+ "sha256:4ac6148008c169603070c092e81f88738f1a0c511e07bd2bb0f9ef542d375da9",
+ "sha256:4deb1d2a45861ae6f0b12ea0a786a03d19d29edcc7e05775b85ec2877cb54c5e",
+ "sha256:59aa2c124df72cc75ed72c8d6005c442d4685691a30c55321e00ed915ad1a291",
+ "sha256:5a47d2123a9ec86660fe0e8d0ebf0aa6bc6a17edc63f338b73ea20ba11713f12",
+ "sha256:5cc901c2ab9409b4b7ac7b5bcc3e86ac14548627062463da0af3b6b7c555a871",
+ "sha256:6c1db03e8dff7b9f955a0fb9907eb9ca5da75b5ce056c0c93d33100a35050281",
+ "sha256:7ce80c0a65a6ea90ef9c1f63c8593fcd2929448613fc8da0adf3e6bfad669d08",
+ "sha256:809c19241c14433c5d6135e1b6c72da4e3b56d5c865ad5736ab99af8896b8f41",
+ "sha256:83792cb4e0b5af480588601467c0764242b9a483caea71ef12d22a0d0d6bdce2",
+ "sha256:846fa202bd7ee0f6215c897a1d33238ef071b50766339186687bd9b7a6d26ac5",
+ "sha256:9f5529fc02009f96ba95bea48870173426879dc19eec49ca8e08cd63ecd82ddb",
+ "sha256:a423c2ea001c6265ed28700df056f75e26215fd28c001e93ef4380b0f05f9547",
+ "sha256:ac4428094b42907aba5879c7c000d01c8278d451a3b7cccd2103e21f6397ea75",
+ "sha256:b1ae48d87f10d1384e5beecd169c77502fcc04a2c00a4c02b85f0a94b419e5f9",
+ "sha256:bf4e972a88f8841d8fdc6db1a75e0f8d763e66e3754b03006cbc3854d89f1cb1",
+ "sha256:c6414f6aad598364aaf81068cabb077894eb88fed99c6a65e6e8217bab62ae7a",
+ "sha256:c710fcb7ee32f67baf25aa9ffede4795fd5d93b163ce95fdc724383e38c9df96",
+ "sha256:c7be4b8a09852291c3c48d3c25d1b876d2494a0a674980089ac9d5e0d78bd132",
+ "sha256:c9e5ffb910b14f090ac9c38599063e354887a5f6d7e6d26795e916b4514f2c1a",
+ "sha256:e0697b826da6c2472bb6488db4c0a7fa8af0d52fa08833ceb3681358914b14e5",
+ "sha256:e9a3edd5f714229d41057d56ac0f39ad9bdba6767e8c888c951869f0bdd129b0"
+ ],
+ "version": "==6.2.1"
+ },
"pipenv": {
"hashes": [
"sha256:56ad5f5cb48f1e58878e14525a6e3129d4306049cb76d2f6a3e95df0d5fc6330",
@@ -899,10 +1116,10 @@
},
"pluggy": {
"hashes": [
- "sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6",
- "sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34"
+ "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
+ "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
],
- "version": "==0.13.0"
+ "version": "==0.13.1"
},
"py": {
"hashes": [
@@ -936,36 +1153,51 @@
],
"version": "==2.1.1"
},
+ "pyglet": {
+ "hashes": [
+ "sha256:8b07aea16f34ac861cffd06a0c17723ca944d172e577b57b21859b7990709a66",
+ "sha256:b00570e7cdf6971af8953b6ece50d83d13272afa5d1f1197c58c0f478dd17743"
+ ],
+ "version": "==1.3.2"
+ },
"pygments": {
"hashes": [
- "sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127",
- "sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"
+ "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b",
+ "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"
],
"index": "pypi",
- "version": "==2.4.2"
+ "version": "==2.5.2"
},
"pymdown-extensions": {
"hashes": [
- "sha256:24c1a0afbae101c4e2b2675ff4dd936470a90133f93398b9cbe0c855e2d2ec10",
- "sha256:960486dea995f1759dfd517aa140b3d851cd7b44d4c48d276fd2c74fc4e1bce9"
+ "sha256:27953f071d37b63d418738f75d847d824c0e4430e93f085cfdd9f8dc08a8c5c3",
+ "sha256:328b9e114925729e0789558a94325be8e7ca9e0323ed2a2b705d9bc1de4d2716"
],
"index": "pypi",
- "version": "==6.1"
+ "version": "==6.2"
},
"pyparsing": {
"hashes": [
- "sha256:4acadc9a2b96c19fe00932a38ca63e601180c39a189a696abce1eaab641447e1",
- "sha256:61b5ed888beab19ddccab3478910e2076a6b5a0295dffc43021890e136edf764"
+ "sha256:20f995ecd72f2a1f4bf6b072b63b22e2eb457836601e76d6e5dfcd75436acc1f",
+ "sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a"
],
- "version": "==2.4.4"
+ "version": "==2.4.5"
},
"pytest": {
"hashes": [
- "sha256:27abc3fef618a01bebb1f0d6d303d2816a99aa87a5968ebc32fe971be91eb1e6",
- "sha256:58cee9e09242937e136dbb3dab466116ba20d6b7828c7620f23947f37eb4dae4"
+ "sha256:63344a2e3bce2e4d522fd62b4fdebb647c019f1f9e4ca075debbd13219db4418",
+ "sha256:f67403f33b2b1d25a6756184077394167fe5e2f9d8bdaab30707d19ccec35427"
],
"index": "pypi",
- "version": "==5.2.2"
+ "version": "==5.3.1"
+ },
+ "pytest-asyncio": {
+ "hashes": [
+ "sha256:9fac5100fd716cbecf6ef89233e8590a4ad61d729d1732e0a96b84182df1daaf",
+ "sha256:d734718e25cfc32d2bf78d346e99d33724deeba774cc4afdf491530c6184b63b"
+ ],
+ "index": "pypi",
+ "version": "==0.10.0"
},
"pytest-cov": {
"hashes": [
@@ -975,6 +1207,39 @@
"index": "pypi",
"version": "==2.8.1"
},
+ "python-dateutil": {
+ "hashes": [
+ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
+ "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
+ ],
+ "version": "==2.8.1"
+ },
+ "pywavelets": {
+ "hashes": [
+ "sha256:076ca8907001fdfe4205484f719d12b4a0262dfe6652fa1cfc3c5c362d14dc84",
+ "sha256:18a51b3f9416a2ae6e9a35c4af32cf520dd7895f2b69714f4aa2f4342fca47f9",
+ "sha256:1a64b40f6acb4ffbaccce0545d7fc641744f95351f62e4c6aaa40549326008c9",
+ "sha256:35959c041ec014648575085a97b498eafbbaa824f86f6e4a59bfdef8a3fe6308",
+ "sha256:55e39ec848ceec13c9fa1598253ae9dd5c31d09dfd48059462860d2b908fb224",
+ "sha256:6162dc0ae04669ea04b4b51420777b9ea2d30b0a9d02901b2a3b4d61d159c2e9",
+ "sha256:68b5c33741d26c827074b3d8f0251de1c3019bb9567b8d303eb093c822ce28f1",
+ "sha256:720dbcdd3d91c6dfead79c80bf8b00a1d8aa4e5d551dc528c6d5151e4efc3403",
+ "sha256:7947e51ca05489b85928af52a34fe67022ab5b81d4ae32a4109a99e883a0635e",
+ "sha256:79f5b54f9dc353e5ee47f0c3f02bebd2c899d49780633aa771fed43fa20b3149",
+ "sha256:80b924edbc012ded8aa8b91cb2fd6207fb1a9a3a377beb4049b8a07445cec6f0",
+ "sha256:889d4c5c5205a9c90118c1980df526857929841df33e4cd1ff1eff77c6817a65",
+ "sha256:935ff247b8b78bdf77647fee962b1cc208c51a7b229db30b9ba5f6da3e675178",
+ "sha256:98b2669c5af842a70cfab33a7043fcb5e7535a690a00cd251b44c9be0be418e5",
+ "sha256:9e2528823ccf5a0a1d23262dfefe5034dce89cd84e4e124dc553dfcdf63ebb92",
+ "sha256:bc5e87b72371da87c9bebc68e54882aada9c3114e640de180f62d5da95749cd3",
+ "sha256:be105382961745f88d8196bba5a69ee2c4455d87ad2a2e5d1eed6bd7fda4d3fd",
+ "sha256:c06d2e340c7bf8b9ec71da2284beab8519a3908eab031f4ea126e8ccfc3fd567",
+ "sha256:cfe79844526dd92e3ecc9490b5031fca5f8ab607e1e858feba232b1b788ff0ea",
+ "sha256:d510aef84d9852653d079c84f2f81a82d5d09815e625f35c95714e7364570ad4",
+ "sha256:e02a0558e0c2ac8b8bbe6a6ac18c136767ec56b96a321e0dfde2173adfa5a504"
+ ],
+ "version": "==1.1.1"
+ },
"pyyaml": {
"hashes": [
"sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9",
@@ -994,6 +1259,53 @@
"index": "pypi",
"version": "==5.1.2"
},
+ "scikit-image": {
+ "hashes": [
+ "sha256:063d1c20fcd53762f82ee58c29783ae4e8f6fbed445b41b704fa33b6f355729d",
+ "sha256:0715b7940778ba5d73da3908d60ddf2eb93863f7c394493a522fe56d3859295c",
+ "sha256:0808ab5f8218d91a1c008036993636535a37efd67a52ab0f2e6e3f4b7e75aeda",
+ "sha256:2a54bea469eb1b611bee1ce36e60710f5f94f29205bc5bd67a51793909b1e62b",
+ "sha256:2aa962aa82d815606d7dad7f045f5d7ca55c65b4320d47e15a98fc92612c2d6c",
+ "sha256:2d346d49b6852cffb47cbde995e2696d5b07f688d8c057a0a4548abf3a98f920",
+ "sha256:3ad2efa792ab8de5fcefe6f4f5bc1ab64c411cdb5c829ce1526ab3a5a7729627",
+ "sha256:3af3d781ce085573ced37b2b5b9abfd32ce3d4723bd17f37e829025d189b0421",
+ "sha256:41e28db0136f29ecd305bef0408fdfc64be9d415e54f5099a95555c65f5c1865",
+ "sha256:6786b127f33470fd843e644435522fbf43bce05c9f5527946c390ccb9e1cac27",
+ "sha256:8b2b768b02c6b7476f2e16ddd91f827d3817aef73f82cf28bff7a8dcdfd8c55c",
+ "sha256:a48fb0d34a090b578b87ffebab0fe035295c1945dbc2b28e1a55ea2cf6031751",
+ "sha256:dd7fbd32da74d4e9967dc15845f731f16e7966cee61f5dc0e12e2abb1305068c",
+ "sha256:e18d73cc8893e2268b172c29f9aab530faf8cd3b7c11ae0bee3e763d719d35c5",
+ "sha256:e774377876cb258e8f4d63f7809863f961c98aa02263b3ff54a39483bc6f7d26"
+ ],
+ "index": "pypi",
+ "version": "==0.16.2"
+ },
+ "scipy": {
+ "hashes": [
+ "sha256:0b8c9dc042b9a47912b18b036b4844029384a5b8d89b64a4901ac3e06876e5f6",
+ "sha256:18ad034be955df046b5a27924cdb3db0e8e1d76aaa22c635403fe7aee17f1482",
+ "sha256:225d0b5e140bb66df23d438c7b535303ce8e533f94454f4e5bde5f8d109103ea",
+ "sha256:2f690ba68ed7caa7c30b6dc48c1deed22c78f3840fa4736083ef4f2bd8baa19e",
+ "sha256:4b8746f4a755bdb2eeb39d6e253a60481e165cfd74fdfb54d27394bd2c9ec8ac",
+ "sha256:4ba2ce1a58fe117e993cf316a149cf9926c7c5000c0cdc4bc7c56ae8325612f6",
+ "sha256:546f0dc020b155b8711159d53c87b36591d31f3327c47974a4fb6b50d91589c2",
+ "sha256:583f2ccd6a112656c9feb2345761d2b19e9213a094cfced4e7d2c1cae4173272",
+ "sha256:64bf4e8ae0db2d42b58477817f648d81e77f0b381d0ea4427385bba3f959380a",
+ "sha256:7be424ee09bed7ced36c9457f99c826ce199fd0c0f5b272cf3d098ff7b29e3ae",
+ "sha256:869465c7ff89fc0a1e2ea1642b0c65f1b3c05030f3a4c0d53d6a57b2dba7c242",
+ "sha256:884e619821f47eccd42979488d10fa1e15dbe9f3b7660b1c8c928d203bd3c1a3",
+ "sha256:a42b0d02150ef4747e225c31c976a304de5dc8202ec35a27111b7bb8176e5f13",
+ "sha256:a70308bb065562afb936c963780deab359966d71ab4f230368b154dde3136ea4",
+ "sha256:b01ea5e4cf95a93dc335089f8fbe97852f56fdb74afff238cbdf09793103b6b7",
+ "sha256:b7b8cf45f9a48f23084f19deb9384a1cccb5e92fbc879b12f97dc4d56fb2eb92",
+ "sha256:bb0899d3f8b9fe8ef95b79210cf0deb6709542889fadaa438eeb3a28001e09e7",
+ "sha256:c008f1b58f99f1d1cc546957b3effe448365e0a217df1f1894e358906e91edad",
+ "sha256:cfee99d085d562a7e3c4afe51ac1fe9b434363489e565a130459307f30077973",
+ "sha256:dfcb0f0a2d8e958611e0b56536285bb435f03746b6feac0e29f045f7c6caf164",
+ "sha256:f5d47351aeb1cb6bda14a8908e56648926a6b2d714f89717c71f7ada41282141"
+ ],
+ "version": "==1.3.3"
+ },
"six": {
"hashes": [
"sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
@@ -1010,6 +1322,13 @@
],
"version": "==2.0.0"
},
+ "soupsieve": {
+ "hashes": [
+ "sha256:bdb0d917b03a1369ce964056fc195cfdff8819c40de04695a80bc813c3cfa1f5",
+ "sha256:e2c1c5dee4a1c36bcb790e0fabd5492d874b8ebd4617622c4f6a731701060dda"
+ ],
+ "version": "==1.9.5"
+ },
"toml": {
"hashes": [
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
@@ -1080,10 +1399,10 @@
},
"virtualenv": {
"hashes": [
- "sha256:11cb4608930d5fd3afb545ecf8db83fa50e1f96fc4fca80c94b07d2c83146589",
- "sha256:d257bb3773e48cac60e475a19b608996c73f4d333b3ba2e4e57d5ac6134e0136"
+ "sha256:116655188441670978117d0ebb6451eb6a7526f9ae0796cc0dee6bd7356909b0",
+ "sha256:b57776b44f91511866594e477dd10e76a6eb44439cdd7f06dcd30ba4c5bd854f"
],
- "version": "==16.7.7"
+ "version": "==16.7.8"
},
"virtualenv-clone": {
"hashes": [
diff --git a/aea/__version__.py b/aea/__version__.py
index c3a21af00a..2bb5df68ee 100644
--- a/aea/__version__.py
+++ b/aea/__version__.py
@@ -23,7 +23,7 @@
__title__ = 'aea'
__description__ = 'Autonomous Economic Agent framework'
__url__ = 'https://github.com/fetchai/agents-aea.git'
-__version__ = '0.1.13'
+__version__ = '0.1.14'
__author__ = 'Fetch.AI Limited'
__license__ = 'Apache 2.0'
__copyright__ = '2019 Fetch.AI Limited'
diff --git a/aea/aea.py b/aea/aea.py
index e970d2cf21..881ba84d58 100644
--- a/aea/aea.py
+++ b/aea/aea.py
@@ -19,14 +19,16 @@
"""This module contains the implementation of an Autonomous Economic Agent."""
import logging
-from typing import Optional, cast
+from asyncio import AbstractEventLoop
+from typing import Optional, cast, List
from aea.agent import Agent
+from aea.connections.base import Connection
from aea.context.base import AgentContext
from aea.crypto.ledger_apis import LedgerApis
from aea.crypto.wallet import Wallet
from aea.decision_maker.base import DecisionMaker
-from aea.mail.base import Envelope, MailBox
+from aea.mail.base import Envelope
from aea.registries.base import Filter, Resources
from aea.skills.error.handlers import ErrorHandler
@@ -37,10 +39,11 @@ class AEA(Agent):
"""This class implements an autonomous economic agent."""
def __init__(self, name: str,
- mailbox: MailBox,
+ connections: List[Connection],
wallet: Wallet,
ledger_apis: LedgerApis,
resources: Resources,
+ loop: Optional[AbstractEventLoop] = None,
timeout: float = 0.0,
debug: bool = False,
max_reactions: int = 20) -> None:
@@ -48,7 +51,8 @@ def __init__(self, name: str,
Instantiate the agent.
:param name: the name of the agent
- :param mailbox: the mailbox of the agent.
+ :param connections: the list of connections of the agent.
+ :param loop: the event loop to run the connections.
:param wallet: the wallet of the agent.
:param ledger_apis: the ledger apis of the agent.
:param resources: the resources of the agent.
@@ -58,11 +62,9 @@ def __init__(self, name: str,
:return: None
"""
- super().__init__(name=name, wallet=wallet, timeout=timeout, debug=debug)
+ super().__init__(name=name, wallet=wallet, connections=connections, loop=loop, timeout=timeout, debug=debug)
self.max_reactions = max_reactions
-
- self.mailbox = mailbox
self._decision_maker = DecisionMaker(self.name,
self.max_reactions,
self.outbox,
@@ -72,12 +74,12 @@ def __init__(self, name: str,
self.wallet.public_keys,
self.wallet.addresses,
ledger_apis,
- self.mailbox.connection_status,
+ self.multiplexer.connection_status,
self.outbox,
self.decision_maker.message_in_queue,
self.decision_maker.ownership_state,
self.decision_maker.preferences,
- self.decision_maker.is_ready_to_pursuit_goals)
+ self.decision_maker.goal_pursuit_readiness)
self._resources = resources
self._filter = Filter(self.resources, self.decision_maker.message_out_queue)
diff --git a/aea/agent.py b/aea/agent.py
index 48b9e6e064..e7de2d4e69 100644
--- a/aea/agent.py
+++ b/aea/agent.py
@@ -23,11 +23,13 @@
import logging
import time
from abc import abstractmethod, ABC
+from asyncio import AbstractEventLoop
from enum import Enum
-from typing import Optional
+from typing import Optional, List
+from aea.connections.base import Connection
from aea.crypto.wallet import Wallet
-from aea.mail.base import InBox, OutBox, MailBox
+from aea.mail.base import InBox, OutBox, Multiplexer
logger = logging.getLogger(__name__)
@@ -57,39 +59,49 @@ class Agent(ABC):
"""This class implements a template agent."""
def __init__(self, name: str,
+ connections: List[Connection],
wallet: Wallet,
+ loop: Optional[AbstractEventLoop] = None,
timeout: float = 1.0,
debug: bool = False) -> None:
"""
Instantiate the agent.
:param name: the name of the agent
+ :param connections: the list of connections of the agent.
:param wallet: the crypto wallet of the agent.
+ :param loop: the event loop to run the connections.
:param timeout: the time in (fractions of) seconds to time out an agent between act and react
:param debug: if True, run the agent in debug mode.
:return: None
"""
self._name = name
+ self._connections = connections
self._wallet = wallet
+
+ self._multiplexer = Multiplexer(self._connections, loop=loop)
+ self._inbox = InBox(self._multiplexer)
+ self._outbox = OutBox(self._multiplexer)
self._liveness = Liveness()
self._timeout = timeout
self.debug = debug
- self.mailbox = None # type: Optional[MailBox]
+ @property
+ def multiplexer(self) -> Multiplexer:
+ """Get the multiplexer."""
+ return self._multiplexer
@property
def inbox(self) -> InBox:
"""Get the inbox."""
- assert self.mailbox is not None, "Cannot retrieve inbox. No mailbox specified."
- return self.mailbox.inbox
+ return self._inbox
@property
def outbox(self) -> OutBox:
"""Get the outbox."""
- assert self.mailbox is not None, "Cannot retrieve outbox. No mailbox specified."
- return self.mailbox.outbox
+ return self._outbox
@property
def name(self) -> str:
@@ -119,11 +131,11 @@ def agent_state(self) -> AgentState:
:return the agent state.
:raises ValueError: if the state does not satisfy any of the foreseen conditions.
"""
- if self.mailbox is None or not self.mailbox.is_connected:
+ if self.multiplexer is not None and not self.multiplexer.connection_status.is_connected:
return AgentState.INITIATED
- elif self.mailbox.is_connected and self.liveness.is_stopped:
+ elif self.multiplexer.connection_status.is_connected and self.liveness.is_stopped:
return AgentState.CONNECTED
- elif self.mailbox.is_connected and not self.liveness.is_stopped:
+ elif self.multiplexer.connection_status.is_connected and not self.liveness.is_stopped:
return AgentState.RUNNING
else:
raise ValueError("Agent state not recognized.") # pragma: no cover
@@ -134,9 +146,8 @@ def start(self) -> None:
:return: None
"""
- assert self.mailbox is not None, "Cannot call start without mailbox instantiated."
- if not self.debug and not self.mailbox.is_connected:
- self.mailbox.connect()
+ if not self.debug and not self.multiplexer.connection_status.is_connected:
+ self.multiplexer.connect()
logger.debug("[{}]: Calling setup method...".format(self.name))
self.setup()
@@ -164,15 +175,14 @@ def stop(self) -> None:
:return: None
"""
- assert self.mailbox is not None, "Cannot call stop without mailbox instantiated."
- logger.debug("[{}]: Stopping message processing...".format(self.name))
self.liveness._is_stopped = True
- if self.mailbox.is_connected:
- self.mailbox.disconnect()
-
logger.debug("[{}]: Calling teardown method...".format(self.name))
self.teardown()
+ logger.debug("[{}]: Stopping message processing...".format(self.name))
+ if self.multiplexer.connection_status.is_connected:
+ self.multiplexer.disconnect()
+
@abstractmethod
def setup(self) -> None:
"""
diff --git a/aea/cli/add.py b/aea/cli/add.py
index ca29917f6b..b8ea94531c 100644
--- a/aea/cli/add.py
+++ b/aea/cli/add.py
@@ -33,30 +33,20 @@
from aea.cli.common import Context, pass_ctx, logger, _try_to_load_agent_config
from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE, DEFAULT_CONNECTION_CONFIG_FILE, DEFAULT_SKILL_CONFIG_FILE, \
DEFAULT_PROTOCOL_CONFIG_FILE
+from aea.cli.registry.utils import fetch_package, split_public_id
@click.group()
+@click.option('--registry', is_flag=True, help="For adding from Registry.")
@pass_ctx
-def add(ctx: Context):
+def add(ctx: Context, registry):
"""Add a resource to the agent."""
+ if registry:
+ ctx.set_config("is_registry", True)
_try_to_load_agent_config(ctx)
-@add.command()
-@click.argument('connection_name', type=str, required=True)
-@pass_context
-def connection(click_context, connection_name):
- """Add a connection to the configuration file."""
- ctx = cast(Context, click_context.obj)
- agent_name = ctx.agent_config.agent_name
- logger.debug("Adding connection {} to the agent {}...".format(connection_name, agent_name))
-
- # check if we already have a connection with the same name
- logger.debug("Connection already supported by the agent: {}".format(ctx.agent_config.connections))
- if connection_name in ctx.agent_config.connections:
- logger.error("A connection with name '{}' already exists. Aborting...".format(connection_name))
- sys.exit(1)
-
+def _find_connection_locally(ctx, connection_name):
# check that the provided path points to a proper connection directory -> look for connection.yaml file.
# first check in aea dir
registry_path = ctx.agent_config.registry_path
@@ -72,7 +62,7 @@ def connection(click_context, connection_name):
# try to load the connection configuration file
try:
connection_configuration = ctx.connection_loader.load(open(str(connection_configuration_filepath)))
- logger.info("Connection supports the following protocols: {}".format(connection_configuration.supported_protocols))
+ logger.info("Connection '{}' supports the following protocols: {}".format(connection_name, connection_configuration.restricted_to_protocols))
except ValidationError as e:
logger.error("Connection configuration file not valid: {}".format(str(e)))
sys.exit(1)
@@ -80,13 +70,44 @@ def connection(click_context, connection_name):
# copy the connection package into the agent's supported connections.
src = str(Path(os.path.join(registry_path, "connections", connection_name)).absolute())
dest = os.path.join(ctx.cwd, "connections", connection_name)
- logger.info("Copying connection modules. src={} dst={}".format(src, dest))
+ logger.debug("Copying connection modules. src={} dst={}".format(src, dest))
try:
shutil.copytree(src, dest)
except Exception as e:
logger.error(str(e))
sys.exit(1)
+
+@add.command()
+@click.argument(
+ 'connection_name', type=str, required=True
+)
+@pass_context
+def connection(click_context, connection_name):
+ """Add a connection to the configuration file."""
+ ctx = cast(Context, click_context.obj)
+ agent_name = ctx.agent_config.agent_name
+
+ is_registry = ctx.config.get("is_registry")
+ if is_registry:
+ public_id = str(connection_name)
+ connection_name = split_public_id(connection_name)[1]
+
+ logger.info("Adding connection '{}' to the agent '{}'...".format(connection_name, agent_name))
+
+ # check if we already have a connection with the same name
+ logger.debug("Connections already supported by the agent: {}".format(ctx.agent_config.connections))
+ if connection_name in ctx.agent_config.connections:
+ logger.error("A connection with name '{}' already exists. Aborting...".format(connection_name))
+ sys.exit(1)
+
+ # find and add connection
+ if is_registry:
+ # fetch from Registry
+ fetch_package('connection', public_id=public_id, cwd=ctx.cwd)
+ else:
+ _find_connection_locally(ctx, connection_name)
+
# make the 'connections' folder a Python package.
connections_init_module = os.path.join(ctx.cwd, "connections", "__init__.py")
logger.debug("Creating {}".format(connections_init_module))
@@ -98,21 +119,7 @@ def connection(click_context, connection_name):
ctx.agent_loader.dump(ctx.agent_config, open(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w"))
-@add.command()
-@click.argument('protocol_name', type=str, required=True)
-@pass_context
-def protocol(click_context, protocol_name):
- """Add a protocol to the agent."""
- ctx = cast(Context, click_context.obj)
- agent_name = cast(str, ctx.agent_config.agent_name)
- logger.debug("Adding protocol {} to the agent {}...".format(protocol_name, agent_name))
-
- # check if we already have a protocol with the same name
- logger.debug("Protocols already supported by the agent: {}".format(ctx.agent_config.protocols))
- if protocol_name in ctx.agent_config.protocols:
- logger.error("A protocol with name '{}' already exists. Aborting...".format(protocol_name))
- sys.exit(1)
-
+def _find_protocol_locally(ctx, protocol_name):
# check that the provided path points to a proper protocol directory -> look for protocol.yaml file.
# first check in aea dir
registry_path = ctx.agent_config.registry_path
@@ -128,7 +135,7 @@ def protocol(click_context, protocol_name):
# try to load the protocol configuration file
try:
protocol_configuration = ctx.protocol_loader.load(open(str(protocol_configuration_filepath)))
- logger.info("Protocol available: {}".format(protocol_configuration.name))
+ logger.debug("Protocol available: {}".format(protocol_configuration.name))
except ValidationError as e:
logger.error("Protocol configuration file not valid: {}".format(str(e)))
sys.exit(1)
@@ -136,13 +143,44 @@ def protocol(click_context, protocol_name):
# copy the protocol package into the agent's supported connections.
src = str(Path(os.path.join(registry_path, "protocols", protocol_name)).absolute())
dest = os.path.join(ctx.cwd, "protocols", protocol_name)
- logger.info("Copying protocol modules. src={} dst={}".format(src, dest))
+ logger.debug("Copying protocol modules. src={} dst={}".format(src, dest))
try:
shutil.copytree(src, dest)
except Exception as e:
logger.error(str(e))
sys.exit(1)
+
+@add.command()
+@click.argument(
+ 'protocol_name', type=str, required=True
+)
+@pass_context
+def protocol(click_context, protocol_name):
+ """Add a protocol to the agent."""
+ ctx = cast(Context, click_context.obj)
+ agent_name = cast(str, ctx.agent_config.agent_name)
+
+ is_registry = ctx.config.get("is_registry")
+ if is_registry:
+ public_id = str(protocol_name)
+ protocol_name = split_public_id(protocol_name)[1]
+
+ logger.info("Adding protocol '{}' to the agent '{}'...".format(protocol_name, agent_name))
+
+ # check if we already have a protocol with the same name
+ logger.debug("Protocols already supported by the agent: {}".format(ctx.agent_config.protocols))
+ if protocol_name in ctx.agent_config.protocols:
+ logger.error("A protocol with name '{}' already exists. Aborting...".format(protocol_name))
+ sys.exit(1)
+
+ # find and add protocol
+ if is_registry:
+ # fetch from Registry
+ fetch_package('protocol', public_id=public_id, cwd=ctx.cwd)
+ else:
+ _find_protocol_locally(ctx, protocol_name)
+
# make the 'protocols' folder a Python package.
logger.debug("Creating {}".format(os.path.join(agent_name, "protocols", "__init__.py")))
Path(os.path.join(ctx.cwd, "protocols", "__init__.py")).touch(exist_ok=True)
@@ -153,21 +191,7 @@ def protocol(click_context, protocol_name):
ctx.agent_loader.dump(ctx.agent_config, open(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w"))
-@add.command()
-@click.argument('skill_name', type=str, required=True)
-@pass_context
-def skill(click_context, skill_name):
- """Add a skill to the agent."""
- ctx = cast(Context, click_context.obj)
- agent_name = ctx.agent_config.agent_name
- logger.debug("Adding skill {} to the agent {}...".format(skill_name, agent_name))
-
- # check if we already have a skill with the same name
- logger.debug("Skills already supported by the agent: {}".format(ctx.agent_config.skills))
- if skill_name in ctx.agent_config.skills:
- logger.error("A skill with name '{}' already exists. Aborting...".format(skill_name))
- sys.exit(1)
-
+def _find_skill_locally(ctx, skill_name, click_context):
# check that the provided path points to a proper skill directory -> look for skill.yaml file.
# first check in aea dir
registry_path = ctx.agent_config.registry_path
@@ -190,24 +214,53 @@ def skill(click_context, skill_name):
# copy the skill package into the agent's supported skills.
src = str(Path(os.path.join(registry_path, "skills", skill_name)).absolute())
dest = os.path.join(ctx.cwd, "skills", skill_name)
- logger.info("Copying skill modules. src={} dst={}".format(src, dest))
+ logger.debug("Copying skill modules. src={} dst={}".format(src, dest))
try:
shutil.copytree(src, dest)
except Exception as e:
logger.error(str(e))
sys.exit(1)
- # make the 'skills' folder a Python package.
- skills_init_module = os.path.join(ctx.cwd, "skills", "__init__.py")
- logger.debug("Creating {}".format(skills_init_module))
- Path(skills_init_module).touch(exist_ok=True)
-
# check for not supported protocol, and add it.
for protocol_name in skill_configuration.protocols:
if protocol_name not in ctx.agent_config.protocols:
- logger.info("Adding protocol '{}' to the agent...".format(protocol_name))
+ logger.debug("Adding protocol '{}' to the agent...".format(protocol_name))
click_context.invoke(protocol, protocol_name=protocol_name)
+
+@add.command()
+@click.argument('skill_name', type=str, required=True)
+@pass_context
+def skill(click_context, skill_name):
+ """Add a skill to the agent."""
+ ctx = cast(Context, click_context.obj)
+ agent_name = ctx.agent_config.agent_name
+
+ is_registry = ctx.config.get("is_registry")
+ if is_registry:
+ public_id = str(skill_name)
+ skill_name = split_public_id(skill_name)[1]
+
+ logger.info("Adding skill '{}' to the agent '{}'...".format(skill_name, agent_name))
+
+ # check if we already have a skill with the same name
+ logger.debug("Skills already supported by the agent: {}".format(ctx.agent_config.skills))
+ if skill_name in ctx.agent_config.skills:
+ logger.error("A skill with name '{}' already exists. Aborting...".format(skill_name))
+ sys.exit(1)
+
+ # find and add protocol
+ if is_registry:
+ # fetch from Registry
+ fetch_package('skill', public_id=public_id, cwd=ctx.cwd)
+ else:
+ _find_skill_locally(ctx, skill_name, click_context)
+
+ # make the 'skills' folder a Python package.
+ skills_init_module = os.path.join(ctx.cwd, "skills", "__init__.py")
+ logger.debug("Creating {}".format(skills_init_module))
+ Path(skills_init_module).touch(exist_ok=True)
+
# add the skill to the configurations.
logger.debug("Registering the skill into {}".format(DEFAULT_AEA_CONFIG_FILE))
ctx.agent_config.skills.add(skill_name)
diff --git a/aea/cli/common.py b/aea/cli/common.py
index aa3ffd37ea..942dba211c 100644
--- a/aea/cli/common.py
+++ b/aea/cli/common.py
@@ -25,7 +25,7 @@
import os
import sys
from pathlib import Path
-from typing import Dict, List, cast
+from typing import Dict, List, cast, Optional
import click
import jsonschema # type: ignore
@@ -140,5 +140,77 @@ def _load_env_file(env_file: str):
load_dotenv(dotenv_path=Path(env_file), override=False)
+def format_items(items):
+ """Format list of items (protocols/connections) to a string for CLI output."""
+ list_str = ''
+ for item in items:
+ list_str += (
+ '{line}\n'
+ 'Name: {name}\n'
+ 'Description: {description}\n'
+ 'Version: {version}\n'
+ '{line}\n'.format(
+ name=item['name'],
+ description=item['description'],
+ version=item['version'],
+ line='-' * 30
+ ))
+ return list_str
+
+
+def format_skills(items):
+ """Format list of skills to a string for CLI output."""
+ list_str = ''
+ for item in items:
+ list_str += (
+ '{line}\n'
+ 'Name: {name}\n'
+ 'Description: {description}\n'
+ 'Protocols: {protocols}\n'
+ 'Version: {version}\n'
+ '{line}\n'.format(
+ name=item['name'],
+ description=item['description'],
+ version=item['version'],
+ protocols=''.join(
+ name + ' | ' for name in item['protocol_names']
+ ),
+ line='-' * 30
+ ))
+ return list_str
+
+
+def retrieve_details(name: str, loader: ConfigLoader, config_filepath: str):
+ """Return description of a protocol, skill or connection."""
+ config = loader.load(open(str(config_filepath)))
+ assert config.name == name
+ return {"name": config.name, "description": config.description, "version": config.version}
+
+
class AEAConfigException(Exception):
"""Exception about AEA configuration."""
+
+
+class ConnectionsOption(click.Option):
+ """Click option for the --connections option in 'aea run'."""
+
+ def type_cast_value(self, ctx, value) -> Optional[List[str]]:
+ """
+ Parse the list of string passed through command line.
+
+ E.g. from 'stub,local' to ['stub', 'local'].
+
+ :param ctx: the click context
+ :param value: the list of connection names, as a string.
+ :return:
+ """
+ if value is None:
+ return None
+ try:
+ def arg_strip(s):
+ return s.strip(" '\"")
+
+ connection_names = set(arg_strip(s) for s in value.split(",") if arg_strip(s) != "")
+ return list(connection_names)
+ except Exception:
+ raise click.BadParameter(value)
diff --git a/aea/cli/core.py b/aea/cli/core.py
index 034d903d92..4e78f3f1c5 100644
--- a/aea/cli/core.py
+++ b/aea/cli/core.py
@@ -41,11 +41,12 @@
from aea.cli.run import run
from aea.cli.scaffold import scaffold
from aea.cli.search import search
-from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE, AgentConfig
+from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE, AgentConfig, PrivateKeyPathConfig
from aea.crypto.default import DefaultCrypto
from aea.crypto.ethereum import EthereumCrypto
from aea.crypto.fetchai import FetchAICrypto
-from aea.crypto.helpers import DEFAULT_PRIVATE_KEY_FILE, FETCHAI_PRIVATE_KEY_FILE, ETHEREUM_PRIVATE_KEY_FILE
+from aea.crypto.helpers import DEFAULT_PRIVATE_KEY_FILE, FETCHAI_PRIVATE_KEY_FILE, ETHEREUM_PRIVATE_KEY_FILE, \
+ _validate_private_key_path
DEFAULT_CONNECTION = "oef"
DEFAULT_SKILL = "error"
@@ -67,27 +68,28 @@ def create(click_context, agent_name):
"""Create an agent."""
ctx = cast(Context, click_context.obj)
path = Path(agent_name)
- logger.info("Creating agent's directory in '{}'".format(path))
+ logger.info("Initializing AEA project '{}'".format(agent_name))
+ logger.info("Creating project directory '/{}'".format(agent_name))
# create the agent's directory
try:
path.mkdir(exist_ok=False)
# create a config file inside it
+ logger.info("Creating config file {}".format(DEFAULT_AEA_CONFIG_FILE))
config_file = open(os.path.join(agent_name, DEFAULT_AEA_CONFIG_FILE), "w")
agent_config = AgentConfig(agent_name=agent_name, aea_version=aea.__version__, authors="", version="v1", license="", url="", registry_path=DEFAULT_REGISTRY_PATH, description="")
agent_config.default_connection = DEFAULT_CONNECTION
ctx.agent_loader.dump(agent_config, config_file)
- logger.info("Created config file {}".format(DEFAULT_AEA_CONFIG_FILE))
# next commands must be done from the agent's directory -> overwrite ctx.cwd
ctx.agent_config = agent_config
ctx.cwd = agent_config.agent_name
- logger.info("Adding default connection '{}' to the agent...".format(DEFAULT_CONNECTION))
+ logger.info("Default connections:")
click_context.invoke(connection, connection_name=DEFAULT_CONNECTION)
- logger.info("Adding default skill '{}' to the agent...".format(DEFAULT_SKILL))
+ logger.info("Default skills:")
click_context.invoke(skill, skill_name=DEFAULT_SKILL)
except OSError:
@@ -123,7 +125,7 @@ def delete(ctx: Context, agent_name):
finally:
os.chdir(cwd)
- logger.info("Deleting agent's directory in '{}'...".format(path))
+ logger.info("Deleting agent project directory '/{}'...".format(path))
# delete the agent's directory
try:
@@ -144,11 +146,12 @@ def freeze(ctx: Context):
@cli.command()
@pass_ctx
-def gui(ctx: Context):
+@click.option('-p', '--port', default=8080)
+def gui(ctx: Context, port):
"""Run the CLI GUI."""
import aea.cli_gui # pragma: no cover
logger.info("Running the GUI.....(press Ctrl+C to exit)") # pragma: no cover
- aea.cli_gui.run() # pragma: no cover
+ aea.cli_gui.run(port) # pragma: no cover
@cli.command()
@@ -168,6 +171,26 @@ def generate_key(ctx: Context, type_):
EthereumCrypto().dump(open(ETHEREUM_PRIVATE_KEY_FILE, "wb"))
+@cli.command()
+@click.argument("type_", metavar="TYPE", type=click.Choice([
+ DefaultCrypto.identifier,
+ FetchAICrypto.identifier,
+ EthereumCrypto.identifier
+]), required=True)
+@click.argument("file", metavar="FILE", type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True),
+ required=True)
+@pass_ctx
+def add_key(ctx: Context, type_, file):
+ """Add a private key to the wallet."""
+ _try_to_load_agent_config(ctx)
+ _validate_private_key_path(file, type_)
+ try:
+ ctx.agent_config.private_key_paths.create(type_, PrivateKeyPathConfig(type_, file))
+ except ValueError as e: # pragma: no cover
+ logger.error(str(e)) # pragma: no cover
+ ctx.agent_loader.dump(ctx.agent_config, open(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w"))
+
+
cli.add_command(add)
cli.add_command(_list)
cli.add_command(search)
diff --git a/aea/cli/list.py b/aea/cli/list.py
index 5c749edf62..6279a50863 100644
--- a/aea/cli/list.py
+++ b/aea/cli/list.py
@@ -18,10 +18,14 @@
# ------------------------------------------------------------------------------
"""Implementation of the 'aea list' subcommand."""
+import os
import click
-from aea.cli.common import Context, pass_ctx, _try_to_load_agent_config
+
+from aea.cli.common import Context, pass_ctx, _try_to_load_agent_config, retrieve_details, format_items
+from aea.configurations.base import DEFAULT_CONNECTION_CONFIG_FILE, DEFAULT_SKILL_CONFIG_FILE, \
+ DEFAULT_PROTOCOL_CONFIG_FILE
@click.group()
@@ -35,21 +39,36 @@ def list(ctx: Context):
@pass_ctx
def connections(ctx: Context):
"""List all the installed connections."""
+ result = []
for connection_id in sorted(ctx.agent_config.connections):
- print(connection_id)
+ connection_configuration_filepath = os.path.join("connections", connection_id, DEFAULT_CONNECTION_CONFIG_FILE)
+ details = retrieve_details(connection_id, ctx.connection_loader, connection_configuration_filepath)
+ result.append(details)
+
+ print(format_items(sorted(result, key=lambda k: k['name'])))
@list.command()
@pass_ctx
def protocols(ctx: Context):
"""List all the installed protocols."""
+ result = []
for protocol_id in sorted(ctx.agent_config.protocols):
- print(protocol_id)
+ protocol_configuration_filepath = os.path.join("protocols", protocol_id, DEFAULT_PROTOCOL_CONFIG_FILE)
+ details = retrieve_details(protocol_id, ctx.protocol_loader, protocol_configuration_filepath)
+ result.append(details)
+
+ print(format_items(sorted(result, key=lambda k: k['name'])))
@list.command()
@pass_ctx
def skills(ctx: Context):
"""List all the installed skills."""
+ result = []
for skill_id in sorted(ctx.agent_config.skills):
- print(skill_id)
+ skill_configuration_filepath = os.path.join("skills", skill_id, DEFAULT_SKILL_CONFIG_FILE)
+ details = retrieve_details(skill_id, ctx.skill_loader, skill_configuration_filepath)
+ result.append(details)
+
+ print(format_items(sorted(result, key=lambda k: k['name'])))
diff --git a/packages/skills/fipa_negotiation/__init__.py b/aea/cli/registry/__init__.py
similarity index 92%
rename from packages/skills/fipa_negotiation/__init__.py
rename to aea/cli/registry/__init__.py
index 81d567366d..e2bea78385 100644
--- a/packages/skills/fipa_negotiation/__init__.py
+++ b/aea/cli/registry/__init__.py
@@ -17,4 +17,4 @@
#
# ------------------------------------------------------------------------------
-"""This module contains the implementation of the default skill."""
+"""This module contains tools for operating Registry with CLI."""
diff --git a/aea/helpers/state/base.py b/aea/cli/registry/settings.py
similarity index 65%
rename from aea/helpers/state/base.py
rename to aea/cli/registry/settings.py
index d0177513e6..ed917cfb85 100644
--- a/aea/helpers/state/base.py
+++ b/aea/cli/registry/settings.py
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
-
# ------------------------------------------------------------------------------
#
# Copyright 2018-2019 Fetch.AI Limited
@@ -8,7 +7,7 @@
# 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
+# 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,
@@ -17,19 +16,7 @@
# limitations under the License.
#
# ------------------------------------------------------------------------------
+"""Settings for operating Registry with CLI."""
-"""This module contains the classes which define the states of an agent."""
-
-
-class AgentState:
- """Represent the state of an agent during the game."""
-
- def __init__(self):
- """Initialize."""
-
-
-class WorldState:
- """Represent the state of an agent during the game."""
- def __init__(self):
- """Initialize."""
+REGISTRY_API_URL = 'http://localhost:8000'
diff --git a/aea/cli/registry/utils.py b/aea/cli/registry/utils.py
new file mode 100644
index 0000000000..1df3d5f205
--- /dev/null
+++ b/aea/cli/registry/utils.py
@@ -0,0 +1,142 @@
+# -*- 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.
+#
+# ------------------------------------------------------------------------------
+"""Utils used for operating Registry with CLI."""
+
+
+import click
+import os
+import requests
+import tarfile
+
+from typing import List, Dict
+
+from aea.cli.registry.settings import REGISTRY_API_URL
+
+
+def request_api(method: str, path: str, params=None) -> Dict:
+ """Request Registry API."""
+ resp = requests.request(
+ method=method,
+ url='{}{}'.format(REGISTRY_API_URL, path),
+ params=params
+ )
+ if resp.status_code == 200:
+ return resp.json()
+ elif resp.status_code == 403:
+ raise click.ClickException('You are not authenticated.')
+ elif resp.status_code == 404:
+ raise click.ClickException('Not found in Registry.')
+ else:
+ raise click.ClickException(
+ 'Wrong server response. Status code: {}'.format(resp.status_code)
+ )
+
+
+def split_public_id(public_id: str) -> List[str]:
+ """
+ Split public ID to ownwer, name, version.
+
+ :param public_id: public ID of item from Registry.
+
+ :return: list of str [owner, name, version]
+ """
+ public_id = public_id.replace(':', '/')
+ return public_id.split('/')
+
+
+def _download_file(url: str, cwd: str) -> str:
+ """
+ Download file from URL and save it in CWD (current working directory).
+
+ :param url: str url of the file to download.
+ :param cwd: str path to current working directory.
+
+ :return: str path to downloaded file
+ """
+ local_filename = url.split('/')[-1]
+ filepath = os.path.join(cwd, local_filename)
+ # NOTE the stream=True parameter below
+ response = requests.get(url, stream=True)
+ if response.status_code == 200:
+ with open(filepath, 'wb') as f:
+ f.write(response.raw.read())
+ else:
+ raise click.ClickException(
+ 'Wrong response from server when downloading package.'
+ )
+ return filepath
+
+
+def _extract(source: str, target: str) -> None:
+ """
+ Extract tarball and remove source file.
+
+ :param source: str path to a source tarball file.
+ :param target: str path to target directory.
+
+ :return: None
+ """
+ if (source.endswith("tar.gz")):
+ tar = tarfile.open(source, "r:gz")
+ tar.extractall(path=target)
+ tar.close()
+ else:
+ raise Exception('Unknown file type: {}'.format(source))
+
+ os.remove(source)
+
+
+def fetch_package(obj_type: str, public_id: str, cwd: str) -> None:
+ """
+ Fetch connection/protocol/skill from Registry.
+
+ :param obj_type: str type of object you want to fetch:
+ 'connection', 'protocol', 'skill'
+ :param public_id: str public ID of object.
+ :param cwd: str path to current working directory.
+
+ :return: None
+ """
+ click.echo('Fetching {obj_type} {public_id} from Registry...'.format(
+ public_id=public_id,
+ obj_type=obj_type
+ ))
+ owner, name, version = split_public_id(public_id)
+ plural_obj_type = obj_type + 's' # used for API and folder paths
+
+ api_path = '/{}/{}/{}/{}'.format(plural_obj_type, owner, name, version)
+ resp = request_api('GET', api_path)
+ file_url = resp['file']
+
+ click.echo('Downloading {obj_type} {public_id}...'.format(
+ public_id=public_id,
+ obj_type=obj_type
+ ))
+ filepath = _download_file(file_url, cwd)
+ target_folder = os.path.join(cwd, plural_obj_type)
+
+ click.echo('Extracting {obj_type} {public_id}...'.format(
+ public_id=public_id,
+ obj_type=obj_type
+ ))
+ _extract(filepath, target_folder)
+ click.echo('Successfully fetched {obj_type}: {public_id}.'.format(
+ public_id=public_id,
+ obj_type=obj_type
+ ))
diff --git a/aea/cli/remove.py b/aea/cli/remove.py
index fab4626e7e..534d1e5a62 100644
--- a/aea/cli/remove.py
+++ b/aea/cli/remove.py
@@ -42,7 +42,7 @@ def remove(ctx: Context):
def connection(ctx: Context, connection_name):
"""Remove a connection from the agent."""
agent_name = ctx.agent_config.agent_name
- logger.info("Removing connection {connection_name} from the agent {agent_name}..."
+ logger.info("Removing connection '{connection_name}' from the agent '{agent_name}'..."
.format(agent_name=agent_name, connection_name=connection_name))
if connection_name not in ctx.agent_config.connections:
@@ -69,7 +69,7 @@ def connection(ctx: Context, connection_name):
def protocol(ctx: Context, protocol_name):
"""Remove a protocol from the agent."""
agent_name = ctx.agent_config.agent_name
- logger.info("Removing protocol {protocol_name} from the agent {agent_name}..."
+ logger.info("Removing protocol '{protocol_name}' from the agent '{agent_name}'..."
.format(agent_name=agent_name, protocol_name=protocol_name))
if protocol_name not in ctx.agent_config.protocols:
@@ -96,7 +96,7 @@ def protocol(ctx: Context, protocol_name):
def skill(ctx: Context, skill_name):
"""Remove a skill from the agent."""
agent_name = ctx.agent_config.agent_name
- logger.info("Removing skill {skill_name} from the agent {agent_name}..."
+ logger.info("Removing skill '{skill_name}' from the agent '{agent_name}'..."
.format(agent_name=agent_name, skill_name=skill_name))
if skill_name not in ctx.agent_config.skills:
diff --git a/aea/cli/run.py b/aea/cli/run.py
index 634f774603..f8ce2e5e1d 100644
--- a/aea/cli/run.py
+++ b/aea/cli/run.py
@@ -24,24 +24,26 @@
import re
import sys
from pathlib import Path
-from typing import cast
+from typing import cast, List
import click
from click import pass_context
from aea.aea import AEA
from aea.cli.common import Context, logger, _try_to_load_agent_config, _try_to_load_protocols, \
- AEAConfigException, _load_env_file
+ AEAConfigException, _load_env_file, ConnectionsOption
from aea.cli.install import install
-from aea.connections.base import Connection
-from aea.configurations.loader import ConfigLoader
from aea.configurations.base import AgentConfig, DEFAULT_AEA_CONFIG_FILE, PrivateKeyPathConfig, LedgerAPIConfig
+from aea.configurations.loader import ConfigLoader
+from aea.connections.base import Connection
from aea.crypto.ethereum import ETHEREUM
from aea.crypto.fetchai import FETCHAI
-from aea.crypto.helpers import _create_default_private_key, _create_fetchai_private_key, _create_ethereum_private_key, DEFAULT_PRIVATE_KEY_FILE, FETCHAI_PRIVATE_KEY_FILE, ETHEREUM_PRIVATE_KEY_FILE, _try_validate_private_key_pem_path, _try_validate_fet_private_key_path, _try_validate_ethereum_private_key_path
-from aea.crypto.ledger_apis import LedgerApis, _try_to_instantiate_fetchai_ledger_api, _try_to_instantiate_ethereum_ledger_api, SUPPORTED_LEDGER_APIS
+from aea.crypto.helpers import _create_default_private_key, _create_fetchai_private_key, _create_ethereum_private_key, \
+ DEFAULT_PRIVATE_KEY_FILE, FETCHAI_PRIVATE_KEY_FILE, ETHEREUM_PRIVATE_KEY_FILE, _try_validate_private_key_pem_path, \
+ _try_validate_fet_private_key_path, _try_validate_ethereum_private_key_path
+from aea.crypto.ledger_apis import LedgerApis, _try_to_instantiate_fetchai_ledger_api, \
+ _try_to_instantiate_ethereum_ledger_api, SUPPORTED_LEDGER_APIS
from aea.crypto.wallet import Wallet, DEFAULT, SUPPORTED_CRYPTOS
-from aea.mail.base import MailBox
from aea.registries.base import Resources
@@ -173,14 +175,14 @@ def _setup_connection(connection_name: str, public_key: str, ctx: Context) -> Co
@click.command()
-@click.option('--connection', 'connection_name', metavar="CONN_NAME", type=str, required=False, default=None,
- help="The connection name. Must be declared in the agent's configuration file.")
+@click.option('--connections', "connection_names", cls=ConnectionsOption, required=False, default=None,
+ help="The connection names to use for running the agent. Must be declared in the agent's configuration file.")
@click.option('--env', 'env_file', type=click.Path(), required=False, default=".env",
help="Specify an environment file (default: .env)")
@click.option('--install-deps', 'install_deps', is_flag=True, required=False, default=False,
help="Install all the dependencies before running the agent.")
@pass_context
-def run(click_context, connection_name: str, env_file: str, install_deps: bool):
+def run(click_context, connection_names: List[str], env_file: str, install_deps: bool):
"""Run the agent."""
ctx = cast(Context, click_context.obj)
_try_to_load_agent_config(ctx)
@@ -195,10 +197,13 @@ def run(click_context, connection_name: str, env_file: str, install_deps: bool):
wallet = Wallet(private_key_paths)
ledger_apis = LedgerApis(ledger_api_configs)
- connection_name = ctx.agent_config.default_connection if connection_name is None else connection_name
+ connection_names = [ctx.agent_config.default_connection] if connection_names is None else connection_names
+ connections = []
_try_to_load_protocols(ctx)
try:
- connection = _setup_connection(connection_name, wallet.public_keys[FETCHAI], ctx)
+ for connection_name in connection_names:
+ connection = _setup_connection(connection_name, wallet.public_keys[FETCHAI], ctx)
+ connections.append(connection)
except AEAConfigException as e:
logger.error(str(e))
sys.exit(1)
@@ -209,8 +214,7 @@ def run(click_context, connection_name: str, env_file: str, install_deps: bool):
else:
click_context.invoke(install)
- mailbox = MailBox(connection)
- agent = AEA(agent_name, mailbox, wallet, ledger_apis, resources=Resources(str(Path("."))))
+ agent = AEA(agent_name, connections, wallet, ledger_apis, resources=Resources(str(Path("."))))
try:
agent.start()
except KeyboardInterrupt:
diff --git a/aea/cli/scaffold.py b/aea/cli/scaffold.py
index ec98d2071d..27d9f52aa2 100644
--- a/aea/cli/scaffold.py
+++ b/aea/cli/scaffold.py
@@ -29,7 +29,7 @@
from aea import AEA_DIR
from aea.cli.common import Context, pass_ctx, logger, _try_to_load_agent_config
-from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE
+from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE, DEFAULT_CONNECTION_CONFIG_FILE, DEFAULT_PROTOCOL_CONFIG_FILE, DEFAULT_SKILL_CONFIG_FILE # noqa: F401
@click.group()
@@ -39,88 +39,73 @@ def scaffold(ctx: Context):
_try_to_load_agent_config(ctx)
-@scaffold.command()
-@click.argument('connection_name', type=str, required=True)
-@pass_ctx
-def connection(ctx: Context, connection_name: str) -> None:
- """Add a connection scaffolding to the configuration file and agent."""
- # check if we already have a connection with the same name
- logger.debug("Connections already supported by the agent: {}".format(ctx.agent_config.connections))
- if connection_name in ctx.agent_config.connections:
- logger.error("A connection with name '{}' already exists. Aborting...".format(connection_name))
+def _scaffold_item(ctx: Context, item_type, item_name):
+ """Add an item scaffolding to the configuration file and agent."""
+ existing_item_list = getattr(ctx.agent_config, "{}s".format(item_type))
+ loader = getattr(ctx, "{}_loader".format(item_type))
+ default_config_filename = globals()["DEFAULT_{}_CONFIG_FILE".format(item_type.upper())]
+
+ item_type_plural = item_type + "s"
+
+ # check if we already have an item with the same name
+ logger.debug("{} already supported by the agent: {}".format(item_type_plural, existing_item_list))
+ if item_name in existing_item_list:
+ logger.error("A {} with name '{}' already exists. Aborting...".format(item_type, item_name))
sys.exit(1)
try:
+ agent_name = ctx.agent_config.agent_name
+ logger.info("Adding {} scaffold '{}' to the agent '{}'...".format(item_type, item_name, agent_name))
- Path("connections").mkdir(exist_ok=True)
+ Path(item_type_plural).mkdir(exist_ok=True)
# create the connection folder
- dest = Path(os.path.join("connections", connection_name))
+ dest = Path(os.path.join(item_type_plural, item_name))
# copy the skill package into the agent's supported skills.
- src = Path(os.path.join(AEA_DIR, "connections", "scaffold"))
- logger.info("Copying connection modules. src={} dst={}".format(src, dest))
+ src = Path(os.path.join(AEA_DIR, item_type_plural, "scaffold"))
+ logger.debug("Copying {} modules. src={} dst={}".format(item_type, src, dest))
shutil.copytree(src, dest)
# add the connection to the configurations.
- logger.info("Registering the connection into {}".format(DEFAULT_AEA_CONFIG_FILE))
- ctx.agent_config.connections.add(connection_name)
+ logger.debug("Registering the {} into {}".format(item_type, DEFAULT_AEA_CONFIG_FILE))
+ existing_item_list.add(item_name)
ctx.agent_loader.dump(ctx.agent_config, open(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w"))
+ # ensure the name in the yaml and the name of the folder are the same
+ config_filepath = os.path.join(ctx.cwd, item_type_plural, item_name, default_config_filename)
+ config = loader.load(open(str(config_filepath)))
+ config.name = item_name
+ loader.dump(config, open(config_filepath, "w"))
+
except FileExistsError:
- logger.error("A connection with this name already exists. Please choose a different name and try again.")
+ logger.error("A {} with this name already exists. Please choose a different name and try again.".format(item_type))
sys.exit(1)
except ValidationError:
logger.error("Error when validating the skill configuration file.")
- shutil.rmtree(os.path.join("connections", connection_name), ignore_errors=True)
+ shutil.rmtree(os.path.join(item_type_plural, item_name), ignore_errors=True)
sys.exit(1)
except Exception as e:
logger.exception(e)
- shutil.rmtree(os.path.join("connections", connection_name), ignore_errors=True)
+ shutil.rmtree(os.path.join(item_type_plural, item_name), ignore_errors=True)
sys.exit(1)
+@scaffold.command()
+@click.argument('connection_name', type=str, required=True)
+@pass_ctx
+def connection(ctx: Context, connection_name: str) -> None:
+ """Add a connection scaffolding to the configuration file and agent."""
+ _scaffold_item(ctx, "connection", connection_name)
+
+
@scaffold.command()
@click.argument('protocol_name', type=str, required=True)
@pass_ctx
def protocol(ctx: Context, protocol_name: str):
"""Add a protocol scaffolding to the configuration file and agent."""
- # check if we already have a protocol with the same name
- logger.debug("Protocols already supported by the agent: {}".format(ctx.agent_config.protocols))
- if protocol_name in ctx.agent_config.protocols:
- logger.error("A protocol with name '{}' already exists. Aborting...".format(protocol_name))
- sys.exit(1)
-
- try:
- # create the 'protocols' folder if it doesn't exist:
- Path("protocols").mkdir(exist_ok=True)
-
- # create the protocol folder
- dest = Path(os.path.join("protocols", protocol_name))
-
- # copy the skill package into the agent's supported skills.
- src = Path(os.path.join(AEA_DIR, "protocols", "scaffold"))
- logger.info("Copying protocol modules. src={} dst={}".format(src, dest))
-
- shutil.copytree(src, dest)
-
- # add the protocol to the configurations.
- logger.info("Registering the protocol into {}".format(DEFAULT_AEA_CONFIG_FILE))
- ctx.agent_config.protocols.add(protocol_name)
- ctx.agent_loader.dump(ctx.agent_config, open(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w"))
-
- except FileExistsError:
- logger.error("A protocol with this name already exists. Please choose a different name and try again.")
- sys.exit(1)
- except ValidationError:
- logger.error("Error when validating the skill configuration file.")
- shutil.rmtree(os.path.join("protocols", protocol_name), ignore_errors=True)
- sys.exit(1)
- except Exception as e:
- logger.exception(e)
- shutil.rmtree(os.path.join("protocols", protocol_name), ignore_errors=True)
- sys.exit(1)
+ _scaffold_item(ctx, "protocol", protocol_name)
@scaffold.command()
@@ -128,38 +113,4 @@ def protocol(ctx: Context, protocol_name: str):
@pass_ctx
def skill(ctx: Context, skill_name: str):
"""Add a skill scaffolding to the configuration file and agent."""
- # check if we already have a skill with the same name
- logger.debug("Skills already supported by the agent: {}".format(ctx.agent_config.skills))
- if skill_name in ctx.agent_config.skills:
- logger.error("A skill with name '{}' already exists. Aborting...".format(skill_name))
- sys.exit(1)
-
- try:
- # create the 'skills' folder if it doesn't exist:
- Path("skills").mkdir(exist_ok=True)
-
- # create the skill folder
- dest = Path(os.path.join("skills", skill_name))
-
- # copy the skill package into the agent's supported skills.
- src = Path(os.path.join(AEA_DIR, "skills", "scaffold"))
- logger.info("Copying skill modules. src={} dst={}".format(src, dest))
-
- shutil.copytree(src, dest)
-
- # add the skill to the configurations.
- logger.info("Registering the protocol into {}".format(DEFAULT_AEA_CONFIG_FILE))
- ctx.agent_config.skills.add(skill_name)
- ctx.agent_loader.dump(ctx.agent_config, open(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w"))
-
- except FileExistsError:
- logger.error("A skill with this name already exists. Please choose a different name and try again.")
- sys.exit(1)
- except ValidationError:
- logger.error("Error when validating the skill configuration file.")
- shutil.rmtree(os.path.join("skills", skill_name), ignore_errors=True)
- sys.exit(1)
- except Exception as e:
- logger.exception(e)
- shutil.rmtree(os.path.join("skills", skill_name), ignore_errors=True)
- sys.exit(1)
+ _scaffold_item(ctx, "skill", skill_name)
diff --git a/aea/cli/search.py b/aea/cli/search.py
index 1776fea071..501e8d6754 100644
--- a/aea/cli/search.py
+++ b/aea/cli/search.py
@@ -19,17 +19,18 @@
"""Implementation of the 'aea search' subcommand."""
from pathlib import Path
-from typing import Set, cast
+from typing import cast, List, Dict
import click
import os
from aea import AEA_DIR
-from aea.cli.common import Context, pass_ctx, DEFAULT_REGISTRY_PATH, logger
+from aea.cli.common import Context, pass_ctx, DEFAULT_REGISTRY_PATH, logger, retrieve_details, ConfigLoader, format_items, format_skills
+from aea.configurations.base import DEFAULT_CONNECTION_CONFIG_FILE, DEFAULT_SKILL_CONFIG_FILE, DEFAULT_PROTOCOL_CONFIG_FILE
+from aea.cli.registry.utils import request_api
@click.group()
-@click.option("--registry", type=click.Path(exists=True, file_okay=False, dir_okay=True, readable=True, resolve_path=True),
- default=None, help="Path/URL to the registry.")
+@click.option('--registry', is_flag=True, help="For Registry search.")
@pass_ctx
def search(ctx: Context, registry):
"""Search for components in the registry.
@@ -38,73 +39,107 @@ def search(ctx: Context, registry):
aea search --registry packages/ skills
"""
- if registry is None:
- registry = os.path.join(AEA_DIR, DEFAULT_REGISTRY_PATH)
- logger.debug("Using registry {}".format(registry))
- ctx.set_config("registry", str(registry))
+ if registry:
+ ctx.set_config("is_registry", True)
+ else:
+ registry = os.path.join(ctx.cwd, DEFAULT_REGISTRY_PATH)
+ ctx.set_config("registry", registry)
+ logger.debug("Using registry {}".format(registry))
+
+
+def _is_invalid_item(name, dir_path, config_path):
+ """Return true if this protocol, connection or skill should not be returned in the list."""
+ return ".py" in name or "__" in name or name == "scaffold" or os.path.isfile(dir_path) or not os.path.isfile(config_path)
+
+
+def _get_details_from_dir(loader: ConfigLoader, root_path: str, sub_dir_name: str, config_filename: str, results: List[Dict]):
+ for r in Path(root_path).glob(sub_dir_name + "/*/"):
+ dir_path = os.path.join(root_path, sub_dir_name, r.name)
+ config_path = os.path.join(root_path, sub_dir_name, r.name, config_filename)
+
+ if _is_invalid_item(r.name, dir_path, config_path):
+ continue
+
+ details = retrieve_details(r.name, loader, config_path)
+ results.append(details)
@search.command()
+@click.option('--query', default='',
+ help='Query string to search Connections by name.')
@pass_ctx
-def connections(ctx: Context):
- """List all the connections available in the registry."""
+def connections(ctx: Context, query):
+ """Search for Connections."""
+ if ctx.config.get("is_registry"):
+ click.echo('Searching for "{}"...'.format(query))
+ resp = request_api(
+ 'GET', '/connections', params={'search': query}
+ )
+ if not len(resp):
+ click.echo('No connections found.')
+ else:
+ click.echo('Connections found:\n')
+ click.echo(format_items(resp))
+ return
+
registry = cast(str, ctx.config.get("registry"))
- result = set() # type: Set[str]
- for r in Path(AEA_DIR).glob("connections/[!_]*[!.py]/"):
- result.add(r.name)
-
- try:
- for r in Path(registry).glob("connections/[!_]*[!.py]/"):
- result.add(r.name)
- except Exception: # pragma: no cover
- pass
-
- if "scaffold" in result: result.remove("scaffold")
- if ".DS_Store" in result: result.remove(".DS_Store")
+ result: List[Dict] = []
+ _get_details_from_dir(ctx.connection_loader, AEA_DIR, "connections", DEFAULT_CONNECTION_CONFIG_FILE, result)
+ _get_details_from_dir(ctx.connection_loader, registry, "connections", DEFAULT_CONNECTION_CONFIG_FILE, result)
+
print("Available connections:")
- for conn in sorted(result):
- print("- " + conn)
+ print(format_items(sorted(result, key=lambda k: k['name'])))
@search.command()
+@click.option('--query', default='',
+ help='Query string to search Protocols by name.')
@pass_ctx
-def protocols(ctx: Context):
- """List all the protocols available in the registry."""
+def protocols(ctx: Context, query):
+ """Search for Protocols."""
+ if ctx.config.get("is_registry"):
+ click.echo('Searching for "{}"...'.format(query))
+ resp = request_api(
+ 'GET', '/protocols', params={'search': query}
+ )
+ if not len(resp):
+ click.echo('No protocols found.')
+ else:
+ click.echo('Protocols found:\n')
+ click.echo(format_items(resp))
+ return
+
registry = cast(str, ctx.config.get("registry"))
- result = set() # type: Set[str]
- for r in Path(AEA_DIR).glob("protocols/[!_]*[!.py]"):
- result.add(r.name)
-
- try:
- for r in Path(registry).glob("protocols/[!_]*[!.py]/"):
- result.add(r.name)
- except Exception: # pragma: no cover
- pass
-
- if "scaffold" in result: result.remove("scaffold")
- if ".DS_Store" in result: result.remove(".DS_Store")
+ result: List[Dict] = []
+ _get_details_from_dir(ctx.protocol_loader, AEA_DIR, "protocols", DEFAULT_PROTOCOL_CONFIG_FILE, result)
+ _get_details_from_dir(ctx.protocol_loader, registry, "protocols", DEFAULT_PROTOCOL_CONFIG_FILE, result)
+
print("Available protocols:")
- for protocol in sorted(result):
- print("- " + protocol)
+ print(format_items(sorted(result, key=lambda k: k['name'])))
@search.command()
+@click.option('--query', default='',
+ help='Query string to search Skills by name.')
@pass_ctx
-def skills(ctx: Context):
- """List all the skills available in the registry."""
+def skills(ctx: Context, query):
+ """Search for Skills."""
+ if ctx.config.get("is_registry"):
+ click.echo('Searching for "{}"...'.format(query))
+ resp = request_api(
+ 'GET', '/skills', params={'search': query}
+ )
+ if not len(resp):
+ click.echo('No skills found.')
+ else:
+ click.echo('Skills found:\n')
+ click.echo(format_skills(resp))
+ return
+
registry = cast(str, ctx.config.get("registry"))
- result = set() # type: Set[str]
- for r in Path(AEA_DIR).glob("skills/[!_]*[!.py]"):
- result.add(r.name)
-
- try:
- for r in Path(registry).glob("skills/[!_]*[!.py]/"):
- result.add(r.name)
- except Exception: # pragma: no cover
- pass
-
- if "scaffold" in result: result.remove("scaffold")
- if ".DS_Store" in result: result.remove(".DS_Store")
+ result: List[Dict] = []
+ _get_details_from_dir(ctx.skill_loader, AEA_DIR, "skills", DEFAULT_SKILL_CONFIG_FILE, result)
+ _get_details_from_dir(ctx.skill_loader, registry, "skills", DEFAULT_SKILL_CONFIG_FILE, result)
+
print("Available skills:")
- for skill in sorted(result):
- print("- " + skill)
+ print(format_items(sorted(result, key=lambda k: k['name'])))
diff --git a/aea/cli_gui/__init__.py b/aea/cli_gui/__init__.py
index ff4816ebfd..32de53c428 100644
--- a/aea/cli_gui/__init__.py
+++ b/aea/cli_gui/__init__.py
@@ -26,10 +26,11 @@
import os
import subprocess
import threading
+import time
+from typing import List, Dict
import connexion
import flask
-import yaml
elements = [['local', 'agent', 'localAgents'],
@@ -56,40 +57,38 @@ class ProcessState(Enum):
lock = threading.Lock()
-def read_description(dir_name, yaml_name):
- """Return true if this directory contains an items in an AEA project i.e. protocol, skill or connection."""
- assert os.path.isdir(dir_name)
- file_path = os.path.join(dir_name, yaml_name + ".yaml")
- assert os.path.isfile(file_path)
- with open(file_path, 'r') as stream:
- try:
- yaml_data = yaml.safe_load(stream)
- if "description" in yaml_data:
- return yaml_data["description"]
- except yaml.YAMLError as exc:
- logging.error(exc)
- return "Placeholder description"
+class AppContext:
+ """Store useful global information about the app.
+ Can't add it into the app object itself because mypy complains.
+ """
-def is_agent_dir(dir_name):
- """Return trye if this directory contains an AEA project (an agent)."""
- if not os.path.isdir(dir_name):
- return False
- else:
- return os.path.isfile(os.path.join(dir_name, "aea-config.yaml"))
+ oef_process = None
+ agent_processes: Dict[str, subprocess.Popen] = {}
+ agent_tty: Dict[str, List[str]] = {}
+ agent_error: Dict[str, List[str]] = {}
+ oef_tty: List[str] = []
+ oef_error: List[str] = []
+
+ ui_is_starting = False
+ agents_dir = os.path.abspath(os.getcwd())
+ module_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), "../../")
-def is_item_dir(dir_name, item_type):
- """Return true if this directory contains an items in an AEA project i.e. protocol, skill or connection."""
+app_context = AppContext()
+
+
+def is_agent_dir(dir_name: str) -> bool:
+ """Return true if this directory contains an AEA project (an agent)."""
if not os.path.isdir(dir_name):
return False
else:
- return os.path.isfile(os.path.join(dir_name, item_type + ".yaml"))
+ return os.path.isfile(os.path.join(dir_name, "aea-config.yaml"))
-def get_agents():
+def get_agents() -> List[Dict]:
"""Return list of all local agents."""
- file_list = glob.glob(os.path.join(flask.app.agents_dir, '*'))
+ file_list = glob.glob(os.path.join(app_context.agents_dir, '*'))
agent_list = []
@@ -101,101 +100,118 @@ def get_agents():
return agent_list
-def _add_items(item_dir, item_type, items_list):
- file_list = glob.glob(os.path.join(item_dir, '*'))
- for path in file_list:
- if is_item_dir(path, item_type):
- head, tail = os.path.split(path)
- desc = read_description(path, item_type)
- if not {"id": tail, "description": desc} in items_list:
- items_list.append({"id": tail, "description": desc})
+def _sync_extract_items_from_tty(pid: subprocess.Popen):
+ item_ids = []
+ item_descs = []
+ output = []
+ err = ""
+ for line in io.TextIOWrapper(pid.stdout, encoding="utf-8"):
+ if line[:6] == "Name: ":
+ item_ids.append(line[6:-1])
+
+ if line[:13] == "Description: ":
+ item_descs.append(line[13:-1])
+ assert len(item_ids) == len(item_descs)
-def get_registered_items(item_type):
- """Return list of all protocols, connections or skills in the registry."""
- items_list = []
+ for i in range(0, len(item_ids)):
+ output.append({"id": item_ids[i], "description": item_descs[i]})
- _add_items(os.path.join(flask.app.agents_dir, "packages/" + item_type + "s"), item_type, items_list)
- _add_items(os.path.join(flask.app.module_dir, "aea/" + item_type + "s"), item_type, items_list)
+ for line in io.TextIOWrapper(pid.stderr, encoding="utf-8"):
+ err += line + "\n"
- return items_list
+ while pid.poll() is None:
+ time.sleep(0.1) # pragma: no cover
+ if pid.poll() == 0:
+ return output, 200 # 200 (Success)
+ else:
+ return {"detail": err}, 400 # 400 Bad request
-def create_agent(agent_id):
+
+def get_registered_items(item_type: str):
"""Create a new AEA project."""
- if _call_aea(["aea", "create", agent_id], flask.app.agents_dir) == 0:
+ # need to place ourselves one directory down so the searcher can find the packages
+ pid = _call_aea_async(["aea", "search", item_type + "s"], os.path.join(app_context.module_dir, "aea"))
+ return _sync_extract_items_from_tty(pid)
+
+
+def search_registered_items(item_type: str, search_term: str):
+ """Create a new AEA project."""
+ # need to place ourselves one directory down so the searcher can find the packages
+ pid = _call_aea_async(["aea", "search", item_type + "s", "--query", search_term], os.path.join(app_context.module_dir, "aea"))
+ ret = _sync_extract_items_from_tty(pid)
+ return ret[0], item_type, search_term, ret[1]
+
+
+def create_agent(agent_id: str):
+ """Create a new AEA project."""
+ if _call_aea(["aea", "create", agent_id], app_context.agents_dir) == 0:
return agent_id, 201 # 201 (Created)
else:
return {"detail": "Failed to create Agent {} - a folder of this name may exist already".format(agent_id)}, 400 # 400 Bad request
-def delete_agent(agent_id):
+def delete_agent(agent_id: str):
"""Delete an existing AEA project."""
- if _call_aea(["aea", "delete", agent_id], flask.app.agents_dir) == 0:
+ if _call_aea(["aea", "delete", agent_id], app_context.agents_dir) == 0:
return 'Agent {} deleted'.format(agent_id), 200 # 200 (OK)
else:
- return {"detail": "Failed to delete Agent {} - it ay not exist".format(agent_id)}, 400 # 400 Bad request
+ return {"detail": "Failed to delete Agent {} - it may not exist".format(agent_id)}, 400 # 400 Bad request
-def add_item(agent_id, item_type, item_id):
+def add_item(agent_id: str, item_type: str, item_id: str):
"""Add a protocol, skill or connection to the register to a local agent."""
- agent_dir = os.path.join(flask.app.agents_dir, agent_id)
+ agent_dir = os.path.join(app_context.agents_dir, agent_id)
if _call_aea(["aea", "add", item_type, item_id], agent_dir) == 0:
return agent_id, 201 # 200 (OK)
else:
- return {"detail": "Failed to add protocol {} to agent {}".format(item_id, agent_id)}, 400 # 400 Bad request
+ return {"detail": "Failed to add {} {} to agent {}".format(item_type, item_id, agent_id)}, 400 # 400 Bad request
-def remove_local_item(agent_id, item_type, item_id):
+def remove_local_item(agent_id: str, item_type: str, item_id: str):
"""Remove a protocol, skill or connection from a local agent."""
- agent_dir = os.path.join(flask.app.agents_dir, agent_id)
+ agent_dir = os.path.join(app_context.agents_dir, agent_id)
if _call_aea(["aea", "remove", item_type, item_id], agent_dir) == 0:
return agent_id, 201 # 200 (OK)
else:
return {"detail": "Failed to remove {} {} from agent {}".format(item_type, item_id, agent_id)}, 400 # 400 Bad request
-def get_local_items(agent_id, item_type):
+def get_local_items(agent_id: str, item_type: str):
"""Return a list of protocols, skills or connections supported by a local agent."""
- items_dir = os.path.join(os.path.join(flask.app.agents_dir, agent_id), item_type + "s")
-
- file_list = glob.glob(os.path.join(items_dir, '*'))
-
- items_list = []
+ if agent_id == "NONE":
+ return [], 200 # 200 (Success)
- for path in file_list:
- if is_item_dir(path, item_type):
- head, tail = os.path.split(path)
- desc = read_description(path, item_type)
- items_list.append({"id": tail, "description": desc})
+ # need to place ourselves one directory down so the searcher can find the packages
+ pid = _call_aea_async(["aea", "list", item_type + "s"], os.path.join(app_context.agents_dir, agent_id))
+ return _sync_extract_items_from_tty(pid)
- return items_list
-
-def scaffold_item(agent_id, item_type, item_id):
+def scaffold_item(agent_id: str, item_type: str, item_id: str):
"""Scaffold a moslty empty item on an agent (either protocol, skill or connection)."""
- agent_dir = os.path.join(flask.app.agents_dir, agent_id)
+ agent_dir = os.path.join(app_context.agents_dir, agent_id)
if _call_aea(["aea", "scaffold", item_type, item_id], agent_dir) == 0:
return agent_id, 201 # 200 (OK)
else:
return {"detail": "Failed to scaffold a new {} in to agent {}".format(item_type, agent_id)}, 400 # 400 Bad request
-def _call_aea(param_list, dir):
- # Should lock here to prevet multiple calls coming in at once and changing the current working directory weirdly
+def _call_aea(param_list: List[str], dir_arg: str) -> int:
with lock:
old_cwd = os.getcwd()
- os.chdir(dir)
+ os.chdir(dir_arg)
ret = subprocess.call(param_list)
os.chdir(old_cwd)
return ret
-def _call_aea_async(param_list, dir):
+def _call_aea_async(param_list: List[str], dir_arg: str) -> subprocess.Popen:
# Should lock here to prevet multiple calls coming in at once and changing the current working directory weirdly
with lock:
old_cwd = os.getcwd()
- os.chdir(dir)
+
+ os.chdir(dir_arg)
env = os.environ.copy()
env["PYTHONUNBUFFERED"] = "1"
ret = subprocess.Popen(param_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
@@ -203,32 +219,32 @@ def _call_aea_async(param_list, dir):
return ret
-def start_oef_node(dummy):
+def start_oef_node():
"""Start an OEF node running."""
_kill_running_oef_nodes()
param_list = [
"python",
- "scripts/oef/launch.py",
+ "./scripts/oef/launch.py",
"--disable_stdin",
"--name",
oef_node_name,
"-c",
"./scripts/oef/launch_config.json"]
- flask.app.oef_process = _call_aea_async(param_list, flask.app.agents_dir)
+ app_context.oef_process = _call_aea_async(param_list, app_context.agents_dir)
- if flask.app.oef_process is not None:
- flask.app.oef_tty = []
- flask.app.oef_error = []
+ if app_context.oef_process is not None:
+ app_context.oef_tty = []
+ app_context.oef_error = []
- tty_read_thread = threading.Thread(target=_read_tty, args=(flask.app.oef_process, flask.app.oef_tty))
+ tty_read_thread = threading.Thread(target=_read_tty, args=(app_context.oef_process, app_context.oef_tty))
tty_read_thread.start()
- error_read_thread = threading.Thread(target=_read_error, args=(flask.app.oef_process, flask.app.oef_error))
+ error_read_thread = threading.Thread(target=_read_error, args=(app_context.oef_process, app_context.oef_error))
error_read_thread.start()
- return "All fine {}".format(dummy), 200 # 200 (OK)
+ return "OEF Node started", 200 # 200 (OK)
else:
return {"detail": "Failed to start OEF Node"}, 400 # 400 Bad request
@@ -239,18 +255,18 @@ def get_oef_node_status():
error_str = ""
status_str = str(ProcessState.NOT_STARTED).replace('ProcessState.', '')
- if flask.app.oef_process is not None:
- status_str = str(get_process_status(flask.app.oef_process)).replace('ProcessState.', '')
+ if app_context.oef_process is not None:
+ status_str = str(get_process_status(app_context.oef_process)).replace('ProcessState.', '')
- total_num_lines = len(flask.app.oef_tty)
+ total_num_lines = len(app_context.oef_tty)
for i in range(max(0, total_num_lines - max_log_lines), total_num_lines):
- tty_str += flask.app.oef_tty[i]
+ tty_str += app_context.oef_tty[i]
tty_str = tty_str.replace("\n", "
")
- total_num_lines = len(flask.app.oef_error)
+ total_num_lines = len(app_context.oef_error)
for i in range(max(0, total_num_lines - max_log_lines), total_num_lines):
- error_str += flask.app.oef_error[i]
+ error_str += app_context.oef_error[i]
error_str = error_str.replace("\n", "
")
@@ -260,95 +276,95 @@ def get_oef_node_status():
def stop_oef_node():
"""Stop an OEF node running."""
_kill_running_oef_nodes()
- flask.app.oef_process = None
+ app_context.oef_process = None
return "All fine", 200 # 200 (OK)
-def start_agent(agent_id, connection_id):
+def start_agent(agent_id: str, connection_id: str):
"""Start a local agent running."""
# Test if it is already running in some form
- if agent_id in flask.app.agent_processes:
- if get_process_status(flask.app.agent_processes[agent_id]) != ProcessState.RUNNING:
- if flask.app.agent_processes[agent_id] is not None:
- flask.app.agent_processes[agent_id].terminate()
- flask.app.agent_processes[agent_id].wait()
- del flask.app.agent_processes[agent_id]
- del flask.app.agent_tty[agent_id]
- del flask.app.agent_erroe[agent_id]
+ if agent_id in app_context.agent_processes:
+ if get_process_status(app_context.agent_processes[agent_id]) != ProcessState.RUNNING:
+ if app_context.agent_processes[agent_id] is not None:
+ app_context.agent_processes[agent_id].terminate()
+ app_context.agent_processes[agent_id].wait()
+ del app_context.agent_processes[agent_id]
+ del app_context.agent_tty[agent_id]
+ del app_context.agent_error[agent_id]
else:
return {"detail": "Agent {} is already running".format(agent_id)}, 400 # 400 Bad request
- agent_dir = os.path.join(flask.app.agents_dir, agent_id)
+ agent_dir = os.path.join(app_context.agents_dir, agent_id)
if connection_id is not None and connection_id != "":
- connections = get_local_items(agent_id, "connection")
+ connections = get_local_items(agent_id, "connection")[0]
has_named_connection = False
for element in connections:
if element["id"] == connection_id:
has_named_connection = True
if has_named_connection:
- agent_process = _call_aea_async(["aea", "run", "--connection", connection_id], agent_dir)
+ agent_process = _call_aea_async(["aea", "run", "--connections", connection_id], agent_dir)
else:
- return {"detail": "Trying to run agent {} with non-existant connection: {}".format(agent_id, connection_id)}, 400 # 400 Bad request
+ return {"detail": "Trying to run agent {} with non-existent connection: {}".format(agent_id, connection_id)}, 400 # 400 Bad request
else:
agent_process = _call_aea_async(["aea", "run"], agent_dir)
if agent_process is None:
return {"detail": "Failed to run agent {}".format(agent_id)}, 400 # 400 Bad request
else:
- flask.app.agent_processes[agent_id] = agent_process
- flask.app.agent_tty[agent_id] = []
- flask.app.agent_error[agent_id] = []
+ app_context.agent_processes[agent_id] = agent_process
+ app_context.agent_tty[agent_id] = []
+ app_context.agent_error[agent_id] = []
- tty_read_thread = threading.Thread(target=_read_tty, args=(flask.app.agent_processes[agent_id], flask.app.agent_tty[agent_id]))
+ tty_read_thread = threading.Thread(target=_read_tty, args=(app_context.agent_processes[agent_id], app_context.agent_tty[agent_id]))
tty_read_thread.start()
- error_read_thread = threading.Thread(target=_read_error, args=(flask.app.agent_processes[agent_id], flask.app.agent_error[agent_id]))
+ error_read_thread = threading.Thread(target=_read_error, args=(app_context.agent_processes[agent_id], app_context.agent_error[agent_id]))
error_read_thread.start()
return agent_id, 201 # 200 (OK)
-def _read_tty(process, str_list):
- for line in io.TextIOWrapper(process.stdout, encoding="utf-8"):
+def _read_tty(pid: subprocess.Popen, str_list: List[str]):
+ for line in io.TextIOWrapper(pid.stdout, encoding="utf-8"):
logging.info("stdout: " + line.replace("\n", ""))
str_list.append(line)
str_list.append("process terminated\n")
-def _read_error(process, str_list):
- for line in io.TextIOWrapper(process.stderr, encoding="utf-8"):
+def _read_error(pid: subprocess.Popen, str_list: List[str]):
+ for line in io.TextIOWrapper(pid.stderr, encoding="utf-8"):
logging.error("stderr: " + line.replace("\n", ""))
str_list.append(line)
str_list.append("process terminated\n")
-def get_agent_status(agent_id):
+def get_agent_status(agent_id: str):
"""Get the status of the running agent Node."""
status_str = str(ProcessState.NOT_STARTED).replace('ProcessState.', '')
tty_str = ""
error_str = ""
- if agent_id in flask.app.agent_processes and flask.app.agent_processes[agent_id] is not None:
-
- status_str = str(get_process_status(flask.app.agent_processes[agent_id])).replace('ProcessState.', '')
+ # agent_id will not be in lists if we haven't run it yet
+ if agent_id in app_context.agent_processes and app_context.agent_processes[agent_id] is not None:
+ status_str = str(get_process_status(app_context.agent_processes[agent_id])).replace('ProcessState.', '')
- if agent_id in flask.app.agent_tty:
- total_num_lines = len(flask.app.agent_tty[agent_id])
+ if agent_id in app_context.agent_tty:
+ total_num_lines = len(app_context.agent_tty[agent_id])
for i in range(max(0, total_num_lines - max_log_lines), total_num_lines):
- tty_str += flask.app.agent_tty[agent_id][i]
+ tty_str += app_context.agent_tty[agent_id][i]
else:
tty_str = ""
tty_str = tty_str.replace("\n", "
")
- if agent_id in flask.app.agent_error:
- total_num_lines = len(flask.app.agent_error[agent_id])
+ if agent_id in app_context.agent_error:
+ total_num_lines = len(app_context.agent_error[agent_id])
for i in range(max(0, total_num_lines - max_log_lines), total_num_lines):
- error_str += flask.app.agent_error[agent_id][i]
+ error_str += app_context.agent_error[agent_id][i]
else:
error_str = ""
@@ -358,33 +374,35 @@ def get_agent_status(agent_id):
return {"status": status_str, "tty": tty_str, "error": error_str}, 200 # (OK)
-def stop_agent(agent_id):
+def stop_agent(agent_id: str):
"""Stop agent running."""
+ # pass to private function to make it easier to mock
+ return _stop_agent(agent_id)
+
+
+def _stop_agent(agent_id: str):
# Test if we have the process id
- if agent_id not in flask.app.agent_processes:
+ if agent_id not in app_context.agent_processes:
return {"detail": "Agent {} is not running".format(agent_id)}, 400 # 400 Bad request
- flask.app.agent_processes[agent_id].terminate()
- flask.app.agent_processes[agent_id].wait()
- del flask.app.agent_processes[agent_id]
+ app_context.agent_processes[agent_id].terminate()
+ app_context.agent_processes[agent_id].wait()
+ del app_context.agent_processes[agent_id]
return "stop_agent: All fine {}".format(agent_id), 200 # 200 (OK)
-def get_process_status(process_id) -> ProcessState:
+def get_process_status(process_id: subprocess.Popen) -> ProcessState:
"""Return the state of the execution."""
- if process_id is None:
- return ProcessState.NOT_STARTED
+ assert process_id is not None
return_code = process_id.poll()
if return_code is None:
return ProcessState.RUNNING
elif return_code <= 0:
return ProcessState.FINISHED
- elif return_code > 0:
- return ProcessState.FAILED
else:
- raise ValueError("Unexpected return code.")
+ return ProcessState.FAILED
def _kill_running_oef_nodes():
@@ -396,13 +414,16 @@ def create_app():
"""Run the flask server."""
CUR_DIR = os.path.abspath(os.path.dirname(__file__))
app = connexion.FlaskApp(__name__, specification_dir=CUR_DIR)
- flask.app.oef_process = None
- flask.app.agent_processes = {}
- flask.app.agent_tty = {}
- flask.app.agent_error = {}
- flask.app.ui_is_starting = False
- flask.app.agents_dir = os.path.abspath(os.getcwd())
- flask.app.module_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), "../../")
+ global app_context
+ app_context = AppContext()
+
+ app_context.oef_process = None
+ app_context.agent_processes = {}
+ app_context.agent_tty = {}
+ app_context.agent_error = {}
+ app_context.ui_is_starting = False
+ app_context.agents_dir = os.path.abspath(os.getcwd())
+ app_context.module_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), "../../")
app.add_api('aea_cli_rest.yaml')
@@ -425,11 +446,12 @@ def favicon():
return app
-def run():
+def run(port: int):
"""Run the GUI."""
_kill_running_oef_nodes()
+
app = create_app()
- app.run(host='127.0.0.1', port=8080, debug=False)
+ app.run(host='127.0.0.1', port=port, debug=False)
return app
@@ -438,8 +460,3 @@ def run_test():
"""Run the gui in the form where we can run tests against it."""
app = create_app()
return app.app.test_client()
-
-
-# If we're running in stand alone mode, run the application
-if __name__ == '__main__':
- run()
diff --git a/aea/cli_gui/__main__.py b/aea/cli_gui/__main__.py
index 9dd8cbe954..e4352e4ca3 100644
--- a/aea/cli_gui/__main__.py
+++ b/aea/cli_gui/__main__.py
@@ -20,6 +20,18 @@
"""Main entry point for CLI GUI."""
import aea.cli_gui
+import argparse
+
+parser = argparse.ArgumentParser(description='Launch the gui through python')
+parser.add_argument(
+ '-p',
+ '--port',
+ help='Port that the web server listens on',
+ type=int,
+ default=8080)
+
+args, unknown = parser.parse_known_args()
+
# If we're running in stand alone mode, run the application
if __name__ == '__main__':
- aea.cli_gui.run()
+ aea.cli_gui.run(args.port) # pragma: no cover
diff --git a/aea/cli_gui/aea_cli_rest.yaml b/aea/cli_gui/aea_cli_rest.yaml
index 308105bd77..51f68a1269 100644
--- a/aea/cli_gui/aea_cli_rest.yaml
+++ b/aea/cli_gui/aea_cli_rest.yaml
@@ -61,7 +61,7 @@ paths:
type: string
required: True
responses:
- 201:
+ 200:
description: Agent deleted successfully
schema:
type: string
@@ -222,19 +222,37 @@ paths:
items:
type: string
+ /{item_type}/{search_term}:
+ get:
+ operationId: aea.cli_gui.search_registered_items
+ tags:
+ - agents
+ summary: Return list of all registered items (protocols, connections, skills)
+ parameters:
+ - name: item_type
+ in: path
+ description: type of item to remove
+ type: string
+ required: True
+ - name: search_term
+ in: path
+ description: type of item to remove
+ type: string
+ required: True
+ responses:
+ 200:
+ description: Successfully read item list operation
+ schema:
+ type: array
+ items:
+ type: string
+
/oef:
post:
operationId: aea.cli_gui.start_oef_node
tags:
- oef
summary: Start an OEF node that our agents can communicate with
- parameters:
- - name: dummy
- in: body
- description: Seeing if this makes it work
- schema:
- type: string
- required: True
responses:
201:
description: Start the OEF Nodoe
diff --git a/aea/cli_gui/static/css/home.css b/aea/cli_gui/static/css/home.css
index ea32d223e4..f3dee5d8f3 100644
--- a/aea/cli_gui/static/css/home.css
+++ b/aea/cli_gui/static/css/home.css
@@ -4,156 +4,464 @@
@import url(http://fonts.googleapis.com/css?family=Roboto:400,300,500,700);
-body, .ui-btn {
- font-family: Roboto;
+
+/* http://meyerweb.com/eric/tools/css/reset/
+ v2.0 | 20110126
+ License: none (public domain)
+*/
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ font: inherit;
+ vertical-align: baseline;
+ cursor: auto;
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure,
+footer, header, hgroup, menu, nav, section {
+ display: block;
+}
+body {
+ line-height: 1.1;
+}
+ol, ul {
+ list-style: none;
+}
+blockquote, q {
+ quotes: none;
}
+blockquote:before, blockquote:after,
+q:before, q:after {
+ content: '';
+ content: none;
+}
+
+table{
+ border-collapse: collapse;
+ border-spacing: 0;
+ table-layout: fixed;
-.container {
- padding: 10px;
+}
+
+
+
+/* colours
+$primary-colour: #1e2844;
+
+$secondary-blue: #5887da;
+$secondary-green: #2bb5a2;
+$secondary-pink: #f176a9;
+$secondary-mauve: #a45a95;
+$secondary-purple: $secondary-mauve;
+$secondary-orange: #f6923b;
+$secondary-yellow: #fdda64;
+$neutral-dark-grey: #0d0d0d;
+$neutral-dark-cyan: #33383e;
+$neutral-cyan: #467083;
+$neutral-light-cyan: #a2b7c1;
+$neutral-mid-grey: #d1dbe0;
+$neutral-light-grey: #ecf1f3;
+$overlay: rgba(0, 0, 0, 0.5);
+
+*/
+
+
+/* Fetch.ai branding colours */
+:root {
+
+ --primary-colour: #1e2844;
+ --secondary-blue: #5887da;
+ --neutral-mid-grey: #d1dbe0;
+ --neutral-light-grey: #ecf1f3;
+ --neutral-dark-grey: #0d0d0d;
+ --neutral-dark-cyan: #33383e;
+ --neutral-cyan: #467083;
+ --neutral-light-cyan: #a2b7c1;
+ --secondary-orange: #f6923b;
+
+
+}
+
+/* apply a natural box layout model to all elements, but allowing components to change */
+html {
+ box-sizing: border-box;
+}
+*,
+*:before,
+*:after {
+ box-sizing: inherit;
+}
+html,
+body {
+ margin: 0;
+ padding: 0;
+
+ font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ font-weight: 300;
+ font-size: 10px;
+ text-rendering: optimizeLegibility;
+ letter-spacing: .2px;
+ color: var(--primary-colour);
+
+}
+
+
+.center-me {
+ margin: 0 auto;
+}
+
+.addLeftMargin{
+ margin-left: 40px;
+}
+
+.addRightMargin{
+ margin-right: 40px;
+}
+
+/* Account for the border spacing on tables */
+.addLeftMarginBordered{
+ margin-left: 30px;
+}
+
+.addRightMarginBordered{
+ margin-right: 30px;
}
.banner {
- text-align: center;
+ background-color: var(--primary-colour);
+ width: 100%;
+ height: 100px;
+ color: white;
+
+}
+
+.innerMargin {
+ max-width: 1000px;
+ width: 100%;
+ height: 100px;
+
}
-table {
- table-layout: fixed ;
- width: 100% ;
+.leftBanner {
+ width: 50%;
+ height: 100px;
+ vertical-align: middle;
}
-.mainTable {
- vertical-align: top;
+.rightBanner {
width: 50%;
+ vertical-align: middle;
+ table-layout: fixed;
}
-.editor {
- width: 90%;
- margin-left: auto;
- margin-right: auto;
- padding: 5px;
- border: 1px solid lightgrey;
- border-radius: 3px;
- margin-bottom: 5px;
+
+.logo {
+ height: 25%;
+ width: auto;
+ margin-left: 40px;
}
+.pageTitle {
+ text-align: right;
+ margin-right: 40px;
+ font-size: 20px;
-label {
- display: inline-block;
- margin-bottom: 5px;
}
-button {
- padding: 5px;
- margin-right: 5px;
- border-radius: 3px;
- background-color: #eee;
+/* Wigets */
+
+label{
+ vertical-align: middle;
}
-table {
- width: 100%;
- border-collapse: collapse;
- text-align: center;
+
+input{
+ height: 18px;
+ vertical-align: middle;
+ font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ font-weight: 400;
+ font-size: 10px;
+ outline: none;
+ border-width: 1px;
+ border-style: solid;
+ border-color: var(--neutral-mid-grey);
}
-table, caption, th, td {
- border: 1px solid lightgrey;
+
+input:focus {
+ border-color: var(--primary-colour);
}
-table caption {
- height: 33px;
- font-weight: bold;
- padding-top: 5px;
- border-bottom: none;
+
+ .interactive{
+ cursor: pointer;
+ }
+
+
+button{
+ height: 18px;
+ vertical-align: middle;
+ background-color: var(--neutral-cyan);
+ border-width: 0px;
+ outline: none;
+ color: white;
+ display: inline-block;
+ cursor: pointer;
}
-tr {
- height: 33px;
+button:hover {
+ background-color: var(--primary-colour);
+ color: white;
+ cursor: pointer;
}
+button:disabled,
+button[disabled]{
+ border: 1px solid #999999;
+ background-color: #cccccc;
+ color: #666666;
+ cursor: auto;
+}
+
+button:active {
+ background-color: var(--neutral-light-cyan);
+}
-td {
- padding-left: 5px;
+
+/* tables */
+th{
text-align: left;
}
-.error{
- position: fixed; /* Sit on top of the page content */
- width: 100%; /* Full width (cover the whole page) */
- height: 10%; /* A bit of the page */
- bottom: 0;
- visibility: hidden;
- border: 1px solid lightgrey;
- border-radius: 3px;
- background-color: #fbbd;
+
+/* main area */
+.mainContent {
+ color: black;
+}
+
+.simpleBox{
+ display: block
+
+}
+
+.simpleOutlinedBox{
+ display: block;
+ border-width: 1px;
+ border-style: solid;
+ border-color: var(--neutral-light-grey);
+}
+
+.lightFill{
+ background-color: var(--neutral-light-grey);
+}
+
+.whiteFill{
+ background-color: white;
+}
+
+.idWidth{
+ width: 25%
+}
+
+.descriptionWidth{
+ width: 75%
+}
+
+.halfSpaceAllRound{
+ margin-top: 5px;
+ margin-bottom: 5px;
+ margin-right: 5px;
+ margin-left: 5px;
+}
+
+.spaceAllRound{
+ margin-top: 10px;
+ margin-bottom: 10px;
+ margin-right: 10px;
+ margin-left: 10px;
+}
+
+.spaceAllRoundNoTop{
+ margin-top: 0px;
+ margin-bottom: 10px;
+ margin-right: 10px;
+ margin-left: 10px;
+}
+
+.spaceBottom{
+ margin-top: 0px;
+ margin-bottom: 20px;
+ margin-right: 00px;
+ margin-left: 0px;
+}
+
+.bigSpaceBottom{
+ margin-top: 0px;
+ margin-bottom: 30px;
+ margin-right: 00px;
+ margin-left: 0px;
+}
+
+.halfSpaceBottom{
+ margin-top: 0px;
+ margin-bottom: 10px;
+ margin-right: 00px;
+ margin-left: 0px;
+}
+
+
+.tableSpaceAllRound{
+ margin-top: 5px;
+ margin-bottom: 5px;
+ margin-right: 5px;
+ margin-left: 5px;
+}
+
+
+.fullWidth{
+ width: 100%;
+}
+
+
+.mainTable{
+ border-collapse: separate;
+ border-spacing: 10px;
+}
+
+.mainTableSide {
+ width: 50%;
+ vertical-align: top;
+
+}
+
+.contentTitle {
+ height: 40px;
+ margin-right: 40px;
+ margin-top: 10px;
+ font-size: 18px;
text-align: center;
+ vertical-align: middle;
+
+}
+
+.sectionTitle {
font-weight: bold;
+ padding: 0px;
+ margin-top: 4px;
+ margin-bottom: 4px;
+ margin-top: 8px;
+ margin-left: auto;
+ margin-right: auto;
+ text-align: center;
+ vertical-align: middle;
+}
+
+/*aea tables*/
+
+.aea table{
+ table-layout: fixed;
}
-/* Split the screen in half */
-.split {
- height: 90%;
- width: 50%;
- position: fixed;
- z-index: 1;
- top: 1;
- overflow-x: hidden;
- padding-top: 20px;
+.aea caption {
+ font-weight: bold;
+ padding: 0px;
+ margin-top: 8px;
+ margin-bottom: 4px;
+
}
-/* Control the left side */
-.left {
- left: 0;
+.aea th {
+ font-weight: 400;
+ padding: 2px;
+ border: none;
+ outline: none;
+ background-color: var(--neutral-mid-grey);
}
+/*
+tr {
+ border: none;
+ outline: none;
+}
+
-/* Control the right side */
-.right {
- right: 0;
+.aea_selected{
+ border: 1px;
+ border-style: solid;
+ border-color: var(--secondary-blue);
}
+*/
+
+tr {
+ padding: 2px;
+ border: none;
+ outline: none;
-/* If you want the content centered horizontally and vertically */
-.centered {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- text-align: center;
}
-/* Style the image inside the centered container, if needed */
-.centered img {
- width: 150px;
- border-radius: 50%;
+
+.aea_selected{
+ padding: 2px;
+ outline: 2px;
+ outline-style: solid;
+ outline-color: var(--secondary-blue);
+}
+.aea td {
+ padding: 2px;
+ border: none;
+ outline: none;
}
.code {
display:block;
overflow: scroll;
- width: 90%;
height:150px;
- margin-left: auto;
- margin-right: auto;
- padding: 5px;
- border: 1px solid lightgrey;
- border-radius: 3px;
+ margin-left: 10px;
+ margin-right: 10px;
background-color: black;
color: white;
font-family: "Courier New", Courier, monospace;
+ padding: 8px;
}
.codeError {
display:block;
overflow: scroll;
- width: 90%;
height:150px;
- margin-left: auto;
- margin-right: auto;
- padding: 5px;
- border: 1px solid lightgrey;
- border-radius: 3px;
+ margin-left: 10px;
+ margin-right: 10px;
background-color: black;
color: red;
font-family: "Courier New", Courier, monospace;
+ padding: 8px;
}
-own:hover .dropbtn {background-color: #3e8e41;}w {display:block;}
\ No newline at end of file
+
+.error{
+ position: fixed; /* Sit on top of the page content */
+ width: 100%; /* Full width (cover the whole page) */
+ height: 10%; /* A bit of the page */
+ bottom: 0;
+ visibility: hidden;
+ border: 1px solid lightgrey;
+ border-radius: 3px;
+ background-color: #fbbd;
+ text-align: center;
+ font-weight: bold;
+
+}
\ No newline at end of file
diff --git a/aea/cli_gui/static/logo.png b/aea/cli_gui/static/logo.png
new file mode 100644
index 0000000000..051c4501f5
Binary files /dev/null and b/aea/cli_gui/static/logo.png differ
diff --git a/aea/cli_gui/templates/home.html b/aea/cli_gui/templates/home.html
index c604fdb9e7..9229485f22 100644
--- a/aea/cli_gui/templates/home.html
+++ b/aea/cli_gui/templates/home.html
@@ -12,148 +12,179 @@
-
-
-
Fetch.AI AEA CLI REST API
-
+
+
-
- Local Agents
-
-
-
-
-
-
-
-
- Local Agents
-
-
- Agent id |
- Description |
-
-
-
-
-
-
-
+ |
+
+ |
+
+ AEA CLI REST API
+ |
+
+
+
-
-
Selected Agent: NONE
-
- {% for i in range(0, len) %}
- {% if htmlElements[i][0] == "local" and htmlElements[i][1] != "agent" %}
-
-
-
-
-
- Local's {{htmlElements[i][1]}}s
+
+
+
+
+
+ Local
+
+
+
+
+
+
+
+
+
+
+
+ Local Agents
- {{htmlElements[i][1]}} id |
- Description |
+ Agent id |
+ Description |
-
+
-
- Selected {{htmlElements[i][1]}}: NONE
-
-
-
-
-
-
-
- {% endif %}
- {% endfor %}
-
-
-
+
+
-
+
+ {% for i in range(0, len) %}
+ {% if htmlElements[i][0] == "local" and htmlElements[i][1] != "agent" %}
+
+
+
+
+ Local's {{htmlElements[i][1]}}s
+
+
+ {{htmlElements[i][1]}} id |
+ Description |
+
+
+
+
+
+
+
+ Selected {{htmlElements[i][1]}}: NONE
+
+
+
+
+
+
+
+ {% endif %}
+ {% endfor %}
+
+
+
-
- Running "NONE" Agent
+ Running "NONE" Agent
+
+
+
+
+
+ Agent Status: NONE
+
+
+
-
-
-
-
- Agent Status: NONE
-
-
-
-
-
-
-
|
-
- The Registry
-
- {% for i in range(0, len) %}
- {% if htmlElements[i][0] == "registered"%}
-
-
-
- Registered {{htmlElements[i][1]}}s
+
+ Registry
+
+
+ Search registry
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Search results
- {{htmlElements[i][1]}} id |
- Description |
+ NONE id |
+ Description |
-
+
-
- Selected {{htmlElements[i][1]}}: NONE
-
-
-
- {% endif %}
- {% endfor %}
-
- OEF Node
+
+
-
-
- OEF Node Status: NONE
-
-
-
+
+
+ OEF Node
+
+
+
+ OEF Node Status: NONE
+
+
+
-
-
-
+
|
+
+
+
+
+
|