browsergym-workarena 0.1.0rc6__py3-none-any.whl → 0.2.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.
Files changed (43) hide show
  1. browsergym/workarena/__init__.py +7 -2
  2. browsergym/workarena/api/ui_themes.py +35 -0
  3. browsergym/workarena/api/user.py +153 -0
  4. browsergym/workarena/api/utils.py +1 -1
  5. browsergym/workarena/config.py +43 -1
  6. browsergym/workarena/data_files/setup_files/lists/expected_asset_list_columns.json +34 -1
  7. browsergym/workarena/data_files/setup_files/lists/expected_change_request_list_columns.json +48 -1
  8. browsergym/workarena/data_files/setup_files/lists/expected_hardware_list_columns.json +53 -1
  9. browsergym/workarena/data_files/setup_files/lists/expected_incident_list_columns.json +28 -1
  10. browsergym/workarena/data_files/setup_files/lists/expected_service_catalog_list_columns.json +29 -1
  11. browsergym/workarena/data_files/setup_files/ui_themes/workarena_themes.xml +2313 -0
  12. browsergym/workarena/data_files/task_configs/dashboard_retrieval_minmax_task.json +1 -0
  13. browsergym/workarena/data_files/task_configs/dashboard_retrieval_value_task.json +1 -0
  14. browsergym/workarena/data_files/task_configs/report_retrieval_minmax_task.json +1 -0
  15. browsergym/workarena/data_files/task_configs/report_retrieval_value_task.json +1 -0
  16. browsergym/workarena/data_files/task_configs/sort_asset_list_task.json +547 -11391
  17. browsergym/workarena/data_files/task_configs/sort_change_request_list_task.json +558 -11090
  18. browsergym/workarena/data_files/task_configs/sort_hardware_list_task.json +576 -11162
  19. browsergym/workarena/data_files/task_configs/sort_incident_list_task.json +528 -11172
  20. browsergym/workarena/data_files/task_configs/sort_service_catalog_item_list_task.json +533 -11491
  21. browsergym/workarena/data_files/task_configs/sort_user_list_task.json +568 -10582
  22. browsergym/workarena/install.py +625 -153
  23. browsergym/workarena/tasks/base.py +85 -26
  24. browsergym/workarena/tasks/dashboard.py +620 -0
  25. browsergym/workarena/tasks/form.py +127 -90
  26. browsergym/workarena/tasks/knowledge.py +30 -14
  27. browsergym/workarena/tasks/list.py +157 -65
  28. browsergym/workarena/tasks/navigation.py +18 -16
  29. browsergym/workarena/tasks/scripts/generate_dashboard_configs.py +272 -0
  30. browsergym/workarena/tasks/scripts/generate_forms.py +2 -2
  31. browsergym/workarena/tasks/scripts/list.py +33 -9
  32. browsergym/workarena/tasks/scripts/validate.py +2 -2
  33. browsergym/workarena/tasks/service_catalog.py +106 -74
  34. browsergym/workarena/tasks/utils/form.py +5 -3
  35. browsergym/workarena/tasks/utils/js_utils.js +123 -2
  36. browsergym/workarena/tasks/utils/string.py +15 -0
  37. browsergym/workarena/tasks/utils/utils.py +20 -0
  38. browsergym/workarena/utils.py +31 -2
  39. {browsergym_workarena-0.1.0rc6.dist-info → browsergym_workarena-0.2.0.dist-info}/METADATA +7 -3
  40. {browsergym_workarena-0.1.0rc6.dist-info → browsergym_workarena-0.2.0.dist-info}/RECORD +43 -32
  41. {browsergym_workarena-0.1.0rc6.dist-info → browsergym_workarena-0.2.0.dist-info}/WHEEL +1 -1
  42. {browsergym_workarena-0.1.0rc6.dist-info → browsergym_workarena-0.2.0.dist-info}/entry_points.txt +0 -0
  43. {browsergym_workarena-0.1.0rc6.dist-info → browsergym_workarena-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -3,15 +3,16 @@ Tasks related to lists
3
3
 
4
4
  """
5
5
 
6
+ import itertools
6
7
  import json
7
8
  import logging
8
9
  import playwright.sync_api
9
10
  import re
10
- import urllib.parse
11
11
 
12
12
  from playwright.sync_api import Page
13
13
  from tenacity import retry, retry_if_exception_type, stop_after_delay
14
- from typing import Tuple
14
+ from typing import List, Tuple
15
+ from urllib import parse
15
16
  from warnings import warn
16
17
 
17
18
  from ..api.utils import table_api_call, table_column_info
@@ -39,27 +40,28 @@ from ..config import (
39
40
  )
40
41
  from .base import AbstractServiceNowTask
41
42
  from .utils.form import fill_text
43
+ from .utils.utils import check_url_suffix_match
42
44
 
43
45
 
44
46
  LISTS = {
45
47
  "alm_asset": {
46
- "url": "/now/nav/ui/classic/params/target/alm_asset_list.do%3Fsysparm_view%3Ditam_workspace%26sysparm_userpref.alm_asset_list.view%3Ditam_workspace%26sysparm_userpref.alm_asset.view%3Ditam_workspace%26sysparm_query%3D%26sysparm_fixed_query%3D",
48
+ "url": "/now/nav/ui/classic/params/target/alm_asset_list.do",
47
49
  "forbidden_fields": ["sys_class_name"],
48
50
  },
49
51
  "alm_hardware": {
50
- "url": "/now/nav/ui/classic/params/target/alm_hardware_list.do%3Fsysparm_view%3Ditam_workspace%26sysparm_userpref.alm_hardware_list.view%3Ditam_workspace%26sysparm_userpref.alm_hardware.view%3Ditam_workspace%3D%26sysparm_query%3Dinstall_status%253D6%255Esubstatus%253Dpre_allocated",
52
+ "url": "/now/nav/ui/classic/params/target/alm_hardware_list.do",
51
53
  "forbidden_fields": [],
52
54
  },
53
55
  "change_request": {
54
- "url": "/now/nav/ui/classic/params/target/change_request_list.do%3Fsysparm_view%3Dsow%26sysparm_userpref.change_request_list.view%3Dsow%26sysparm_userpref.change_request.view%3Dsow%26sysparm_query%3D%26sysparm_fixed_query%3D",
56
+ "url": "/now/nav/ui/classic/params/target/change_request_list.do",
55
57
  "forbidden_fields": [],
56
58
  },
57
59
  "incident": {
58
- "url": "/now/nav/ui/classic/params/target/incident_list.do%3Fsysparm_query%3Dactive%253Dtrue%26sysparm_first_row%3D1%26sysparm_view%3DMajor%2520Incidents",
60
+ "url": "/now/nav/ui/classic/params/target/incident_list.do",
59
61
  "forbidden_fields": [],
60
62
  },
61
63
  "sys_user": {
62
- "url": "/now/nav/ui/classic/params/target/sys_user_list.do%3Fsysparm_view%3D%26sysparm_userpref.sys_user_list.view%3D%26sysparm_userpref.sys_user.view%3D%26sysparm_query%3Dactive%253Dtrue%255Ecompany%253D81fd65ecac1d55eb42a426568fc87a63",
64
+ "url": "/now/nav/ui/classic/params/target/sys_user_list.do",
63
65
  "forbidden_fields": [
64
66
  "sys_class_name",
65
67
  "roles",
@@ -69,13 +71,16 @@ LISTS = {
69
71
  ],
70
72
  },
71
73
  "sc_cat_item": {
72
- "url": "/now/nav/ui/classic/params/target/sc_cat_item_list.do%3Fsysparm_view%3D%26sysparm_userpref.sc_cat_item_list.view%3D%26sysparm_userpref.sc_cat_item.view%3D%26sysparm_query%3D%26sysparm_fixed_query%3D",
74
+ "url": "/now/nav/ui/classic/params/target/sc_cat_item_list.do",
73
75
  "forbidden_fields": ["roles", "sc_catalogs"],
74
76
  },
75
77
  }
76
78
 
77
79
 
78
80
  class ServiceNowListTask(AbstractServiceNowTask):
81
+ def get_init_scripts(self) -> List[str]:
82
+ return super().get_init_scripts() + ["registerGsftMainLoaded();"]
83
+
79
84
  def _get_visible_list(self, page: Page):
80
85
  self._wait_for_ready(page)
81
86
 
@@ -163,16 +168,6 @@ class ServiceNowListTask(AbstractServiceNowTask):
163
168
  page.wait_for_function("window.gsft_main.GlideList2 !== undefined")
164
169
  logging.debug("Detected Glide list API ready")
165
170
 
166
- def pre_setup(self, seed: int, page: Page):
167
- super().pre_setup(seed, page)
168
-
169
- self._add_init_scripts_to_context_and_reload(
170
- page,
171
- [
172
- "registerGsftMainLoaded();",
173
- ],
174
- )
175
-
176
171
 
177
172
  class SortListTask(ServiceNowListTask):
178
173
  """
@@ -180,6 +175,8 @@ class SortListTask(ServiceNowListTask):
180
175
 
181
176
  Parameters:
182
177
  -----------
178
+ seed: int
179
+ Random seed
183
180
  instance: SNowInstance
184
181
  The instance to use.
185
182
  list_url: str
@@ -200,6 +197,7 @@ class SortListTask(ServiceNowListTask):
200
197
 
201
198
  def __init__(
202
199
  self,
200
+ seed: int,
203
201
  instance=None,
204
202
  list_url="",
205
203
  forbidden_fields=[],
@@ -207,7 +205,7 @@ class SortListTask(ServiceNowListTask):
207
205
  config_path: str = None,
208
206
  expected_fields_path: str = None,
209
207
  ) -> None:
210
- super().__init__(instance=instance, start_rel_url=list_url)
208
+ super().__init__(seed=seed, instance=instance, start_rel_url=list_url)
211
209
  self.min_sort_len = 1
212
210
  self.max_sort_len = 3
213
211
  self.forbidden_fields = forbidden_fields
@@ -218,29 +216,71 @@ class SortListTask(ServiceNowListTask):
218
216
  with open(expected_fields_path, "r") as f:
219
217
  self.expected_fields = set(json.load(f))
220
218
 
221
- def setup(self, seed: int, page: Page) -> tuple[str, dict]:
222
- self.pre_setup(seed, page)
223
- self._wait_for_ready(page)
224
- # Extract the list from the page
225
- self.list_info = self._extract_list_info(page)
226
- visible_columns = set(self.list_info["fields"].split(","))
227
- config = self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
219
+ def setup_goal(self, page: Page) -> tuple[str, dict]:
220
+ super().setup_goal(page=page)
228
221
 
229
- config = self.random.choice(self.all_configs)
222
+ # Get the task configuration
223
+ config = self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
230
224
  self.sort_fields = config["sort_fields"]
231
225
  self.sort_dirs = config["sort_dirs"]
226
+
227
+ # Get the task goal
232
228
  goal = config["goal"]
233
- # Ensure that the fields that need to be sorted are visible
229
+ info = {}
230
+
231
+ return goal, info
232
+
233
+ def start(self, page: Page) -> None:
234
+ super().start(page)
235
+ self._wait_for_ready(page)
236
+
237
+ # Ensure that the fields that need to be sorted are visible (task feasibility check)
238
+ self.list_info = self._extract_list_info(page)
239
+ visible_columns = set(self.list_info["fields"].split(","))
234
240
  assert (
235
241
  set(self.sort_fields) <= visible_columns and visible_columns == self.expected_fields
236
242
  ), f"Fields {self.sort_fields} are not all visible in the list. Re-run workarena-install to correct this."
237
243
 
238
- info = {}
244
+ def _generate_all_configs(self, seed: int, page: Page, n_fields_to_sort: int):
245
+ self.setup(seed=seed, page=page)
246
+ self._wait_for_ready(page)
247
+ list_info = self._extract_list_info(page)
239
248
 
240
- return goal, info
249
+ # Get available fields
250
+ available_fields = list(list_info["columns"].keys())
251
+ # ... remove forbidden fields
252
+ available_fields = [f for f in available_fields if f not in self.forbidden_fields]
241
253
 
242
- def _generate_random_config(self, seed: int, page: Page):
243
- self.pre_setup(seed, page)
254
+ field_txt = {k: x["label"] for k, x in list_info["columns"].items()}
255
+ dir_txt = {"asc": "ascending", "desc": "descending"}
256
+
257
+ # compute all field combinations
258
+ all_sort_fields = list(itertools.combinations(available_fields, n_fields_to_sort))
259
+ # compute all direction combinations
260
+ all_sort_dirs = list(itertools.product(*[["asc", "desc"] for _ in range(n_fields_to_sort)]))
261
+
262
+ # product of field combinations x direction combinations
263
+ all_configs = list(itertools.product(all_sort_fields, all_sort_dirs))
264
+
265
+ all_configs = [
266
+ {
267
+ "sort_fields": sort_fields,
268
+ "sort_dirs": sort_dirs,
269
+ "goal": f'Sort the "{list_info["title"]}" list by the following fields:\n'
270
+ + "\n".join(
271
+ [
272
+ f" - {field_txt[field]} ({dir_txt[dir]})"
273
+ for field, dir in zip(sort_fields, sort_dirs)
274
+ ]
275
+ ),
276
+ }
277
+ for sort_fields, sort_dirs in all_configs
278
+ ]
279
+
280
+ return all_configs
281
+
282
+ def _generate_random_config(self, page: Page):
283
+ self.setup(page=page)
244
284
  self._wait_for_ready(page)
245
285
  self.list_info = self._extract_list_info(page)
246
286
  # Get available fields
@@ -287,7 +327,7 @@ class SortListTask(ServiceNowListTask):
287
327
  return goal, info
288
328
 
289
329
  def cheat(self, page: Page, chat_messages: list[str]) -> None:
290
- super().cheat(page, chat_messages)
330
+ super().cheat(page=page, chat_messages=chat_messages)
291
331
  self._wait_for_ready(page)
292
332
 
293
333
  iframe, _, _ = self._get_visible_list(page)
@@ -349,8 +389,19 @@ class SortListTask(ServiceNowListTask):
349
389
  def validate(
350
390
  self, page: playwright.sync_api.Page, chat_messages: list[str]
351
391
  ) -> Tuple[float, bool, str, dict]:
392
+ right_url = check_url_suffix_match(
393
+ page, expected_url=self.start_url[: self.start_url.find("%3F")], task=self
394
+ )
395
+ if not right_url:
396
+ return (
397
+ 0,
398
+ False,
399
+ "",
400
+ {
401
+ "message": f"The page is not in the right URL to validate task {self.__class__.__name__}."
402
+ },
403
+ )
352
404
  self._wait_for_ready(page)
353
-
354
405
  if len(self.sort_fields) == 1:
355
406
  # XXX: Treat this as a separate case because the user may have sorted by clicking
356
407
  # on the column header. In that case, the URL will not contain the ORDERBY.
@@ -371,9 +422,9 @@ class SortListTask(ServiceNowListTask):
371
422
  else:
372
423
  # pre-process the URL
373
424
  page_url = page.evaluate("() => window.location.href")
374
- page_url = urllib.parse.unquote(page_url)
375
- page_query = urllib.parse.urlparse(page_url).query
376
- page_qs = urllib.parse.parse_qs(page_query)
425
+ page_url = parse.unquote(page_url)
426
+ page_query = parse.urlparse(page_url).query
427
+ page_qs = parse.parse_qs(page_query)
377
428
 
378
429
  # make sure "sysparm_query" is present
379
430
  if "sysparm_query" not in page_qs:
@@ -426,6 +477,7 @@ class FilterListTask(ServiceNowListTask):
426
477
 
427
478
  def __init__(
428
479
  self,
480
+ seed: int,
429
481
  instance=None,
430
482
  list_url="",
431
483
  fixed_config: dict = None,
@@ -434,7 +486,7 @@ class FilterListTask(ServiceNowListTask):
434
486
  ) -> None:
435
487
  self.min_filter_len = 2
436
488
  self.max_filter_len = 5
437
- super().__init__(instance=instance, start_rel_url=list_url)
489
+ super().__init__(seed=seed, instance=instance, start_rel_url=list_url)
438
490
  self.fixed_config = fixed_config
439
491
  if config_path:
440
492
  with open(config_path, "r") as f:
@@ -442,23 +494,18 @@ class FilterListTask(ServiceNowListTask):
442
494
  with open(expected_fields_path, "r") as f:
443
495
  self.expected_fields = set(json.load(f))
444
496
 
445
- def setup(self, seed: int, page: Page) -> tuple[str, dict]:
446
- self.pre_setup(seed, page)
447
- self._wait_for_ready(page)
448
- config = self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
497
+ def setup_goal(self, page: Page) -> tuple[str, dict]:
498
+ super().setup_goal(page=page)
449
499
 
500
+ # Get the task configuration
501
+ config = self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
450
502
  self.filter_columns = config["filter_columns"]
451
503
  self.filter_values = config["filter_values"]
452
504
  self.filter_kind = config["filter_kind"]
453
505
  self.list_info = config["list_info"]
454
506
  self.filter_len = len(self.filter_columns)
455
- visible_list_info = self._extract_list_info(page)
456
- visible_columns = set(visible_list_info["fields"].split(","))
457
- # Assert that required fields are visible
458
- assert (
459
- set(self.filter_columns) <= visible_columns and visible_columns == self.expected_fields
460
- ), f"Fields {self.filter_columns} are not all visible in the list. Re-run workarena-install to correct this."
461
- # generate goal
507
+
508
+ # Generate goal
462
509
  goal = (
463
510
  f"Create a filter for the list to extract all entries where "
464
511
  + f" {'and' if self.filter_kind == 'AND' else 'or'} ".join(
@@ -473,8 +520,20 @@ class FilterListTask(ServiceNowListTask):
473
520
 
474
521
  return goal, info
475
522
 
476
- def _generate_random_config(self, seed: int, page: Page):
477
- self.pre_setup(seed, page)
523
+ def start(self, page: Page) -> None:
524
+ super().start(page)
525
+
526
+ self._wait_for_ready(page)
527
+
528
+ # Assert that required fields are visible (task feasibility check)
529
+ visible_list_info = self._extract_list_info(page)
530
+ visible_columns = set(visible_list_info["fields"].split(","))
531
+ assert (
532
+ set(self.filter_columns) <= visible_columns and visible_columns == self.expected_fields
533
+ ), f"Fields {self.filter_columns} are not all visible in the list. Re-run workarena-install to correct this."
534
+
535
+ def _generate_random_config(self, page: Page):
536
+ self.setup(page=page)
478
537
  self._wait_for_ready(page)
479
538
 
480
539
  # Extract the list from the page
@@ -556,7 +615,7 @@ class FilterListTask(ServiceNowListTask):
556
615
  return goal, {}
557
616
 
558
617
  def cheat(self, page: Page, chat_messages: list[str]) -> None:
559
- super().cheat(page, chat_messages)
618
+ super().cheat(page=page, chat_messages=chat_messages)
560
619
  self._wait_for_ready(page)
561
620
 
562
621
  iframe, _, _ = self._get_visible_list(page)
@@ -648,8 +707,17 @@ class FilterListTask(ServiceNowListTask):
648
707
  Note: current implementation is limited to AND and OR filters (single type per filter) with equality operators
649
708
 
650
709
  """
710
+ right_url = check_url_suffix_match(page, expected_url=self.start_url, task=self)
711
+ if not right_url:
712
+ return (
713
+ 0,
714
+ False,
715
+ "",
716
+ {
717
+ "message": f"The page is not in the right URL to validate task {self.__class__.__name__}."
718
+ },
719
+ )
651
720
  self._wait_for_ready(page)
652
-
653
721
  if self.filter_kind not in ["AND", "OR"]:
654
722
  raise NotImplementedError("Only AND and OR filters are supported.")
655
723
  # Excludes AND because that's the default and its sep is ^ which matches everywhere
@@ -729,11 +797,13 @@ class FilterListTask(ServiceNowListTask):
729
797
  class FilterAssetListTask(FilterListTask):
730
798
  def __init__(
731
799
  self,
800
+ seed: int,
732
801
  instance=None,
733
802
  fixed_config: dict = None,
734
803
  ) -> None:
735
804
  super().__init__(
736
- instance,
805
+ seed=seed,
806
+ instance=instance,
737
807
  list_url=LISTS["alm_asset"]["url"],
738
808
  fixed_config=fixed_config,
739
809
  config_path=FILTER_ASSET_LIST_CONFIG_PATH,
@@ -744,11 +814,13 @@ class FilterAssetListTask(FilterListTask):
744
814
  class FilterChangeRequestListTask(FilterListTask):
745
815
  def __init__(
746
816
  self,
817
+ seed: int,
747
818
  instance=None,
748
819
  fixed_config: dict = None,
749
820
  ) -> None:
750
821
  super().__init__(
751
- instance,
822
+ seed=seed,
823
+ instance=instance,
752
824
  list_url=LISTS["change_request"]["url"],
753
825
  fixed_config=fixed_config,
754
826
  config_path=FILTER_CHANGE_REQUEST_LIST_CONFIG_PATH,
@@ -759,11 +831,13 @@ class FilterChangeRequestListTask(FilterListTask):
759
831
  class FilterHardwareListTask(FilterListTask):
760
832
  def __init__(
761
833
  self,
834
+ seed: int,
762
835
  instance=None,
763
836
  fixed_config: dict = None,
764
837
  ) -> None:
765
838
  super().__init__(
766
- instance,
839
+ seed=seed,
840
+ instance=instance,
767
841
  list_url=LISTS["alm_hardware"]["url"],
768
842
  fixed_config=fixed_config,
769
843
  config_path=FILTER_HARDWARE_LIST_CONFIG_PATH,
@@ -774,11 +848,13 @@ class FilterHardwareListTask(FilterListTask):
774
848
  class FilterIncidentListTask(FilterListTask):
775
849
  def __init__(
776
850
  self,
851
+ seed: int,
777
852
  instance=None,
778
853
  fixed_config: dict = None,
779
854
  ) -> None:
780
855
  super().__init__(
781
- instance,
856
+ seed=seed,
857
+ instance=instance,
782
858
  list_url=LISTS["incident"]["url"],
783
859
  fixed_config=fixed_config,
784
860
  config_path=FILTER_INCIDENT_LIST_CONFIG_PATH,
@@ -789,11 +865,13 @@ class FilterIncidentListTask(FilterListTask):
789
865
  class FilterServiceCatalogItemListTask(FilterListTask):
790
866
  def __init__(
791
867
  self,
868
+ seed: int,
792
869
  instance=None,
793
870
  fixed_config: dict = None,
794
871
  ) -> None:
795
872
  super().__init__(
796
- instance,
873
+ seed=seed,
874
+ instance=instance,
797
875
  list_url=LISTS["sc_cat_item"]["url"],
798
876
  fixed_config=fixed_config,
799
877
  config_path=FILTER_SERVICE_CATALOG_ITEM_LIST_CONFIG_PATH,
@@ -804,11 +882,13 @@ class FilterServiceCatalogItemListTask(FilterListTask):
804
882
  class FilterUserListTask(FilterListTask):
805
883
  def __init__(
806
884
  self,
885
+ seed: int,
807
886
  instance=None,
808
887
  fixed_config: dict = None,
809
888
  ) -> None:
810
889
  super().__init__(
811
- instance,
890
+ seed=seed,
891
+ instance=instance,
812
892
  list_url=LISTS["sys_user"]["url"],
813
893
  fixed_config=fixed_config,
814
894
  config_path=FILTER_USER_LIST_CONFIG_PATH,
@@ -819,11 +899,13 @@ class FilterUserListTask(FilterListTask):
819
899
  class SortAssetListTask(SortListTask):
820
900
  def __init__(
821
901
  self,
902
+ seed: int,
822
903
  instance=None,
823
904
  fixed_config: dict = None,
824
905
  ) -> None:
825
906
  super().__init__(
826
- instance,
907
+ seed=seed,
908
+ instance=instance,
827
909
  list_url=LISTS["alm_asset"]["url"],
828
910
  forbidden_fields=LISTS["alm_asset"]["forbidden_fields"],
829
911
  fixed_config=fixed_config,
@@ -835,11 +917,13 @@ class SortAssetListTask(SortListTask):
835
917
  class SortChangeRequestListTask(SortListTask):
836
918
  def __init__(
837
919
  self,
920
+ seed: int,
838
921
  instance=None,
839
922
  fixed_config: dict = None,
840
923
  ) -> None:
841
924
  super().__init__(
842
- instance,
925
+ seed=seed,
926
+ instance=instance,
843
927
  list_url=LISTS["change_request"]["url"],
844
928
  forbidden_fields=LISTS["change_request"]["forbidden_fields"],
845
929
  fixed_config=fixed_config,
@@ -851,11 +935,13 @@ class SortChangeRequestListTask(SortListTask):
851
935
  class SortHardwareListTask(SortListTask):
852
936
  def __init__(
853
937
  self,
938
+ seed: int,
854
939
  instance=None,
855
940
  fixed_config: dict = None,
856
941
  ) -> None:
857
942
  super().__init__(
858
- instance,
943
+ seed=seed,
944
+ instance=instance,
859
945
  list_url=LISTS["alm_hardware"]["url"],
860
946
  forbidden_fields=LISTS["alm_hardware"]["forbidden_fields"],
861
947
  fixed_config=fixed_config,
@@ -867,11 +953,13 @@ class SortHardwareListTask(SortListTask):
867
953
  class SortIncidentListTask(SortListTask):
868
954
  def __init__(
869
955
  self,
956
+ seed: int,
870
957
  instance=None,
871
958
  fixed_config: dict = None,
872
959
  ) -> None:
873
960
  super().__init__(
874
- instance,
961
+ seed=seed,
962
+ instance=instance,
875
963
  list_url=LISTS["incident"]["url"],
876
964
  forbidden_fields=LISTS["incident"]["forbidden_fields"],
877
965
  fixed_config=fixed_config,
@@ -883,11 +971,13 @@ class SortIncidentListTask(SortListTask):
883
971
  class SortServiceCatalogItemListTask(SortListTask):
884
972
  def __init__(
885
973
  self,
974
+ seed: int,
886
975
  instance=None,
887
976
  fixed_config: dict = None,
888
977
  ) -> None:
889
978
  super().__init__(
890
- instance,
979
+ seed=seed,
980
+ instance=instance,
891
981
  list_url=LISTS["sc_cat_item"]["url"],
892
982
  forbidden_fields=LISTS["sc_cat_item"]["forbidden_fields"],
893
983
  fixed_config=fixed_config,
@@ -899,11 +989,13 @@ class SortServiceCatalogItemListTask(SortListTask):
899
989
  class SortUserListTask(SortListTask):
900
990
  def __init__(
901
991
  self,
992
+ seed: int,
902
993
  instance=None,
903
994
  fixed_config: dict = None,
904
995
  ) -> None:
905
996
  super().__init__(
906
- instance,
997
+ seed=seed,
998
+ instance=instance,
907
999
  list_url=LISTS["sys_user"]["url"],
908
1000
  forbidden_fields=LISTS["sys_user"]["forbidden_fields"],
909
1001
  fixed_config=fixed_config,
@@ -34,27 +34,29 @@ class AllMenuTask(AbstractServiceNowTask):
34
34
 
35
35
  """
36
36
 
37
- def __init__(self, instance: SNowInstance = None, fixed_config: dict = None) -> None:
38
- super().__init__(instance=instance, start_rel_url="/now/nav/ui/home")
37
+ def __init__(self, seed: int, instance: SNowInstance = None, fixed_config: dict = None) -> None:
38
+ super().__init__(seed=seed, instance=instance, start_rel_url="/now/nav/ui/home")
39
39
  self.fixed_config = fixed_config
40
40
  with open(ALL_MENU_PATH, "r") as f:
41
41
  self.all_configs = json.load(f)
42
42
 
43
- def setup(self, page: Page, seed: int = None) -> tuple[str, dict]:
44
- self.pre_setup(seed, page)
43
+ def setup_goal(self, page: Page) -> tuple[str, dict]:
44
+ super().setup_goal(page=page)
45
+
46
+ # Get task configuration
45
47
  self.module = (
46
48
  self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
47
49
  )
48
50
  self.final_url = self.instance.snow_url + self.module["url"]
49
51
 
50
- # generate goal
52
+ # Generate goal
51
53
  goal = f'Navigate to the "{self.module["module"]}" module of the "{self.module["application"]}" application.'
52
54
  info = {}
53
55
 
54
56
  return goal, info
55
57
 
56
58
  def cheat(self, page: Page, chat_messages: list[str]) -> None:
57
- super().cheat(page, chat_messages)
59
+ super().cheat(page=page, chat_messages=chat_messages)
58
60
 
59
61
  menu_button = page.locator('div[aria-label="All"]')
60
62
  if menu_button.get_attribute("aria-expanded").lower() != "true":
@@ -74,7 +76,7 @@ class AllMenuTask(AbstractServiceNowTask):
74
76
  path = [m.strip() for m in self.module["module"].split(">")]
75
77
  # Navigate to the application's location in the menu and select its parent
76
78
  locator = menu.get_by_label(self.module["application"], exact=True).and_(
77
- menu.get_by_role("menuitem")
79
+ menu.get_by_role("button")
78
80
  )
79
81
  locator = locator.locator("xpath=ancestor::div[contains(@class, 'snf-collapsible-list')]")
80
82
  for module in path[:-1]:
@@ -106,6 +108,7 @@ class AllMenuTask(AbstractServiceNowTask):
106
108
  def validate(
107
109
  self, page: playwright.sync_api.Page, chat_messages: list[str]
108
110
  ) -> Tuple[float, bool, str, dict]:
111
+ page.wait_for_load_state("domcontentloaded")
109
112
 
110
113
  # Get the current URL and the final URL
111
114
  current_url = urlunparse(urlparse(unquote(page.evaluate("() => window.location.href"))))
@@ -136,35 +139,34 @@ class ImpersonationTask(AbstractServiceNowTask):
136
139
 
137
140
  """
138
141
 
139
- def __init__(self, instance=None, fixed_config: dict = None) -> None:
140
- super().__init__(instance=instance, start_rel_url="/now/nav/ui/home")
142
+ def __init__(self, seed: int, instance=None, fixed_config: dict = None) -> None:
143
+ super().__init__(seed=seed, instance=instance, start_rel_url="/now/nav/ui/home")
141
144
  self.fixed_config = fixed_config
142
145
  with open(IMPERSONATION_CONFIG_PATH, "r") as f:
143
146
  self.all_configs = json.load(f)
144
147
 
145
- def setup(self, page: Page, seed: int = None) -> tuple[str, dict]:
146
- self.pre_setup(seed, page)
147
- # Retrieve the list of users from the instance
148
- # XXX: We exclude the admin to avoid problems with validation (task would always be valid by default)
148
+ def setup_goal(self, page: Page) -> tuple[str, dict]:
149
+ super().setup_goal(page=page)
150
+
151
+ # Get task configuration
149
152
  self.user_full_name = (
150
153
  self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
151
154
  )
152
155
  assert self.user_full_name in self.all_configs
153
156
 
154
- # generate goal
157
+ # Generate goal
155
158
  goal = f"Impersonate the user {self.user_full_name}."
156
159
  info = {}
157
160
 
158
161
  return goal, info
159
162
 
160
163
  def cheat(self, page: Page, chat_messages: list[str]) -> None:
161
- super().cheat(page, chat_messages)
164
+ super().cheat(page=page, chat_messages=chat_messages)
162
165
  impersonate_user(self.user_full_name, page)
163
166
 
164
167
  def validate(
165
168
  self, page: playwright.sync_api.Page, chat_messages: list[str]
166
169
  ) -> Tuple[float, bool, str, dict]:
167
-
168
170
  user_info = self.page.evaluate("window.NOW")["user"]
169
171
 
170
172
  # If the current user is not being impersonated, fail.