browsergym-workarena 0.2.1__py3-none-any.whl → 0.3.1__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 +1 -1
  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 +1 -1
  32. browsergym/workarena/data_files/task_configs/impersonation_users.json +1 -1
  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 +194 -12
  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.1.dist-info → browsergym_workarena-0.3.1.dist-info}/METADATA +19 -18
  87. browsergym_workarena-0.3.1.dist-info/RECORD +138 -0
  88. {browsergym_workarena-0.2.1.dist-info → browsergym_workarena-0.3.1.dist-info}/entry_points.txt +1 -0
  89. browsergym_workarena-0.2.1.dist-info/RECORD +0 -85
  90. {browsergym_workarena-0.2.1.dist-info → browsergym_workarena-0.3.1.dist-info}/WHEEL +0 -0
  91. {browsergym_workarena-0.2.1.dist-info → browsergym_workarena-0.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,341 @@
1
+ import faker
2
+
3
+ faker = faker.Faker()
4
+ import json
5
+
6
+ from playwright.sync_api import Page
7
+ from typing import List, Tuple
8
+
9
+ from .base import AbstractServiceNowTask
10
+
11
+ from ..utils.utils import check_url_suffix_match
12
+
13
+ from ...api.utils import db_delete_from_table, table_api_call
14
+
15
+
16
+ class DeleteRecordTask(AbstractServiceNowTask):
17
+ """
18
+ Delete a record from a list.
19
+
20
+ Parameters:
21
+ -----------
22
+ instance: SNowInstance
23
+ The instance to use.
24
+ start_rel_url: str
25
+ The relative URL of the list containing the record to delete.
26
+ list_name: str
27
+ The displayed name of the list containing the record to delete.
28
+ fixed_config: dict
29
+ Configuration to use for the task. If provided, the task will use the provided configuration instead of
30
+ selecting a random one. See browsergym/workarena/data_files/task_configs/filter_change_request_list_task.json
31
+ for an example of a configuration file.
32
+ all_configs: list[dict]
33
+ A list of all possible configurations to use for the task.
34
+ record_sys_id: str
35
+ The sys_id of the record to delete. If not provided, a record will be created during the setup.
36
+ record_number: str
37
+ The number of the record to delete; used in the cheat. If not provided, the cheat will select the last one.
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ seed: int = None,
43
+ instance=None,
44
+ start_rel_url: str = "",
45
+ list_name: str = "",
46
+ fixed_config: dict = None,
47
+ all_configs: list[dict] = None,
48
+ record_sys_id: str = None,
49
+ record_number: str = None,
50
+ **kwargs,
51
+ ) -> None:
52
+ super().__init__(seed=seed, instance=instance, start_rel_url=start_rel_url)
53
+ self.list_name = list_name
54
+ self.table_name = start_rel_url.split("/")[-1].split("_list.do")[0]
55
+ self.fixed_config = fixed_config
56
+ self.config = None
57
+ self.pretty_printed_field_name = None
58
+ self.field_name = None
59
+ self.field_value = None
60
+ self.other_fields = None
61
+ self.all_configs = all_configs
62
+ # If the record_sys_id is not provided, it will be created during the setup
63
+ self.record_sys_id = record_sys_id
64
+ self.record_number = record_number
65
+ self.__dict__.update(kwargs)
66
+
67
+ def setup_goal(self, page: Page) -> tuple[str, dict]:
68
+ self.config = (
69
+ self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
70
+ )
71
+ self.field_name = self.config.get("field_name")
72
+ self.pretty_printed_field_name = self.config.get("pretty_printed_field_name")
73
+ self.field_value = self.config.get("field_value")
74
+ self.other_fields = self.config.get("other_fields")
75
+ if self.record_sys_id is None:
76
+ # First, check if the record already exists
77
+ record = table_api_call(
78
+ instance=self.instance,
79
+ table=self.table_name,
80
+ params={
81
+ "sysparm_query": f"{self.field_name}={self.field_value}",
82
+ "sysparm_fields": "sys_id",
83
+ },
84
+ )["result"]
85
+ if len(record) > 0:
86
+ raise ValueError(
87
+ f"Record already with {self.field_name} = {self.field_value} exists. Please delete it before proceeding."
88
+ )
89
+
90
+ self.record_sys_id = table_api_call(
91
+ instance=self.instance,
92
+ table=self.table_name,
93
+ data=json.dumps(
94
+ {
95
+ self.field_name: self.field_value,
96
+ **self.other_fields,
97
+ }
98
+ ),
99
+ method="POST",
100
+ )["result"]["sys_id"]
101
+
102
+ goal = self.get_pretty_printed_description()
103
+
104
+ return goal, {}
105
+
106
+ def get_init_scripts(self) -> List[str]:
107
+ return super().get_init_scripts() + ["registerGsftMainLoaded();"]
108
+
109
+ def get_pretty_printed_description(self) -> str:
110
+ """
111
+ Get the task info for this task when used in a private task; Used in L3 compositional tasks.
112
+ called by subclasses
113
+ """
114
+ task_info = f"- Delete the record with {self.pretty_printed_field_name}={self.field_value} from the {self.list_name} list."
115
+
116
+ return task_info
117
+
118
+ def cheat(self, page: Page, chat_messages: list[str]) -> None:
119
+ super().cheat(page, chat_messages)
120
+ frame = page.wait_for_selector('iframe[name="gsft_main"]').content_frame()
121
+
122
+ # If the record number is provided, click on the record with that number...
123
+ if self.record_number is not None:
124
+ frame.locator(f"[aria-label='Preview record: {self.record_number}']").click()
125
+ page.wait_for_timeout(500)
126
+ frame.get_by_text("Open Record").click()
127
+ # ....Otherwise, otherwise filter the list and click on the record
128
+ else:
129
+ # Search for the record
130
+ frame.get_by_label(
131
+ f"Search a specific field of the {self.list_name} list"
132
+ ).select_option(f"{self.field_name}")
133
+ search_input = frame.locator('input[aria-label="Search"]')
134
+ search_input.click()
135
+ search_input.fill(self.field_value)
136
+ search_input.press("Enter")
137
+ page.wait_for_function(
138
+ "typeof window.gsft_main !== 'undefined' && window.gsft_main.WORKARENA_LOAD_COMPLETE"
139
+ )
140
+ # Click on the record to open it
141
+ # The first 2 displays of the record are in the search bar; the 3rd and last will be the link to open it
142
+ frame.get_by_label(self.field_value).last.click()
143
+
144
+ page.wait_for_function(
145
+ "typeof window.gsft_main !== 'undefined' && window.gsft_main.WORKARENA_LOAD_COMPLETE"
146
+ )
147
+ frame = page.wait_for_selector('iframe[name="gsft_main"]').content_frame()
148
+ # Click on delete, then confirm delete in the popup
149
+ frame.get_by_text("delete").first.click()
150
+ frame.wait_for_selector('header[aria-label="Confirmation"]')
151
+ page.keyboard.press("Enter")
152
+ # Wait for record to be updated in the DB
153
+ record_deleted = False
154
+ while not record_deleted:
155
+ record = table_api_call(
156
+ instance=self.instance,
157
+ table=self.table_name,
158
+ params={
159
+ "sysparm_query": f"{self.field_name}={self.field_value}",
160
+ "sysparm_fields": "sys_id",
161
+ },
162
+ )["result"]
163
+ record_deleted = len(record) == 0
164
+ page.wait_for_timeout(3000)
165
+
166
+ def validate(self, page: Page, chat_messages: list[str]) -> Tuple[float, bool, str, dict]:
167
+ """
168
+ Validate the solution
169
+ """
170
+ record = table_api_call(
171
+ instance=self.instance,
172
+ table=self.table_name,
173
+ params={"sysparm_query": f"{self.field_name}={self.field_value}"},
174
+ )["result"]
175
+ if len(record) > 0:
176
+ return 0, False, "", {"message": "Record was not deleted."}
177
+
178
+ return 1, True, "Nice work, thank you!", {"message": "Record was deleted successfully."}
179
+
180
+ def teardown(self) -> None:
181
+ super().teardown()
182
+ result = table_api_call(
183
+ instance=self.instance,
184
+ table=self.table_name,
185
+ params={
186
+ "sysparm_query": f"{self.field_name}={self.field_value}",
187
+ "sysparm_fields": "sys_id",
188
+ },
189
+ )
190
+ if len(result["result"]) > 0:
191
+ db_delete_from_table(
192
+ instance=self.instance,
193
+ table=self.table_name,
194
+ sys_id=self.record_sys_id,
195
+ )
196
+
197
+
198
+ class DeleteUserTask(DeleteRecordTask):
199
+ def __init__(self, instance=None, fixed_config=None, record_sys_id=None, **kwargs) -> None:
200
+ super().__init__(
201
+ instance=instance,
202
+ start_rel_url="/now/nav/ui/classic/params/target/sys_user_list.do",
203
+ list_name="Users",
204
+ fixed_config=fixed_config,
205
+ record_sys_id=record_sys_id,
206
+ **kwargs,
207
+ )
208
+ if fixed_config is None:
209
+ first_name = faker.first_name()
210
+ last_name = faker.last_name()
211
+ email = first_name.lower() + "." + last_name.lower() + "@workarena.com"
212
+ self.fixed_config = {
213
+ "field_name": "user_name",
214
+ "pretty_printed_field_name": "User ID",
215
+ "field_value": first_name + " " + last_name,
216
+ "other_fields": {"email": email},
217
+ }
218
+
219
+
220
+ class DeleteExpenseLineExpenseManagementTask(DeleteRecordTask):
221
+ """
222
+ Delete one row from the expense lines list
223
+
224
+ Args:
225
+ --------
226
+ goal_type (str):
227
+ The type of goal to generate. Choice of "base", "date", "amount", "any"
228
+ level (int):
229
+ The level of the task
230
+ skip_description (bool):
231
+ Whether to skip the description of the task
232
+
233
+ """
234
+
235
+ def __init__(
236
+ self,
237
+ instance=None,
238
+ fixed_config=None,
239
+ record_sys_id=None,
240
+ goal_type="base",
241
+ level=2,
242
+ skip_description=False,
243
+ **kwargs,
244
+ ) -> None:
245
+ super().__init__(
246
+ instance=instance,
247
+ start_rel_url="/now/nav/ui/classic/params/target/fm_expense_line_list.do",
248
+ list_name="Expense Lines",
249
+ fixed_config=fixed_config,
250
+ record_sys_id=record_sys_id,
251
+ **kwargs,
252
+ )
253
+ self.goal_type = goal_type
254
+ self.level = level
255
+ self.skip_description = skip_description
256
+
257
+ def get_pretty_printed_description(self) -> str:
258
+ """
259
+ Get the task info for this task when used in a private task; Used in compositional tasks.
260
+ called by subclasses
261
+ """
262
+ task_info = f"Delete expense lines with duplicated short descriptions"
263
+ if self.skip_description:
264
+ task_info = ""
265
+ elif self.level == 3:
266
+ task_info += f" according to the protocol."
267
+ elif self.goal_type == "base":
268
+ task_info += f" where the duplicated expense lines are not associated with tasks."
269
+ elif self.goal_type == "date":
270
+ task_info += f", keeping only the one that has the oldest date."
271
+ elif self.goal_type == "amount":
272
+ task_info += f", keeping only the most expensive duplicate."
273
+ elif self.goal_type == "any":
274
+ task_info += f", keeping only one."
275
+
276
+ return task_info
277
+
278
+
279
+ class DeleteExpenseLineKnapsack(DeleteRecordTask):
280
+ """
281
+ Delete one row from the expense lines list
282
+
283
+ Args:
284
+ --------
285
+ goal_type (str):
286
+ The type of goal to generate. Choice of "base", "date", "amount", "any"
287
+ answer_format (str):
288
+ The type of answer to generate. Choice of total_return_only, total_return_and_investments, investments_only, cleanup, cleanup_and_return
289
+ level (int):
290
+ The level of the task
291
+ skip_description (bool):
292
+ Whether to skip the description of the task
293
+
294
+ """
295
+
296
+ def __init__(
297
+ self,
298
+ instance=None,
299
+ fixed_config=None,
300
+ record_sys_id=None,
301
+ goal_type="base",
302
+ level=2,
303
+ answer_format=None,
304
+ skip_description=False,
305
+ **kwargs,
306
+ ) -> None:
307
+ super().__init__(
308
+ instance=instance,
309
+ start_rel_url="/now/nav/ui/classic/params/target/fm_expense_line_list.do",
310
+ list_name="Expense Lines",
311
+ fixed_config=fixed_config,
312
+ record_sys_id=record_sys_id,
313
+ **kwargs,
314
+ )
315
+ self.goal_type = goal_type
316
+ self.level = level
317
+ self.answer_format = answer_format
318
+ self.skip_description = skip_description
319
+
320
+ def get_pretty_printed_description(self) -> str:
321
+ if self.skip_description:
322
+ return ""
323
+ if self.level == 3:
324
+ task_info = "Allocate the budget to maximize revenue."
325
+ elif self.level == 2:
326
+ task_info = f"Allocate the budget to maximize revenue. This involves going over expense lines and identifying the ones maximizing revenue while fitting in the allowed budget of {self.budget}. The returns are written in their short description."
327
+ if self.answer_format == "total_return_only":
328
+ task_info += " Provide only the total return of the investments in the chat."
329
+ if self.answer_format == "total_return_and_investments":
330
+ task_info += " Provide the total return of the investments as well as the number of the investments in the chat."
331
+ if self.answer_format == "investments_only":
332
+ task_info += " Provide only the numbers of the investments in the chat."
333
+ if self.answer_format == "cleanup":
334
+ task_info += " Delete the investments that will not be kept so that only the selected investments remain."
335
+ if self.answer_format == "cleanup_and_return":
336
+ task_info += " Delete the investments that will not be kept so that only the selected investments remain as well as returning their total value in the chat."
337
+
338
+ return task_info
339
+
340
+
341
+ __TASKS__ = [DeleteUserTask]