browsergym-workarena 0.1.0rc6__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. browsergym/workarena/__init__.py +7 -2
  2. browsergym/workarena/api/ui_themes.py +35 -0
  3. browsergym/workarena/api/user.py +153 -0
  4. browsergym/workarena/api/utils.py +1 -1
  5. browsergym/workarena/config.py +43 -1
  6. browsergym/workarena/data_files/setup_files/lists/expected_asset_list_columns.json +34 -1
  7. browsergym/workarena/data_files/setup_files/lists/expected_change_request_list_columns.json +48 -1
  8. browsergym/workarena/data_files/setup_files/lists/expected_hardware_list_columns.json +53 -1
  9. browsergym/workarena/data_files/setup_files/lists/expected_incident_list_columns.json +28 -1
  10. browsergym/workarena/data_files/setup_files/lists/expected_service_catalog_list_columns.json +29 -1
  11. browsergym/workarena/data_files/setup_files/ui_themes/workarena_themes.xml +2313 -0
  12. browsergym/workarena/data_files/task_configs/dashboard_retrieval_minmax_task.json +1 -0
  13. browsergym/workarena/data_files/task_configs/dashboard_retrieval_value_task.json +1 -0
  14. browsergym/workarena/data_files/task_configs/report_retrieval_minmax_task.json +1 -0
  15. browsergym/workarena/data_files/task_configs/report_retrieval_value_task.json +1 -0
  16. browsergym/workarena/data_files/task_configs/sort_asset_list_task.json +547 -11391
  17. browsergym/workarena/data_files/task_configs/sort_change_request_list_task.json +558 -11090
  18. browsergym/workarena/data_files/task_configs/sort_hardware_list_task.json +576 -11162
  19. browsergym/workarena/data_files/task_configs/sort_incident_list_task.json +528 -11172
  20. browsergym/workarena/data_files/task_configs/sort_service_catalog_item_list_task.json +533 -11491
  21. browsergym/workarena/data_files/task_configs/sort_user_list_task.json +568 -10582
  22. browsergym/workarena/install.py +625 -153
  23. browsergym/workarena/tasks/base.py +85 -26
  24. browsergym/workarena/tasks/dashboard.py +620 -0
  25. browsergym/workarena/tasks/form.py +127 -90
  26. browsergym/workarena/tasks/knowledge.py +30 -14
  27. browsergym/workarena/tasks/list.py +157 -65
  28. browsergym/workarena/tasks/navigation.py +18 -16
  29. browsergym/workarena/tasks/scripts/generate_dashboard_configs.py +272 -0
  30. browsergym/workarena/tasks/scripts/generate_forms.py +2 -2
  31. browsergym/workarena/tasks/scripts/list.py +33 -9
  32. browsergym/workarena/tasks/scripts/validate.py +2 -2
  33. browsergym/workarena/tasks/service_catalog.py +106 -74
  34. browsergym/workarena/tasks/utils/form.py +5 -3
  35. browsergym/workarena/tasks/utils/js_utils.js +123 -2
  36. browsergym/workarena/tasks/utils/string.py +15 -0
  37. browsergym/workarena/tasks/utils/utils.py +20 -0
  38. browsergym/workarena/utils.py +31 -2
  39. {browsergym_workarena-0.1.0rc6.dist-info → browsergym_workarena-0.2.0.dist-info}/METADATA +7 -3
  40. {browsergym_workarena-0.1.0rc6.dist-info → browsergym_workarena-0.2.0.dist-info}/RECORD +43 -32
  41. {browsergym_workarena-0.1.0rc6.dist-info → browsergym_workarena-0.2.0.dist-info}/WHEEL +1 -1
  42. {browsergym_workarena-0.1.0rc6.dist-info → browsergym_workarena-0.2.0.dist-info}/entry_points.txt +0 -0
  43. {browsergym_workarena-0.1.0rc6.dist-info → browsergym_workarena-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -7,6 +7,7 @@ from english_words import get_english_words_set
7
7
  from playwright.sync_api._generated import Page
8
8
  from time import sleep
9
9
  from typing import List, Tuple
10
+ from urllib import parse
10
11
 
11
12
  from ..api.utils import (
12
13
  db_delete_from_table,
@@ -32,6 +33,7 @@ from ..config import (
32
33
  )
33
34
  from ..instance import SNowInstance
34
35
  from .utils.form import fill_text
36
+ from .utils.utils import check_url_suffix_match
35
37
 
36
38
 
37
39
  ENGLISH_WORDS = list(get_english_words_set(["web2"]))
@@ -40,6 +42,7 @@ ENGLISH_WORDS = list(get_english_words_set(["web2"]))
40
42
  class ServiceNowFormTask(AbstractServiceNowTask):
41
43
  def __init__(
42
44
  self,
45
+ seed: int,
43
46
  start_rel_url,
44
47
  instance: SNowInstance = None,
45
48
  extra_mandatory_fields: List = [],
@@ -68,14 +71,13 @@ class ServiceNowFormTask(AbstractServiceNowTask):
68
71
  # Prohibited fields: fields that we shouldn't interact with
69
72
  self.prohibited_fields = prohibited_fields
70
73
 
71
- super().__init__(instance=instance, start_rel_url=start_rel_url)
74
+ super().__init__(seed=seed, instance=instance, start_rel_url=start_rel_url)
72
75
 
73
76
  def _get_form(self, page):
74
77
  """
75
78
  Loads a bunch of info about the form on a page into object variables
76
79
 
77
80
  """
78
- self._wait_for_ready(page)
79
81
 
80
82
  # Extract Glide table information
81
83
  logging.debug("Extracting Glide table metadata")
@@ -106,10 +108,19 @@ class ServiceNowFormTask(AbstractServiceNowTask):
106
108
  )["result"][0]["label"].lower()
107
109
 
108
110
  # Get the form fields
111
+ def is_field_visible(field):
112
+ return page.evaluate(
113
+ f"""
114
+ {self.form_js_selector}.isVisible(
115
+ {self.form_js_selector}.getGlideUIElement('{field}'),
116
+ {self.form_js_selector}.getControl('{field}')
117
+ );"""
118
+ )
119
+
109
120
  logging.debug("Extracting valid form fields")
110
121
  editable_fields = page.evaluate(f"{self.form_js_selector}.getEditableFields()")
111
122
  field_elements = page.evaluate(f"{self.form_js_selector}.elements")
112
- all_visible_fields = [f["fieldName"] for f in field_elements]
123
+ all_fields = [f["fieldName"] for f in field_elements]
113
124
  self.fields = {
114
125
  f["fieldName"]: f
115
126
  for f in field_elements
@@ -130,16 +141,25 @@ class ServiceNowFormTask(AbstractServiceNowTask):
130
141
  self.optional_fields = [f for f in set(self.fields.keys()) - set(self.mandatory_fields)]
131
142
 
132
143
  # Sanity check
133
- assert len(self.fields) > 0, "No editable fields found."
144
+ assert len(self.fields) > 0, "No fields found on page."
145
+ assert len(editable_fields) > 0, "No editable fields found on page."
146
+ # ... check that the script that marks some fields as mandatory worked
134
147
  assert set(self.extra_mandatory_fields) <= set(
135
148
  self.mandatory_fields
136
149
  ), "Some extra mandatory fields are not mandatory in the form."
150
+ # ... check that the script that makes some fields read-only worked
137
151
  assert all(
138
152
  f not in self.fields for f in self.prohibited_fields
139
153
  ), "Some prohibited fields are editable in the form."
140
- assert set(all_visible_fields) == set(
141
- self.expected_fields
142
- ), "Some fields are missing from the form., Re-run workarena-install to correct this."
154
+ # ... check that all the fields that the config expects are present and that extra fields are not visible
155
+ all_visible_fields = set([f for f in all_fields if is_field_visible(f)])
156
+ expected_visible_fields = set([f for f in self.expected_fields if is_field_visible(f)])
157
+ set_diff = all_visible_fields.union(
158
+ expected_visible_fields
159
+ ) - all_visible_fields.intersection(expected_visible_fields)
160
+ assert (
161
+ len(set_diff) == 0
162
+ ), f"The fields {set_diff} are either missing or unexpectedly visible on the form. Re-run 'workarena-install' to correct this."
143
163
 
144
164
  def _preprocess_fields(self, field, value):
145
165
  """
@@ -174,12 +194,12 @@ class ServiceNowFormTask(AbstractServiceNowTask):
174
194
 
175
195
  def _wait_for_ready(self, page: Page) -> None:
176
196
  """
177
- Waits for the main iframe to be fully loaded
197
+ Waits for the main iframe to be fully loaded.
178
198
 
179
199
  """
180
200
  logging.debug(f"Waiting for {self.js_prefix} to be fully loaded")
181
201
  page.wait_for_function(
182
- f"typeof window.{self.js_prefix} !== 'undefined' && window.{self.js_prefix}.WORKARENA_LOAD_COMPLETE"
202
+ f"typeof window.{self.js_prefix} !== 'undefined' && window.{self.js_prefix}.WORKARENA_LOAD_COMPLETE",
183
203
  )
184
204
  logging.debug(f"Detected {self.js_prefix} ready")
185
205
 
@@ -191,33 +211,28 @@ class ServiceNowFormTask(AbstractServiceNowTask):
191
211
  page.wait_for_function(f"typeof window.{self.js_prefix}.g_tabs2Sections !== 'undefined'")
192
212
  logging.debug("Detected Glide tabs API ready")
193
213
 
194
- def pre_setup(self, seed: int, page: Page):
195
- super().pre_setup(seed, page)
196
-
197
- # Register a few initialization scripts
198
- self._add_init_scripts_to_context_and_reload(
199
- page,
200
- [
201
- "registerGsftMainLoaded();",
202
- # ... Mark the extra mandatory fields as such
203
- f"""
204
- // Check that the script is running in the main iframe
205
- if (window.frameElement?.id === '{self.js_prefix}') {{
206
- waLog('Setting mandatory fields');
207
- waitForCondition(() => typeof {self.js_api_forms} !== 'undefined', 100)
208
- .then(waitForCondition(() => typeof window.WORKARENA_LOAD_COMPLETE !== 'undefined' && window.WORKARENA_LOAD_COMPLETE, 100)
209
- .then(
210
- function (){{
211
- {';'.join([self.js_api_forms + '.setMandatory("' + f + '", true)' for f in self.extra_mandatory_fields])}
212
- waLog('Mandatory fields set successfully.');
213
- }}
214
- )
215
- );
216
- }}
217
- """,
218
- ],
219
- )
214
+ def get_init_scripts(self) -> List[str]:
215
+ # Extract expected URL suffix
216
+ url_suffix = parse.urlparse(self.start_url).path.split("/")[-1]
217
+
218
+ # Add a few initialization scripts
219
+ return super().get_init_scripts() + [
220
+ "registerGsftMainLoaded();",
221
+ # ... Mark the extra mandatory fields as such
222
+ f"""
223
+ function addFormMandatoryFields() {{
224
+ waLog('Setting mandatory fields', 'addFormMandatoryFields');
225
+ {";".join([f"{self.js_api_forms}.setMandatory('{f}', true)" for f in self.extra_mandatory_fields])}
226
+ waLog('Mandatory fields set successfully.', 'addFormMandatoryFields');
227
+ }}
228
+
229
+ runInGsftMainOnlyAndProtectByURL(addFormMandatoryFields, '{url_suffix}');
230
+ """,
231
+ ]
220
232
 
233
+ def start(self, page: Page) -> None:
234
+ super().start(page)
235
+ self._wait_for_ready(page)
221
236
  self._get_form(page)
222
237
 
223
238
 
@@ -252,7 +267,9 @@ class GenericNewRecordTask(ServiceNowFormTask):
252
267
 
253
268
  def __init__(
254
269
  self,
270
+ seed: int,
255
271
  form_url: str,
272
+ table_label: str,
256
273
  instance: SNowInstance = None,
257
274
  extra_mandatory_fields: List = [],
258
275
  prohibited_fields: List = [],
@@ -264,6 +281,7 @@ class GenericNewRecordTask(ServiceNowFormTask):
264
281
  expected_fields_path: str = None,
265
282
  ) -> None:
266
283
  super().__init__(
284
+ seed=seed,
267
285
  instance=instance,
268
286
  start_rel_url=form_url,
269
287
  extra_mandatory_fields=extra_mandatory_fields,
@@ -271,6 +289,9 @@ class GenericNewRecordTask(ServiceNowFormTask):
271
289
  )
272
290
  self.form_url = form_url
273
291
 
292
+ # Table pretty printed name
293
+ self.table_label = table_label
294
+
274
295
  # Key in which the sys_id of the created record will be stored in the local storage
275
296
  self.session_sys_id_field = f"{id(self)}.record_sys_id"
276
297
 
@@ -294,67 +315,65 @@ class GenericNewRecordTask(ServiceNowFormTask):
294
315
  with open(expected_fields_path, "r") as f:
295
316
  self.expected_fields = json.load(f)
296
317
 
297
- def setup(self, seed: int, page: Page) -> tuple[str, dict]:
298
- self.pre_setup(seed, page)
299
- self._run_init_scripts(page)
318
+ def get_init_scripts(self) -> List[str]:
319
+ # Extract expected URL suffix
320
+ url_suffix = parse.urlparse(self.start_url).path.split("/")[-1]
321
+
322
+ # Add a few initialization scripts
323
+ return super().get_init_scripts() + [
324
+ f"""
325
+ function patchSubmitButton() {{
326
+ waLog('Attempting to override form submit function', 'patchSubmitButton');
327
+ // Save the original function if it hasn't been saved yet
328
+ if(typeof old_gsftSubmit == 'undefined'){{
329
+ old_gsftSubmit = new Function('return ' + gsftSubmit.toString())();
330
+ waLog('Saved original submit function', 'patchSubmitButton');
331
+ }}
332
+
333
+ // Override the function to save the sys_id in the local storage
334
+ gsftSubmit = function(control, form, action_name) {{
335
+ localStorage['{self.session_sys_id_field}'] = {self.js_api_forms}.getUniqueValue();
336
+ old_gsftSubmit(control, form, action_name);
337
+ }};
338
+ waLog('Patched submit function. All done.', 'patchSubmitButton');
339
+ }}
340
+
341
+ runInGsftMainOnlyAndProtectByURL(patchSubmitButton, '{url_suffix}');
342
+ """
343
+ ]
344
+
345
+ def setup_goal(self, page: Page) -> tuple[str, dict]:
346
+ super().setup_goal(page=page)
347
+
348
+ # Get the task configuration
300
349
  assert self.all_configs is not None, "No configuration available for the task."
301
350
  config = self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
302
-
303
351
  self.template_record = config["template_record"]
352
+ for f, func in self.unique_valued_fields.items():
353
+ self.template_record[f] = func(self.template_record[f])
304
354
  self.task_fields = config["task_fields"]
305
-
306
355
  self.created_sysids = []
307
356
 
308
- # generate the goal
357
+ # Generate the goal
309
358
  goal = (
310
359
  f"Create a new {self.table_label} with "
311
360
  + " and ".join(
312
361
  [
313
- f'a value of "{self.template_record[f]}"' + f' for field "{self.fields[f]}"'
362
+ f'a value of "{self.template_record[f]}"'
363
+ + f' for field "{config["fields"][f]}"'
314
364
  for f in self.task_fields
315
365
  ]
316
366
  )
317
367
  + "."
318
368
  )
319
369
  info = {}
320
- return goal, info
321
370
 
322
- def _run_init_scripts(self, page: Page) -> None:
323
- self._add_init_scripts_to_context_and_reload(
324
- page,
325
- [
326
- f"""
327
- // Check that the script is running in the main iframe
328
- if (window.frameElement?.id === '{self.js_prefix}') {{
329
- waLog('Attempting to override form submit function');
330
- waitForCondition(() => typeof {self.js_api_forms} !== 'undefined', 100)
331
- .then(waitForCondition(() => typeof gsftSubmit !== 'undefined', 100)
332
- .then(
333
- function overrideSubmit(){{
334
- // Save the original function if it hasn't been saved yet
335
- if(typeof old_gsftSubmit == 'undefined'){{
336
- old_gsftSubmit = new Function('return ' + gsftSubmit.toString())();
337
- waLog('Saved original submit function');
338
- }}
339
-
340
- // Override the function to save the sys_id in the local storage
341
- gsftSubmit = function(control, form, action_name) {{
342
- localStorage['{self.session_sys_id_field}'] = {self.js_api_forms}.getUniqueValue();
343
- old_gsftSubmit(control, form, action_name);
344
- }};
345
- waLog('Patched submit function. All done.');
346
- }}
347
- )
348
- );
349
- }}
350
- """
351
- ],
352
- )
371
+ return goal, info
353
372
 
354
- def _generate_random_config(self, seed: int, page: Page) -> None:
373
+ def _generate_random_config(self, page: Page) -> None:
355
374
  """Generate a random configuration for the task."""
356
- self.pre_setup(seed, page)
357
- self._run_init_scripts(page)
375
+ self.setup(page=page)
376
+
358
377
  # Determine task fields
359
378
  logging.debug("Determining task fields")
360
379
  # ... check that we have enough fields
@@ -479,7 +498,7 @@ class GenericNewRecordTask(ServiceNowFormTask):
479
498
  return goal, info
480
499
 
481
500
  def cheat(self, page: Page, chat_messages: list[str]) -> None:
482
- super().cheat(page, chat_messages)
501
+ super().cheat(page=page, chat_messages=chat_messages)
483
502
  self._wait_for_ready(page)
484
503
  iframe = page.frame_locator(f'iframe[name="{self.js_prefix}"]')
485
504
 
@@ -519,11 +538,11 @@ class GenericNewRecordTask(ServiceNowFormTask):
519
538
  if section_id not in tab_sections:
520
539
  return
521
540
 
522
- page.evaluate(
541
+ page.evaluate_handle(
523
542
  f"""{self.js_prefix}.g_tabs2Sections.tabsTabs[
524
543
  {tab_sections[section_id]}
525
- ].element.click()"""
526
- )
544
+ ].element"""
545
+ ).click(force=True)
527
546
 
528
547
  for field in self.task_fields:
529
548
  # Get the field's input control
@@ -568,8 +587,17 @@ class GenericNewRecordTask(ServiceNowFormTask):
568
587
  that are not part of the task.
569
588
 
570
589
  """
571
- self._wait_for_ready(page)
572
-
590
+ # check that the page is at the right url
591
+ right_url = check_url_suffix_match(page, expected_url=self.start_url, task=self)
592
+ if not right_url:
593
+ return (
594
+ 0,
595
+ False,
596
+ "",
597
+ {
598
+ "message": f"The page is not in the right URL to validate task {self.__class__.__name__}."
599
+ },
600
+ )
573
601
  # Retrieve the created record's sys_id from the session storage
574
602
  sys_id = page.evaluate("localStorage").get(self.session_sys_id_field, None)
575
603
 
@@ -627,7 +655,7 @@ class GenericNewRecordTask(ServiceNowFormTask):
627
655
  logging.info(
628
656
  f'The field "{self.fields[f]["label"]}" has the wrong value. Expected: "{self.template_record[f]}", got: "{record[f]}".'
629
657
  )
630
- error_msg = (f'The field "{self.fields[f]["label"]}" has the wrong value.',)
658
+ error_msg = f'The field "{self.fields[f]["label"]}" has the wrong value.'
631
659
  return (
632
660
  0,
633
661
  True, # End episode (incorrect information pushed to the DB)
@@ -638,7 +666,6 @@ class GenericNewRecordTask(ServiceNowFormTask):
638
666
  return 1, True, "Nice work, thank you!", {"message": "The record was successfully created."}
639
667
 
640
668
  def teardown(self) -> None:
641
- self._wait_for_ready(self.page)
642
669
 
643
670
  # Retrieve the current record's sys_id from the session storage
644
671
  sys_id = self.page.evaluate("localStorage").get(self.session_sys_id_field, None)
@@ -664,10 +691,12 @@ class CreateChangeRequestTask(GenericNewRecordTask):
664
691
 
665
692
  """
666
693
 
667
- def __init__(self, instance=None, fixed_config: dict = None) -> None:
694
+ def __init__(self, seed: int, instance=None, fixed_config: dict = None) -> None:
668
695
  super().__init__(
696
+ seed=seed,
669
697
  instance=instance,
670
698
  form_url="/now/nav/ui/classic/params/target/change_request.do",
699
+ table_label="change request",
671
700
  prohibited_fields=["chg_model", "state"],
672
701
  fixed_config=fixed_config,
673
702
  config_path=CREATE_CHANGE_REQUEST_CONFIG_PATH,
@@ -681,10 +710,12 @@ class CreateIncidentTask(GenericNewRecordTask):
681
710
 
682
711
  """
683
712
 
684
- def __init__(self, instance=None, fixed_config: dict = None) -> None:
713
+ def __init__(self, seed: int, instance=None, fixed_config: dict = None) -> None:
685
714
  super().__init__(
715
+ seed=seed,
686
716
  instance=instance,
687
717
  form_url="/now/nav/ui/classic/params/target/incident.do",
718
+ table_label="incident",
688
719
  prohibited_fields=["state"],
689
720
  fixed_config=fixed_config,
690
721
  config_path=CREATE_INCIDENT_CONFIG_PATH,
@@ -698,10 +729,12 @@ class CreateHardwareAssetTask(GenericNewRecordTask):
698
729
 
699
730
  """
700
731
 
701
- def __init__(self, instance=None, fixed_config: dict = None) -> None:
732
+ def __init__(self, seed: int, instance=None, fixed_config: dict = None) -> None:
702
733
  super().__init__(
734
+ seed=seed,
703
735
  instance=instance,
704
736
  form_url="/now/nav/ui/classic/params/target/alm_hardware.do",
737
+ table_label="hardware asset",
705
738
  prohibited_fields=["install_status"],
706
739
  extra_mandatory_fields=[
707
740
  "model",
@@ -722,10 +755,12 @@ class CreateProblemTask(GenericNewRecordTask):
722
755
 
723
756
  """
724
757
 
725
- def __init__(self, instance=None, fixed_config: dict = None) -> None:
758
+ def __init__(self, seed: int, instance=None, fixed_config: dict = None) -> None:
726
759
  super().__init__(
760
+ seed=seed,
727
761
  instance=instance,
728
762
  form_url="/now/nav/ui/classic/params/target/problem.do",
763
+ table_label="problem",
729
764
  prohibited_fields=["state", "first_reported_by_task"],
730
765
  fixed_config=fixed_config,
731
766
  config_path=CREATE_PROBLEM_CONFIG_PATH,
@@ -743,10 +778,12 @@ class CreateUserTask(GenericNewRecordTask):
743
778
 
744
779
  """
745
780
 
746
- def __init__(self, instance=None, fixed_config: dict = None) -> None:
781
+ def __init__(self, seed: int, instance=None, fixed_config: dict = None) -> None:
747
782
  super().__init__(
783
+ seed=seed,
748
784
  instance=instance,
749
785
  form_url="/now/nav/ui/classic/params/target/sys_user.do",
786
+ table_label="user",
750
787
  extra_mandatory_fields=["user_name", "first_name", "last_name", "email"],
751
788
  unique_valued_fields={"user_name": lambda x: str(hash(x + self.unique_id))},
752
789
  fixed_config=fixed_config,
@@ -7,11 +7,13 @@ import json
7
7
  import logging
8
8
 
9
9
  from playwright.sync_api import Page
10
+ from urllib import parse
10
11
 
11
12
  from .base import AbstractServiceNowTask
12
- from ..config import KB_FILEPATH, KB_CONFIG_PATH, SNOW_BROWSER_TIMEOUT
13
+ from ..config import KB_NAME, KB_FILEPATH, KB_CONFIG_PATH, SNOW_BROWSER_TIMEOUT
13
14
  from ..install import check_knowledge_base
14
15
  from ..instance import SNowInstance
16
+ from .utils.utils import check_url_suffix_match
15
17
 
16
18
 
17
19
  class KnowledgeBaseSearchTask(AbstractServiceNowTask):
@@ -30,17 +32,18 @@ class KnowledgeBaseSearchTask(AbstractServiceNowTask):
30
32
 
31
33
  """
32
34
 
33
- def __init__(self, instance=None, fixed_config: dict = None) -> None:
35
+ def __init__(self, seed: int, instance=None, fixed_config: dict = None) -> None:
34
36
  super().__init__(
37
+ seed=seed,
35
38
  instance=instance,
36
- start_rel_url="/now/nav/ui/classic/params/target/%24knowledge.do",
39
+ start_rel_url="/now/nav/ui/classic/params/target/kb?id=kb_home",
37
40
  )
38
41
 
39
42
  # Load the knowledge base and check its integrity
40
43
  with open(KB_FILEPATH, "r") as f:
41
44
  self.kb_entries = json.load(f)
42
45
  _, requires_install, requires_delete = check_knowledge_base(
43
- self.instance, kb_data=self.kb_entries
46
+ self.instance, kb_data=self.kb_entries, kb_name=KB_NAME
44
47
  )
45
48
  with open(KB_CONFIG_PATH, "r") as f:
46
49
  self.all_configs = json.load(f)
@@ -53,7 +56,7 @@ class KnowledgeBaseSearchTask(AbstractServiceNowTask):
53
56
 
54
57
  def _wait_for_ready(self, page: Page) -> None:
55
58
  """
56
- Waits for the main iframe to be fully loaded
59
+ Checks that the main iframe is fully loaded
57
60
 
58
61
  """
59
62
  # TODO: We don't use the flag-based method used in other tasks
@@ -80,32 +83,36 @@ class KnowledgeBaseSearchTask(AbstractServiceNowTask):
80
83
  f"Timed out waiting for iframe to be ready in {self.instance.snow_url}"
81
84
  )
82
85
 
83
- def setup(self, page: Page, seed: int = None) -> tuple[str, dict]:
84
- self.pre_setup(seed, page)
86
+ def setup_goal(self, page: Page) -> tuple[str, dict]:
87
+ super().setup_goal(page=page)
88
+
89
+ # Get task configuration
85
90
  config = self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
86
91
  self.item = config["item"]
87
92
  self.answer = config["value"]
88
93
  self.alternative_answers = config["alternative_answers"]
89
94
  self.question = config["question"]
90
95
 
91
- # generate goal
96
+ # Generate goal
92
97
  goal = f'Answer the following question using the knowledge base: "{self.question}"'
93
98
  info = {}
94
99
 
95
100
  return goal, info
96
101
 
97
102
  def cheat(self, page: Page, chat_messages: list[str]) -> None:
98
- super().cheat(page, chat_messages)
103
+ super().cheat(page=page, chat_messages=chat_messages)
99
104
  self._wait_for_ready(page)
100
105
 
101
106
  iframe = page.frame(name="gsft_main")
102
- search = iframe.locator("input.form-control-search")
107
+ search = iframe.locator('input[aria-label="Search"][role="textbox"]')
103
108
  search.fill(f'"{self.item}"')
104
- self.page.keyboard.press("Enter")
109
+
110
+ with page.expect_navigation():
111
+ self.page.keyboard.press("Enter")
105
112
 
106
113
  # Click on the article
107
- with self.page.expect_navigation():
108
- iframe.locator(".kb_link").first.click()
114
+ with page.expect_navigation():
115
+ iframe.locator("a.kb-title").first.click()
109
116
 
110
117
  # Color the query and answer (this is just for visualization, it changes nothing to the validation)
111
118
  paragraphs = iframe.locator("p")
@@ -130,7 +137,16 @@ class KnowledgeBaseSearchTask(AbstractServiceNowTask):
130
137
  chat_messages.append({"role": "assistant", "message": str(self.answer)})
131
138
 
132
139
  def validate(self, page: Page, chat_messages: list[str]) -> tuple[float, bool, str, dict]:
133
-
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
+ )
134
150
  if chat_messages and chat_messages[-1]["role"] == "assistant":
135
151
  answer = chat_messages[-1]["message"]
136
152
  else: