browsergym-workarena 0.1.0rc7__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 (32) hide show
  1. browsergym/workarena/__init__.py +3 -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/ui_themes/workarena_themes.xml +2313 -0
  7. browsergym/workarena/data_files/task_configs/dashboard_retrieval_minmax_task.json +1 -0
  8. browsergym/workarena/data_files/task_configs/dashboard_retrieval_value_task.json +1 -0
  9. browsergym/workarena/data_files/task_configs/report_retrieval_minmax_task.json +1 -0
  10. browsergym/workarena/data_files/task_configs/report_retrieval_value_task.json +1 -0
  11. browsergym/workarena/install.py +620 -155
  12. browsergym/workarena/tasks/base.py +85 -26
  13. browsergym/workarena/tasks/dashboard.py +620 -0
  14. browsergym/workarena/tasks/form.py +121 -85
  15. browsergym/workarena/tasks/knowledge.py +30 -14
  16. browsergym/workarena/tasks/list.py +121 -67
  17. browsergym/workarena/tasks/navigation.py +18 -16
  18. browsergym/workarena/tasks/scripts/generate_dashboard_configs.py +272 -0
  19. browsergym/workarena/tasks/scripts/generate_forms.py +2 -2
  20. browsergym/workarena/tasks/scripts/list.py +2 -2
  21. browsergym/workarena/tasks/scripts/validate.py +2 -2
  22. browsergym/workarena/tasks/service_catalog.py +106 -74
  23. browsergym/workarena/tasks/utils/form.py +5 -3
  24. browsergym/workarena/tasks/utils/js_utils.js +123 -2
  25. browsergym/workarena/tasks/utils/string.py +15 -0
  26. browsergym/workarena/tasks/utils/utils.py +20 -0
  27. browsergym/workarena/utils.py +31 -2
  28. {browsergym_workarena-0.1.0rc7.dist-info → browsergym_workarena-0.2.0.dist-info}/METADATA +7 -3
  29. {browsergym_workarena-0.1.0rc7.dist-info → browsergym_workarena-0.2.0.dist-info}/RECORD +32 -21
  30. {browsergym_workarena-0.1.0rc7.dist-info → browsergym_workarena-0.2.0.dist-info}/WHEEL +1 -1
  31. {browsergym_workarena-0.1.0rc7.dist-info → browsergym_workarena-0.2.0.dist-info}/entry_points.txt +0 -0
  32. {browsergym_workarena-0.1.0rc7.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,7 +71,7 @@ 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
  """
@@ -105,10 +108,19 @@ class ServiceNowFormTask(AbstractServiceNowTask):
105
108
  )["result"][0]["label"].lower()
106
109
 
107
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
+
108
120
  logging.debug("Extracting valid form fields")
109
121
  editable_fields = page.evaluate(f"{self.form_js_selector}.getEditableFields()")
110
122
  field_elements = page.evaluate(f"{self.form_js_selector}.elements")
111
- all_visible_fields = [f["fieldName"] for f in field_elements]
123
+ all_fields = [f["fieldName"] for f in field_elements]
112
124
  self.fields = {
113
125
  f["fieldName"]: f
114
126
  for f in field_elements
@@ -129,16 +141,25 @@ class ServiceNowFormTask(AbstractServiceNowTask):
129
141
  self.optional_fields = [f for f in set(self.fields.keys()) - set(self.mandatory_fields)]
130
142
 
131
143
  # Sanity check
132
- 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
133
147
  assert set(self.extra_mandatory_fields) <= set(
134
148
  self.mandatory_fields
135
149
  ), "Some extra mandatory fields are not mandatory in the form."
150
+ # ... check that the script that makes some fields read-only worked
136
151
  assert all(
137
152
  f not in self.fields for f in self.prohibited_fields
138
153
  ), "Some prohibited fields are editable in the form."
139
- assert set(all_visible_fields) == set(
140
- self.expected_fields
141
- ), "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."
142
163
 
143
164
  def _preprocess_fields(self, field, value):
144
165
  """
@@ -173,7 +194,7 @@ class ServiceNowFormTask(AbstractServiceNowTask):
173
194
 
174
195
  def _wait_for_ready(self, page: Page) -> None:
175
196
  """
176
- Waits for the main iframe to be fully loaded
197
+ Waits for the main iframe to be fully loaded.
177
198
 
178
199
  """
179
200
  logging.debug(f"Waiting for {self.js_prefix} to be fully loaded")
@@ -190,33 +211,27 @@ class ServiceNowFormTask(AbstractServiceNowTask):
190
211
  page.wait_for_function(f"typeof window.{self.js_prefix}.g_tabs2Sections !== 'undefined'")
191
212
  logging.debug("Detected Glide tabs API ready")
192
213
 
193
- def pre_setup(self, seed: int, page: Page):
194
- super().pre_setup(seed, page)
195
-
196
- # Register a few initialization scripts
197
- self._add_init_scripts_to_context_and_reload(
198
- page,
199
- [
200
- "registerGsftMainLoaded();",
201
- # ... Mark the extra mandatory fields as such
202
- f"""
203
- // Check that the script is running in the main iframe
204
- if (window.frameElement?.id === '{self.js_prefix}') {{
205
- waLog('Setting mandatory fields');
206
- waitForCondition(() => typeof {self.js_api_forms} !== 'undefined', 100)
207
- .then(waitForCondition(() => typeof window.WORKARENA_LOAD_COMPLETE !== 'undefined' && window.WORKARENA_LOAD_COMPLETE, 100)
208
- .then(
209
- function (){{
210
- {';'.join([self.js_api_forms + '.setMandatory("' + f + '", true)' for f in self.extra_mandatory_fields])}
211
- waLog('Mandatory fields set successfully.');
212
- }}
213
- )
214
- );
215
- }}
216
- """,
217
- ],
218
- )
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
+ ]
219
232
 
233
+ def start(self, page: Page) -> None:
234
+ super().start(page)
220
235
  self._wait_for_ready(page)
221
236
  self._get_form(page)
222
237
 
@@ -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,70 +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"]
304
352
  for f, func in self.unique_valued_fields.items():
305
353
  self.template_record[f] = func(self.template_record[f])
306
-
307
354
  self.task_fields = config["task_fields"]
308
355
  self.created_sysids = []
309
356
 
310
- # generate the goal
357
+ # Generate the goal
311
358
  goal = (
312
359
  f"Create a new {self.table_label} with "
313
360
  + " and ".join(
314
361
  [
315
362
  f'a value of "{self.template_record[f]}"'
316
- + f' for field "{self.fields[f]["label"]}"'
363
+ + f' for field "{config["fields"][f]}"'
317
364
  for f in self.task_fields
318
365
  ]
319
366
  )
320
367
  + "."
321
368
  )
322
369
  info = {}
323
- return goal, info
324
370
 
325
- def _run_init_scripts(self, page: Page) -> None:
326
- self._add_init_scripts_to_context_and_reload(
327
- page,
328
- [
329
- f"""
330
- // Check that the script is running in the main iframe
331
- if (window.frameElement?.id === '{self.js_prefix}') {{
332
- waLog('Attempting to override form submit function');
333
- waitForCondition(() => typeof {self.js_api_forms} !== 'undefined', 100)
334
- .then(waitForCondition(() => typeof gsftSubmit !== 'undefined', 100)
335
- .then(
336
- function overrideSubmit(){{
337
- // Save the original function if it hasn't been saved yet
338
- if(typeof old_gsftSubmit == 'undefined'){{
339
- old_gsftSubmit = new Function('return ' + gsftSubmit.toString())();
340
- waLog('Saved original submit function');
341
- }}
342
-
343
- // Override the function to save the sys_id in the local storage
344
- gsftSubmit = function(control, form, action_name) {{
345
- localStorage['{self.session_sys_id_field}'] = {self.js_api_forms}.getUniqueValue();
346
- old_gsftSubmit(control, form, action_name);
347
- }};
348
- waLog('Patched submit function. All done.');
349
- }}
350
- )
351
- );
352
- }}
353
- """
354
- ],
355
- )
371
+ return goal, info
356
372
 
357
- def _generate_random_config(self, seed: int, page: Page) -> None:
373
+ def _generate_random_config(self, page: Page) -> None:
358
374
  """Generate a random configuration for the task."""
359
- self.pre_setup(seed, page)
360
- self._run_init_scripts(page)
375
+ self.setup(page=page)
376
+
361
377
  # Determine task fields
362
378
  logging.debug("Determining task fields")
363
379
  # ... check that we have enough fields
@@ -482,7 +498,7 @@ class GenericNewRecordTask(ServiceNowFormTask):
482
498
  return goal, info
483
499
 
484
500
  def cheat(self, page: Page, chat_messages: list[str]) -> None:
485
- super().cheat(page, chat_messages)
501
+ super().cheat(page=page, chat_messages=chat_messages)
486
502
  self._wait_for_ready(page)
487
503
  iframe = page.frame_locator(f'iframe[name="{self.js_prefix}"]')
488
504
 
@@ -522,11 +538,11 @@ class GenericNewRecordTask(ServiceNowFormTask):
522
538
  if section_id not in tab_sections:
523
539
  return
524
540
 
525
- page.evaluate(
541
+ page.evaluate_handle(
526
542
  f"""{self.js_prefix}.g_tabs2Sections.tabsTabs[
527
543
  {tab_sections[section_id]}
528
- ].element.click()"""
529
- )
544
+ ].element"""
545
+ ).click(force=True)
530
546
 
531
547
  for field in self.task_fields:
532
548
  # Get the field's input control
@@ -571,7 +587,17 @@ class GenericNewRecordTask(ServiceNowFormTask):
571
587
  that are not part of the task.
572
588
 
573
589
  """
574
-
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
+ )
575
601
  # Retrieve the created record's sys_id from the session storage
576
602
  sys_id = page.evaluate("localStorage").get(self.session_sys_id_field, None)
577
603
 
@@ -665,10 +691,12 @@ class CreateChangeRequestTask(GenericNewRecordTask):
665
691
 
666
692
  """
667
693
 
668
- def __init__(self, instance=None, fixed_config: dict = None) -> None:
694
+ def __init__(self, seed: int, instance=None, fixed_config: dict = None) -> None:
669
695
  super().__init__(
696
+ seed=seed,
670
697
  instance=instance,
671
698
  form_url="/now/nav/ui/classic/params/target/change_request.do",
699
+ table_label="change request",
672
700
  prohibited_fields=["chg_model", "state"],
673
701
  fixed_config=fixed_config,
674
702
  config_path=CREATE_CHANGE_REQUEST_CONFIG_PATH,
@@ -682,10 +710,12 @@ class CreateIncidentTask(GenericNewRecordTask):
682
710
 
683
711
  """
684
712
 
685
- def __init__(self, instance=None, fixed_config: dict = None) -> None:
713
+ def __init__(self, seed: int, instance=None, fixed_config: dict = None) -> None:
686
714
  super().__init__(
715
+ seed=seed,
687
716
  instance=instance,
688
717
  form_url="/now/nav/ui/classic/params/target/incident.do",
718
+ table_label="incident",
689
719
  prohibited_fields=["state"],
690
720
  fixed_config=fixed_config,
691
721
  config_path=CREATE_INCIDENT_CONFIG_PATH,
@@ -699,10 +729,12 @@ class CreateHardwareAssetTask(GenericNewRecordTask):
699
729
 
700
730
  """
701
731
 
702
- def __init__(self, instance=None, fixed_config: dict = None) -> None:
732
+ def __init__(self, seed: int, instance=None, fixed_config: dict = None) -> None:
703
733
  super().__init__(
734
+ seed=seed,
704
735
  instance=instance,
705
736
  form_url="/now/nav/ui/classic/params/target/alm_hardware.do",
737
+ table_label="hardware asset",
706
738
  prohibited_fields=["install_status"],
707
739
  extra_mandatory_fields=[
708
740
  "model",
@@ -723,10 +755,12 @@ class CreateProblemTask(GenericNewRecordTask):
723
755
 
724
756
  """
725
757
 
726
- def __init__(self, instance=None, fixed_config: dict = None) -> None:
758
+ def __init__(self, seed: int, instance=None, fixed_config: dict = None) -> None:
727
759
  super().__init__(
760
+ seed=seed,
728
761
  instance=instance,
729
762
  form_url="/now/nav/ui/classic/params/target/problem.do",
763
+ table_label="problem",
730
764
  prohibited_fields=["state", "first_reported_by_task"],
731
765
  fixed_config=fixed_config,
732
766
  config_path=CREATE_PROBLEM_CONFIG_PATH,
@@ -744,10 +778,12 @@ class CreateUserTask(GenericNewRecordTask):
744
778
 
745
779
  """
746
780
 
747
- def __init__(self, instance=None, fixed_config: dict = None) -> None:
781
+ def __init__(self, seed: int, instance=None, fixed_config: dict = None) -> None:
748
782
  super().__init__(
783
+ seed=seed,
749
784
  instance=instance,
750
785
  form_url="/now/nav/ui/classic/params/target/sys_user.do",
786
+ table_label="user",
751
787
  extra_mandatory_fields=["user_name", "first_name", "last_name", "email"],
752
788
  unique_valued_fields={"user_name": lambda x: str(hash(x + self.unique_id))},
753
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: