browsergym-workarena 0.2.0__py3-none-any.whl → 0.3.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 (91) hide show
  1. browsergym/workarena/__init__.py +13 -1
  2. browsergym/workarena/api/category.py +74 -0
  3. browsergym/workarena/api/change_request.py +87 -0
  4. browsergym/workarena/api/computer_asset.py +90 -0
  5. browsergym/workarena/api/cost_center.py +19 -0
  6. browsergym/workarena/api/expense_line.py +89 -0
  7. browsergym/workarena/api/incident.py +45 -0
  8. browsergym/workarena/api/knowledge.py +29 -0
  9. browsergym/workarena/api/problem.py +90 -0
  10. browsergym/workarena/api/report.py +183 -0
  11. browsergym/workarena/api/requested_items.py +63 -0
  12. browsergym/workarena/api/user.py +11 -8
  13. browsergym/workarena/api/utils.py +47 -3
  14. browsergym/workarena/config.py +21 -1
  15. browsergym/workarena/data_files/setup_files/forms/expected_incident_form_fields.json +1 -1
  16. browsergym/workarena/data_files/setup_files/forms/expected_request_item_form_fields.json +1 -0
  17. browsergym/workarena/data_files/setup_files/knowledge/protocols.json +46 -0
  18. browsergym/workarena/data_files/setup_files/knowledge/test.html +1 -0
  19. browsergym/workarena/data_files/setup_files/lists/expected_asset_list_columns.json +2 -24
  20. browsergym/workarena/data_files/setup_files/lists/expected_change_request_list_columns.json +4 -40
  21. browsergym/workarena/data_files/setup_files/lists/expected_expense_line_list_columns.json +12 -0
  22. browsergym/workarena/data_files/setup_files/lists/expected_hardware_list_columns.json +1 -42
  23. browsergym/workarena/data_files/setup_files/lists/expected_incident_list_columns.json +2 -18
  24. browsergym/workarena/data_files/setup_files/lists/expected_problem_list_columns.json +12 -0
  25. browsergym/workarena/data_files/setup_files/lists/expected_requested_items_list_columns.json +12 -0
  26. browsergym/workarena/data_files/setup_files/lists/expected_service_catalog_list_columns.json +2 -19
  27. browsergym/workarena/data_files/setup_files/lists/expected_user_list_columns.json +3 -50
  28. browsergym/workarena/data_files/task_configs/all_menu.json +95 -95
  29. browsergym/workarena/data_files/task_configs/dashboard_retrieval_minmax_task.json +1 -1
  30. browsergym/workarena/data_files/task_configs/dashboard_retrieval_value_task.json +1 -1
  31. browsergym/workarena/data_files/task_configs/filter_service_catalog_item_list_task.json +7986 -7982
  32. browsergym/workarena/data_files/task_configs/impersonation_users.json +3 -3
  33. browsergym/workarena/data_files/task_configs/report_retrieval_minmax_task.json +1 -1
  34. browsergym/workarena/data_files/task_configs/report_retrieval_value_task.json +1 -1
  35. browsergym/workarena/human_eval/console.js +176 -0
  36. browsergym/workarena/human_eval/tool.py +366 -0
  37. browsergym/workarena/install.py +81 -20
  38. browsergym/workarena/tasks/base.py +55 -20
  39. browsergym/workarena/tasks/comp_building_block.py +4 -0
  40. browsergym/workarena/tasks/compositional/__init__.py +76 -0
  41. browsergym/workarena/tasks/compositional/base.py +364 -0
  42. browsergym/workarena/tasks/compositional/dash_do_base.py +1366 -0
  43. browsergym/workarena/tasks/compositional/dash_do_catalog.py +1127 -0
  44. browsergym/workarena/tasks/compositional/dash_do_catalog_infeasible.py +2047 -0
  45. browsergym/workarena/tasks/compositional/dash_do_create_incident.py +403 -0
  46. browsergym/workarena/tasks/compositional/dash_do_create_incident_infeasible.py +278 -0
  47. browsergym/workarena/tasks/compositional/dash_do_create_problem.py +336 -0
  48. browsergym/workarena/tasks/compositional/dash_do_create_problem_infeasible.py +235 -0
  49. browsergym/workarena/tasks/compositional/dash_do_filter.py +1600 -0
  50. browsergym/workarena/tasks/compositional/dash_do_request_item.py +1315 -0
  51. browsergym/workarena/tasks/compositional/dash_do_request_item_infeasible.py +693 -0
  52. browsergym/workarena/tasks/compositional/delete_record.py +341 -0
  53. browsergym/workarena/tasks/compositional/edit_knowledge_base.py +457 -0
  54. browsergym/workarena/tasks/compositional/expense_management.py +598 -0
  55. browsergym/workarena/tasks/compositional/filter_and_do.py +139 -0
  56. browsergym/workarena/tasks/compositional/find_and_order_item.py +345 -0
  57. browsergym/workarena/tasks/compositional/manage_change_request_schedule.py +1417 -0
  58. browsergym/workarena/tasks/compositional/mark_duplicate_problems.py +499 -0
  59. browsergym/workarena/tasks/compositional/maximize_investment_return.py +1763 -0
  60. browsergym/workarena/tasks/compositional/navigate_and_do.py +1151 -0
  61. browsergym/workarena/tasks/compositional/navigate_and_do_infeasible.py +2100 -0
  62. browsergym/workarena/tasks/compositional/offboard_user.py +207 -0
  63. browsergym/workarena/tasks/compositional/onboard_user.py +226 -0
  64. browsergym/workarena/tasks/compositional/update_task.py +145 -0
  65. browsergym/workarena/tasks/compositional/utils/curriculum.py +215 -0
  66. browsergym/workarena/tasks/compositional/utils/infeasible_configs.py +151 -0
  67. browsergym/workarena/tasks/compositional/utils/knapsack.py +192 -0
  68. browsergym/workarena/tasks/compositional/warranty_check.py +227 -0
  69. browsergym/workarena/tasks/compositional/work_assignment.py +804 -0
  70. browsergym/workarena/tasks/compositional/workload_balancing.py +396 -0
  71. browsergym/workarena/tasks/dashboard.py +188 -8
  72. browsergym/workarena/tasks/form.py +1024 -232
  73. browsergym/workarena/tasks/knowledge.py +216 -25
  74. browsergym/workarena/tasks/list.py +519 -102
  75. browsergym/workarena/tasks/mark_duplicate_problem.py +171 -0
  76. browsergym/workarena/tasks/navigation.py +55 -13
  77. browsergym/workarena/tasks/scripts/extract_all_menu_items.py +9 -2
  78. browsergym/workarena/tasks/scripts/generate_dashboard_configs.py +6 -5
  79. browsergym/workarena/tasks/scripts/service_catalog.py +2 -1
  80. browsergym/workarena/tasks/scripts/validate.py +8 -2
  81. browsergym/workarena/tasks/send_chat_message.py +90 -0
  82. browsergym/workarena/tasks/service_catalog.py +94 -26
  83. browsergym/workarena/tasks/utils/form.py +1 -4
  84. browsergym/workarena/tasks/utils/private_tasks.py +63 -0
  85. browsergym/workarena/tasks/utils/utils.py +13 -0
  86. {browsergym_workarena-0.2.0.dist-info → browsergym_workarena-0.3.0.dist-info}/METADATA +27 -20
  87. browsergym_workarena-0.3.0.dist-info/RECORD +138 -0
  88. {browsergym_workarena-0.2.0.dist-info → browsergym_workarena-0.3.0.dist-info}/entry_points.txt +1 -0
  89. browsergym_workarena-0.2.0.dist-info/RECORD +0 -85
  90. {browsergym_workarena-0.2.0.dist-info → browsergym_workarena-0.3.0.dist-info}/WHEEL +0 -0
  91. {browsergym_workarena-0.2.0.dist-info → browsergym_workarena-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -15,6 +15,8 @@ from typing import List, Tuple
15
15
  from urllib import parse
16
16
  from warnings import warn
17
17
 
18
+ from .comp_building_block import CompositionalBuildingBlockTask
19
+
18
20
  from ..api.utils import table_api_call, table_column_info
19
21
  from ..config import (
20
22
  SNOW_BROWSER_TIMEOUT,
@@ -35,6 +37,7 @@ from ..config import (
35
37
  EXPECTED_CHANGE_REQUEST_COLUMNS_PATH,
36
38
  EXPECTED_HARDWARE_COLUMNS_PATH,
37
39
  EXPECTED_INCIDENT_COLUMNS_PATH,
40
+ EXPECTED_PROBLEM_COLUMNS_PATH,
38
41
  EXPECTED_SERVICE_CATALOG_COLUMNS_PATH,
39
42
  EXPECTED_USER_COLUMNS_PATH,
40
43
  )
@@ -76,8 +79,34 @@ LISTS = {
76
79
  },
77
80
  }
78
81
 
82
+ EXTRACT_USER_LIST_INFO_CONFIG = [
83
+ {
84
+ "start_rel_url": "/now/nav/ui/classic/params/target/sys_user_list.do%3Fsysparm_query%3Dactive%253Dtrue%255Ecompany%253D81fd65ecac1d55eb42a426568fc87a63%255Eemail%253Dlucius.bagnoli%40example.com%26sysparm_first_row%3D1%26sysparm_view%3D",
85
+ "fields": {
86
+ "user_name": "User ID",
87
+ "email": "Email",
88
+ "first_name": "First name",
89
+ "last_name": "Last name",
90
+ },
91
+ "expected_values": [
92
+ {
93
+ "user_name": "lucius.bagnoli",
94
+ "email": "lucius.bagnoli@example.com",
95
+ "first_name": "Lucius",
96
+ "last_name": "Bagnoli",
97
+ }
98
+ ],
99
+ }
100
+ ]
101
+
79
102
 
80
103
  class ServiceNowListTask(AbstractServiceNowTask):
104
+
105
+ @classmethod
106
+ def all_configs(cls) -> List[dict]:
107
+ with open(cls.config_path, "r") as f:
108
+ return json.load(f)
109
+
81
110
  def get_init_scripts(self) -> List[str]:
82
111
  return super().get_init_scripts() + ["registerGsftMainLoaded();"]
83
112
 
@@ -129,12 +158,10 @@ class ServiceNowListTask(AbstractServiceNowTask):
129
158
  }
130
159
 
131
160
  # Get column info
132
- fields = list_info["fields"].split(",")
133
161
  list_info["columns"] = table_column_info(
134
162
  instance=self.instance,
135
163
  table=list_info["glide_table"],
136
164
  )
137
- list_info["columns"] = {k: v for k, v in list_info["columns"].items() if k in fields}
138
165
 
139
166
  # Get the list data
140
167
  if with_data:
@@ -189,43 +216,46 @@ class SortListTask(ServiceNowListTask):
189
216
  Configuration to use for the task. If provided, the task will use the provided configuration instead of
190
217
  selecting a random one. See browsergym/workarena/data_files/task_configs/sort_change_request_list_task.json
191
218
  for an example of a configuration file.
192
- config_path:
193
- The path to the JSON file containing all configurations for the task. Provided by subclasses
194
219
  expected_fields_path:
195
220
  The path to the JSON file containing all expected fields for the task. Provided by subclasses
196
221
  """
197
222
 
198
223
  def __init__(
199
224
  self,
200
- seed: int,
225
+ seed: int = None,
201
226
  instance=None,
202
227
  list_url="",
203
228
  forbidden_fields=[],
204
229
  fixed_config: dict = None,
205
- config_path: str = None,
206
230
  expected_fields_path: str = None,
231
+ **kwargs,
207
232
  ) -> None:
208
233
  super().__init__(seed=seed, instance=instance, start_rel_url=list_url)
209
234
  self.min_sort_len = 1
210
235
  self.max_sort_len = 3
211
236
  self.forbidden_fields = forbidden_fields
212
237
  self.fixed_config = fixed_config
213
- if config_path:
214
- with open(config_path, "r") as f:
215
- self.all_configs = json.load(f)
238
+ self.config = None
239
+ if hasattr(self, "config_path"):
240
+ self.all_configs = self.all_configs()
241
+
216
242
  with open(expected_fields_path, "r") as f:
217
243
  self.expected_fields = set(json.load(f))
244
+ self.list_info = None
245
+ self.__dict__.update(kwargs)
218
246
 
219
247
  def setup_goal(self, page: Page) -> tuple[str, dict]:
220
248
  super().setup_goal(page=page)
221
249
 
222
250
  # Get the task configuration
223
- config = self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
224
- self.sort_fields = config["sort_fields"]
225
- self.sort_dirs = config["sort_dirs"]
251
+ self.config = (
252
+ self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
253
+ )
254
+ self.sort_fields = self.config["sort_fields"]
255
+ self.sort_dirs = self.config["sort_dirs"]
226
256
 
227
257
  # Get the task goal
228
- goal = config["goal"]
258
+ goal = self.config["goal"]
229
259
  info = {}
230
260
 
231
261
  return goal, info
@@ -236,10 +266,13 @@ class SortListTask(ServiceNowListTask):
236
266
 
237
267
  # Ensure that the fields that need to be sorted are visible (task feasibility check)
238
268
  self.list_info = self._extract_list_info(page)
239
- visible_columns = set(self.list_info["fields"].split(","))
240
- assert (
241
- set(self.sort_fields) <= visible_columns and visible_columns == self.expected_fields
242
- ), f"Fields {self.sort_fields} are not all visible in the list. Re-run workarena-install to correct this."
269
+
270
+ def get_pretty_printed_description(self) -> str:
271
+ """
272
+ Get the task info for this task when used in a private task; Used in L3 compositional tasks.
273
+ called by subclasses
274
+ """
275
+ return self.config["goal"] + "\n"
243
276
 
244
277
  def _generate_all_configs(self, seed: int, page: Page, n_fields_to_sort: int):
245
278
  self.setup(seed=seed, page=page)
@@ -311,7 +344,7 @@ class SortListTask(ServiceNowListTask):
311
344
  sort_dirs_txt = [dir_txt[sort_dir] for sort_dir in self.sort_dirs]
312
345
 
313
346
  # check if the task is already solved (can happen if the chosen field is already sorted in the default view)
314
- _, done, _, _ = self.validate(self.page, [])
347
+ _, done, _, _ = self.validate(page, [])
315
348
  # if so, pick new fields
316
349
  if done:
317
350
  logging.warning("Trivial config for sort list task, picking a new config.")
@@ -329,6 +362,8 @@ class SortListTask(ServiceNowListTask):
329
362
  def cheat(self, page: Page, chat_messages: list[str]) -> None:
330
363
  super().cheat(page=page, chat_messages=chat_messages)
331
364
  self._wait_for_ready(page)
365
+ if self.list_info is None:
366
+ self.list_info = self._extract_list_info(page)
332
367
 
333
368
  iframe, _, _ = self._get_visible_list(page)
334
369
 
@@ -408,8 +443,8 @@ class SortListTask(ServiceNowListTask):
408
443
  # ... retrieve list
409
444
  list_info = self._extract_list_info(page)
410
445
  # ... get sorting info
411
- sort_by = self.page.evaluate(f'{list_info["js_selector"]}.getOrderBy()')
412
- sort_dir = self.page.evaluate(f'{list_info["js_selector"]}.sortDir')
446
+ sort_by = page.evaluate(f'{list_info["js_selector"]}.getOrderBy()')
447
+ sort_dir = page.evaluate(f'{list_info["js_selector"]}.sortDir')
413
448
  # ... check if the list is sorted correctly
414
449
  if sort_by == self.sort_fields[0] and sort_dir.lower() == self.sort_dirs[0]:
415
450
  return (
@@ -469,30 +504,31 @@ class FilterListTask(ServiceNowListTask):
469
504
  Configuration to use for the task. If provided, the task will use the provided configuration instead of
470
505
  selecting a random one. See browsergym/workarena/data_files/task_configs/filter_change_request_list_task.json
471
506
  for an example of a configuration file.
472
- config_path:
473
- The path to the JSON file containing all configurations for the task. Provided by subclasses
474
507
  expected_fields_path:
475
508
  The path to the JSON file containing all expected fields for the task. Provided by subclasses
476
509
  """
477
510
 
478
511
  def __init__(
479
512
  self,
480
- seed: int,
513
+ seed: int = None,
481
514
  instance=None,
482
515
  list_url="",
483
516
  fixed_config: dict = None,
484
- config_path: str = None,
485
517
  expected_fields_path: str = None,
518
+ **kwargs,
486
519
  ) -> None:
487
520
  self.min_filter_len = 2
488
521
  self.max_filter_len = 5
489
522
  super().__init__(seed=seed, instance=instance, start_rel_url=list_url)
490
523
  self.fixed_config = fixed_config
491
- if config_path:
492
- with open(config_path, "r") as f:
493
- self.all_configs = json.load(f)
524
+ self.config = None
525
+ if hasattr(self, "config_path"):
526
+ self.all_configs = self.all_configs()
527
+
494
528
  with open(expected_fields_path, "r") as f:
495
529
  self.expected_fields = set(json.load(f))
530
+ self.table_name = list_url.split("/")[-1].split("_list.do")[0]
531
+ self.__dict__.update(kwargs)
496
532
 
497
533
  def setup_goal(self, page: Page) -> tuple[str, dict]:
498
534
  super().setup_goal(page=page)
@@ -501,36 +537,48 @@ class FilterListTask(ServiceNowListTask):
501
537
  config = self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
502
538
  self.filter_columns = config["filter_columns"]
503
539
  self.filter_values = config["filter_values"]
540
+ # Base filter configs do not have filter_operands, so we default to "is"
541
+ self.filter_operators = config.get("filter_operators", ["is" for _ in self.filter_columns])
504
542
  self.filter_kind = config["filter_kind"]
505
- self.list_info = config["list_info"]
543
+ list_info = config.get("list_info")
544
+ if list_info is None:
545
+ list_info = {"columns": table_column_info(self.instance, self.table_name)}
546
+ self.list_info = list_info
506
547
  self.filter_len = len(self.filter_columns)
507
548
 
508
549
  # Generate goal
509
- goal = (
510
- f"Create a filter for the list to extract all entries where "
511
- + f" {'and' if self.filter_kind == 'AND' else 'or'} ".join(
512
- [
513
- f'"{self.list_info["columns"][col]["label"]}" is "{val}"'
514
- for col, val in zip(self.filter_columns, self.filter_values)
515
- ]
516
- )
517
- + "."
518
- )
550
+ goal = self.get_pretty_printed_description(goal=True)
519
551
  info = {}
520
552
 
521
553
  return goal, info
522
554
 
523
555
  def start(self, page: Page) -> None:
524
556
  super().start(page)
525
-
526
557
  self._wait_for_ready(page)
527
558
 
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."
559
+ def get_pretty_printed_description(self, goal=False) -> str:
560
+ """
561
+ Get the task info for this task when used in a private task; Used in L3 compositional tasks.
562
+ called by subclasses
563
+
564
+ args:
565
+ goal: bool
566
+ If True, return as the goal of the task (without the starting dash)
567
+ """
568
+ task_info = "" if goal else "- "
569
+ task_info += (
570
+ f"Create a filter for the list to extract all entries where:"
571
+ + f" {'and' if self.filter_kind == 'AND' else 'or'} ".join(
572
+ [
573
+ f'\n - "{self.list_info["columns"][col]["label"]}" {filter_operator} "{val}"'
574
+ for col, filter_operator, val in zip(
575
+ self.filter_columns, self.filter_operators, self.filter_values
576
+ )
577
+ ]
578
+ )
579
+ )
580
+
581
+ return task_info
534
582
 
535
583
  def _generate_random_config(self, page: Page):
536
584
  self.setup(page=page)
@@ -563,7 +611,7 @@ class FilterListTask(ServiceNowListTask):
563
611
  # We do this by loading a single record at random and using its values
564
612
  # This is significantly faster than loading all records and then filtering
565
613
  offset = self.random.randint(
566
- 0, self.page.evaluate(f'{self.list_info["js_selector"]}.grandTotalRows')
614
+ 0, page.evaluate(f'{self.list_info["js_selector"]}.grandTotalRows')
567
615
  )
568
616
  data = table_api_call(
569
617
  instance=self.instance,
@@ -652,7 +700,7 @@ class FilterListTask(ServiceNowListTask):
652
700
  f'.filterToolbar .filerTableAction:text-is("{self.filter_kind}")'
653
701
  ).click()
654
702
  # TODO: Hack to solve bug where the filter condition has not yet appeared
655
- self.page.wait_for_timeout(1000)
703
+ page.wait_for_timeout(1000)
656
704
 
657
705
  # Refresh since new rows are added at each iteration
658
706
  filter_rows = iframe.locator(".filter_row")
@@ -664,8 +712,14 @@ class FilterListTask(ServiceNowListTask):
664
712
  field_selector.select_option(self.filter_columns[i])
665
713
 
666
714
  # Select the right operator
667
- logging.debug("Choosing operator =")
668
- row.locator("select.condOperator").select_option("=")
715
+ operator = self.filter_operators[i]
716
+ operator_symbol = (
717
+ row.locator("select.condOperator")
718
+ .get_by_text(operator, exact=True)
719
+ .get_attribute("value")
720
+ )
721
+ logging.debug(f"Choosing operator {operator}")
722
+ row.locator("select.condOperator").select_option(operator_symbol)
669
723
 
670
724
  # Fill in the value
671
725
  logging.debug("Filling in value " + self.filter_values[i])
@@ -718,26 +772,19 @@ class FilterListTask(ServiceNowListTask):
718
772
  },
719
773
  )
720
774
  self._wait_for_ready(page)
721
- if self.filter_kind not in ["AND", "OR"]:
722
- raise NotImplementedError("Only AND and OR filters are supported.")
723
- # Excludes AND because that's the default and its sep is ^ which matches everywhere
724
- query_sep = {"OR": "^NQ"}
725
775
 
726
- # Retrieve list
776
+ # Retrieve the current query
727
777
  list_info = self._extract_list_info(page)
728
-
729
- # Check if the list is filtered correctly
730
778
  current_query = list_info["query"]
731
779
 
780
+ # Replace "new query" statements with the standard OR separator
781
+ current_query = current_query.replace("^NQ", "^OR")
782
+
732
783
  # Validate query kind is ok
733
- current_kind = None
734
- for kind in query_sep:
735
- if query_sep[kind] in current_query:
736
- current_kind = kind
737
- current_sep = query_sep[kind]
738
- break
784
+ if "^OR" in current_query:
785
+ current_kind = "OR"
786
+ current_sep = "^OR"
739
787
  else:
740
- # If no separator is found, then the query is just assumed to be AND (it's a single condition)
741
788
  current_kind = "AND"
742
789
  current_sep = "^"
743
790
 
@@ -760,24 +807,44 @@ class FilterListTask(ServiceNowListTask):
760
807
  # This is the tricky part because we need to expand the values to their display values
761
808
  # We also need to handle the case where the value is a reference
762
809
  current_values = [x.split("=")[1] for x in current_query]
763
- for col, val in zip(current_columns, current_values):
764
- col_info = self.list_info["columns"][col]
765
810
 
811
+ # Handle filtering across multiple rows
812
+ if len(set(current_columns)) < len(current_columns):
813
+ if len(set(current_columns)) != 1:
814
+ raise Exception("Filtering is only allowed across rows for the same column.")
815
+ # Filter multiple rows with a column
816
+ is_homogenous_filter = True
817
+ else:
818
+ # Current setting where we use multiple columns to filter
819
+ is_homogenous_filter = False
820
+ for index, (col, val) in enumerate(zip(current_columns, current_values)):
821
+ col_info = self.list_info["columns"][col]
766
822
  # Get the column type
767
823
  if col_info["type"] == "reference" and val != "":
768
824
  # Get the reference table
769
825
  ref_table = col_info["reference"]
770
826
  ref_field = col_info["reference_attributes"]["display_field"]
771
- # Get the reference display value
772
- current_values[current_columns.index(col)] = table_api_call(
773
- instance=self.instance,
774
- table=ref_table,
775
- params={
776
- "sysparm_query": f"sys_id={val}",
777
- "sysparm_fields": ref_field,
778
- "sysparm_display_value": "all",
779
- },
780
- )["result"][0][ref_field]["display_value"]
827
+ if is_homogenous_filter:
828
+ current_values[index] = table_api_call(
829
+ instance=self.instance,
830
+ table=ref_table,
831
+ params={
832
+ "sysparm_query": f"sys_id={val}",
833
+ "sysparm_fields": ref_field,
834
+ "sysparm_display_value": "all",
835
+ },
836
+ )["result"][0][ref_field]["display_value"]
837
+ else:
838
+ # Get the reference display value
839
+ current_values[current_columns.index(col)] = table_api_call(
840
+ instance=self.instance,
841
+ table=ref_table,
842
+ params={
843
+ "sysparm_query": f"sys_id={val}",
844
+ "sysparm_fields": ref_field,
845
+ "sysparm_display_value": "all",
846
+ },
847
+ )["result"][0][ref_field]["display_value"]
781
848
 
782
849
  elif col_info["type"] == "choice":
783
850
  # Get the choice display value
@@ -794,114 +861,409 @@ class FilterListTask(ServiceNowListTask):
794
861
  return 1, True, "Nice work, thank you!", {"message": "Correct filter."}
795
862
 
796
863
 
864
+ class ExtractListInfoTask(ServiceNowListTask):
865
+ """
866
+ Extract information from some fields in a list. Works with any list.
867
+
868
+ Parameters:
869
+ -----------
870
+ instance: SNowInstance
871
+ The instance to use.
872
+ list_url: str
873
+ The relative URL of the list to filter.
874
+ fixed_config: dict
875
+ Configuration to use for the task. If provided, the task will use the provided configuration instead of
876
+ selecting a random one. See browsergym/workarena/data_files/task_configs/filter_change_request_list_task.json
877
+ for an example of a configuration file.
878
+ config_path:
879
+ The path to the JSON file containing all configurations for the task. Provided by subclasses
880
+ list_name: str
881
+ Name of the list to extract information from.
882
+ list_url: str
883
+ url of the list to extract information from.
884
+ unique_field_name: str
885
+ Name of the field used as unique in the list. This field is required in configs.
886
+ """
887
+
888
+ def __init__(
889
+ self,
890
+ seed: int = None,
891
+ instance=None,
892
+ fixed_config: dict = None,
893
+ configs: str = "",
894
+ list_name: str = "",
895
+ list_url: str = "",
896
+ unique_field_name: str = "",
897
+ **kwargs,
898
+ ) -> None:
899
+ super().__init__(
900
+ seed=seed, instance=instance, start_rel_url=list_url
901
+ ) # For these tasks, the start URL is defined in the setup method, as the URL depends on the configuration
902
+ self.fixed_config = fixed_config
903
+ self.config = None
904
+ self.all_configs = configs
905
+ self.list_name = list_name
906
+ self.table_name = ""
907
+ self.unique_field_name = unique_field_name
908
+ self.__dict__.update(kwargs)
909
+
910
+ def setup_goal(self, page: Page) -> tuple[str, dict]:
911
+ super().setup_goal(page=page)
912
+
913
+ # Get the task configuration
914
+ config = self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
915
+ self.fields = config["fields"] # mapping between fields and their display names
916
+ self.printed_field_names = {
917
+ v: k for k, v in self.fields.items()
918
+ } # mapping between fields and their system names
919
+ self.expected_values = config[
920
+ "expected_values"
921
+ ] # mapping between fields and their expected values
922
+ # This is setup here because the start_url depends on the config
923
+ assert (
924
+ self.unique_field_name in self.fields.keys()
925
+ ), f"Unique field name {self.unique_field_name} not in fields."
926
+ assert all(
927
+ [self.unique_field_name in expected_value for expected_value in self.expected_values]
928
+ ), f"Unique field name {self.unique_field_name} not in expected values."
929
+
930
+ if not self.start_url or self.start_url == self.instance.snow_url:
931
+ self.start_rel_url = config["start_rel_url"]
932
+ self.start_url = self.instance.snow_url + self.start_rel_url
933
+ # table_name can be passed in the constructor or extracted from the start_rel_url, located in the config
934
+ if self.table_name is None:
935
+ self.table_name = self.start_rel_url.split("/")[-1].split("_list.do")[0]
936
+
937
+ goal = self.get_pretty_printed_description()
938
+ info = {}
939
+
940
+ return goal, info
941
+
942
+ def start(self, page: Page) -> None:
943
+ super().start(page)
944
+ # TODO: We should add a check to make sure the required columns are present in the list
945
+
946
+ def get_pretty_printed_description(self) -> str:
947
+ """
948
+ Get the task info for this task when used in a private task; Used in L3 compositional tasks and used as goal in L1 tasks.
949
+ called by subclasses
950
+ """
951
+ print_field_names = list(self.fields.values())
952
+ print_field_names.remove(
953
+ self.fields[self.unique_field_name]
954
+ ) # the unique fields are the keys in the dict
955
+ if len(print_field_names) > 1:
956
+ fields_str = (
957
+ '"' + '", "'.join(print_field_names[:-1]) + f'" and "{print_field_names[-1]}"'
958
+ )
959
+ printed_unique_field_name = self.fields[self.unique_field_name]
960
+ task_description = (
961
+ f"- Extract information of field(s) {fields_str} "
962
+ + f'from the "{self.list_name}" list. Return the result as a json where keys are the values of the "{printed_unique_field_name}" field and values are mappings between the fields and the extracted information. Please provide this information in the chat.'
963
+ )
964
+ else:
965
+ fields_str = print_field_names[0]
966
+ task_description = f'- Extract information of field "{fields_str}" from the "{self.list_name}" list. Please provide this information in the chat.'
967
+
968
+ return task_description
969
+
970
+ def _wait_for_ready(self, page: Page) -> bool:
971
+ """
972
+ Waits for the main iframe to be fully loaded; over-rides the parent method as the cheat
973
+ can be called on a filtered list on which there is no gsft_main.
974
+
975
+ Returns True if the gsft_main is present, False otherwise.
976
+ """
977
+ gsft_main_present = False
978
+ logging.debug(f"Waiting up to 3 seconds for gsft_main to be ready")
979
+ try:
980
+ page.wait_for_function(
981
+ "typeof window.gsft_main !== 'undefined' && window.gsft_main.WORKARENA_LOAD_COMPLETE",
982
+ timeout=3000,
983
+ )
984
+ logging.debug("Detected gsft_main ready")
985
+ gsft_main_present = True
986
+ except TimeoutError:
987
+ logging.debug(
988
+ "Timed out waiting for gsft_main to be ready; searching for GlideList API directly"
989
+ )
990
+ pass
991
+
992
+ logging.debug("Waiting for Glide list API to be available")
993
+ if gsft_main_present:
994
+ page.wait_for_function("window.gsft_main.GlideList2 !== undefined")
995
+ else:
996
+ page.wait_for_function("window.GlideList2 !== undefined")
997
+
998
+ logging.debug("Detected Glide list API ready")
999
+
1000
+ return gsft_main_present
1001
+
1002
+ def cheat(self, page: Page, chat_messages: list[str]) -> None:
1003
+ super().cheat(page=page, chat_messages=chat_messages)
1004
+ right_url = check_url_suffix_match(page, expected_url=self.start_url, task=self)
1005
+ if not right_url:
1006
+ return
1007
+ gft_main_present = self._wait_for_ready(page)
1008
+ if gft_main_present:
1009
+ main_element = page.wait_for_selector("iframe#gsft_main").content_frame()
1010
+ else:
1011
+ main_element = page
1012
+
1013
+ main_element.wait_for_selector(
1014
+ f"#hdr_{self.table_name}"
1015
+ ) # Selector for the name of the columns
1016
+ # system name mapped to their order in the table
1017
+ all_column_elements = main_element.query_selector_all(f"#hdr_{self.table_name} th")
1018
+ required_fields_order = {}
1019
+ for i, element in enumerate(all_column_elements):
1020
+ if element.get_attribute("name") in self.fields:
1021
+ required_fields_order[element.get_attribute("name")] = i
1022
+
1023
+ # Lines of the table
1024
+ table_lines = main_element.query_selector_all(
1025
+ f".list2_body [record_class={self.table_name}]"
1026
+ )
1027
+
1028
+ # will hold the values to extract
1029
+ table_values = {}
1030
+
1031
+ # Extract the values of the required fields
1032
+ for line_element in table_lines:
1033
+ line_fields = line_element.query_selector_all("td")
1034
+ line_values = {}
1035
+ for field, order in required_fields_order.items():
1036
+ printed_field_name = self.fields[field]
1037
+ line_values[printed_field_name] = line_fields[order].inner_text()
1038
+ printed_unique_value_name = self.fields[self.unique_field_name]
1039
+ unique_field_value = line_values[printed_unique_value_name]
1040
+ line_values.pop(printed_unique_value_name)
1041
+ table_values[unique_field_value] = line_values
1042
+
1043
+ # Add the "extracted" answer to the chat messages
1044
+ if len(self.fields) > 2:
1045
+ chat_messages.append({"role": "assistant", "message": json.dumps(table_values)})
1046
+ # In this case, we expect only one field to be extracted
1047
+ else:
1048
+ expected_field = list(self.fields.keys() - {self.unique_field_name})[0]
1049
+ pretty_field_name = self.fields[expected_field]
1050
+ # Here we assume that unique_field_value is unique in the table_values
1051
+ chat_messages.append(
1052
+ {
1053
+ "role": "assistant",
1054
+ "message": str(table_values[unique_field_value][pretty_field_name]),
1055
+ }
1056
+ )
1057
+
1058
+ def validate(
1059
+ self, page: playwright.sync_api.Page, chat_messages: list[str]
1060
+ ) -> Tuple[float, bool, str, dict]:
1061
+ """
1062
+ Validate the solution
1063
+
1064
+ Note: current implementation is limited to AND and OR filters (single type per filter) with equality operators
1065
+
1066
+ """
1067
+ if (
1068
+ len(chat_messages) == 0
1069
+ or chat_messages[-1]["role"] != "assistant"
1070
+ or not chat_messages[-1]["message"]
1071
+ ):
1072
+ return 0, False, "", {"message": "No extracted values found."}
1073
+
1074
+ # When 2 or more fields (unique field is always present so at least 2 fields are present), we expect a dict
1075
+ # Otherwise, we only look for the presence of the expected value in the message sent by the agent
1076
+ if len(self.fields) > 2:
1077
+ answer = json.loads(chat_messages[-1]["message"])
1078
+ for expected_line in self.expected_values:
1079
+ # Check if the line is in the visible lines
1080
+ if expected_line[self.unique_field_name] not in answer:
1081
+ return (
1082
+ 0,
1083
+ False,
1084
+ "",
1085
+ {
1086
+ "message": f"Value {expected_line[self.unique_field_name]} for unique field {self.unique_field_name} not found in the list."
1087
+ },
1088
+ )
1089
+ # Check if the values are correct
1090
+ unique_value = expected_line[self.unique_field_name]
1091
+ # This checks all fields inside the dict for the unique value
1092
+ for field, value in expected_line.items():
1093
+ # The unique field's presence is implicitly validated by the above check
1094
+ if field == self.unique_field_name:
1095
+ continue
1096
+ printed_field_name = self.fields[field]
1097
+ if answer[unique_value][printed_field_name] != value:
1098
+ return 0, False, "", {"message": "Incorrect value."}
1099
+ # In this case, we expect only one field to be extracted
1100
+ else:
1101
+ # get the field that is not the unique field
1102
+ field = list(self.fields.keys() - {self.unique_field_name})[0]
1103
+ expected_value = str(self.expected_values[0][field])
1104
+ if expected_value not in chat_messages[-1]["message"]:
1105
+ return 0, False, "", {"message": "Incorrect value."}
1106
+
1107
+ return (
1108
+ 1,
1109
+ True,
1110
+ "Nice work, thank you!",
1111
+ {"message": "Correct information extracted."},
1112
+ )
1113
+
1114
+
797
1115
  class FilterAssetListTask(FilterListTask):
1116
+ config_path = FILTER_ASSET_LIST_CONFIG_PATH
1117
+
798
1118
  def __init__(
799
1119
  self,
800
- seed: int,
1120
+ seed: int = None,
801
1121
  instance=None,
802
1122
  fixed_config: dict = None,
1123
+ **kwargs,
803
1124
  ) -> None:
804
1125
  super().__init__(
805
1126
  seed=seed,
806
1127
  instance=instance,
807
1128
  list_url=LISTS["alm_asset"]["url"],
808
1129
  fixed_config=fixed_config,
809
- config_path=FILTER_ASSET_LIST_CONFIG_PATH,
810
1130
  expected_fields_path=EXPECTED_ASSET_LIST_COLUMNS_PATH,
1131
+ **kwargs,
811
1132
  )
812
1133
 
813
1134
 
814
1135
  class FilterChangeRequestListTask(FilterListTask):
1136
+ config_path = FILTER_CHANGE_REQUEST_LIST_CONFIG_PATH
1137
+
815
1138
  def __init__(
816
1139
  self,
817
- seed: int,
1140
+ seed: int = None,
818
1141
  instance=None,
819
1142
  fixed_config: dict = None,
1143
+ **kwargs,
820
1144
  ) -> None:
821
1145
  super().__init__(
822
1146
  seed=seed,
823
1147
  instance=instance,
824
1148
  list_url=LISTS["change_request"]["url"],
825
1149
  fixed_config=fixed_config,
826
- config_path=FILTER_CHANGE_REQUEST_LIST_CONFIG_PATH,
827
1150
  expected_fields_path=EXPECTED_CHANGE_REQUEST_COLUMNS_PATH,
1151
+ **kwargs,
828
1152
  )
829
1153
 
830
1154
 
831
1155
  class FilterHardwareListTask(FilterListTask):
1156
+ config_path = FILTER_HARDWARE_LIST_CONFIG_PATH
1157
+
832
1158
  def __init__(
833
1159
  self,
834
- seed: int,
1160
+ seed: int = None,
835
1161
  instance=None,
836
1162
  fixed_config: dict = None,
1163
+ **kwargs,
837
1164
  ) -> None:
838
1165
  super().__init__(
839
1166
  seed=seed,
840
1167
  instance=instance,
841
1168
  list_url=LISTS["alm_hardware"]["url"],
842
1169
  fixed_config=fixed_config,
843
- config_path=FILTER_HARDWARE_LIST_CONFIG_PATH,
844
1170
  expected_fields_path=EXPECTED_HARDWARE_COLUMNS_PATH,
1171
+ **kwargs,
845
1172
  )
846
1173
 
847
1174
 
848
1175
  class FilterIncidentListTask(FilterListTask):
1176
+ config_path = FILTER_INCIDENT_LIST_CONFIG_PATH
1177
+
849
1178
  def __init__(
850
1179
  self,
851
- seed: int,
1180
+ seed: int = None,
852
1181
  instance=None,
853
1182
  fixed_config: dict = None,
1183
+ **kwargs,
854
1184
  ) -> None:
855
1185
  super().__init__(
856
1186
  seed=seed,
857
1187
  instance=instance,
858
1188
  list_url=LISTS["incident"]["url"],
859
1189
  fixed_config=fixed_config,
860
- config_path=FILTER_INCIDENT_LIST_CONFIG_PATH,
861
1190
  expected_fields_path=EXPECTED_INCIDENT_COLUMNS_PATH,
1191
+ **kwargs,
862
1192
  )
863
1193
 
864
1194
 
1195
+ class FilterProblemListForWorkLoadBalancingTask(FilterListTask, CompositionalBuildingBlockTask):
1196
+ def __init__(
1197
+ self,
1198
+ seed: int = None,
1199
+ instance=None,
1200
+ fixed_config: dict = None,
1201
+ **kwargs,
1202
+ ) -> None:
1203
+ super().__init__(
1204
+ seed=seed,
1205
+ instance=instance,
1206
+ list_url="/now/nav/ui/classic/params/target/problem_list.do",
1207
+ fixed_config=fixed_config,
1208
+ expected_fields_path=EXPECTED_PROBLEM_COLUMNS_PATH,
1209
+ **kwargs,
1210
+ )
1211
+
1212
+ def get_pretty_printed_description(self, goal=False) -> str:
1213
+ """Override the parent method to provide a more detailed description of the task"""
1214
+
1215
+ return self.goal
1216
+
1217
+
865
1218
  class FilterServiceCatalogItemListTask(FilterListTask):
1219
+ config_path = FILTER_SERVICE_CATALOG_ITEM_LIST_CONFIG_PATH
1220
+
866
1221
  def __init__(
867
1222
  self,
868
- seed: int,
1223
+ seed: int = None,
869
1224
  instance=None,
870
1225
  fixed_config: dict = None,
1226
+ **kwargs,
871
1227
  ) -> None:
872
1228
  super().__init__(
873
1229
  seed=seed,
874
1230
  instance=instance,
875
1231
  list_url=LISTS["sc_cat_item"]["url"],
876
1232
  fixed_config=fixed_config,
877
- config_path=FILTER_SERVICE_CATALOG_ITEM_LIST_CONFIG_PATH,
878
1233
  expected_fields_path=EXPECTED_SERVICE_CATALOG_COLUMNS_PATH,
1234
+ **kwargs,
879
1235
  )
880
1236
 
881
1237
 
882
1238
  class FilterUserListTask(FilterListTask):
1239
+ config_path = FILTER_USER_LIST_CONFIG_PATH
1240
+
883
1241
  def __init__(
884
1242
  self,
885
- seed: int,
1243
+ seed: int = None,
886
1244
  instance=None,
887
1245
  fixed_config: dict = None,
1246
+ **kwargs,
888
1247
  ) -> None:
889
1248
  super().__init__(
890
1249
  seed=seed,
891
1250
  instance=instance,
892
1251
  list_url=LISTS["sys_user"]["url"],
893
1252
  fixed_config=fixed_config,
894
- config_path=FILTER_USER_LIST_CONFIG_PATH,
895
1253
  expected_fields_path=EXPECTED_USER_COLUMNS_PATH,
1254
+ **kwargs,
896
1255
  )
897
1256
 
898
1257
 
899
1258
  class SortAssetListTask(SortListTask):
1259
+ config_path = SORT_ASSET_LIST_CONFIG_PATH
1260
+
900
1261
  def __init__(
901
1262
  self,
902
- seed: int,
1263
+ seed: int = None,
903
1264
  instance=None,
904
1265
  fixed_config: dict = None,
1266
+ **kwargs,
905
1267
  ) -> None:
906
1268
  super().__init__(
907
1269
  seed=seed,
@@ -909,17 +1271,20 @@ class SortAssetListTask(SortListTask):
909
1271
  list_url=LISTS["alm_asset"]["url"],
910
1272
  forbidden_fields=LISTS["alm_asset"]["forbidden_fields"],
911
1273
  fixed_config=fixed_config,
912
- config_path=SORT_ASSET_LIST_CONFIG_PATH,
913
1274
  expected_fields_path=EXPECTED_ASSET_LIST_COLUMNS_PATH,
1275
+ **kwargs,
914
1276
  )
915
1277
 
916
1278
 
917
1279
  class SortChangeRequestListTask(SortListTask):
1280
+ config_path = SORT_CHANGE_REQUEST_LIST_CONFIG_PATH
1281
+
918
1282
  def __init__(
919
1283
  self,
920
- seed: int,
1284
+ seed: int = None,
921
1285
  instance=None,
922
1286
  fixed_config: dict = None,
1287
+ **kwargs,
923
1288
  ) -> None:
924
1289
  super().__init__(
925
1290
  seed=seed,
@@ -927,17 +1292,20 @@ class SortChangeRequestListTask(SortListTask):
927
1292
  list_url=LISTS["change_request"]["url"],
928
1293
  forbidden_fields=LISTS["change_request"]["forbidden_fields"],
929
1294
  fixed_config=fixed_config,
930
- config_path=SORT_CHANGE_REQUEST_LIST_CONFIG_PATH,
931
1295
  expected_fields_path=EXPECTED_CHANGE_REQUEST_COLUMNS_PATH,
1296
+ **kwargs,
932
1297
  )
933
1298
 
934
1299
 
935
1300
  class SortHardwareListTask(SortListTask):
1301
+ config_path = SORT_HARDWARE_LIST_CONFIG_PATH
1302
+
936
1303
  def __init__(
937
1304
  self,
938
- seed: int,
1305
+ seed: int = None,
939
1306
  instance=None,
940
1307
  fixed_config: dict = None,
1308
+ **kwargs,
941
1309
  ) -> None:
942
1310
  super().__init__(
943
1311
  seed=seed,
@@ -945,17 +1313,20 @@ class SortHardwareListTask(SortListTask):
945
1313
  list_url=LISTS["alm_hardware"]["url"],
946
1314
  forbidden_fields=LISTS["alm_hardware"]["forbidden_fields"],
947
1315
  fixed_config=fixed_config,
948
- config_path=SORT_HARDWARE_LIST_CONFIG_PATH,
949
1316
  expected_fields_path=EXPECTED_HARDWARE_COLUMNS_PATH,
1317
+ **kwargs,
950
1318
  )
951
1319
 
952
1320
 
953
1321
  class SortIncidentListTask(SortListTask):
1322
+ config_path = SORT_INCIDENT_LIST_CONFIG_PATH
1323
+
954
1324
  def __init__(
955
1325
  self,
956
- seed: int,
1326
+ seed: int = None,
957
1327
  instance=None,
958
1328
  fixed_config: dict = None,
1329
+ **kwargs,
959
1330
  ) -> None:
960
1331
  super().__init__(
961
1332
  seed=seed,
@@ -963,17 +1334,20 @@ class SortIncidentListTask(SortListTask):
963
1334
  list_url=LISTS["incident"]["url"],
964
1335
  forbidden_fields=LISTS["incident"]["forbidden_fields"],
965
1336
  fixed_config=fixed_config,
966
- config_path=SORT_INCIDENT_LIST_CONFIG_PATH,
967
1337
  expected_fields_path=EXPECTED_INCIDENT_COLUMNS_PATH,
1338
+ **kwargs,
968
1339
  )
969
1340
 
970
1341
 
971
1342
  class SortServiceCatalogItemListTask(SortListTask):
1343
+ config_path = SORT_SERVICE_CATALOG_ITEM_LIST_CONFIG_PATH
1344
+
972
1345
  def __init__(
973
1346
  self,
974
- seed: int,
1347
+ seed: int = None,
975
1348
  instance=None,
976
1349
  fixed_config: dict = None,
1350
+ **kwargs,
977
1351
  ) -> None:
978
1352
  super().__init__(
979
1353
  seed=seed,
@@ -981,17 +1355,20 @@ class SortServiceCatalogItemListTask(SortListTask):
981
1355
  list_url=LISTS["sc_cat_item"]["url"],
982
1356
  forbidden_fields=LISTS["sc_cat_item"]["forbidden_fields"],
983
1357
  fixed_config=fixed_config,
984
- config_path=SORT_SERVICE_CATALOG_ITEM_LIST_CONFIG_PATH,
985
1358
  expected_fields_path=EXPECTED_SERVICE_CATALOG_COLUMNS_PATH,
1359
+ **kwargs,
986
1360
  )
987
1361
 
988
1362
 
989
1363
  class SortUserListTask(SortListTask):
1364
+ config_path = SORT_USER_LIST_CONFIG_PATH
1365
+
990
1366
  def __init__(
991
1367
  self,
992
- seed: int,
1368
+ seed: int = None,
993
1369
  instance=None,
994
1370
  fixed_config: dict = None,
1371
+ **kwargs,
995
1372
  ) -> None:
996
1373
  super().__init__(
997
1374
  seed=seed,
@@ -999,12 +1376,52 @@ class SortUserListTask(SortListTask):
999
1376
  list_url=LISTS["sys_user"]["url"],
1000
1377
  forbidden_fields=LISTS["sys_user"]["forbidden_fields"],
1001
1378
  fixed_config=fixed_config,
1002
- config_path=SORT_USER_LIST_CONFIG_PATH,
1003
1379
  expected_fields_path=EXPECTED_USER_COLUMNS_PATH,
1380
+ **kwargs,
1381
+ )
1382
+
1383
+
1384
+ class ExtractUserListInfoTask(ExtractListInfoTask, CompositionalBuildingBlockTask):
1385
+ def __init__(
1386
+ self,
1387
+ seed: int = None,
1388
+ instance=None,
1389
+ fixed_config: dict = None,
1390
+ config_path=EXTRACT_USER_LIST_INFO_CONFIG,
1391
+ list_name="User",
1392
+ unique_field_name="user_name",
1393
+ **kwargs,
1394
+ ) -> None:
1395
+ super().__init__(
1396
+ seed=seed,
1397
+ instance=instance,
1398
+ fixed_config=fixed_config,
1399
+ config_path=config_path,
1400
+ list_name=list_name,
1401
+ unique_field_name=unique_field_name,
1402
+ table_name="sys_user",
1403
+ **kwargs,
1004
1404
  )
1005
1405
 
1006
1406
 
1007
1407
  # Register all tasks
1008
- __TASKS__ = [
1009
- value for name, value in locals().items() if re.compile(r"^Filter\w+ListTask$").match(name)
1010
- ] + [value for name, value in locals().items() if re.compile(r"^Sort\w+ListTask$").match(name)]
1408
+ __TASKS__ = (
1409
+ [
1410
+ value
1411
+ for name, value in locals().items()
1412
+ if re.compile(r"^Filter\w+ListTask$").match(name)
1413
+ and not issubclass(value, CompositionalBuildingBlockTask)
1414
+ ]
1415
+ + [
1416
+ value
1417
+ for name, value in locals().items()
1418
+ if re.compile(r"^Sort\w+ListTask$").match(name)
1419
+ and not issubclass(value, CompositionalBuildingBlockTask)
1420
+ ]
1421
+ + [
1422
+ value
1423
+ for name, value in locals().items()
1424
+ if re.compile(r"^Extract\w+ListInfoTask$").match(name)
1425
+ and not issubclass(value, CompositionalBuildingBlockTask)
1426
+ ]
1427
+ )