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,1366 @@
1
+ """
2
+ Dashboard retrieval and do action comp tasks
3
+ """
4
+
5
+ import json
6
+ from functools import partial
7
+ import random
8
+ import numpy as np
9
+ from typing import List
10
+
11
+ from faker import Faker
12
+
13
+ fake = Faker()
14
+
15
+ from playwright.sync_api._generated import Page
16
+
17
+ from .base import CompositionalTask, InfeasibleCompositionalTask, HumanEvalTask
18
+ from .utils.infeasible_configs import get_infeasible_service_catalog_config
19
+ from ..base import AbstractServiceNowTask
20
+ from ..knowledge import KnowledgeBaseSearchTask
21
+
22
+ from ...api.incident import create_incident
23
+ from ...api.report import create_report
24
+ from ...api.user import create_user
25
+ from ...api.utils import table_api_call, db_delete_from_table
26
+ from ...instance import SNowInstance
27
+
28
+ from browsergym.workarena.tasks.navigation import AllMenuTask
29
+ from browsergym.workarena.tasks.service_catalog import META_CONFIGS
30
+
31
+
32
+ class DashboardRetrieveAndDoTask(CompositionalTask, HumanEvalTask):
33
+ def __init__(
34
+ self,
35
+ instance: SNowInstance = None,
36
+ dashboard_class: AbstractServiceNowTask = None,
37
+ seed: int = None,
38
+ fixed_config: list[AbstractServiceNowTask] = None,
39
+ dashboard_config: dict = None,
40
+ level: int = 2,
41
+ ) -> None:
42
+ """
43
+ Generic task to perform a dashboard retrieval and perform a task.
44
+ Parameters:
45
+ -----------
46
+ instance: SNowInstance
47
+ The ServiceNow instance to run the task on.
48
+ fixed_config: list[AbstractServiceNowTask]
49
+ A list of tuples, each containing a subtask
50
+ dashboard_config: dict
51
+ Configuration to use for the dashboard task.
52
+ level: int
53
+ 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.
54
+ L3 will start in a private task page describing the information needed to complete the task and the related company protocol
55
+ to complete it.
56
+ Attributes:
57
+ -----------
58
+ task_description: str
59
+ The start of the task description to be completed. Provided by the child class.
60
+ short_description: str
61
+ A short description of the task to be completed. Provided by the child class.
62
+ """
63
+ assert level in [2, 3], "Level must be either 2 or 3"
64
+ self.level = level
65
+ super().__init__(
66
+ instance=instance,
67
+ seed=seed,
68
+ fixed_config=fixed_config,
69
+ level=level,
70
+ )
71
+ self.used_in_level_2 = self.level == 2
72
+ self.dashboard_config = dashboard_config
73
+ self.task_description = None
74
+ self.short_description = None
75
+ self.dashboard_class = dashboard_class
76
+ self.protocol_name = "Dashboard Retrieve Information and Perform Task"
77
+ self.description_mapping = {
78
+ "max": self.random.choice(["maximum", "highest", "greatest"]),
79
+ "min": self.random.choice(["minimum", "lowest", "least"]),
80
+ "mean": self.random.choice(["mean", "average"]),
81
+ "median": "median",
82
+ "mode": "mode (most frequent)",
83
+ }
84
+
85
+ def create_report(self) -> None:
86
+ """
87
+ Create task relevant dashboard report
88
+ """
89
+ raise NotImplementedError
90
+
91
+ def set_compositional_task(self) -> None:
92
+ """
93
+ Create and return the compositional task
94
+ """
95
+ raise NotImplementedError
96
+
97
+ def get_compositional_task(self) -> list[AbstractServiceNowTask]:
98
+ """
99
+ Return the compositional task
100
+ """
101
+ return self.compositional_task
102
+
103
+ def _get_config(self) -> list[AbstractServiceNowTask]:
104
+
105
+ navigate_to_protocol_subtask = [
106
+ # Navigate to the KB
107
+ AllMenuTask(
108
+ instance=self.instance,
109
+ fixed_config={
110
+ "application": "Self-Service",
111
+ "module": "Knowledge",
112
+ "url": "/now/nav/ui/classic/params/target/%24knowledge.do",
113
+ },
114
+ is_validated=False,
115
+ used_in_level_2=False,
116
+ ),
117
+ # Find the protocol for on-boarding a new user
118
+ KnowledgeBaseSearchTask(
119
+ instance=self.instance,
120
+ fixed_config={
121
+ "alternative_answers": [],
122
+ "item": f"{self.protocol_name}",
123
+ "question": f"Can you find the '{self.protocol_name}' Protocol in the Knowledge Base?",
124
+ "value": "",
125
+ },
126
+ is_validated=False,
127
+ used_in_level_2=False,
128
+ ),
129
+ ]
130
+
131
+ dashboard_retrieval_subtask = [
132
+ # Navigate to the reports list
133
+ AllMenuTask(
134
+ instance=self.instance,
135
+ fixed_config={
136
+ "application": "Reports",
137
+ "module": "Administration > All",
138
+ "url": "/now/nav/ui/classic/params/target/sys_report_list.do",
139
+ },
140
+ is_validated=False,
141
+ used_in_level_2=True,
142
+ ),
143
+ # Find the user with the desired config
144
+ self.dashboard_class(
145
+ instance=self.instance,
146
+ seed=None,
147
+ fixed_config=self.dashboard_config,
148
+ is_validated=False,
149
+ used_in_level_2=True,
150
+ ),
151
+ ]
152
+
153
+ config = (
154
+ navigate_to_protocol_subtask
155
+ + dashboard_retrieval_subtask
156
+ + self.get_compositional_task()
157
+ )
158
+ return config
159
+
160
+ def teardown(self) -> None:
161
+ return super().teardown()
162
+
163
+
164
+ class DashboardRetrieveAndDoInfeasibleTask(InfeasibleCompositionalTask):
165
+ def __init__(
166
+ self,
167
+ instance: SNowInstance = None,
168
+ dashboard_class: AbstractServiceNowTask = None,
169
+ seed: int = None,
170
+ fixed_config: list[AbstractServiceNowTask] = None,
171
+ dashboard_config: dict = None,
172
+ level: int = 2,
173
+ ) -> None:
174
+ """
175
+ Generic task to perform a dashboard retrieval and perform a task.
176
+ Parameters:
177
+ -----------
178
+ instance: SNowInstance
179
+ The ServiceNow instance to run the task on.
180
+ fixed_config: list[AbstractServiceNowTask]
181
+ A list of tuples, each containing a subtask
182
+ dashboard_config: dict
183
+ Configuration to use for the dashboard task.
184
+ level: int
185
+ 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.
186
+ L3 will start in a private task page describing the information needed to complete the task and the related company protocol
187
+ to complete it.
188
+ Attributes:
189
+ -----------
190
+ task_description: str
191
+ The start of the task description to be completed. Provided by the child class.
192
+ short_description: str
193
+ A short description of the task to be completed. Provided by the child class.
194
+ """
195
+ assert level in [2, 3], "Level must be either 2 or 3"
196
+ self.level = level
197
+ super().__init__(
198
+ instance=instance,
199
+ seed=seed,
200
+ fixed_config=fixed_config,
201
+ level=level,
202
+ )
203
+ self.used_in_level_2 = self.level == 2
204
+ self.dashboard_config = dashboard_config
205
+ self.task_description = None
206
+ self.short_description = None
207
+ self.dashboard_class = dashboard_class
208
+ self.protocol_name = "Dashboard Retrieve Information and Perform Task"
209
+ self.description_mapping = {
210
+ "max": self.random.choice(["maximum", "highest", "most"]),
211
+ "min": self.random.choice(["minimum", "lowest", "least"]),
212
+ "mean": self.random.choice(["mean", "average"]),
213
+ "median": "median",
214
+ "mode": "mode (most frequent)",
215
+ }
216
+
217
+ def create_report(self) -> None:
218
+ """
219
+ Create task relevant dashboard report
220
+ """
221
+ raise NotImplementedError
222
+
223
+ def set_compositional_task(self) -> None:
224
+ """
225
+ Create and return the compositional task
226
+ """
227
+ raise NotImplementedError
228
+
229
+ def get_compositional_task(self) -> list[AbstractServiceNowTask]:
230
+ """
231
+ Return the compositional task
232
+ """
233
+ return self.compositional_task
234
+
235
+ def _get_config(self) -> list[AbstractServiceNowTask]:
236
+
237
+ navigate_to_protocol_subtask = [
238
+ # Navigate to the KB
239
+ AllMenuTask(
240
+ instance=self.instance,
241
+ fixed_config={
242
+ "application": "Self-Service",
243
+ "module": "Knowledge",
244
+ "url": "/now/nav/ui/classic/params/target/%24knowledge.do",
245
+ },
246
+ is_validated=False,
247
+ used_in_level_2=False,
248
+ has_description=False,
249
+ ),
250
+ # Find the protocol for on-boarding a new user
251
+ KnowledgeBaseSearchTask(
252
+ instance=self.instance,
253
+ fixed_config={
254
+ "alternative_answers": [],
255
+ "item": f"{self.protocol_name}",
256
+ "question": f"Can you find the '{self.protocol_name}' Protocol in the Knowledge Base?",
257
+ "value": "",
258
+ },
259
+ is_validated=False,
260
+ used_in_level_2=False,
261
+ has_description=False,
262
+ ),
263
+ ]
264
+
265
+ dashboard_retrieval_subtask = [
266
+ # Navigate to the reports list
267
+ AllMenuTask(
268
+ instance=self.instance,
269
+ fixed_config={
270
+ "application": "Reports",
271
+ "module": "Administration > All",
272
+ "url": "/now/nav/ui/classic/params/target/sys_report_list.do",
273
+ },
274
+ is_validated=False,
275
+ used_in_level_2=True,
276
+ has_description=False,
277
+ ),
278
+ # Find the user with the desired config
279
+ self.dashboard_class(
280
+ instance=self.instance,
281
+ seed=None,
282
+ fixed_config=self.dashboard_config,
283
+ is_validated=False,
284
+ used_in_level_2=True,
285
+ ),
286
+ ]
287
+
288
+ config = (
289
+ navigate_to_protocol_subtask
290
+ + dashboard_retrieval_subtask
291
+ + self.get_compositional_task()
292
+ )
293
+ return config
294
+
295
+ def teardown(self) -> None:
296
+ return super().teardown()
297
+
298
+
299
+ class DashboardRetrieveIncidentAndDoTask(DashboardRetrieveAndDoTask):
300
+ def __init__(
301
+ self,
302
+ instance: SNowInstance = None,
303
+ seed: int = None,
304
+ fixed_config: list[AbstractServiceNowTask] = None,
305
+ level: int = 2,
306
+ max_incidents_per_agent: int = 4,
307
+ min_incidents_per_agent: int = 1,
308
+ num_agents: int = 4,
309
+ question: str = "",
310
+ dashboard_class: AbstractServiceNowTask = None,
311
+ ) -> None:
312
+ """
313
+ Retrieve information based on incidents from the dashboard and do the task.
314
+ """
315
+ self.incident_hashtag = (
316
+ f"#INC{str(id(self) % (10**8)).zfill(9)}" # identifier to select problems
317
+ )
318
+ self.chart_title = f"Incidents with hashtag {self.incident_hashtag}"
319
+ super().__init__(
320
+ instance=instance,
321
+ seed=seed,
322
+ fixed_config=fixed_config,
323
+ dashboard_config={
324
+ "url": "/now/nav/ui/classic/params/target/sys_report",
325
+ "chart_title": self.chart_title,
326
+ "question": question,
327
+ "chart_series": "",
328
+ },
329
+ level=level,
330
+ dashboard_class=dashboard_class,
331
+ )
332
+ self.question = question
333
+ self.max_incidents_per_agent = max_incidents_per_agent
334
+ self.min_incidents_per_agent = min_incidents_per_agent
335
+ self.num_agents = num_agents
336
+ if (self.max_incidents_per_agent - self.min_incidents_per_agent) < 2 or self.num_agents < 2:
337
+ raise Exception(
338
+ "The difference between maximum incidents and minimum incidents should be at least two. The number of agents should also be at least 2."
339
+ )
340
+ self.task_description = f"You have to retrieve some information from a dashboard chart based on the description below. The chart presents the number of 'incidents' assigned to different agents. After retrieving the information, you will be asked to use it to complete a task.\n \n"
341
+ self.task_description += f"Title of the report: {self.incident_hashtag}\n\n"
342
+ if self.level == 3:
343
+ self.task_description += f"Referring to the company protocol '{self.protocol_name}' (located in the 'Company Protocols' knowledge base), complete the dashboard retrieval task.\n\n"
344
+ self.short_description = (
345
+ f"Retrieve information from the chart with title {self.incident_hashtag} and perform the mentioned task."
346
+ + "\n For calculations, please round off to the next highest integer if required. If the required calculation has multiple possible answers (for example, 'mode' or 'most frequently' occuring value), please consider the highest value.\n\n"
347
+ )
348
+
349
+ def create_report(
350
+ self,
351
+ user_roles=["itil"],
352
+ ) -> None:
353
+ self.agents = {}
354
+ self.agent_sysids = []
355
+ for _ in range(self.num_agents):
356
+ agent_response = create_user(
357
+ instance=self.instance,
358
+ first_name=f"{fake.first_name()}-{fake.first_name()}",
359
+ last_name=f"{fake.last_name()}-{fake.last_name()}",
360
+ return_full_response=True,
361
+ user_roles=user_roles,
362
+ )
363
+ self.agents[agent_response["sys_id"]] = agent_response
364
+ self.agent_sysids.append(agent_response["sys_id"])
365
+
366
+ highest_agent = self.agent_sysids[
367
+ -1
368
+ ] # Choose last agent as the agent with maximum incidents
369
+ self.agents[highest_agent]["num_incidents"] = self.max_incidents_per_agent
370
+ self.agents[highest_agent]["incident_configs"] = []
371
+
372
+ lowest_agent = self.agent_sysids[
373
+ 0
374
+ ] # Choose first agent as the agent with minimum incidents
375
+ self.agents[lowest_agent]["num_incidents"] = self.min_incidents_per_agent
376
+ self.agents[lowest_agent]["incident_configs"] = []
377
+
378
+ for agent_sysid in self.agent_sysids[1:-1]:
379
+ self.agents[agent_sysid]["num_incidents"] = self.random.randint(
380
+ self.min_incidents_per_agent + 1, self.max_incidents_per_agent - 1
381
+ )
382
+ self.agents[agent_sysid]["incident_configs"] = []
383
+
384
+ number_assignments = sum([agent["num_incidents"] for agent in self.agents.values()])
385
+
386
+ all_existing_incidents = table_api_call(
387
+ instance=self.instance, table="incident", method="GET"
388
+ )["result"]
389
+ self.all_incident_numbers = [incident["number"] for incident in all_existing_incidents]
390
+
391
+ self.new_incident_numbers = []
392
+ for _ in range(number_assignments):
393
+ incident_number = (
394
+ self.prefix
395
+ + str(id(self) % (10**8)).zfill(8)[:4]
396
+ + str(self.random.randint(1000, 9999))
397
+ )
398
+ while (
399
+ incident_number in self.all_incident_numbers
400
+ or incident_number in self.new_incident_numbers
401
+ ):
402
+ incident_number = (
403
+ self.prefix
404
+ + str(id(self) % (10**8)).zfill(8)[:4]
405
+ + str(random.randint(1000, 9999))
406
+ )
407
+ self.new_incident_numbers.append(incident_number)
408
+
409
+ incident_number_idx = 0
410
+ for agent, agent_attributes in self.agents.items():
411
+ for _ in range(agent_attributes["num_incidents"]):
412
+ incident_response = create_incident(
413
+ instance=self.instance,
414
+ incident_number=self.new_incident_numbers[incident_number_idx],
415
+ caller_sys_id=self._base_user_sysid,
416
+ category="software",
417
+ priority=4,
418
+ impact=2, # priority is calculated as some combination of impact and urgency
419
+ urgency=3,
420
+ incident_hastag=self.incident_hashtag,
421
+ assigned_to=agent,
422
+ )
423
+ self.agents[agent]["incident_configs"].append(incident_response)
424
+ incident_number_idx += 1
425
+
426
+ self.report_sys_id, _ = create_report(
427
+ instance=self.instance,
428
+ table="incident",
429
+ filter_hashtag=self.incident_hashtag,
430
+ field="assigned_to",
431
+ plot_title=self.chart_title,
432
+ random=self.random,
433
+ )
434
+
435
+ def get_agent_values(self, attribute_name, filter_than) -> list[str]:
436
+ agent_values = []
437
+ agent_value_sysids = []
438
+ agent_incidents = [
439
+ agent_attributes["num_incidents"] for agent_attributes in self.agents.values()
440
+ ]
441
+
442
+ if self.question == "max":
443
+ agent_value_sysids.append(self.agents[self.agent_sysids[-1]]["sys_id"])
444
+ if attribute_name == "assigned_to":
445
+ agent_full_name = (
446
+ self.agents[self.agent_sysids[-1]]["first_name"]
447
+ + " "
448
+ + self.agents[self.agent_sysids[-1]]["last_name"]
449
+ )
450
+ agent_values.append(agent_full_name)
451
+ elif attribute_name == "first_name":
452
+ agent_first_name = self.agents[self.agent_sysids[-1]]["first_name"]
453
+ agent_values.append(agent_first_name)
454
+ else:
455
+ raise Exception("Filter column not supported.")
456
+ elif self.question == "min":
457
+ agent_value_sysids.append(self.agents[self.agent_sysids[0]]["sys_id"])
458
+ if attribute_name == "assigned_to":
459
+ agent_full_name = (
460
+ self.agents[self.agent_sysids[0]]["first_name"]
461
+ + " "
462
+ + self.agents[self.agent_sysids[0]]["last_name"]
463
+ )
464
+ agent_values.append(agent_full_name)
465
+ elif attribute_name == "first_name":
466
+ agent_first_name = self.agents[self.agent_sysids[0]]["first_name"]
467
+ agent_values.append(agent_first_name)
468
+ else:
469
+ raise Exception("Filter column not supported.")
470
+ elif self.question == "mean" or self.question == "median" or self.question == "mode":
471
+ if self.question == "mean":
472
+ mean_incidents = np.mean(agent_incidents)
473
+ incidents_count = int(np.ceil(mean_incidents))
474
+ elif self.question == "median":
475
+ incidents_count = int(np.ceil(np.median(agent_incidents)))
476
+ elif self.question == "mode":
477
+ # We select the maximum value if there are two or more modes
478
+ frequencies = {}
479
+ for count in agent_incidents:
480
+ if count not in frequencies:
481
+ frequencies[count] = 1
482
+ else:
483
+ frequencies[count] += 1
484
+ sorted_frequencies = {
485
+ count: frequency
486
+ for count, frequency in sorted(
487
+ frequencies.items(), key=lambda item: item[1], reverse=True
488
+ )
489
+ }
490
+ max_frequency = list(sorted_frequencies.values())[0]
491
+ max_frequencies = [
492
+ count
493
+ for count, frequency in sorted_frequencies.items()
494
+ if frequency == max_frequency
495
+ ]
496
+ incidents_count = int(max(max_frequencies))
497
+
498
+ for agent_sysid, agent_attributes in self.agents.items():
499
+ if (
500
+ filter_than == "greater"
501
+ and agent_attributes["num_incidents"] >= incidents_count
502
+ ) or (
503
+ filter_than == "lesser" and agent_attributes["num_incidents"] <= incidents_count
504
+ ):
505
+ agent_value_sysids.append(agent_sysid)
506
+ if attribute_name == "assigned_to":
507
+ agent_full_name = (
508
+ agent_attributes["first_name"] + " " + agent_attributes["last_name"]
509
+ )
510
+ agent_values.append(agent_full_name)
511
+
512
+ elif attribute_name == "first_name":
513
+ agent_first_name = agent_attributes["first_name"]
514
+ agent_values.append(agent_first_name)
515
+ else:
516
+ raise Exception("Filter column not supported.")
517
+ else:
518
+ raise Exception("Unsopprted question type.")
519
+
520
+ return agent_values, agent_value_sysids
521
+
522
+ def set_compositional_task(self) -> None:
523
+ raise NotImplementedError
524
+
525
+ def teardown(self) -> None:
526
+ # Delete the report
527
+ db_delete_from_table(
528
+ instance=self.instance,
529
+ table="sys_report",
530
+ sys_id=self.report_sys_id,
531
+ )
532
+ # Delete the incidents and users
533
+ for agent_sys_id in self.agents:
534
+ for incident in self.agents[agent_sys_id]["incident_configs"]:
535
+ db_delete_from_table(
536
+ instance=self.instance,
537
+ table="incident",
538
+ sys_id=incident["sys_id"],
539
+ )
540
+ db_delete_from_table(
541
+ instance=self.instance,
542
+ table="sys_user",
543
+ sys_id=agent_sys_id,
544
+ )
545
+ return super().teardown()
546
+
547
+
548
+ class DashboardRetrieveIncidentAndDoInfeasibleTask(DashboardRetrieveAndDoInfeasibleTask):
549
+ def __init__(
550
+ self,
551
+ instance: SNowInstance = None,
552
+ seed: int = None,
553
+ fixed_config: list[AbstractServiceNowTask] = None,
554
+ level: int = 2,
555
+ max_incidents_per_agent: int = 4,
556
+ min_incidents_per_agent: int = 1,
557
+ num_agents: int = 4,
558
+ question: str = "",
559
+ dashboard_class: AbstractServiceNowTask = None,
560
+ function: callable = None,
561
+ provide_reason: bool = True,
562
+ ) -> None:
563
+ """
564
+ Retrieve information based on incidents from the dashboard and do the task.
565
+ """
566
+ self.incident_hashtag = (
567
+ f"#INC{str(id(self) % (10**8)).zfill(9)}" # identifier to select problems
568
+ )
569
+ self.chart_title = f"Incidents with hashtag {self.incident_hashtag}"
570
+ super().__init__(
571
+ instance=instance,
572
+ seed=seed,
573
+ fixed_config=fixed_config,
574
+ dashboard_config={
575
+ "url": "/now/nav/ui/classic/params/target/sys_report",
576
+ "chart_title": self.chart_title,
577
+ "question": question,
578
+ "chart_series": "",
579
+ },
580
+ level=level,
581
+ dashboard_class=dashboard_class,
582
+ )
583
+ self.question = question
584
+ self.max_incidents_per_agent = max_incidents_per_agent
585
+ self.min_incidents_per_agent = min_incidents_per_agent
586
+ self.num_agents = num_agents
587
+ if (self.max_incidents_per_agent - self.min_incidents_per_agent) < 2 or self.num_agents < 2:
588
+ raise Exception(
589
+ "The difference between maximum incidents and minimum incidents should be at least two. The number of agents should also be at least 2."
590
+ )
591
+ self.task_description = f"Retrieve the information mentioned in the following description from the report of the incidents with the title {self.incident_hashtag}. Using the information, follow the subsequent task steps mentioned. For all calculations, round of to the next highest integer first. For multiple modes, choose the highest value.\n"
592
+ if self.level == 3:
593
+ self.task_description += f"Follow the '{self.protocol_name}' protocol from the knowledge base for extra instructions.\n"
594
+ self.short_description = "Retrieve incident information and perform the mentioned task"
595
+ self.function = partial(function, provide_reason=provide_reason)
596
+
597
+ def create_report(
598
+ self,
599
+ user_roles=["itil"],
600
+ ) -> None:
601
+ self.agents = {}
602
+ self.agent_sysids = []
603
+ for _ in range(self.num_agents):
604
+ agent_response = create_user(
605
+ instance=self.instance,
606
+ first_name=f"{fake.first_name()}-{fake.first_name()}",
607
+ last_name=f"{fake.last_name()}-{fake.last_name()}",
608
+ return_full_response=True,
609
+ user_roles=user_roles,
610
+ )
611
+ self.agents[agent_response["sys_id"]] = agent_response
612
+ self.agent_sysids.append(agent_response["sys_id"])
613
+
614
+ highest_agent = self.agent_sysids[
615
+ -1
616
+ ] # Choose last agent as the agent with maximum incidents
617
+ self.agents[highest_agent]["num_incidents"] = self.max_incidents_per_agent
618
+ self.agents[highest_agent]["incident_configs"] = []
619
+
620
+ lowest_agent = self.agent_sysids[
621
+ 0
622
+ ] # Choose first agent as the agent with minimum incidents
623
+ self.agents[lowest_agent]["num_incidents"] = self.min_incidents_per_agent
624
+ self.agents[lowest_agent]["incident_configs"] = []
625
+
626
+ for agent_sysid in self.agent_sysids[1:-1]:
627
+ self.agents[agent_sysid]["num_incidents"] = self.random.randint(
628
+ self.min_incidents_per_agent + 1, self.max_incidents_per_agent - 1
629
+ )
630
+ self.agents[agent_sysid]["incident_configs"] = []
631
+
632
+ number_assignments = sum([agent["num_incidents"] for agent in self.agents.values()])
633
+
634
+ all_existing_incidents = table_api_call(
635
+ instance=self.instance, table="incident", method="GET"
636
+ )["result"]
637
+ self.all_incident_numbers = [incident["number"] for incident in all_existing_incidents]
638
+
639
+ self.new_incident_numbers = []
640
+ for _ in range(number_assignments):
641
+ incident_number = (
642
+ self.prefix + str(id(self) % (10**8)).zfill(8)[:4] + str(random.randint(1000, 9999))
643
+ )
644
+ while (
645
+ incident_number in self.all_incident_numbers
646
+ or incident_number in self.new_incident_numbers
647
+ ):
648
+ incident_number = (
649
+ self.prefix
650
+ + str(id(self) % (10**8)).zfill(8)[:4]
651
+ + str(random.randint(1000, 9999))
652
+ )
653
+ self.new_incident_numbers.append(incident_number)
654
+
655
+ incident_number_idx = 0
656
+ for agent, agent_attributes in self.agents.items():
657
+ for _ in range(agent_attributes["num_incidents"]):
658
+ incident_response = create_incident(
659
+ instance=self.instance,
660
+ incident_number=self.new_incident_numbers[incident_number_idx],
661
+ caller_sys_id=self._base_user_sysid,
662
+ category="software",
663
+ priority=4,
664
+ impact=2, # priority is calculated as some combination of impact and urgency
665
+ urgency=3,
666
+ incident_hastag=self.incident_hashtag,
667
+ assigned_to=agent,
668
+ )
669
+ self.agents[agent]["incident_configs"].append(incident_response)
670
+ incident_number_idx += 1
671
+
672
+ self.report_sys_id, _ = create_report(
673
+ instance=self.instance,
674
+ table="incident",
675
+ filter_hashtag=self.incident_hashtag,
676
+ field="assigned_to",
677
+ plot_title=self.chart_title,
678
+ random=self.random,
679
+ )
680
+
681
+ def get_agent_values(self, attribute_name, filter_than) -> list[str]:
682
+ agent_values = []
683
+ agent_value_sysids = []
684
+ agent_incidents = [
685
+ agent_attributes["num_incidents"] for agent_attributes in self.agents.values()
686
+ ]
687
+
688
+ if self.question == "max":
689
+ agent_value_sysids.append(self.agents[self.agent_sysids[-1]]["sys_id"])
690
+ if attribute_name == "assigned_to":
691
+ agent_full_name = (
692
+ self.agents[self.agent_sysids[-1]]["first_name"]
693
+ + " "
694
+ + self.agents[self.agent_sysids[-1]]["last_name"]
695
+ )
696
+ agent_values.append(agent_full_name)
697
+ elif attribute_name == "first_name":
698
+ agent_first_name = self.agents[self.agent_sysids[-1]]["first_name"]
699
+ agent_values.append(agent_first_name)
700
+ else:
701
+ raise Exception("Filter column not supported.")
702
+ elif self.question == "min":
703
+ agent_value_sysids.append(self.agents[self.agent_sysids[0]]["sys_id"])
704
+ if attribute_name == "assigned_to":
705
+ agent_full_name = (
706
+ self.agents[self.agent_sysids[0]]["first_name"]
707
+ + " "
708
+ + self.agents[self.agent_sysids[0]]["last_name"]
709
+ )
710
+ agent_values.append(agent_full_name)
711
+ elif attribute_name == "first_name":
712
+ agent_first_name = self.agents[self.agent_sysids[0]]["first_name"]
713
+ agent_values.append(agent_first_name)
714
+ else:
715
+ raise Exception("Filter column not supported.")
716
+ elif self.question == "mean" or self.question == "median" or self.question == "mode":
717
+ if self.question == "mean":
718
+ mean_incidents = np.mean(agent_incidents)
719
+ incidents_count = int(np.ceil(mean_incidents))
720
+ elif self.question == "median":
721
+ incidents_count = int(np.ceil(np.median(agent_incidents)))
722
+ elif self.question == "mode":
723
+ # We select the maximum value if there are two or more modes
724
+ frequencies = {}
725
+ for count in agent_incidents:
726
+ if count not in frequencies:
727
+ frequencies[count] = 1
728
+ else:
729
+ frequencies[count] += 1
730
+ sorted_frequencies = {
731
+ count: frequency
732
+ for count, frequency in sorted(
733
+ frequencies.items(), key=lambda item: item[1], reverse=True
734
+ )
735
+ }
736
+ max_frequency = list(sorted_frequencies.values())[0]
737
+ max_frequencies = [
738
+ count
739
+ for count, frequency in sorted_frequencies.items()
740
+ if frequency == max_frequency
741
+ ]
742
+ incidents_count = int(max(max_frequencies))
743
+
744
+ for agent_sysid, agent_attributes in self.agents.items():
745
+ if (
746
+ filter_than == "greater"
747
+ and agent_attributes["num_incidents"] >= incidents_count
748
+ ) or (
749
+ filter_than == "lesser" and agent_attributes["num_incidents"] <= incidents_count
750
+ ):
751
+ agent_value_sysids.append(agent_sysid)
752
+ if attribute_name == "assigned_to":
753
+ agent_full_name = (
754
+ agent_attributes["first_name"] + " " + agent_attributes["last_name"]
755
+ )
756
+ agent_values.append(agent_full_name)
757
+
758
+ elif attribute_name == "first_name":
759
+ agent_first_name = agent_attributes["first_name"]
760
+ agent_values.append(agent_first_name)
761
+ else:
762
+ raise Exception("Filter column not supported.")
763
+ else:
764
+ raise Exception("Unsopprted question type.")
765
+
766
+ return agent_values, agent_value_sysids
767
+
768
+ def set_compositional_task(self) -> None:
769
+ raise NotImplementedError
770
+
771
+ def teardown(self) -> None:
772
+ # Delete the report
773
+ db_delete_from_table(
774
+ instance=self.instance,
775
+ table="sys_report",
776
+ sys_id=self.report_sys_id,
777
+ )
778
+ # Delete the incidents and users
779
+ for agent_sys_id in self.agents:
780
+ for incident in self.agents[agent_sys_id]["incident_configs"]:
781
+ db_delete_from_table(
782
+ instance=self.instance,
783
+ table="incident",
784
+ sys_id=incident["sys_id"],
785
+ )
786
+ db_delete_from_table(
787
+ instance=self.instance,
788
+ table="sys_user",
789
+ sys_id=agent_sys_id,
790
+ )
791
+ return super().teardown()
792
+
793
+
794
+ class DashboardRetrieveCatalogAndDoTask(DashboardRetrieveAndDoTask):
795
+ def __init__(
796
+ self,
797
+ instance: SNowInstance = None,
798
+ seed: int = None,
799
+ fixed_config: list[AbstractServiceNowTask] = None,
800
+ level: int = 2,
801
+ max_items: int = 5,
802
+ min_items: int = 3,
803
+ question: str = "",
804
+ dashboard_class: AbstractServiceNowTask = None,
805
+ min_catalog_item: str = None,
806
+ ) -> None:
807
+ """
808
+ Retrieve information based on incidents from the dashboard and do the task.
809
+ """
810
+ self.catalog_hashtag = (
811
+ f"#CAT{str(id(self) % (10**8)).zfill(9)}" # identifier to select problems
812
+ )
813
+ self.chart_title = f"Catalog with hashtag {self.catalog_hashtag}"
814
+ super().__init__(
815
+ instance=instance,
816
+ seed=seed,
817
+ fixed_config=fixed_config,
818
+ dashboard_config={
819
+ "url": "/now/nav/ui/classic/params/target/sys_report",
820
+ "chart_title": self.chart_title,
821
+ "question": question,
822
+ "chart_series": "",
823
+ },
824
+ level=level,
825
+ dashboard_class=dashboard_class,
826
+ )
827
+ self.question = question
828
+ self.max_number_per_item = self.random.choice([5, 6, 7])
829
+ self.min_number_per_item = self.random.choice([1, 2])
830
+ self.max_items = max_items
831
+ self.min_items = min_items
832
+ if self.max_items < 2 or self.min_items < 2:
833
+ raise Exception("The items allowed should at least be 2.")
834
+ self.min_catalog_item = min_catalog_item
835
+ self.task_description = f"You have to retrieve some information from a dashboard chart based on the description below. The chart presents the number of 'hardware items' available in stock. After retrieving the information, you will be asked to use it to complete a task.\n \n"
836
+ self.task_description += f"Title of the report: {self.catalog_hashtag}\n\n"
837
+ if self.level == 3:
838
+ self.task_description += f"Referring to the company protocol '{self.protocol_name}' (located in the 'Company Protocols' knowledge base), complete the dashboard retrieval task.\n\n"
839
+ self.short_description = (
840
+ f"Retrieve information from the chart with the title {self.catalog_hashtag} and perform the mentioned task."
841
+ + "\nFor calculations, please round off to the next highest integer if required. If the required calculation has multiple possible answers (for example, 'mode' or 'most frequently' occuring value), please consider the highest value.\n\n"
842
+ )
843
+
844
+ def get_catalog_item_sysid(self, catalog_item: str) -> str:
845
+ catalog_item_response = table_api_call(
846
+ instance=self.instance,
847
+ table="sc_cat_item",
848
+ params={"sysparm_query": f"sys_name={catalog_item}", "sysparm_fields": "sys_id"},
849
+ method="GET",
850
+ )["result"]
851
+ if len(catalog_item_response) == 0:
852
+ raise Exception("Catalog item not found.")
853
+ elif len(catalog_item_response) > 1:
854
+ raise Exception("Multiple catalog items found.")
855
+ return catalog_item_response[0]["sys_id"]
856
+
857
+ def create_report(
858
+ self,
859
+ user_roles=["itil"],
860
+ ) -> None:
861
+ catalog_item_list = list(META_CONFIGS.keys())
862
+ catalog_item_list.remove(self.min_catalog_item)
863
+ random_service_catalog_items = self.random.choice(
864
+ catalog_item_list, self.random.randint(self.min_items, self.max_items), replace=False
865
+ ).tolist()
866
+ cat_item_sys_name = {
867
+ "Developer Laptop (Mac)": "Developer Laptop (Mac)",
868
+ "iPad mini": "iPad mini",
869
+ "iPad pro": "iPad pro",
870
+ "Sales Laptop": "Sales Laptop",
871
+ "Standard Laptop": "Standard Laptop",
872
+ "Apple Watch": "Apple Watch",
873
+ "Apple MacBook Pro 15": 'Apple MacBook Pro 15"',
874
+ "Development Laptop (PC)": "Development Laptop (PC)",
875
+ "Loaner Laptop": "Notebook Computer Loaner",
876
+ }
877
+
878
+ # shuffle
879
+ self.random.shuffle(random_service_catalog_items)
880
+ self.random_service_catalog_items = random_service_catalog_items
881
+ random_service_catalog_items = [self.min_catalog_item] + random_service_catalog_items
882
+
883
+ service_catalog_report_config = {}
884
+ service_catalog_report_config[random_service_catalog_items[0]] = {
885
+ "quantity": self.min_number_per_item,
886
+ "description": META_CONFIGS[random_service_catalog_items[0]]["desc"],
887
+ "configuration": {},
888
+ "item": random_service_catalog_items[0],
889
+ "sys_id": self.get_catalog_item_sysid(
890
+ cat_item_sys_name[random_service_catalog_items[0]]
891
+ ),
892
+ }
893
+ service_catalog_report_config[random_service_catalog_items[-1]] = {
894
+ "quantity": self.max_number_per_item,
895
+ "description": META_CONFIGS[random_service_catalog_items[-1]]["desc"],
896
+ "configuration": {},
897
+ "item": random_service_catalog_items[-1],
898
+ "sys_id": self.get_catalog_item_sysid(
899
+ cat_item_sys_name[random_service_catalog_items[-1]]
900
+ ),
901
+ }
902
+
903
+ for service_catalog_item in random_service_catalog_items[1:-1]:
904
+ service_catalog_report_config[service_catalog_item] = {
905
+ "quantity": self.random.randint(
906
+ self.min_number_per_item + 1, self.max_number_per_item - 1
907
+ ),
908
+ "description": META_CONFIGS[service_catalog_item]["desc"],
909
+ "configuration": {},
910
+ "item": service_catalog_item,
911
+ "sys_id": self.get_catalog_item_sysid(cat_item_sys_name[service_catalog_item]),
912
+ }
913
+
914
+ self.service_catalog_report_config = service_catalog_report_config
915
+ created_request_items = []
916
+ for (
917
+ service_catalog_item,
918
+ service_catalog_item_config,
919
+ ) in service_catalog_report_config.items():
920
+ for _ in range(service_catalog_item_config["quantity"]):
921
+ request_item_dict = {
922
+ "requested_for": self._base_user_sysid,
923
+ "quantity": 1,
924
+ "cat_item": service_catalog_item_config["sys_id"],
925
+ }
926
+ criteria_response = table_api_call(
927
+ instance=self.instance,
928
+ table="sc_req_item",
929
+ json=request_item_dict,
930
+ method="POST",
931
+ )["result"]
932
+ created_request_items.append((service_catalog_item, criteria_response["sys_id"]))
933
+
934
+ self.created_request_items = created_request_items
935
+
936
+ user_details = table_api_call(
937
+ instance=self.instance,
938
+ table="sys_user",
939
+ params={
940
+ "sysparm_query": f"sys_id={self._base_user_sysid}",
941
+ "sysparm_fields": "first_name,last_name",
942
+ },
943
+ method="GET",
944
+ )["result"][0]
945
+ user_full_name = user_details["first_name"] + " " + user_details["last_name"]
946
+
947
+ self.report_sys_id, _ = create_report(
948
+ instance=self.instance,
949
+ table="sc_req_item",
950
+ filter_hashtag=user_full_name,
951
+ filter_field="requested_for",
952
+ field="cat_item",
953
+ plot_title=self.chart_title,
954
+ random=self.random,
955
+ )
956
+
957
+ def get_order_quantity_value(self) -> list[str]:
958
+ quantities = [
959
+ service_catalog_report_config_attribute["quantity"]
960
+ for service_catalog_report_config_attribute in self.service_catalog_report_config.values()
961
+ ]
962
+ if self.question == "max":
963
+ if max(quantities) != self.max_number_per_item:
964
+ raise Exception("Maximum of quantities does not match attribute. Please check.")
965
+ target_quantity = self.max_number_per_item
966
+ elif self.question == "mean":
967
+ mean_quantity = np.mean(quantities)
968
+ target_quantity = int(np.ceil(mean_quantity))
969
+ elif self.question == "median":
970
+ target_quantity = int(np.ceil(np.median(quantities)))
971
+ elif self.question == "mode":
972
+ frequencies = {}
973
+ for count in quantities:
974
+ if count not in frequencies:
975
+ frequencies[count] = 1
976
+ else:
977
+ frequencies[count] += 1
978
+ sorted_frequencies = {
979
+ count: frequency
980
+ for count, frequency in sorted(
981
+ frequencies.items(), key=lambda item: item[1], reverse=True
982
+ )
983
+ }
984
+ max_frequency = list(sorted_frequencies.values())[0]
985
+ max_frequencies = [
986
+ count
987
+ for count, frequency in sorted_frequencies.items()
988
+ if frequency == max_frequency
989
+ ]
990
+ target_quantity = int(max(max_frequencies))
991
+ if target_quantity - self.min_number_per_item <= 0:
992
+ raise Exception("Unable to order quantity {target_quantity - self.min_number_per_item}")
993
+ return int(target_quantity - self.min_number_per_item)
994
+
995
+ def set_compositional_task(self) -> None:
996
+
997
+ order_config = {
998
+ "configuration": {},
999
+ "description": META_CONFIGS[self.min_catalog_item]["desc"],
1000
+ "item": self.min_catalog_item,
1001
+ "quantity": self.get_order_quantity_value(),
1002
+ }
1003
+
1004
+ create_order_item_subtask = [
1005
+ AllMenuTask(
1006
+ instance=self.instance,
1007
+ fixed_config={
1008
+ "application": "Self-Service",
1009
+ "module": "Service Catalog",
1010
+ "url": "/now/nav/ui/classic/params/target/catalog_home.do",
1011
+ },
1012
+ is_validated=False,
1013
+ used_in_level_2=True,
1014
+ ),
1015
+ self.order_item_class(
1016
+ instance=self.instance,
1017
+ fixed_config=order_config,
1018
+ is_validated=True,
1019
+ used_in_level_2=True,
1020
+ ),
1021
+ ]
1022
+
1023
+ self.compositional_task = create_order_item_subtask
1024
+
1025
+ def setup_goal(self, page: Page) -> tuple[str, dict]:
1026
+ self.create_report()
1027
+ self.set_compositional_task()
1028
+ config = self.fixed_config if self.fixed_config else self._get_config()
1029
+
1030
+ if self.level == 3:
1031
+ self.task_description = (
1032
+ self.task_description
1033
+ + f"\t - Please retrieve the '{self.description_mapping[self.question]}' value of all the items in stock.\n\n"
1034
+ + f"\t - Task: Place an order for the least available item in stock. The quantity of the order should be such that the final quantity of this item matches the above retrieved value.\n"
1035
+ + f"\t For example, consider the above task asks you to retrieve the maximum number of items in stock, say 4, and the least available item is an Apple Watch and its quantity is 1. You have to order 3 more Apple Watches.\n\n"
1036
+ + f"\t - Please do not change any other configuration while placing the order for the item. You can find important links to the pages in the protocol article.\n\n"
1037
+ + self.final_private_task_instructions
1038
+ )
1039
+
1040
+ goal, info = super().setup_goal(
1041
+ page=page, config=config, build_pretty_print_description=False
1042
+ )
1043
+
1044
+ if self.level == 2:
1045
+ goal = (
1046
+ self.short_description
1047
+ + f"\n1. Navigate to the Reports > View/Run page.\n"
1048
+ + f"\n2. Given the title of the report, search for it on this page.\n"
1049
+ + f"\n3. Find the value which is the {self.description_mapping[self.question]} of the items present in stock as per the chart. Also remember the least available item in the stock.\n"
1050
+ + f"\n4. Navigate to Self-Service > Service Catalog. \n"
1051
+ + f"\n5. For the least available item in stock, place an order for extra items such that its quantity matches the value you found."
1052
+ + "\nFor example, if you were requested to find the maximum value across the items, you would place an order for the least available item such that its NEW quantity matches this number. Please do not change any 'configuration' when placing the order.\n"
1053
+ )
1054
+
1055
+ return goal, info
1056
+
1057
+ def teardown(self) -> None:
1058
+ # Delete the report
1059
+ db_delete_from_table(
1060
+ instance=self.instance,
1061
+ table="sys_report",
1062
+ sys_id=self.report_sys_id,
1063
+ )
1064
+ # Delete the request items
1065
+ for created_request_item in self.created_request_items:
1066
+ db_delete_from_table(
1067
+ instance=self.instance,
1068
+ table="sc_req_item",
1069
+ sys_id=created_request_item[1],
1070
+ )
1071
+ return super().teardown()
1072
+
1073
+
1074
+ class DashboardRetrieveCatalogAndDoInfeasibleTask(DashboardRetrieveAndDoInfeasibleTask):
1075
+ def __init__(
1076
+ self,
1077
+ instance: SNowInstance = None,
1078
+ seed: int = None,
1079
+ fixed_config: list[AbstractServiceNowTask] = None,
1080
+ level: int = 2,
1081
+ max_items: int = 5,
1082
+ min_items: int = 3,
1083
+ question: str = "",
1084
+ dashboard_class: AbstractServiceNowTask = None,
1085
+ min_catalog_item: str = None,
1086
+ provide_reason: bool = None,
1087
+ ) -> None:
1088
+ """
1089
+ Retrieve information based on incidents from the dashboard and do the task.
1090
+ """
1091
+ self.catalog_hashtag = (
1092
+ f"#CAT{str(id(self) % (10**8)).zfill(9)}" # identifier to select problems
1093
+ )
1094
+ self.chart_title = f"Catalog with hashtag {self.catalog_hashtag}"
1095
+ super().__init__(
1096
+ instance=instance,
1097
+ seed=seed,
1098
+ fixed_config=fixed_config,
1099
+ dashboard_config={
1100
+ "url": "/now/nav/ui/classic/params/target/sys_report",
1101
+ "chart_title": self.chart_title,
1102
+ "question": question,
1103
+ "chart_series": "",
1104
+ },
1105
+ level=level,
1106
+ dashboard_class=dashboard_class,
1107
+ )
1108
+ self.question = question
1109
+ self.max_number_per_item = self.random.choice([5, 6, 7])
1110
+ self.min_number_per_item = self.random.choice([1, 2])
1111
+ self.max_items = max_items
1112
+ self.min_items = min_items
1113
+ if self.max_items < 2 or self.min_items < 2:
1114
+ raise Exception("The items allowed should at least be 2.")
1115
+ self.task_description = f"Retrieve the information mentioned in the following description from the report of the catalogs with the title {self.catalog_hashtag}. Using the information, follow the subsequent task steps mentioned. For all calculations, round of to the next highest integer first. For multiple modes, choose the highest value.\n"
1116
+ if self.level == 3:
1117
+ self.task_description += f"Follow the '{self.protocol_name}' protocol from the knowledge base for extra instructions.\n"
1118
+ self.short_description = "Retrieve catalog information and perform the mentioned task"
1119
+ self.min_catalog_item = min_catalog_item
1120
+ self.function = partial(
1121
+ get_infeasible_service_catalog_config, provide_reason=provide_reason
1122
+ )
1123
+ self.all_configs = self.all_configs()
1124
+
1125
+ @classmethod
1126
+ def all_configs(cls) -> List[dict]:
1127
+ with open(cls.config_path, "r") as f:
1128
+ return json.load(f)
1129
+
1130
+ def get_catalog_item_sysid(self, catalog_item: str) -> str:
1131
+ catalog_item_response = table_api_call(
1132
+ instance=self.instance,
1133
+ table="sc_cat_item",
1134
+ params={"sysparm_query": f"sys_name={catalog_item}", "sysparm_fields": "sys_id"},
1135
+ method="GET",
1136
+ )["result"]
1137
+ if len(catalog_item_response) == 0:
1138
+ raise Exception("Catalog item not found.")
1139
+ elif len(catalog_item_response) > 1:
1140
+ raise Exception("Multiple catalog items found.")
1141
+ return catalog_item_response[0]["sys_id"]
1142
+
1143
+ def create_report(
1144
+ self,
1145
+ user_roles=["itil"],
1146
+ ) -> None:
1147
+ catalog_item_list = list(META_CONFIGS.keys())
1148
+ catalog_item_list.remove(self.min_catalog_item)
1149
+ random_service_catalog_items = self.random.choice(
1150
+ catalog_item_list, self.random.randint(self.min_items, self.max_items), replace=False
1151
+ ).tolist()
1152
+ cat_item_sys_name = {
1153
+ "Developer Laptop (Mac)": "Developer Laptop (Mac)",
1154
+ "iPad mini": "iPad mini",
1155
+ "iPad pro": "iPad pro",
1156
+ "Sales Laptop": "Sales Laptop",
1157
+ "Standard Laptop": "Standard Laptop",
1158
+ "Apple Watch": "Apple Watch",
1159
+ "Apple MacBook Pro 15": 'Apple MacBook Pro 15"',
1160
+ "Development Laptop (PC)": "Development Laptop (PC)",
1161
+ "Loaner Laptop": "Notebook Computer Loaner",
1162
+ }
1163
+
1164
+ # shuffle
1165
+ self.random.shuffle(random_service_catalog_items)
1166
+ random_service_catalog_items = [
1167
+ self.min_catalog_item
1168
+ ] + random_service_catalog_items.tolist()
1169
+ self.random_service_catalog_items = random_service_catalog_items
1170
+
1171
+ service_catalog_report_config = {}
1172
+ service_catalog_report_config[random_service_catalog_items[0]] = {
1173
+ "quantity": self.min_number_per_item,
1174
+ "description": META_CONFIGS[random_service_catalog_items[0]]["desc"],
1175
+ "configuration": {},
1176
+ "item": random_service_catalog_items[0],
1177
+ "sys_id": self.get_catalog_item_sysid(
1178
+ cat_item_sys_name[random_service_catalog_items[0]]
1179
+ ),
1180
+ }
1181
+ service_catalog_report_config[random_service_catalog_items[-1]] = {
1182
+ "quantity": self.max_number_per_item,
1183
+ "description": META_CONFIGS[random_service_catalog_items[-1]]["desc"],
1184
+ "configuration": {},
1185
+ "item": random_service_catalog_items[-1],
1186
+ "sys_id": self.get_catalog_item_sysid(
1187
+ cat_item_sys_name[random_service_catalog_items[-1]]
1188
+ ),
1189
+ }
1190
+
1191
+ for service_catalog_item in random_service_catalog_items[1:-1]:
1192
+ service_catalog_report_config[service_catalog_item] = {
1193
+ "quantity": self.random.randint(
1194
+ self.min_number_per_item + 1, self.max_number_per_item - 1
1195
+ ),
1196
+ "description": META_CONFIGS[service_catalog_item]["desc"],
1197
+ "configuration": {},
1198
+ "item": service_catalog_item,
1199
+ "sys_id": self.get_catalog_item_sysid(cat_item_sys_name[service_catalog_item]),
1200
+ }
1201
+
1202
+ self.service_catalog_report_config = service_catalog_report_config
1203
+ created_request_items = []
1204
+ for (
1205
+ service_catalog_item,
1206
+ service_catalog_item_config,
1207
+ ) in service_catalog_report_config.items():
1208
+ for _ in range(service_catalog_item_config["quantity"]):
1209
+ request_item_dict = {
1210
+ "requested_for": self._base_user_sysid,
1211
+ "quantity": 1,
1212
+ "cat_item": service_catalog_item_config["sys_id"],
1213
+ }
1214
+ criteria_response = table_api_call(
1215
+ instance=self.instance,
1216
+ table="sc_req_item",
1217
+ json=request_item_dict,
1218
+ method="POST",
1219
+ )["result"]
1220
+ created_request_items.append((service_catalog_item, criteria_response["sys_id"]))
1221
+
1222
+ self.created_request_items = created_request_items
1223
+
1224
+ user_details = table_api_call(
1225
+ instance=self.instance,
1226
+ table="sys_user",
1227
+ params={
1228
+ "sysparm_query": f"sys_id={self._base_user_sysid}",
1229
+ "sysparm_fields": "first_name,last_name",
1230
+ },
1231
+ method="GET",
1232
+ )["result"][0]
1233
+ user_full_name = user_details["first_name"] + " " + user_details["last_name"]
1234
+
1235
+ self.report_sys_id, _ = create_report(
1236
+ instance=self.instance,
1237
+ table="sc_req_item",
1238
+ filter_hashtag=user_full_name,
1239
+ filter_field="requested_for",
1240
+ field="cat_item",
1241
+ plot_title=self.chart_title,
1242
+ random=self.random,
1243
+ )
1244
+
1245
+ def get_order_quantity_value(self) -> list[str]:
1246
+ quantities = [
1247
+ service_catalog_report_config_attribute["quantity"]
1248
+ for service_catalog_report_config_attribute in self.service_catalog_report_config.values()
1249
+ ]
1250
+ if self.question == "max":
1251
+ if max(quantities) != self.max_number_per_item:
1252
+ raise Exception("Maximum of quantities does not match attribute. Please check.")
1253
+ target_quantity = self.max_number_per_item
1254
+ elif self.question == "mean":
1255
+ mean_quantity = np.mean(quantities)
1256
+ target_quantity = int(np.ceil(mean_quantity))
1257
+ elif self.question == "median":
1258
+ target_quantity = int(np.ceil(np.median(quantities)))
1259
+ elif self.question == "mode":
1260
+ frequencies = {}
1261
+ for count in quantities:
1262
+ if count not in frequencies:
1263
+ frequencies[count] = 1
1264
+ else:
1265
+ frequencies[count] += 1
1266
+ sorted_frequencies = {
1267
+ count: frequency
1268
+ for count, frequency in sorted(
1269
+ frequencies.items(), key=lambda item: item[1], reverse=True
1270
+ )
1271
+ }
1272
+ max_frequency = list(sorted_frequencies.values())[0]
1273
+ max_frequencies = [
1274
+ count
1275
+ for count, frequency in sorted_frequencies.items()
1276
+ if frequency == max_frequency
1277
+ ]
1278
+ target_quantity = int(max(max_frequencies))
1279
+ if target_quantity - self.min_number_per_item <= 0:
1280
+ raise Exception("Unable to order quantity {target_quantity - self.min_number_per_item}")
1281
+ return int(target_quantity - self.min_number_per_item)
1282
+
1283
+ def set_compositional_task(self) -> None:
1284
+
1285
+ config = self.random.choice(self.all_configs)
1286
+ self.configuration = config["configuration"]
1287
+ order_config = {
1288
+ "configuration": self.configuration,
1289
+ "description": META_CONFIGS[self.min_catalog_item]["desc"],
1290
+ "item": self.min_catalog_item,
1291
+ "quantity": self.get_order_quantity_value(),
1292
+ }
1293
+ order_config, self.infeasible_reasons = self.function(
1294
+ config=order_config, random=self.random
1295
+ )
1296
+
1297
+ create_order_item_subtask = [
1298
+ AllMenuTask(
1299
+ instance=self.instance,
1300
+ fixed_config={
1301
+ "application": "Self-Service",
1302
+ "module": "Service Catalog",
1303
+ "url": "/now/nav/ui/classic/params/target/catalog_home.do",
1304
+ },
1305
+ is_validated=False,
1306
+ used_in_level_2=True,
1307
+ has_description=False,
1308
+ ),
1309
+ self.order_item_class(
1310
+ instance=self.instance,
1311
+ fixed_config=order_config,
1312
+ is_validated=False,
1313
+ used_in_level_2=True,
1314
+ ),
1315
+ ]
1316
+
1317
+ self.compositional_task = create_order_item_subtask
1318
+
1319
+ def setup_goal(self, page: Page) -> tuple[str, dict]:
1320
+ self.create_report()
1321
+ self.set_compositional_task()
1322
+ config = self.fixed_config if self.fixed_config else self._get_config()
1323
+ if self.level == 3:
1324
+ self.task_description = (
1325
+ self.task_description
1326
+ + f"Value to retrieve: {self.description_mapping[self.question]} of all the catalog items.\n"
1327
+ + f"Task: Place an order for requesting more of the least available item in the report. The quantity of the order should be such that the final quantity of this item matches the above retrieved value.\n\n"
1328
+ + self.final_private_task_instructions
1329
+ )
1330
+
1331
+ goal, info = super().setup_goal(
1332
+ page=page, config=config, build_pretty_print_description=False
1333
+ )
1334
+
1335
+ if self.level == 2:
1336
+ goal = (
1337
+ self.task_description
1338
+ + f"\n1. Navigate to the CMDB reports and look for the catalog report with the mentioned hashtag. \n"
1339
+ + f"\n2. Find the value which is the {self.description_mapping[self.question]} of the catalog items present in stock shown in the report. \n"
1340
+ + f"\n3. Navigate to Self-Service > Service Catalog. \n"
1341
+ + f"\n4. For the least available item in stock, place an order for extra items such that its quantity matches the value you found.\n"
1342
+ )
1343
+
1344
+ return goal, info
1345
+
1346
+ def teardown(self) -> None:
1347
+ # Delete the report
1348
+ db_delete_from_table(
1349
+ instance=self.instance,
1350
+ table="sys_report",
1351
+ sys_id=self.report_sys_id,
1352
+ )
1353
+ # Delete the request items
1354
+ for created_request_item in self.created_request_items:
1355
+ db_delete_from_table(
1356
+ instance=self.instance,
1357
+ table="sc_req_item",
1358
+ sys_id=created_request_item[1],
1359
+ )
1360
+ return super().teardown()
1361
+
1362
+
1363
+ class DashDoFinalTask:
1364
+ """Base class for dash do final tasks block tasks. Used to include these tasks across multiple superclasses."""
1365
+
1366
+ pass