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
@@ -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 ..config import KB_NAME, KB_FILEPATH, KB_CONFIG_PATH, SNOW_BROWSER_TIMEOUT
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__(self, seed: int, instance=None, fixed_config: dict = None) -> None:
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
- _, requires_install, requires_delete = check_knowledge_base(
46
- self.instance, kb_data=self.kb_entries, kb_name=KB_NAME
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
- goal = f'Answer the following question using the knowledge base: "{self.question}"'
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
- iframe.locator("a.kb-title").first.click()
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
- inner_html = inner_html.replace(
129
- str(self.answer),
130
- f'<span style="background-color: yellow;">{self.answer}</span>',
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 1, True, "That is correct, thank you!", {"message": "Correct answer."}
230
+ return (
231
+ 1,
232
+ True,
233
+ "That is correct, thank you!",
234
+ {"message": "Correct answer."},
235
+ )
164
236
  else:
165
- return 0, False, "", {"message": "Incorrect answer provided by the assistant."}
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__ = [KnowledgeBaseSearchTask]
357
+ __TASKS__ = [
358
+ KnowledgeBaseSearchTask,
359
+ ]