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.
- 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 +1 -1
- 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 +1 -1
- browsergym/workarena/data_files/task_configs/impersonation_users.json +1 -1
- 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 +194 -12
- 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.1.dist-info → browsergym_workarena-0.3.1.dist-info}/METADATA +19 -18
- browsergym_workarena-0.3.1.dist-info/RECORD +138 -0
- {browsergym_workarena-0.2.1.dist-info → browsergym_workarena-0.3.1.dist-info}/entry_points.txt +1 -0
- browsergym_workarena-0.2.1.dist-info/RECORD +0 -85
- {browsergym_workarena-0.2.1.dist-info → browsergym_workarena-0.3.1.dist-info}/WHEEL +0 -0
- {browsergym_workarena-0.2.1.dist-info → browsergym_workarena-0.3.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,15 +5,20 @@ Tasks related to knowledge bases.
|
|
|
5
5
|
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
|
+
import re
|
|
8
9
|
|
|
9
10
|
from playwright.sync_api import Page
|
|
11
|
+
from typing import Tuple
|
|
10
12
|
from urllib import parse
|
|
11
13
|
|
|
12
14
|
from .base import AbstractServiceNowTask
|
|
13
|
-
from
|
|
15
|
+
from .comp_building_block import CompositionalBuildingBlockTask
|
|
16
|
+
from .utils.utils import check_url_suffix_match
|
|
17
|
+
|
|
18
|
+
from ..api.utils import table_api_call
|
|
19
|
+
from ..config import KB_FILEPATH, KB_CONFIG_PATH, KB_NAME, SNOW_BROWSER_TIMEOUT
|
|
14
20
|
from ..install import check_knowledge_base
|
|
15
21
|
from ..instance import SNowInstance
|
|
16
|
-
from .utils.utils import check_url_suffix_match
|
|
17
22
|
|
|
18
23
|
|
|
19
24
|
class KnowledgeBaseSearchTask(AbstractServiceNowTask):
|
|
@@ -32,7 +37,33 @@ class KnowledgeBaseSearchTask(AbstractServiceNowTask):
|
|
|
32
37
|
|
|
33
38
|
"""
|
|
34
39
|
|
|
35
|
-
def __init__(
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
instance=None,
|
|
43
|
+
fixed_config: dict = None,
|
|
44
|
+
is_correct: bool = True,
|
|
45
|
+
is_only_navigating: bool = False,
|
|
46
|
+
search_by_title: bool = False,
|
|
47
|
+
seed: int = None,
|
|
48
|
+
**kwargs,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Parameters:
|
|
52
|
+
-----------
|
|
53
|
+
instance: SNowInstance
|
|
54
|
+
The ServiceNow instance to run the task on.
|
|
55
|
+
fixed_config:
|
|
56
|
+
A fixed configuration for the task, if required.
|
|
57
|
+
is_correct: bool
|
|
58
|
+
Used for the compositional task.
|
|
59
|
+
If false, the answer is highlighted in 'red' instead of 'yellow' when using cheat.
|
|
60
|
+
is_only_navigating: bool
|
|
61
|
+
Used for the compositional task.
|
|
62
|
+
If we only are navigating and not searching, change the goal for the agent.
|
|
63
|
+
search_by_title: bool
|
|
64
|
+
Used for the compositional task.
|
|
65
|
+
If true, clicks on the article title using the article name, else opens the first article.
|
|
66
|
+
"""
|
|
36
67
|
super().__init__(
|
|
37
68
|
seed=seed,
|
|
38
69
|
instance=instance,
|
|
@@ -42,9 +73,18 @@ class KnowledgeBaseSearchTask(AbstractServiceNowTask):
|
|
|
42
73
|
# Load the knowledge base and check its integrity
|
|
43
74
|
with open(KB_FILEPATH, "r") as f:
|
|
44
75
|
self.kb_entries = json.load(f)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
76
|
+
if hasattr(self, "_base_initial_instance"):
|
|
77
|
+
_, requires_install, requires_delete = check_knowledge_base(
|
|
78
|
+
self._base_initial_instance, # if user does not have permission to view the kb then this breaks
|
|
79
|
+
kb_name=KB_NAME,
|
|
80
|
+
kb_data=self.kb_entries, # Need admin permissions to check
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
_, requires_install, requires_delete = check_knowledge_base(
|
|
84
|
+
SNowInstance(), # instance would be the non-admin instance here and this might break in case user does not have required permissions
|
|
85
|
+
kb_name=KB_NAME,
|
|
86
|
+
kb_data=self.kb_entries, # Need admin permissions to check
|
|
87
|
+
)
|
|
48
88
|
with open(KB_CONFIG_PATH, "r") as f:
|
|
49
89
|
self.all_configs = json.load(f)
|
|
50
90
|
if any([requires_install, requires_delete]):
|
|
@@ -53,6 +93,14 @@ class KnowledgeBaseSearchTask(AbstractServiceNowTask):
|
|
|
53
93
|
"See README for setup instructions."
|
|
54
94
|
)
|
|
55
95
|
self.fixed_config = fixed_config
|
|
96
|
+
self.config = None
|
|
97
|
+
|
|
98
|
+
# Attributes for compositional task
|
|
99
|
+
self.is_correct = is_correct
|
|
100
|
+
self.is_only_navigating = is_only_navigating
|
|
101
|
+
self.search_by_title = search_by_title
|
|
102
|
+
|
|
103
|
+
self.__dict__.update(kwargs)
|
|
56
104
|
|
|
57
105
|
def _wait_for_ready(self, page: Page) -> None:
|
|
58
106
|
"""
|
|
@@ -92,13 +140,33 @@ class KnowledgeBaseSearchTask(AbstractServiceNowTask):
|
|
|
92
140
|
self.answer = config["value"]
|
|
93
141
|
self.alternative_answers = config["alternative_answers"]
|
|
94
142
|
self.question = config["question"]
|
|
143
|
+
if self.search_by_title:
|
|
144
|
+
self.kb_article_title = config["kb_article_title"]
|
|
95
145
|
|
|
96
146
|
# Generate goal
|
|
97
|
-
|
|
147
|
+
if self.is_only_navigating:
|
|
148
|
+
goal = f'Navigate to a relevant article in the knowledge base by searching for: "{self.item}" and open the article: "{self.kb_article_title}"'
|
|
149
|
+
else:
|
|
150
|
+
goal = f'Answer the following question using the knowledge base: "{self.question}"'
|
|
98
151
|
info = {}
|
|
99
152
|
|
|
100
153
|
return goal, info
|
|
101
154
|
|
|
155
|
+
def get_pretty_printed_description(self) -> str:
|
|
156
|
+
"""
|
|
157
|
+
Get the task info for this task when used in a private task; Used in L3 compositional tasks.
|
|
158
|
+
called by subclasses
|
|
159
|
+
"""
|
|
160
|
+
class_name = self.__class__.__name__
|
|
161
|
+
class_name = class_name.replace("Task", "")
|
|
162
|
+
# Split the words
|
|
163
|
+
words = re.findall(r"[A-Z][^A-Z]*", class_name)
|
|
164
|
+
class_name_formatted = " ".join(words)
|
|
165
|
+
|
|
166
|
+
task_info = f"- {class_name_formatted}: {self.item} \n"
|
|
167
|
+
|
|
168
|
+
return task_info
|
|
169
|
+
|
|
102
170
|
def cheat(self, page: Page, chat_messages: list[str]) -> None:
|
|
103
171
|
super().cheat(page=page, chat_messages=chat_messages)
|
|
104
172
|
self._wait_for_ready(page)
|
|
@@ -112,7 +180,10 @@ class KnowledgeBaseSearchTask(AbstractServiceNowTask):
|
|
|
112
180
|
|
|
113
181
|
# Click on the article
|
|
114
182
|
with page.expect_navigation():
|
|
115
|
-
|
|
183
|
+
if self.search_by_title:
|
|
184
|
+
iframe.locator(f'a.kb-title:has-text("{self.kb_article_title}")').click()
|
|
185
|
+
else:
|
|
186
|
+
iframe.locator("a.kb-title").first.click()
|
|
116
187
|
|
|
117
188
|
# Color the query and answer (this is just for visualization, it changes nothing to the validation)
|
|
118
189
|
paragraphs = iframe.locator("p")
|
|
@@ -125,10 +196,16 @@ class KnowledgeBaseSearchTask(AbstractServiceNowTask):
|
|
|
125
196
|
self.item,
|
|
126
197
|
f'<span style="background-color: cyan;">{self.item}</span>',
|
|
127
198
|
)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
199
|
+
if self.is_correct:
|
|
200
|
+
inner_html = inner_html.replace(
|
|
201
|
+
str(self.answer),
|
|
202
|
+
f'<span style="background-color: yellow;">{self.answer}</span>',
|
|
203
|
+
)
|
|
204
|
+
else:
|
|
205
|
+
inner_html = inner_html.replace(
|
|
206
|
+
str(self.answer),
|
|
207
|
+
f'<span style="background-color: pink;">{self.answer}</span>',
|
|
208
|
+
)
|
|
132
209
|
paragraph.evaluate(f"element => element.innerHTML = `{inner_html}`")
|
|
133
210
|
break
|
|
134
211
|
|
|
@@ -137,16 +214,6 @@ class KnowledgeBaseSearchTask(AbstractServiceNowTask):
|
|
|
137
214
|
chat_messages.append({"role": "assistant", "message": str(self.answer)})
|
|
138
215
|
|
|
139
216
|
def validate(self, page: Page, chat_messages: list[str]) -> tuple[float, bool, str, dict]:
|
|
140
|
-
right_url = check_url_suffix_match(page, expected_url="target/kb", task=self)
|
|
141
|
-
if not right_url:
|
|
142
|
-
return (
|
|
143
|
-
0,
|
|
144
|
-
False,
|
|
145
|
-
"",
|
|
146
|
-
{
|
|
147
|
-
"message": f"The page is not in the right URL to validate task {self.__class__.__name__}."
|
|
148
|
-
},
|
|
149
|
-
)
|
|
150
217
|
if chat_messages and chat_messages[-1]["role"] == "assistant":
|
|
151
218
|
answer = chat_messages[-1]["message"]
|
|
152
219
|
else:
|
|
@@ -160,9 +227,133 @@ class KnowledgeBaseSearchTask(AbstractServiceNowTask):
|
|
|
160
227
|
accepted_answers = [a.lower() for a in [self.answer] + self.alternative_answers]
|
|
161
228
|
answer = answer.lower()
|
|
162
229
|
if any(a in answer for a in accepted_answers):
|
|
163
|
-
return
|
|
230
|
+
return (
|
|
231
|
+
1,
|
|
232
|
+
True,
|
|
233
|
+
"That is correct, thank you!",
|
|
234
|
+
{"message": "Correct answer."},
|
|
235
|
+
)
|
|
164
236
|
else:
|
|
165
|
-
return
|
|
237
|
+
return (
|
|
238
|
+
0,
|
|
239
|
+
False,
|
|
240
|
+
"",
|
|
241
|
+
{"message": "Incorrect answer provided by the assistant."},
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class AddCommentToKnowledgeArticleTask(AbstractServiceNowTask, CompositionalBuildingBlockTask):
|
|
246
|
+
"""
|
|
247
|
+
Task to add a comment to a knowledge base article. Only used as a part of the compositional task for edit knowledge base
|
|
248
|
+
Parameters:
|
|
249
|
+
-----------
|
|
250
|
+
instance: SNowInstance
|
|
251
|
+
The instance on which to create the record.
|
|
252
|
+
fixed_config: dict
|
|
253
|
+
Configuration to use for the task.
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
def __init__(
|
|
257
|
+
self, seed: int = None, instance=None, fixed_config: dict = None, **kwargs
|
|
258
|
+
) -> None:
|
|
259
|
+
super().__init__(
|
|
260
|
+
seed=seed,
|
|
261
|
+
instance=instance,
|
|
262
|
+
start_rel_url="/now/nav/ui/classic/params/target/kb?id=kb_home",
|
|
263
|
+
user_roles=[],
|
|
264
|
+
)
|
|
265
|
+
self.fixed_config = fixed_config
|
|
266
|
+
if self.fixed_config is None:
|
|
267
|
+
raise Exception("Please provide a config for the add comment task.")
|
|
268
|
+
self.__dict__.update(kwargs)
|
|
269
|
+
|
|
270
|
+
def setup_goal(self, page: Page) -> tuple[str, dict]:
|
|
271
|
+
super().setup_goal(page=page)
|
|
272
|
+
config = self.fixed_config
|
|
273
|
+
|
|
274
|
+
if "kb_article_title" not in config.keys():
|
|
275
|
+
raise Exception("Need title in config file...")
|
|
276
|
+
self.article_name = config["kb_article_title"]
|
|
277
|
+
adhoc_kb_response = table_api_call(
|
|
278
|
+
instance=self.instance, # admin permissions to contribute to the KB
|
|
279
|
+
table="kb_knowledge",
|
|
280
|
+
method="GET",
|
|
281
|
+
params={
|
|
282
|
+
"sysparm_query": f"short_description={self.article_name}",
|
|
283
|
+
},
|
|
284
|
+
)["result"]
|
|
285
|
+
if len(adhoc_kb_response) != 1:
|
|
286
|
+
raise Exception("Required article not found, please fix config...")
|
|
287
|
+
|
|
288
|
+
self.kb_article_sys_id = adhoc_kb_response[0]["sys_id"]
|
|
289
|
+
self.comment = config["comment"]
|
|
290
|
+
|
|
291
|
+
goal = f'Add the following comment to the knowledge base: "{self.comment}"'
|
|
292
|
+
info = {}
|
|
293
|
+
|
|
294
|
+
return goal, info
|
|
295
|
+
|
|
296
|
+
def _wait_for_ready(self, page: Page) -> None:
|
|
297
|
+
"""
|
|
298
|
+
Checks that the main iframe is fully loaded
|
|
299
|
+
|
|
300
|
+
"""
|
|
301
|
+
# TODO: We don't use the flag-based method used in other tasks
|
|
302
|
+
# because gsft_main doesn't have the event we register
|
|
303
|
+
# on this page. Not sure why.
|
|
304
|
+
logging.debug(f"Waiting for page to be fully loaded")
|
|
305
|
+
page.wait_for_load_state("networkidle")
|
|
306
|
+
page.wait_for_selector('iframe[name="gsft_main"]')
|
|
307
|
+
logging.debug(f"Detected page ready")
|
|
308
|
+
|
|
309
|
+
# Get main iframe
|
|
310
|
+
# XXX: We use a loop because sometimes the iframe evaluates to None
|
|
311
|
+
# even though we wait for it to be ready. This seems like a
|
|
312
|
+
# playwright bug.
|
|
313
|
+
timeout = SNOW_BROWSER_TIMEOUT
|
|
314
|
+
while timeout > 0:
|
|
315
|
+
iframe = page.frame(name="gsft_main")
|
|
316
|
+
if iframe:
|
|
317
|
+
break
|
|
318
|
+
page.wait_for_timeout(100)
|
|
319
|
+
timeout -= 100
|
|
320
|
+
else:
|
|
321
|
+
raise TimeoutError(
|
|
322
|
+
f"Timed out waiting for iframe to be ready in {self.instance.snow_url}"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
def get_pretty_printed_description(self) -> str:
|
|
326
|
+
"""
|
|
327
|
+
Get the task info for this task when used in a private task; Used in L3 compositional tasks.
|
|
328
|
+
called by subclasses
|
|
329
|
+
"""
|
|
330
|
+
class_name = self.__class__.__name__
|
|
331
|
+
class_name = class_name.replace("Task", "")
|
|
332
|
+
# Split the words
|
|
333
|
+
words = re.findall(r"[A-Z][^A-Z]*", class_name)
|
|
334
|
+
class_name_formatted = " ".join(words)
|
|
335
|
+
|
|
336
|
+
task_info = f"- {class_name_formatted}: Add the comment '{self.comment}' for the article with title {self.article_name} \n"
|
|
337
|
+
|
|
338
|
+
return task_info
|
|
339
|
+
|
|
340
|
+
def cheat(self, page: Page, chat_messages: list[str]) -> None:
|
|
341
|
+
super().cheat(page=page, chat_messages=chat_messages)
|
|
342
|
+
|
|
343
|
+
# Check if we need to do something else, gsft_main is not loading, it seems to load when navigating from the search, so might need for compositional tasks
|
|
344
|
+
self._wait_for_ready(page)
|
|
345
|
+
frame = page.frame("gsft_main")
|
|
346
|
+
frame.locator("button.comment-text").click()
|
|
347
|
+
frame.frame_locator('iframe[title="Rich Text Area"]').locator("html").click()
|
|
348
|
+
frame.frame_locator('iframe[title="Rich Text Area"]').get_by_label("Comments").fill(
|
|
349
|
+
self.comment
|
|
350
|
+
)
|
|
351
|
+
frame.get_by_role("button", name="Submit").click()
|
|
352
|
+
|
|
353
|
+
def validate(self, page: Page, chat_messages: list[str]) -> Tuple[float, bool, str, dict]:
|
|
354
|
+
return super().validate(page, chat_messages)
|
|
166
355
|
|
|
167
356
|
|
|
168
|
-
__TASKS__ = [
|
|
357
|
+
__TASKS__ = [
|
|
358
|
+
KnowledgeBaseSearchTask,
|
|
359
|
+
]
|