# XXX: Some notes on plot types
#      - We currently don't support maps because they are clickable and would require a more evolved cheat function
SUPPORTED_PLOT_TYPES = ["area", "bar", "column", "line", "pie", "spline"]

# Get report filter config
config = SNowInstance().report_filter_config
if config is None:
    REPORT_DATE_FILTER = REPORT_TIME_FILTER = None
else:
    REPORT_DATE_FILTER = config["report_date_filter"]
    REPORT_TIME_FILTER = config["report_time_filter"]
del config

class DashboardRetrievalTask(AbstractServiceNowTask, ABC):
    """
    A task to retrieve information from a ServiceNow dashboard

    """

    def __init__(
        self, seed: int = None, instance: SNowInstance = None, fixed_config: dict = None, **kwargs
    ) -> None:
        super().__init__(seed=seed, instance=instance, start_rel_url="")
        self.iframe_id = "gsft_main"
        self.fixed_config = fixed_config
        self.__dict__.update(kwargs)

    @abstractmethod
    def all_configs(self) -> List[dict]:
        pass

    @abstractmethod
    def all_configs(self) -> List[dict]:
        pass

    def _get_charts(self, page: playwright.sync_api.Page) -> None:
        """
        Extract all charts on the page

        Parameters:
        -----------
        page: playwright.sync_api.Page
            The playright page on which the charts are to be extracted

        Returns:
        --------


class SingleChartValueRetrievalTask(DashboardRetrievalTask):
    def all_configs(self):
        return json.load(open(REPORT_RETRIEVAL_VALUE_CONFIG_PATH, "r"))

class SingleChartMinMaxRetrievalTask(DashboardRetrievalTask):
    def all_configs(self):
        return json.load(open(REPORT_RETRIEVAL_MINMAX_CONFIG_PATH, "r"))

class SingleChartMeanMedianModeRetrievalTask(
    DashboardRetrievalTask, CompositionalBuildingBlockTask
):
    def all_configs(self):
        return json.load(open(REPORT_RETRIEVAL_MINMAX_CONFIG_PATH, "r"))

class WorkLoadBalancingMinMaxRetrievalTask(
    SingleChartMinMaxRetrievalTask, CompositionalBuildingBlockTask
):
    def all_configs(self):
        return json.load(open(REPORT_RETRIEVAL_MINMAX_CONFIG_PATH, "r"))

    def setup_goal(self, page: playwright.sync_api.Page) -> Tuple[str | dict]:
        super().setup_goal(page=page)

        # Configure task
        # ... sample a configuration
        self.config = (
            self.fixed_config if self.fixed_config else self.random.choice(self.all_configs())
        )
        # ... set start URL based on config
        self.start_url = self.instance.snow_url + self.config["url"]

        goal = f"Create a filter to find reports whose title contains hashtag {self.problem_hashtag} and open the report."
        goal += " From the report, identify the user with the most assigned problems and the user with the least assigned problems."

        return goal, {}

__TASKS__ = [
    var


    A task to retrieve information from a ServiceNow dashboard

    """

    def __init__(
        self, seed: int = None, instance: SNowInstance = None, fixed_config: dict = None, **kwargs
    ) -> None:
        super().__init__(seed=seed, instance=instance, start_rel_url="")
        self.iframe_id = "gsft_main"
        self.fixed_config = fixed_config
        self.__dict__.update(kwargs)

    @abstractmethod
    def all_configs(self) -> List[dict]:
        pass

    @abstractmethod
    def all_configs(self) -> List[dict]:
        pass

    def _get_charts(self, page: playwright.sync_api.Page) -> None:
        """
        Extract all charts on the page

        Parameters:
        -----------
        page: playwright.sync_api.Page
            The playright page on which the charts are to be extracted

        Returns:
        --------
        charts: list
            A list of charts where the chart is represented as a tuple of the chart title and the id of the
            element that contains the chart.

        """
        iframe = page.frame(name=self.iframe_id)

        charts = page.evaluate(
            f"{self.iframe_id}.Highcharts.charts.map((x) => {{if(x){{return [x.renderTo.ariaLabel, x.renderTo.id];}}}})"
        )
        charts = [
            (title.replace("Highcharts interactive chart.", "").replace(".", "").strip(), id)
            for title, id in charts
            if title
            and iframe.locator(f"#{id}").count()
            > 0  # Check if the element is actually on page (sometime rendering breaks)
        ]

        return charts

    def _read_chart(self, page: playwright.sync_api.Page, element_id: str) -> str:
        """
        Read the chart at the specified index

        Parameters:
        -----------
        page: playwright.sync_api.Page
            The playright page on which the charts are to be extracted
        element_id: str
            The ID of the element that contains the chart

        Returns:
        --------
        chart_type: str
            The type of the chart
        chart_data: dict
            The data of the chart

        """


        charts: list
            A list of charts where the chart is represented as a tuple of the chart title and the id of the
            element that contains the chart.

        """
        iframe = page.frame(name=self.iframe_id)

        charts = page.evaluate(
            f"{self.iframe_id}.Highcharts.charts.map((x) => {{if(x){{return [x.renderTo.ariaLabel, x.renderTo.id];}}}})"
        )
        charts = [
            (title.replace("Highcharts interactive chart.", "").replace(".", "").strip(), id)
            for title, id in charts
            if title
            and iframe.locator(f"#{id}").count()
            > 0  # Check if the element is actually on page (sometime rendering breaks)
        ]

        return charts

    def _read_chart(self, page: playwright.sync_api.Page, element_id: str) -> str:
        """
        Read the chart at the specified index

        Parameters:
        -----------
        page: playwright.sync_api.Page
            The playright page on which the charts are to be extracted
        element_id: str
            The ID of the element that contains the chart

        Returns:
        --------
        chart_type: str
            The type of the chart
        chart_data: dict
            The data of the chart

        """
        self._wait_for_ready(page)

        # Validate plot type
        types = page.evaluate(
            f"{self.iframe_id}.Highcharts.charts.find(chart => chart && chart.renderTo.id === '{element_id}').types"
        )
        if len(set(types)) > 1:
            raise NotImplementedError("Multiple chart types in the same chart not supported")
        type = types[0]
        if type not in SUPPORTED_PLOT_TYPES:
            raise NotImplementedError(f"Chart type {type} not supported")

        # Get data
        data = page.evaluate(
            f"""
            {self.iframe_id}.Highcharts.charts.find(chart => chart && chart.renderTo.id === "{element_id}")
            .series.map(series => ({{
                name: series.name,
                data: series.data.map(
                              point => ({{
                                 label_cat: point.category,
                                 label_name: point.name,
                                 label_origx: point.origXValue,
                                 count: point.y,
                                 percent: point.percent
                              }}))
                }}));
            """
        )

        # Post-process each series
        for i in range(len(data)):
            # For each data point in the series
            for j in range(len(data[i]["data"])):
                data_point = data[i]["data"][j]

                # Remove None percent values when count is 0
                if data_point["count"] == 0:
                    data_point["percent"] = 0

                # Strip trailing spaces from labels
                data_point["label_cat"] = (
                    data_point["label_cat"].strip() if data_point["label_cat"] else ""
                )
                data_point["label_name"] = (
                    data_point["label_name"].strip() if data_point["label_name"] else ""
                )
                data_point["label_origx"] = (
                    data_point["label_origx"].strip() if data_point["label_origx"] else ""
                )

                # Determine which label to use (this is a heuristic)
                # Usually, when the origx value is present, it is more detailed than the other labels.
                # However, in some rare cases, it corresponds to some strange value that doesn't get rendered.
                # As a heuristic for the last point, let's just make sure that origx has at least a one trigram
                # overlap with any of the other labels.
                if data_point["label_origx"] != "" and any(
                    share_tri_gram(data_point["label_origx"], data_point[x])
                    for x in ["label_cat", "label_name"]
                ):
                    data_point["label"] = data_point["label_origx"]
                else:
                    if type in ["bar", "column", "spline"]:
                        data_point["label"] = data_point["label_cat"]
                    else:
                        data_point["label"] = data_point["label_name"]
                del data_point["label_cat"]
                del data_point["label_name"]
                del data_point["label_origx"]

            assert len(set([dp["label"] for dp in data[i]["data"]])) == len(
                data[i]["data"]
            ), "Detected duplicate labels in the same series"

        return type, data

    # retry because sometimes the page is not fully loaded
    @retry(stop=stop_after_attempt(3), wait=wait_fixed(1))
    def _get_chart_by_title(
        self, page: playwright.sync_api.Page, title: str = None
    ) -> Tuple[str, dict]:
        """
        Get the chart data by title

        Parameters:
        -----------
        page: playwright.sync_api.Page
            The playright page on which the charts are to be extracted
        title: str
            The title of the chart to be read. If None, returns the first chart.

        Returns:
        --------
        chart_type: str
            The type of the chart


                    for x in ["label_cat", "label_name"]
                ):
                    data_point["label"] = data_point["label_origx"]
                else:
                    if type in ["bar", "column", "spline"]:
                        data_point["label"] = data_point["label_cat"]
                    else:
                        data_point["label"] = data_point["label_name"]
                del data_point["label_cat"]
                del data_point["label_name"]
                del data_point["label_origx"]

            assert len(set([dp["label"] for dp in data[i]["data"]])) == len(
                data[i]["data"]
            ), "Detected duplicate labels in the same series"

        return type, data

    # retry because sometimes the page is not fully loaded
    @retry(stop=stop_after_attempt(3), wait=wait_fixed(1))
    def _get_chart_by_title(
        self, page: playwright.sync_api.Page, title: str = None
    ) -> Tuple[str, dict]:
        """
        Get the chart data by title

        Parameters:
        -----------
        page: playwright.sync_api.Page
            The playright page on which the charts are to be extracted
        title: str
            The title of the chart to be read. If None, returns the first chart.

        Returns:
        --------
        chart_type: str
            The type of the chart
        chart_data: dict
            The data of the chart
        element_id: str
            The ID of the element that contains the chart

        """
        # Get chart titles and element IDs
        charts = self._get_charts(page)

        if not title:
            title = charts[0][0]

        # Find chart index by title
        chart_idx = [title.lower() for title, _ in charts].index(title.lower())

        # Load chart data
        return *self._read_chart(page, element_id=charts[chart_idx][1]), charts[chart_idx][1]

    def _wait_for_ready(self, page: playwright.sync_api.Page) -> None:
        """
        Wait for the page to be ready for task execution

        Parameters:
        -----------
        page: playwright.sync_api.Page
            The page to wait on

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

        logging.debug("Waiting for Highcharts API to be available")
        page.wait_for_function(f"window.{self.iframe_id}.Highcharts")
        logging.debug("Detected Highcharts API ready")


        chart_type: str
            The type of the chart
        chart_data: dict
            The data of the chart
        element_id: str
            The ID of the element that contains the chart

        """
        # Get chart titles and element IDs
        charts = self._get_charts(page)

        if not title:
            title = charts[0][0]

        # Find chart index by title
        chart_idx = [title.lower() for title, _ in charts].index(title.lower())

        # Load chart data
        return *self._read_chart(page, element_id=charts[chart_idx][1]), charts[chart_idx][1]

    def _wait_for_ready(self, page: playwright.sync_api.Page) -> None:
        """
        Wait for the page to be ready for task execution

        Parameters:
        -----------
        page: playwright.sync_api.Page
            The page to wait on

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

        logging.debug("Waiting for Highcharts API to be available")
        page.wait_for_function(f"window.{self.iframe_id}.Highcharts")
        logging.debug("Detected Highcharts API ready")

        logging.debug("Waiting for all plots to be loaded available")
        page.wait_for_function(f"window.{self.iframe_id}.WORKARENA_HIGHCHARTS_ALL_LOADED")
        logging.debug("All plots loaded")

    def get_init_scripts(self) -> List[str]:
        return super().get_init_scripts() + [
            "registerGsftMainLoaded();",
            f"""
            async function renderAllCharts() {{
                waLog('Forcing load of all charts', 'loadAllCharts');

                await waitForCondition(() => window.WORKARENA_LOAD_COMPLETE, 100);

                const canvas = window.SNC.canvas;
                if (canvas) {{
                    waLog('This is a dashboard page.', 'loadAllCharts');
                    // Trigger the rendering of each widget
                    canvas.layoutJson.panes.forEach((p) => canvas.canvasUtils.renderSlowWidget(canvas.canvasUtils.getWidgetContainer(p.uuid)));
                    // Wait for all widgets to be rendered
                    await waitForCondition(() => window.SNC.canvas.layoutJson.panes.map((p) => p.isRendered).every(value => value == true), 100);
                }}
                else {{
                    waLog('This is a report page.', 'loadAllCharts');


        Parameters:
        -----------
        page: playwright.sync_api.Page
            The page to wait on

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

        logging.debug("Waiting for Highcharts API to be available")
        page.wait_for_function(f"window.{self.iframe_id}.Highcharts")
        logging.debug("Detected Highcharts API ready")

        logging.debug("Waiting for all plots to be loaded available")
        page.wait_for_function(f"window.{self.iframe_id}.WORKARENA_HIGHCHARTS_ALL_LOADED")
        logging.debug("All plots loaded")

    def get_init_scripts(self) -> List[str]:
        return super().get_init_scripts() + [
            "registerGsftMainLoaded();",
            f"""
            async function renderAllCharts() {{
                waLog('Forcing load of all charts', 'loadAllCharts');

                await waitForCondition(() => window.WORKARENA_LOAD_COMPLETE, 100);

                const canvas = window.SNC.canvas;
                if (canvas) {{
                    waLog('This is a dashboard page.', 'loadAllCharts');
                    // Trigger the rendering of each widget
                    canvas.layoutJson.panes.forEach((p) => canvas.canvasUtils.renderSlowWidget(canvas.canvasUtils.getWidgetContainer(p.uuid)));
                    // Wait for all widgets to be rendered
                    await waitForCondition(() => window.SNC.canvas.layoutJson.panes.map((p) => p.isRendered).every(value => value == true), 100);
                }}
                else {{
                    waLog('This is a report page.', 'loadAllCharts');
                    // Wait for axes to be visible (we need to use this approach since there is no canvas to help us)
                    await waitForCondition(() => document.body.innerText.toLowerCase().includes("no data to display") || document.querySelectorAll(".highcharts-point").length > 0, 100);
                }}

                // Wait for Highcharts to say that the charts are rendered
                waitForCondition(() => Highcharts.charts.all((c) => c.hasLoaded), 100)
                .then(() => {{
                            window.WORKARENA_HIGHCHARTS_ALL_LOADED = true;
                            waLog('All charts loaded', 'loadAllCharts');
                        }});
            }}
            // Run on both dashboard and reports pages
            runInGsftMainOnlyAndProtectByURL(renderAllCharts, 'pa_dashboard.do');
            runInGsftMainOnlyAndProtectByURL(renderAllCharts, 'sys_report_template.do');
            """,
            f"""
            function purifyReportUIButtons() {{
                // Delete a lot of UI features that were causing issues due to the report refreshing without
                // reloading the page. This makes the task easier, but it doesn't matter because we really
                // want to evaluate retrieval and this doesn't prevent that.
                document.querySelectorAll('[ng-click*="main.runReport"], #sidebar, #nlq-over-cb, #open-tree-navigation-button, .data-filtering-wrap').forEach(element => {{
                    if (element && element.parentNode) {{
                        element.parentNode.removeChild(element);
                    }}
                }});
                document.addEventListener('click', function(event) {{
                    event.stopPropagation();
                    event.preventDefault();
                }}, true);
                waLog('Purified report UI.', 'purifyReportUIButtons');
            }}
            // Run it only on the reports page
            runInGsftMainOnlyAndProtectByURL(purifyReportUIButtons, 'sys_report_template.do');
            """,
        ]

    def setup_goal(self, page: playwright.sync_api.Page) -> Tuple[str | dict]:
        super().setup_goal(page=page)

        # Check that the report filters are properly setup
        if REPORT_DATE_FILTER is None or REPORT_TIME_FILTER is None:
            raise RuntimeError(
                "The report date and time filters are not set. Please run the install script to set them."
            )

        # Configure task
        # ... sample a configuration
        self.config = (
            self.fixed_config if self.fixed_config else self.random.choice(self.all_configs())
        )
        # ... set start URL based on config
        # ...... some of the reports have need a date filter to be applied so we do this by patching a placeholder in the URL
        self.start_url = self.instance.snow_url + self.config["url"].replace(
            "REPORT_DATE_FILTER", REPORT_DATE_FILTER
        ).replace("REPORT_TIME_FILTER", REPORT_TIME_FILTER)


class SingleChartMinMaxRetrievalTask(DashboardRetrievalTask):
    def all_configs(self):
        return json.load(open(REPORT_RETRIEVAL_MINMAX_CONFIG_PATH, "r"))

class SingleChartMeanMedianModeRetrievalTask(
    DashboardRetrievalTask, CompositionalBuildingBlockTask
):
    def all_configs(self):
        return json.load(open(REPORT_RETRIEVAL_MINMAX_CONFIG_PATH, "r"))

class WorkLoadBalancingMinMaxRetrievalTask(
    SingleChartMinMaxRetrievalTask, CompositionalBuildingBlockTask
):
    def all_configs(self):
        return json.load(open(REPORT_RETRIEVAL_MINMAX_CONFIG_PATH, "r"))

    def setup_goal(self, page: playwright.sync_api.Page) -> Tuple[str | dict]:
        super().setup_goal(page=page)

        # Configure task
        # ... sample a configuration
        self.config = (
            self.fixed_config if self.fixed_config else self.random.choice(self.all_configs())
        )
        # ... set start URL based on config
        self.start_url = self.instance.snow_url + self.config["url"]

        goal = f"Create a filter to find reports whose title contains hashtag {self.problem_hashtag} and open the report."
        goal += " From the report, identify the user with the most assigned problems and the user with the least assigned problems."

        return goal, {}

__TASKS__ = [
    var
    for var in locals().values()
    if isinstance(var, type)
    and issubclass(var, DashboardRetrievalTask)
    and not issubclass(var, CompositionalBuildingBlockTask)
    and var is not DashboardRetrievalTask
]


cheat

            else "the chart"
        )
        if self.config["question"].startswith("value"):
            q_info = [x.strip() for x in self.config["question"].split(";")]
            goal = f'What is the value of "{q_info[2]}" in {chart_locator} (in {q_info[1]})?'
        elif self.config["question"] == "max":
            goal = f"What is the maximum value in {chart_locator}? Give me both the label and the count. If there are many, pick one."
        elif self.config["question"] == "min":
            goal = f"What is the minimum value in {chart_locator}? Give me both the label and the count. If there are many, pick one."
        elif self.config["question"] == "mean":
            goal = f"What is the average value in {chart_locator}? Round off to the next highest integer."
        elif self.config["question"] == "median":
            goal = f"What is the median value in {chart_locator}?"
        elif self.config["question"] == "mode":
            goal = f"What is the mode value in {chart_locator}?"
        else:
            raise NotImplementedError(f"Question type {self.config['question']} not supported")

        return goal, {}

    def cheat(self, page: playwright.sync_api.Page, chat_messages: list[str]) -> None:
        super().cheat(page, chat_messages)
        # Check if the page is the report list view. If so, open the report
        page_is_report_list_view = check_url_suffix_match(
            page, "/now/nav/ui/classic/params/target/sys_report_list.do", self
        )
        chart_title = self.config["chart_title"]
        if page_is_report_list_view:
            # Open the report
            frame = page.wait_for_selector('iframe[name="gsft_main"]').content_frame()
            # Search for the report by title
            frame.get_by_label("Search a specific field of the Reports list").select_option("Title")
            search_input = frame.locator('input[aria-label="Search"]')
            search_input.click()
            search_input.fill(chart_title)
            search_input.press("Enter")
            page.wait_for_function(
                "typeof window.gsft_main !== 'undefined' && window.gsft_main.WORKARENA_LOAD_COMPLETE"
            )
            # Click on the chart preview to open it
            frame.wait_for_selector(f'a[aria-label="Preview record: {chart_title}"]').click()
            page.wait_for_timeout(1000)
            page.keyboard.press("Enter")
            # Now in the form view, wait for the page to load and click to view the report
            page.wait_for_function(
                "typeof window.gsft_main !== 'undefined' && window.gsft_main.WORKARENA_LOAD_COMPLETE"
            )
            frame = page.wait_for_selector('iframe[name="gsft_main"]').content_frame()
            frame.get_by_text("View Report").first.click()

        self._wait_for_ready(page)

        # Get the chart data
        chart_type, chart_data, chart_element_id = self._get_chart_by_title(
            page, self.config["chart_title"]
        )

        # Extract the series
        if len(chart_data) == 1:
            chart_data = chart_data[0]["data"]
        else:
            chart_data = [
                series["data"]
                for series in chart_data
                if series["name"] == self.config["chart_series"]
            ][0]

        # Scroll to the chart
        iframe = page.frame(name=self.iframe_id)
        iframe.evaluate_handle(
            f"findElementInShadowDOM('#{chart_element_id}')"
        ).scroll_into_view_if_needed()

        # Extract the value and add it to the chat
        if self.config["question"].startswith("value"):
            format = self.config["question"].split(";")[1].strip()
            label = self.config["question"].split(";")[2].strip()
            value = [
                point["count" if format == "count" else "percent"]
                for point in chart_data
                if point["label"] == label
            ][0]
            chat_messages.append({"message": str(value), "role": "assistant"})
        elif self.config["question"] == "max":
            max_point = max(chart_data, key=lambda x: x["count"])
            chat_messages.append(
                {"message": f"{max_point['label']}, {max_point['count']}", "role": "assistant"}
            )
        elif self.config["question"] == "min":
            min_point = min(chart_data, key=lambda x: x["count"])
            chat_messages.append(
                {"message": f"{min_point['label']}, {min_point['count']}", "role": "assistant"}
            )
        elif self.config["question"] == "mean":
            counts = [data["count"] for data in chart_data]
            target_count = np.mean(counts)
            chat_messages.append({"message": f"Mean / Average {target_count}", "role": "assistant"})
        elif self.config["question"] == "median":
            counts = [data["count"] for data in chart_data]
            target_count = np.median(counts)
            chat_messages.append({"message": f"Median {target_count}", "role": "assistant"})
        elif self.config["question"] == "mode":
            counts = [data["count"] for data in chart_data]
            # We select the maximum value if there are two or more modes
            frequencies = {}
            for count in counts:
                if count not in frequencies:
                    frequencies[count] = 1
                else:
                    frequencies[count] += 1
            sorted_frequencies = {
                count: frequency
                for count, frequency in sorted(
                    frequencies.items(), key=lambda item: item[1], reverse=True
                )
            }
            max_frequency = list(sorted_frequencies.values())[0]
            max_frequencies = [
                count
                for count, frequency in sorted_frequencies.items()
                if frequency == max_frequency
            ]
            target_count = max(max_frequencies)
            chat_messages.append({"message": f"Mode {target_count}", "role": "assistant"})
        else:
            raise NotImplementedError(f"Question type \"{self.config['question']}\" not supported")

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

        # Check if the page is in the right URL
        logging.debug("Checking if the page is in the right URL to validate the task")
        right_url = check_url_suffix_match(page, expected_url=self.start_url, task=self)
        if not right_url:
            return (
                0,
                False,
                "",
                {
                    "message": f"The page is not in the right URL to validate task {self.__class__.__name__}."
                },
            )

        self._wait_for_ready(page)


validate

                    frequencies[count] = 1
                else:
                    frequencies[count] += 1
            sorted_frequencies = {
                count: frequency
                for count, frequency in sorted(
                    frequencies.items(), key=lambda item: item[1], reverse=True
                )
            }
            max_frequency = list(sorted_frequencies.values())[0]
            max_frequencies = [
                count
                for count, frequency in sorted_frequencies.items()
                if frequency == max_frequency
            ]
            target_count = max(max_frequencies)
            chat_messages.append({"message": f"Mode {target_count}", "role": "assistant"})
        else:
            raise NotImplementedError(f"Question type \"{self.config['question']}\" not supported")

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

        # Check if the page is in the right URL
        logging.debug("Checking if the page is in the right URL to validate the task")
        right_url = check_url_suffix_match(page, expected_url=self.start_url, task=self)
        if not right_url:
            return (
                0,
                False,
                "",
                {
                    "message": f"The page is not in the right URL to validate task {self.__class__.__name__}."
                },
            )

        self._wait_for_ready(page)

        # Get the chart data
        logging.debug("Extracting chart data")
        _, chart_data, _ = self._get_chart_by_title(page, self.config["chart_title"])

        # Extract the series
        logging.debug("Extracting the series")
        if len(chart_data) == 1:
            chart_data = chart_data[0]["data"]
        else:
            chart_data = [
                series["data"]
                for series in chart_data
                if series["name"] == self.config["chart_series"]
            ][0]

        # Extract the agent's response
        logging.debug("Extracting the agent's response")
        if chat_messages and chat_messages[-1]["role"] == "assistant":
            response = chat_messages[-1]["message"]
        else:
            return (
                0,
                False,
                "",
                {"message": "The assistant did not provide an answer."},
            )

        # Extract all numbers mentioned by the agent
        logging.debug("Extracting all numbers mentioned by the agent")
        # ... some value labels may contain numbers so we need to remove the labels from the response first
        labels = set([point["label"] for point in chart_data])
        response_ = str(response)
        for label in labels:
            response_ = response_.replace(label, "")
        # ... then we extract numbers
        response_floats = np.unique(
            [float(x) for x in re.findall(r"[\d]+(?:[.,]\d+)?", response_.replace(",", ""))]
        )
        del response_

        if len(response_floats) == 0:
            return (
                0.0,
                False,
                "No number detected in the response.",
                {"message": "No number detected in the response."},
            )

        # Validate the response
        logging.debug("Validating the response based on the question type")
        if self.config["question"].startswith("value"):
            logging.debug("The question is a value question")
            # if more than one number is in the prompt, there is necessarily a false positive
            if len(response_floats) > 1:
                error_msg = "Incorrect answer. More than one number detected in the response."
                return 0.0, True, error_msg, {"message": error_msg}

            logging.debug(
                f"Extracting expected format and label from question for validation: {self.config['question']}"
            )
            format = self.config["question"].split(";")[1].strip()
            label = self.config["question"].split(";")[2].strip()
            logging.debug(f"Extracted format: {format}, label: {label}")

            expected_value = float(
                [
                    point["count" if format == "count" else "percent"]
                    for point in chart_data
                    if point["label"] == label
                ][0]
            )
            if np.isclose(expected_value, response_floats[0]):
                return 1.0, True, "Nice work, thank you!", {"message": "Correct answer."}
            else:
                return 0.0, True, f"Incorrect answer.", {"message": "Incorrect answer."}

        # ... validate max/min responses
        elif "max" in self.config["question"] or "min" in self.config["question"]:
            # Determine whether to find max or min based on configuration
            target_func = max if self.config["question"] == "max" else min
            logging.debug(f"The question is a {str(target_func)} question")

            # Get the target count value (max or min)
            target_count = float(target_func(chart_data, key=lambda x: x["count"])["count"])

            # Find all points with the target count value
            target_points = [point for point in chart_data if point["count"] == target_count]

            # if more than one number is in the prompt, there is necessarily a false positive
            if len(response_floats) > 1:
                error_msg = "Incorrect answer. More than one number detected in the response."
                return 0.0, True, error_msg, {"message": error_msg}

            # Check if any of these points are mentioned in the response
            for point in target_points:
                if point["label"].lower() in response.lower() and np.isclose(
                    target_count, response_floats[0]
                ):
                    return 1.0, True, "Nice work, thank you!", {"message": "Correct answer."}

            # If no correct point is mentioned in the response
            return 0.0, True, "Incorrect answer.", {"message": "Incorrect answer."}
        # ... validate mean/median/mode responses
        elif (
            "mean" in self.config["question"]
            or "median" in self.config["question"]
            or "mode" in self.config["question"]
        ):
            counts = [data["count"] for data in chart_data]
            if self.config["question"] == "mean":
                target_count = np.mean(counts)
            elif self.config["question"] == "median":
                target_count = np.median(counts)
            elif self.config["question"] == "mode":
                _vals, _counts = np.unique(counts, return_counts=True)
                max_frequency_index = np.argmax(_counts)
                target_count = -_vals[max_frequency_index]

            # if more than one number is in the prompt, there is necessarily a false positive
            if len(response_floats) > 1:
                error_msg = "Incorrect answer. More than one number detected in the response."
                return 0.0, True, error_msg, {"message": error_msg}

            # Check if any of these points are mentioned in the response
            if np.isclose(target_count, response_floats[0]):
                return 1.0, True, "Nice work, thank you!", {"message": "Correct answer."}

            # If no correct point is mentioned in the response
            return 0.0, True, "Incorrect answer.", {"message": "Incorrect answer."}

        else:
            raise NotImplementedError(f"Question type \"{self.config['question']}\" not supported")

    def teardown(self) -> None:
        return super().teardown()

    def _generate_random_config(
        self, page: playwright.sync_api.Page, is_report=True, question_types=["value"]
    ) -> dict:
        """
        Generate a random configuration for the task

        This can be used to regenerate configs that are valid under an updated date filter.

        Parameters:
        -----------
        page: playwright.sync_api.Page
            The page on which the task is to be executed
        is_report: bool
            Whether to sample a report or a dashboard task configuration
        question_types: list
            The types of questions to sample from (uniformely)


teardown

            elif self.config["question"] == "mode":
                _vals, _counts = np.unique(counts, return_counts=True)
                max_frequency_index = np.argmax(_counts)
                target_count = -_vals[max_frequency_index]

            # if more than one number is in the prompt, there is necessarily a false positive
            if len(response_floats) > 1:
                error_msg = "Incorrect answer. More than one number detected in the response."
                return 0.0, True, error_msg, {"message": error_msg}

            # Check if any of these points are mentioned in the response
            if np.isclose(target_count, response_floats[0]):
                return 1.0, True, "Nice work, thank you!", {"message": "Correct answer."}

            # If no correct point is mentioned in the response
            return 0.0, True, "Incorrect answer.", {"message": "Incorrect answer."}

        else:
            raise NotImplementedError(f"Question type \"{self.config['question']}\" not supported")

    def teardown(self) -> None:
        return super().teardown()

    def _generate_random_config(
        self, page: playwright.sync_api.Page, is_report=True, question_types=["value"]
    ) -> dict:
        """
        Generate a random configuration for the task

        This can be used to regenerate configs that are valid under an updated date filter.

        Parameters:
        -----------
        page: playwright.sync_api.Page
            The page on which the task is to be executed
        is_report: bool
            Whether to sample a report or a dashboard task configuration
        question_types: list
            The types of questions to sample from (uniformely)

        """
        # Check that the report filters are properly setup


                target_count = -_vals[max_frequency_index]

            # if more than one number is in the prompt, there is necessarily a false positive
            if len(response_floats) > 1:
                error_msg = "Incorrect answer. More than one number detected in the response."
                return 0.0, True, error_msg, {"message": error_msg}

            # Check if any of these points are mentioned in the response
            if np.isclose(target_count, response_floats[0]):
                return 1.0, True, "Nice work, thank you!", {"message": "Correct answer."}

            # If no correct point is mentioned in the response
            return 0.0, True, "Incorrect answer.", {"message": "Incorrect answer."}

        else:
            raise NotImplementedError(f"Question type \"{self.config['question']}\" not supported")

    def teardown(self) -> None:
        return super().teardown()

    def _generate_random_config(
        self, page: playwright.sync_api.Page, is_report=True, question_types=["value"]
    ) -> dict:
        """
        Generate a random configuration for the task

        This can be used to regenerate configs that are valid under an updated date filter.

        Parameters:
        -----------
        page: playwright.sync_api.Page
            The page on which the task is to be executed
        is_report: bool
            Whether to sample a report or a dashboard task configuration
        question_types: list
            The types of questions to sample from (uniformely)

        """
        # Check that the report filters are properly setup
        if REPORT_DATE_FILTER is None or REPORT_TIME_FILTER is None:
            raise RuntimeError(
                "The report date and time filters are not set. Please run the install script to set them."
            )

        # Generate a bunch of reports based on valid table fields
        ON_THE_FLY_REPORTS = []
        for table in [
            "alm_asset",
            "alm_hardware",
            "asmt_assessment_instance_question",
            "asmt_m2m_stakeholder",
            "ast_contract",
            "change_request",
            "cmdb_ci_computer",
            "incident",
            "sc_cat_item",
            "sys_user",
        ]:
            cols = [
                x
                for x, y in table_column_info(instance=self.instance, table=table).items()
                if y.get("cangroup", False)
                and y.get("type", None) == "choice"
                and "upon" not in x.lower()
            ]
            for col in cols:
                ON_THE_FLY_REPORTS.append({"table": table, "field": col, "type": "pie"})
                ON_THE_FLY_REPORTS.append({"table": table, "field": col, "type": "bar"})

        # Reports that are already in the instance
        system_report_tables = "alm_asset,alm_hardware,asmt_assessment_instance_question,asmt_m2m_stakeholder,ast_contract,change_request,cmdb_ci_computer"
        SYSTEM_REPORTS = table_api_call(
            instance=self.instance,
            table="sys_report",
            params={
                "sysparm_query": f"sys_class_name=sys_report^active=true^typeINtrend,donut,vertical_bar,line,horizontal_bar,pie,bar,spline,area^descriptionLIKE{REPORT_PATCH_FLAG}^tableIN{system_report_tables}",
                "sysparm_fields": "sys_id",
            },
        )["result"]

        REPORTS = ON_THE_FLY_REPORTS + SYSTEM_REPORTS

        # XXX: It's not ideal to use sys_ids but I couldn't find a better way
        DASHBOARDS = [
            "812fa4400f1130101527008c07767e1a",  # Assessment overview
            "fa5fe3e1773130107384c087cc5a99d5",  # Asset overview
            "68ee1f30770230107384c087cc5a992e",  # Asset contract overview
            "05b0a8b7c3123010a282a539e540dd69",  # Change overview
            "18b1f472533130104c90ddeeff7b12a6",  # Incident overview
            "287d07d1ff3130106c1ef9a7cddcbd5d",  # Request overview
            "7ab78953eb32011008f2951ff15228e6",  # Service catalog overview
            "2d297c880f1130101527008c07767e27",  # Survey overview
            "6b706f448f231110953ddffc9071a4f3",  # Telemetry - Table growth
            "15c5d2d377213010a435478c4f5a993c",  # Usage overview
            "85a57f9677100110ba155631dc5a9905",  # Web api usage overview
            "c38ca3a273031010ae8dd21efaf6a747",  # Data classification
            "3d48f669538223008329ddeeff7b1253",  # Problem overview
        ]

        # Select between a full dashboard and a report
        if is_report:
            report = REPORTS[self.random.randint(0, len(REPORTS))]

            # On the fly generated report
            if not report.get("sys_id", None):
                # ... these receive a filter that is added through the URL
                url = f"/now/nav/ui/classic/params/target/sys_report_template.do%3Fsysparm_field%3D{report['field']}%26sysparm_type%3D{report['type']}%26sysparm_table%3D{report['table']}%26sysparm_from_list%3Dtrue%26sysparm_chart_size%3Dlarge%26sysparm_manual_labor%3Dtrue%26sysparm_query=sys_created_on<javascript:gs.dateGenerate('{REPORT_DATE_FILTER}','{REPORT_TIME_FILTER}')^EQ"
            # Report from the database
            else:
                url = f"/now/nav/ui/classic/params/target/sys_report_template.do%3Fjvar_report_id={report['sys_id']}"
        else:
            # Dashboard from the database
            dashboard = self.random.choice(DASHBOARDS)
            url = f"/now/nav/ui/classic/params/target/%24pa_dashboard.do%3Fsysparm_dashboard%3D{dashboard}"

        # We need to do this to bypass the init script protection by URL
        self.fixed_config = {
            "url": url,
            "chart_title": "",
            "chart_series": "",
            "question": "max",
        }  # Dummy config
        self.setup(page=page)

        # Handle the case where a dashboard is not found
        page.wait_for_load_state("networkidle")
        iframe = page.frame(name=self.iframe_id)
        assert iframe.get_by_text("not found").count() == 0, "Report or dashboard not found"

        # Find all the charts
        self._wait_for_ready(page)
        charts = self._get_charts(page)

        # Randomly select a chart
        assert len(charts) > 0, f"No charts found on the page {self.instance.snow_url}{url}"
        chart_idx = self.random.randint(0, len(charts))
        chart_title = charts[chart_idx][0] if not is_report else ""  # No title for reports
        _, chart_data, _ = self._get_chart_by_title(page, chart_title)

        # Select a series randomly
        series_idx = self.random.randint(len(chart_data))
        chart_series = chart_data[series_idx]["name"] if len(chart_data) > 1 else ""
        chart_data = chart_data[series_idx]["data"]

        # Check if the data is interesting
        labels = [point["label"] for point in chart_data]
        assert len(labels) > 1, f"Not enough data in the chart (only {len(labels)} label)"
        assert not any(
            l.isdigit() for l in labels
        ), "Some chart labels are digits, which would cause errors in validation. Skipping."

        # Sample a type of question
        question = self.random.choice(question_types)

        if question == "value":
            # Sample a random type of value to ask for
            format = self.random.choice(["count", "percent"])

            # Select a random label from the chart data
            label = self.random.choice(labels)

            return {
                "url": url,
                "chart_title": chart_title,
                "chart_series": chart_series,
                "question": f"{question}; {format}; {label}",
            }
        else:
            return {
                "url": url,
                "chart_title": chart_title,
                "chart_series": chart_series,
                "question": question,
            }

class MultiChartValueRetrievalTask(DashboardRetrievalTask):
    def all_configs(self):
        return json.load(open(DASHBOARD_RETRIEVAL_VALUE_CONFIG_PATH, "r"))

class MultiChartMinMaxRetrievalTask(DashboardRetrievalTask):
    def all_configs(self):
        return json.load(open(DASHBOARD_RETRIEVAL_MINMAX_CONFIG_PATH, "r"))

class SingleChartValueRetrievalTask(DashboardRetrievalTask):
    def all_configs(self):
        return json.load(open(REPORT_RETRIEVAL_VALUE_CONFIG_PATH, "r"))

class SingleChartMinMaxRetrievalTask(DashboardRetrievalTask):
    def all_configs(self):
        return json.load(open(REPORT_RETRIEVAL_MINMAX_CONFIG_PATH, "r"))


import json
import time
from ...api.utils import table_api_call
from playwright.sync_api import Page

def create_private_task_and_get_sys_id(
    instance,
    page: Page,
    private_task_id: str,
    task_info: str,
    short_description: str,
    user_sys_id: str = None,
) -> None:
    """
    Create a private task in the ServiceNow instance to store the task information. Used for level 3 tasks.
    Sets the sys_id of the private task to the sys_id attribute of the task.
    Returns the sys_id of the private task.
    Parameters:
    ----------
    instance: SNowInstance
        The instance to use.
    page: Page
        playwright page
    private_task_id: str
        ID of the private task to be created.
    task_info: str
        The information needed to complete the task, written in the private task description.
    short_description: str
        A short description of the task, written in the private task short description.
    user_sys_id: str
        The sys_id of the user to assign the task to. If None, the task will be assigned to the admin user.
    """
    page.wait_for_load_state("networkidle")
    if user_sys_id is None:
        # Get the user sys_id; if the page is blank, use the admin user
        if page.url == "about:blank":
            user_sys_id = table_api_call(
                instance=instance,
                table="sys_user",
                params={"sysparm_query": "user_name=admin"},
            )["result"][0]["sys_id"]
        else:
            user_sys_id = page.evaluate("() => NOW.user.userID")
    # Create private task containing the information needed to complete the task
    result = table_api_call(
        instance=instance,
        table="vtb_task",
        data=json.dumps(
            {
                "number": f"{private_task_id}",
                "description": f"{task_info}",
                "short_description": f"{short_description}",
                "assigned_to": f"{user_sys_id}",
            }
        ),
        method="POST",
    )["result"]
    sys_id = result["sys_id"]

    assert result, "Failed to create private task"

    return sys_id


import playwright.sync_api

# TODO: is this used?
def debug_task(task, random_seed=3):
    with playwright.sync_api.sync_playwright() as p:
        browser = p.chromium.launch_persistent_context(
            user_data_dir="/tmp/workarena_cromium_user_data",
            headless=False,  # Set headless=True for a non-GUI mode
        )
        (page,) = browser.pages

        try:
            task.setup(seed=random_seed, page=page)
            valid_res = task.validate(page, [])
            assert valid_res is False
            task.cheat()
            assert task.validate(page, []) is True
        finally:
            task.teardown()
            browser.close()


import time
from ...config import SNOW_BROWSER_TIMEOUT

def fill_text(page, input_field, value, iframe=None):
    """
    Fills the value of text field, while handling autocomplete menus.

    Parameters
    ----------
    page : playwright.sync_api.page.Page
        The page object
    input_field : playwright locator
        The locator of the input field
    value : str
        The value to fill in
    iframe : playwright locator, optional
        The locator of the iframe that contains the input field, by default None

    """
    if iframe is None:
        iframe = page

    # Click into the field (this sometimes causes some aria autocompletion attributes to be set)
    input_field.click(force=True)

    # If the field uses autocomplete, we need to wait for Ajax to finish (and expand the menu)
    if input_field.get_attribute("aria-autocomplete") == "list" and value != "":
        # Fill in the value using a procedure that triggers the autocomplete
        input_field.fill(value[:-1])
        page.keyboard.press(value[-1])
        time.sleep(0.5)

        # Wait for the autocomplete menu to open and be ready
        max_wait_time = SNOW_BROWSER_TIMEOUT  # maximum time to wait in seconds
        start_time = time.time()
        while True:
            if input_field.get_attribute("aria-expanded") == "true" and not input_field.evaluate(
                "e => e.ac.isResolving()"
            ):
                break
            if time.time() - start_time > (max_wait_time / 1000):
                raise TimeoutError("Timeout waiting for autocompletion menu to open")
            time.sleep(0.5)  # wait for a short period before checking again

        # Select the desired value
        options = iframe.locator("[id^='ac_option_']")
        for i in range(options.count()):
            opt = options.nth(i)

            # Extract the value from the option element
            if opt.locator(".ac_cell").count() > 0:
                # ... element is multi part (use only the main info)
                opt_value = opt.locator(".ac_cell").first.text_content()
            else:
                # ... element is single part (use the whole text)
                opt_value = opt.text_content()

            if opt_value.lower() == value.lower():
                opt.click()
                break
        else:
            raise ValueError(f"No match for value {value} found in autocomplete menu")

    # All other normal text fields
    else:
        input_field.fill(value)


import logging
import playwright.sync_api

from urllib import parse

def check_url_suffix_match(page: playwright.sync_api.Page, expected_url: str, task) -> bool:
    """
    Check if the current page URL matches the expected URL
    """
    expected_url = parse.unquote(expected_url)
    expected_url_suffix = parse.urlparse(expected_url).path

    page_url = page.evaluate("window.location.href")
    page_url = parse.unquote(page_url)
    page_suffix = parse.urlparse(page_url).path
    if expected_url_suffix not in page_suffix:
        logging.debug(f"Not in the expected URL for {task.__class__.__name__}, but in {page.url}")
        return False
    return True

def prettyprint_enum(items, conjunction="and"):
    """
    Pretty print a list of items with a conjunction

    """
    if not items:
        return ""
    elif len(items) == 1:
        return items[0]
    else:
        return ", ".join(items[:-1]) + f", {conjunction} " + items[-1]


from urllib import parse

def check_url_suffix_match(page: playwright.sync_api.Page, expected_url: str, task) -> bool:
    """
    Check if the current page URL matches the expected URL
    """
    expected_url = parse.unquote(expected_url)
    expected_url_suffix = parse.urlparse(expected_url).path

    page_url = page.evaluate("window.location.href")
    page_url = parse.unquote(page_url)
    page_suffix = parse.urlparse(page_url).path
    if expected_url_suffix not in page_suffix:
        logging.debug(f"Not in the expected URL for {task.__class__.__name__}, but in {page.url}")
        return False
    return True

def prettyprint_enum(items, conjunction="and"):
    """
    Pretty print a list of items with a conjunction

    """
    if not items:
        return ""
    elif len(items) == 1:
        return items[0]
    else:
        return ", ".join(items[:-1]) + f", {conjunction} " + items[-1]


"""
Various utility functions for string manipulation.

"""

def generate_trigrams(word):
    return [word[i : i + 3] for i in range(len(word) - 2)]

def share_tri_gram(str1, str2):
    tri_grams1 = set(generate_trigrams(str1))
    tri_grams2 = set(generate_trigrams(str2))

    return bool(tri_grams1.intersection(tri_grams2))


import re

from datetime import datetime, timedelta
from faker import Faker
from typing import List, Tuple

fake = Faker()

from playwright.sync_api._generated import Page

from browsergym.workarena.tasks.form import (
    EditChangeRequestScheduleTask,
)

from .base import HumanEvalTask
from .filter_and_do import FilterAndDoTask

from ..base import AbstractServiceNowTask

from ...api.change_request import create_change_request
from ...api.utils import table_api_call, db_delete_from_table
from ...config import (
    # Expected columns for the different lists
    EXPECTED_CHANGE_REQUEST_COLUMNS_PATH,
)
from ...instance import SNowInstance

class ManageChangeRequestScheduleTask(FilterAndDoTask):
    """Task to schedule change requests.
    Args:

    goal_type: str
        The type of goal to set. Choices are "base", "priority", "tight", "tight priority". Used for validation
    wide_schedule: bool
        Whether or not the change requests should be scheduled in a 'wide' schedule. If set to True, the change requests
        will have a period of 2 longer than the optimal schedule to be fitted in. Otherwise, they will have
        a period of 2 days longer than the optimal schedule.
    uniform_risk: bool
        whether to use uniform risk for the change requests. The risk is between 2 (high) and 4 (low) and sets the
        duration of the change request (high) risk=2 -> 3 days, (medium) risk=3 -> 2 days, (low) risk=4 -> 1 day
    num_change_requests: int
        The number of change requests to create a schedule for
    pre_existing_schedule: bool
        Whether to create a pre-existing schedule for the change requests. If set to True, the change requests created
        will all overlap and have durations of one day.
    """

    # mapping between risk and duration
    risk_to_duration = {2: 3, 3: 2, 4: 1}

    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: list[AbstractServiceNowTask] = None,
        goal_type: str = "base",
        wide_schedule: bool = False,
        uniform_risk: bool = True,
        num_change_requests: int = 2,
        pre_existing_schedule: bool = False,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            navigation_config={
                "module": "All",
                "application": "Change",
            },
            level=level,
            protocol_name="Scheduling Your Change Requests",
        )
        self.goal_type = goal_type
        self.wide_schedule = wide_schedule
        self.uniform_risk = uniform_risk
        self.num_change_requests = num_change_requests
        self.pre_existing_schedule = pre_existing_schedule
        self.change_request_sys_ids = []
        self.change_request_numbers = []
        self.change_request_impacts = [2] * num_change_requests  # Medium priorities by default

        # start and end dates of the schedule
        self.schedule_start_date = fake.date_time_this_decade(
            after_now=True, before_now=False, tzinfo=None
        ).replace(microsecond=0)
        self.schedule_end_date = None
        self.schedule_bounds_goal = None  # Part of the goal to append at the end of the goal to indicate the start and end of the schedule. Used in L2 tasks

        if self.uniform_risk:
            self.risks = [4] * num_change_requests
        else:
            self.risks = list(self.random.randint(2, 4, num_change_requests))

        self.change_request_hashtag = "#SERIES-" + self.unique_id[:10]
        if not self.pre_existing_schedule:
            schedule_type = "tight schedule" if "tight" in self.goal_type else "schedule"
            self.short_description = f"Scheduling Your Change Requests"
            self.task_description = f'Referring to company protocol "{self.protocol_name}" (located in the "Company Protocols" knowledge base) create a {schedule_type} for your change requests for those with hashtag {self.change_request_hashtag}.'
        else:
            self.short_description = f"Re-scheduling Your Change Requests"
            self.task_description = f'The schedule for your change requests with hashtag {self.change_request_hashtag} is currently broken. Please refer to company protocol "{self.protocol_name}" (located in the company protocols knowledge base) to fix it.'
            if "tight" in self.goal_type:
                self.task_description += " The change requests should be scheduled according to the tight schedule setting."
        self.tasks = []

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

        if self.level == 2:
            goal += self.schedule_bounds_goal

        return goal, info

    def _setup_list(self) -> None:
        self.filter_config = {
            "list_url": "/now/nav/ui/classic/params/target/change_request_list.do",
            "expected_fields_path": EXPECTED_CHANGE_REQUEST_COLUMNS_PATH,
            "filter_columns": [
                "short_description",
            ],
            "filter_kind": "AND",
            "filter_operators": ["contains"],
            "filter_values": [
                f"{self.change_request_hashtag}",
            ],
        }
        self.change_request_impacts.sort()  # Sort the impacts to make sure the the top impact requests are scheduled first

        start_date = self.schedule_start_date

        for risk, impact in zip(self.risks, self.change_request_impacts):
            if self.pre_existing_schedule:
                change_request_start_date = start_date + timedelta(hours=self.random.randint(1, 4))
                change_request_end_date = change_request_start_date + timedelta(days=1)
            else:
                change_request_start_date = ""
                change_request_end_date = ""
            change_request_sys_id, change_request_number = create_change_request(
                instance=self.instance,
                user_sys_id=self._base_user_sysid,
                risk=risk,
                start_date=str(change_request_start_date),
                end_date=str(change_request_end_date),
                impact=impact,
                hashtag=self.change_request_hashtag,
                random=self.random,
            )
            self.change_request_sys_ids.append(change_request_sys_id)
            self.change_request_numbers.append(change_request_number)

        for i, risk in enumerate(self.risks):
            skip_description = i > 0
            duration = self.risk_to_duration[risk]
            end_date = start_date + timedelta(days=duration)
            self.tasks.append(
                EditChangeRequestScheduleTask(
                    instance=self.instance,
                    is_validated=False,
                    used_in_level_2=True,
                    record_sys_id=self.change_request_sys_ids[i],
                    record_number=self.change_request_numbers[i],
                    # Here the values will only be used by the cheat; the goal will be over-ridden to explain the task
                    # at a high level only; see the get_pretty_printed_description method
                    new_values={"start_date": str(start_date), "end_date": str(end_date)},
                    level=self.level,
                    goal_type=self.goal_type,
                    skip_description=skip_description,
                )
            )
            start_date = end_date + timedelta(minutes=1)

        if self.wide_schedule:
            self.schedule_end_date = end_date + timedelta(weeks=2)
        else:
            self.schedule_end_date = end_date + timedelta(days=2)

        self.schedule_bounds_goal = f" All the change requests should be scheduled between {self.schedule_start_date} and {self.schedule_end_date}, inclusively. "
        # Add the schedule bounds to the task description
        self.task_description += self.schedule_bounds_goal

    def validate(self, page: Page, chat_messages: list[str]) -> Tuple[float, bool, str, dict]:
        change_requests = table_api_call(
            instance=self.instance,
            table="change_request",
            params={
                "sysparm_query": f"short_descriptionLIKE{self.change_request_hashtag}",
                "sysparm_fields": "impact,start_date,end_date,risk",
            },
        )["result"]
        change_requests = sorted(change_requests, key=lambda x: x["start_date"])

        # max difference is 1 day if not tight, 1 hour if tight
        max_difference = 1 if self.goal_type == "tight" else 24

        for i, change_request in enumerate(change_requests):
            # Check that the change request has start/end dates
            if (
                not change_request["start_date"]
                or not change_request["end_date"]
                or (i > 0 and not change_requests[i - 1]["end_date"])
            ):
                return (
                    0,
                    False,
                    "",
                    {"message": "Change request start date or end date is missing."},
                )
            # Confirm that the change request has appropriate duration (within 20% of expected duration)
            current_start_date = datetime.strptime(
                change_request["start_date"], "%Y-%m-%d %H:%M:%S"
            )
            current_end_date = datetime.strptime(change_request["end_date"], "%Y-%m-%d %H:%M:%S")

            # Check that the bounds of the schedule are respected
            if (
                current_start_date < self.schedule_start_date
                or current_end_date > self.schedule_end_date
            ):
                return (
                    0,
                    False,
                    "",
                    {
                        "message": "Change request start date or end date is outside of the target schedule."
                    },
                )

            difference = current_end_date - current_start_date
            # Expected duration is 3 days for high risk, 2 days for medium risk, 1 day for low risk
            duration = self.risk_to_duration[int(change_request["risk"])]
            expected_duration = timedelta(days=duration)

            if difference < expected_duration * 0.95 or difference > expected_duration * 1.05:
                return (
                    0,
                    False,
                    "",
                    {
                        "message": "Change request duration is not within 5% of the expected duration."
                    },
                )

            if i == 0:
                continue
            # Confirm change requests are not overlapping and respect maximum spacing (1 day if not tight, 1h if tight)
            previous_end_date = datetime.strptime(
                change_requests[i - 1]["end_date"], "%Y-%m-%d %H:%M:%S"
            )
            difference = current_start_date - previous_end_date
            if difference > timedelta(hours=max_difference) or difference < timedelta(0):
                return (
                    0,
                    False,
                    "",
                    {
                        "message": "Change requests are overlapping or not respecting the maximum spacing."
                    },
                )
            # Confirm change requests are ordered by impact - lower number being more impactful
            if change_request["impact"] > change_requests[i - 1]["impact"]:
                return (
                    0,
                    False,
                    "",
                    {"message": "Change requests are not ordered by priority."},
                )

        # Validate final_l3 tasks
        reward, done, message, info = super().validate(page, chat_messages)
        return reward, done, message, info

    def teardown(self) -> None:
        for change_request_sys_id in self.change_request_sys_ids:
            record_exists = table_api_call(
                instance=self.instance,
                table="change_request",
                params={"sysparm_query": f"sys_id={change_request_sys_id}"},
            )["result"]
            if record_exists:
                db_delete_from_table(
                    instance=self.instance,
                    table="change_request",
                    sys_id=change_request_sys_id,
                )
        super().teardown()

class TwoChangesBasicUniformRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            num_change_requests=num_change_requests,
            level=level,
        )

class TwoChangesWideBasicUniformRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            wide_schedule=True,
            num_change_requests=num_change_requests,
            level=level,
        )

class TwoChangesFixBasicUniformRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            num_change_requests=num_change_requests,
            pre_existing_schedule=True,
            level=level,
        )

class TwoChangesFixWideBasicUniformRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            wide_schedule=True,
            num_change_requests=num_change_requests,
            pre_existing_schedule=True,
            level=level,
        )

class TwoChangesBasicVariedRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            uniform_risk=False,
            num_change_requests=num_change_requests,
            level=level,
        )

class TwoChangesWideBasicVariedRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            wide_schedule=True,
            uniform_risk=False,
            num_change_requests=num_change_requests,
            level=level,
        )

class TwoChangesFixBasicVariedRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            uniform_risk=False,
            num_change_requests=num_change_requests,
            pre_existing_schedule=True,
            level=level,
        )

class TwoChangesFixWideBasicVariedRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            wide_schedule=True,
            uniform_risk=False,
            num_change_requests=num_change_requests,
            pre_existing_schedule=True,
            level=level,
        )

class TwoChangesPriorityUniformRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="priority",
            num_change_requests=num_change_requests,
            level=level,
        )

class TwoChangesWidePriorityUniformRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="priority",
            wide_schedule=True,
            num_change_requests=num_change_requests,
            level=level,
        )

class TwoChangesFixPriorityUniformRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="priority",
            num_change_requests=num_change_requests,
            pre_existing_schedule=True,
            level=level,
        )

class TwoChangesFixWidePriorityUniformRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="priority",
            wide_schedule=True,
            num_change_requests=num_change_requests,
# ... truncated ...


ManageChangeRequestScheduleTask

from playwright.sync_api._generated import Page

from browsergym.workarena.tasks.form import (
    EditChangeRequestScheduleTask,
)

from .base import HumanEvalTask
from .filter_and_do import FilterAndDoTask

from ..base import AbstractServiceNowTask

from ...api.change_request import create_change_request
from ...api.utils import table_api_call, db_delete_from_table
from ...config import (
    # Expected columns for the different lists
    EXPECTED_CHANGE_REQUEST_COLUMNS_PATH,
)
from ...instance import SNowInstance

class ManageChangeRequestScheduleTask(FilterAndDoTask):
    """Task to schedule change requests.
    Args:

    goal_type: str
        The type of goal to set. Choices are "base", "priority", "tight", "tight priority". Used for validation
    wide_schedule: bool
        Whether or not the change requests should be scheduled in a 'wide' schedule. If set to True, the change requests
        will have a period of 2 longer than the optimal schedule to be fitted in. Otherwise, they will have
        a period of 2 days longer than the optimal schedule.
    uniform_risk: bool
        whether to use uniform risk for the change requests. The risk is between 2 (high) and 4 (low) and sets the
        duration of the change request (high) risk=2 -> 3 days, (medium) risk=3 -> 2 days, (low) risk=4 -> 1 day
    num_change_requests: int
        The number of change requests to create a schedule for
    pre_existing_schedule: bool
        Whether to create a pre-existing schedule for the change requests. If set to True, the change requests created
        will all overlap and have durations of one day.
    """

    # mapping between risk and duration
    risk_to_duration = {2: 3, 3: 2, 4: 1}

    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: list[AbstractServiceNowTask] = None,
        goal_type: str = "base",
        wide_schedule: bool = False,
        uniform_risk: bool = True,
        num_change_requests: int = 2,
        pre_existing_schedule: bool = False,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            navigation_config={
                "module": "All",
                "application": "Change",
            },
            level=level,
            protocol_name="Scheduling Your Change Requests",
        )
        self.goal_type = goal_type
        self.wide_schedule = wide_schedule
        self.uniform_risk = uniform_risk
        self.num_change_requests = num_change_requests
        self.pre_existing_schedule = pre_existing_schedule
        self.change_request_sys_ids = []
        self.change_request_numbers = []
        self.change_request_impacts = [2] * num_change_requests  # Medium priorities by default

        # start and end dates of the schedule
        self.schedule_start_date = fake.date_time_this_decade(
            after_now=True, before_now=False, tzinfo=None
        ).replace(microsecond=0)
        self.schedule_end_date = None
        self.schedule_bounds_goal = None  # Part of the goal to append at the end of the goal to indicate the start and end of the schedule. Used in L2 tasks

        if self.uniform_risk:
            self.risks = [4] * num_change_requests
        else:
            self.risks = list(self.random.randint(2, 4, num_change_requests))

        self.change_request_hashtag = "#SERIES-" + self.unique_id[:10]
        if not self.pre_existing_schedule:
            schedule_type = "tight schedule" if "tight" in self.goal_type else "schedule"
            self.short_description = f"Scheduling Your Change Requests"
            self.task_description = f'Referring to company protocol "{self.protocol_name}" (located in the "Company Protocols" knowledge base) create a {schedule_type} for your change requests for those with hashtag {self.change_request_hashtag}.'
        else:
            self.short_description = f"Re-scheduling Your Change Requests"
            self.task_description = f'The schedule for your change requests with hashtag {self.change_request_hashtag} is currently broken. Please refer to company protocol "{self.protocol_name}" (located in the company protocols knowledge base) to fix it.'
            if "tight" in self.goal_type:
                self.task_description += " The change requests should be scheduled according to the tight schedule setting."
        self.tasks = []

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

        if self.level == 2:
            goal += self.schedule_bounds_goal

        return goal, info

    def _setup_list(self) -> None:
        self.filter_config = {
            "list_url": "/now/nav/ui/classic/params/target/change_request_list.do",
            "expected_fields_path": EXPECTED_CHANGE_REQUEST_COLUMNS_PATH,
            "filter_columns": [
                "short_description",
            ],
            "filter_kind": "AND",
            "filter_operators": ["contains"],
            "filter_values": [
                f"{self.change_request_hashtag}",
            ],
        }
        self.change_request_impacts.sort()  # Sort the impacts to make sure the the top impact requests are scheduled first

        start_date = self.schedule_start_date

        for risk, impact in zip(self.risks, self.change_request_impacts):
            if self.pre_existing_schedule:
                change_request_start_date = start_date + timedelta(hours=self.random.randint(1, 4))
                change_request_end_date = change_request_start_date + timedelta(days=1)
            else:
                change_request_start_date = ""
                change_request_end_date = ""
            change_request_sys_id, change_request_number = create_change_request(
                instance=self.instance,
                user_sys_id=self._base_user_sysid,
                risk=risk,
                start_date=str(change_request_start_date),
                end_date=str(change_request_end_date),
                impact=impact,
                hashtag=self.change_request_hashtag,
                random=self.random,
            )
            self.change_request_sys_ids.append(change_request_sys_id)
            self.change_request_numbers.append(change_request_number)

        for i, risk in enumerate(self.risks):
            skip_description = i > 0
            duration = self.risk_to_duration[risk]
            end_date = start_date + timedelta(days=duration)
            self.tasks.append(
                EditChangeRequestScheduleTask(
                    instance=self.instance,
                    is_validated=False,
                    used_in_level_2=True,
                    record_sys_id=self.change_request_sys_ids[i],
                    record_number=self.change_request_numbers[i],
                    # Here the values will only be used by the cheat; the goal will be over-ridden to explain the task
                    # at a high level only; see the get_pretty_printed_description method
                    new_values={"start_date": str(start_date), "end_date": str(end_date)},
                    level=self.level,
                    goal_type=self.goal_type,
                    skip_description=skip_description,
                )
            )
            start_date = end_date + timedelta(minutes=1)

        if self.wide_schedule:
            self.schedule_end_date = end_date + timedelta(weeks=2)
        else:
            self.schedule_end_date = end_date + timedelta(days=2)

        self.schedule_bounds_goal = f" All the change requests should be scheduled between {self.schedule_start_date} and {self.schedule_end_date}, inclusively. "
        # Add the schedule bounds to the task description
        self.task_description += self.schedule_bounds_goal

    def validate(self, page: Page, chat_messages: list[str]) -> Tuple[float, bool, str, dict]:
        change_requests = table_api_call(
            instance=self.instance,
            table="change_request",
            params={
                "sysparm_query": f"short_descriptionLIKE{self.change_request_hashtag}",
                "sysparm_fields": "impact,start_date,end_date,risk",
            },
        )["result"]
        change_requests = sorted(change_requests, key=lambda x: x["start_date"])

        # max difference is 1 day if not tight, 1 hour if tight
        max_difference = 1 if self.goal_type == "tight" else 24

        for i, change_request in enumerate(change_requests):
            # Check that the change request has start/end dates
            if (
                not change_request["start_date"]
                or not change_request["end_date"]
                or (i > 0 and not change_requests[i - 1]["end_date"])
            ):
                return (
                    0,
                    False,
                    "",
                    {"message": "Change request start date or end date is missing."},
                )
            # Confirm that the change request has appropriate duration (within 20% of expected duration)
            current_start_date = datetime.strptime(
                change_request["start_date"], "%Y-%m-%d %H:%M:%S"
            )
            current_end_date = datetime.strptime(change_request["end_date"], "%Y-%m-%d %H:%M:%S")

            # Check that the bounds of the schedule are respected
            if (
                current_start_date < self.schedule_start_date
                or current_end_date > self.schedule_end_date
            ):
                return (
                    0,
                    False,
                    "",
                    {
                        "message": "Change request start date or end date is outside of the target schedule."
                    },
                )

            difference = current_end_date - current_start_date
            # Expected duration is 3 days for high risk, 2 days for medium risk, 1 day for low risk
            duration = self.risk_to_duration[int(change_request["risk"])]
            expected_duration = timedelta(days=duration)

            if difference < expected_duration * 0.95 or difference > expected_duration * 1.05:
                return (
                    0,
                    False,
                    "",
                    {
                        "message": "Change request duration is not within 5% of the expected duration."
                    },
                )

            if i == 0:
                continue
            # Confirm change requests are not overlapping and respect maximum spacing (1 day if not tight, 1h if tight)
            previous_end_date = datetime.strptime(
                change_requests[i - 1]["end_date"], "%Y-%m-%d %H:%M:%S"
            )
            difference = current_start_date - previous_end_date
            if difference > timedelta(hours=max_difference) or difference < timedelta(0):
                return (
                    0,
                    False,
                    "",
                    {
                        "message": "Change requests are overlapping or not respecting the maximum spacing."
                    },
                )
            # Confirm change requests are ordered by impact - lower number being more impactful
            if change_request["impact"] > change_requests[i - 1]["impact"]:
                return (
                    0,
                    False,
                    "",
                    {"message": "Change requests are not ordered by priority."},
                )

        # Validate final_l3 tasks
        reward, done, message, info = super().validate(page, chat_messages)
        return reward, done, message, info

    def teardown(self) -> None:
        for change_request_sys_id in self.change_request_sys_ids:
            record_exists = table_api_call(
                instance=self.instance,
                table="change_request",
                params={"sysparm_query": f"sys_id={change_request_sys_id}"},
            )["result"]
            if record_exists:
                db_delete_from_table(
                    instance=self.instance,
                    table="change_request",
                    sys_id=change_request_sys_id,
                )
        super().teardown()

class TwoChangesBasicUniformRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            num_change_requests=num_change_requests,
            level=level,


        # Validate final_l3 tasks
        reward, done, message, info = super().validate(page, chat_messages)
        return reward, done, message, info

    def teardown(self) -> None:
        for change_request_sys_id in self.change_request_sys_ids:
            record_exists = table_api_call(
                instance=self.instance,
                table="change_request",
                params={"sysparm_query": f"sys_id={change_request_sys_id}"},
            )["result"]
            if record_exists:
                db_delete_from_table(
                    instance=self.instance,
                    table="change_request",
                    sys_id=change_request_sys_id,
                )
        super().teardown()

class TwoChangesBasicUniformRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            num_change_requests=num_change_requests,
            level=level,
        )

class TwoChangesWideBasicUniformRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            wide_schedule=True,
            num_change_requests=num_change_requests,


    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            num_change_requests=num_change_requests,
            level=level,
        )

class TwoChangesWideBasicUniformRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            wide_schedule=True,
            num_change_requests=num_change_requests,
            level=level,
        )

class TwoChangesFixBasicUniformRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            num_change_requests=num_change_requests,
            pre_existing_schedule=True,


):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            wide_schedule=True,
            num_change_requests=num_change_requests,
            level=level,
        )

class TwoChangesFixBasicUniformRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            num_change_requests=num_change_requests,
            pre_existing_schedule=True,
            level=level,
        )

class TwoChangesFixWideBasicUniformRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            wide_schedule=True,
            num_change_requests=num_change_requests,


):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            num_change_requests=num_change_requests,
            pre_existing_schedule=True,
            level=level,
        )

class TwoChangesFixWideBasicUniformRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            wide_schedule=True,
            num_change_requests=num_change_requests,
            pre_existing_schedule=True,
            level=level,
        )

class TwoChangesBasicVariedRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            uniform_risk=False,
            num_change_requests=num_change_requests,


    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            wide_schedule=True,
            num_change_requests=num_change_requests,
            pre_existing_schedule=True,
            level=level,
        )

class TwoChangesBasicVariedRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            uniform_risk=False,
            num_change_requests=num_change_requests,
            level=level,
        )

class TwoChangesWideBasicVariedRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            wide_schedule=True,
            uniform_risk=False,


):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            uniform_risk=False,
            num_change_requests=num_change_requests,
            level=level,
        )

class TwoChangesWideBasicVariedRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            wide_schedule=True,
            uniform_risk=False,
            num_change_requests=num_change_requests,
            level=level,
        )

class TwoChangesFixBasicVariedRiskChangeRequestSchedulingTask(
    ManageChangeRequestScheduleTask, HumanEvalTask
):
    def __init__(
        self,
        seed: int,
        instance: SNowInstance = None,
        fixed_config: List[AbstractServiceNowTask] = None,
        num_change_requests: int = 2,
        level: int = 2,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            goal_type="base",
            uniform_risk=False,
            num_change_requests=num_change_requests,