browsergym-workarena 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- browsergym/workarena/__init__.py +13 -1
- browsergym/workarena/api/category.py +74 -0
- browsergym/workarena/api/change_request.py +87 -0
- browsergym/workarena/api/computer_asset.py +90 -0
- browsergym/workarena/api/cost_center.py +19 -0
- browsergym/workarena/api/expense_line.py +89 -0
- browsergym/workarena/api/incident.py +45 -0
- browsergym/workarena/api/knowledge.py +29 -0
- browsergym/workarena/api/problem.py +90 -0
- browsergym/workarena/api/report.py +183 -0
- browsergym/workarena/api/requested_items.py +63 -0
- browsergym/workarena/api/user.py +11 -8
- browsergym/workarena/api/utils.py +47 -3
- browsergym/workarena/config.py +21 -1
- browsergym/workarena/data_files/setup_files/forms/expected_incident_form_fields.json +1 -1
- browsergym/workarena/data_files/setup_files/forms/expected_request_item_form_fields.json +1 -0
- browsergym/workarena/data_files/setup_files/knowledge/protocols.json +46 -0
- browsergym/workarena/data_files/setup_files/knowledge/test.html +1 -0
- browsergym/workarena/data_files/setup_files/lists/expected_asset_list_columns.json +2 -24
- browsergym/workarena/data_files/setup_files/lists/expected_change_request_list_columns.json +4 -40
- browsergym/workarena/data_files/setup_files/lists/expected_expense_line_list_columns.json +12 -0
- browsergym/workarena/data_files/setup_files/lists/expected_hardware_list_columns.json +1 -42
- browsergym/workarena/data_files/setup_files/lists/expected_incident_list_columns.json +2 -18
- browsergym/workarena/data_files/setup_files/lists/expected_problem_list_columns.json +12 -0
- browsergym/workarena/data_files/setup_files/lists/expected_requested_items_list_columns.json +12 -0
- browsergym/workarena/data_files/setup_files/lists/expected_service_catalog_list_columns.json +2 -19
- browsergym/workarena/data_files/setup_files/lists/expected_user_list_columns.json +3 -50
- browsergym/workarena/data_files/task_configs/all_menu.json +95 -95
- browsergym/workarena/data_files/task_configs/dashboard_retrieval_minmax_task.json +1 -1
- browsergym/workarena/data_files/task_configs/dashboard_retrieval_value_task.json +1 -1
- browsergym/workarena/data_files/task_configs/filter_service_catalog_item_list_task.json +7986 -7982
- browsergym/workarena/data_files/task_configs/impersonation_users.json +3 -3
- browsergym/workarena/data_files/task_configs/report_retrieval_minmax_task.json +1 -1
- browsergym/workarena/data_files/task_configs/report_retrieval_value_task.json +1 -1
- browsergym/workarena/human_eval/console.js +176 -0
- browsergym/workarena/human_eval/tool.py +366 -0
- browsergym/workarena/install.py +81 -20
- browsergym/workarena/tasks/base.py +55 -20
- browsergym/workarena/tasks/comp_building_block.py +4 -0
- browsergym/workarena/tasks/compositional/__init__.py +76 -0
- browsergym/workarena/tasks/compositional/base.py +364 -0
- browsergym/workarena/tasks/compositional/dash_do_base.py +1366 -0
- browsergym/workarena/tasks/compositional/dash_do_catalog.py +1127 -0
- browsergym/workarena/tasks/compositional/dash_do_catalog_infeasible.py +2047 -0
- browsergym/workarena/tasks/compositional/dash_do_create_incident.py +403 -0
- browsergym/workarena/tasks/compositional/dash_do_create_incident_infeasible.py +278 -0
- browsergym/workarena/tasks/compositional/dash_do_create_problem.py +336 -0
- browsergym/workarena/tasks/compositional/dash_do_create_problem_infeasible.py +235 -0
- browsergym/workarena/tasks/compositional/dash_do_filter.py +1600 -0
- browsergym/workarena/tasks/compositional/dash_do_request_item.py +1315 -0
- browsergym/workarena/tasks/compositional/dash_do_request_item_infeasible.py +693 -0
- browsergym/workarena/tasks/compositional/delete_record.py +341 -0
- browsergym/workarena/tasks/compositional/edit_knowledge_base.py +457 -0
- browsergym/workarena/tasks/compositional/expense_management.py +598 -0
- browsergym/workarena/tasks/compositional/filter_and_do.py +139 -0
- browsergym/workarena/tasks/compositional/find_and_order_item.py +345 -0
- browsergym/workarena/tasks/compositional/manage_change_request_schedule.py +1417 -0
- browsergym/workarena/tasks/compositional/mark_duplicate_problems.py +499 -0
- browsergym/workarena/tasks/compositional/maximize_investment_return.py +1763 -0
- browsergym/workarena/tasks/compositional/navigate_and_do.py +1151 -0
- browsergym/workarena/tasks/compositional/navigate_and_do_infeasible.py +2100 -0
- browsergym/workarena/tasks/compositional/offboard_user.py +207 -0
- browsergym/workarena/tasks/compositional/onboard_user.py +226 -0
- browsergym/workarena/tasks/compositional/update_task.py +145 -0
- browsergym/workarena/tasks/compositional/utils/curriculum.py +215 -0
- browsergym/workarena/tasks/compositional/utils/infeasible_configs.py +151 -0
- browsergym/workarena/tasks/compositional/utils/knapsack.py +192 -0
- browsergym/workarena/tasks/compositional/warranty_check.py +227 -0
- browsergym/workarena/tasks/compositional/work_assignment.py +804 -0
- browsergym/workarena/tasks/compositional/workload_balancing.py +396 -0
- browsergym/workarena/tasks/dashboard.py +188 -8
- browsergym/workarena/tasks/form.py +1024 -232
- browsergym/workarena/tasks/knowledge.py +216 -25
- browsergym/workarena/tasks/list.py +519 -102
- browsergym/workarena/tasks/mark_duplicate_problem.py +171 -0
- browsergym/workarena/tasks/navigation.py +55 -13
- browsergym/workarena/tasks/scripts/extract_all_menu_items.py +9 -2
- browsergym/workarena/tasks/scripts/generate_dashboard_configs.py +6 -5
- browsergym/workarena/tasks/scripts/service_catalog.py +2 -1
- browsergym/workarena/tasks/scripts/validate.py +8 -2
- browsergym/workarena/tasks/send_chat_message.py +90 -0
- browsergym/workarena/tasks/service_catalog.py +94 -26
- browsergym/workarena/tasks/utils/form.py +1 -4
- browsergym/workarena/tasks/utils/private_tasks.py +63 -0
- browsergym/workarena/tasks/utils/utils.py +13 -0
- {browsergym_workarena-0.2.0.dist-info → browsergym_workarena-0.3.0.dist-info}/METADATA +27 -20
- browsergym_workarena-0.3.0.dist-info/RECORD +138 -0
- {browsergym_workarena-0.2.0.dist-info → browsergym_workarena-0.3.0.dist-info}/entry_points.txt +1 -0
- browsergym_workarena-0.2.0.dist-info/RECORD +0 -85
- {browsergym_workarena-0.2.0.dist-info → browsergym_workarena-0.3.0.dist-info}/WHEEL +0 -0
- {browsergym_workarena-0.2.0.dist-info → browsergym_workarena-0.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|