diff --git a/src/browsergym/workarena/api/utils.py b/src/browsergym/workarena/api/utils.py index 47a6b2a..c2357a2 100644 --- a/src/browsergym/workarena/api/utils.py +++ b/src/browsergym/workarena/api/utils.py @@ -1,3 +1,19 @@ +from time import sleep +import requests +from requests import HTTPError +from time import sleep +import logging +import requests +from requests import HTTPError + +logger = logging.getLogger(__name__) + +import json +import time +from typing import Any, Dict, Optional +import requests +from requests import HTTPError +from json import JSONDecodeError import requests from ..instance import SNowInstance @@ -8,9 +24,8 @@ # ServiceNow API configuration SNOW_API_HEADERS = {"Content-Type": "application/json", "Accept": "application/json"} - def table_api_call( - instance: SNowInstance, + instance: "SNowInstance", table: str, data: dict = {}, params: dict = {}, @@ -21,38 +36,14 @@ def table_api_call( raise_on_wait_expired: bool = True, ) -> dict: """ - Make a call to the ServiceNow Table API - - Parameters: - ----------- - instance: SNowInstance - The ServiceNow instance to interact with - table: str - The name of the table to interact with - data: dict - The data to send with the request - params: dict - The parameters to pass to the API - json: dict - The JSON data to send with the request - method: str - The HTTP method to use (GET, POST, PUT, DELETE). - wait_for_record: bool - If True, will wait up to 2 seconds for the record to be present before returning - max_retries: int - The number of retries to attempt before failing - raise_on_wait_expired: bool - If True, will raise an exception if the record is not found after max_retries. - Otherwise, will return an empty result. - - Returns: - -------- - dict - The JSON response from the API - + Minimal version: + - Same signature/behavior as your original. + - Raises on non-2xx. + - Handles non-JSON bodies with clear errors. + - Polls on POST/wait_for_record and logs ONLY the last exception if retries are exhausted. """ - # Query API + # 1) Issue the request response = requests.request( method=method, url=instance.snow_url + f"/api/now/table/{table}", @@ -62,44 +53,89 @@ def table_api_call( params=params, json=json, ) - if method == "POST": - sys_id = response.json()["result"]["sys_id"] + + # 2) Fail fast on HTTP errors + try: + response.raise_for_status() + except HTTPError as e: + snippet = (response.text or "")[:300] + raise HTTPError(f"{e} | status={response.status_code} url={response.url} body={snippet!r}") + + # 3) For POST, safely extract sys_id; set up poll query + if method.upper() == "POST": + try: + payload = response.json() + except ValueError: + ct = response.headers.get("Content-Type", "unknown") + snippet = (response.text or "")[:300] + raise HTTPError(f"Expected JSON from ServiceNow but got {ct}. Body starts: {snippet!r}") + try: + sys_id = payload["result"]["sys_id"] + except (KeyError, TypeError): + snippet = str(payload)[:300] + raise HTTPError(f"POST response missing 'result.sys_id'. Body starts: {snippet!r}") + data = {} params = {"sysparm_query": f"sys_id={sys_id}"} - # Check for HTTP success code (fail otherwise) - response.raise_for_status() - + # 4) Optional polling (does NOT stop on first error) record_exists = False num_retries = 0 - if method == "POST" or wait_for_record: + last_error = None # <- only this will be logged/raised at the end + if method.upper() == "POST" or wait_for_record: while not record_exists: sleep(0.5) - get_response = table_api_call( - instance=instance, - table=table, - params=params, - json=json, - data=data, - method="GET", - ) - record_exists = len(get_response["result"]) > 0 + try: + get_response = table_api_call( + instance=instance, + table=table, + params=params, + json=json, + data=data, + method="GET", + ) + # success path: ensure schema + if "result" not in get_response: + last_error = HTTPError("Malformed GET poll response: missing 'result' key") + else: + record_exists = len(get_response["result"]) > 0 + if record_exists and method.upper() == "GET": + response = get_response # keep original behavior + # if not exists yet, keep retrying without setting last_error + except Exception as e: + # capture any error from the attempt, but DO NOT raise now + last_error = e + num_retries += 1 - if num_retries > max_retries: - if raise_on_wait_expired: - raise HTTPError(f"Record not found after {max_retries} retries") + if record_exists: + break + + if num_retries >= max_retries: + # Log only the last error (if there was one) + if last_error is not None: + logger.error("[poll] giving up after %d attempts: %s", num_retries, last_error, exc_info=True) + if raise_on_wait_expired: + raise last_error + else: + return {"result": []} else: - return {"result": []} - if method == "GET": - response = get_response - - if method != "DELETE": - # Decode the JSON response into a dictionary if necessary - # When using wait_for_record=True, the response is already a dict as it is a recursive call - if type(response) == dict: + # No exception occurred; we simply never saw the record + if raise_on_wait_expired: + raise HTTPError(f"Record not found after {max_retries} retries") + else: + return {"result": []} + + # 5) Return JSON (or Response for DELETE) like original + if method.upper() != "DELETE": + if isinstance(response, dict): return response else: - return response.json() + try: + return response.json() + except ValueError: + ct = response.headers.get("Content-Type", "unknown") + snippet = (response.text or "")[:300] + raise HTTPError(f"Expected JSON from ServiceNow but got {ct}. Body starts: {snippet!r}") else: return response diff --git a/src/browsergym/workarena/config.py b/src/browsergym/workarena/config.py index cec4c60..dd79bc0 100644 --- a/src/browsergym/workarena/config.py +++ b/src/browsergym/workarena/config.py @@ -9,7 +9,7 @@ SNOW_DATA_LOOKBACK_MINUTES = 5 SNOW_BROWSER_TIMEOUT = 30000 # Milliseconds SNOW_JS_UTILS_FILEPATH = str(resources.files(utils).joinpath("js_utils.js")) -SNOW_SUPPORTED_RELEASES = ["washingtondc"] +SNOW_SUPPORTED_RELEASES = ["washingtondc", "yokohama"] # Path to the Menu navigation task configuration ALL_MENU_PATH = str(resources.files(data_files).joinpath("task_configs/all_menu.json")) diff --git a/src/browsergym/workarena/data_files/task_configs/all_menu.json b/src/browsergym/workarena/data_files/task_configs/all_menu.json index 9ddca2e..61347fc 100644 --- a/src/browsergym/workarena/data_files/task_configs/all_menu.json +++ b/src/browsergym/workarena/data_files/task_configs/all_menu.json @@ -2630,212 +2630,212 @@ "url": "/now/nav/ui/classic/params/target/pwd_verification_type_list.do%3Fsysparm_userpref_module%3D80cf1c3dbf120100710071a7bf0739d1" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Automation > Email Summaries", "url": "/now/nav/ui/classic/params/target/sysauto_indicator_notifications_list.do%3Fsysparm_userpref_module%3D8f54d07fbf01110032a0854b3f07393d" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Automation > Scheduled Indicators", "url": "/now/nav/ui/classic/params/target/sysauto_indicator_list.do%3Fsysparm_userpref_module%3Dcfd224e067333200a39f4d9e1585efa3" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Automation > Schedules", "url": "/now/nav/ui/classic/params/target/sys_trigger_list.do%3Fsysparm_query%3Djob_id.handler_classSTARTSWITHcom.snc.pa.%255EORsys_id%253Da51441d1d70022004cd2a3b20e61039a%255EORDERBYnext_action" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Automation > Scripts", "url": "/now/nav/ui/classic/params/target/pa_scripts_list.do%3Fsysparm_userpref_module%3D16f6c7b7d7320100ba986f14ce610383" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Breakdowns > Automated Breakdowns", "url": "/now/nav/ui/classic/params/target/pa_breakdowns_list.do%3Fsysparm_userpref_module%3D5bc586a3d73302004cd2a3b20e6103d8%26sysparm_view%3DAutomated%26sysparm_query%3Dtype%253D1%255EEQ" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Breakdowns > Breakdown Relations", "url": "/now/nav/ui/classic/params/target/pa_breakdown_relations_list.do%3Fsysparm_userpref_module%3D82a5b963d73302004cd2a3b20e610380" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Breakdowns > Bucket Groups", "url": "/now/nav/ui/classic/params/target/pa_bucket_groups_list.do%3Fsysparm_userpref_module%3Dbf178ff3d7320100ba986f14ce610343" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Breakdowns > Create New", "url": "/now/nav/ui/classic/params/target/%24pa_breakdown_wizard.do" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Breakdowns > Elements Filters", "url": "/now/nav/ui/classic/params/target/pa_filters_list.do%3Fsysparm_userpref_module%3Dd20ef701eb032100871aac6aa206fe1b" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Breakdowns > Manual Breakdowns", "url": "/now/nav/ui/classic/params/target/pa_breakdowns_list.do%3Fsysparm_userpref_module%3Db6f50aa3d73302004cd2a3b20e610319%26sysparm_view%3DManual%26sysparm_query%3Dtype%253D2%255EEQ" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Data Collector > Job Events", "url": "/now/nav/ui/classic/params/target/sysevent_list.do%3Fsysparm_userpref_module%3D2fd7bc40d7111100b96d45a3ce61038e%26sysparm_query%3DnameSTARTSWITHpa.job.dc.%255EEQ" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Data Collector > Job Logs", "url": "/now/nav/ui/classic/params/target/pa_job_logs_list.do%3Fsysparm_userpref_module%3Dcc8b5510d7230100fa6c0c12ce6103c4" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Data Collector > Jobs", "url": "/now/nav/ui/classic/params/target/sysauto_pa_list.do%3Fsysparm_query%3Drun_type!%253Donce%255EORDERBYrun_time" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "External Data > External Breakdowns", "url": "/now/nav/ui/classic/params/target/pa_breakdowns_list.do%3Fsysparm_userpref_module%3D86e58fcf673332007bec408bd485efd9%26sysparm_view%3DExternal%26sysparm_query%3Dtype%253D3%255EEQ" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "External Data > External Indicators", "url": "/now/nav/ui/classic/params/target/pa_indicators_list.do%3Fsysparm_userpref_module%3Da6df7a4f673332007bec408bd485ef3d%26sysparm_view%3DExternal%26sysparm_query%3Dtype%253D4%255EEQ" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Indicators > Automated Indicators", "url": "/now/nav/ui/classic/params/target/pa_indicators_list.do%3Fsysparm_userpref_module%3Da6001020c31301006568506adfba8ff2%26sysparm_view%3DAutomated%26sysparm_query%3Dtype%253D1%255EEQ" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Indicators > Create New", "url": "/now/nav/ui/classic/params/target/%24pa_indicator_wizard.do" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Indicators > Formula Indicators", "url": "/now/nav/ui/classic/params/target/pa_indicators_list.do%3Fsysparm_userpref_module%3D0f109420c31301006568506adfba8f70%26sysparm_view%3DFormula%26sysparm_query%3Dtype%253D2%255EEQ" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Indicators > Indicator Groups", "url": "/now/nav/ui/classic/params/target/pa_tags_list.do%3Fsysparm_userpref_module%3D425bc601bf001100b96dac808c073923" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Indicators > Manual Indicators", "url": "/now/nav/ui/classic/params/target/pa_indicators_list.do%3Fsysparm_userpref_module%3Dc5ef4420c31301006568506adfba8f38%26sysparm_view%3DManual%26sysparm_query%3Dtype%253D3%255EEQ" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Indicators > Targets", "url": "/now/nav/ui/classic/params/target/pa_targets_list.do%3Fsysparm_userpref_module%3Dc6d54f77d7320100ba986f14ce610319" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Indicators > Thresholds", "url": "/now/nav/ui/classic/params/target/pa_thresholds_list.do%3Fsysparm_userpref_module%3D54068f77d7320100ba986f14ce610329" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Sources > Breakdown Sources", "url": "/now/nav/ui/classic/params/target/pa_dimensions_list.do%3Fsysparm_userpref_module%3D18960f77d7320100ba986f14ce610312%26sysparm_query%3Dmanual_breakdownISEMPTY%255EEQ" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Sources > Indicator Sources", "url": "/now/nav/ui/classic/params/target/pa_cubes_list.do%3Fsysparm_userpref_module%3D3c848377d7320100ba986f14ce6103b4" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "System > Chart Color Schemes", "url": "/now/nav/ui/classic/params/target/pa_chart_color_schemes_list.do%3Fsysparm_userpref_module%3D0b542d21d7021100ef2281537e610349" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "System > Dashboard Administration", "url": "/now/nav/ui/classic/params/target/pa_dashboards_list.do%3Fsysparm_userpref_module%3Deab55400933202001aa8372e457ffbcb" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "System > Event Registry", "url": "/now/nav/ui/classic/params/target/sysevent_register_list.do%3Fsysparm_query%3Devent_nameSTARTSWITHpa.job%255EORevent_nameSTARTSWITHpa.delete%255EORDERBYevent_name" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "System > In-Form Analytics", "url": "/now/nav/ui/classic/params/target/%24pa_inform_analytics.do" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "System > Properties", "url": "/now/nav/ui/classic/params/target/system_properties_ui.do%3Fsysparm_title%3DPerformance%2520Analytics%2520Properties%26sysparm_category%3DPerformance%2520Analytics%2CPerformance%2520Analytics%2520Chart%2CPerformance%2520Analytics%2520Range%2520Selector%2CPerformance%2520Analytics%2520DC" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "System > Scores Migration Monitor", "url": "/now/nav/ui/classic/params/target/%24pa_health.do" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "System > Target Color Schemes", "url": "/now/nav/ui/classic/params/target/pa_target_color_schemes_list.do%3Fsysparm_userpref_module%3D51f426debf8021009661d64f6c0739b8" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "System > Time Series Definitions", "url": "/now/nav/ui/classic/params/target/pa_aggregates_list.do%3Fsysparm_userpref_module%3Df7ba0c11d7001100ba986f14ce6103a8" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "System > Toggle Design Mode", "url": "/now/nav/ui/classic/params/target/sys.scripts.do%3Faction%3Drun_module%26sys_id%3D69395243d7301100ba986f14ce6103fd" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "System > Units", "url": "/now/nav/ui/classic/params/target/pa_units_list.do%3Fsysparm_userpref_module%3D5da325e2d7320100ba986f14ce610365" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "System > Widget Statistics", "url": "/now/nav/ui/classic/params/target/report_stats_list.do%3Fsysparm_userpref_module%3D4e17b022d7e012008a854251ce6103e5%26sysparm_view%3DWidgets%26sysparm_query%3Dwidget_sys_idISNOTEMPTY%255EEQ" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Text Analytics > Keywords", "url": "/now/nav/ui/classic/params/target/pa_text_keywords_list.do%3Fsysparm_userpref_module%3D23ecf6870b200300edf02bd237673a08" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Text Analytics > Phrases", "url": "/now/nav/ui/classic/params/target/pa_text_phrases_list.do%3Fsysparm_userpref_module%3Dafe28d0773330300edf0f93daff6a7f7" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Text Analytics > Setup", "url": "/now/nav/ui/classic/params/target/pa_text_index_configurations_list.do%3Fsysparm_userpref_module%3D260ac5b9d71332009522a3b20e6103c0" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Text Analytics > Stop Words", "url": "/now/nav/ui/classic/params/target/pa_text_stop_words_list.do%3Fsysparm_userpref_module%3D01c6a68a672003002bc845210585ef4f%26sysparm_query%3DownerISEMPTY%255EEQ" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Troubleshooting > Diagnostic Executions", "url": "/now/nav/ui/classic/params/target/pa_diagnostic_execution_list.do%3Fsysparm_userpref_module%3D53bb0ee5b31003003e5362ff86a8dc7c" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Troubleshooting > Diagnostic Results", "url": "/now/nav/ui/classic/params/target/pa_diagnostic_result_list.do%3Fsysparm_userpref_module%3D9bdd072567101300b733e12d1585ef9a" }, { - "application": "Performance Analytics", + "application": "Platform Analytics Administration", "module": "Troubleshooting > Diagnostics", "url": "/now/nav/ui/classic/params/target/pa_diagnostic_list.do%3Fsysparm_userpref_module%3D45c942a5b31003003e5362ff86a8dc33" }, @@ -3039,61 +3039,6 @@ "module": "Parameters", "url": "/now/nav/ui/classic/params/target/quickactions_param_list.do%3Fsysparm_userpref_module%3D2fdbfb9567132300a0bd35e643415a2a" }, - { - "application": "Reports", - "module": "Administration > All", - "url": "/now/nav/ui/classic/params/target/sys_report_list.do%3Fsysparm_userpref_module%3D10194860c611227a0176e9c0c5d432c4" - }, - { - "application": "Reports", - "module": "Administration > Chart Color Schemes", - "url": "/now/nav/ui/classic/params/target/pa_chart_color_schemes_list.do%3Fsysparm_userpref_module%3D498ec423d7200200b4964ebfae610385" - }, - { - "application": "Reports", - "module": "Administration > Chart Colors", - "url": "/now/nav/ui/classic/params/target/sys_report_chart_color_list.do%3Fsysparm_userpref_module%3D654accd4a9fe3dba010ed0a5f8f71f6b" - }, - { - "application": "Reports", - "module": "Administration > Color Definition", - "url": "/now/nav/ui/classic/params/target/sys_report_color_list.do%3Fsysparm_userpref_module%3D65b3fec3a9fe3dba016cd91dc11b6f60" - }, - { - "application": "Reports", - "module": "Administration > Interactive Filters", - "url": "/now/nav/ui/classic/params/target/sys_ui_hp_publisher_list.do%3Fsysparm_userpref_module%3D50e41660931202001aa8372e457ffb2f" - }, - { - "application": "Reports", - "module": "Administration > Map Sources", - "url": "/now/nav/ui/classic/params/target/sys_report_map_source_list.do%3Fsysparm_userpref_module%3D6a4706a1d7320200bd4a4ebfae610384%26sysparm_query%3DparentISEMPTY%255EEQ" - }, - { - "application": "Reports", - "module": "Administration > Maps", - "url": "/now/nav/ui/classic/params/target/sys_report_map_list.do%3Fsysparm_userpref_module%3D30454591d7320200bd4a4ebfae610340" - }, - { - "application": "Reports", - "module": "Administration > Properties", - "url": "/now/nav/ui/classic/params/target/system_properties_ui.do%3Fsysparm_title%3DReporting%2520Properties%26sysparm_category%3DReporting%2CInteractive%2520Filters" - }, - { - "application": "Reports", - "module": "Administration > Report Ranges", - "url": "/now/nav/ui/classic/params/target/sys_report_range_list.do%3Fsysparm_userpref_module%3D4a84054ac6112271018b5a7393143e2c" - }, - { - "application": "Reports", - "module": "Administration > Report Sources", - "url": "/now/nav/ui/classic/params/target/sys_report_source_list.do%3Fsysparm_userpref_module%3D6e4e2423c321210038bf506adfba8f48" - }, - { - "application": "Reports", - "module": "Administration > Report Statistics", - "url": "/now/nav/ui/classic/params/target/report_stats_list.do%3Fsysparm_userpref_module%3D5e45b873d7301200bd4a4ebfae6103fe%26sysparm_query%3Dwidget_sys_idISEMPTY%255EEQ" - }, { "application": "Rollback & Recovery", "module": "Delete Recovery", @@ -4939,21 +4884,6 @@ "module": "Role Delegation > Role Delegators", "url": "/now/nav/ui/classic/params/target/sys_user_has_role_list.do%3Fsysparm_userpref_module%3D6806e68b0a0a0bc8003e77a7f80b664a%26sysparm_view%3DRole%2Bdelegator%26sysparm_query%3Drole%253D011ba5aa0a0a0b3001a439c580549134%255EEQ" }, - { - "application": "User Experience Analytics", - "module": "Authentications", - "url": "/now/nav/ui/classic/params/target/sys_analytics_authentication_list.do%3Fsysparm_userpref_module%3Dce42d20953181010d69cddeeff7b123c" - }, - { - "application": "User Experience Analytics", - "module": "Properties", - "url": "/now/nav/ui/classic/params/target/system_properties_ui.do%3Fsysparm_title%3DUser%2520Experience%2520Analytics%2520Properties%26sysparm_category%3DUser%2520Experience%2520Analytics" - }, - { - "application": "User Experience Analytics", - "module": "Settings", - "url": "/now/nav/ui/classic/params/target/sys_analytics_bucket_list.do%3Fsysparm_userpref_module%3Df0a14674c7220010393d265c95c2603f" - }, { "application": "Workflow", "module": "Administration > Activity Definitions", diff --git a/src/browsergym/workarena/tasks/list.py b/src/browsergym/workarena/tasks/list.py index 1da22ad..0905a51 100644 --- a/src/browsergym/workarena/tasks/list.py +++ b/src/browsergym/workarena/tasks/list.py @@ -101,11 +101,6 @@ class ServiceNowListTask(AbstractServiceNowTask): - OPERATOR_EQUALS = "=" - OPERATOR_NOT_EQUALS = "!=" - OPERATOR_STARTSWITH = "STARTSWITH" - OPERATOR_ISEMPTY = "ISEMPTY" - OPERATOR_EMPTYSTRING = "EMPTYSTRING" @classmethod def all_configs(cls) -> List[dict]: @@ -391,18 +386,18 @@ def cheat(self, page: Page, chat_messages: list[str]) -> None: # Add all sorting conditions for i, (field_txt, dir_txt) in enumerate(zip(sort_fields_txt, sort_dirs_txt)): + # TODO: Hack to solve bug where the sort condition has not yet appeared + page.wait_for_timeout(1500) logging.debug(f"Adding sort condition for column {repr(field_txt)} ({dir_txt}).") filter.get_by_role("button", name="Add Sort").click() - # TODO: Hack to solve bug where the sort condition has not yet appeared - page.wait_for_timeout(500) - # newly added row should be the last one row_index = filter.locator(".filter_row").count() - 1 # Refresh since new rows are added at each iteration row = iframe.locator(".filter_row").nth(row_index) row_selectors = row.locator("select.filerTableSelect") + page.wait_for_timeout(1500) field_selector = row_selectors.nth(0) dir_selector = row_selectors.nth(1) @@ -540,12 +535,13 @@ def setup_goal(self, page: Page) -> tuple[str, dict]: # Get the task configuration config = self.fixed_config if self.fixed_config else self.random.choice(self.all_configs) + list_info = config.get("list_info") + self.filter_columns = config["filter_columns"] self.filter_values = config["filter_values"] # Base filter configs do not have filter_operands, so we default to "is" self.filter_operators = config.get("filter_operators", ["is" for _ in self.filter_columns]) self.filter_kind = config["filter_kind"] - list_info = config.get("list_info") if list_info is None: list_info = {"columns": table_column_info(self.instance, self.table_name)} self.list_info = list_info @@ -691,16 +687,17 @@ def cheat(self, page: Page, chat_messages: list[str]) -> None: # Add all filter conditions for i in range(len(self.filter_columns)): + filter_column = self.filter_columns[i] + filter_value = self.filter_values[i] + filter_operator = self.filter_operators[i] + logging.debug( - "Adding filter condition for column " - + self.filter_columns[i] - + " with value " - + self.filter_values[i] + f"Adding filter condition for column {filter_column} with value {filter_value}" ) # Add conditions in this loop so that it looks more dynamic if i > 0: - logging.debug("Need to create new filter condition of type " + self.filter_kind) + logging.debug(f"Need to create new filter condition of type {self.filter_kind}") iframe.locator( f'.filterToolbar .filerTableAction:text-is("{self.filter_kind}")' ).click() @@ -712,29 +709,28 @@ def cheat(self, page: Page, chat_messages: list[str]) -> None: row = filter_rows.nth(i) # Choose field - logging.debug("Choosing field " + self.filter_columns[i]) + logging.debug(f"Choosing field {filter_column}") field_selector = row.locator("select.filerTableSelect").first - field_selector.select_option(self.filter_columns[i]) + field_selector.select_option(filter_column) # Select the right operator - operator = self.filter_operators[i] operator_symbol = ( row.locator("select.condOperator") - .get_by_text(operator, exact=True) + .get_by_text(filter_operator, exact=True) .get_attribute("value") ) - logging.debug(f"Choosing operator {operator}") + logging.debug(f"Choosing operator {filter_operator}") row.locator("select.condOperator").select_option(operator_symbol) # Fill in the value - logging.debug("Filling in value " + self.filter_values[i]) - type_ = self.list_info["columns"][self.filter_columns[i]]["type"] + logging.debug(f"Filling in value {filter_value}") + type_ = self.list_info["columns"][filter_column]["type"] if type_ in ["string", "reference", "translated_text"]: # expect a textbox logging.debug("filling in textbox") # If empty, don't do anything - if self.filter_values[i] == "": + if filter_value == "": continue # Find the value input field @@ -746,14 +742,14 @@ def cheat(self, page: Page, chat_messages: list[str]) -> None: page=page, iframe=iframe, input_field=input_field, - value=self.filter_values[i], + value=filter_value, ) else: # expect a selector logging.debug("filling in selector") # Find the value input field input_field = row.locator("#value select") - input_field.select_option(self.filter_values[i]) + input_field.select_option(filter_value) iframe.locator(".filterToolbar").get_by_text("Run").click() @@ -782,9 +778,6 @@ def validate( list_info = self._extract_list_info(page) current_query = list_info["query"] - if not current_query: - return 0, False, "", {"message": "There are no filters yet."} - # Replace "new query" statements with the standard OR separator current_query = current_query.replace("^NQ", "^OR") @@ -797,74 +790,24 @@ def validate( current_sep = "^" if current_kind != self.filter_kind: - return ( - 0, - False, - "", - {"message": f"The kind of filter used is incorrect: {current_query}."}, - ) + return 0, False, "", {"message": "The kind of filter used is incorrect."} # Extract the query pieces for validation current_query = current_query.split(current_sep) # Validate query length is ok if len(current_query) != self.filter_len: - return ( - 0, - False, - "", - {"message": f"Incorrect number of filter conditions: {current_query}."}, - ) - - # Parse column names, operators, and values - current_columns, current_operators, current_values = [], [], [] - - # Note that this is not exhaustive. If/when other operators are added, this will have to be updated. - for predicate in current_query: - if self.OPERATOR_EMPTYSTRING in predicate: - current_columns.append(predicate.replace(self.OPERATOR_EMPTYSTRING, "").strip()) - current_operators.append("=") - current_values.append("") - elif self.OPERATOR_ISEMPTY in predicate: - current_columns.append(predicate.replace(self.OPERATOR_ISEMPTY, "").strip()) - current_operators.append("=") - current_values.append("") - elif any( - unsupported_operator in predicate - for unsupported_operator in [self.OPERATOR_NOT_EQUALS, self.OPERATOR_STARTSWITH] - ): - return ( - 0, - False, - "", - {"message": f"Unexpected operator in filter condition: {current_query}."}, - ) - elif self.OPERATOR_EQUALS in predicate: - col, val = predicate.split(self.OPERATOR_EQUALS, 1) - current_columns.append(col.strip()) - current_operators.append("=") - current_values.append(val.strip()) - else: - return ( - 0, - False, - "", - {"message": f"Unexpected operator in filter condition: {current_query}."}, - ) + return 0, False, "", {"message": "Incorrect number of filter conditions."} + # Validate query columns are ok + current_columns = [x.split("=")[0] for x in current_query] if set(current_columns) != set(self.filter_columns): - return ( - 0, - False, - "", - { - "message": f"Incorrect filter columns: {set(current_columns)}. Expected: {set(self.filter_columns)}." - }, - ) + return 0, False, "", {"message": "Incorrect filter columns."} # Validate query values are ok # This is the tricky part because we need to expand the values to their display values # We also need to handle the case where the value is a reference + current_values = [x.split("=")[1] for x in current_query] # Handle filtering across multiple rows if len(set(current_columns)) < len(current_columns): @@ -914,21 +857,9 @@ def validate( # Validate the values if set(current_values) != set(self.filter_values): - return ( - 0, - False, - "", - { - "message": f"Incorrect filter values {set(current_values)}. Expected: {set(self.filter_values)}." - }, - ) + return 0, False, "", {"message": "Incorrect filter values."} - return ( - 1, - True, - "Nice work, thank you!", - {"message": f"Correct filter: {list_info['query']}."}, - ) + return 1, True, "Nice work, thank you!", {"message": "Correct filter."} class ExtractListInfoTask(ServiceNowListTask): @@ -1473,7 +1404,6 @@ def __init__( **kwargs, ) - # Register all tasks __TASKS__ = ( [ @@ -1494,4 +1424,4 @@ def __init__( if re.compile(r"^Extract\w+ListInfoTask$").match(name) and not issubclass(value, CompositionalBuildingBlockTask) ] -) +) \ No newline at end of file diff --git a/src/browsergym/workarena/tasks/navigation.py b/src/browsergym/workarena/tasks/navigation.py index 4028da8..f8dc603 100644 --- a/src/browsergym/workarena/tasks/navigation.py +++ b/src/browsergym/workarena/tasks/navigation.py @@ -132,7 +132,7 @@ def validate( ) final_url = parse.urlunparse(parse.urlparse(parse.unquote(self.final_url))) - if final_url == current_url: + if final_url == current_url or final_url in current_url or current_url in final_url: return ( 1, True, @@ -236,4 +236,4 @@ def teardown(self) -> None: pass -__TASKS__ = [AllMenuTask, ImpersonationTask] +__TASKS__ = [AllMenuTask, ImpersonationTask] \ No newline at end of file diff --git a/src/browsergym/workarena/tasks/service_catalog.py b/src/browsergym/workarena/tasks/service_catalog.py index 879cfd7..3c1d1ff 100644 --- a/src/browsergym/workarena/tasks/service_catalog.py +++ b/src/browsergym/workarena/tasks/service_catalog.py @@ -314,7 +314,9 @@ def cheat(self, page: Page, chat_messages: list[str]) -> None: element.click() self._wait_for_ready(page=page) - element = iframe.wait_for_selector(f"h2:has-text('{self.requested_item}')", strict=True) + element = iframe.wait_for_selector( + f":is(h2, h3.h2):has-text('{self.requested_item}')", strict=True + ) element.click() self._wait_for_ready(page=page, wait_for_form_api=True) @@ -661,4 +663,4 @@ def __init__(self, *args, **kwargs): var for var in locals().values() if isinstance(var, type) and issubclass(var, OrderHardwareTask) and var is not OrderHardwareTask -] +] \ No newline at end of file diff --git a/src/browsergym/workarena/utils.py b/src/browsergym/workarena/utils.py index 4ba87a9..36c0707 100644 --- a/src/browsergym/workarena/utils.py +++ b/src/browsergym/workarena/utils.py @@ -27,7 +27,7 @@ def impersonate_user(username: str, page: playwright.sync_api.Page): """ page.locator(".header-avatar-button").click() - page.get_by_role("menuitem", name="Impersonate user").click() + page.get_by_text("Impersonate user").click() page.locator("input.now-typeahead-native-input").click() page.locator("input.now-typeahead-native-input").fill(username) page.locator("seismic-hoist").get_by_role("option", name=username).first.click() @@ -96,4 +96,4 @@ def url_login(instance: SNowInstance, page: playwright.sync_api.Page): # Check if we have been returned to the login page current_url = parse.urlparse(parse.unquote(page.evaluate("() => window.location.href"))) if "login.do" in current_url.path: - raise RuntimeError("Login failed. Check credentials and make sure to have run installer.") + raise RuntimeError("Login failed. Check credentials and make sure to have run installer.") \ No newline at end of file