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.
- browsergym/workarena/__init__.py +3 -2
- browsergym/workarena/api/ui_themes.py +35 -0
- browsergym/workarena/api/user.py +153 -0
- browsergym/workarena/api/utils.py +1 -1
- browsergym/workarena/config.py +43 -1
- browsergym/workarena/data_files/setup_files/ui_themes/workarena_themes.xml +2313 -0
- browsergym/workarena/data_files/task_configs/dashboard_retrieval_minmax_task.json +1 -0
- browsergym/workarena/data_files/task_configs/dashboard_retrieval_value_task.json +1 -0
- browsergym/workarena/data_files/task_configs/report_retrieval_minmax_task.json +1 -0
- browsergym/workarena/data_files/task_configs/report_retrieval_value_task.json +1 -0
- browsergym/workarena/install.py +620 -155
- browsergym/workarena/tasks/base.py +85 -26
- browsergym/workarena/tasks/dashboard.py +620 -0
- browsergym/workarena/tasks/form.py +121 -85
- browsergym/workarena/tasks/knowledge.py +30 -14
- browsergym/workarena/tasks/list.py +121 -67
- browsergym/workarena/tasks/navigation.py +18 -16
- browsergym/workarena/tasks/scripts/generate_dashboard_configs.py +272 -0
- browsergym/workarena/tasks/scripts/generate_forms.py +2 -2
- browsergym/workarena/tasks/scripts/list.py +2 -2
- browsergym/workarena/tasks/scripts/validate.py +2 -2
- browsergym/workarena/tasks/service_catalog.py +106 -74
- browsergym/workarena/tasks/utils/form.py +5 -3
- browsergym/workarena/tasks/utils/js_utils.js +123 -2
- browsergym/workarena/tasks/utils/string.py +15 -0
- browsergym/workarena/tasks/utils/utils.py +20 -0
- browsergym/workarena/utils.py +31 -2
- {browsergym_workarena-0.1.0rc7.dist-info → browsergym_workarena-0.2.0.dist-info}/METADATA +7 -3
- {browsergym_workarena-0.1.0rc7.dist-info → browsergym_workarena-0.2.0.dist-info}/RECORD +32 -21
- {browsergym_workarena-0.1.0rc7.dist-info → browsergym_workarena-0.2.0.dist-info}/WHEEL +1 -1
- {browsergym_workarena-0.1.0rc7.dist-info → browsergym_workarena-0.2.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
298
|
-
|
|
299
|
-
self.
|
|
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
|
-
#
|
|
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 "{
|
|
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
|
-
|
|
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,
|
|
373
|
+
def _generate_random_config(self, page: Page) -> None:
|
|
358
374
|
"""Generate a random configuration for the task."""
|
|
359
|
-
self.
|
|
360
|
-
|
|
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.
|
|
541
|
+
page.evaluate_handle(
|
|
526
542
|
f"""{self.js_prefix}.g_tabs2Sections.tabsTabs[
|
|
527
543
|
{tab_sections[section_id]}
|
|
528
|
-
].element
|
|
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
|
|
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
|
-
|
|
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
|
|
84
|
-
|
|
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
|
-
#
|
|
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(
|
|
107
|
+
search = iframe.locator('input[aria-label="Search"][role="textbox"]')
|
|
103
108
|
search.fill(f'"{self.item}"')
|
|
104
|
-
|
|
109
|
+
|
|
110
|
+
with page.expect_navigation():
|
|
111
|
+
self.page.keyboard.press("Enter")
|
|
105
112
|
|
|
106
113
|
# Click on the article
|
|
107
|
-
with
|
|
108
|
-
iframe.locator(".
|
|
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:
|