browsergym-workarena 0.4.3__py3-none-any.whl → 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- browsergym/workarena/__init__.py +1 -1
- browsergym/workarena/api/system_properties.py +66 -0
- browsergym/workarena/config.py +9 -1
- browsergym/workarena/data_files/task_configs/report_retrieval_minmax_task.json +1 -1
- browsergym/workarena/data_files/task_configs/report_retrieval_value_task.json +1 -1
- browsergym/workarena/install.py +138 -50
- browsergym/workarena/instance.py +124 -17
- browsergym/workarena/tasks/dashboard.py +34 -4
- browsergym/workarena/tasks/scripts/generate_dashboard_configs.py +11 -2
- browsergym/workarena/tasks/scripts/{navigation.py → generate_navigation_tasks.py} +4 -1
- browsergym/workarena/tasks/scripts/knowledge.py +6 -4
- {browsergym_workarena-0.4.3.dist-info → browsergym_workarena-0.5.0.dist-info}/METADATA +9 -21
- {browsergym_workarena-0.4.3.dist-info → browsergym_workarena-0.5.0.dist-info}/RECORD +16 -15
- {browsergym_workarena-0.4.3.dist-info → browsergym_workarena-0.5.0.dist-info}/WHEEL +0 -0
- {browsergym_workarena-0.4.3.dist-info → browsergym_workarena-0.5.0.dist-info}/entry_points.txt +0 -0
- {browsergym_workarena-0.4.3.dist-info → browsergym_workarena-0.5.0.dist-info}/licenses/LICENSE +0 -0
browsergym/workarena/install.py
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
1
4
|
import html
|
|
2
5
|
import json
|
|
3
6
|
import logging
|
|
@@ -10,6 +13,7 @@ from tenacity import retry, stop_after_attempt, retry_if_exception_type
|
|
|
10
13
|
from requests import HTTPError
|
|
11
14
|
from time import sleep
|
|
12
15
|
|
|
16
|
+
from .api.system_properties import get_sys_property, set_sys_property
|
|
13
17
|
from .api.ui_themes import get_workarena_theme_variants
|
|
14
18
|
from .api.user import create_user
|
|
15
19
|
from .api.utils import table_api_call, table_column_info
|
|
@@ -38,7 +42,7 @@ from .config import (
|
|
|
38
42
|
EXPECTED_USER_FORM_FIELDS_PATH,
|
|
39
43
|
# Patch flag for reports
|
|
40
44
|
REPORT_PATCH_FLAG,
|
|
41
|
-
|
|
45
|
+
REPORT_FILTER_PROPERTY,
|
|
42
46
|
# Supported ServiceNow releases
|
|
43
47
|
SNOW_SUPPORTED_RELEASES,
|
|
44
48
|
# For workflows setup
|
|
@@ -47,55 +51,52 @@ from .config import (
|
|
|
47
51
|
UI_THEMES_UPDATE_SET,
|
|
48
52
|
)
|
|
49
53
|
from .api.user import set_user_preference
|
|
50
|
-
from .instance import SNowInstance
|
|
54
|
+
from .instance import SNowInstance as _BaseSNowInstance
|
|
51
55
|
from .utils import url_login
|
|
52
56
|
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
_CLI_INSTANCE_URL: str | None = None
|
|
59
|
+
_CLI_INSTANCE_PASSWORD: str | None = None
|
|
60
|
+
|
|
57
61
|
|
|
62
|
+
def SNowInstance(snow_credentials: tuple[str, str] | None = None):
|
|
58
63
|
"""
|
|
59
|
-
instance
|
|
64
|
+
Wrapper around the standard SNowInstance that uses CLI-provided instance URL and password if none are provided.
|
|
65
|
+
"""
|
|
66
|
+
if not _CLI_INSTANCE_URL:
|
|
67
|
+
raise RuntimeError("Installer requires --instance-url to create a SNowInstance.")
|
|
60
68
|
|
|
61
|
-
|
|
62
|
-
instance=instance,
|
|
63
|
-
table="sys_properties",
|
|
64
|
-
params={"sysparm_query": f"name={property_name}", "sysparm_fields": "sys_id"},
|
|
65
|
-
)["result"]
|
|
69
|
+
resolved_creds = snow_credentials
|
|
66
70
|
|
|
67
|
-
if
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
if resolved_creds is None:
|
|
72
|
+
if not _CLI_INSTANCE_PASSWORD:
|
|
73
|
+
raise RuntimeError(
|
|
74
|
+
"Installer requires --instance-password (or explicit credentials) to create a SNowInstance."
|
|
75
|
+
)
|
|
76
|
+
resolved_creds = ("admin", _CLI_INSTANCE_PASSWORD)
|
|
73
77
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
method=method,
|
|
78
|
-
json={"name": property_name, "value": value},
|
|
78
|
+
return _BaseSNowInstance(
|
|
79
|
+
snow_url=_CLI_INSTANCE_URL,
|
|
80
|
+
snow_credentials=resolved_creds,
|
|
79
81
|
)
|
|
80
82
|
|
|
81
|
-
# Verify that the property was updated
|
|
82
|
-
assert property["result"]["value"] == value, f"Error setting {property_name}."
|
|
83
|
-
|
|
84
83
|
|
|
85
|
-
def
|
|
84
|
+
def _is_dev_portal_instance() -> bool:
|
|
86
85
|
"""
|
|
87
|
-
|
|
86
|
+
Check if the instance is a ServiceNow Developer Portal instance.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
--------
|
|
90
|
+
bool: True if the instance is a developer portal instance, False otherwise.
|
|
88
91
|
|
|
89
92
|
"""
|
|
90
93
|
instance = SNowInstance()
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
instance
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return property_value
|
|
94
|
+
# Check if the instance url has the for devXXXXXX.service-now.com format (where X is a digit)
|
|
95
|
+
if re.match(r"^https?://dev\d{6}\.service-now\.com", instance.snow_url):
|
|
96
|
+
logging.info("Detected a developer portal instance...")
|
|
97
|
+
return True
|
|
98
|
+
logging.info("Detected an internal instance...")
|
|
99
|
+
return False
|
|
99
100
|
|
|
100
101
|
|
|
101
102
|
def _install_update_set(path: str, name: str):
|
|
@@ -797,7 +798,9 @@ def enable_url_login():
|
|
|
797
798
|
Configure the instance to allow login via URL.
|
|
798
799
|
|
|
799
800
|
"""
|
|
800
|
-
|
|
801
|
+
set_sys_property(
|
|
802
|
+
instance=SNowInstance(), property_name="glide.security.restrict.get.login", value="false"
|
|
803
|
+
)
|
|
801
804
|
logging.info("URL login enabled.")
|
|
802
805
|
|
|
803
806
|
|
|
@@ -808,7 +811,28 @@ def disable_password_policies():
|
|
|
808
811
|
Notes: this is required to allow the creation of users with weak passwords.
|
|
809
812
|
|
|
810
813
|
"""
|
|
811
|
-
|
|
814
|
+
set_sys_property(
|
|
815
|
+
instance=SNowInstance(),
|
|
816
|
+
property_name="glide.security.password.policy.enabled",
|
|
817
|
+
value="false",
|
|
818
|
+
)
|
|
819
|
+
set_sys_property(
|
|
820
|
+
instance=SNowInstance(), property_name="glide.apply.password_policy.on_login", value="false"
|
|
821
|
+
)
|
|
822
|
+
# Exception handling since this property is sometimes read-only on some instances
|
|
823
|
+
try:
|
|
824
|
+
set_sys_property(
|
|
825
|
+
instance=SNowInstance(),
|
|
826
|
+
property_name="glide.authenticate.api.user.reset_password.mandatory",
|
|
827
|
+
value="false",
|
|
828
|
+
)
|
|
829
|
+
except Exception:
|
|
830
|
+
logging.warning(
|
|
831
|
+
"Warning: Failed to set sys property "
|
|
832
|
+
"'glide.authenticate.api.user.reset_password.mandatory'. Continuing.",
|
|
833
|
+
exc_info=True,
|
|
834
|
+
)
|
|
835
|
+
|
|
812
836
|
logging.info("Password policies disabled.")
|
|
813
837
|
|
|
814
838
|
|
|
@@ -817,8 +841,14 @@ def disable_guided_tours():
|
|
|
817
841
|
Hide guided tour popups
|
|
818
842
|
|
|
819
843
|
"""
|
|
820
|
-
|
|
821
|
-
|
|
844
|
+
set_sys_property(
|
|
845
|
+
instance=SNowInstance(), property_name="com.snc.guided_tours.sp.enable", value="false"
|
|
846
|
+
)
|
|
847
|
+
set_sys_property(
|
|
848
|
+
instance=SNowInstance(),
|
|
849
|
+
property_name="com.snc.guided_tours.standard_ui.enable",
|
|
850
|
+
value="false",
|
|
851
|
+
)
|
|
822
852
|
logging.info("Guided tours disabled.")
|
|
823
853
|
|
|
824
854
|
|
|
@@ -836,7 +866,9 @@ def disable_analytics_popups():
|
|
|
836
866
|
Disable analytics popups (needs to be done through UI since Vancouver release)
|
|
837
867
|
|
|
838
868
|
"""
|
|
839
|
-
|
|
869
|
+
set_sys_property(
|
|
870
|
+
instance=SNowInstance(), property_name="glide.analytics.enabled", value="false"
|
|
871
|
+
)
|
|
840
872
|
logging.info("Analytics popups disabled.")
|
|
841
873
|
|
|
842
874
|
|
|
@@ -850,7 +882,8 @@ def setup_ui_themes():
|
|
|
850
882
|
check_ui_themes_installed()
|
|
851
883
|
|
|
852
884
|
logging.info("Setting default UI theme")
|
|
853
|
-
|
|
885
|
+
set_sys_property(
|
|
886
|
+
instance=SNowInstance(),
|
|
854
887
|
property_name="glide.ui.polaris.theme.custom",
|
|
855
888
|
value=get_workarena_theme_variants(SNowInstance())[0]["theme.sys_id"],
|
|
856
889
|
)
|
|
@@ -894,7 +927,9 @@ def check_ui_themes_installed():
|
|
|
894
927
|
|
|
895
928
|
def set_home_page():
|
|
896
929
|
logging.info("Setting default home page")
|
|
897
|
-
|
|
930
|
+
set_sys_property(
|
|
931
|
+
instance=SNowInstance(), property_name="glide.login.home", value="/now/nav/ui/home"
|
|
932
|
+
)
|
|
898
933
|
|
|
899
934
|
|
|
900
935
|
def wipe_system_admin_preferences():
|
|
@@ -918,9 +953,9 @@ def wipe_system_admin_preferences():
|
|
|
918
953
|
)
|
|
919
954
|
|
|
920
955
|
|
|
921
|
-
def
|
|
956
|
+
def is_report_filter_using_relative_time(filter):
|
|
922
957
|
"""
|
|
923
|
-
Heuristic to check if a report is filtering based on time
|
|
958
|
+
Heuristic to check if a report is filtering based on relative time
|
|
924
959
|
|
|
925
960
|
This aims to detect the use of functions like "gs.endOfToday()". To avoid hardcoding all of them,
|
|
926
961
|
we simply check for the use of keywords. Our filter is definitely too wide, but that's ok.
|
|
@@ -938,6 +973,32 @@ def patch_report_filters():
|
|
|
938
973
|
logging.info("Patching reports with date filter...")
|
|
939
974
|
|
|
940
975
|
instance = SNowInstance()
|
|
976
|
+
filter_config = instance.report_filter_config
|
|
977
|
+
|
|
978
|
+
# If the report date filter is already set, we use the existing values (would be the case on reinstall)
|
|
979
|
+
if not filter_config:
|
|
980
|
+
# Set the report date filter to current date as YYYY-MM-DD and time filter to current time as HH:MM:SS
|
|
981
|
+
now = datetime.now()
|
|
982
|
+
report_date_filter = now.strftime("%Y-%m-%d")
|
|
983
|
+
report_time_filter = now.strftime("%H:%M:%S")
|
|
984
|
+
# ... save the filter config
|
|
985
|
+
logging.info(
|
|
986
|
+
f"Setting report date filter to {report_date_filter} and time filter to {report_time_filter} via {REPORT_FILTER_PROPERTY}"
|
|
987
|
+
)
|
|
988
|
+
set_sys_property(
|
|
989
|
+
instance=instance,
|
|
990
|
+
property_name=REPORT_FILTER_PROPERTY,
|
|
991
|
+
value=json.dumps(
|
|
992
|
+
{"report_date_filter": report_date_filter, "report_time_filter": report_time_filter}
|
|
993
|
+
),
|
|
994
|
+
)
|
|
995
|
+
else:
|
|
996
|
+
# Use the existing configuration
|
|
997
|
+
logging.info(
|
|
998
|
+
f"Using existing report date filter {filter_config['report_date_filter']} and time filter {filter_config['report_time_filter']}"
|
|
999
|
+
)
|
|
1000
|
+
report_date_filter = filter_config["report_date_filter"]
|
|
1001
|
+
report_time_filter = filter_config["report_time_filter"]
|
|
941
1002
|
|
|
942
1003
|
# Get all reports that are not already patched
|
|
943
1004
|
reports = table_api_call(
|
|
@@ -959,27 +1020,31 @@ def patch_report_filters():
|
|
|
959
1020
|
logging.info(f"Discarding report {report['title']} {report['sys_id']}...")
|
|
960
1021
|
raise NotImplementedError() # Mark for deletion
|
|
961
1022
|
|
|
962
|
-
if not
|
|
1023
|
+
if not is_report_filter_using_relative_time(report["filter"]):
|
|
963
1024
|
# That's a report we want to keep (use date cutoff filter)
|
|
964
|
-
filter_date =
|
|
1025
|
+
filter_date = report_date_filter
|
|
1026
|
+
filter_time = report_time_filter
|
|
965
1027
|
logging.info(
|
|
966
1028
|
f"Keeping report {report['title']} {report['sys_id']} (columns: {sys_created_on_cols})..."
|
|
967
1029
|
)
|
|
968
1030
|
else:
|
|
969
|
-
# XXX: We do not support reports with filters that rely on time (e.g., last 10 days) because
|
|
1031
|
+
# XXX: We do not support reports with filters that rely on relative time (e.g., last 10 days) because
|
|
970
1032
|
# there are not stable. In this case, we don't delete them but add a filter to make
|
|
971
1033
|
# them empty. They will be shown as "No data available".
|
|
972
1034
|
logging.info(
|
|
973
1035
|
f"Disabling report {report['title']} {report['sys_id']} because it uses time filters..."
|
|
974
1036
|
)
|
|
975
1037
|
filter_date = "1900-01-01"
|
|
1038
|
+
filter_time = "00:00:00"
|
|
976
1039
|
|
|
1040
|
+
# Format the filter
|
|
977
1041
|
filter = "".join(
|
|
978
1042
|
[
|
|
979
|
-
f"^{col}<javascript:gs.dateGenerate('{filter_date}','
|
|
1043
|
+
f"^{col}<javascript:gs.dateGenerate('{filter_date}','{filter_time}')"
|
|
980
1044
|
for col in sys_created_on_cols
|
|
981
1045
|
]
|
|
982
1046
|
) + ("^" if len(report["filter"]) > 0 and not report["filter"].startswith("^") else "")
|
|
1047
|
+
# Patch the report with the new filter
|
|
983
1048
|
table_api_call(
|
|
984
1049
|
instance=instance,
|
|
985
1050
|
table=f"sys_report/{report['sys_id']}",
|
|
@@ -1055,7 +1120,11 @@ def setup():
|
|
|
1055
1120
|
|
|
1056
1121
|
# Save installation date
|
|
1057
1122
|
logging.info("Saving installation date")
|
|
1058
|
-
|
|
1123
|
+
set_sys_property(
|
|
1124
|
+
instance=SNowInstance(),
|
|
1125
|
+
property_name="workarena.installation.date",
|
|
1126
|
+
value=datetime.now().isoformat(),
|
|
1127
|
+
)
|
|
1059
1128
|
|
|
1060
1129
|
logging.info("WorkArena setup complete.")
|
|
1061
1130
|
|
|
@@ -1065,10 +1134,29 @@ def main():
|
|
|
1065
1134
|
Entrypoint for package CLI installation command
|
|
1066
1135
|
|
|
1067
1136
|
"""
|
|
1137
|
+
parser = argparse.ArgumentParser(
|
|
1138
|
+
description="Install WorkArena artifacts on a ServiceNow instance."
|
|
1139
|
+
)
|
|
1140
|
+
parser.add_argument(
|
|
1141
|
+
"--instance-url", required=True, help="URL of the target ServiceNow instance."
|
|
1142
|
+
)
|
|
1143
|
+
parser.add_argument(
|
|
1144
|
+
"--instance-password",
|
|
1145
|
+
required=True,
|
|
1146
|
+
help="Password for the admin user on the target ServiceNow instance.",
|
|
1147
|
+
)
|
|
1148
|
+
args = parser.parse_args()
|
|
1149
|
+
|
|
1150
|
+
global _CLI_INSTANCE_URL, _CLI_INSTANCE_PASSWORD
|
|
1151
|
+
_CLI_INSTANCE_URL = args.instance_url
|
|
1152
|
+
_CLI_INSTANCE_PASSWORD = args.instance_password
|
|
1153
|
+
|
|
1068
1154
|
logging.basicConfig(level=logging.INFO)
|
|
1069
1155
|
|
|
1070
1156
|
try:
|
|
1071
|
-
past_install_date =
|
|
1157
|
+
past_install_date = get_sys_property(
|
|
1158
|
+
instance=SNowInstance(), property_name="workarena.installation.date"
|
|
1159
|
+
)
|
|
1072
1160
|
logging.info(f"Detected previous installation on {past_install_date}. Reinstalling...")
|
|
1073
1161
|
except:
|
|
1074
1162
|
past_install_date = "never"
|
browsergym/workarena/instance.py
CHANGED
|
@@ -1,11 +1,88 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
1
4
|
import os
|
|
5
|
+
import random
|
|
2
6
|
import requests
|
|
3
|
-
import
|
|
7
|
+
from itertools import cycle
|
|
4
8
|
|
|
9
|
+
from huggingface_hub import hf_hub_download
|
|
10
|
+
from huggingface_hub.utils import disable_progress_bars
|
|
5
11
|
from playwright.sync_api import sync_playwright
|
|
6
12
|
from typing import Optional
|
|
7
13
|
|
|
8
|
-
from .config import
|
|
14
|
+
from .config import (
|
|
15
|
+
INSTANCE_REPO_FILENAME,
|
|
16
|
+
INSTANCE_REPO_ID,
|
|
17
|
+
INSTANCE_REPO_TYPE,
|
|
18
|
+
INSTANCE_XOR_SEED,
|
|
19
|
+
REPORT_FILTER_PROPERTY,
|
|
20
|
+
SNOW_BROWSER_TIMEOUT,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Required to read the instance credentials
|
|
25
|
+
if not INSTANCE_XOR_SEED:
|
|
26
|
+
raise ValueError("INSTANCE_XOR_SEED must be configured")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _xor_cipher(data: bytes, key: bytes) -> bytes:
|
|
30
|
+
return bytes(b ^ k for b, k in zip(data, cycle(key)))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def decrypt_instance_password(encrypted_password: str) -> str:
|
|
34
|
+
"""Decrypt a base64-encoded XOR-obfuscated password using the shared key."""
|
|
35
|
+
|
|
36
|
+
cipher_bytes = base64.b64decode(encrypted_password)
|
|
37
|
+
plain_bytes = _xor_cipher(cipher_bytes, INSTANCE_XOR_SEED.encode("utf-8"))
|
|
38
|
+
return plain_bytes.decode("utf-8")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def encrypt_instance_password(password: str) -> str:
|
|
42
|
+
"""Helper to produce encrypted passwords for populating the instance file."""
|
|
43
|
+
|
|
44
|
+
cipher_bytes = _xor_cipher(password.encode("utf-8"), INSTANCE_XOR_SEED.encode("utf-8"))
|
|
45
|
+
return base64.b64encode(cipher_bytes).decode("utf-8")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def fetch_instances():
|
|
49
|
+
"""
|
|
50
|
+
Load the latest instances from either a custom pool (SNOW_INSTANCE_POOL env var) or the gated HF dataset.
|
|
51
|
+
"""
|
|
52
|
+
pool_path = os.getenv("SNOW_INSTANCE_POOL")
|
|
53
|
+
if pool_path:
|
|
54
|
+
path = os.path.expanduser(pool_path)
|
|
55
|
+
if not os.path.exists(path):
|
|
56
|
+
raise FileNotFoundError(
|
|
57
|
+
f"SNOW_INSTANCE_POOL points to '{pool_path}', but the file does not exist."
|
|
58
|
+
)
|
|
59
|
+
logging.info("Loading ServiceNow instances from custom pool: %s", path)
|
|
60
|
+
else:
|
|
61
|
+
try:
|
|
62
|
+
disable_progress_bars()
|
|
63
|
+
path = hf_hub_download(
|
|
64
|
+
repo_id=INSTANCE_REPO_ID,
|
|
65
|
+
filename=INSTANCE_REPO_FILENAME,
|
|
66
|
+
repo_type=INSTANCE_REPO_TYPE,
|
|
67
|
+
)
|
|
68
|
+
logging.info("Loaded ServiceNow instances from the default instance pool.")
|
|
69
|
+
except Exception as e:
|
|
70
|
+
raise RuntimeError(
|
|
71
|
+
f"Could not access {INSTANCE_REPO_ID}/{INSTANCE_REPO_FILENAME}. "
|
|
72
|
+
"Make sure you have been granted access to the gated repo and that you are "
|
|
73
|
+
"authenticated (run `huggingface-cli login` or set HUGGING_FACE_HUB_TOKEN)."
|
|
74
|
+
) from e
|
|
75
|
+
|
|
76
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
77
|
+
entries = json.load(f)
|
|
78
|
+
|
|
79
|
+
for entry in entries:
|
|
80
|
+
entry["url"] = entry["u"]
|
|
81
|
+
entry["password"] = decrypt_instance_password(entry["p"])
|
|
82
|
+
del entry["u"]
|
|
83
|
+
del entry["p"]
|
|
84
|
+
|
|
85
|
+
return entries
|
|
9
86
|
|
|
10
87
|
|
|
11
88
|
class SNowInstance:
|
|
@@ -25,31 +102,39 @@ class SNowInstance:
|
|
|
25
102
|
Parameters:
|
|
26
103
|
-----------
|
|
27
104
|
snow_url: str
|
|
28
|
-
The URL of a SNow instance.
|
|
105
|
+
The URL of a SNow instance. When omitted, the constructor first looks for SNOW_INSTANCE_URL and falls back
|
|
106
|
+
to a random instance from the benchmark's instance pool if the environment variable is not set.
|
|
29
107
|
snow_credentials: (str, str)
|
|
30
|
-
The username and password used to access the SNow instance.
|
|
31
|
-
|
|
108
|
+
The username and password used to access the SNow instance. When omitted, environment variables
|
|
109
|
+
SNOW_INSTANCE_UNAME/SNOW_INSTANCE_PWD are used if set; otherwise, a random instance from the benchmark's
|
|
110
|
+
instance pool is selected.
|
|
32
111
|
|
|
33
112
|
"""
|
|
34
113
|
# try to get these values from environment variables if not provided
|
|
35
|
-
if snow_url is None:
|
|
36
|
-
|
|
114
|
+
if snow_url is None or snow_credentials is None:
|
|
115
|
+
|
|
116
|
+
# Check if all required environment variables are set and if yes, fetch url and credentials from there
|
|
117
|
+
if (
|
|
118
|
+
"SNOW_INSTANCE_URL" in os.environ
|
|
119
|
+
and "SNOW_INSTANCE_UNAME" in os.environ
|
|
120
|
+
and "SNOW_INSTANCE_PWD" in os.environ
|
|
121
|
+
):
|
|
37
122
|
snow_url = os.environ["SNOW_INSTANCE_URL"]
|
|
38
|
-
else:
|
|
39
|
-
raise ValueError(
|
|
40
|
-
f"Please provide a ServiceNow instance URL (you can use the environment variable SNOW_INSTANCE_URL)"
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
if snow_credentials is None:
|
|
44
|
-
if "SNOW_INSTANCE_UNAME" in os.environ and "SNOW_INSTANCE_PWD" in os.environ:
|
|
45
123
|
snow_credentials = (
|
|
46
124
|
os.environ["SNOW_INSTANCE_UNAME"],
|
|
47
125
|
os.environ["SNOW_INSTANCE_PWD"],
|
|
48
126
|
)
|
|
127
|
+
|
|
128
|
+
# Otherwise, load all instances and select one randomly
|
|
49
129
|
else:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
130
|
+
instances = fetch_instances()
|
|
131
|
+
if not instances:
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"No instances found in the dataset {INSTANCE_REPO_ID}. Please provide instance details via parameters or environment variables."
|
|
134
|
+
)
|
|
135
|
+
instance = random.choice(instances)
|
|
136
|
+
snow_url = instance["url"]
|
|
137
|
+
snow_credentials = ("admin", instance["password"])
|
|
53
138
|
|
|
54
139
|
# remove trailing slashes in the URL, if any
|
|
55
140
|
self.snow_url = snow_url.rstrip("/")
|
|
@@ -124,3 +209,25 @@ class SNowInstance:
|
|
|
124
209
|
browser.close()
|
|
125
210
|
|
|
126
211
|
return release_info
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def report_filter_config(self) -> dict:
|
|
215
|
+
"""
|
|
216
|
+
Get the report filter configuration from the ServiceNow instance.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
--------
|
|
220
|
+
dict
|
|
221
|
+
The report filter configuration, or an empty dictionary if not found.
|
|
222
|
+
|
|
223
|
+
"""
|
|
224
|
+
from .api.system_properties import (
|
|
225
|
+
get_sys_property,
|
|
226
|
+
) # Import here to avoid circular import issues
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
config = get_sys_property(self, REPORT_FILTER_PROPERTY)
|
|
230
|
+
config = json.loads(config)
|
|
231
|
+
return config
|
|
232
|
+
except Exception:
|
|
233
|
+
return None
|
|
@@ -19,7 +19,6 @@ from ..config import (
|
|
|
19
19
|
DASHBOARD_RETRIEVAL_VALUE_CONFIG_PATH,
|
|
20
20
|
REPORT_RETRIEVAL_MINMAX_CONFIG_PATH,
|
|
21
21
|
REPORT_RETRIEVAL_VALUE_CONFIG_PATH,
|
|
22
|
-
REPORT_DATE_FILTER,
|
|
23
22
|
REPORT_PATCH_FLAG,
|
|
24
23
|
)
|
|
25
24
|
from ..instance import SNowInstance
|
|
@@ -295,9 +294,29 @@ class DashboardRetrievalTask(AbstractServiceNowTask, ABC):
|
|
|
295
294
|
""",
|
|
296
295
|
]
|
|
297
296
|
|
|
297
|
+
def _get_filter_config(self) -> str:
|
|
298
|
+
# Get report filter config
|
|
299
|
+
config = self.instance.report_filter_config
|
|
300
|
+
if config is None:
|
|
301
|
+
REPORT_DATE_FILTER = REPORT_TIME_FILTER = None
|
|
302
|
+
else:
|
|
303
|
+
REPORT_DATE_FILTER = config["report_date_filter"]
|
|
304
|
+
REPORT_TIME_FILTER = config["report_time_filter"]
|
|
305
|
+
del config
|
|
306
|
+
|
|
307
|
+
# Check that the report filters are properly setup
|
|
308
|
+
if REPORT_DATE_FILTER is None or REPORT_TIME_FILTER is None:
|
|
309
|
+
raise RuntimeError(
|
|
310
|
+
"The report date and time filters are not set. Please run the install script to set them."
|
|
311
|
+
)
|
|
312
|
+
return REPORT_DATE_FILTER, REPORT_TIME_FILTER
|
|
313
|
+
|
|
298
314
|
def setup_goal(self, page: playwright.sync_api.Page) -> Tuple[str | dict]:
|
|
299
315
|
super().setup_goal(page=page)
|
|
300
316
|
|
|
317
|
+
# Get the instance report filter config
|
|
318
|
+
REPORT_DATE_FILTER, REPORT_TIME_FILTER = self._get_filter_config()
|
|
319
|
+
|
|
301
320
|
# Configure task
|
|
302
321
|
# ... sample a configuration
|
|
303
322
|
self.config = (
|
|
@@ -305,7 +324,9 @@ class DashboardRetrievalTask(AbstractServiceNowTask, ABC):
|
|
|
305
324
|
)
|
|
306
325
|
# ... set start URL based on config
|
|
307
326
|
# ...... some of the reports have need a date filter to be applied so we do this by patching a placeholder in the URL
|
|
308
|
-
self.start_url = self.instance.snow_url + self.config["url"]
|
|
327
|
+
self.start_url = self.instance.snow_url + self.config["url"].replace(
|
|
328
|
+
"REPORT_DATE_FILTER", REPORT_DATE_FILTER
|
|
329
|
+
).replace("REPORT_TIME_FILTER", REPORT_TIME_FILTER)
|
|
309
330
|
|
|
310
331
|
# Produce goal string based on question type
|
|
311
332
|
chart_locator = (
|
|
@@ -616,6 +637,15 @@ class DashboardRetrievalTask(AbstractServiceNowTask, ABC):
|
|
|
616
637
|
The types of questions to sample from (uniformely)
|
|
617
638
|
|
|
618
639
|
"""
|
|
640
|
+
# Get the instance report filter config
|
|
641
|
+
REPORT_DATE_FILTER, REPORT_TIME_FILTER = self._get_filter_config()
|
|
642
|
+
|
|
643
|
+
# Check that the report filters are properly setup
|
|
644
|
+
if REPORT_DATE_FILTER is None or REPORT_TIME_FILTER is None:
|
|
645
|
+
raise RuntimeError(
|
|
646
|
+
"The report date and time filters are not set. Please run the install script to set them."
|
|
647
|
+
)
|
|
648
|
+
|
|
619
649
|
# Generate a bunch of reports based on valid table fields
|
|
620
650
|
ON_THE_FLY_REPORTS = []
|
|
621
651
|
for table in [
|
|
@@ -678,7 +708,7 @@ class DashboardRetrievalTask(AbstractServiceNowTask, ABC):
|
|
|
678
708
|
# On the fly generated report
|
|
679
709
|
if not report.get("sys_id", None):
|
|
680
710
|
# ... these receive a filter that is added through the URL
|
|
681
|
-
url = f"/now/nav/ui/classic/params/target/sys_report_template.do%3Fsysparm_field%3D{report['field']}%26sysparm_type%3D{report['type']}%26sysparm_table%3D{report['table']}%26sysparm_from_list%3Dtrue%26sysparm_chart_size%3Dlarge%26sysparm_manual_labor%3Dtrue%26sysparm_query=sys_created_on<javascript:gs.dateGenerate('{REPORT_DATE_FILTER}','
|
|
711
|
+
url = f"/now/nav/ui/classic/params/target/sys_report_template.do%3Fsysparm_field%3D{report['field']}%26sysparm_type%3D{report['type']}%26sysparm_table%3D{report['table']}%26sysparm_from_list%3Dtrue%26sysparm_chart_size%3Dlarge%26sysparm_manual_labor%3Dtrue%26sysparm_query=sys_created_on<javascript:gs.dateGenerate('{REPORT_DATE_FILTER}','{REPORT_TIME_FILTER}')^EQ"
|
|
682
712
|
# Report from the database
|
|
683
713
|
else:
|
|
684
714
|
url = f"/now/nav/ui/classic/params/target/sys_report_template.do%3Fjvar_report_id={report['sys_id']}"
|
|
@@ -776,7 +806,7 @@ class SingleChartMeanMedianModeRetrievalTask(
|
|
|
776
806
|
|
|
777
807
|
|
|
778
808
|
class WorkLoadBalancingMinMaxRetrievalTask(
|
|
779
|
-
|
|
809
|
+
SingleChartMinMaxRetrievalTask, CompositionalBuildingBlockTask
|
|
780
810
|
):
|
|
781
811
|
def all_configs(self):
|
|
782
812
|
return json.load(open(REPORT_RETRIEVAL_MINMAX_CONFIG_PATH, "r"))
|
|
@@ -15,7 +15,6 @@ from playwright.sync_api import sync_playwright
|
|
|
15
15
|
|
|
16
16
|
from browsergym.workarena.api.utils import table_api_call, table_column_info
|
|
17
17
|
from browsergym.workarena.config import (
|
|
18
|
-
REPORT_DATE_FILTER,
|
|
19
18
|
REPORT_PATCH_FLAG,
|
|
20
19
|
REPORT_RETRIEVAL_MINMAX_CONFIG_PATH,
|
|
21
20
|
REPORT_RETRIEVAL_VALUE_CONFIG_PATH,
|
|
@@ -44,6 +43,10 @@ class DummyDashboard(DashboardRetrievalTask):
|
|
|
44
43
|
|
|
45
44
|
|
|
46
45
|
def get_report_urls(instance):
|
|
46
|
+
# Get the instance report filter config
|
|
47
|
+
REPORT_DATE_FILTER, REPORT_TIME_FILTER = instance._get_filter_config()
|
|
48
|
+
raise NotImplementedError("TODO: Include the time filter as in dashboard.py")
|
|
49
|
+
|
|
47
50
|
# Generate a bunch of reports on the fly based on valid table fields
|
|
48
51
|
ON_THE_FLY_REPORTS = []
|
|
49
52
|
for table in [
|
|
@@ -226,7 +229,13 @@ def get_all_configs_by_url(url, is_report):
|
|
|
226
229
|
|
|
227
230
|
|
|
228
231
|
if __name__ == "__main__":
|
|
229
|
-
|
|
232
|
+
|
|
233
|
+
# XXX: Make sure to specific the exact instance on which to generate configs (and not use a random one)
|
|
234
|
+
raise NotImplementedError(
|
|
235
|
+
"Make sure to specific instance URL and credentials below, then comment this line."
|
|
236
|
+
)
|
|
237
|
+
instance = SNowInstance(snow_url=None, snow_credentials=None)
|
|
238
|
+
|
|
230
239
|
reports = get_report_urls(instance)
|
|
231
240
|
gen_func = partial(get_all_configs_by_url, is_report=REPORT)
|
|
232
241
|
|
|
@@ -6,7 +6,10 @@ NUM_CONFIGS = 650 # number of impersonation tasks in the paper
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def get_all_impersonation_users():
|
|
9
|
-
|
|
9
|
+
raise NotImplementedError(
|
|
10
|
+
"Make sure to specific instance URL and credentials below, then comment this line."
|
|
11
|
+
)
|
|
12
|
+
instance = SNowInstance(snow_url=None, snow_credentials=None)
|
|
10
13
|
candidate_users = [
|
|
11
14
|
u["first_name"] + " " + u["last_name"]
|
|
12
15
|
for u in table_api_call(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# TODO: Can we delete this file?
|
|
1
2
|
import json
|
|
2
3
|
import random
|
|
3
4
|
|
|
@@ -9,9 +10,8 @@ from tenacity import retry, stop_after_attempt
|
|
|
9
10
|
from tqdm import tqdm
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
def generate_all_kb_configs(instance
|
|
13
|
+
def generate_all_kb_configs(instance, num_configs=1000) -> list[dict]:
|
|
13
14
|
"""Generate all possible KB configs"""
|
|
14
|
-
instance = instance if instance is not None else SNowInstance()
|
|
15
15
|
with open(KB_FILEPATH, "r") as f:
|
|
16
16
|
kb_entries = json.load(f)
|
|
17
17
|
all_configs = []
|
|
@@ -33,5 +33,7 @@ def generate_all_kb_configs(instance=None, num_configs=1000) -> list[dict]:
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
if __name__ == "__main__":
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
raise NotImplementedError(
|
|
37
|
+
"Make sure to specific instance URL and credentials below, then comment this line."
|
|
38
|
+
)
|
|
39
|
+
generate_all_kb_configs(instance=SNowInstance(snow_url=None))
|