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
@@ -0,0 +1,804 @@
1
+ from typing import Tuple
2
+ from faker import Faker
3
+ import random
4
+
5
+ fake = Faker()
6
+
7
+ from playwright.sync_api._generated import Page
8
+
9
+ from .base import CompositionalTask, HumanEvalTask
10
+
11
+ from ...api.incident import create_incident
12
+ from ...api.user import create_user
13
+ from ...api.utils import table_api_call, db_delete_from_table
14
+ from ..base import AbstractServiceNowTask
15
+ from ..list import FilterIncidentListTask
16
+ from ..form import EditIncidentTask
17
+ from ..knowledge import KnowledgeBaseSearchTask
18
+ from ..navigation import AllMenuTask
19
+
20
+ from ...instance import SNowInstance
21
+
22
+
23
+ class WorkAssignmentTask(CompositionalTask):
24
+ def __init__(
25
+ self,
26
+ instance: SNowInstance = None,
27
+ fixed_config: list[AbstractServiceNowTask] = None,
28
+ level: int = 2,
29
+ max_experts_per_category: int = 2,
30
+ max_assignments: int = None,
31
+ min_assignments: int = None,
32
+ num_categories: int = None,
33
+ seed: int = None,
34
+ prefix: str = None,
35
+ ) -> None:
36
+ """
37
+ Create a compositional task with specific subtasks
38
+
39
+ Parameters:
40
+ -----------
41
+ instance: SNowInstance
42
+ The ServiceNow instance to run the task on.
43
+ fixed_config: list[tuple[AbstractServiceNowTask, dict, bool]]
44
+ A list of tuples, each containing a subtask, its configuration and whether or not it should be validated.
45
+ level: int
46
+ The level of the task; choice between 2 and 3. L2 will have all the info in the the goal and start in the SNOW home page.
47
+ L3 will start in a private task page describing the information needed to complete the task and the related company protocol
48
+ to complete it.
49
+ max_experts_per_category: int
50
+ How many maximum new agents to create for each category.
51
+ max_assignments: int
52
+ Maximum number of incidents created to be assigned.
53
+ For a task, the number is randomly sampled between max_assignments and min_assignments.
54
+ max_assignments: int
55
+ Minimum number of incidents created to be assigned.
56
+ For a task, the number is randomly sampled between max_assignments and min_assignments.
57
+ prefix: str
58
+ Prefix to name the incidents created with a unique prefix
59
+ Attributes:
60
+ -----------
61
+ task_description: str
62
+ The start of the task description to be completed. e.g. "Referring to company protocol 'Work Assignment', assign incidents to different agents with the following information: \n"
63
+ short_description: str
64
+ A short description of the task to be completed. e.g. "Assign task to relevant expert agents"
65
+ """
66
+ assert level in [2, 3], "Level must be either 2 or 3"
67
+ self.level = level
68
+ self.protocol_name = "Work Assignment: Assign Incidents to Relevant Agents"
69
+ super().__init__(
70
+ instance=instance,
71
+ fixed_config=fixed_config,
72
+ level=level,
73
+ protocol_name=self.protocol_name,
74
+ seed=seed,
75
+ )
76
+
77
+ self.task_description = None
78
+ self.short_description = f"Assign work to relevant agents"
79
+ self.max_experts_per_category = max_experts_per_category
80
+ self.max_assignments = max_assignments
81
+ self.min_assignments = min_assignments
82
+ self.num_categories = num_categories
83
+ if self.num_categories > 4 or self.num_categories < 1:
84
+ raise Exception("Should have at least 1 and at most 4 categories.")
85
+ self.prefix = prefix
86
+
87
+ def setup_goal(self, page: Page) -> tuple[str, dict]:
88
+ self.incident_configs = []
89
+ number_assignments = self.random.randint(self.min_assignments, self.max_assignments)
90
+
91
+ all_existing_incidents = table_api_call(
92
+ instance=self.instance, table="incident", method="GET"
93
+ )["result"]
94
+ all_incident_numbers = [incident["number"] for incident in all_existing_incidents]
95
+ new_incident_numbers = []
96
+ for _ in range(number_assignments):
97
+ incident_number = (
98
+ self.prefix + str(id(self) % (10**8)).zfill(8)[:4] + str(random.randint(100, 999))
99
+ )
100
+ while (
101
+ incident_number in all_incident_numbers or incident_number in new_incident_numbers
102
+ ):
103
+ incident_number = (
104
+ self.prefix
105
+ + str(id(self) % (10**8)).zfill(8)[:4]
106
+ + str(random.randint(100, 999))
107
+ )
108
+ new_incident_numbers.append(incident_number)
109
+
110
+ self.active_categories = self.random.choice(
111
+ ["hardware", "software", "network", "database"], self.num_categories, replace=False
112
+ )
113
+ for incident_number in new_incident_numbers:
114
+ ### We can reduce the categories here if the setup takes too long
115
+ category = self.random.choice(self.active_categories)
116
+ incident_response = create_incident(
117
+ instance=self.instance,
118
+ incident_number=incident_number,
119
+ caller_sys_id=self._base_user_sysid,
120
+ category=category,
121
+ priority=4,
122
+ impact=2, # priority is calculated as some combination of impact and urgency
123
+ urgency=3,
124
+ )
125
+ self.incident_configs.append(incident_response)
126
+
127
+ self.experts = dict({category: [] for category in self.active_categories})
128
+ for _ in range(self.max_experts_per_category):
129
+ for category in self.active_categories:
130
+ self.experts[category].append(
131
+ create_user(
132
+ instance=self.instance,
133
+ first_name=f"{fake.first_name()}-{fake.first_name()}",
134
+ last_name=f"{fake.last_name()}-{fake.last_name()}",
135
+ return_full_response=True,
136
+ user_roles=["itil"],
137
+ random=self.random,
138
+ )
139
+ )
140
+ expert_string = ""
141
+ for category in self.active_categories:
142
+ category_experts = ", ".join(
143
+ expert["first_name"] + " " + expert["last_name"]
144
+ for expert in self.experts[category]
145
+ )
146
+ expert_string += f"{category.capitalize()} agents: {category_experts} \n"
147
+ incident_numbers = ", ".join(new_incident_numbers)
148
+
149
+ # Get the task description
150
+ self.task_description = (
151
+ f'Referring to company protocol "{self.protocol_name}" (located in the "Company Protocols" knowledge base) assign work to the agents with the following information: \n'
152
+ + f"Incidents to assign: {incident_numbers} \n\n"
153
+ + f"{expert_string}"
154
+ )
155
+
156
+ # Sample a configuration
157
+ config = self.fixed_config if self.fixed_config else self._get_config()
158
+
159
+ goal, info = super().setup_goal(page=page, config=config)
160
+
161
+ if self.level == 2:
162
+ goal = (
163
+ self.short_description
164
+ + f"\n1. Navigate to the Service Desk > Incidents. \n"
165
+ + f"\n2. You have to assign the following incidents to relevant agents: {incident_numbers}. You can filter the list using each incident number and use the 'Assigned to' field to assign an incident.\n"
166
+ + f"\n3. You have to ensure that each incident is assigned to a relevant agent based on the category of the incident.\n"
167
+ + f"\nThe category wise agents are as follows. You can assign an incident to ANY agent from the category:\n"
168
+ + f"{expert_string}"
169
+ )
170
+
171
+ return goal, info
172
+
173
+ def _get_config(self) -> list[tuple[AbstractServiceNowTask, dict, bool]]:
174
+
175
+ navigate_to_protocol_subtask = [
176
+ # Navigate to the KB
177
+ AllMenuTask(
178
+ instance=self.instance,
179
+ fixed_config={
180
+ "application": "Self-Service",
181
+ "module": "Knowledge",
182
+ "url": "/now/nav/ui/classic/params/target/%24knowledge.do",
183
+ },
184
+ is_validated=False,
185
+ used_in_level_2=False,
186
+ ),
187
+ # Find the protocol for on-boarding a new user
188
+ KnowledgeBaseSearchTask(
189
+ instance=self.instance,
190
+ fixed_config={
191
+ "alternative_answers": [],
192
+ "item": f"{self.protocol_name}",
193
+ "question": "Can you find the Work Assignment Protocol in the Knowledge Base?",
194
+ "value": "",
195
+ },
196
+ is_validated=False,
197
+ used_in_level_2=False,
198
+ ),
199
+ ]
200
+
201
+ all_incident_assignments = []
202
+
203
+ for incident_config in self.incident_configs:
204
+ assigned_to = self.random.choice(self.experts[incident_config["category"]])
205
+ assigned_to = assigned_to["first_name"] + " " + assigned_to["last_name"]
206
+ assign_incidents_subtask = [
207
+ # Navigate to the incidents list
208
+ AllMenuTask(
209
+ instance=self.instance,
210
+ fixed_config={
211
+ "application": "Service Desk",
212
+ "module": "Incidents",
213
+ "url": "/now/nav/ui/classic/params/target/incident_list.do",
214
+ },
215
+ is_validated=False,
216
+ used_in_level_2=True,
217
+ ),
218
+ # Filter incident
219
+ FilterIncidentListTask(
220
+ instance=self.instance,
221
+ fixed_config={
222
+ "filter_columns": [
223
+ "number",
224
+ ],
225
+ "filter_kind": "AND",
226
+ "filter_values": [
227
+ incident_config["number"],
228
+ ],
229
+ },
230
+ is_validated=False,
231
+ used_in_level_2=True,
232
+ ),
233
+ # Edit incident
234
+ EditIncidentTask(
235
+ instance=self.instance,
236
+ # fixed_config=incident_config,
237
+ new_values={"assigned_to": assigned_to},
238
+ is_validated=False,
239
+ used_in_level_2=True,
240
+ record_sys_id=incident_config["sys_id"],
241
+ level=self.level,
242
+ ),
243
+ ]
244
+ all_incident_assignments.extend(assign_incidents_subtask)
245
+
246
+ config = navigate_to_protocol_subtask + all_incident_assignments
247
+
248
+ return config
249
+
250
+ def validate(self, page: Page, chat_messages: list[str]) -> Tuple[float, bool, str, dict]:
251
+ experts_sys_ids = {
252
+ category: [expert["sys_id"] for expert in self.experts[category]]
253
+ for category in self.experts
254
+ }
255
+ for incident_config in self.incident_configs:
256
+ incident_response = table_api_call(
257
+ instance=self.instance,
258
+ table="incident",
259
+ params={
260
+ "sysparm_query": f"sys_id={incident_config['sys_id']}",
261
+ "sysparm_fields": "category,assigned_to",
262
+ },
263
+ method="GET",
264
+ )["result"][0]
265
+ if incident_response["category"] != incident_config["category"]:
266
+ raise Exception("Corrupted incident data")
267
+ if not incident_response["assigned_to"]:
268
+ return (
269
+ 0,
270
+ False,
271
+ "",
272
+ {
273
+ "message": f"The incident {incident_config['number']} has not been assigned to anyone."
274
+ },
275
+ )
276
+ if (
277
+ incident_response["assigned_to"]["value"]
278
+ not in experts_sys_ids[incident_response["category"]]
279
+ ):
280
+ return (
281
+ 0,
282
+ False,
283
+ "",
284
+ {
285
+ "message": f"The incident {incident_config['number']} was assigned to an incorrect expert."
286
+ },
287
+ )
288
+ # Validate final_l3 tasks
289
+ reward, done, message, info = super().validate(page, chat_messages)
290
+ return reward, done, message, info
291
+
292
+ def teardown(self) -> None:
293
+ for incident in self.incident_configs:
294
+ db_delete_from_table(
295
+ instance=self.instance, table="incident", sys_id=incident["sys_id"]
296
+ )
297
+
298
+ for experts in self.experts.values():
299
+ for expert in experts:
300
+ db_delete_from_table(
301
+ instance=self.instance, table="sys_user", sys_id=expert["sys_id"]
302
+ )
303
+
304
+ return super().teardown()
305
+
306
+
307
+ class WorkAssignmentSmallTask(WorkAssignmentTask, HumanEvalTask):
308
+ def __init__(
309
+ self,
310
+ instance: SNowInstance = None,
311
+ seed: int = None,
312
+ fixed_config: list[AbstractServiceNowTask] = None,
313
+ level: int = 2,
314
+ ) -> None:
315
+ """
316
+ Small version of workassignment task.
317
+ """
318
+ super().__init__(
319
+ instance=instance,
320
+ level=level,
321
+ max_experts_per_category=2,
322
+ max_assignments=4,
323
+ min_assignments=3,
324
+ num_categories=2,
325
+ fixed_config=fixed_config,
326
+ seed=seed,
327
+ prefix="WAS",
328
+ )
329
+
330
+
331
+ class WorkAssignmentMediumTask(WorkAssignmentTask):
332
+ def __init__(
333
+ self,
334
+ instance: SNowInstance = None,
335
+ seed: int = None,
336
+ fixed_config: list[AbstractServiceNowTask] = None,
337
+ level: int = 2,
338
+ ) -> None:
339
+ """
340
+ Medium version of workassignment task.
341
+ """
342
+ super().__init__(
343
+ instance=instance,
344
+ level=level,
345
+ max_experts_per_category=2,
346
+ max_assignments=6,
347
+ min_assignments=5,
348
+ num_categories=3,
349
+ fixed_config=fixed_config,
350
+ seed=seed,
351
+ prefix="WAM",
352
+ )
353
+
354
+
355
+ class WorkAssignmentLargeTask(WorkAssignmentTask):
356
+ def __init__(
357
+ self,
358
+ instance: SNowInstance = None,
359
+ seed: int = None,
360
+ fixed_config: list[AbstractServiceNowTask] = None,
361
+ level: int = 2,
362
+ ) -> None:
363
+ """
364
+ Large version of workassignment task.
365
+ """
366
+ super().__init__(
367
+ instance=instance,
368
+ level=level,
369
+ max_experts_per_category=2,
370
+ max_assignments=8,
371
+ min_assignments=7,
372
+ num_categories=4,
373
+ fixed_config=fixed_config,
374
+ seed=seed,
375
+ prefix="WAL",
376
+ )
377
+
378
+
379
+ class PriorityAssignmentTask(CompositionalTask):
380
+ def __init__(
381
+ self,
382
+ instance: SNowInstance = None,
383
+ fixed_config: list[AbstractServiceNowTask] = None,
384
+ level: int = 2,
385
+ max_tasks_per_priority: int = 2,
386
+ min_tasks_per_priority: int = 1,
387
+ num_categories: int = None,
388
+ seed: int = None,
389
+ prefix: str = None,
390
+ ) -> None:
391
+ """
392
+ Create a compositional task with specific subtasks
393
+
394
+ Parameters:
395
+ -----------
396
+ instance: SNowInstance
397
+ The ServiceNow instance to run the task on.
398
+ fixed_config: list[tuple[AbstractServiceNowTask, dict, bool]]
399
+ A list of tuples, each containing a subtask, its configuration and whether or not it should be validated.
400
+ level: int
401
+ The level of the task; choice between 2 and 3. L2 will have all the info in the the goal and start in the SNOW home page.
402
+ L3 will start in a private task page describing the information needed to complete the task and the related company protocol
403
+ to complete it.
404
+ experts_per_category: int
405
+ How many new agents to create for each category.
406
+ max_assignments: int
407
+ Maximum number of incidents created to be assigned.
408
+ For a task, the number is randomly sampled between max_assignments and min_assignments.
409
+ max_assignments: int
410
+ Minimum number of incidents created to be assigned.
411
+ For a task, the number is randomly sampled between max_assignments and min_assignments.
412
+ prefix: str
413
+ Prefix to name the incidents created with a unique prefix
414
+ Attributes:
415
+ -----------
416
+ task_description: str
417
+ The start of the task description to be completed. e.g. "Referring to company protocol 'Priority Assignment', assign incidents to different agents in terms of priority with the following information: \n"
418
+ short_description: str
419
+ A short description of the task to be completed. e.g. "Assign task to relevant expert agents based on the incident priorities"
420
+ """
421
+ assert level in [2, 3], "Level must be either 2 or 3"
422
+ self.level = level
423
+ self.protocol_name = "Work Assignment: Assign Incidents to Relevant Agents"
424
+ super().__init__(
425
+ instance=instance,
426
+ fixed_config=fixed_config,
427
+ level=level,
428
+ protocol_name=self.protocol_name,
429
+ seed=seed,
430
+ )
431
+
432
+ self.task_description = None
433
+ self.short_description = None
434
+ self.experts_per_category = 3 # We divide agents into 'expert', 'supporter', and 'planner'
435
+ self.max_tasks_per_priority = max_tasks_per_priority
436
+ self.min_tasks_per_priority = min_tasks_per_priority
437
+ # Priority 1 is urgent, 3 is moderate, 5 is planning.
438
+ # Also priority depends on impact and urgency rather than being an independent attribute
439
+ self.priorities = {
440
+ 1: {
441
+ "impact": 1,
442
+ "urgency": 1,
443
+ "num_incidents": self.random.randint(
444
+ self.min_tasks_per_priority, self.max_tasks_per_priority
445
+ ),
446
+ "agent_type": "expert",
447
+ },
448
+ 3: {
449
+ "impact": 2,
450
+ "urgency": 2,
451
+ "num_incidents": self.random.randint(
452
+ self.min_tasks_per_priority, self.max_tasks_per_priority
453
+ ),
454
+ "agent_type": "supporter",
455
+ },
456
+ 5: {
457
+ "impact": 3,
458
+ "urgency": 3,
459
+ "num_incidents": self.random.randint(
460
+ self.min_tasks_per_priority, self.max_tasks_per_priority
461
+ ),
462
+ "agent_type": "planner",
463
+ },
464
+ }
465
+ self.num_categories = num_categories
466
+ if self.num_categories > 4 or self.num_categories < 1:
467
+ raise Exception("Should have at least 1 and at most 4 categories.")
468
+ self.prefix = prefix
469
+
470
+ def setup_goal(self, page: Page) -> tuple[str, dict]:
471
+ self.incident_configs = []
472
+ number_assignments = sum(
473
+ [attribute["num_incidents"] for attribute in self.priorities.values()]
474
+ )
475
+
476
+ all_existing_incidents = table_api_call(
477
+ instance=self.instance, table="incident", method="GET"
478
+ )["result"]
479
+ all_incident_numbers = [incident["number"] for incident in all_existing_incidents]
480
+
481
+ new_incident_numbers = []
482
+ for _ in range(number_assignments):
483
+ incident_number = (
484
+ self.prefix + str(id(self) % (10**8)).zfill(8)[:4] + str(random.randint(100, 999))
485
+ )
486
+ while (
487
+ incident_number in all_incident_numbers or incident_number in new_incident_numbers
488
+ ):
489
+ incident_number = (
490
+ self.prefix
491
+ + str(id(self) % (10**8)).zfill(8)[:4]
492
+ + str(random.randint(100, 999))
493
+ )
494
+ new_incident_numbers.append(incident_number)
495
+ incident_category = []
496
+ self.active_categories = self.random.choice(
497
+ ["hardware", "software", "network", "database"], self.num_categories, replace=False
498
+ )
499
+ incident_number_idx = 0
500
+ for priority, attributes in self.priorities.items():
501
+ for _ in range(attributes["num_incidents"]):
502
+ category = self.random.choice(self.active_categories)
503
+ incident_response = create_incident(
504
+ instance=self.instance,
505
+ incident_number=new_incident_numbers[incident_number_idx],
506
+ caller_sys_id=self._base_user_sysid,
507
+ category=category,
508
+ priority=priority,
509
+ impact=attributes[
510
+ "impact"
511
+ ], # priority is calculated as some combination of impact and urgency
512
+ urgency=attributes["urgency"],
513
+ )
514
+ self.incident_configs.append(incident_response)
515
+ incident_category.append(
516
+ [new_incident_numbers[incident_number_idx], category, priority]
517
+ )
518
+ incident_number_idx += 1
519
+
520
+ self.agents_per_category = dict({category: {} for category in self.active_categories})
521
+ for category in self.agents_per_category:
522
+ self.agents_per_category[category]["expert"] = create_user(
523
+ instance=self.instance,
524
+ first_name=f"{fake.first_name()}-{fake.first_name()}",
525
+ last_name=f"{fake.last_name()}-{fake.last_name()}",
526
+ return_full_response=True,
527
+ user_roles=["itil"],
528
+ random=self.random,
529
+ )
530
+ self.agents_per_category[category]["expert"]["full_name"] = (
531
+ self.agents_per_category[category]["expert"]["first_name"]
532
+ + " "
533
+ + self.agents_per_category[category]["expert"]["last_name"]
534
+ )
535
+
536
+ self.agents_per_category[category]["supporter"] = create_user(
537
+ instance=self.instance,
538
+ first_name=f"{fake.first_name()}-{fake.first_name()}",
539
+ last_name=f"{fake.last_name()}-{fake.last_name()}",
540
+ return_full_response=True,
541
+ user_roles=["itil"],
542
+ random=self.random,
543
+ )
544
+ self.agents_per_category[category]["supporter"]["full_name"] = (
545
+ self.agents_per_category[category]["supporter"]["first_name"]
546
+ + " "
547
+ + self.agents_per_category[category]["supporter"]["last_name"]
548
+ )
549
+
550
+ self.agents_per_category[category]["planner"] = create_user(
551
+ instance=self.instance,
552
+ first_name=f"{fake.first_name()}-{fake.first_name()}",
553
+ last_name=f"{fake.last_name()}-{fake.last_name()}",
554
+ return_full_response=True,
555
+ user_roles=["itil"],
556
+ random=self.random,
557
+ )
558
+ self.agents_per_category[category]["planner"]["full_name"] = (
559
+ self.agents_per_category[category]["planner"]["first_name"]
560
+ + " "
561
+ + self.agents_per_category[category]["planner"]["last_name"]
562
+ )
563
+
564
+ incident_numbers = ", ".join(new_incident_numbers)
565
+
566
+ expert_string = ""
567
+ for category in self.active_categories:
568
+ category_experts = f"Expert: {self.agents_per_category[category]['expert']['full_name']}, Supporter: {self.agents_per_category[category]['supporter']['full_name']}, Planner: {self.agents_per_category[category]['planner']['full_name']}"
569
+ expert_string += f"{category.capitalize()} agents - {category_experts} \n"
570
+ # Get the task description
571
+ self.short_description = f"Assign work using priority to relevant agents"
572
+ self.task_description = (
573
+ f'Referring to company protocol "{self.protocol_name}" (located in the "Company Protocols" knowledge base) assign work to the agents with the following information: \n'
574
+ + f"Incidents to assign: {incident_numbers} \n\n"
575
+ + f"{expert_string}"
576
+ )
577
+ # Sample a configuration
578
+ config = self.fixed_config if self.fixed_config else self._get_config()
579
+
580
+ goal, info = super().setup_goal(page=page, config=config)
581
+
582
+ if self.level == 2:
583
+ goal = (
584
+ self.short_description
585
+ + f"\n1. Navigate to the Service Desk > Incidents. \n"
586
+ + f"\n2. You have to assign the following incidents to relevant agents: {incident_numbers}. You can filter the list using each incident number and use the 'Assigned to' field to assign an incident.\n"
587
+ + f"\n3. You have to ensure that each incident is assigned to a relevant agent based on the priority of the incident and its category. For an incident with priority 1 - Critical, assign it to an 'expert' agent of the category, for priority 3 - Moderate, assign it to a 'supporter' of the category, and for priority 5 - Planning assign it to a 'planner' of the category.\n"
588
+ + f"\nThe category wise relevant agent are as follows:\n"
589
+ + f"{expert_string}"
590
+ )
591
+
592
+ return goal, info
593
+
594
+ def _get_config(self) -> list[tuple[AbstractServiceNowTask, dict, bool]]:
595
+
596
+ navigate_to_protocol_subtask = [
597
+ # Navigate to the KB
598
+ AllMenuTask(
599
+ instance=self.instance,
600
+ fixed_config={
601
+ "application": "Self-Service",
602
+ "module": "Knowledge",
603
+ "url": "/now/nav/ui/classic/params/target/%24knowledge.do",
604
+ },
605
+ is_validated=False,
606
+ used_in_level_2=False,
607
+ ),
608
+ # Find the protocol for on-boarding a new user
609
+ KnowledgeBaseSearchTask(
610
+ instance=self.instance,
611
+ fixed_config={
612
+ "alternative_answers": [],
613
+ "item": f"{self.protocol_name}",
614
+ "question": "Can you find the Work Assignment Protocol in the Knowledge Base?",
615
+ "value": "",
616
+ },
617
+ is_validated=False,
618
+ used_in_level_2=False,
619
+ ),
620
+ ]
621
+
622
+ all_incident_assignments = []
623
+
624
+ for incident_config in self.incident_configs:
625
+ assigned_to = self.agents_per_category[incident_config["category"]][
626
+ self.priorities[int(incident_config["priority"])]["agent_type"]
627
+ ]["full_name"]
628
+ assign_incidents_subtask = [
629
+ # Navigate to the incidents list
630
+ AllMenuTask(
631
+ instance=self.instance,
632
+ fixed_config={
633
+ "application": "Service Desk",
634
+ "module": "Incidents",
635
+ "url": "/now/nav/ui/classic/params/target/incident_list.do",
636
+ },
637
+ is_validated=False,
638
+ used_in_level_2=True,
639
+ ),
640
+ # Filter incident
641
+ FilterIncidentListTask(
642
+ instance=self.instance,
643
+ fixed_config={
644
+ "filter_columns": [
645
+ "number",
646
+ ],
647
+ "filter_kind": "AND",
648
+ "filter_values": [
649
+ incident_config["number"],
650
+ ],
651
+ },
652
+ is_validated=False,
653
+ used_in_level_2=True,
654
+ ),
655
+ # Edit incident
656
+ EditIncidentTask(
657
+ instance=self.instance,
658
+ # fixed_config=incident_config,
659
+ new_values={"assigned_to": assigned_to},
660
+ is_validated=False,
661
+ used_in_level_2=True,
662
+ record_sys_id=incident_config["sys_id"],
663
+ level=self.level,
664
+ ),
665
+ ]
666
+ all_incident_assignments.extend(assign_incidents_subtask)
667
+
668
+ config = navigate_to_protocol_subtask + all_incident_assignments
669
+
670
+ return config
671
+
672
+ def validate(self, page: Page, chat_messages: list[str]) -> Tuple[float, bool, str, dict]:
673
+ agents_per_category_sys_ids = {
674
+ category: {
675
+ agent_type: agent["sys_id"]
676
+ for agent_type, agent in self.agents_per_category[category].items()
677
+ }
678
+ for category in self.agents_per_category
679
+ }
680
+ for incident_config in self.incident_configs:
681
+ incident_response = table_api_call(
682
+ instance=self.instance,
683
+ table="incident",
684
+ params={
685
+ "sysparm_query": f"sys_id={incident_config['sys_id']}",
686
+ "sysparm_fields": "category,assigned_to,priority",
687
+ },
688
+ method="GET",
689
+ )["result"][0]
690
+ if incident_response["category"] != incident_config["category"]:
691
+ raise Exception("Corrupted incident data")
692
+ if not incident_response["assigned_to"]:
693
+ return (
694
+ 0,
695
+ False,
696
+ "",
697
+ {
698
+ "message": f"The incident {incident_config['number']} has not been assigned to anyone."
699
+ },
700
+ )
701
+ if (
702
+ incident_response["assigned_to"]["value"]
703
+ != agents_per_category_sys_ids[incident_response["category"]][
704
+ self.priorities[int(incident_response["priority"])]["agent_type"]
705
+ ]
706
+ ):
707
+ return (
708
+ 0,
709
+ False,
710
+ "",
711
+ {
712
+ "message": f"The incident {incident_config['number']} was assigned to an incorrect agent."
713
+ },
714
+ )
715
+ # Validate final_l3 tasks
716
+ reward, done, message, info = super().validate(page, chat_messages)
717
+ return reward, done, message, info
718
+
719
+ def teardown(self) -> None:
720
+ for incident in self.incident_configs:
721
+ db_delete_from_table(
722
+ instance=self.instance, table="incident", sys_id=incident["sys_id"]
723
+ )
724
+
725
+ for category in self.agents_per_category.values():
726
+ for agent in category.values():
727
+ db_delete_from_table(
728
+ instance=self.instance, table="sys_user", sys_id=agent["sys_id"]
729
+ )
730
+
731
+ return super().teardown()
732
+
733
+
734
+ class PriorityAssignmentSmallTask(PriorityAssignmentTask, HumanEvalTask):
735
+ def __init__(
736
+ self,
737
+ instance: SNowInstance = None,
738
+ seed: int = None,
739
+ fixed_config: list[AbstractServiceNowTask] = None,
740
+ level: int = 3,
741
+ ) -> None:
742
+ """
743
+ Small version of priority assignment task.
744
+ """
745
+ super().__init__(
746
+ instance=instance,
747
+ level=level,
748
+ num_categories=2,
749
+ fixed_config=fixed_config,
750
+ seed=0,
751
+ prefix="PAS",
752
+ )
753
+
754
+
755
+ class PriorityAssignmentMediumTask(PriorityAssignmentTask):
756
+ def __init__(
757
+ self,
758
+ instance: SNowInstance = None,
759
+ seed: int = None,
760
+ fixed_config: list[AbstractServiceNowTask] = None,
761
+ level: int = 3,
762
+ ) -> None:
763
+ """
764
+ Medium version of priority assignment task.
765
+ """
766
+ super().__init__(
767
+ instance=instance,
768
+ level=level,
769
+ num_categories=3,
770
+ fixed_config=fixed_config,
771
+ seed=seed,
772
+ prefix="PAM",
773
+ )
774
+
775
+
776
+ class PriorityAssignmentLargeTask(PriorityAssignmentTask):
777
+ def __init__(
778
+ self,
779
+ instance: SNowInstance = None,
780
+ seed: int = None,
781
+ fixed_config: list[AbstractServiceNowTask] = None,
782
+ level: int = 3,
783
+ ) -> None:
784
+ """
785
+ Large version of priority assignment task.
786
+ """
787
+ super().__init__(
788
+ instance=instance,
789
+ level=level,
790
+ num_categories=4,
791
+ fixed_config=fixed_config,
792
+ seed=seed,
793
+ prefix="PAL",
794
+ )
795
+
796
+
797
+ __TASKS__ = [
798
+ WorkAssignmentSmallTask,
799
+ WorkAssignmentMediumTask,
800
+ WorkAssignmentLargeTask,
801
+ PriorityAssignmentSmallTask,
802
+ PriorityAssignmentMediumTask,
803
+ PriorityAssignmentLargeTask,
804
+ ]