diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b461c8ae..761766c5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -29,3 +29,30 @@ _Put an `x` in the boxes that apply._ ## Further comments If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... + +DELETE INCLUSIVE THIS AND BELOW FOR STANDARD PR +------ + +## Release summary + +Version number: [e.g. 1.0.1] + +## Release details + +Describe in short the main changes with the new release. + +## Checklist + +_Put an `x` in the boxes that apply._ + +- [ ] I have read the [CONTRIBUTING](../master/CONTRIBUTING.rst) doc +- [ ] I am making a pull request against the `master` branch (left side), from `develop` +- [ ] Lint and unit tests pass locally +- [ ] I built the documentation and updated it with the latest changes +- [ ] I've added an item in `HISTORY.rst` for this release +- [ ] I bumped the version number in the `tac/__version__.py` file. +- [ ] I bumped the version number in every Docker image of the repo and published it. Also, I built and published them with tag `latest` + +## Further comments + +Write here any other comment about the release, if any. diff --git a/.gitignore b/.gitignore index c2f3efb6..2395e550 100644 --- a/.gitignore +++ b/.gitignore @@ -118,6 +118,11 @@ data/* !data/oef-logs data/oef-logs/* !data/.gitkeep +!data/shared +data/shared/* +!data/shared/.gitkeep +scripts/data/* + notebooks/.ipynb_checkpoints diff --git a/HISTORY.rst b/HISTORY.rst index ce487b06..790d4c61 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -46,3 +46,9 @@ Release History - Adds versioning to TAC via `version_id` - Ports `tac` protocol from aea repo - Multiple small fixes + +0.1.7 (2019-10-02) +------------------- + +- Improves launcher gui +- Multiple small fixes diff --git a/README.md b/README.md index cbee3a6a..45b09243 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,23 @@ This repository contains submodules. Clone with recursive strategy: The controller GUI at http://localhost:8097 provides real time insights. -## Option 2: Step by step: +## Option 2: Launcher GUI: -- [x] You have followed the steps under 'Dependencies' and 'Preliminaries' below +- [x] Follow the steps under 'Dependencies' and 'Preliminaries' below +- [x] Build the sandbox: + + cd sandbox && docker-compose build && cd .. + +- [x] Enter the virtual environment and start the launcher GUI. Then launch the sandbox with your prefered configs: + + pipenv shell + python tac/gui/launcher/app.py + +The controller GUI at http://localhost:8097 provides real time insights. + +## Option 3: Step by step: + +- [x] Follow the steps under 'Dependencies' and 'Preliminaries' below - [x] In one terminal, build the sandbox and then launch it: cd sandbox && docker-compose build @@ -29,7 +43,7 @@ The controller GUI at http://localhost:8097 provides real time insights. - [x] Optionally, in another terminal, enter the virtual environment and connect a template agent to the sandbox: pipenv shell - python templates/v1/basic.py --name my_agent --dashboard + python templates/v1/basic.py --name my_agent --dashboard --expected-version-id tac_v1 The sandbox is starting up:

diff --git a/data/shared/.gitkeep b/data/shared/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sandbox/.env b/sandbox/.env index 98d3cb13..c9ef2e3b 100644 --- a/sandbox/.env +++ b/sandbox/.env @@ -5,7 +5,7 @@ SERVICES_INTERVAL=5 OEF_ADDR=172.28.1.1 OEF_PORT=10000 DATA_OUTPUT_DIR=data -VERSION_ID=1 +VERSION_ID=tac_v1 LOWER_BOUND_FACTOR=0 UPPER_BOUND_FACTOR=0 TX_FEE=0.1 @@ -16,4 +16,5 @@ SEED=42 WHITELIST= REGISTER_AS=both SEARCH_FOR=both -PENDING_TRANSACTION_TIMEOUT=120 \ No newline at end of file +PENDING_TRANSACTION_TIMEOUT=120 +SHARED_DIR="./../data/shared" \ No newline at end of file diff --git a/sandbox/docker-compose.yml b/sandbox/docker-compose.yml index 848a624d..ba43d907 100644 --- a/sandbox/docker-compose.yml +++ b/sandbox/docker-compose.yml @@ -106,6 +106,12 @@ services: - "${SEED}" - "--whitelist-file" - "${WHITELIST}" + - "--version-id" + - "${VERSION_ID}" + volumes: + - type: bind + source: ${SHARED_DIR} + target: /build/data/shared networks: main_net: ipam: diff --git a/sandbox/playground.py b/sandbox/playground.py index 355763c2..1328bfcd 100644 --- a/sandbox/playground.py +++ b/sandbox/playground.py @@ -40,7 +40,7 @@ from tac.agents.participant.v1.examples.strategy import BaselineStrategy from tac.platform.game.base import GameData -CUR_PATH = inspect.getfile(inspect.currentframe()) +CUR_PATH = inspect.getfile(inspect.currentframe()) # type: ignore ROOT_DIR = os.path.join(os.path.dirname(CUR_PATH), "..") diff --git a/sandbox/run_iterated_games.py b/sandbox/run_iterated_games.py index ef2eb865..60120114 100644 --- a/sandbox/run_iterated_games.py +++ b/sandbox/run_iterated_games.py @@ -37,7 +37,7 @@ from tac.platform.game.stats import GameStats -OUR_DIRECTORY = os.path.dirname(inspect.getfile(inspect.currentframe())) +OUR_DIRECTORY = os.path.dirname(inspect.getfile(inspect.currentframe())) # type: ignore ROOT_DIR = os.path.join(OUR_DIRECTORY, "..") logging.basicConfig(level=logging.INFO) diff --git a/scripts/launch.py b/scripts/launch.py index eadd70ae..d6573a30 100644 --- a/scripts/launch.py +++ b/scripts/launch.py @@ -30,8 +30,9 @@ import docker from tac.agents.participant.v1.examples.baseline import main as participant_agent_main +from tac.platform.shared_sim_status import register_shared_dir, get_shared_dir -CUR_PATH = inspect.getfile(inspect.currentframe()) +CUR_PATH = inspect.getfile(inspect.currentframe()) # type: ignore ROOT_DIR = os.path.join(os.path.dirname(CUR_PATH), "..") @@ -59,6 +60,9 @@ def __enter__(self): self._stop_oef_search_images() self._build_sandbox() + register_shared_dir(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/shared')) + os.environ['SHARED_DIR'] = get_shared_dir() + print("Launching sandbox...") self.sandbox_process = subprocess.Popen(["docker-compose", "up", "--abort-on-container-exit"], env=os.environ, @@ -80,11 +84,11 @@ def wait_for_oef(): ":" ], env=os.environ, cwd=ROOT_DIR) - wait_for_oef.wait(30) + wait_for_oef.wait(60) if __name__ == '__main__': with Sandbox(): wait_for_oef() - participant_agent_main(name="my_agent", dashboard=True) + participant_agent_main(name="my_agent", dashboard=True, expected_version_id='tac_v1') diff --git a/scripts/launch_alt.py b/scripts/launch_alt.py index 2616802c..20bbd9f1 100644 --- a/scripts/launch_alt.py +++ b/scripts/launch_alt.py @@ -33,9 +33,10 @@ import tac from tac.platform.oef_health_check import OEFHealthCheck +from tac.platform.shared_sim_status import register_shared_dir, get_shared_dir from tac.platform.simulation import parse_arguments, build_simulation_parameters -CUR_PATH = inspect.getfile(inspect.currentframe()) +CUR_PATH = inspect.getfile(inspect.currentframe()) # type: ignore ROOT_DIR = os.path.join(os.path.dirname(CUR_PATH), "..") stack_tracer = importlib.import_module("stack_tracer", package=CUR_PATH) @@ -83,6 +84,10 @@ def __enter__(self): self._stop_oef_search_images() script_path = os.path.join("scripts", "oef", "launch.py") configuration_file_path = os.path.join("scripts", "oef", "launch_config.json") + + register_shared_dir(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/shared')) + os.environ['SHARED_DIR'] = get_shared_dir() + print("Launching new OEF Node...") self.oef_process = subprocess.Popen(["python3", script_path, "-c", configuration_file_path, "--background"], stdout=subprocess.PIPE, env=os.environ, cwd=ROOT_DIR) @@ -103,7 +108,7 @@ def _get_image_id(self): if __name__ == '__main__': - sys.argv += ['--dashboard'] + sys.argv += ['--dashboard', '--version-id', 'tac_v1'] args = parse_arguments() simulation_params = build_simulation_parameters(args) diff --git a/setup.py b/setup.py index e0d7263e..710d19fa 100644 --- a/setup.py +++ b/setup.py @@ -80,9 +80,11 @@ + glob.glob("sandbox/*.py") + glob.glob("sandbox/*.sh")), ("templates/v1", glob.glob("templates/v1/*.py")), + ("scripts/oef", glob.glob("scripts/oef/*.json")), ("simulation/v1", glob.glob("simulation/v1/*")), ("oef_search_pluto_scripts", glob.glob("oef_search_pluto_scripts/*.py") + glob.glob("oef_search_pluto_scripts/*.json")) ], license=about['__license__'], ) + diff --git a/simulation/v1/tac_agent_spawner.py b/simulation/v1/tac_agent_spawner.py index 0d6a6966..109d695c 100644 --- a/simulation/v1/tac_agent_spawner.py +++ b/simulation/v1/tac_agent_spawner.py @@ -19,10 +19,14 @@ # ------------------------------------------------------------------------------ """Spawn several TAC agents.""" +import os +from tac.platform.shared_sim_status import register_shared_dir from tac.platform.simulation import parse_arguments, build_simulation_parameters, run if __name__ == '__main__': + register_shared_dir(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../data/shared')) + arguments = parse_arguments() simulation_parameters = build_simulation_parameters(arguments) run(simulation_parameters) diff --git a/tac/__init__.py b/tac/__init__.py index f6c941d0..42cfd044 100644 --- a/tac/__init__.py +++ b/tac/__init__.py @@ -36,5 +36,5 @@ logger.addHandler(handler) logger.propagate = False -ROOT_DIR = os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), "..") +ROOT_DIR = os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), "..") # type: ignore diff --git a/tac/__version__.py b/tac/__version__.py index 75006734..78337f09 100644 --- a/tac/__version__.py +++ b/tac/__version__.py @@ -23,7 +23,7 @@ __title__ = 'tac' __description__ = 'Trading Agent Competition agents' __url__ = 'https://github.com/fetchai/agents-tac.git' -__version__ = '0.1.6' +__version__ = '0.1.7' __author__ = 'Fetch.AI Limited' __license__ = 'Apache 2.0' __copyright__ = '2019 Fetch.AI Limited' diff --git a/tac/agents/controller/agent.py b/tac/agents/controller/agent.py index cdce820a..794db6b1 100644 --- a/tac/agents/controller/agent.py +++ b/tac/agents/controller/agent.py @@ -20,16 +20,16 @@ # ------------------------------------------------------------------------------ """This module contains the ControllerAgent.""" - import argparse import datetime +import dateutil.parser import logging import pprint import random import time from typing import Optional -import dateutil.parser + from aea.agent import Agent from aea.channels.oef.connection import OEFMailBox from aea.mail.base import Envelope @@ -37,6 +37,7 @@ from tac.agents.controller.base.handlers import OEFHandler, GameHandler, AgentMessageDispatcher from tac.agents.controller.base.tac_parameters import TACParameters from tac.platform.game.base import GamePhase +from tac.platform.shared_sim_status import set_controller_state, ControllerAgentState from tac.gui.monitor import Monitor, NullMonitor, VisdomMonitor if __name__ != "__main__": @@ -82,6 +83,7 @@ def __init__(self, name: str, self.last_activity = datetime.datetime.now() logger.debug("[{}]: Initialized myself as Controller Agent :\n{}".format(self.name, pprint.pformat(vars()))) + set_controller_state(self.game_handler.tac_parameters.version_id, ControllerAgentState.STARTING) def act(self) -> None: """ @@ -96,10 +98,14 @@ def act(self) -> None: logger.debug("[{}]: waiting for starting the competition: start_time={}, current_time={}, timedelta ={}s" .format(self.name, str(self.game_handler.tac_parameters.start_time), str(now), seconds_to_wait)) self.game_handler.competition_start = now + datetime.timedelta(seconds=seconds_to_wait + self.game_handler.tac_parameters.registration_timedelta.seconds) + time.sleep(seconds_to_wait) logger.debug("[{}]: Register competition with parameters: {}" .format(self.name, pprint.pformat(self.game_handler.tac_parameters.__dict__))) self.oef_handler.register_tac() + + set_controller_state(self.game_handler.tac_parameters.version_id, ControllerAgentState.REGISTRATION_OPEN) + self.game_handler._game_phase = GamePhase.GAME_SETUP elif self.game_handler.game_phase == GamePhase.GAME_SETUP: assert self.game_handler.competition_start is not None, "No competition start time set!" @@ -108,6 +114,9 @@ def act(self) -> None: logger.debug("[{}]: Checking if we can start the competition.".format(self.name)) min_nb_agents = self.game_handler.tac_parameters.min_nb_agents nb_reg_agents = len(self.game_handler.registered_agents) + + set_controller_state(self.game_handler.tac_parameters.version_id, ControllerAgentState.RUNNING) + if nb_reg_agents >= min_nb_agents: logger.debug("[{}]: Start competition. Registered agents: {}, minimum number of agents: {}." .format(self.name, nb_reg_agents, min_nb_agents)) @@ -115,6 +124,7 @@ def act(self) -> None: else: logger.debug("[{}]: Not enough agents to start TAC. Registered agents: {}, minimum number of agents: {}." .format(self.name, nb_reg_agents, min_nb_agents)) + set_controller_state(self.game_handler.tac_parameters.version_id, ControllerAgentState.STOPPING_UNSUFFICIENT_AGENTS) self.stop() return elif self.game_handler.game_phase == GamePhase.GAME: @@ -122,10 +132,12 @@ def act(self) -> None: inactivity_duration = current_time - self.last_activity if inactivity_duration > self.game_handler.tac_parameters.inactivity_timedelta: logger.debug("[{}]: Inactivity timeout expired. Terminating...".format(self.name)) + set_controller_state(self.game_handler.tac_parameters.version_id, ControllerAgentState.FINISHED_INACTIVITY) self.stop() return elif current_time > self.game_handler.tac_parameters.end_time: logger.debug("[{}]: Competition timeout expired. Terminating...".format(self.name)) + set_controller_state(self.game_handler.tac_parameters.version_id, ControllerAgentState.FINISHED_GAME_TIMEOUT) self.stop() return diff --git a/tac/gui/dashboards/agent.py b/tac/gui/dashboards/agent.py index ef141d84..777ac525 100644 --- a/tac/gui/dashboards/agent.py +++ b/tac/gui/dashboards/agent.py @@ -32,7 +32,7 @@ from tac.gui.dashboards.helpers import generate_html_table_from_dict, escape_html from tac.platform.game.base import Transaction -CUR_PATH = inspect.getfile(inspect.currentframe()) +CUR_PATH = inspect.getfile(inspect.currentframe()) # type: ignore CUR_DIR = os.path.dirname(CUR_PATH) ROOT_PATH = os.path.join(CUR_DIR, "..", "..") diff --git a/tac/gui/dashboards/base.py b/tac/gui/dashboards/base.py index b324c342..4336aa81 100644 --- a/tac/gui/dashboards/base.py +++ b/tac/gui/dashboards/base.py @@ -28,7 +28,7 @@ from visdom import Visdom -CUR_PATH = inspect.getfile(inspect.currentframe()) +CUR_PATH = inspect.getfile(inspect.currentframe()) # type: ignore CUR_DIR = os.path.dirname(CUR_PATH) diff --git a/tac/gui/launcher/api/__init__.py b/tac/gui/launcher/api/__init__.py index 37850ab3..240b772a 100644 --- a/tac/gui/launcher/api/__init__.py +++ b/tac/gui/launcher/api/__init__.py @@ -17,12 +17,13 @@ # limitations under the License. # # ------------------------------------------------------------------------------ +"""Register the resources with flask and set up the shared status file.""" -"""Define the REST APIs for the launcher app.""" from flask_restful import Api from .resources.sandboxes import SandboxList, Sandbox from .resources.agents import Agent +from tac.platform.shared_sim_status import clear_temp_dir def create_api(app): @@ -32,3 +33,5 @@ def create_api(app): api.add_resource(SandboxList, "/sandboxes") api.add_resource(Sandbox, "/sandboxes/") api.add_resource(Agent, "/agent") + + clear_temp_dir() diff --git a/tac/gui/launcher/api/resources/agents.py b/tac/gui/launcher/api/resources/agents.py index 89d2e37a..db71654b 100644 --- a/tac/gui/launcher/api/resources/agents.py +++ b/tac/gui/launcher/api/resources/agents.py @@ -20,14 +20,15 @@ """Implement the agent resource and other utility classes.""" +from enum import Enum import logging import os import subprocess -from enum import Enum from typing import Dict, Any, Optional from flask_restful import Resource, reqparse +from tac.platform.shared_sim_status import get_agent_state, remove_agent_state from tac import ROOT_DIR logger = logging.getLogger(__name__) @@ -38,11 +39,13 @@ parser.add_argument("max_reactions", type=int, default=100, help="The maximum number of reactions (messages processed) per call to react.") parser.add_argument("register_as", choices=['seller', 'buyer', 'both'], default='both', help="The string indicates whether the baseline agent registers as seller, buyer or both on the oef.") parser.add_argument("search_for", choices=['sellers', 'buyers', 'both'], default='both', help="The string indicates whether the baseline agent searches for sellers, buyers or both on the oef.") -parser.add_argument("is_world_modeling", type=bool, default=False, help="Whether the agent uses a workd model or not.") +parser.add_argument("is_world_modeling", type=bool, default=False, help="Whether the agent uses a world model or not.") parser.add_argument("services_interval", type=int, default=5, help="The number of seconds to wait before doing another search.") parser.add_argument("pending_transaction_timeout", type=int, default=30, help="The timeout in seconds to wait for pending transaction/negotiations.") parser.add_argument("private_key_pem", default=None, help="Path to a file containing a private key in PEM format.") +parser.add_argument("expected_version_id", default="", help="Version id of the game we are trying to connect to") parser.add_argument("rejoin", type=bool, default=False, help="Whether the agent is joining a running TAC.") +parser.add_argument("btn-start-agent", default="Test", help="Test") current_agent = None # type: Optional[AgentRunner] @@ -82,7 +85,8 @@ def __call__(self): "--register-as", str(self.params["register_as"]), "--search-for", str(self.params["search_for"]), "--services-interval", str(self.params["services_interval"]), - "--pending-transaction-timeout", str(self.params["pending_transaction_timeout"])] + "--pending-transaction-timeout", str(self.params["pending_transaction_timeout"]), + "--expected-version-id", str(self.params["expected_version_id"])] if self.params["is_world_modeling"]: args.append("--is-world-modeling") @@ -94,7 +98,7 @@ def __call__(self): self.process = subprocess.Popen([ "python3", - os.path.join(ROOT_DIR, "templates", "v1", "basic.py"), + os.path.join(ROOT_DIR, "tac", "gui", "launcher", "api", "resources", "reporting_agent.py"), *args, "--dashboard", "--visdom-addr", "127.0.0.1", @@ -118,14 +122,23 @@ def status(self) -> AgentState: def to_dict(self): """Serialize the object into a dictionary.""" + game_id = self.params["expected_version_id"] + agent_status = get_agent_state(game_id) + if (agent_status is not None): + agent_status_text = agent_status.value + else: + agent_status_text = "Uninitialised" + return { "id": self.id, - "status": self.status.value, + "process_status": self.status.value, + "agent_status": agent_status_text, "params": self.params } def stop(self): """Stop the execution of the sandbox.""" + remove_agent_state(self.params["expected_version_id"]) try: self.process.terminate() self.process.wait() @@ -143,7 +156,7 @@ def get(self): if current_agent is not None: return current_agent.to_dict(), 200 else: - return None, 200 + return "", 200 def post(self): """Create an agent instance.""" diff --git a/tac/gui/launcher/api/resources/reporting_agent.py b/tac/gui/launcher/api/resources/reporting_agent.py new file mode 100644 index 00000000..32cd7336 --- /dev/null +++ b/tac/gui/launcher/api/resources/reporting_agent.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Like the basic agents but with thread to report on status to shared file.""" + +import argparse +import logging +import threading +import time + + +from tac.agents.participant.v1.base.strategy import RegisterAs, SearchFor +from tac.agents.participant.v1.examples.baseline import BaselineAgent +from tac.agents.participant.v1.examples.strategy import BaselineStrategy +from tac.gui.dashboards.agent import AgentDashboard +from tac.platform.shared_sim_status import set_agent_state + +logger = logging.getLogger(__name__) + + +def parse_arguments(): + """Arguments parsing.""" + parser = argparse.ArgumentParser("my_agent", description="Launch my agent.") + parser.add_argument("--name", default="my_baseline_agent", help="Name of the agent.") + parser.add_argument("--oef-addr", default="127.0.0.1", help="TCP/IP address of the OEF Agent") + parser.add_argument("--oef-port", default=10000, help="TCP/IP port of the OEF Agent") + parser.add_argument("--agent-timeout", type=float, default=1.0, help="The time in (fractions of) seconds to time out an agent between act and react.") + parser.add_argument("--max-reactions", type=int, default=100, help="The maximum number of reactions (messages processed) per call to react.") + parser.add_argument("--register-as", choices=['seller', 'buyer', 'both'], default='both', help="The string indicates whether the baseline agent registers as seller, buyer or both on the oef.") + parser.add_argument("--search-for", choices=['sellers', 'buyers', 'both'], default='both', help="The string indicates whether the baseline agent searches for sellers, buyers or both on the oef.") + parser.add_argument("--is-world-modeling", type=bool, default=False, help="Whether the agent uses a workd model or not.") + parser.add_argument("--services-interval", type=int, default=5, help="The number of seconds to wait before doing another search.") + parser.add_argument("--pending-transaction-timeout", type=int, default=30, help="The timeout in seconds to wait for pending transaction/negotiations.") + parser.add_argument("--private-key-pem", default=None, help="Path to a file containing a private key in PEM format.") + parser.add_argument("--expected-version-id", type=str, help="The expected version id of the TAC.") + parser.add_argument("--rejoin", action="store_true", default=False, help="Whether the agent is joining a running TAC.") + parser.add_argument("--dashboard", action="store_true", help="Show the agent dashboard.") + parser.add_argument("--visdom-addr", type=str, default="localhost", help="IP address to the Visdom server") + parser.add_argument("--visdom-port", type=int, default=8097, help="Port of the Visdom server") + + return parser.parse_args() + + +def monitor_status(agent): + """Poll agent and reports on status.""" + while True: + set_agent_state(str(agent._game_instance.expected_version_id), agent.agent_state) + time.sleep(1) + + +def main(): + """Run the script.""" + args = parse_arguments() + + if args.dashboard: + agent_dashboard = AgentDashboard(agent_name=args.name, visdom_addr=args.visdom_addr, visdom_port=args.visdom_port, env_name=args.name) + else: + agent_dashboard = None + + set_agent_state(args.expected_version_id, None) + + strategy = BaselineStrategy(register_as=RegisterAs(args.register_as), search_for=SearchFor(args.search_for), is_world_modeling=args.is_world_modeling) + agent = BaselineAgent(name=args.name, oef_addr=args.oef_addr, oef_port=args.oef_port, agent_timeout=args.agent_timeout, strategy=strategy, + max_reactions=args.max_reactions, services_interval=args.services_interval, pending_transaction_timeout=args.pending_transaction_timeout, + dashboard=agent_dashboard, private_key_pem=args.private_key_pem, expected_version_id=args.expected_version_id) + + # Create thread to pull status + kill_event = threading.Event() + status_thread = threading.Thread(target=monitor_status, args=(agent, )) + status_thread.start() + + try: + agent.start(rejoin=args.rejoin) + finally: + agent.stop() + + # Stop the status monitoring thread + kill_event.set() + status_thread.join(120) + + set_agent_state(args.expected_version_id, None) + + +if __name__ == '__main__': + main() diff --git a/tac/gui/launcher/api/resources/sandboxes.py b/tac/gui/launcher/api/resources/sandboxes.py index df4349ee..850ec10f 100644 --- a/tac/gui/launcher/api/resources/sandboxes.py +++ b/tac/gui/launcher/api/resources/sandboxes.py @@ -21,17 +21,22 @@ """Implement the sandbox resource and other utility classes.""" # import datetime +from enum import Enum import logging +import math import os -import subprocess -from enum import Enum from queue import Queue +import random +import subprocess +import time from typing import Dict, Any, Optional from flask_restful import Resource, reqparse +from tac.platform.shared_sim_status import set_controller_state, get_controller_state, get_controller_last_time, remove_controller_state, ControllerAgentState, get_shared_dir from tac import ROOT_DIR + logger = logging.getLogger(__name__) parser = reqparse.RequestParser() @@ -60,11 +65,12 @@ sandbox_queue = Queue() # type: Queue -class SandboxState(Enum): +class SandboxProcessState(Enum): """The state of execution of a sandbox.""" NOT_STARTED = "Not started yet" RUNNING = "Running" + STOPPING = "Stopping" FINISHED = "Finished" FAILED = "Failed" @@ -72,7 +78,7 @@ class SandboxState(Enum): class SandboxRunner: """Wrapper class to track the execution of a sandbox.""" - def __init__(self, id: int, params: Dict[str, Any]): + def __init__(self, id: int, params: Dict[str, Any], game_id: str): """ Initialize the sandbox runner. @@ -81,12 +87,14 @@ def __init__(self, id: int, params: Dict[str, Any]): """ self.id = id self.params = params + self.game_id = game_id + self.ui_is_starting = False # set to true if the UI is trying to start the sandbox self.process = None # type: Optional[subprocess.Popen] def __call__(self): """Launch the sandbox.""" - if self.status != SandboxState.NOT_STARTED: + if self.status != SandboxProcessState.NOT_STARTED: return args = self.params @@ -101,7 +109,6 @@ def __call__(self): "OEF_ADDR": "172.28.1.1", "OEF_PORT": "10000", "DATA_OUTPUT_DIR": str(args["data_output_dir"]), - "VERSION_ID": str(args["version_id"]), "LOWER_BOUND_FACTOR": str(args["lower_bound_factor"]), "UPPER_BOUND_FACTOR": str(args["upper_bound_factor"]), "TX_FEE": str(args["tx_fee"]), @@ -109,9 +116,18 @@ def __call__(self): "INACTIVITY_TIMEOUT": str(args["inactivity_timeout"]), "COMPETITION_TIMEOUT": str(args["competition_timeout"]), "SEED": str(args["seed"]), + "VERSION_ID": self.game_id, "WHITELIST": str(args["whitelist_file"]), + "SHARED_DIR": str(get_shared_dir()), **os.environ } + + # print("env[SHARED_DIR] = {}".format(env["SHARED_DIR"])) + # Needed for UI countdown + self.rec_registration_timeout = args["registration_timeout"] + + self.ui_is_starting = True + set_controller_state(self.game_id, ControllerAgentState.STARTING_DOCKER) self.process = subprocess.Popen([ "docker-compose", "-f", @@ -121,30 +137,49 @@ def __call__(self): env=env) @property - def status(self) -> SandboxState: + def status(self) -> SandboxProcessState: """Return the state of the execution.""" if self.process is None: - return SandboxState.NOT_STARTED + return SandboxProcessState.NOT_STARTED returncode = self.process.poll() if returncode is None: - return SandboxState.RUNNING + if self.ui_is_starting: + return SandboxProcessState.RUNNING + else: + return SandboxProcessState.STOPPING elif returncode == 0: - return SandboxState.FINISHED + return SandboxProcessState.FINISHED elif returncode > 0: - return SandboxState.FAILED + return SandboxProcessState.FAILED else: raise ValueError("Unexpected return code.") def to_dict(self) -> Dict[str, Any]: - """Serialize the object into a dictionary.""" + """Convert class data into dictionary so we can jasonise it and send it to web frontend.""" + controller_status = get_controller_state(self.game_id) + if controller_status is not None: + controller_status_text = controller_status.value + else: + controller_status_text = "Uninitialised" + + if controller_status == ControllerAgentState.REGISTRATION_OPEN: + duration = time.time() - get_controller_last_time(self.game_id) + controller_status_text += ": " + str(1 + math.floor(self.rec_registration_timeout - duration)) + return { "id": self.id, - "status": self.status.value, + "controller_status": controller_status_text, + "process_status": self.status.value, + "game_id": self.game_id, "params": self.params } def stop(self) -> None: + """Stop the sandbox.""" + self.ui_is_starting = False """Stop the execution of the sandbox.""" + remove_controller_state(self.game_id) + self.game_id = "" if self.process is None: return try: @@ -167,7 +202,7 @@ def get(self, sandbox_id): if sandbox_id in sandboxes: return sandboxes[sandbox_id].to_dict(), 200 else: - return None, 404 + return "", 200 def delete(self, sandbox_id): """Delete the current sandbox instance.""" @@ -195,7 +230,8 @@ def post(self): args = self._post_args_preprocessing(args, sandbox_id) # create the simulation runner wrapper - simulation_runner = SandboxRunner(sandbox_id, args) + game_id = "game_id_" + str(random.randrange(0, 10000)) + simulation_runner = SandboxRunner(sandbox_id, args, game_id) # save the created simulation to the global state sandboxes[sandbox_id] = simulation_runner diff --git a/tac/gui/launcher/app.py b/tac/gui/launcher/app.py index ca87864c..7424b462 100644 --- a/tac/gui/launcher/app.py +++ b/tac/gui/launcher/app.py @@ -19,9 +19,13 @@ # ------------------------------------------------------------------------------ """The main entrypoint for the launcher app.""" +import os from tac.gui.launcher import create_app +from tac.platform.shared_sim_status import register_shared_dir if __name__ == '__main__': + register_shared_dir(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../../data/shared')) + app = create_app() app.run("127.0.0.1", 5000, debug=True, use_reloader=False) diff --git a/tac/gui/launcher/forms/sandbox.py b/tac/gui/launcher/forms/sandbox.py index 3a9c4be4..69e88957 100644 --- a/tac/gui/launcher/forms/sandbox.py +++ b/tac/gui/launcher/forms/sandbox.py @@ -37,7 +37,7 @@ class SandboxForm(Form): lower_bound_factor = IntegerField('Lower bound factor', default=0, widget=widgets.Input(input_type="number"), validators=[wtforms.validators.NumberRange(min=0, message="Must be non-negative")],) upper_bound_factor = IntegerField('Upper bound factor', default=0, widget=widgets.Input(input_type="number"), validators=[wtforms.validators.NumberRange(min=0, message="Must be non-negative")],) tx_fee = FloatField("Transaction fee", default=0.1, validators=[wtforms.validators.NumberRange(min=0, message="Must be non-negative")],) - registration_timeout = IntegerField("Registration timeout", default=10, validators=[wtforms.validators.NumberRange(min=0, message="Must be non-negative")]) + registration_timeout = IntegerField("Registration timeout", default=20, validators=[wtforms.validators.NumberRange(min=0, message="Must be non-negative")]) inactivity_timeout = IntegerField("Inactivity timeout", default=60, validators=[wtforms.validators.NumberRange(min=0, message="Must be non-negative")]) competition_timeout = IntegerField("Competition timeout", default=240, validators=[wtforms.validators.NumberRange(min=0, message="Must be non-negative")]) # start_time = DateTimeField("Start time", id='datepick', validators=[wtforms.validators.Required()]) diff --git a/tac/gui/launcher/static/js/grid_search.js b/tac/gui/launcher/static/js/grid_search.js index e2daa728..c2f03ae5 100644 --- a/tac/gui/launcher/static/js/grid_search.js +++ b/tac/gui/launcher/static/js/grid_search.js @@ -1,4 +1,4 @@ -(function() { + (function() { let firstCard = $("#card-0"); let cardHtmlTemplate = firstCard.html(); @@ -15,12 +15,19 @@ this.setup(); + //this.getSandboxStatus() + } setup() { let jqueryObj = this.jqueryObj; let id = this.id; + let process_statusBtn = document.getElementById("process-status-sandbox-"+ id); + let controller_statusBtn = document.getElementById("controller-status-sandbox-" + id); + let game_id = document.getElementById("info-game-id-" + id); + + let remove = function(){ jqueryObj.remove(); delete sandboxes[id]; @@ -32,8 +39,30 @@ XHR.send(); }; + let getSandboxStatus = function() { + let XHR = new XMLHttpRequest(); + XHR.onreadystatechange = function () { + if (this.readyState == 4 && this.status == 200) { + if (XHR.response != null && XHR.response != "null" && XHR.response != ""){ + let jsonResponse = JSON.parse(XHR.response); + + document.getElementById("process-status-sandbox-"+ id).innerHTML = "Process Status: " + jsonResponse["process_status"]; + document.getElementById("controller-status-sandbox-" + id).innerHTML = "Controller Status: " + jsonResponse["controller_status"]; + document.getElementById("info-game-id-" + id).innerHTML = "Game Id: " + jsonResponse["game_id"] + + } + } + }; + XHR.open("GET", "/api/sandboxes/" + id, true); + XHR.send(); + setTimeout(getSandboxStatus, 500); + } + getSandboxStatus() + this.trashBtn.on('click', remove); this.stopBtn.on('click', stop); + + } static fromTemplate(id){ @@ -41,7 +70,10 @@ card.html(cardHtmlTemplate .replace(/collapse-0/g, "collapse-" + id) .replace(/heading-0/g, "heading-"+id) - .replace(/Sandbox 1/, "Sandbox " + (id + 1))); + .replace(/Sandbox 1/, "Sandbox " + (id + 1)) + .replace('process-status-sandbox-0', "process-status-sandbox-"+id) + .replace('controller-status-sandbox-0', "controller-status-sandbox-"+id) + .replace('info-game-id-0', "info-game-id-"+id)); card.addClass("card"); card.attr("id", "card-" + id); @@ -49,18 +81,24 @@ let jqueryObj = $('#card-'+id); return new SandboxCard(id, jqueryObj); } + + + } function buildJSONFromSandboxFormList(){ let result = []; for (let sandboxId in sandboxes){ let inputs = sandboxes[sandboxId].jqueryObj.find('.card-body :input'); - let values = {}; + let values = new FormData(); inputs.each(function() { - values[this.name] = $(this).val(); + let key = this.name; + let val = $(this).val() + values.append(key, val); }); result.push(values); } + return result } @@ -79,7 +117,13 @@ console.log("Error on request " + i + " event: " + event) }); XHR.open("POST", "/api/sandboxes", true); - XHR.send(sandboxObjectList[i]); + // Display the key/value pairs + let FD = sandboxObjectList[i]; + console.log("Form data"); + for (var pair of FD.entries()) { + console.log(pair[0]+ ', ' + pair[1]); + } + XHR.send(FD); } }); diff --git a/tac/gui/launcher/static/js/launcher.js b/tac/gui/launcher/static/js/launcher.js index 56187bb3..2f7854fc 100644 --- a/tac/gui/launcher/static/js/launcher.js +++ b/tac/gui/launcher/static/js/launcher.js @@ -2,13 +2,16 @@ // the ID of the current sandbox running/finished/stopped. 'null' if no sandbox has been started yet. let currentSandboxID = null; + let currentGameId = "" let configureSandboxForm = function () { let form = document.getElementById("form-sandbox"); let startBtn = document.getElementById("btn-start-sandbox"); let stopBtn = document.getElementById("btn-stop-sandbox"); - let statusBtn = document.getElementById("btn-info-sandbox"); + let process_statusBtn = document.getElementById("process-status-sandbox"); + let controller_statusBtn = document.getElementById("controller-status-sandbox"); + let game_id = document.getElementById("info-game-id"); stopBtn.disabled = true; @@ -45,6 +48,11 @@ }); XHR.open("POST", "/api/sandboxes", false); + // Display the key/value pairs + console.log("Form data"); + for (var pair of FD.entries()) { + console.log(pair[0]+ ', ' + pair[1]); + } XHR.send(FD); let jsonResponse = JSON.parse(XHR.response); console.log("ID=" + jsonResponse["id"]); @@ -81,26 +89,36 @@ if (currentSandboxID != null) { let XHR = new XMLHttpRequest(); XHR.onreadystatechange = function () { - let jsonResponse = JSON.parse(XHR.response); - if (this.readyState == 4 && this.status == 200) { - statusBtn.innerHTML = jsonResponse["status"]; + if (XHR.response != null && XHR.response != "null" && XHR.response != ""){ + let jsonResponse = JSON.parse(XHR.response); + if (this.readyState == 4 && this.status == 200) { + process_statusBtn.innerHTML = "Process Status: " + jsonResponse["process_status"]; + controller_statusBtn.innerHTML = "Controller Status: " + jsonResponse["controller_status"]; + currentGameId = jsonResponse["game_id"] + game_id.innerHTML = "Game Id: " + currentGameId + } } }; + console.log(" XHR Open: /api/sandboxes/" + currentSandboxID) XHR.open("GET", "/api/sandboxes/" + currentSandboxID, true); XHR.send(); } - setTimeout(getSandboxStatus, 1000); + setTimeout(getSandboxStatus, 500); }; getSandboxStatus(); }; + let configureAgentForm = function () { let form = document.getElementById("form-agent"); let startBtn = document.getElementById("btn-start-agent"); let stopBtn = document.getElementById("btn-stop-agent"); + let process_statusBtn = document.getElementById("process-status-agent"); + let agent_statusBtn = document.getElementById("agent-status-agent"); stopBtn.disabled = true; form.addEventListener("submit", function (ev) { + ev.preventDefault(); let clickedBtnId = ev.target.target; @@ -115,6 +133,9 @@ }); let startAgent = function () { + console.log("******* startAgent") + + let XHR = new XMLHttpRequest(); // Bind the FormData object and the form element @@ -132,12 +153,20 @@ startBtn.disabled = false; stopBtn.disabled = true; }); + // Add the game id + FD.append("expected_version_id", currentGameId) + // Display the key/value pairs + console.log("Form data"); + for (var pair of FD.entries()) { + console.log(pair[0]+ ', ' + pair[1]); + } XHR.open("POST", "/api/agent", true); XHR.send(FD); return XHR.responseText; }; + let stopAgent = function () { let XHR = new XMLHttpRequest(); @@ -161,6 +190,22 @@ XHR.send(FD); return XHR.responseText; }; + let getAgentStatus = function () { + let XHR = new XMLHttpRequest(); + XHR.onreadystatechange = function () { + if (XHR.response != null && XHR.response != "null" && XHR.response != ""){ + let jsonResponse = JSON.parse(XHR.response); + if (this.readyState == 4 && this.status == 200) { + process_statusBtn.innerHTML = "Process Status: " + jsonResponse["process_status"]; + agent_statusBtn.innerHTML = "Agent Status: " + jsonResponse["agent_status"]; + } + } + }; + XHR.open("GET", "/api/agent", true); + XHR.send(); + setTimeout(getAgentStatus, 2000); + }; + getAgentStatus(); }; diff --git a/tac/gui/launcher/templates/grid_search.html b/tac/gui/launcher/templates/grid_search.html index 36a8effb..c9286aaa 100644 --- a/tac/gui/launcher/templates/grid_search.html +++ b/tac/gui/launcher/templates/grid_search.html @@ -47,13 +47,22 @@

{% endfor %} +
+
+ Process Status: Uninitialised +
+
+ Controller Status: Uninitialised +
+
Game Id:
- +
+ The controller GUI: http://localhost:8097 diff --git a/tac/gui/launcher/templates/launcher.html b/tac/gui/launcher/templates/launcher.html index c82a67bc..479284fe 100644 --- a/tac/gui/launcher/templates/launcher.html +++ b/tac/gui/launcher/templates/launcher.html @@ -16,11 +16,16 @@

Sandbox Configurations

onclick='this.form.target="btn-start-sandbox";'> - +
+
+ Process Status: Uninitialised +
+
+ Controller Status: Uninitialised +
+
Game Id:

Agent Configurations

@@ -35,15 +40,21 @@

Agent Configurations

onclick='this.form.target="btn-start-agent";'> - - - +
- +
+
+ Process Status: Uninitialised +
+
+ Controller Status: Uninitialised +
+ -{% endblock %} +
+The controller GUI: http://localhost:8097{% endblock %} {% block scripts %} {% endblock %} diff --git a/tac/platform/shared_sim_status.py b/tac/platform/shared_sim_status.py new file mode 100644 index 00000000..a7a10d04 --- /dev/null +++ b/tac/platform/shared_sim_status.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Pass status of systems between docker images and processes using the file system and a bind mount in docker.""" + +import glob +import os + +from enum import Enum + +from aea.agent import AgentState + + +class ControllerAgentState(Enum): + """The state of execution of the TAC cotnroller agent.""" + + NONE = "State not set" + STARTING_DOCKER = "Starting docker image" + STARTING = "Starting controller agent" + REGISTRATION_OPEN = "Registration is open for agents to connect" + RUNNING = "Running simulation" + STOPPING_UNSUFFICIENT_AGENTS = "Stopping due to insufficient agente registered" + FINISHED_INACTIVITY = "Finished due to inactivity timeout" + FINISHED_GAME_TIMEOUT = "Finished due to game timeout" + + +def register_shared_dir(temp_dir) -> None: + """Call this from somewhere near the entry point of the program to set he location of the temp directory.""" + # print("register_shared_dir: " + temp_dir) + # logging.error("register_shared_dir: " + temp_dir) + + os.environ['TAC_SHARED_DIR'] = temp_dir + + +def get_shared_dir() -> str: + """Call this to return the bases shared folder location.""" + return os.environ['TAC_SHARED_DIR'] + + +def clear_temp_dir() -> None: + """Call this once at the beginning (after registering the temp director) - cleans out dir of old status files.""" + shared_dir = str(os.getenv('TAC_SHARED_DIR')) + # Get a list of all the file paths that ends with .txt from in specified directory + file_list = glob.glob(os.path.join(shared_dir, '*.txt')) + + # Iterate over the list of filepaths & remove each file. + for filePath in file_list: + os.remove(filePath) + + +def set_controller_state(game_id: str, state: ControllerAgentState) -> None: + """Set controller state.""" + _set_str_status("controller_" + game_id, str(state.name)) + + +def get_controller_state(game_id: str) -> ControllerAgentState: + """Get controller state.""" + key = _get_str_status("controller_" + game_id) + if key != "": + return ControllerAgentState[key] + else: + return ControllerAgentState.NONE + + +def get_controller_last_time(game_id: str, ) -> float: + """Return the last time the controller state was changed as UTC time.""" + return _get_last_status_time("controller_" + game_id) + + +def remove_controller_state(game_id: str) -> None: + """Remove the status file from the temp folder.""" + temp_file_path = _construct_temp_filename("controller_" + game_id) + if temp_file_path is not None: + os.remove(temp_file_path) + + +def set_agent_state(game_id: str, state: AgentState) -> None: + """Set agent state.""" + if state is not None: + _set_str_status("agent_" + game_id, str(state.name)) + else: + _set_str_status("agent_" + game_id, "") + + +def get_agent_state(game_id: str) -> AgentState: + """Get agent state.""" + key = _get_str_status("agent_" + game_id) + if key != "": + return AgentState[key] + else: + # This is well dodgy, but I don't have a good thing to return in this case + return AgentState.INITIATED + + +def remove_agent_state(game_id: str) -> None: + """Remove the status file from the temp folder.""" + temp_file_path = _construct_temp_filename("agent_" + game_id) + if temp_file_path is not None: + os.remove(temp_file_path) + + +def _get_last_status_time(id_name) -> float: + """Return the last time the agent state was changed as UTC time.""" + return os.path.getmtime(_construct_temp_filename(id_name)) + + +def _construct_temp_filename(id_name) -> str: + shared_dir = os.getenv('TAC_SHARED_DIR') + + # Actually it is fine not to set this up - should just fail gracefully as + # this is only needed when using the GUI launcher + # assert shared_dir is not None, "must call register_shared_dir() from entry point of program" + + if shared_dir is not None and os.path.isdir(shared_dir): + return os.path.join(shared_dir, "tempfile_" + id_name + "_status.txt") + return "" + + +def _set_str_status(id_name, status) -> None: + temp_file_path = _construct_temp_filename(id_name) + if temp_file_path != "": + f = open(temp_file_path, "w+") + f.write(status) + f.close() + + +def _get_str_status(id_name): + temp_file_path = _construct_temp_filename(id_name) + if temp_file_path != "": + if (os.path.isfile(temp_file_path)): + f = open(temp_file_path, "r") + status = f.read() + f.close() + return status + + return "" diff --git a/templates/v1/advanced.py b/templates/v1/advanced.py index 1ac8b195..6f95cd18 100644 --- a/templates/v1/advanced.py +++ b/templates/v1/advanced.py @@ -48,6 +48,7 @@ def parse_arguments(): parser.add_argument("--services-interval", type=int, default=10, help="The number of seconds to wait before doing another search.") parser.add_argument("--pending-transaction-timeout", type=int, default=30, help="The timeout in seconds to wait for pending transaction/negotiations.") parser.add_argument("--private-key-pem", default=None, help="Path to a file containing a private key in PEM format.") + parser.add_argument("--expected-version-id", type=str, help="The epected version id of the TAC.") parser.add_argument("--rejoin", action="store_true", default=False, help="Whether the agent is joining a running TAC.") parser.add_argument("--dashboard", action="store_true", help="Show the agent dashboard.") parser.add_argument("--visdom-addr", type=str, default="localhost", help="IP address to the Visdom server") @@ -128,7 +129,7 @@ def main(): strategy = MyStrategy(register_as=RegisterAs(args.register_as), search_for=SearchFor(args.search_for), is_world_modeling=args.is_world_modeling) agent = BaselineAgent(name=args.name, oef_addr=args.oef_addr, oef_port=args.oef_port, agent_timeout=args.agent_timeout, strategy=strategy, max_reactions=args.max_reactions, services_interval=args.services_interval, pending_transaction_timeout=args.pending_transaction_timeout, - dashboard=agent_dashboard, private_key_pem=args.private_key_pem) + dashboard=agent_dashboard, private_key_pem=args.private_key_pem, expected_version_id=args.expected_version_id) try: agent.start(rejoin=args.rejoin) diff --git a/templates/v1/basic.py b/templates/v1/basic.py index d881c2d7..1b32ac91 100644 --- a/templates/v1/basic.py +++ b/templates/v1/basic.py @@ -29,6 +29,7 @@ from tac.agents.participant.v1.examples.strategy import BaselineStrategy from tac.gui.dashboards.agent import AgentDashboard + logger = logging.getLogger(__name__) @@ -46,6 +47,7 @@ def parse_arguments(): parser.add_argument("--services-interval", type=int, default=5, help="The number of seconds to wait before doing another search.") parser.add_argument("--pending-transaction-timeout", type=int, default=30, help="The timeout in seconds to wait for pending transaction/negotiations.") parser.add_argument("--private-key-pem", default=None, help="Path to a file containing a private key in PEM format.") + parser.add_argument("--expected-version-id", type=str, help="The expected version id of the TAC.") parser.add_argument("--rejoin", action="store_true", default=False, help="Whether the agent is joining a running TAC.") parser.add_argument("--dashboard", action="store_true", help="Show the agent dashboard.") parser.add_argument("--visdom-addr", type=str, default="localhost", help="IP address to the Visdom server") @@ -66,7 +68,7 @@ def main(): strategy = BaselineStrategy(register_as=RegisterAs(args.register_as), search_for=SearchFor(args.search_for), is_world_modeling=args.is_world_modeling) agent = BaselineAgent(name=args.name, oef_addr=args.oef_addr, oef_port=args.oef_port, agent_timeout=args.agent_timeout, strategy=strategy, max_reactions=args.max_reactions, services_interval=args.services_interval, pending_transaction_timeout=args.pending_transaction_timeout, - dashboard=agent_dashboard, private_key_pem=args.private_key_pem) + dashboard=agent_dashboard, private_key_pem=args.private_key_pem, expected_version_id=args.expected_version_id) try: agent.start(rejoin=args.rejoin) diff --git a/templates/v1/expert.py b/templates/v1/expert.py index 1b2b1501..a73dcf3a 100644 --- a/templates/v1/expert.py +++ b/templates/v1/expert.py @@ -23,6 +23,7 @@ import argparse import logging +import random from typing import Optional from aea.agent import Agent @@ -39,6 +40,7 @@ def parse_arguments(): parser.add_argument("--oef-port", default=10000, help="TCP/IP port of the OEF Agent") parser.add_argument("--agent-timeout", type=float, default=1.0, help="The time in (fractions of) seconds to time out an agent between act and react.") parser.add_argument("--private-key-pem", default=None, help="Path to a file containing a private key in PEM format.") + parser.add_argument("--expected-version-id", type=str, help="The epected version id of the TAC.") return parser.parse_args() @@ -46,10 +48,11 @@ def parse_arguments(): class MyAgent(Agent): """My agent implementation.""" - def __init__(self, name: str, oef_addr: str, oef_port: int, agent_timeout: float = 1.0, private_key_pem_path: Optional[str] = None): + def __init__(self, name: str, oef_addr: str, oef_port: int, agent_timeout: float = 1.0, private_key_pem_path: Optional[str] = None, expected_version_id: str = str(random.randint(0, 10000))): """Agent initialization.""" super().__init__(name, private_key_pem_path, agent_timeout) self.mailbox = OEFMailBox(self.crypto.public_key, oef_addr, oef_port) + self.expected_version_id = expected_version_id raise NotImplementedError("Your agent must implement the interface defined in Agent.") @@ -58,7 +61,7 @@ def main(): """Run the script.""" args = parse_arguments() - agent = MyAgent(name=args.name, oef_addr=args.oef_addr, oef_port=args.oef_port, agent_timeout=args.agent_timeout, private_key_pem_path=args.private_key_pem) + agent = MyAgent(name=args.name, oef_addr=args.oef_addr, oef_port=args.oef_port, agent_timeout=args.agent_timeout, private_key_pem_path=args.private_key_pem, expected_version_id=args.expected_version_id) try: agent.start() diff --git a/tests/conftest.py b/tests/conftest.py index a8c092a2..f23b1800 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,7 +33,7 @@ logger = logging.getLogger(__name__) -CUR_PATH = inspect.getfile(inspect.currentframe()) +CUR_PATH = inspect.getfile(inspect.currentframe()) # type: ignore ROOT_DIR = os.path.join(os.path.dirname(CUR_PATH), "..")