diff --git a/src/browsergym/workarena/__init__.py b/src/browsergym/workarena/__init__.py index 47aaf5e..04dac6d 100644 --- a/src/browsergym/workarena/__init__.py +++ b/src/browsergym/workarena/__init__.py @@ -1,4 +1,14 @@ -__version__ = "0.5.1" +__version__ = "0.5.2" + +# Check playwright version early to avoid cryptic errors +import importlib.metadata + +_playwright_version = importlib.metadata.version("playwright") +if _playwright_version != "1.44.0": + raise RuntimeError( + f"browsergym-workarena requires playwright==1.44.0, but found {_playwright_version}. " + f"Please install the correct version: pip install playwright==1.44.0" + ) import inspect from logging import warning diff --git a/src/browsergym/workarena/config.py b/src/browsergym/workarena/config.py index 8d5dd13..74a8bdc 100644 --- a/src/browsergym/workarena/config.py +++ b/src/browsergym/workarena/config.py @@ -13,7 +13,7 @@ # Hugging Face dataset containing available instances INSTANCE_REPO_ID = "ServiceNow/WorkArena-Instances" -INSTANCE_REPO_FILENAME = "instances.json" +INSTANCE_REPO_FILENAME = "instances_v2.json" INSTANCE_REPO_TYPE = "dataset" INSTANCE_XOR_SEED = "x3!+-9mi#nhlo%a02$9hna{]" diff --git a/src/browsergym/workarena/instance.py b/src/browsergym/workarena/instance.py index 8424fe0..39ca7ea 100644 --- a/src/browsergym/workarena/instance.py +++ b/src/browsergym/workarena/instance.py @@ -45,10 +45,18 @@ def encrypt_instance_password(password: str) -> str: return base64.b64encode(cipher_bytes).decode("utf-8") -def fetch_instances(): +def fetch_instances(filename: str = None): """ Load the latest instances from either a custom pool (SNOW_INSTANCE_POOL env var) or the gated HF dataset. + + Parameters: + ----------- + filename: str + Optional filename to fetch from the HF dataset. Defaults to INSTANCE_REPO_FILENAME. """ + if filename is None: + filename = INSTANCE_REPO_FILENAME + pool_path = os.getenv("SNOW_INSTANCE_POOL") if pool_path: path = os.path.expanduser(pool_path) @@ -62,13 +70,13 @@ def fetch_instances(): disable_progress_bars() path = hf_hub_download( repo_id=INSTANCE_REPO_ID, - filename=INSTANCE_REPO_FILENAME, + filename=filename, repo_type=INSTANCE_REPO_TYPE, ) logging.info("Loaded ServiceNow instances from the default instance pool.") except Exception as e: raise RuntimeError( - f"Could not access {INSTANCE_REPO_ID}/{INSTANCE_REPO_FILENAME}. " + f"Could not access {INSTANCE_REPO_ID}/{filename}. " "Make sure you have been granted access to the gated repo and that you are " "authenticated (run `huggingface-cli login` or set HUGGING_FACE_HUB_TOKEN)." ) from e @@ -77,6 +85,8 @@ def fetch_instances(): entries = json.load(f) for entry in entries: + if entry.get("error"): + raise RuntimeError(entry.get("message", "Unknown error from instance pool")) entry["url"] = entry["u"] entry["password"] = decrypt_instance_password(entry["p"]) del entry["u"] diff --git a/src/browsergym/workarena/tasks/form.py b/src/browsergym/workarena/tasks/form.py index 7ea3561..742dc0e 100644 --- a/src/browsergym/workarena/tasks/form.py +++ b/src/browsergym/workarena/tasks/form.py @@ -371,6 +371,30 @@ def get_init_scripts(self) -> List[str]: runInGsftMainOnlyAndProtectByURL(monitorChangeOnFields, '{url_suffix}'); """, + f""" + function removePersonalizeFormButton() {{ + waLog('Searching for Personalize Form button...', 'removePersonalizeFormButton'); + let button = document.querySelector('#togglePersonalizeForm'); + if (button) {{ + button.remove(); + waLog('Removed Personalize Form button', 'removePersonalizeFormButton'); + }} + }} + + runInGsftMainOnlyAndProtectByURL(removePersonalizeFormButton, '{url_suffix}'); + """, + f""" + function removeAdditionalActionsButton() {{ + waLog('Searching for Additional Actions button...', 'removeAdditionalActionsButton'); + let button = document.querySelector('button.additional-actions-context-menu-button'); + if (button) {{ + button.remove(); + waLog('Removed Additional Actions button', 'removeAdditionalActionsButton'); + }} + }} + + runInGsftMainOnlyAndProtectByURL(removeAdditionalActionsButton, '{url_suffix}'); + """, ] def start(self, page: Page) -> None: diff --git a/src/browsergym/workarena/tasks/list.py b/src/browsergym/workarena/tasks/list.py index 1da22ad..0f67f42 100644 --- a/src/browsergym/workarena/tasks/list.py +++ b/src/browsergym/workarena/tasks/list.py @@ -113,7 +113,28 @@ def all_configs(cls) -> List[dict]: return json.load(f) def get_init_scripts(self) -> List[str]: - return super().get_init_scripts() + ["registerGsftMainLoaded();"] + return super().get_init_scripts() + [ + "registerGsftMainLoaded();", + self._get_remove_personalize_list_button_script(), + ] + + def _get_remove_personalize_list_button_script(self): + """ + Removes the 'Personalize List' button on list pages. + """ + script = """ + function removePersonalizeListButton() { + waLog('Searching for Personalize List buttons...', 'removePersonalizeListButton'); + let buttons = document.querySelectorAll('i[data-type="list_mechanic2_open"]'); + buttons.forEach((button) => { + button.remove(); + }); + waLog('Removed ' + buttons.length + ' Personalize List buttons', 'removePersonalizeListButton'); + } + + runInGsftMainOnlyAndProtectByURL(removePersonalizeListButton, '_list.do'); + """ + return script def _get_visible_list(self, page: Page): self._wait_for_ready(page) diff --git a/src/browsergym/workarena/tasks/service_catalog.py b/src/browsergym/workarena/tasks/service_catalog.py index 879cfd7..dc7a760 100644 --- a/src/browsergym/workarena/tasks/service_catalog.py +++ b/src/browsergym/workarena/tasks/service_catalog.py @@ -225,6 +225,9 @@ def get_init_scripts(self) -> List[str]: "registerGsftMainLoaded()", self._get_disable_add_to_cart_script(), self._get_remove_top_items_panel_script(), + self._get_remove_add_content_button_script(), + self._get_remove_header_decorations_script(), + self._get_remove_more_options_buttons_script(), ] def _get_disable_add_to_cart_script(self): @@ -276,6 +279,60 @@ def _get_remove_top_items_panel_script(self): """ return script + def _get_remove_add_content_button_script(self): + """ + Removes the 'Add content' button from the service catalog page. + """ + script = """ + function removeAddContentButton() { + waLog('Searching for Add content button...', 'removeAddContentButton'); + let button = document.querySelector('button[aria-label="Add content"]'); + if (button) { + button.remove(); + waLog('Removed Add content button', 'removeAddContentButton'); + } + } + + runInGsftMainOnlyAndProtectByURL(removeAddContentButton, 'catalog_home'); + """ + return script + + def _get_remove_header_decorations_script(self): + """ + Removes all header decoration panels (edit/settings/close buttons) from the service catalog page. + """ + script = """ + function removeHeaderDecorations() { + waLog('Searching for header decoration panels...', 'removeHeaderDecorations'); + let panels = document.querySelectorAll('div.header_decorations'); + panels.forEach((panel) => { + panel.remove(); + }); + waLog('Removed ' + panels.length + ' header decoration panels', 'removeHeaderDecorations'); + } + + runInGsftMainOnlyAndProtectByURL(removeHeaderDecorations, 'catalog_home'); + """ + return script + + def _get_remove_more_options_buttons_script(self): + """ + Removes all 'More Options' buttons from the service catalog page. + """ + script = """ + function removeMoreOptionsButtons() { + waLog('Searching for More Options buttons...', 'removeMoreOptionsButtons'); + let buttons = document.querySelectorAll('button.btn.btn-icon.icon-ellipsis'); + buttons.forEach((button) => { + button.remove(); + }); + waLog('Removed ' + buttons.length + ' More Options buttons', 'removeMoreOptionsButtons'); + } + + runInGsftMainOnlyAndProtectByURL(removeMoreOptionsButtons, 'com.glideapp.servicecatalog'); + """ + return script + def setup_goal(self, page: Page) -> tuple[str, dict]: super().setup_goal(page=page)