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.
- browsergym/workarena/__init__.py +13 -1
- browsergym/workarena/api/category.py +74 -0
- browsergym/workarena/api/change_request.py +87 -0
- browsergym/workarena/api/computer_asset.py +90 -0
- browsergym/workarena/api/cost_center.py +19 -0
- browsergym/workarena/api/expense_line.py +89 -0
- browsergym/workarena/api/incident.py +45 -0
- browsergym/workarena/api/knowledge.py +29 -0
- browsergym/workarena/api/problem.py +90 -0
- browsergym/workarena/api/report.py +183 -0
- browsergym/workarena/api/requested_items.py +63 -0
- browsergym/workarena/api/user.py +11 -8
- browsergym/workarena/api/utils.py +47 -3
- browsergym/workarena/config.py +21 -1
- browsergym/workarena/data_files/setup_files/forms/expected_incident_form_fields.json +1 -1
- browsergym/workarena/data_files/setup_files/forms/expected_request_item_form_fields.json +1 -0
- browsergym/workarena/data_files/setup_files/knowledge/protocols.json +46 -0
- browsergym/workarena/data_files/setup_files/knowledge/test.html +1 -0
- browsergym/workarena/data_files/setup_files/lists/expected_asset_list_columns.json +2 -24
- browsergym/workarena/data_files/setup_files/lists/expected_change_request_list_columns.json +4 -40
- browsergym/workarena/data_files/setup_files/lists/expected_expense_line_list_columns.json +12 -0
- browsergym/workarena/data_files/setup_files/lists/expected_hardware_list_columns.json +1 -42
- browsergym/workarena/data_files/setup_files/lists/expected_incident_list_columns.json +2 -18
- browsergym/workarena/data_files/setup_files/lists/expected_problem_list_columns.json +12 -0
- browsergym/workarena/data_files/setup_files/lists/expected_requested_items_list_columns.json +12 -0
- browsergym/workarena/data_files/setup_files/lists/expected_service_catalog_list_columns.json +2 -19
- browsergym/workarena/data_files/setup_files/lists/expected_user_list_columns.json +3 -50
- browsergym/workarena/data_files/task_configs/all_menu.json +95 -95
- browsergym/workarena/data_files/task_configs/dashboard_retrieval_minmax_task.json +1 -1
- browsergym/workarena/data_files/task_configs/dashboard_retrieval_value_task.json +1 -1
- browsergym/workarena/data_files/task_configs/filter_service_catalog_item_list_task.json +7986 -7982
- browsergym/workarena/data_files/task_configs/impersonation_users.json +3 -3
- 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/human_eval/console.js +176 -0
- browsergym/workarena/human_eval/tool.py +366 -0
- browsergym/workarena/install.py +81 -20
- browsergym/workarena/tasks/base.py +55 -20
- browsergym/workarena/tasks/comp_building_block.py +4 -0
- browsergym/workarena/tasks/compositional/__init__.py +76 -0
- browsergym/workarena/tasks/compositional/base.py +364 -0
- browsergym/workarena/tasks/compositional/dash_do_base.py +1366 -0
- browsergym/workarena/tasks/compositional/dash_do_catalog.py +1127 -0
- browsergym/workarena/tasks/compositional/dash_do_catalog_infeasible.py +2047 -0
- browsergym/workarena/tasks/compositional/dash_do_create_incident.py +403 -0
- browsergym/workarena/tasks/compositional/dash_do_create_incident_infeasible.py +278 -0
- browsergym/workarena/tasks/compositional/dash_do_create_problem.py +336 -0
- browsergym/workarena/tasks/compositional/dash_do_create_problem_infeasible.py +235 -0
- browsergym/workarena/tasks/compositional/dash_do_filter.py +1600 -0
- browsergym/workarena/tasks/compositional/dash_do_request_item.py +1315 -0
- browsergym/workarena/tasks/compositional/dash_do_request_item_infeasible.py +693 -0
- browsergym/workarena/tasks/compositional/delete_record.py +341 -0
- browsergym/workarena/tasks/compositional/edit_knowledge_base.py +457 -0
- browsergym/workarena/tasks/compositional/expense_management.py +598 -0
- browsergym/workarena/tasks/compositional/filter_and_do.py +139 -0
- browsergym/workarena/tasks/compositional/find_and_order_item.py +345 -0
- browsergym/workarena/tasks/compositional/manage_change_request_schedule.py +1417 -0
- browsergym/workarena/tasks/compositional/mark_duplicate_problems.py +499 -0
- browsergym/workarena/tasks/compositional/maximize_investment_return.py +1763 -0
- browsergym/workarena/tasks/compositional/navigate_and_do.py +1151 -0
- browsergym/workarena/tasks/compositional/navigate_and_do_infeasible.py +2100 -0
- browsergym/workarena/tasks/compositional/offboard_user.py +207 -0
- browsergym/workarena/tasks/compositional/onboard_user.py +226 -0
- browsergym/workarena/tasks/compositional/update_task.py +145 -0
- browsergym/workarena/tasks/compositional/utils/curriculum.py +215 -0
- browsergym/workarena/tasks/compositional/utils/infeasible_configs.py +151 -0
- browsergym/workarena/tasks/compositional/utils/knapsack.py +192 -0
- browsergym/workarena/tasks/compositional/warranty_check.py +227 -0
- browsergym/workarena/tasks/compositional/work_assignment.py +804 -0
- browsergym/workarena/tasks/compositional/workload_balancing.py +396 -0
- browsergym/workarena/tasks/dashboard.py +188 -8
- browsergym/workarena/tasks/form.py +1024 -232
- browsergym/workarena/tasks/knowledge.py +216 -25
- browsergym/workarena/tasks/list.py +519 -102
- browsergym/workarena/tasks/mark_duplicate_problem.py +171 -0
- browsergym/workarena/tasks/navigation.py +55 -13
- browsergym/workarena/tasks/scripts/extract_all_menu_items.py +9 -2
- browsergym/workarena/tasks/scripts/generate_dashboard_configs.py +6 -5
- browsergym/workarena/tasks/scripts/service_catalog.py +2 -1
- browsergym/workarena/tasks/scripts/validate.py +8 -2
- browsergym/workarena/tasks/send_chat_message.py +90 -0
- browsergym/workarena/tasks/service_catalog.py +94 -26
- browsergym/workarena/tasks/utils/form.py +1 -4
- browsergym/workarena/tasks/utils/private_tasks.py +63 -0
- browsergym/workarena/tasks/utils/utils.py +13 -0
- {browsergym_workarena-0.2.0.dist-info → browsergym_workarena-0.3.0.dist-info}/METADATA +27 -20
- browsergym_workarena-0.3.0.dist-info/RECORD +138 -0
- {browsergym_workarena-0.2.0.dist-info → browsergym_workarena-0.3.0.dist-info}/entry_points.txt +1 -0
- browsergym_workarena-0.2.0.dist-info/RECORD +0 -85
- {browsergym_workarena-0.2.0.dist-info → browsergym_workarena-0.3.0.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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 =
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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(
|
|
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 =
|
|
412
|
-
sort_dir =
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
668
|
-
|
|
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
|
|
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
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
|
|
1010
|
-
|
|
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
|
+
)
|