def do_fetch( ctx: Context, public_id: PublicId, local: bool, remote: bool, alias: Optional[str] = None, target_dir: Optional[str] = None, ) -> None: """ Run the Fetch command. :param ctx: the CLI context. :param public_id: the public id. :param local: whether to fetch from local :param remote whether to fetch from remote :param alias: the agent alias. :param target_dir: the target directory, in case fetching locally. :return: None """ enforce( not (local and remote), "'local' and 'remote' options are mutually exclusive." ) is_mixed = not local and not remote ctx.set_config("is_local", local and not remote) ctx.set_config("is_mixed", is_mixed) if remote: fetch_agent(ctx, public_id, alias=alias, target_dir=target_dir) elif local: fetch_agent_locally(ctx, public_id, alias=alias, target_dir=target_dir) else: fetch_mixed(ctx, public_id, alias=alias, target_dir=target_dir)
def _update_agent_config(ctx: Context): """ Update agent configurations. :param ctx: the context. :return: None """ # update aea_version in case current framework version is different version = Version(aea.__version__) if not ctx.agent_config.aea_version_specifiers.contains(version): new_aea_version = compute_specifier_from_version(version) old_aea_version = ctx.agent_config.aea_version click.echo( f"Updating AEA version specifier from {old_aea_version} to {new_aea_version}." ) ctx.agent_config.aea_version = new_aea_version # update author name if it is different cli_author = ctx.config.get("cli_author") if cli_author and ctx.agent_config.author != cli_author: click.echo( f"Updating author from {ctx.agent_config.author} to {cli_author}") ctx.agent_config._author = cli_author # pylint: disable=protected-access ctx.agent_config.version = DEFAULT_VERSION ctx.dump_agent_config()
def fetch_local_or_mixed(ctx: Context, item_type: str, item_id: PublicId) -> None: """ Fetch item, either local or mixed, depending on the parameters/context configuration. It will first try to fetch from local registry; in case of failure, if the 'is_mixed' flag is set, it will try to fetch from remote registry. Context expects 'is_local' and 'is_mixed' to be set. :param ctx: the CLI context. :param item_type: the type of the package. :param item_id: the public id of the item. :return: None """ def _raise(item_type_: str, item_id_: PublicId, exception): """Temporary function to raise exception (used below twice).""" raise click.ClickException( f"Failed to add {item_type_} dependency {item_id_}: {str(exception)}" ) try: add_item(ctx, item_type, item_id) except click.ClickException as e: if not ctx.config.get("is_mixed", False): _raise(item_type, item_id, e) ctx.set_config("is_local", False) try: add_item(ctx, item_type, item_id) except click.ClickException as e: _raise(item_type, item_id, e) ctx.set_config("is_local", True)
def load( cls, working_dir: str, public_id: PublicId, is_local: bool = False, registry_path: str = "packages", skip_consistency_check: bool = False, ) -> "Project": """ Load project with given public_id to working_dir. :param working_dir: the working directory :param public_id: the public id :param is_local: whether to fetch from local or remote :param registry_path: the path to the registry locally :param skip_consistency_check: consistency checks flag """ ctx = Context(cwd=working_dir, registry_path=registry_path) ctx.set_config("skip_consistency_check", skip_consistency_check) path = os.path.join(working_dir, public_id.author, public_id.name) target_dir = os.path.join(public_id.author, public_id.name) if is_local: fetch_agent_locally(ctx, public_id, target_dir=target_dir) else: fetch_agent(ctx, public_id, target_dir=target_dir) return cls(public_id, path)
def verify_or_create_private_keys(ctx: Context) -> None: """ Verify or create private keys. :param ctx: Context """ path = Path(DEFAULT_AEA_CONFIG_FILE) agent_loader = ConfigLoader("aea-config_schema.json", AgentConfig) fp = path.open(mode="r", encoding="utf-8") aea_conf = agent_loader.load(fp) for identifier, _value in aea_conf.private_key_paths.read_all(): if identifier not in registry.supported_crypto_ids: ValueError("Unsupported identifier in private key paths.") for identifier, private_key_path in IDENTIFIER_TO_KEY_FILES.items(): config_private_key_path = aea_conf.private_key_paths.read(identifier) if config_private_key_path is None: create_private_key(identifier) aea_conf.private_key_paths.update(identifier, private_key_path) else: try: _try_validate_private_key_path(identifier, private_key_path) except FileNotFoundError: # pragma: no cover raise click.ClickException( "File {} for private key {} not found.".format( repr(private_key_path), identifier, ) ) # update aea config path = Path(DEFAULT_AEA_CONFIG_FILE) fp = path.open(mode="w", encoding="utf-8") agent_loader.dump(aea_conf, fp) ctx.agent_config = aea_conf
def try_to_load_agent_config( ctx: Context, is_exit_on_except: bool = True, agent_src_path: str = None ) -> None: """ Load agent config to a click context object. :param ctx: click command context object. :param is_exit_on_except: bool option to exit on exception (default = True). :param agent_src_path: path to an agent dir if needed to load a custom config. :return None """ if agent_src_path is None: agent_src_path = ctx.cwd try: path = Path(os.path.join(agent_src_path, DEFAULT_AEA_CONFIG_FILE)) with path.open(mode="r", encoding="utf-8") as fp: ctx.agent_config = ctx.agent_loader.load(fp) logging.config.dictConfig(ctx.agent_config.logging_config) except FileNotFoundError: if is_exit_on_except: raise click.ClickException( "Agent configuration file '{}' not found in the current directory.".format( DEFAULT_AEA_CONFIG_FILE ) ) except jsonschema.exceptions.ValidationError: if is_exit_on_except: raise click.ClickException( "Agent configuration file '{}' is invalid. Please check the documentation.".format( DEFAULT_AEA_CONFIG_FILE ) )
def verify_or_create_private_keys_ctx( ctx: Context, aea_project_path: Path = ROOT, exit_on_error: bool = False, ) -> None: """ Verify or create private keys with ctx provided. :param ctx: Context """ try: AgentConfigManager.verify_or_create_private_keys( aea_project_path, private_key_helper=private_key_verify_or_create, substitude_env_vars=False, ).dump_config() agent_config = AgentConfigManager.verify_or_create_private_keys( aea_project_path, private_key_helper=private_key_verify_or_create).agent_config if ctx is not None: ctx.agent_config = agent_config except ValueError as e: # pragma: nocover if exit_on_error: sys.exit(1) raise click.ClickException(str(e))
def _fetch_agent_deps(ctx: Context, is_mixed: bool = True) -> None: """ Fetch agent dependencies. :param ctx: context object. :param is_mixed: flag to enable mixed mode (try first local, then remote). :return: None :raises: ClickException re-raises if occurs in add_item call. """ ctx.set_config("is_local", True) ctx.set_config("is_mixed", is_mixed) for item_type in ("protocol", "contract", "connection", "skill"): item_type_plural = "{}s".format(item_type) required_items = getattr(ctx.agent_config, item_type_plural) for item_id in required_items: fetch_local_or_mixed(ctx, item_type, item_id)
def fetch_agent_locally( ctx: Context, public_id: PublicId, alias: Optional[str] = None, target_dir: Optional[str] = None, ) -> None: """ Fetch Agent from local packages. :param ctx: a Context object. :param public_id: public ID of agent to be fetched. :param alias: an optional alias. :param target_dir: the target directory to which the agent is fetched. :return: None """ packages_path = ( DEFAULT_REGISTRY_NAME if ctx.registry_path is None else ctx.registry_path ) source_path = try_get_item_source_path( packages_path, public_id.author, AGENTS, public_id.name ) enforce( ctx.config.get("is_local") is True or ctx.config.get("is_mixed") is True, "Please use `ctx.set_config('is_local', True)` or `ctx.set_config('is_mixed', True)` to fetch agent and all components locally.", ) try_to_load_agent_config(ctx, agent_src_path=source_path) if not _is_version_correct(ctx, public_id): raise click.ClickException( "Wrong agent version in public ID: specified {}, found {}.".format( public_id.version, ctx.agent_config.version ) ) folder_name = target_dir or (public_id.name if alias is None else alias) target_path = os.path.join(ctx.cwd, folder_name) if os.path.exists(target_path): path = Path(target_path) raise click.ClickException( f'Item "{path.name}" already exists in target folder "{path.parent}".' ) if target_dir is not None: os.makedirs(target_path) # pragma: nocover ctx.clean_paths.append(target_path) copy_tree(source_path, target_path) ctx.cwd = target_path try_to_load_agent_config(ctx) if alias is not None: ctx.agent_config.agent_name = alias ctx.agent_loader.dump( ctx.agent_config, open_file(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w"), ) _fetch_agent_deps(ctx) click.echo("Agent {} successfully fetched.".format(public_id.name))
def cli(click_context, skip_consistency_check: bool) -> None: """Command-line tool for setting up an Autonomous Economic Agent.""" verbosity_option = click_context.meta.pop("verbosity") click_context.obj = Context(cwd=".", verbosity=verbosity_option) click_context.obj.set_config("skip_consistency_check", skip_consistency_check) # enables CTRL+C support on windows! enable_ctrl_c_support()
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.""" ctx = Context(cwd=os.path.join(app_context.agents_dir, agent_id)) ctx.set_config("is_local", app_context.local) try: try_to_load_agent_config(ctx) cli_add_item(ctx, item_type, PublicId.from_str(item_id)) except ClickException as e: return ( { "detail": "Failed to add {} {} to agent {}. {}".format( item_type, item_id, agent_id, str(e) ) }, 400, ) # 400 Bad request else: return agent_id, 201 # 200 (OK)
def fetch_agent(ctx: Context, public_id: PublicId, alias: Optional[str] = None) -> None: """ Fetch Agent from Registry. :param ctx: Context :param public_id: str public ID of desirable Agent. :param ctx: a Context object. :param alias: an optional alias. :return: None """ author, name, version = public_id.author, public_id.name, public_id.version api_path = "/agents/{}/{}/{}".format(author, name, version) resp = request_api("GET", api_path) file_url = resp["file"] filepath = download_file(file_url, ctx.cwd) folder_name = name if alias is None else alias aea_folder = os.path.join(ctx.cwd, folder_name) ctx.clean_paths.append(aea_folder) extract(filepath, ctx.cwd) if alias is not None: os.rename(name, alias) ctx.cwd = aea_folder try_to_load_agent_config(ctx) if alias is not None: ctx.agent_config.agent_name = alias ctx.agent_loader.dump( ctx.agent_config, open(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w")) click.echo("Fetching dependencies...") for item_type in ("connection", "contract", "skill", "protocol"): item_type_plural = item_type + "s" # initialize fetched agent with empty folders for custom packages custom_items_folder = os.path.join(ctx.cwd, item_type_plural) os.makedirs(custom_items_folder) config = getattr(ctx.agent_config, item_type_plural) for item_public_id in config: try: add_item(ctx, item_type, item_public_id) except Exception as e: raise click.ClickException( 'Unable to fetch dependency for agent "{}", aborting. {}'. format(name, e)) click.echo("Dependencies successfully fetched.") click.echo("Agent {} successfully fetched to {}.".format(name, aea_folder))
def update_agent_config(ctx: Context) -> None: """ Update agent configurations. In particular: - update aea_version in case current framework version is different - update author name if it is different :param ctx: the context. :return: None """ update_aea_version_range(ctx.agent_config) cli_author = ctx.config.get("cli_author") if cli_author and ctx.agent_config.author != cli_author: click.echo( f"Updating author from {ctx.agent_config.author} to {cli_author}") ctx.agent_config._author = cli_author # pylint: disable=protected-access ctx.agent_config.version = DEFAULT_VERSION ctx.dump_agent_config()
def delete_agent(agent_id: str): """Delete an existing AEA project.""" ctx = Context(cwd=app_context.agents_dir) try: cli_delete_aea(ctx, agent_id) except ClickException: return ( {"detail": "Failed to delete Agent {} - it may not exist".format(agent_id)}, 400, ) # 400 Bad request else: return "Agent {} deleted".format(agent_id), 200 # 200 (OK)
def create_agent(agent_id: str): """Create a new AEA project.""" ctx = Context(cwd=app_context.agents_dir) try: cli_create_aea(ctx, agent_id, local=app_context.local) except ClickException as e: return ( {"detail": "Failed to create Agent. {}".format(str(e))}, 400, ) # 400 Bad request else: return agent_id, 201 # 201 (Created)
def get_registered_items(item_type: str): """Create a new AEA project.""" # need to place ourselves one directory down so the cher can find the packages ctx = Context(cwd=app_context.agents_dir) try: cli_setup_search_ctx(ctx, local=app_context.local) result, _ = cli_search_items(ctx, item_type, query="", page=1) except ClickException: return {"detail": "Failed to search items."}, 400 # 400 Bad request else: sorted_items = sort_items(result) return sorted_items, 200 # 200 (Success)
def setup_search_ctx(ctx: Context, local: bool) -> None: """ Set up search command. :param click_context: click context object. :param local: bool flag for local search. :return: None. """ if local: ctx.set_config("is_local", True) # if we are in an agent directory, try to load the configuration file. # otherwise, use the default path (i.e. 'packages/' in the current directory.) try: try_to_load_agent_config(ctx, is_exit_on_except=False) registry_directory = ctx.agent_config.registry_path except Exception: # pylint: disable=broad-except registry_directory = os.path.join(ctx.cwd, DEFAULT_REGISTRY_PATH) ctx.set_config("registry_directory", registry_directory) logger.debug("Using registry {}".format(registry_directory))
def _fetch_agent_deps(ctx: Context) -> None: """ Fetch agent dependencies. :param ctx: context object. :return: None :raises: ClickException re-raises if occures in add_item call. """ ctx.set_config("is_local", True) for item_type in ("protocol", "contract", "connection", "skill"): item_type_plural = "{}s".format(item_type) required_items = getattr(ctx.agent_config, item_type_plural) for item_id in required_items: try: add_item(ctx, item_type, item_id) except click.ClickException as e: raise click.ClickException( "Failed to add {} dependency {}: {}".format( item_type, item_id, str(e)))
def fetch_agent_locally( ctx: Context, public_id: PublicId, alias: Optional[str] = None, target_dir: Optional[str] = None, is_mixed: bool = True, ) -> None: """ Fetch Agent from local packages. :param ctx: a Context object. :param public_id: public ID of agent to be fetched. :param alias: an optional alias. :param target_dir: the target directory to which the agent is fetched. :param is_mixed: flag to enable mixed mode (try first local, then remote). :return: None """ packages_path = (DEFAULT_REGISTRY_PATH if ctx.registry_path is None else ctx.registry_path) source_path = try_get_item_source_path(packages_path, public_id.author, "agents", public_id.name) try_to_load_agent_config(ctx, agent_src_path=source_path) if not _is_version_correct(ctx, public_id): raise click.ClickException( "Wrong agent version in public ID: specified {}, found {}.".format( public_id.version, ctx.agent_config.version)) folder_name = target_dir or (public_id.name if alias is None else alias) target_path = os.path.join(ctx.cwd, folder_name) if os.path.exists(target_path): raise click.ClickException( 'Item "{}" already exists in target folder.'.format( public_id.name)) if target_dir is not None: os.makedirs(target_path) # pragma: nocover ctx.clean_paths.append(target_path) copy_tree(source_path, target_path) ctx.cwd = target_path try_to_load_agent_config(ctx) if alias is not None: ctx.agent_config.agent_name = alias ctx.agent_loader.dump( ctx.agent_config, open(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w")) # add dependencies _fetch_agent_deps(ctx, is_mixed) click.echo("Agent {} successfully fetched.".format(public_id.name))
def fetch_agent(agent_id: str): """Fetch an agent.""" ctx = Context(cwd=app_context.agents_dir) fetch_agent_ = cli_fetch_agent_locally if app_context.local else cli_fetch_agent try: agent_public_id = PublicId.from_str(agent_id) fetch_agent_(ctx, agent_public_id) except ClickException as e: return ( {"detail": "Failed to fetch an agent {}. {}".format(agent_id, str(e))}, 400, ) # 400 Bad request else: return agent_public_id.name, 201 # 200 (OK)
def verify_or_create_private_keys_ctx( ctx: Context, aea_project_path: Path = ROOT, exit_on_error: bool = True, ) -> None: """ Verify or create private keys with ctx provided. :param ctx: Context """ try: agent_config = verify_or_create_private_keys(aea_project_path, exit_on_error) if ctx is not None: ctx.agent_config = agent_config except ValueError as e: # pragma: nocover click.ClickException(str(e))
def load( cls, working_dir: str, public_id: PublicId, is_local: bool = False, is_remote: bool = False, is_restore: bool = False, registry_path: str = DEFAULT_REGISTRY_NAME, skip_consistency_check: bool = False, ) -> "Project": """ Load project with given public_id to working_dir. If local = False and remote = False, then the packages are fetched in mixed mode (i.e. first try from local registry, and then from remote registry in case of failure). :param working_dir: the working directory :param public_id: the public id :param is_local: whether to fetch from local :param is_remote whether to fetch from remote :param registry_path: the path to the registry locally :param skip_consistency_check: consistency checks flag """ ctx = Context(cwd=working_dir, registry_path=registry_path) ctx.set_config("skip_consistency_check", skip_consistency_check) path = os.path.join(working_dir, public_id.author, public_id.name) target_dir = os.path.join(public_id.author, public_id.name) if not is_restore and not os.path.exists(target_dir): do_fetch(ctx, public_id, is_local, is_remote, target_dir=target_dir) return cls(public_id, path)
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 ctx = Context(cwd=app_context.agents_dir) try: cli_setup_search_ctx(ctx, local=app_context.local) result = cli_search_items(ctx, item_type, query=search_term) except ClickException: return {"detail": "Failed to search items."}, 400 # 400 Bad request else: sorted_items = sort_items(result) response = { "search_result": sorted_items, "item_type": item_type, "search_term": search_term, } return response, 200 # 200 (Success)
def get_local_items(agent_id: str, item_type: str): """Return a list of protocols, skills or connections supported by a local agent.""" if agent_id == "NONE": return [], 200 # 200 (Success) # need to place ourselves one directory down so the searcher can find the packages ctx = Context(cwd=os.path.join(app_context.agents_dir, agent_id)) try: try_to_load_agent_config(ctx) result = cli_list_agent_items(ctx, item_type) except ClickException: return { "detail": "Failed to list agent items." }, 400 # 400 Bad request else: sorted_items = sort_items(result) return sorted_items, 200 # 200 (Success)
def scaffold_item(agent_id: str, item_type: str, item_id: str): """Scaffold a moslty empty item on an agent (either protocol, skill or connection).""" agent_dir = os.path.join(app_context.agents_dir, agent_id) ctx = Context(cwd=agent_dir) try: try_to_load_agent_config(ctx) cli_scaffold_item(ctx, item_type, item_id) except ClickException: return ( { "detail": "Failed to scaffold a new {} in to agent {}".format( item_type, agent_id ) }, 400, ) # 400 Bad request else: return agent_id, 201 # 200 (OK)
def remove_local_item(agent_id: str, item_type: str, item_id: str): """Remove a protocol, skill or connection from a local agent.""" agent_dir = os.path.join(app_context.agents_dir, agent_id) ctx = Context(cwd=agent_dir) try: try_to_load_agent_config(ctx) cli_remove_item(ctx, item_type, PublicId.from_str(item_id)) except ClickException: return ( { "detail": "Failed to remove {} {} from agent {}".format( item_type, item_id, agent_id ) }, 400, ) # 400 Bad request else: return agent_id, 201 # 200 (OK)
def fetch_agent_locally(ctx: Context, public_id: PublicId, alias: Optional[str] = None) -> None: """ Fetch Agent from local packages. :param ctx: a Context object. :param public_id: public ID of agent to be fetched. :param alias: an optional alias. :return: None """ packages_path = os.path.basename(DEFAULT_REGISTRY_PATH) source_path = try_get_item_source_path(packages_path, public_id.author, "agents", public_id.name) try_to_load_agent_config(ctx, agent_src_path=source_path) if not _is_version_correct(ctx, public_id): raise click.ClickException( "Wrong agent version in public ID: specified {}, found {}.".format( public_id.version, ctx.agent_config.version)) folder_name = public_id.name if alias is None else alias target_path = os.path.join(ctx.cwd, folder_name) if os.path.exists(target_path): raise click.ClickException( 'Item "{}" already exists in target folder.'.format( public_id.name)) ctx.clean_paths.append(target_path) copy_tree(source_path, target_path) ctx.cwd = target_path try_to_load_agent_config(ctx) if alias is not None: ctx.agent_config.agent_name = alias ctx.agent_loader.dump( ctx.agent_config, open(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w")) # add dependencies _fetch_agent_deps(ctx) click.echo("Agent {} successfully fetched.".format(public_id.name))
def test_real_create_local(*mocks): """Really create an agent (have to test the call_aea at some point).""" # Set up a temporary current working directory to make agents in with TempCWD() as temp_cwd: app = create_app() # copy the 'packages' directory in the parent of the agent folder. shutil.copytree(Path(CUR_PATH, "..", "packages"), Path(temp_cwd.temp_dir, "packages")) agent_id = "test_agent_id" # Make an agent # We do it programmatically as we need to create an agent with default author # that was prevented from GUI. ctx = Context(cwd=temp_cwd.temp_dir) create_aea(ctx, agent_id, local=True, author=DEFAULT_AUTHOR) # Give it a bit of time so the polling funcionts get called time.sleep(1) # Check that we can actually see this agent too response_agents = app.get( "api/agent", data=None, content_type="application/json", ) data = json.loads(response_agents.get_data(as_text=True)) assert response_agents.status_code == 200 assert len(data) == 1 assert data[0]["public_id"] == agent_id assert data[0]["description"] == "placeholder description" # do same but this time find that this is not an agent directory. with patch("os.path.isdir", return_value=False): response_agents = app.get( "api/agent", data=None, content_type="application/json", ) data = json.loads(response_agents.get_data(as_text=True)) assert response_agents.status_code == 200 assert len(data) == 0
def try_to_load_agent_config( ctx: Context, is_exit_on_except: bool = True, agent_src_path: str = None ) -> None: """ Load agent config to a click context object. :param ctx: click command context object. :param is_exit_on_except: bool option to exit on exception (default = True). :param agent_src_path: path to an agent dir if needed to load a custom config. :return None """ if agent_src_path is None: agent_src_path = ctx.cwd try: path = Path(os.path.join(agent_src_path, DEFAULT_AEA_CONFIG_FILE)) with open_file(path, mode="r", encoding="utf-8") as fp: ctx.agent_config = ctx.agent_loader.load(fp) ctx.agent_config.directory = Path(agent_src_path) except FileNotFoundError: if is_exit_on_except: raise click.ClickException( "Agent configuration file '{}' not found in the current directory.".format( DEFAULT_AEA_CONFIG_FILE ) ) except ( jsonschema.exceptions.ValidationError, ExtraPropertiesError, AEAValidationError, ) as e: if is_exit_on_except: raise click.ClickException( "Agent configuration file '{}' is invalid: `{}`. Please check the documentation.".format( DEFAULT_AEA_CONFIG_FILE, str(e) ) ) except AEAEnforceError as e: raise click.ClickException(str(e)) # pragma: nocover
def do_install(ctx: Context, requirement: Optional[str] = None) -> None: """ Install necessary dependencies. :param ctx: context object. :param requirement: optional str requirement. :return: None :raises: ClickException if AEAException occurres. """ try: if requirement: logger.debug( "Installing the dependencies in '{}'...".format(requirement)) _install_from_requirement(requirement) else: logger.debug("Installing all the dependencies...") dependencies = ctx.get_dependencies() for name, d in dependencies.items(): _install_dependency(name, d) except AEAException as e: raise click.ClickException(str(e))