main

    return curriculum

@tenacity.retry(wait=tenacity.wait_fixed(1), stop=tenacity.stop_after_attempt(5), reraise=True)
def validate_solution(env):
    infos = []
    messages = []
    for p in env.context.pages:
        reward, stop, message, info = env.task.validate(p, env.chat.messages)

        # If a terminal condition is encountered, return it.
        if reward == 1 or (reward == 0 and stop):
            return reward, stop, message, info

        infos.append(info)
        messages.append(message)

    return reward, stop, ", ".join(messages), {"message": ", ".join([i["message"] for i in infos])}

def main():

    # Initialize the argument parser
    parser = argparse.ArgumentParser(
        description="Get annotator info and log path from command line arguments."
    )

    # Define the command line arguments
    parser.add_argument("--email", type=str, required=True, help="Email of the annotator")
    parser.add_argument(
        "--curriculum",
        type=str,
        required=True,
        help='Path to the curriculum file (optional: use "random" for a random one)',
    )
    parser.add_argument(
        "--log",
        type=str,
        required=False,
        default="human_eval_log.json",
        help="Path to the log file",
    )
    parser.add_argument("--reset-log", action="store_true", help="Reset the log file")

    # Parse the arguments
    args = parser.parse_args()

    annotator_info = {"email": args.email}
    logging.info(f"Annotator info: {annotator_info}")

    # Reset the log file if requested
    logging.info(f"Log file: {args.log}")
    if args.reset_log:
        logging.info("Resetting log file")
        json.dump([], open(args.log, "w"))

    # Loop over the curriculum
    curriculum = load_curriculum(args.curriculum)
    logging.info(f"Starting evaluation for {len(curriculum)} tasks")
    for i, task_info in enumerate(curriculum):

        if task_already_evaluated(args.log, annotator_info, task_info):
            logging.info(f"Task {task_info} already evaluated. Skipping.")
            continue

        # Setup the environment
        logging.info(f"Setting up environment for task {task_info}")
        env = setup_environment(task_info)

        # Game loop
        logging.info(f"Starting evaluation for task {task_info}")
        start_time = time()
        end = False
        success = False
        prev_chat_len = len(env.chat.messages)
        while True:
            human_console_set_progress_status(
                f"Task {i + 1} / {len(curriculum)} --- Elapsed: {round(time() - start_time, 2)} sec.",
                env.context,
            )

            # Event: Human marked task as infeasible
            infeasible, infeasible_reason = infeasible_flag_activated(env.context)
            if infeasible and not any([m["role"] == "infeasible" for m in env.chat.messages]):
                logging.info(f"Human marked task as infeasible. Reason: {infeasible_reason}")
                human_console_set_status("Task marked as infeasible.", env.context)
                env.chat.messages.append({"role": "infeasible", "message": infeasible_reason})
                # TODO: There is a small glitch where if the user changes their message after,
                #       the new infeasible message will be saved instead of the initial one that
                #       was added to the chat messages. We can't stop after infeasible has been
                #       declared.

            # Event: Validation is required
            if validation_flag_activated(env.context) or len(env.chat.messages) != prev_chat_len:
                human_console_set_status("Validation in progress...", env.context)

                # Patch all chat messages
                for m in env.chat.messages:
                    if not m.get("patched", False):
                        if m["role"] == "user":
                            m["role"] = "assistant"
                        elif m["role"] == "assistant":
                            m["role"] = "user"
                        m["patched"] = True

                reward, stop, message, info = validate_solution(env)
                logging.info(f"Validation: {info} -- reward: {reward} -- stop: {stop}")

                if reward == 1:
                    human_console_set_status("Success!", env.context)
                    end = True
                    success = True
                else:
                    if not end:  # If we're not already stopping for another reason
                        if stop:
                            human_console_set_status(
                                "Task not completed. Stop required.", env.context
                            )
                            end = True
                            success = False
                        else:
                            human_console_set_status("Task not completed. Keep going.", env.context)

                prev_chat_len = len(env.chat.messages)
                reset_validation_flag(env.context)

            # Event: Human abandoned task
            if abandon_flag_activated(env.context):
                end = True
                success = False
                human_console_set_status("Task abandoned by human.", env.context)

            # Event: Task is finished
            if end:
                log_result(
                    path=args.log,
                    annotator_info=annotator_info,
                    task_info=task_info,
                    metrics={
                        "duration": time() - start_time,
                        "success": success,
                        "infeasible": infeasible_reason if infeasible else None,
                        "abandoned": abandon_flag_activated(env.context),
                        "chat_messages": env.chat.messages,
                    },
                )
                sleep(3)  # Sleep so human has time to read status before it closes
                break

            sleep(0.1)

        human_console_set_status("Cleaning environment. This may take a while...", env.context)
        env.close()
        logging.info(f"Finished evaluation for task {task_info}")

if __name__ == "__main__":
    main()


src.browsergym.workarena.tasks.base

"""
A bunch of base classes

"""

import logging

import numpy as np
import playwright.sync_api

from abc import ABC, abstractmethod
from copy import deepcopy
from typing import List, Optional, Tuple
from uuid import uuid4
from urllib import parse

from browsergym.core.task import AbstractBrowserTask
from ..api.user import create_user
from ..api.utils import table_api_call
from ..config import SNOW_BROWSER_TIMEOUT, SNOW_JS_UTILS_FILEPATH
from ..utils import url_login
from ..instance import SNowInstance

class AbstractServiceNowTask(AbstractBrowserTask, ABC):
    """
    A base class for tasks that interacts with the ServiceNow instance

    """

    def __init__(
        self,
        seed: int,
        start_rel_url: str,
        instance: SNowInstance = None,
        final_rel_url: Optional[str] = None,
        user_roles: List[str] = ["admin"],
        has_description: bool = False,
    ) -> None:
        """
        Initialize the task

        Parameters:
        -----------
        seed: int
            Random seed
        instance: SNowInstance
            The ServiceNow instance in which the task will be performed
        start_rel_url: str
            The URL for the starting page of the task
        final_rel_url: str (optional)
            The URL for the final page of the task (default: uses the value of base_url)
        user_roles: list[str]
            The roles to assign to the user (default: ["admin"])
        has_description: bool
            Whether the task has a description in L3 compositional tasks

        """
        super().__init__(seed)

        # task properties, will be used to set up the browsergym environment
        self.viewport = {"width": 1280, "height": 720}
        self.slow_mo = 1000  # ms
        self.timeout = 10000  # ms

        self.instance = instance if instance is not None else SNowInstance()
        self.start_url = self.instance.snow_url + start_rel_url

        if final_rel_url is not None:
            self.final_url = self.instance.snow_url + final_rel_url
        else:
            self.final_url = self.start_url
        self.final_url_ = parse.urlparse(self.final_url)

        # Set the task's unique ID
        self.unique_id = str(uuid4())
        # Flag to ensure the task is setup only once
        self.task_is_setup = False
        self.delete_user_on_teardown = False
        self.user_roles = user_roles
        self.has_description = (
            has_description  # Whether the task has a description in L3 compositional tasks
        )

    def cheat(self, page: playwright.sync_api.Page, chat_messages: list[str]) -> None:
        # Don't call super cheat function because it's not implemented at the base level
        logging.debug("Cheat is solving the task")

    def get_init_scripts(self) -> List[str]:
        """
        Get the initialization scripts for the task. These are javascript scripts that will be run
        on any page and iframe that is loaded during the task.

        """
        return []

    @classmethod
    def get_task_id(cls):
        # Get the class name and remove the word 'Task' from the end if it exists
        class_name = cls.__name__.replace("Task", "")
        # Convert CamelCase to hyphen-separated format
        formatted_name = "".join(
            ["-" + i.lower() if i.isupper() else i for i in class_name]
        ).lstrip("-")
        return f"workarena.servicenow.{formatted_name}"

    def setup(self, page: playwright.sync_api.Page, do_start=True) -> tuple[str, dict]:
        """
        Set up the task

        Parameters:
        -----------
        page: playwright.sync_api.Page
            The Playwright page object
        do_start: bool
            Whether to start the task or not (including navigating to start page) (default: True)

        """
        logging.debug("Setting up the base task")
        if self.task_is_setup:
            raise ValueError("The task is already setup")

        # Keep the page for client-side validation
        self.page = page

        # Set the page timeout
        page.set_default_timeout(SNOW_BROWSER_TIMEOUT)

        # Create a new user to run the task if this is the starting task
        if do_start:
            self._base_initial_instance = self.instance
            self._base_user_name, self._base_user_password, self._base_user_sysid = create_user(
                instance=self.instance, user_roles=self.user_roles, random=self.random
            )
            self.instance = deepcopy(self.instance)
            self.instance.snow_credentials = (self._base_user_name, self._base_user_password)
            self.delete_user_on_teardown = True
        # Set the task's unique ID
        self.unique_id = str(uuid4())

        # Configure the task
        goal, info = self.setup_goal(page=page)

        # Load a few utility functions for init scripts
        page.context.add_init_script(path=SNOW_JS_UTILS_FILEPATH)

        # Add the initialization scripts to the page context
        for script in self.get_init_scripts():
            page.context.add_init_script(script)

        # Start the task if requested
        if do_start:
            self.start(page)

        self.task_is_setup = True

        return goal, info

    def create_user(self, first_name: str = None, last_name: str = None):
        """
        Create a user in the ServiceNow instance

        """

    @abstractmethod
    def setup_goal(self, page: playwright.sync_api.Page) -> tuple[str, dict]:
        """
        Setup the task configuration and produce the goal

        """
        pass

    def start(self, page: playwright.sync_api.Page) -> None:
        logging.debug("Navigating to task start page")

        # Authenticate
        url_login(
            instance=self.instance,
            page=page,
        )

        # Navigate to the task's url
        page.goto(self.start_url)

    def teardown(self) -> None:
        """
        Clean up after the task

        Notes:
        ------
        This method should not make assumptions on the state of the page (e.g., a specific URL).

        """
        logging.debug("Tearing down the task")

        if self.delete_user_on_teardown:
            # Delete the user
            table_api_call(
                instance=self._base_initial_instance,
                table=f"sys_user/{self._base_user_sysid}",
                method="DELETE",
            )


AbstractServiceNowTask

import logging

import numpy as np
import playwright.sync_api

from abc import ABC, abstractmethod
from copy import deepcopy
from typing import List, Optional, Tuple
from uuid import uuid4
from urllib import parse

from browsergym.core.task import AbstractBrowserTask
from ..api.user import create_user
from ..api.utils import table_api_call
from ..config import SNOW_BROWSER_TIMEOUT, SNOW_JS_UTILS_FILEPATH
from ..utils import url_login
from ..instance import SNowInstance

class AbstractServiceNowTask(AbstractBrowserTask, ABC):
    """
    A base class for tasks that interacts with the ServiceNow instance

    """

    def __init__(
        self,
        seed: int,
        start_rel_url: str,
        instance: SNowInstance = None,
        final_rel_url: Optional[str] = None,
        user_roles: List[str] = ["admin"],
        has_description: bool = False,
    ) -> None:
        """
        Initialize the task

        Parameters:
        -----------
        seed: int
            Random seed
        instance: SNowInstance
            The ServiceNow instance in which the task will be performed
        start_rel_url: str
            The URL for the starting page of the task
        final_rel_url: str (optional)
            The URL for the final page of the task (default: uses the value of base_url)
        user_roles: list[str]
            The roles to assign to the user (default: ["admin"])
        has_description: bool
            Whether the task has a description in L3 compositional tasks

        """
        super().__init__(seed)

        # task properties, will be used to set up the browsergym environment
        self.viewport = {"width": 1280, "height": 720}
        self.slow_mo = 1000  # ms
        self.timeout = 10000  # ms

        self.instance = instance if instance is not None else SNowInstance()
        self.start_url = self.instance.snow_url + start_rel_url

        if final_rel_url is not None:
            self.final_url = self.instance.snow_url + final_rel_url
        else:
            self.final_url = self.start_url
        self.final_url_ = parse.urlparse(self.final_url)

        # Set the task's unique ID
        self.unique_id = str(uuid4())
        # Flag to ensure the task is setup only once
        self.task_is_setup = False
        self.delete_user_on_teardown = False
        self.user_roles = user_roles
        self.has_description = (
            has_description  # Whether the task has a description in L3 compositional tasks
        )

    def cheat(self, page: playwright.sync_api.Page, chat_messages: list[str]) -> None:
        # Don't call super cheat function because it's not implemented at the base level
        logging.debug("Cheat is solving the task")

    def get_init_scripts(self) -> List[str]:
        """
        Get the initialization scripts for the task. These are javascript scripts that will be run
        on any page and iframe that is loaded during the task.

        """
        return []

    @classmethod
    def get_task_id(cls):
        # Get the class name and remove the word 'Task' from the end if it exists
        class_name = cls.__name__.replace("Task", "")
        # Convert CamelCase to hyphen-separated format
        formatted_name = "".join(
            ["-" + i.lower() if i.isupper() else i for i in class_name]
        ).lstrip("-")
        return f"workarena.servicenow.{formatted_name}"

    def setup(self, page: playwright.sync_api.Page, do_start=True) -> tuple[str, dict]:
        """
        Set up the task

        Parameters:
        -----------
        page: playwright.sync_api.Page
            The Playwright page object
        do_start: bool
            Whether to start the task or not (including navigating to start page) (default: True)

        """
        logging.debug("Setting up the base task")
        if self.task_is_setup:
            raise ValueError("The task is already setup")

        # Keep the page for client-side validation
        self.page = page

        # Set the page timeout
        page.set_default_timeout(SNOW_BROWSER_TIMEOUT)

        # Create a new user to run the task if this is the starting task
        if do_start:
            self._base_initial_instance = self.instance
            self._base_user_name, self._base_user_password, self._base_user_sysid = create_user(
                instance=self.instance, user_roles=self.user_roles, random=self.random
            )
            self.instance = deepcopy(self.instance)
            self.instance.snow_credentials = (self._base_user_name, self._base_user_password)
            self.delete_user_on_teardown = True
        # Set the task's unique ID
        self.unique_id = str(uuid4())

        # Configure the task
        goal, info = self.setup_goal(page=page)

        # Load a few utility functions for init scripts
        page.context.add_init_script(path=SNOW_JS_UTILS_FILEPATH)

        # Add the initialization scripts to the page context
        for script in self.get_init_scripts():
            page.context.add_init_script(script)

        # Start the task if requested
        if do_start:
            self.start(page)

        self.task_is_setup = True

        return goal, info

    def create_user(self, first_name: str = None, last_name: str = None):
        """
        Create a user in the ServiceNow instance

        """

    @abstractmethod
    def setup_goal(self, page: playwright.sync_api.Page) -> tuple[str, dict]:
        """
        Setup the task configuration and produce the goal

        """
        pass

    def start(self, page: playwright.sync_api.Page) -> None:
        logging.debug("Navigating to task start page")

        # Authenticate
        url_login(
            instance=self.instance,
            page=page,
        )

        # Navigate to the task's url
        page.goto(self.start_url)

    def teardown(self) -> None:
        """
        Clean up after the task

        Notes:
        ------
        This method should not make assumptions on the state of the page (e.g., a specific URL).

        """
        logging.debug("Tearing down the task")

        if self.delete_user_on_teardown:
            # Delete the user
            table_api_call(
                instance=self._base_initial_instance,
                table=f"sys_user/{self._base_user_sysid}",
                method="DELETE",
            )


from abc import ABC, abstractmethod
from copy import deepcopy
from typing import List, Optional, Tuple
from uuid import uuid4
from urllib import parse

from browsergym.core.task import AbstractBrowserTask
from ..api.user import create_user
from ..api.utils import table_api_call
from ..config import SNOW_BROWSER_TIMEOUT, SNOW_JS_UTILS_FILEPATH
from ..utils import url_login
from ..instance import SNowInstance

class AbstractServiceNowTask(AbstractBrowserTask, ABC):
    """
    A base class for tasks that interacts with the ServiceNow instance

    """

    def __init__(
        self,
        seed: int,
        start_rel_url: str,
        instance: SNowInstance = None,
        final_rel_url: Optional[str] = None,
        user_roles: List[str] = ["admin"],
        has_description: bool = False,
    ) -> None:
        """
        Initialize the task

        Parameters:
        -----------
        seed: int
            Random seed
        instance: SNowInstance
            The ServiceNow instance in which the task will be performed
        start_rel_url: str
            The URL for the starting page of the task
        final_rel_url: str (optional)
            The URL for the final page of the task (default: uses the value of base_url)
        user_roles: list[str]
            The roles to assign to the user (default: ["admin"])
        has_description: bool
            Whether the task has a description in L3 compositional tasks

        """
        super().__init__(seed)

        # task properties, will be used to set up the browsergym environment
        self.viewport = {"width": 1280, "height": 720}
        self.slow_mo = 1000  # ms
        self.timeout = 10000  # ms

        self.instance = instance if instance is not None else SNowInstance()
        self.start_url = self.instance.snow_url + start_rel_url

        if final_rel_url is not None:
            self.final_url = self.instance.snow_url + final_rel_url
        else:
            self.final_url = self.start_url
        self.final_url_ = parse.urlparse(self.final_url)

        # Set the task's unique ID
        self.unique_id = str(uuid4())
        # Flag to ensure the task is setup only once
        self.task_is_setup = False
        self.delete_user_on_teardown = False
        self.user_roles = user_roles
        self.has_description = (
            has_description  # Whether the task has a description in L3 compositional tasks
        )

    def cheat(self, page: playwright.sync_api.Page, chat_messages: list[str]) -> None:
        # Don't call super cheat function because it's not implemented at the base level
        logging.debug("Cheat is solving the task")

    def get_init_scripts(self) -> List[str]:
        """
        Get the initialization scripts for the task. These are javascript scripts that will be run
        on any page and iframe that is loaded during the task.

        """
        return []

    @classmethod
    def get_task_id(cls):
        # Get the class name and remove the word 'Task' from the end if it exists
        class_name = cls.__name__.replace("Task", "")
        # Convert CamelCase to hyphen-separated format
        formatted_name = "".join(
            ["-" + i.lower() if i.isupper() else i for i in class_name]


cheat

        self.instance = instance if instance is not None else SNowInstance()
        self.start_url = self.instance.snow_url + start_rel_url

        if final_rel_url is not None:
            self.final_url = self.instance.snow_url + final_rel_url
        else:
            self.final_url = self.start_url
        self.final_url_ = parse.urlparse(self.final_url)

        # Set the task's unique ID
        self.unique_id = str(uuid4())
        # Flag to ensure the task is setup only once
        self.task_is_setup = False
        self.delete_user_on_teardown = False
        self.user_roles = user_roles
        self.has_description = (
            has_description  # Whether the task has a description in L3 compositional tasks
        )

    def cheat(self, page: playwright.sync_api.Page, chat_messages: list[str]) -> None:
        # Don't call super cheat function because it's not implemented at the base level
        logging.debug("Cheat is solving the task")

    def get_init_scripts(self) -> List[str]:
        """
        Get the initialization scripts for the task. These are javascript scripts that will be run
        on any page and iframe that is loaded during the task.

        """
        return []

    @classmethod
    def get_task_id(cls):
        # Get the class name and remove the word 'Task' from the end if it exists
        class_name = cls.__name__.replace("Task", "")
        # Convert CamelCase to hyphen-separated format
        formatted_name = "".join(
            ["-" + i.lower() if i.isupper() else i for i in class_name]
        ).lstrip("-")
        return f"workarena.servicenow.{formatted_name}"

    def setup(self, page: playwright.sync_api.Page, do_start=True) -> tuple[str, dict]:


        if final_rel_url is not None:
            self.final_url = self.instance.snow_url + final_rel_url
        else:
            self.final_url = self.start_url
        self.final_url_ = parse.urlparse(self.final_url)

        # Set the task's unique ID
        self.unique_id = str(uuid4())
        # Flag to ensure the task is setup only once
        self.task_is_setup = False
        self.delete_user_on_teardown = False
        self.user_roles = user_roles
        self.has_description = (
            has_description  # Whether the task has a description in L3 compositional tasks
        )

    def cheat(self, page: playwright.sync_api.Page, chat_messages: list[str]) -> None:
        # Don't call super cheat function because it's not implemented at the base level
        logging.debug("Cheat is solving the task")

    def get_init_scripts(self) -> List[str]:
        """
        Get the initialization scripts for the task. These are javascript scripts that will be run
        on any page and iframe that is loaded during the task.

        """
        return []

    @classmethod
    def get_task_id(cls):
        # Get the class name and remove the word 'Task' from the end if it exists
        class_name = cls.__name__.replace("Task", "")
        # Convert CamelCase to hyphen-separated format
        formatted_name = "".join(
            ["-" + i.lower() if i.isupper() else i for i in class_name]
        ).lstrip("-")
        return f"workarena.servicenow.{formatted_name}"

    def setup(self, page: playwright.sync_api.Page, do_start=True) -> tuple[str, dict]:
        """
        Set up the task

        Parameters:
        -----------
        page: playwright.sync_api.Page
            The Playwright page object
        do_start: bool


        self.task_is_setup = False
        self.delete_user_on_teardown = False
        self.user_roles = user_roles
        self.has_description = (
            has_description  # Whether the task has a description in L3 compositional tasks
        )

    def cheat(self, page: playwright.sync_api.Page, chat_messages: list[str]) -> None:
        # Don't call super cheat function because it's not implemented at the base level
        logging.debug("Cheat is solving the task")

    def get_init_scripts(self) -> List[str]:
        """
        Get the initialization scripts for the task. These are javascript scripts that will be run
        on any page and iframe that is loaded during the task.

        """
        return []

    @classmethod
    def get_task_id(cls):
        # Get the class name and remove the word 'Task' from the end if it exists
        class_name = cls.__name__.replace("Task", "")
        # Convert CamelCase to hyphen-separated format
        formatted_name = "".join(
            ["-" + i.lower() if i.isupper() else i for i in class_name]
        ).lstrip("-")
        return f"workarena.servicenow.{formatted_name}"

    def setup(self, page: playwright.sync_api.Page, do_start=True) -> tuple[str, dict]:
        """
        Set up the task

        Parameters:
        -----------
        page: playwright.sync_api.Page
            The Playwright page object
        do_start: bool
            Whether to start the task or not (including navigating to start page) (default: True)

        """
        logging.debug("Setting up the base task")
        if self.task_is_setup:
            raise ValueError("The task is already setup")

        # Keep the page for client-side validation
        self.page = page


setup

        logging.debug("Cheat is solving the task")

    def get_init_scripts(self) -> List[str]:
        """
        Get the initialization scripts for the task. These are javascript scripts that will be run
        on any page and iframe that is loaded during the task.

        """
        return []

    @classmethod
    def get_task_id(cls):
        # Get the class name and remove the word 'Task' from the end if it exists
        class_name = cls.__name__.replace("Task", "")
        # Convert CamelCase to hyphen-separated format
        formatted_name = "".join(
            ["-" + i.lower() if i.isupper() else i for i in class_name]
        ).lstrip("-")
        return f"workarena.servicenow.{formatted_name}"

    def setup(self, page: playwright.sync_api.Page, do_start=True) -> tuple[str, dict]:
        """
        Set up the task

        Parameters:
        -----------
        page: playwright.sync_api.Page
            The Playwright page object
        do_start: bool
            Whether to start the task or not (including navigating to start page) (default: True)

        """
        logging.debug("Setting up the base task")
        if self.task_is_setup:
            raise ValueError("The task is already setup")

        # Keep the page for client-side validation
        self.page = page

        # Set the page timeout
        page.set_default_timeout(SNOW_BROWSER_TIMEOUT)

        # Create a new user to run the task if this is the starting task
        if do_start:
            self._base_initial_instance = self.instance
            self._base_user_name, self._base_user_password, self._base_user_sysid = create_user(
                instance=self.instance, user_roles=self.user_roles, random=self.random
            )
            self.instance = deepcopy(self.instance)
            self.instance.snow_credentials = (self._base_user_name, self._base_user_password)
            self.delete_user_on_teardown = True
        # Set the task's unique ID
        self.unique_id = str(uuid4())

        # Configure the task
        goal, info = self.setup_goal(page=page)

        # Load a few utility functions for init scripts
        page.context.add_init_script(path=SNOW_JS_UTILS_FILEPATH)

        # Add the initialization scripts to the page context
        for script in self.get_init_scripts():
            page.context.add_init_script(script)

        # Start the task if requested
        if do_start:
            self.start(page)

        self.task_is_setup = True

        return goal, info

    def create_user(self, first_name: str = None, last_name: str = None):
        """
        Create a user in the ServiceNow instance

        """

    @abstractmethod
    def setup_goal(self, page: playwright.sync_api.Page) -> tuple[str, dict]:
        """
        Setup the task configuration and produce the goal

        """
        pass

    def start(self, page: playwright.sync_api.Page) -> None:
        logging.debug("Navigating to task start page")

        # Authenticate
        url_login(


        self.unique_id = str(uuid4())

        # Configure the task
        goal, info = self.setup_goal(page=page)

        # Load a few utility functions for init scripts
        page.context.add_init_script(path=SNOW_JS_UTILS_FILEPATH)

        # Add the initialization scripts to the page context
        for script in self.get_init_scripts():
            page.context.add_init_script(script)

        # Start the task if requested
        if do_start:
            self.start(page)

        self.task_is_setup = True

        return goal, info

    def create_user(self, first_name: str = None, last_name: str = None):
        """
        Create a user in the ServiceNow instance

        """

    @abstractmethod
    def setup_goal(self, page: playwright.sync_api.Page) -> tuple[str, dict]:
        """
        Setup the task configuration and produce the goal

        """
        pass

    def start(self, page: playwright.sync_api.Page) -> None:
        logging.debug("Navigating to task start page")

        # Authenticate
        url_login(
            instance=self.instance,
            page=page,
        )

        # Navigate to the task's url
        page.goto(self.start_url)


        # Add the initialization scripts to the page context
        for script in self.get_init_scripts():
            page.context.add_init_script(script)

        # Start the task if requested
        if do_start:
            self.start(page)

        self.task_is_setup = True

        return goal, info

    def create_user(self, first_name: str = None, last_name: str = None):
        """
        Create a user in the ServiceNow instance

        """

    @abstractmethod
    def setup_goal(self, page: playwright.sync_api.Page) -> tuple[str, dict]:
        """
        Setup the task configuration and produce the goal

        """
        pass

    def start(self, page: playwright.sync_api.Page) -> None:
        logging.debug("Navigating to task start page")

        # Authenticate
        url_login(
            instance=self.instance,
            page=page,
        )

        # Navigate to the task's url
        page.goto(self.start_url)

    def teardown(self) -> None:
        """
        Clean up after the task

        Notes:
        ------
        This method should not make assumptions on the state of the page (e.g., a specific URL).


start

            self.start(page)

        self.task_is_setup = True

        return goal, info

    def create_user(self, first_name: str = None, last_name: str = None):
        """
        Create a user in the ServiceNow instance

        """

    @abstractmethod
    def setup_goal(self, page: playwright.sync_api.Page) -> tuple[str, dict]:
        """
        Setup the task configuration and produce the goal

        """
        pass

    def start(self, page: playwright.sync_api.Page) -> None:
        logging.debug("Navigating to task start page")

        # Authenticate
        url_login(
            instance=self.instance,
            page=page,
        )

        # Navigate to the task's url
        page.goto(self.start_url)

    def teardown(self) -> None:
        """
        Clean up after the task

        Notes:
        ------
        This method should not make assumptions on the state of the page (e.g., a specific URL).

        """
        logging.debug("Tearing down the task")

        if self.delete_user_on_teardown:
            # Delete the user
            table_api_call(
                instance=self._base_initial_instance,
                table=f"sys_user/{self._base_user_sysid}",
                method="DELETE",
            )


teardown

    @abstractmethod
    def setup_goal(self, page: playwright.sync_api.Page) -> tuple[str, dict]:
        """
        Setup the task configuration and produce the goal

        """
        pass

    def start(self, page: playwright.sync_api.Page) -> None:
        logging.debug("Navigating to task start page")

        # Authenticate
        url_login(
            instance=self.instance,
            page=page,
        )

        # Navigate to the task's url
        page.goto(self.start_url)

    def teardown(self) -> None:
        """
        Clean up after the task

        Notes:
        ------
        This method should not make assumptions on the state of the page (e.g., a specific URL).

        """
        logging.debug("Tearing down the task")

        if self.delete_user_on_teardown:
            # Delete the user
            table_api_call(
                instance=self._base_initial_instance,
                table=f"sys_user/{self._base_user_sysid}",
                method="DELETE",
            )


from typing import Tuple
from playwright.sync_api import Page

from .base import AbstractServiceNowTask
from .comp_building_block import CompositionalBuildingBlockTask

from ..instance import SNowInstance

class SendChatMessageTask(AbstractServiceNowTask, CompositionalBuildingBlockTask):
    """Task to send a chat message in the chat. Only used as a compositional building block for the cheat function.
    Args:
    --------
    message (str):
        The message to send in the chat
    answer_format (str):
        The type of answer to generate. Choice of total_return_only, total_return_and_investments, investments_only, cleanup, cleanup_and_return
    """

    def __init__(
        self,
        instance: SNowInstance,
        message: str,
        answer_format: str,
        use_description_in_l3: bool = False,
        **kwargs,
    ):
        super().__init__(seed=0, instance=instance, start_rel_url="")
        self.message = message
        self.answer_format = answer_format
        self.use_description_in_l3 = use_description_in_l3
        self.__dict__.update(kwargs)

    def setup_goal(self, page: Page):
        return self.get_pretty_printed_description(), {}

    def validate(self, page: Page, chat_messages: list[str]) -> Tuple[float | bool | str | dict]:
        return super().validate(page, chat_messages)

    def cheat(self, page: Page, chat_messages: list[str]):
        super().cheat(page=page, chat_messages=chat_messages)
        chat_messages.append({"role": "assistant", "message": str(self.message)})

    def teardown(self) -> None:
        pass

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in compositional tasks.
        """
        raise NotImplementedError

class SendChatMessageForBudgetAllocationTask(SendChatMessageTask):
    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in compositional tasks.
        """
        if self.level == 3:
            task_info = "Allocate the budget to maximize revenue."
        elif self.level == 2:
            task_info = f"Allocate the budget to maximize revenue. This involves going over expense lines and identifying the ones maximizing revenue while fitting in the allowed budget of {self.budget} $. The returns are written in their short description."
            if self.answer_format == "total_return_only":
                task_info += " Provide only the total return of the investments in the chat."
            if self.answer_format == "total_return_and_investments":
                task_info += " Provide the total return of the investments as well as the value of their 'Number' field in the chat."
            if self.answer_format == "investments_only":
                task_info += " Provide only the value of the 'Number' field of the selected investments in the chat."
            if self.answer_format == "cleanup":
                task_info += " Delete the investments that will not be kept so that only the selected investments remain."
            if self.answer_format == "cleanup_and_return":
                task_info += " Delete the investments that will not be kept so that only the selected investments remain as well as returning their total value in the chat."

        return task_info

class SendChatMessageGenericTask(SendChatMessageTask):

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in compositional tasks.
        """
        if self.use_description_in_l3:
            task_info = self.description
        elif self.level == 3:
            task_info = ""
        elif self.level == 2:
            task_info = self.description

        return task_info


SendChatMessageTask

from typing import Tuple
from playwright.sync_api import Page

from .base import AbstractServiceNowTask
from .comp_building_block import CompositionalBuildingBlockTask

from ..instance import SNowInstance

class SendChatMessageTask(AbstractServiceNowTask, CompositionalBuildingBlockTask):
    """Task to send a chat message in the chat. Only used as a compositional building block for the cheat function.
    Args:
    --------
    message (str):
        The message to send in the chat
    answer_format (str):
        The type of answer to generate. Choice of total_return_only, total_return_and_investments, investments_only, cleanup, cleanup_and_return
    """

    def __init__(
        self,
        instance: SNowInstance,
        message: str,
        answer_format: str,
        use_description_in_l3: bool = False,
        **kwargs,
    ):
        super().__init__(seed=0, instance=instance, start_rel_url="")
        self.message = message
        self.answer_format = answer_format
        self.use_description_in_l3 = use_description_in_l3
        self.__dict__.update(kwargs)

    def setup_goal(self, page: Page):
        return self.get_pretty_printed_description(), {}

    def validate(self, page: Page, chat_messages: list[str]) -> Tuple[float | bool | str | dict]:
        return super().validate(page, chat_messages)

    def cheat(self, page: Page, chat_messages: list[str]):
        super().cheat(page=page, chat_messages=chat_messages)
        chat_messages.append({"role": "assistant", "message": str(self.message)})

    def teardown(self) -> None:
        pass

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in compositional tasks.
        """
        raise NotImplementedError

class SendChatMessageForBudgetAllocationTask(SendChatMessageTask):
    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in compositional tasks.
        """
        if self.level == 3:
            task_info = "Allocate the budget to maximize revenue."
        elif self.level == 2:
            task_info = f"Allocate the budget to maximize revenue. This involves going over expense lines and identifying the ones maximizing revenue while fitting in the allowed budget of {self.budget} $. The returns are written in their short description."
            if self.answer_format == "total_return_only":
                task_info += " Provide only the total return of the investments in the chat."
            if self.answer_format == "total_return_and_investments":
                task_info += " Provide the total return of the investments as well as the value of their 'Number' field in the chat."
            if self.answer_format == "investments_only":
                task_info += " Provide only the value of the 'Number' field of the selected investments in the chat."
            if self.answer_format == "cleanup":
                task_info += " Delete the investments that will not be kept so that only the selected investments remain."
            if self.answer_format == "cleanup_and_return":


SendChatMessageForBudgetAllocationTask

    def setup_goal(self, page: Page):
        return self.get_pretty_printed_description(), {}

    def validate(self, page: Page, chat_messages: list[str]) -> Tuple[float | bool | str | dict]:
        return super().validate(page, chat_messages)

    def cheat(self, page: Page, chat_messages: list[str]):
        super().cheat(page=page, chat_messages=chat_messages)
        chat_messages.append({"role": "assistant", "message": str(self.message)})

    def teardown(self) -> None:
        pass

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in compositional tasks.
        """
        raise NotImplementedError

class SendChatMessageForBudgetAllocationTask(SendChatMessageTask):
    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in compositional tasks.
        """
        if self.level == 3:
            task_info = "Allocate the budget to maximize revenue."
        elif self.level == 2:
            task_info = f"Allocate the budget to maximize revenue. This involves going over expense lines and identifying the ones maximizing revenue while fitting in the allowed budget of {self.budget} $. The returns are written in their short description."
            if self.answer_format == "total_return_only":
                task_info += " Provide only the total return of the investments in the chat."
            if self.answer_format == "total_return_and_investments":
                task_info += " Provide the total return of the investments as well as the value of their 'Number' field in the chat."
            if self.answer_format == "investments_only":
                task_info += " Provide only the value of the 'Number' field of the selected investments in the chat."
            if self.answer_format == "cleanup":
                task_info += " Delete the investments that will not be kept so that only the selected investments remain."
            if self.answer_format == "cleanup_and_return":
                task_info += " Delete the investments that will not be kept so that only the selected investments remain as well as returning their total value in the chat."

        return task_info

class SendChatMessageGenericTask(SendChatMessageTask):

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in compositional tasks.
        """
        if self.use_description_in_l3:
            task_info = self.description
        elif self.level == 3:
            task_info = ""
        elif self.level == 2:
            task_info = self.description

        return task_info


SendChatMessageGenericTask

        Get the task info for this task when used in a private task; Used in compositional tasks.
        """
        if self.level == 3:
            task_info = "Allocate the budget to maximize revenue."
        elif self.level == 2:
            task_info = f"Allocate the budget to maximize revenue. This involves going over expense lines and identifying the ones maximizing revenue while fitting in the allowed budget of {self.budget} $. The returns are written in their short description."
            if self.answer_format == "total_return_only":
                task_info += " Provide only the total return of the investments in the chat."
            if self.answer_format == "total_return_and_investments":
                task_info += " Provide the total return of the investments as well as the value of their 'Number' field in the chat."
            if self.answer_format == "investments_only":
                task_info += " Provide only the value of the 'Number' field of the selected investments in the chat."
            if self.answer_format == "cleanup":
                task_info += " Delete the investments that will not be kept so that only the selected investments remain."
            if self.answer_format == "cleanup_and_return":
                task_info += " Delete the investments that will not be kept so that only the selected investments remain as well as returning their total value in the chat."

        return task_info

class SendChatMessageGenericTask(SendChatMessageTask):

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in compositional tasks.
        """
        if self.use_description_in_l3:
            task_info = self.description
        elif self.level == 3:
            task_info = ""
        elif self.level == 2:
            task_info = self.description

        return task_info


from typing import Tuple
from playwright.sync_api import Page

from .base import AbstractServiceNowTask
from .comp_building_block import CompositionalBuildingBlockTask

from ..instance import SNowInstance

class SendChatMessageTask(AbstractServiceNowTask, CompositionalBuildingBlockTask):
    """Task to send a chat message in the chat. Only used as a compositional building block for the cheat function.
    Args:
    --------
    message (str):
        The message to send in the chat
    answer_format (str):
        The type of answer to generate. Choice of total_return_only, total_return_and_investments, investments_only, cleanup, cleanup_and_return
    """

    def __init__(
        self,
        instance: SNowInstance,
        message: str,
        answer_format: str,
        use_description_in_l3: bool = False,
        **kwargs,
    ):
        super().__init__(seed=0, instance=instance, start_rel_url="")
        self.message = message
        self.answer_format = answer_format
        self.use_description_in_l3 = use_description_in_l3
        self.__dict__.update(kwargs)

    def setup_goal(self, page: Page):
        return self.get_pretty_printed_description(), {}

    def validate(self, page: Page, chat_messages: list[str]) -> Tuple[float | bool | str | dict]:
        return super().validate(page, chat_messages)

    def cheat(self, page: Page, chat_messages: list[str]):
        super().cheat(page=page, chat_messages=chat_messages)
        chat_messages.append({"role": "assistant", "message": str(self.message)})

    def teardown(self) -> None:
        pass

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in compositional tasks.
        """
        raise NotImplementedError


    message (str):
        The message to send in the chat
    answer_format (str):
        The type of answer to generate. Choice of total_return_only, total_return_and_investments, investments_only, cleanup, cleanup_and_return
    """

    def __init__(
        self,
        instance: SNowInstance,
        message: str,
        answer_format: str,
        use_description_in_l3: bool = False,
        **kwargs,
    ):
        super().__init__(seed=0, instance=instance, start_rel_url="")
        self.message = message
        self.answer_format = answer_format
        self.use_description_in_l3 = use_description_in_l3
        self.__dict__.update(kwargs)

    def setup_goal(self, page: Page):
        return self.get_pretty_printed_description(), {}

    def validate(self, page: Page, chat_messages: list[str]) -> Tuple[float | bool | str | dict]:
        return super().validate(page, chat_messages)

    def cheat(self, page: Page, chat_messages: list[str]):
        super().cheat(page=page, chat_messages=chat_messages)
        chat_messages.append({"role": "assistant", "message": str(self.message)})

    def teardown(self) -> None:
        pass

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in compositional tasks.
        """
        raise NotImplementedError

class SendChatMessageForBudgetAllocationTask(SendChatMessageTask):
    def get_pretty_printed_description(self) -> str:


validate

        The type of answer to generate. Choice of total_return_only, total_return_and_investments, investments_only, cleanup, cleanup_and_return
    """

    def __init__(
        self,
        instance: SNowInstance,
        message: str,
        answer_format: str,
        use_description_in_l3: bool = False,
        **kwargs,
    ):
        super().__init__(seed=0, instance=instance, start_rel_url="")
        self.message = message
        self.answer_format = answer_format
        self.use_description_in_l3 = use_description_in_l3
        self.__dict__.update(kwargs)

    def setup_goal(self, page: Page):
        return self.get_pretty_printed_description(), {}

    def validate(self, page: Page, chat_messages: list[str]) -> Tuple[float | bool | str | dict]:
        return super().validate(page, chat_messages)

    def cheat(self, page: Page, chat_messages: list[str]):
        super().cheat(page=page, chat_messages=chat_messages)
        chat_messages.append({"role": "assistant", "message": str(self.message)})

    def teardown(self) -> None:
        pass

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in compositional tasks.
        """
        raise NotImplementedError

class SendChatMessageForBudgetAllocationTask(SendChatMessageTask):
    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in compositional tasks.
        """


cheat

    def __init__(
        self,
        instance: SNowInstance,
        message: str,
        answer_format: str,
        use_description_in_l3: bool = False,
        **kwargs,
    ):
        super().__init__(seed=0, instance=instance, start_rel_url="")
        self.message = message
        self.answer_format = answer_format
        self.use_description_in_l3 = use_description_in_l3
        self.__dict__.update(kwargs)

    def setup_goal(self, page: Page):
        return self.get_pretty_printed_description(), {}

    def validate(self, page: Page, chat_messages: list[str]) -> Tuple[float | bool | str | dict]:
        return super().validate(page, chat_messages)

    def cheat(self, page: Page, chat_messages: list[str]):
        super().cheat(page=page, chat_messages=chat_messages)
        chat_messages.append({"role": "assistant", "message": str(self.message)})

    def teardown(self) -> None:
        pass

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in compositional tasks.
        """
        raise NotImplementedError

class SendChatMessageForBudgetAllocationTask(SendChatMessageTask):
    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in compositional tasks.
        """
        if self.level == 3:
            task_info = "Allocate the budget to maximize revenue."
        elif self.level == 2:
            task_info = f"Allocate the budget to maximize revenue. This involves going over expense lines and identifying the ones maximizing revenue while fitting in the allowed budget of {self.budget} $. The returns are written in their short description."


teardown

        answer_format: str,
        use_description_in_l3: bool = False,
        **kwargs,
    ):
        super().__init__(seed=0, instance=instance, start_rel_url="")
        self.message = message
        self.answer_format = answer_format
        self.use_description_in_l3 = use_description_in_l3
        self.__dict__.update(kwargs)

    def setup_goal(self, page: Page):
        return self.get_pretty_printed_description(), {}

    def validate(self, page: Page, chat_messages: list[str]) -> Tuple[float | bool | str | dict]:
        return super().validate(page, chat_messages)

    def cheat(self, page: Page, chat_messages: list[str]):
        super().cheat(page=page, chat_messages=chat_messages)
        chat_messages.append({"role": "assistant", "message": str(self.message)})

    def teardown(self) -> None:
        pass

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in compositional tasks.
        """
        raise NotImplementedError

class SendChatMessageForBudgetAllocationTask(SendChatMessageTask):
    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in compositional tasks.
        """
        if self.level == 3:
            task_info = "Allocate the budget to maximize revenue."
        elif self.level == 2:
            task_info = f"Allocate the budget to maximize revenue. This involves going over expense lines and identifying the ones maximizing revenue while fitting in the allowed budget of {self.budget} $. The returns are written in their short description."
            if self.answer_format == "total_return_only":
                task_info += " Provide only the total return of the investments in the chat."
            if self.answer_format == "total_return_and_investments":


        if self.level == 3:
            task_info = "Allocate the budget to maximize revenue."
        elif self.level == 2:
            task_info = f"Allocate the budget to maximize revenue. This involves going over expense lines and identifying the ones maximizing revenue while fitting in the allowed budget of {self.budget} $. The returns are written in their short description."
            if self.answer_format == "total_return_only":
                task_info += " Provide only the total return of the investments in the chat."
            if self.answer_format == "total_return_and_investments":
                task_info += " Provide the total return of the investments as well as the value of their 'Number' field in the chat."
            if self.answer_format == "investments_only":
                task_info += " Provide only the value of the 'Number' field of the selected investments in the chat."
            if self.answer_format == "cleanup":
                task_info += " Delete the investments that will not be kept so that only the selected investments remain."
            if self.answer_format == "cleanup_and_return":
                task_info += " Delete the investments that will not be kept so that only the selected investments remain as well as returning their total value in the chat."

        return task_info

class SendChatMessageGenericTask(SendChatMessageTask):

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in compositional tasks.
        """
        if self.use_description_in_l3:
            task_info = self.description
        elif self.level == 3:
            task_info = ""
        elif self.level == 2:
            task_info = self.description

        return task_info


"""
Tasks that require interacting with the service catalog

"""

import json
import logging
from typing import List
import numpy as np
import playwright.sync_api

from playwright.sync_api import Page
import re
from time import sleep
from urllib import parse

from .base import AbstractServiceNowTask
from .utils.form import fill_text

from ..api.requests import (
    get_request_by_id,
    db_delete_from_table,
)
from ..config import (
    ORDER_DEVELOPER_LAPTOP_TASK_CONFIG_PATH,
    ORDER_IPAD_MINI_TASK_CONFIG_PATH,
    ORDER_IPAD_PRO_TASK_CONFIG_PATH,
    ORDER_SALES_LAPTOP_TASK_CONFIG_PATH,
    ORDER_STANDARD_LAPTOP_TASK_CONFIG_PATH,
    ORDER_APPLE_WATCH_TASK_CONFIG_PATH,
    ORDER_APPLE_MAC_BOOK_PRO15_TASK_CONFIG_PATH,
    ORDER_DEVELOPMENT_LAPTOP_PC_TASK_CONFIG_PATH,
    ORDER_LOANER_LAPTOP_TASK_CONFIG_PATH,
)
from ..instance import SNowInstance
from .utils.utils import check_url_suffix_match

ADDITIONAL_SOFTWARE = [
    "Slack",
    "Trello",
    "Salesforce",
    "QuickBooks",
    "Zoom",
    "Microsoft Office 365",
    "Google Workspace",
    "Asana",
    "HubSpot",
    "Adobe Creative Cloud",
]

META_CONFIGS = {
    "Developer Laptop (Mac)": {
        "desc": "Macbook Pro",
        "options": {
            "Adobe Acrobat": ("checkbox", [True, False]),
            "Eclipse IDE": ("checkbox", [True, False]),
            "Adobe Photoshop": ("checkbox", [True, False]),
            "Additional software requirements": ("textarea", ADDITIONAL_SOFTWARE),
        },
    },
    "iPad mini": {
        "desc": "Request for iPad mini",
        "options": {
            "Choose the colour": (
                "radio",
                ["Space Grey", "Pink", "Purple", "Starlight"],
            ),
            "Choose the storage": ("radio", ["64", "256"]),
        },
    },
    "iPad pro": {
        "desc": "Request for iPad pro",
        "options": {
            "Choose the colour": ("radio", ["Space Grey", "Silver"]),
            "Choose the storage": ("radio", ["128", "256", "512"]),
        },
    },
    "Sales Laptop": {
        "desc": "Acer Aspire NX",
        "options": {
            "Microsoft Powerpoint": ("checkbox", [True, False]),
            "Adobe Acrobat": ("checkbox", [True, False]),
            "Adobe Photoshop": ("checkbox", [True, False]),
            "Siebel Client": ("checkbox", [True, False]),
            "Additional software requirements": ("textarea", ADDITIONAL_SOFTWARE),
        },
    },
    "Standard Laptop": {
        "desc": "Lenovo - Carbon x1",
        "options": {
            "Adobe Acrobat": ("checkbox", [True, False]),
            "Adobe Photoshop": ("checkbox", [True, False]),
            "Additional software requirements": ("textarea", ADDITIONAL_SOFTWARE),
        },
    },
    "Apple Watch": {
        "desc": "Apple Watch - Their most personal device ever",
        "options": {},
    },
    "Apple MacBook Pro 15": {
        "desc": "Apple MacBook Pro",
        "options": {},
    },
    "Development Laptop (PC)": {
        "desc": "Dell XPS 13",
        "options": {
            "What size solid state drive do you want?": (
                "radio",
                [
                    "250",  # This needs to match both the radio option (250 GB [subtract 300$]) and db request (250)
                    "500",  # Similar as above
                ],
            ),
            "Please specify an operating system": ("radio", ["Windows 8", "Ubuntu"]),
        },
    },
    "Loaner Laptop": {
        "desc": "Short term, while computer is repaired/imaged. Waiting for computer order, special projects, etc. Training, special events, check-in process",
        "options": {
            "When do you need it ?": (
                "textarea",
                [
                    "ASAP",
                    "In 2 weeks",
                    "By the end of the month",
                    "On time for the next meeting",
                    "I needed it yesterday but since you are asking I guess I can wait a bit more",
                    "Do your best, I know you are busy",
                    "I don't need it anymore, I just wanted to see what would happen if I clicked on this button",
                ],
            ),
            "How long do you need it for ?": (
                "radio",
                [
                    "1 day",
                    "1 month",
                    "1 week",
                    "2 weeks",
                    "3 days",
                ],
            ),
        },
    },
}

class OrderHardwareTask(AbstractServiceNowTask):
    """
    Order an item from the service catalog.

    Parameters:
    -----------
    seed: int
        Random seed
    instance: SNowInstance
        The instance to use.
    fixed_request_item: str
        The item to order. If provided, the task will always order this item.
    fixed_config: dict
        Configuration to use for the task. If provided, the task will use the provided configuration instead of
        selecting a random one. See browsergym/workarena/data_files/task_configs/order_ipda_pro_task.json
        for an example of a configuration file.
    config_only_in_desc: bool
        If True, the model to order will be omitted from the task description in comp tasks.

    """

    def __init__(
        self,
        seed: int = None,
        instance: SNowInstance = None,
        fixed_request_item: str = None,
        fixed_config: dict = None,
        config_only_in_desc: bool = False,
        **kwargs,
    ):
        super().__init__(
            seed=seed,
            instance=instance,
            start_rel_url="/now/nav/ui/classic/params/target/catalog_home.do%3Fsysparm_view%3Dcatalog_default",
            final_rel_url="/now/nav/ui/classic/params/target/com.glideapp.servicecatalog_checkout_view_v2.do",
        )

        if fixed_request_item is not None and fixed_config is not None:
            if fixed_request_item != fixed_config["item"]:
                raise ValueError(f"'fixed_request_item' and 'fixed_config[\"item\"]' do not match")

        self.fixed_config = fixed_config
        self.config = None
        self.fixed_request_item = fixed_request_item
        self.config_only_in_desc = config_only_in_desc

        self.js_prefix = "gsft_main"
        self.js_api_forms = "g_form"
        self.all_configs = self.all_configs()
        self.__dict__.update(kwargs)

    @classmethod
    def all_configs(cls) -> List[dict]:
        with open(cls.config_path, "r") as f:
            return json.load(f)

    def _wait_for_ready(self, page: Page, wait_for_form_api: bool = False) -> None:
        """
        Waits for the the main iframe to be loaded

        """
        logging.debug(f"Waiting for {self.js_prefix} to be fully loaded")
        page.wait_for_function(
            f"typeof window.{self.js_prefix} !== 'undefined' && window.{self.js_prefix}.WORKARENA_LOAD_COMPLETE"
        )
        logging.debug(f"Detected {self.js_prefix} ready")

        if wait_for_form_api:
            logging.debug("Waiting for Glide form API to be available")
            page.wait_for_function(f"window.{self.form_js_selector}")
            logging.debug("Detected Glide form API ready")

    @property
    def form_js_selector(self):
        return self.js_prefix + "." + self.js_api_forms

    def get_init_scripts(self) -> List[str]:
        return super().get_init_scripts() + [
            "registerGsftMainLoaded()",
            self._get_disable_add_to_cart_script(),
            self._get_remove_top_items_panel_script(),
        ]

    def _get_disable_add_to_cart_script(self):
        """
        Disables the 'Add to Cart' button on the service catalog page
        This is necessary so that agents running in parallel do not interfere with each other (cart is shared between sessions)

        """
        script = """
            function disableAddToCartButton() {
                waLog('Searching for top items panel...', 'disableAddToCartButton');
                let button = document.querySelector('button[aria-label="Add to Cart"]');
                if (button) {
                    button.disabled = true;
                    waLog('WorkArena: Disabled the "Add to Cart" button', 'disableAddToCartButton');
                } else {
                    waLog('WorkArena: Could not find the "Add to Cart" button', 'disableAddToCartButton');
                }
            }

            runInGsftMainOnlyAndProtectByURL(disableAddToCartButton, 'glideapp.servicecatalog_cat_item_view.do');
        """
        return script

    def _get_remove_top_items_panel_script(self):
        """Get script that removes the 'top items' panel that sometimes on the landing page of service catalog
        Disables the 'Top Requests' panel that sometimes appears on the landing page of the service catalog
        Runs in a loop to keep checking for the host element and shadow root
        URL is secured by running only on the catalog_home page; this is a heuristic to avoid running on other pages
        and does not check that the URL is an exact match, as moving back and forth between pages can cause the URL
        to change, but catalog_home will always be present.
        """
        script = """
            function removeTopItemsPanel() {
                waLog('Searching for top items panel...', 'removeTopItemsPanel');
                let headings = Array.from(document.querySelectorAll('[role="heading"]'));
                headings.forEach((heading) => {
                    if (heading.textContent.includes("Top Requests")) {
                        let parentDiv = heading.closest('div.drag_section');
                        if (parentDiv) {
                            parentDiv.remove();
                            waLog('Removed parent div for heading: ' + heading.textContent, 'removeTopItemsPanel');
                        }
                    }
                });
            }

            runInGsftMainOnlyAndProtectByURL(removeTopItemsPanel, `catalog_home`);
            """
        return script

    def setup_goal(self, page: Page) -> tuple[str, dict]:
        super().setup_goal(page=page)

        # Get the task configuration
        assert self.all_configs is not None, "No configuration available for the task."
        self.config = (
            self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
        )
        self.requested_item = self.config["item"]
        self.short_description = self.config["description"]
        self.quantity = self.config["quantity"]
        self.requested_configuration = self.config["configuration"]

        # Generate goal
        if self.config_only_in_desc:
            goal = self.get_pretty_printed_description()
        else:
            goal = f'Go to the hardware store and order {self.quantity} "{self.requested_item}"'
        if len(self.requested_configuration) > 0:
            goal += f" with configuration {dict((k, v[1]) for k, v in self.requested_configuration.items())}"
        info = {}

        # Used to keep track of the sysid of the request for validation
        self.request_sysid = None

        return goal, info

    def cheat(self, page: Page, chat_messages: list[str]) -> None:
        super().cheat(page=page, chat_messages=chat_messages)
        self._wait_for_ready(page=page)

        iframe = page.frame(self.js_prefix)

        # Find hardware buttons
        element = iframe.wait_for_selector("a:text('Hardware')", strict=True)
        element.click()
        self._wait_for_ready(page=page)

        element = iframe.wait_for_selector(f"h2:has-text('{self.requested_item}')", strict=True)
        element.click()
        self._wait_for_ready(page=page, wait_for_form_api=True)

        quantity_input = iframe.wait_for_selector("#quantity", strict=True)
        quantity_input.select_option(str(self.quantity))

        editable_fields = page.evaluate(f"{self.form_js_selector}.getEditableFields()")

        lookup_map = {}
        for idx, field in enumerate(editable_fields):
            control_text = self._get_control_description(page, field)
            lookup_map[control_text] = field

        for field_label, (element, value) in self.requested_configuration.items():
            element_id = lookup_map[field_label]
            control_type = page.evaluate(f"{self.form_js_selector}.getControl('{element_id}').type")

            if control_type in ("radio",):
                num_options = page.evaluate(
                    f"{self.form_js_selector}.getControls('{element_id}').length"
                )
                for i in range(num_options):
                    control_handle = page.evaluate_handle(
                        f"{self.form_js_selector}.getControls('{element_id}')[{i}]"
                    )
                    control_text = control_handle.evaluate("e => e.parentElement.innerText")
                    if control_text.startswith(
                        value
                    ):  # the page changes the text dynamically adding subtract/add to the text
                        control_id = control_handle.get_attribute("id")
                        iframe.wait_for_selector(f'label[for="{control_id}"]', strict=True).click()
                        break
            elif control_type == "hidden":
                element_control = page.evaluate_handle(
                    f"{self.form_js_selector}.getControl('{element_id}')"
                )
                element_value = element_control.evaluate("e => e.value")
                if element_value != str(value).lower():
                    label_id = f"ni.{element_id}_label"
                    element_label = iframe.wait_for_selector(
                        f'label[id="{label_id}"]', strict=True, timeout=1_000
                    )
                    element_label.click()
            elif control_type in ("textarea", "text"):
                element_control = page.evaluate_handle(
                    f"{self.form_js_selector}.getControl('{element_id}')"
                ).as_element()  # this look superfluous
                element_id = element_control.get_attribute("id")  # this look superfluous
                text_element = iframe.query_selector(f'[id="{element_id}"]')
                text_element.click()
                fill_text(page=page, input_field=text_element, value=value, iframe=iframe)

            elif control_type == "select-one":
                iframe.locator(f"id={element_id}").select_option(value)
            else:
                raise ValueError(f"Unknown control type {control_type}")

        order_now_button = iframe.wait_for_selector("#oi_order_now_button", strict=True)

        with page.expect_navigation():
            order_now_button.click()

    def _generate_random_config(self, page: Page):
        """Generate a random configuration for the task"""
        self.task_is_setup = (
            False  # This is a hack to avoid raising an exception in the setup method
        )
        self.setup(page=page, do_start=False)
        if self.fixed_request_item:
            self.requested_item = self.fixed_request_item
        else:
            # ... choose a random item to order
            self.requested_item = self.random.choice(list(META_CONFIGS.keys()))

        meta_config = META_CONFIGS[self.requested_item]
        self.fixed_config = {
            "item": self.requested_item,
            "description": meta_config["desc"],
            "quantity": self.random.randint(1, 11),
            "configuration": {
                ctrl_name: (ctrl_type, self.random.choice(values))
                for ctrl_name, (ctrl_type, values) in meta_config["options"].items()
            },
        }
        self.setup(page=page, do_start=True)

    def _get_control_description(self, page, field):
        """
        Get the description of a control (e.g., the text of a radio button)
        """
        # Wait for everything to be ready
        self._wait_for_ready(page, wait_for_form_api=True)

        control_type = page.evaluate(f"{self.form_js_selector}.getControl('{field}').type")
        if control_type in ("radio", "select-one"):
            return page.evaluate(f"{self.form_js_selector}.getLabelOf('{field}')")
        elif control_type in ("textarea", "hidden", "text"):
            control_handle = page.evaluate_handle(f"{self.form_js_selector}.getControl('{field}')")
            # The control text is not always in the control itself, but in a parent element.
            # Being a heuristic, we try to find it by going up the DOM tree.
            # This is up to the page implementation, 5 is an arbitrary number.
            for depth in range(5):
                control_text = control_handle.evaluate("e => e.innerText")
                if control_text != "":
                    break
                control_handle = control_handle.evaluate_handle("e => e.parentElement")
            else:
                raise ValueError(f"Could not find control text for {field}")
        else:
            raise ValueError(f"Unknown control type {control_type}")
        return control_text

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in L3 compositional tasks.
        called by subclasses
        """
        class_name = self.__class__.__name__
        class_name = class_name.replace("Task", "")
        # Split the words
        words = re.findall(r"[A-Z][^A-Z]*", class_name)
        class_name_formatted = " ".join(words)
        task_specs = {
            "Quantity": self.config["quantity"],
            "Configuration": self.config["configuration"],
        }
        if self.config_only_in_desc:
            task_info = f"- Order the item in the following quantities and with the following configuration:\n"
        else:
            task_specs["Description"] = self.config["description"]
            task_info = f"- {class_name_formatted} with the following specifications:\n"
        for k, v in task_specs.items():
            # Some values might be empty - like the configuration of the apple watch. It is more natural to exclude them
            if not v:
                continue
            # If the value is a dictionary, print it in a nested way
            if isinstance(v, dict):
                task_info += f"    - {k}:\n"
                for k2, v2 in v.items():
                    task_info += f"        - {k2}: {v2[1]}\n"
            else:
                task_info += f"    - {k}: {v}\n"

        return task_info

    def teardown(self) -> None:
        """
        Deletes the request (and automatically all its items)
        """
        self._wait_for_ready(self.page)

        if hasattr(self, "request_sysid") and self.request_sysid is not None:
            db_delete_from_table(
                instance=self.instance, sys_id=self.request_sysid, table="sc_request"
            )

    def validate(self, page: Page, chat_messages: list[str]) -> tuple[int, bool, str, dict]:

        # Retrieve the request sysid from the URL
        current_url = parse.urlparse(parse.unquote(page.evaluate("() => window.location.href")))
        (self.request_sysid,) = parse.parse_qs(current_url.query).get("sysparm_sys_id", [None])
        if self.request_sysid is None:
            return (
                0,
                False,
                "",
                {"message": "The request was not created, the sysid is not in the URL."},
            )

        # Short sleep to make sure the data is saved in the DB
        # TODO: improve this (noted in issue 291)
        sleep(3)
        r = get_request_by_id(instance=self.instance, sysid=self.request_sysid)
        if r is None:
            return 0, False, "", {"message": "The request is not in the database."}

        if len(r["items"]) == 0:
            return 0, False, "", {"message": "No items were requested."}

        if len(r["items"]) > 1:
            error_msg = (
                "M
# ... truncated ...


OrderHardwareTask

                    "I needed it yesterday but since you are asking I guess I can wait a bit more",
                    "Do your best, I know you are busy",
                    "I don't need it anymore, I just wanted to see what would happen if I clicked on this button",
                ],
            ),
            "How long do you need it for ?": (
                "radio",
                [
                    "1 day",
                    "1 month",
                    "1 week",
                    "2 weeks",
                    "3 days",
                ],
            ),
        },
    },
}

class OrderHardwareTask(AbstractServiceNowTask):
    """
    Order an item from the service catalog.

    Parameters:
    -----------
    seed: int
        Random seed
    instance: SNowInstance
        The instance to use.
    fixed_request_item: str
        The item to order. If provided, the task will always order this item.
    fixed_config: dict
        Configuration to use for the task. If provided, the task will use the provided configuration instead of
        selecting a random one. See browsergym/workarena/data_files/task_configs/order_ipda_pro_task.json
        for an example of a configuration file.
    config_only_in_desc: bool
        If True, the model to order will be omitted from the task description in comp tasks.

    """

    def __init__(
        self,
        seed: int = None,
        instance: SNowInstance = None,
        fixed_request_item: str = None,
        fixed_config: dict = None,
        config_only_in_desc: bool = False,
        **kwargs,
    ):
        super().__init__(
            seed=seed,
            instance=instance,
            start_rel_url="/now/nav/ui/classic/params/target/catalog_home.do%3Fsysparm_view%3Dcatalog_default",
            final_rel_url="/now/nav/ui/classic/params/target/com.glideapp.servicecatalog_checkout_view_v2.do",
        )

        if fixed_request_item is not None and fixed_config is not None:
            if fixed_request_item != fixed_config["item"]:
                raise ValueError(f"'fixed_request_item' and 'fixed_config[\"item\"]' do not match")

        self.fixed_config = fixed_config
        self.config = None
        self.fixed_request_item = fixed_request_item
        self.config_only_in_desc = config_only_in_desc

        self.js_prefix = "gsft_main"
        self.js_api_forms = "g_form"
        self.all_configs = self.all_configs()
        self.__dict__.update(kwargs)

    @classmethod
    def all_configs(cls) -> List[dict]:
        with open(cls.config_path, "r") as f:
            return json.load(f)

    def _wait_for_ready(self, page: Page, wait_for_form_api: bool = False) -> None:
        """
        Waits for the the main iframe to be loaded

        """
        logging.debug(f"Waiting for {self.js_prefix} to be fully loaded")
        page.wait_for_function(
            f"typeof window.{self.js_prefix} !== 'undefined' && window.{self.js_prefix}.WORKARENA_LOAD_COMPLETE"
        )
        logging.debug(f"Detected {self.js_prefix} ready")

        if wait_for_form_api:
            logging.debug("Waiting for Glide form API to be available")
            page.wait_for_function(f"window.{self.form_js_selector}")
            logging.debug("Detected Glide form API ready")

    @property
    def form_js_selector(self):
        return self.js_prefix + "." + self.js_api_forms

    def get_init_scripts(self) -> List[str]:
        return super().get_init_scripts() + [
            "registerGsftMainLoaded()",
            self._get_disable_add_to_cart_script(),
            self._get_remove_top_items_panel_script(),
        ]

    def _get_disable_add_to_cart_script(self):
        """
        Disables the 'Add to Cart' button on the service catalog page
        This is necessary so that agents running in parallel do not interfere with each other (cart is shared between sessions)

        """
        script = """
            function disableAddToCartButton() {
                waLog('Searching for top items panel...', 'disableAddToCartButton');
                let button = document.querySelector('button[aria-label="Add to Cart"]');
                if (button) {
                    button.disabled = true;
                    waLog('WorkArena: Disabled the "Add to Cart" button', 'disableAddToCartButton');
                } else {
                    waLog('WorkArena: Could not find the "Add to Cart" button', 'disableAddToCartButton');
                }
            }

            runInGsftMainOnlyAndProtectByURL(disableAddToCartButton, 'glideapp.servicecatalog_cat_item_view.do');
        """
        return script

    def _get_remove_top_items_panel_script(self):
        """Get script that removes the 'top items' panel that sometimes on the landing page of service catalog
        Disables the 'Top Requests' panel that sometimes appears on the landing page of the service catalog
        Runs in a loop to keep checking for the host element and shadow root
        URL is secured by running only on the catalog_home page; this is a heuristic to avoid running on other pages
        and does not check that the URL is an exact match, as moving back and forth between pages can cause the URL
        to change, but catalog_home will always be present.
        """
        script = """
            function removeTopItemsPanel() {
                waLog('Searching for top items panel...', 'removeTopItemsPanel');
                let headings = Array.from(document.querySelectorAll('[role="heading"]'));
                headings.forEach((heading) => {
                    if (heading.textContent.includes("Top Requests")) {
                        let parentDiv = heading.closest('div.drag_section');
                        if (parentDiv) {
                            parentDiv.remove();
                            waLog('Removed parent div for heading: ' + heading.textContent, 'removeTopItemsPanel');
                        }
                    }
                });
            }

            runInGsftMainOnlyAndProtectByURL(removeTopItemsPanel, `catalog_home`);
            """
        return script

    def setup_goal(self, page: Page) -> tuple[str, dict]:
        super().setup_goal(page=page)

        # Get the task configuration
        assert self.all_configs is not None, "No configuration available for the task."
        self.config = (
            self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
        )
        self.requested_item = self.config["item"]
        self.short_description = self.config["description"]
        self.quantity = self.config["quantity"]
        self.requested_configuration = self.config["configuration"]

        # Generate goal
        if self.config_only_in_desc:
            goal = self.get_pretty_printed_description()
        else:
            goal = f'Go to the hardware store and order {self.quantity} "{self.requested_item}"'
        if len(self.requested_configuration) > 0:
            goal += f" with configuration {dict((k, v[1]) for k, v in self.requested_configuration.items())}"
        info = {}

        # Used to keep track of the sysid of the request for validation
        self.request_sysid = None

        return goal, info

    def cheat(self, page: Page, chat_messages: list[str]) -> None:
        super().cheat(page=page, chat_messages=chat_messages)
        self._wait_for_ready(page=page)

        iframe = page.frame(self.js_prefix)

        # Find hardware buttons
        element = iframe.wait_for_selector("a:text('Hardware')", strict=True)
        element.click()
        self._wait_for_ready(page=page)

        element = iframe.wait_for_selector(f"h2:has-text('{self.requested_item}')", strict=True)
        element.click()
        self._wait_for_ready(page=page, wait_for_form_api=True)

        quantity_input = iframe.wait_for_selector("#quantity", strict=True)
        quantity_input.select_option(str(self.quantity))

        editable_fields = page.evaluate(f"{self.form_js_selector}.getEditableFields()")

        lookup_map = {}
        for idx, field in enumerate(editable_fields):
            control_text = self._get_control_description(page, field)
            lookup_map[control_text] = field

        for field_label, (element, value) in self.requested_configuration.items():
            element_id = lookup_map[field_label]
            control_type = page.evaluate(f"{self.form_js_selector}.getControl('{element_id}').type")

            if control_type in ("radio",):
                num_options = page.evaluate(
                    f"{self.form_js_selector}.getControls('{element_id}').length"
                )
                for i in range(num_options):
                    control_handle = page.evaluate_handle(
                        f"{self.form_js_selector}.getControls('{element_id}')[{i}]"
                    )
                    control_text = control_handle.evaluate("e => e.parentElement.innerText")
                    if control_text.startswith(
                        value
                    ):  # the page changes the text dynamically adding subtract/add to the text
                        control_id = control_handle.get_attribute("id")
                        iframe.wait_for_selector(f'label[for="{control_id}"]', strict=True).click()
                        break
            elif control_type == "hidden":
                element_control = page.evaluate_handle(
                    f"{self.form_js_selector}.getControl('{element_id}')"
                )
                element_value = element_control.evaluate("e => e.value")
                if element_value != str(value).lower():
                    label_id = f"ni.{element_id}_label"
                    element_label = iframe.wait_for_selector(
                        f'label[id="{label_id}"]', strict=True, timeout=1_000
                    )
                    element_label.click()
            elif control_type in ("textarea", "text"):
                element_control = page.evaluate_handle(
                    f"{self.form_js_selector}.getControl('{element_id}')"
                ).as_element()  # this look superfluous
                element_id = element_control.get_attribute("id")  # this look superfluous
                text_element = iframe.query_selector(f'[id="{element_id}"]')
                text_element.click()
                fill_text(page=page, input_field=text_element, value=value, iframe=iframe)

            elif control_type == "select-one":
                iframe.locator(f"id={element_id}").select_option(value)
            else:
                raise ValueError(f"Unknown control type {control_type}")

        order_now_button = iframe.wait_for_selector("#oi_order_now_button", strict=True)

        with page.expect_navigation():
            order_now_button.click()

    def _generate_random_config(self, page: Page):
        """Generate a random configuration for the task"""
        self.task_is_setup = (
            False  # This is a hack to avoid raising an exception in the setup method
        )
        self.setup(page=page, do_start=False)
        if self.fixed_request_item:
            self.requested_item = self.fixed_request_item
        else:
            # ... choose a random item to order
            self.requested_item = self.random.choice(list(META_CONFIGS.keys()))

        meta_config = META_CONFIGS[self.requested_item]
        self.fixed_config = {
            "item": self.requested_item,
            "description": meta_config["desc"],
            "quantity": self.random.randint(1, 11),
            "configuration": {
                ctrl_name: (ctrl_type, self.random.choice(values))
                for ctrl_name, (ctrl_type, values) in meta_config["options"].items()
            },
        }
        self.setup(page=page, do_start=True)

    def _get_control_description(self, page, field):
        """
        Get the description of a control (e.g., the text of a radio button)
        """
        # Wait for everything to be ready
        self._wait_for_ready(page, wait_for_form_api=True)

        control_type = page.evaluate(f"{self.form_js_selector}.getControl('{field}').type")
        if control_type in ("radio", "select-one"):
            return page.evaluate(f"{self.form_js_selector}.getLabelOf('{field}')")
        elif control_type in ("textarea", "hidden", "text"):
            control_handle = page.evaluate_handle(f"{self.form_js_selector}.getControl('{field}')")
            # The control text is not always in the control itself, but in a parent element.
            # Being a heuristic, we try to find it by going up the DOM tree.
            # This is up to the page implementation, 5 is an arbitrary number.
            for depth in range(5):
                control_text = control_handle.evaluate("e => e.innerText")
                if control_text != "":
                    break
                control_handle = control_handle.evaluate_handle("e => e.parentElement")
            else:
                raise ValueError(f"Could not find control text for {field}")
        else:
            raise ValueError(f"Unknown control type {control_type}")
        return control_text

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in L3 compositional tasks.
        called by subclasses
        """
        class_name = self.__class__.__name__
        class_name = class_name.replace("Task", "")
        # Split the words
        words = re.findall(r"[A-Z][^A-Z]*", class_name)
        class_name_formatted = " ".join(words)
        task_specs = {
            "Quantity": self.config["quantity"],
            "Configuration": self.config["configuration"],
        }
        if self.config_only_in_desc:
            task_info = f"- Order the item in the following quantities and with the following configuration:\n"
        else:
            task_specs["Description"] = self.config["description"]
            task_info = f"- {class_name_formatted} with the following specifications:\n"
        for k, v in task_specs.items():
            # Some values might be empty - like the configuration of the apple watch. It is more natural to exclude them
            if not v:
                continue
            # If the value is a dictionary, print it in a nested way
            if isinstance(v, dict):
                task_info += f"    - {k}:\n"
                for k2, v2 in v.items():
                    task_info += f"        - {k2}: {v2[1]}\n"
            else:
                task_info += f"    - {k}: {v}\n"

        return task_info

    def teardown(self) -> None:
        """
        Deletes the request (and automatically all its items)
        """
        self._wait_for_ready(self.page)

        if hasattr(self, "request_sysid") and self.request_sysid is not None:
            db_delete_from_table(
                instance=self.instance, sys_id=self.request_sysid, table="sc_request"
            )

    def validate(self, page: Page, chat_messages: list[str]) -> tuple[int, bool, str, dict]:

        # Retrieve the request sysid from the URL
        current_url = parse.urlparse(parse.unquote(page.evaluate("() => window.location.href")))
        (self.request_sysid,) = parse.parse_qs(current_url.query).get("sysparm_sys_id", [None])
        if self.request_sysid is None:
            return (
                0,
                False,
                "",
                {"message": "The request was not created, the sysid is not in the URL."},
            )

        # Short sleep to make sure the data is saved in the DB
        # TODO: improve this (noted in issue 291)
        sleep(3)
        r = get_request_by_id(instance=self.instance, sysid=self.request_sysid)
        if r is None:
            return 0, False, "", {"message": "The request is not in the database."}

        if len(r["items"]) == 0:
            return 0, False, "", {"message": "No items were requested."}

        if len(r["items"]) > 1:
            error_msg = (
                "Multiple kinds of items were requested, but only a single one was expected."
            )
            return (
                0,
                True,
                error_msg,
                {"message": error_msg},
            )
        else:
            (first_item,) = r["items"]

        if first_item["short_description"].lower() != self.short_description.lower():
            error_msg = "The requested item is incorrect."
            return 0, True, error_msg, {"message": error_msg}

        if first_item["quantity"] != str(self.quantity):
            error_msg = "The requested quantity is incorrect."
            return 0, True, error_msg, {"message": error_msg}

        options = first_item["options"]
        for k, (element_type, value) in self.requested_configuration.items():
            if element_type == "checkbox" or element_type == "radio":
                if not option_match_heuristic(value, options[k]):
                    error_msg = (
                        f"The requested {k} is incorrect, expected {value} but got {options[k]}."
                    )
                    return (
                        0,
                        True,
                        error_msg,
                        {"message": error_msg},
                    )
            elif element_type == "textarea":
                if value.lower() not in options[k].lower():
                    error_msg = (
                        f"The requested {k} is incorrect, expected {value} but got {options[k]}."
                    )
                    return (
                        0,
                        True,
                        error_msg,
                        {"message": error_msg},
                    )

        return (
            1,
            True,
            "Nice work, thank you!",
            {"message": "Task completed successfully."},
        )

def option_match_heuristic(value, option):
    def _process(x):
        x = str(x).lower()
        x = x.replace("_", "")
        x = x.replace(" ", "")
        return x

    return _process(value) == _process(option)

class OrderDeveloperLaptopTask(OrderHardwareTask):
    config_path = ORDER_DEVELOPER_LAPTOP_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Developer Laptop (Mac)",
            **kwargs,


            elif element_type == "textarea":
                if value.lower() not in options[k].lower():
                    error_msg = (
                        f"The requested {k} is incorrect, expected {value} but got {options[k]}."
                    )
                    return (
                        0,
                        True,
                        error_msg,
                        {"message": error_msg},
                    )

        return (
            1,
            True,
            "Nice work, thank you!",
            {"message": "Task completed successfully."},
        )

def option_match_heuristic(value, option):
    def _process(x):
        x = str(x).lower()
        x = x.replace("_", "")
        x = x.replace(" ", "")
        return x

    return _process(value) == _process(option)

class OrderDeveloperLaptopTask(OrderHardwareTask):
    config_path = ORDER_DEVELOPER_LAPTOP_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Developer Laptop (Mac)",
            **kwargs,
        )

class OrderIpadMiniTask(OrderHardwareTask):
    config_path = ORDER_IPAD_MINI_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="iPad mini",


OrderDeveloperLaptopTask

                    )

        return (
            1,
            True,
            "Nice work, thank you!",
            {"message": "Task completed successfully."},
        )

def option_match_heuristic(value, option):
    def _process(x):
        x = str(x).lower()
        x = x.replace("_", "")
        x = x.replace(" ", "")
        return x

    return _process(value) == _process(option)

class OrderDeveloperLaptopTask(OrderHardwareTask):
    config_path = ORDER_DEVELOPER_LAPTOP_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Developer Laptop (Mac)",
            **kwargs,
        )

class OrderIpadMiniTask(OrderHardwareTask):
    config_path = ORDER_IPAD_MINI_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="iPad mini",
            **kwargs,
        )

class OrderIpadProTask(OrderHardwareTask):
    config_path = ORDER_IPAD_PRO_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="iPad pro",


OrderIpadMiniTask

    def _process(x):
        x = str(x).lower()
        x = x.replace("_", "")
        x = x.replace(" ", "")
        return x

    return _process(value) == _process(option)

class OrderDeveloperLaptopTask(OrderHardwareTask):
    config_path = ORDER_DEVELOPER_LAPTOP_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Developer Laptop (Mac)",
            **kwargs,
        )

class OrderIpadMiniTask(OrderHardwareTask):
    config_path = ORDER_IPAD_MINI_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="iPad mini",
            **kwargs,
        )

class OrderIpadProTask(OrderHardwareTask):
    config_path = ORDER_IPAD_PRO_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="iPad pro",
            **kwargs,
        )

class OrderSalesLaptopTask(OrderHardwareTask):
    config_path = ORDER_SALES_LAPTOP_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Sales Laptop",