                        "application": "Problem",
                        "module": "All",
                        "url": "/now/nav/ui/classic/params/target/problem_list.do",
                    },
                    is_validated=False,
                    used_in_level_2=True,
                ),
                # Create a problem
                CreateProblemTask(
                    instance=self.instance,
                    fixed_config=problem_config,
                    is_validated=False,
                    used_in_level_2=True,
                    check_record_created=False,
                ),
            ]
            create_problem_subtasks += create_problem_subtask

        self.compositional_task = create_problem_subtasks

    def setup_goal(self, page: Page) -> tuple[str, dict]:
        self.create_report(
            user_roles=[
                "itil",
                "problem_admin",
                "problem_manager",
                "problem_coordinator",
                "problem_task_analyst",
            ]
        )
        self.set_compositional_task()
        config = self.fixed_config if self.fixed_config else self._get_config()

        if self.level == 3:
            self.task_description = (
                self.task_description
                + f"\t - Please retrieve the '{self.description_mapping[self.question]}' value of the number of incidents assigned across agents. Retrieve agents that have less than or equal number of incidents assigned to them compared to this value.\n"
                + f"\t   For example, if you were asked to find the 'mean' or 'average' for a case where there are 4 agents assigned 1,2,3,2 incidents respectively, the mean would be 2. The list of agents required for solving the following task would be all the agents that have less than or equal to 2 assigned incidents.\n\n"
                + f"\t - Task: For each agent that fits the above criteria, create a 'problem' with the following information (only fill these fields) and assign it to them using the 'Assigned to' field: \n"
                + f"\t\t Impact: '1 - High', Urgency: '1 - High', Problem statement: 'Compulsory training for employee in probation'.\n"
                + f"Note that you will create as many problems as there are agents matching the above criteria.\n"
                + f"\t For example, consider the above case and say you have 3 agents with less than or equal to 2 incidents assigned to them in the chart. You will be creating '3' problems here, one assigned to each agent. \n\n"
            )

        goal, info = super().setup_goal(page=page, config=config)

        if self.level == 2:
            goal = (
                self.short_description
                + f"\n1. Navigate to the Reports > View/Run page. \n"
                + f"\n2. Given the title of the report, search for it on this page. The report shows the number of 'incidents' assigned to an 'agent'.\n"
                + f"\n3. Find the agents with a number of incidents assigned less than or equal to the {self.description_mapping[self.question]} of the number of assigned incidents across agents. For example, if you were asked to find the 'mean' or 'average' for a case where there are 4 agents assigned 1,2,3,2 incidents respectively, the mean would be 2. The list of agents required for solving the following task would be all the agents that have less than or equal to 2 assigned incidents.\n"
                + f"\n4. Navigate to All > Problems. \n"
                + f"\n5. You have to create new 'problems' from this page for all the agents based on the above criteria. Only fill the following fields when creating a new problem:- Impact: '1 - High', Urgency: '1 - High', Problem statement: 'Compulsory training for employee in probation' and 'assign' them to each agent.\n\n"
                + f"Note that you will create as many problems as there are agents matching the above criteria."
                + "\nFor example, consider the above case and say you have 3 agents with less than or equal to 2 incidents assigned to them in the chart. You will be creating '3' problems here, one assigned to each agent. \n"
            )

        return goal, info

    def validate(self, page: Page, chat_messages: list[str]) -> Tuple[float, bool, str, dict]:
        fixed_template_record = {
            "short_description": "Compulsory training for employee in probation",
            "impact": "1",
            "urgency": "1",
        }
        for agent_sysid in self.agent_value_sysids:
            created_problem_response = table_api_call(
                instance=self.instance,
                table="problem",
                params={
                    "sysparm_query": f"assigned_to={agent_sysid}^short_description={self.short_description}",
                },
                method="GET",
            )["result"]
            if len(created_problem_response) == 0:
                return (
                    0,
                    False,


validate

                + f"Note that you will create as many problems as there are agents matching the above criteria.\n"
                + f"\t For example, consider the above case and say you have 3 agents with less than or equal to 2 incidents assigned to them in the chart. You will be creating '3' problems here, one assigned to each agent. \n\n"
            )

        goal, info = super().setup_goal(page=page, config=config)

        if self.level == 2:
            goal = (
                self.short_description
                + f"\n1. Navigate to the Reports > View/Run page. \n"
                + f"\n2. Given the title of the report, search for it on this page. The report shows the number of 'incidents' assigned to an 'agent'.\n"
                + f"\n3. Find the agents with a number of incidents assigned less than or equal to the {self.description_mapping[self.question]} of the number of assigned incidents across agents. For example, if you were asked to find the 'mean' or 'average' for a case where there are 4 agents assigned 1,2,3,2 incidents respectively, the mean would be 2. The list of agents required for solving the following task would be all the agents that have less than or equal to 2 assigned incidents.\n"
                + f"\n4. Navigate to All > Problems. \n"
                + f"\n5. You have to create new 'problems' from this page for all the agents based on the above criteria. Only fill the following fields when creating a new problem:- Impact: '1 - High', Urgency: '1 - High', Problem statement: 'Compulsory training for employee in probation' and 'assign' them to each agent.\n\n"
                + f"Note that you will create as many problems as there are agents matching the above criteria."
                + "\nFor example, consider the above case and say you have 3 agents with less than or equal to 2 incidents assigned to them in the chart. You will be creating '3' problems here, one assigned to each agent. \n"
            )

        return goal, info

    def validate(self, page: Page, chat_messages: list[str]) -> Tuple[float, bool, str, dict]:
        fixed_template_record = {
            "short_description": "Compulsory training for employee in probation",
            "impact": "1",
            "urgency": "1",
        }
        for agent_sysid in self.agent_value_sysids:
            created_problem_response = table_api_call(
                instance=self.instance,
                table="problem",
                params={
                    "sysparm_query": f"assigned_to={agent_sysid}^short_description={self.short_description}",
                },
                method="GET",
            )["result"]
            if len(created_problem_response) == 0:
                return (
                    0,
                    False,
                    "",
                    {
                        "message": f"No problem created for agent {self.agents[agent_sysid]['user_name']}."
                    },
                )
            elif len(created_problem_response) > 1:
                return (
                    0,
                    False,
                    "",
                    {
                        "message": f"Multiple problems created for agent {self.agents[agent_sysid]['user_name']}."
                    },
                )
            created_problem_response = created_problem_response[0]
            for key, value in fixed_template_record.items():
                if str(created_problem_response[key]).lower() != str(value).lower():
                    return (
                        0,
                        False,
                        "",
                        {
                            "message": f"Problem for agent {self.agents[agent_sysid]['user_name']} assigned incorrect value to field {key}."
                        },
                    )
        reward, done, message, info = super().validate(page, chat_messages)
        return reward, done, message, info

    def teardown(self) -> None:
        for agent_sysid in self.agent_sysids:
            created_problem_response = table_api_call(
                instance=self.instance,
                table="problem",
                params={
                    "sysparm_query": f"assigned_to={agent_sysid}",
                },
                method="GET",
            )["result"]
            for problem in created_problem_response:
                db_delete_from_table(
                    instance=self.instance,
                    table="problem",
                    sys_id=problem["sys_id"],
                )
        return super().teardown()


teardown

                    False,
                    "",
                    {
                        "message": f"Multiple problems created for agent {self.agents[agent_sysid]['user_name']}."
                    },
                )
            created_problem_response = created_problem_response[0]
            for key, value in fixed_template_record.items():
                if str(created_problem_response[key]).lower() != str(value).lower():
                    return (
                        0,
                        False,
                        "",
                        {
                            "message": f"Problem for agent {self.agents[agent_sysid]['user_name']} assigned incorrect value to field {key}."
                        },
                    )
        reward, done, message, info = super().validate(page, chat_messages)
        return reward, done, message, info

    def teardown(self) -> None:
        for agent_sysid in self.agent_sysids:
            created_problem_response = table_api_call(
                instance=self.instance,
                table="problem",
                params={
                    "sysparm_query": f"assigned_to={agent_sysid}",
                },
                method="GET",
            )["result"]
            for problem in created_problem_response:
                db_delete_from_table(
                    instance=self.instance,
                    table="problem",
                    sys_id=problem["sys_id"],
                )
        return super().teardown()

class DashboardRetrieveIncidentAndMinCreateProblemTask(
    DashboardRetrieveIncidentAndCreateProblemTask, DashDoFinalTask
):
    def __init__(
        self,
        instance: SNowInstance = None,
        seed: int = None,
        fixed_config: list[AbstractServiceNowTask] = None,
        level: int = 2,
    ) -> None:
        """
        Retrieve the worst performing employee and create a problem to assign them a probation course.
        Attributes:
        -----------
        task_description: str
            The start of the task description to be completed.
        short_description: str
            A short description of the task to be completed. "Create a problem for the worst performing employee from the list"


import numpy as np

class KnapsackInstanceGenarator:
    """
    Generates a knapsack instance with the given number of items and maximum capacity, and solves it.
    The instance is guaranteed to have a unique optimal solution in "random" or "single_item" mode .

    Args:
    - random: Random number generator
    - num_items: Number of items
    - max_capacity: Maximum capacity of the knapsack
    - mode: Mode of generation. Choice of "random", "trivial", "single_item", "single_item_uniform", "n_items"
        - random: Randomly generate the instance and return it; guaranteed to have a unique optimal solution
        - trivial: Generate a trivial instance with all items fitting in the knapsack; return the instance
        - single_item: Generate an instance where the optimal solution has only one item
        - n_items: Generate an instance with all items having uniform weight and value; n items fitting in the knapsack
        - single_item_uniform: Generate an instance with all items having uniform weight and value; optimal solution has only one item and it can be any
    - num_items_in_solution: Number of items in the optimal solution. Required for "n_items" mode.
    - default_return: Default return value for investments having uniform weight and value. Required for "n_items" and "single_item_uniform" modes.
    """

    def __init__(
        self,
        random: np.random,
        num_items: int,
        max_capacity: int,
        mode: str = "random",
        num_items_in_solution: int = None,
        default_return: int = 100000,
    ):
        self.random = random
        self.num_items = num_items
        self.max_capacity = max_capacity
        self.mode = mode
        self.num_items_in_solution = num_items_in_solution
        self.default_return = default_return

    def get_instance(self):
        if self.mode in ["random", "trivial"]:
            return self.generate_and_solve_knapsack_instance()
        elif self.mode == "single_item":
            return self.generate_single_item_knapsack_instance()
        elif self.mode in ["single_item_uniform", "n_items"]:
            return self.generate_uniform_knapsack_instance()
        else:
            raise ValueError(f"Invalid mode {self.mode} for knapsack instance generation")

    def generate_and_solve_knapsack_instance(self):
        """
        Generates a knapsack instance with the given number of items and maximum capacity, and solves it.
        Used to generate instances for the "random" and "trivial" mode.
        Returns:
        - investments: List of tuples (cost, investment_return) for each investment
        - max_return: Maximum return achievable with optimal solution
        - selected_indices: Indices of the investments selected in the optimal solution
        """

        assert self.mode in [
            "random",
            "trivial",
        ], f"Mode {self.mode} is invalid for instance generation with generate_and_solve_knapsack_instance"

        multiple_solutions = True
        while multiple_solutions:
            # Generate knapsack instance...
            investments = []
            min_cost = self.max_capacity // (self.num_items * 2)
            max_cost = (
                self.max_capacity // 2
                if self.mode == "random"
                else self.max_capacity // self.num_items
            )
            for _ in range(self.num_items):
                cost = self.random.randint(min_cost, max_cost)
                # Ensure that investments yield positive returns
                investment_return = self.random.randint(
                    self.max_capacity // 2, self.max_capacity // 2 + 40000
                )
                investments.append((cost, investment_return))

            total_cost = sum([investments[i][0] for i in range(self.num_items)])
            # Skip trivial instances where all items fit in the knapsack
            if self.mode == "random" and total_cost <= self.max_capacity:
                continue

            if self.mode == "random":
                # ...Solve it...
                max_return, num_optimal_solutions, selected_indices = self.solve_knapsack(
                    investments, self.max_capacity
                )
                # ...and check if there are multiple solutions
                multiple_solutions = num_optimal_solutions > 1
            else:
                selected_indices = list(range(self.num_items))
                max_return = sum([investments[i][1] for i in selected_indices])
                multiple_solutions = False

        return investments, max_return, selected_indices

    def generate_single_item_knapsack_instance(self):
        """Generate knapsack instance where the optimal solution contains only one item
        Returns:
        - investments: List of tuples (cost, investment_return) for each investment
        - max_investment_return: Investment return of the selected investment in the optimal solution
        - selected_indices: Index of the selected investment in the optimal solution
        """
        assert (
            self.mode == "single_item"
        ), f"Mode {self.mode} is invalid for instance generation with generate_single_item_knapsack_instance"

        # Ensure that the optimal solution contains only one item
        min_cost = self.max_capacity // 2 + 1
        max_cost = self.max_capacity - 1

        max_investment_return = 0
        max_investment_index = 0

        # Generate knapsack instance...
        investments = []
        for i in range(self.num_items):
            cost = self.random.randint(min_cost, max_cost)
            investment_return = self.random.randint(max_cost, 2 * max_cost)

            # Ensure that the optimal solution contains only one item
            while investment_return == max_investment_return:
                investment_return = self.random.randint(max_cost, 2 * max_cost)

            if investment_return > max_investment_return:
                max_investment_return = investment_return
                max_investment_index = i

            investments.append((cost, investment_return))

        return investments, max_investment_return, [max_investment_index]

    def generate_uniform_knapsack_instance(self):
        """Generate knapsack instance where all items have the same cost and return
        Returns:
        - investments: List of tuples (cost, investment_return) for each investment
        - max_return: Maximum return achievable with optimal solution
        - selected_indices=None: No need to return selected indices as all items have the same cost and return. The validation code should check that
          the optimal solution contains a subset of the items of the right length.
        """
        assert self.mode in [
            "single_item_uniform",
            "n_items",
        ], f"Mode {self.mode} is invalid for instance generation with generate_n_items_knapsack_instance"
        items_in_solution = self.num_items_in_solution if self.mode == "n_items" else 1

        # Ensure that the optimal solution contains the specified number of items
        item_weight = self.max_capacity // (items_in_solution + 1) + 1
        # Generate knapsack instance...
        investments = [(item_weight, self.default_return) for _ in range(self.num_items)]

        return investments, self.default_return * items_in_solution, None

    def solve_knapsack(self, investments, max_capacity):
        """Solves the knapsack problem using dynamic programming"""
        num_investments = len(investments)

        # Initialize DP table for maximum return and number of ways
        dp = [[(0, 0) for _ in range(max_capacity + 1)] for _ in range(num_investments + 1)]

        for i in range(1, num_investments + 1):
            cost, return_ = investments[i - 1]
            for w in range(max_capacity + 1):
                if cost <= w:
                    # If adding the current investment yields a higher return, update the cell
                    if return_ + dp[i - 1][w - cost][0] > dp[i - 1][w][0]:
                        dp[i][w] = (return_ + dp[i - 1][w - cost][0], 1)
                    # If it yields the same return, add the number of ways from the cell without the current investment
                    elif return_ + dp[i - 1][w - cost][0] == dp[i - 1][w][0]:
                        dp[i][w] = (dp[i - 1][w][0], dp[i - 1][w][1] + dp[i - 1][w - cost][1])
                    # If it yields a lower return, keep the old maximum return and number of ways
                    else:
                        dp[i][w] = dp[i - 1][w]
                else:
                    dp[i][w] = dp[i - 1][w]

        # Retrieve the maximum return and the number of ways to achieve it
        max_return, num_ways = dp[num_investments][max_capacity]

        # Retrieve the indices of the selected investments
        selected_indices = []
        w = max_capacity
        for i in range(num_investments, 0, -1):
            if dp[i][w] != dp[i - 1][w]:
                selected_indices.append(i - 1)
                w -= investments[i - 1][0]

        return max_return, num_ways, selected_indices


class KnapsackInstanceGenarator:
    """
    Generates a knapsack instance with the given number of items and maximum capacity, and solves it.
    The instance is guaranteed to have a unique optimal solution in "random" or "single_item" mode .

    Args:
    - random: Random number generator
    - num_items: Number of items
    - max_capacity: Maximum capacity of the knapsack
    - mode: Mode of generation. Choice of "random", "trivial", "single_item", "single_item_uniform", "n_items"
        - random: Randomly generate the instance and return it; guaranteed to have a unique optimal solution
        - trivial: Generate a trivial instance with all items fitting in the knapsack; return the instance
        - single_item: Generate an instance where the optimal solution has only one item
        - n_items: Generate an instance with all items having uniform weight and value; n items fitting in the knapsack
        - single_item_uniform: Generate an instance with all items having uniform weight and value; optimal solution has only one item and it can be any
    - num_items_in_solution: Number of items in the optimal solution. Required for "n_items" mode.
    - default_return: Default return value for investments having uniform weight and value. Required for "n_items" and "single_item_uniform" modes.
    """

    def __init__(
        self,
        random: np.random,
        num_items: int,
        max_capacity: int,
        mode: str = "random",
        num_items_in_solution: int = None,
        default_return: int = 100000,
    ):
        self.random = random
        self.num_items = num_items
        self.max_capacity = max_capacity
        self.mode = mode
        self.num_items_in_solution = num_items_in_solution
        self.default_return = default_return

    def get_instance(self):
        if self.mode in ["random", "trivial"]:
            return self.generate_and_solve_knapsack_instance()
        elif self.mode == "single_item":
            return self.generate_single_item_knapsack_instance()
        elif self.mode in ["single_item_uniform", "n_items"]:
            return self.generate_uniform_knapsack_instance()
        else:
            raise ValueError(f"Invalid mode {self.mode} for knapsack instance generation")

    def generate_and_solve_knapsack_instance(self):
        """
        Generates a knapsack instance with the given number of items and maximum capacity, and solves it.
        Used to generate instances for the "random" and "trivial" mode.
        Returns:
        - investments: List of tuples (cost, investment_return) for each investment
        - max_return: Maximum return achievable with optimal solution
        - selected_indices: Indices of the investments selected in the optimal solution
        """


    - num_items_in_solution: Number of items in the optimal solution. Required for "n_items" mode.
    - default_return: Default return value for investments having uniform weight and value. Required for "n_items" and "single_item_uniform" modes.
    """

    def __init__(
        self,
        random: np.random,
        num_items: int,
        max_capacity: int,
        mode: str = "random",
        num_items_in_solution: int = None,
        default_return: int = 100000,
    ):
        self.random = random
        self.num_items = num_items
        self.max_capacity = max_capacity
        self.mode = mode
        self.num_items_in_solution = num_items_in_solution
        self.default_return = default_return

    def get_instance(self):
        if self.mode in ["random", "trivial"]:
            return self.generate_and_solve_knapsack_instance()
        elif self.mode == "single_item":
            return self.generate_single_item_knapsack_instance()
        elif self.mode in ["single_item_uniform", "n_items"]:
            return self.generate_uniform_knapsack_instance()
        else:
            raise ValueError(f"Invalid mode {self.mode} for knapsack instance generation")

    def generate_and_solve_knapsack_instance(self):
        """
        Generates a knapsack instance with the given number of items and maximum capacity, and solves it.
        Used to generate instances for the "random" and "trivial" mode.
        Returns:
        - investments: List of tuples (cost, investment_return) for each investment
        - max_return: Maximum return achievable with optimal solution
        - selected_indices: Indices of the investments selected in the optimal solution
        """

        assert self.mode in [
            "random",
            "trivial",
        ], f"Mode {self.mode} is invalid for instance generation with generate_and_solve_knapsack_instance"

        multiple_solutions = True
        while multiple_solutions:
            # Generate knapsack instance...
            investments = []


        num_items_in_solution: int = None,
        default_return: int = 100000,
    ):
        self.random = random
        self.num_items = num_items
        self.max_capacity = max_capacity
        self.mode = mode
        self.num_items_in_solution = num_items_in_solution
        self.default_return = default_return

    def get_instance(self):
        if self.mode in ["random", "trivial"]:
            return self.generate_and_solve_knapsack_instance()
        elif self.mode == "single_item":
            return self.generate_single_item_knapsack_instance()
        elif self.mode in ["single_item_uniform", "n_items"]:
            return self.generate_uniform_knapsack_instance()
        else:
            raise ValueError(f"Invalid mode {self.mode} for knapsack instance generation")

    def generate_and_solve_knapsack_instance(self):
        """
        Generates a knapsack instance with the given number of items and maximum capacity, and solves it.
        Used to generate instances for the "random" and "trivial" mode.
        Returns:
        - investments: List of tuples (cost, investment_return) for each investment
        - max_return: Maximum return achievable with optimal solution
        - selected_indices: Indices of the investments selected in the optimal solution
        """

        assert self.mode in [
            "random",
            "trivial",
        ], f"Mode {self.mode} is invalid for instance generation with generate_and_solve_knapsack_instance"

        multiple_solutions = True
        while multiple_solutions:
            # Generate knapsack instance...
            investments = []
            min_cost = self.max_capacity // (self.num_items * 2)
            max_cost = (
                self.max_capacity // 2
                if self.mode == "random"
                else self.max_capacity // self.num_items
            )
            for _ in range(self.num_items):
                cost = self.random.randint(min_cost, max_cost)
                # Ensure that investments yield positive returns
                investment_return = self.random.randint(
                    self.max_capacity // 2, self.max_capacity // 2 + 40000
                )
                investments.append((cost, investment_return))

            total_cost = sum([investments[i][0] for i in range(self.num_items)])
            # Skip trivial instances where all items fit in the knapsack
            if self.mode == "random" and total_cost <= self.max_capacity:
                continue

            if self.mode == "random":
                # ...Solve it...
                max_return, num_optimal_solutions, selected_indices = self.solve_knapsack(
                    investments, self.max_capacity
                )
                # ...and check if there are multiple solutions
                multiple_solutions = num_optimal_solutions > 1
            else:
                selected_indices = list(range(self.num_items))
                max_return = sum([investments[i][1] for i in selected_indices])
                multiple_solutions = False

        return investments, max_return, selected_indices

    def generate_single_item_knapsack_instance(self):
        """Generate knapsack instance where the optimal solution contains only one item
        Returns:
        - investments: List of tuples (cost, investment_return) for each investment
        - max_investment_return: Investment return of the selected investment in the optimal solution
        - selected_indices: Index of the selected investment in the optimal solution
        """
        assert (
            self.mode == "single_item"
        ), f"Mode {self.mode} is invalid for instance generation with generate_single_item_knapsack_instance"

        # Ensure that the optimal solution contains only one item
        min_cost = self.max_capacity // 2 + 1
        max_cost = self.max_capacity - 1

        max_investment_return = 0
        max_investment_index = 0

        # Generate knapsack instance...


            total_cost = sum([investments[i][0] for i in range(self.num_items)])
            # Skip trivial instances where all items fit in the knapsack
            if self.mode == "random" and total_cost <= self.max_capacity:
                continue

            if self.mode == "random":
                # ...Solve it...
                max_return, num_optimal_solutions, selected_indices = self.solve_knapsack(
                    investments, self.max_capacity
                )
                # ...and check if there are multiple solutions
                multiple_solutions = num_optimal_solutions > 1
            else:
                selected_indices = list(range(self.num_items))
                max_return = sum([investments[i][1] for i in selected_indices])
                multiple_solutions = False

        return investments, max_return, selected_indices

    def generate_single_item_knapsack_instance(self):
        """Generate knapsack instance where the optimal solution contains only one item
        Returns:
        - investments: List of tuples (cost, investment_return) for each investment
        - max_investment_return: Investment return of the selected investment in the optimal solution
        - selected_indices: Index of the selected investment in the optimal solution
        """
        assert (
            self.mode == "single_item"
        ), f"Mode {self.mode} is invalid for instance generation with generate_single_item_knapsack_instance"

        # Ensure that the optimal solution contains only one item
        min_cost = self.max_capacity // 2 + 1
        max_cost = self.max_capacity - 1

        max_investment_return = 0
        max_investment_index = 0

        # Generate knapsack instance...
        investments = []
        for i in range(self.num_items):
            cost = self.random.randint(min_cost, max_cost)
            investment_return = self.random.randint(max_cost, 2 * max_cost)

            # Ensure that the optimal solution contains only one item
            while investment_return == max_investment_return:
                investment_return = self.random.randint(max_cost, 2 * max_cost)

            if investment_return > max_investment_return:
                max_investment_return = investment_return
                max_investment_index = i

            investments.append((cost, investment_return))

        return investments, max_investment_return, [max_investment_index]

    def generate_uniform_knapsack_instance(self):
        """Generate knapsack instance where all items have the same cost and return
        Returns:
        - investments: List of tuples (cost, investment_return) for each investment
        - max_return: Maximum return achievable with optimal solution
        - selected_indices=None: No need to return selected indices as all items have the same cost and return. The validation code should check that
          the optimal solution contains a subset of the items of the right length.
        """
        assert self.mode in [
            "single_item_uniform",
            "n_items",
        ], f"Mode {self.mode} is invalid for instance generation with generate_n_items_knapsack_instance"
        items_in_solution = self.num_items_in_solution if self.mode == "n_items" else 1

        # Ensure that the optimal solution contains the specified number of items
        item_weight = self.max_capacity // (items_in_solution + 1) + 1
        # Generate knapsack instance...
        investments = [(item_weight, self.default_return) for _ in range(self.num_items)]


        max_investment_index = 0

        # Generate knapsack instance...
        investments = []
        for i in range(self.num_items):
            cost = self.random.randint(min_cost, max_cost)
            investment_return = self.random.randint(max_cost, 2 * max_cost)

            # Ensure that the optimal solution contains only one item
            while investment_return == max_investment_return:
                investment_return = self.random.randint(max_cost, 2 * max_cost)

            if investment_return > max_investment_return:
                max_investment_return = investment_return
                max_investment_index = i

            investments.append((cost, investment_return))

        return investments, max_investment_return, [max_investment_index]

    def generate_uniform_knapsack_instance(self):
        """Generate knapsack instance where all items have the same cost and return
        Returns:
        - investments: List of tuples (cost, investment_return) for each investment
        - max_return: Maximum return achievable with optimal solution
        - selected_indices=None: No need to return selected indices as all items have the same cost and return. The validation code should check that
          the optimal solution contains a subset of the items of the right length.
        """
        assert self.mode in [
            "single_item_uniform",
            "n_items",
        ], f"Mode {self.mode} is invalid for instance generation with generate_n_items_knapsack_instance"
        items_in_solution = self.num_items_in_solution if self.mode == "n_items" else 1

        # Ensure that the optimal solution contains the specified number of items
        item_weight = self.max_capacity // (items_in_solution + 1) + 1
        # Generate knapsack instance...
        investments = [(item_weight, self.default_return) for _ in range(self.num_items)]

        return investments, self.default_return * items_in_solution, None

    def solve_knapsack(self, investments, max_capacity):
        """Solves the knapsack problem using dynamic programming"""
        num_investments = len(investments)

        # Initialize DP table for maximum return and number of ways
        dp = [[(0, 0) for _ in range(max_capacity + 1)] for _ in range(num_investments + 1)]

        for i in range(1, num_investments + 1):
            cost, return_ = investments[i - 1]
            for w in range(max_capacity + 1):
                if cost <= w:
                    # If adding the current investment yields a higher return, update the cell
                    if return_ + dp[i - 1][w - cost][0] > dp[i - 1][w][0]:
                        dp[i][w] = (return_ + dp[i - 1][w - cost][0], 1)
                    # If it yields the same return, add the number of ways from the cell without the current investment
                    elif return_ + dp[i - 1][w - cost][0] == dp[i - 1][w][0]:
                        dp[i][w] = (dp[i - 1][w][0], dp[i - 1][w][1] + dp[i - 1][w - cost][1])
                    # If it yields a lower return, keep the old maximum return and number of ways
                    else:


        """Generate knapsack instance where all items have the same cost and return
        Returns:
        - investments: List of tuples (cost, investment_return) for each investment
        - max_return: Maximum return achievable with optimal solution
        - selected_indices=None: No need to return selected indices as all items have the same cost and return. The validation code should check that
          the optimal solution contains a subset of the items of the right length.
        """
        assert self.mode in [
            "single_item_uniform",
            "n_items",
        ], f"Mode {self.mode} is invalid for instance generation with generate_n_items_knapsack_instance"
        items_in_solution = self.num_items_in_solution if self.mode == "n_items" else 1

        # Ensure that the optimal solution contains the specified number of items
        item_weight = self.max_capacity // (items_in_solution + 1) + 1
        # Generate knapsack instance...
        investments = [(item_weight, self.default_return) for _ in range(self.num_items)]

        return investments, self.default_return * items_in_solution, None

    def solve_knapsack(self, investments, max_capacity):
        """Solves the knapsack problem using dynamic programming"""
        num_investments = len(investments)

        # Initialize DP table for maximum return and number of ways
        dp = [[(0, 0) for _ in range(max_capacity + 1)] for _ in range(num_investments + 1)]

        for i in range(1, num_investments + 1):
            cost, return_ = investments[i - 1]
            for w in range(max_capacity + 1):
                if cost <= w:
                    # If adding the current investment yields a higher return, update the cell
                    if return_ + dp[i - 1][w - cost][0] > dp[i - 1][w][0]:
                        dp[i][w] = (return_ + dp[i - 1][w - cost][0], 1)
                    # If it yields the same return, add the number of ways from the cell without the current investment
                    elif return_ + dp[i - 1][w - cost][0] == dp[i - 1][w][0]:
                        dp[i][w] = (dp[i - 1][w][0], dp[i - 1][w][1] + dp[i - 1][w - cost][1])
                    # If it yields a lower return, keep the old maximum return and number of ways
                    else:
                        dp[i][w] = dp[i - 1][w]
                else:
                    dp[i][w] = dp[i - 1][w]

        # Retrieve the maximum return and the number of ways to achieve it
        max_return, num_ways = dp[num_investments][max_capacity]

        # Retrieve the indices of the selected investments
        selected_indices = []
        w = max_capacity
        for i in range(num_investments, 0, -1):
            if dp[i][w] != dp[i - 1][w]:
                selected_indices.append(i - 1)
                w -= investments[i - 1][0]

        return max_return, num_ways, selected_indices


import numpy as np

from faker import Faker

fake = Faker()

def get_infeasible_form_config(config, random: np.random, provide_reason: bool = True):
    """
    Get an infeasible form config from a feasible config by replacing the name of one of the task_fields with a random word

    Args:
    --------
    config (dict):
        The feasible form config to be transformed into an infeasible one
    random (np.random):
        The random number generator to use
    provide_reason (bool):
        Whether to provide a reason for the infeasibility. If False, the list of reasons will be [""] so that
        any infeasibility can be detected by the absence of a reason

    Returns:
    --------
    infeasible_config (dict):
        The infeasible form config
    infeasible_keywords (list[str]):
        The name of the new field printed and its system name
    """
    replaced_field = (
        random.choice(config["infeasible_task_fields"])
        if "infeasible_task_fields" in config
        else random.choice(config["task_fields"])
    )
    new_field_printed = fake.word().capitalize() + " " + fake.word()
    new_field_system_name = new_field_printed.lower().replace(" ", "_")

    config["task_fields"].remove(replaced_field)
    config["task_fields"].append(new_field_system_name)
    config["fields"][new_field_system_name] = new_field_printed
    config["template_record"][new_field_system_name] = fake.word()

    infeasible_reasons = [new_field_printed, new_field_system_name] if provide_reason else [""]

    return config, infeasible_reasons

def get_infeasible_service_catalog_config(config, random: np.random, provide_reason: bool = True):
    """
    Get an infeasible service catalog config from a feasible config by replacing the name of one of the additional configuration items with a random word

    Args:
    --------
    config (dict):
        The feasible service catalog config to be transformed into an infeasible one
    random (np.random):
        The random number generator to use
    provide_reason (bool):
        Whether to provide a reason for the infeasibility. If False, the list of reasons will be [""] so that
        any infeasibility can be detected by the absence of a reason

    Returns:
    --------
    infeasible_config (dict):
        The infeasible service catalog config
    infeasible_keywords (list[str]):
        The name of the new field printed and its system name
    """
    item_configuration = list(config["configuration"].keys())
    # if there is a configuration item, replace it with a new one; otherwise, simply add a new one
    if item_configuration:
        replaced_field = random.choice(item_configuration)
        config["configuration"].pop(replaced_field)
    new_field_printed = fake.word().capitalize() + " " + fake.word()
    field_type = random.choice(["radio", "textarea", "checkbox", "select"])
    field_options = [fake.word() for _ in range(random.randint(2, 5))]

    config["configuration"][new_field_printed] = [field_type, ", ".join(field_options)]

    infeasible_reasons = [new_field_printed, *field_options] if provide_reason else [""]

    return config, infeasible_reasons

def get_infeasible_sort_config(config, random: np.random, provide_reason: bool = True):
    """
    Get an infeasible sort config from a feasible config by replacing the name of one sort_fields with a random word

    Args:
    --------
    config (dict):
        The feasible sort config to be transformed into an infeasible one
    random (np.random):
        The random number generator to use
    provide_reason (bool):
        Whether to provide a reason for the infeasibility. If False, the list of reasons will be [""] so that
        any infeasibility can be detected by the absence of a reason

    Returns:
    --------
    infeasible_config (dict):
        The infeasible sort config
    infeasible_keywords (list[str]):
        The name of the new sort option printed and its system name
    """
    goal = config["goal"]
    config_fields = [line[3:].split(" (")[0] for line in goal.split("\n")[1:]]
    replaced_field_index = random.randint(0, len(config["sort_fields"]))

    new_field_printed = fake.word().capitalize() + " " + fake.word()
    new_field_system_name = new_field_printed.lower().replace(" ", "_")

    config["goal"] = goal.replace(config_fields[replaced_field_index], new_field_printed)
    config["sort_fields"][replaced_field_index] = new_field_system_name

    infeasible_reasons = [new_field_printed, new_field_system_name] if provide_reason else [""]

    return config, infeasible_reasons

def get_infeasible_filter_config(config, random: np.random, provide_reason: bool = True):
    """
    Get an infeasible filter config from a feasible config by replacing the name of one of the filter_columns with a random word

    Args:
    --------
    config (dict):
        The feasible filter config to be transformed into an infeasible one
    random (np.random):
        The random number generator to use
    provide_reason (bool):
        Whether to provide a reason for the infeasibility. If False, the list of reasons will be [""] so that
        any infeasibility can be detected by the absence of a reason

    Returns:
    --------
    infeasible_config (dict):
        The infeasible filter config
    infeasible_keywords (list[str]):
        The name of the new filter option printed and its system name
    """
    replaced_field_index = random.randint(0, len(config["filter_columns"]))

    new_field_printed = fake.word().capitalize() + " " + fake.word()
    new_field_system_name = new_field_printed.lower().replace(" ", "_")
    config["filter_columns"][replaced_field_index] = new_field_system_name
    config["filter_values"][replaced_field_index] = fake.word().capitalize()
    config["list_info"]["columns"][new_field_system_name] = {"label": new_field_printed}

    infeasible_reasons = [new_field_printed, new_field_system_name] if provide_reason else [""]

    return config, infeasible_reasons


import numpy as np

from faker import Faker

fake = Faker()

def get_infeasible_form_config(config, random: np.random, provide_reason: bool = True):
    """
    Get an infeasible form config from a feasible config by replacing the name of one of the task_fields with a random word

    Args:
    --------
    config (dict):
        The feasible form config to be transformed into an infeasible one
    random (np.random):
        The random number generator to use
    provide_reason (bool):
        Whether to provide a reason for the infeasibility. If False, the list of reasons will be [""] so that
        any infeasibility can be detected by the absence of a reason

    Returns:
    --------
    infeasible_config (dict):
        The infeasible form config
    infeasible_keywords (list[str]):
        The name of the new field printed and its system name
    """
    replaced_field = (
        random.choice(config["infeasible_task_fields"])
        if "infeasible_task_fields" in config
        else random.choice(config["task_fields"])
    )
    new_field_printed = fake.word().capitalize() + " " + fake.word()
    new_field_system_name = new_field_printed.lower().replace(" ", "_")

    config["task_fields"].remove(replaced_field)
    config["task_fields"].append(new_field_system_name)
    config["fields"][new_field_system_name] = new_field_printed
    config["template_record"][new_field_system_name] = fake.word()

    infeasible_reasons = [new_field_printed, new_field_system_name] if provide_reason else [""]

    return config, infeasible_reasons

def get_infeasible_service_catalog_config(config, random: np.random, provide_reason: bool = True):
    """
    Get an infeasible service catalog config from a feasible config by replacing the name of one of the additional configuration items with a random word

    Args:
    --------
    config (dict):
        The feasible service catalog config to be transformed into an infeasible one
    random (np.random):
        The random number generator to use
    provide_reason (bool):
        Whether to provide a reason for the infeasibility. If False, the list of reasons will be [""] so that
        any infeasibility can be detected by the absence of a reason

    Returns:
    --------
    infeasible_config (dict):
        The infeasible service catalog config


        The name of the new field printed and its system name
    """
    replaced_field = (
        random.choice(config["infeasible_task_fields"])
        if "infeasible_task_fields" in config
        else random.choice(config["task_fields"])
    )
    new_field_printed = fake.word().capitalize() + " " + fake.word()
    new_field_system_name = new_field_printed.lower().replace(" ", "_")

    config["task_fields"].remove(replaced_field)
    config["task_fields"].append(new_field_system_name)
    config["fields"][new_field_system_name] = new_field_printed
    config["template_record"][new_field_system_name] = fake.word()

    infeasible_reasons = [new_field_printed, new_field_system_name] if provide_reason else [""]

    return config, infeasible_reasons

def get_infeasible_service_catalog_config(config, random: np.random, provide_reason: bool = True):
    """
    Get an infeasible service catalog config from a feasible config by replacing the name of one of the additional configuration items with a random word

    Args:
    --------
    config (dict):
        The feasible service catalog config to be transformed into an infeasible one
    random (np.random):
        The random number generator to use
    provide_reason (bool):
        Whether to provide a reason for the infeasibility. If False, the list of reasons will be [""] so that
        any infeasibility can be detected by the absence of a reason

    Returns:
    --------
    infeasible_config (dict):
        The infeasible service catalog config
    infeasible_keywords (list[str]):
        The name of the new field printed and its system name
    """
    item_configuration = list(config["configuration"].keys())
    # if there is a configuration item, replace it with a new one; otherwise, simply add a new one
    if item_configuration:
        replaced_field = random.choice(item_configuration)
        config["configuration"].pop(replaced_field)
    new_field_printed = fake.word().capitalize() + " " + fake.word()
    field_type = random.choice(["radio", "textarea", "checkbox", "select"])
    field_options = [fake.word() for _ in range(random.randint(2, 5))]

    config["configuration"][new_field_printed] = [field_type, ", ".join(field_options)]

    infeasible_reasons = [new_field_printed, *field_options] if provide_reason else [""]

    return config, infeasible_reasons

def get_infeasible_sort_config(config, random: np.random, provide_reason: bool = True):
    """
    Get an infeasible sort config from a feasible config by replacing the name of one sort_fields with a random word

    Args:
    --------
    config (dict):
        The feasible sort config to be transformed into an infeasible one
    random (np.random):
        The random number generator to use
    provide_reason (bool):
        Whether to provide a reason for the infeasibility. If False, the list of reasons will be [""] so that
        any infeasibility can be detected by the absence of a reason

    Returns:
    --------
    infeasible_config (dict):
        The infeasible sort config


        The infeasible service catalog config
    infeasible_keywords (list[str]):
        The name of the new field printed and its system name
    """
    item_configuration = list(config["configuration"].keys())
    # if there is a configuration item, replace it with a new one; otherwise, simply add a new one
    if item_configuration:
        replaced_field = random.choice(item_configuration)
        config["configuration"].pop(replaced_field)
    new_field_printed = fake.word().capitalize() + " " + fake.word()
    field_type = random.choice(["radio", "textarea", "checkbox", "select"])
    field_options = [fake.word() for _ in range(random.randint(2, 5))]

    config["configuration"][new_field_printed] = [field_type, ", ".join(field_options)]

    infeasible_reasons = [new_field_printed, *field_options] if provide_reason else [""]

    return config, infeasible_reasons

def get_infeasible_sort_config(config, random: np.random, provide_reason: bool = True):
    """
    Get an infeasible sort config from a feasible config by replacing the name of one sort_fields with a random word

    Args:
    --------
    config (dict):
        The feasible sort config to be transformed into an infeasible one
    random (np.random):
        The random number generator to use
    provide_reason (bool):
        Whether to provide a reason for the infeasibility. If False, the list of reasons will be [""] so that
        any infeasibility can be detected by the absence of a reason

    Returns:
    --------
    infeasible_config (dict):
        The infeasible sort config
    infeasible_keywords (list[str]):
        The name of the new sort option printed and its system name
    """
    goal = config["goal"]
    config_fields = [line[3:].split(" (")[0] for line in goal.split("\n")[1:]]
    replaced_field_index = random.randint(0, len(config["sort_fields"]))

    new_field_printed = fake.word().capitalize() + " " + fake.word()
    new_field_system_name = new_field_printed.lower().replace(" ", "_")

    config["goal"] = goal.replace(config_fields[replaced_field_index], new_field_printed)
    config["sort_fields"][replaced_field_index] = new_field_system_name

    infeasible_reasons = [new_field_printed, new_field_system_name] if provide_reason else [""]

    return config, infeasible_reasons

def get_infeasible_filter_config(config, random: np.random, provide_reason: bool = True):
    """
    Get an infeasible filter config from a feasible config by replacing the name of one of the filter_columns with a random word

    Args:
    --------
    config (dict):
        The feasible filter config to be transformed into an infeasible one
    random (np.random):
        The random number generator to use
    provide_reason (bool):
        Whether to provide a reason for the infeasibility. If False, the list of reasons will be [""] so that
        any infeasibility can be detected by the absence of a reason

    Returns:
    --------
    infeasible_config (dict):
        The infeasible filter config


    infeasible_config (dict):
        The infeasible sort config
    infeasible_keywords (list[str]):
        The name of the new sort option printed and its system name
    """
    goal = config["goal"]
    config_fields = [line[3:].split(" (")[0] for line in goal.split("\n")[1:]]
    replaced_field_index = random.randint(0, len(config["sort_fields"]))

    new_field_printed = fake.word().capitalize() + " " + fake.word()
    new_field_system_name = new_field_printed.lower().replace(" ", "_")

    config["goal"] = goal.replace(config_fields[replaced_field_index], new_field_printed)
    config["sort_fields"][replaced_field_index] = new_field_system_name

    infeasible_reasons = [new_field_printed, new_field_system_name] if provide_reason else [""]

    return config, infeasible_reasons

def get_infeasible_filter_config(config, random: np.random, provide_reason: bool = True):
    """
    Get an infeasible filter config from a feasible config by replacing the name of one of the filter_columns with a random word

    Args:
    --------
    config (dict):
        The feasible filter config to be transformed into an infeasible one
    random (np.random):
        The random number generator to use
    provide_reason (bool):
        Whether to provide a reason for the infeasibility. If False, the list of reasons will be [""] so that
        any infeasibility can be detected by the absence of a reason

    Returns:
    --------
    infeasible_config (dict):
        The infeasible filter config
    infeasible_keywords (list[str]):
        The name of the new filter option printed and its system name
    """
    replaced_field_index = random.randint(0, len(config["filter_columns"]))

    new_field_printed = fake.word().capitalize() + " " + fake.word()
    new_field_system_name = new_field_printed.lower().replace(" ", "_")
    config["filter_columns"][replaced_field_index] = new_field_system_name
    config["filter_values"][replaced_field_index] = fake.word().capitalize()
    config["list_info"]["columns"][new_field_system_name] = {"label": new_field_printed}

    infeasible_reasons = [new_field_printed, new_field_system_name] if provide_reason else [""]

    return config, infeasible_reasons


# from .edit_knowledge_base import __TASKS__ as EDIT_KNOWLEDGE_BASE_TASKS, __L2_TASKS__ as EDIT_KNOWLEDGE_BASE_L2_TASKS, __L3_TASKS__ as EDIT_KNOWLEDGE_BASE_L3TASKS
from ..dash_do_catalog import (
    DASH_AND_ORDER,
    DASH_COMPUTE_MEAN_AND_ORDER,
    DASH_COMPUTE_MEDIAN_AND_ORDER,
    DASH_COMPUTE_MODE_AND_ORDER,
)
from ..dash_do_create_incident import DASH_AND_CREATE_INCIDENT, DASH_COMPUTE_AND_CREATE_INCIDENT
from ..dash_do_create_problem import DASH_AND_CREATE_PROBLEM, DASH_COMPUTE_AND_CREATE_PROBLEM
from ..dash_do_filter import (
    DASH_COMPUTE_MIN_FILTER_LIST,
    DASH_COMPUTE_MAX_FILTER_LIST,
    DASH_COMPUTE_MEAN_FILTER_LIST,
    DASH_COMPUTE_MEDIAN_FILTER_LIST,
    DASH_COMPUTE_MODE_FILTER_LIST,
)
from ..dash_do_request_item import (
    DASH_AND_REQUEST,
    DASH_COMPUTE_MEAN_AND_REQUEST,
    DASH_COMPUTE_MEDIAN_AND_REQUEST,
    DASH_COMPUTE_MODE_AND_REQUEST,
)
from ..expense_management import __TASKS__ as EXPENSE_MANAGEMENT_TASKS
from ..find_and_order_item import __TASKS__ as FIND_AND_ORDER_ITEM_TASKS
from ..manage_change_request_schedule import (
    SMALL_BASE_SCHEDULING_TASKS,
    LARGE_BASE_SCHEDULING_TASKS,
    SMALL_TIGHT_SCHEDULING_TASKS,
    LARGE_TIGHT_SCHEDULING_TASKS,
)
from ..mark_duplicate_problems import __TASKS__ as MARK_DUPLICATE_PROBLEMS_TASKS
from ..maximize_investment_return import __TASKS__ as MAXIMIZE_INVESTMENT_RETURN_TASKS
from ..navigate_and_do import (
    NAVIGATE_AND_CREATE_TASKS,
    NAVIGATE_AND_FILTER_TASKS,
    NAVIGATE_AND_ORDER_TASKS,
    NAVIGATE_AND_SORT_TASKS,
)
from ..navigate_and_do_infeasible import (
    INFEASIBLE_NAVIGATE_AND_CREATE_WITH_REASON,
    INFEASIBLE_NAVIGATE_AND_CREATE,
    INFEASIBLE_NAVIGATE_AND_ORDER_WITH_REASON,
    INFEASIBLE_NAVIGATE_AND_ORDER,
    INFEASIBLE_NAVIGATE_AND_FILTER_WITH_REASON,
    INFEASIBLE_NAVIGATE_AND_FILTER,
    INFEASIBLE_NAVIGATE_AND_SORT_WITH_REASON,
    INFEASIBLE_NAVIGATE_AND_SORT,
)
from ..offboard_user import __TASKS__ as OFFBOARD_USER_TASKS
from ..onboard_user import __TASKS__ as ONBOARD_USER_TASKS
from ..warranty_check import __TASKS__ as WARRANTY_CHECK_TASKS
from ..work_assignment import __TASKS__ as WORK_ASSIGNMENT_TASKS
from ..workload_balancing import __TASKS__ as WORKLOAD_BALANCING_TASKS

AGENT_CURRICULUM = {
    "planning_and_problem_solving": {
        "buckets": [
            MARK_DUPLICATE_PROBLEMS_TASKS,
            WORKLOAD_BALANCING_TASKS,
            WORK_ASSIGNMENT_TASKS,
            SMALL_BASE_SCHEDULING_TASKS,
            LARGE_BASE_SCHEDULING_TASKS,
            SMALL_TIGHT_SCHEDULING_TASKS,
            LARGE_TIGHT_SCHEDULING_TASKS,
        ],
        "num_seeds": 2,
        "weights": [9, 3, 6, 1, 1, 1, 1],
    },
    "information_retrieval": {
        "buckets": [
            DASH_AND_ORDER,
            DASH_AND_CREATE_INCIDENT,
            DASH_AND_CREATE_PROBLEM,
            DASH_COMPUTE_MIN_FILTER_LIST,
            DASH_COMPUTE_MAX_FILTER_LIST,
            DASH_AND_REQUEST,
            WARRANTY_CHECK_TASKS,
            FIND_AND_ORDER_ITEM_TASKS,
        ],
        "num_seeds": 7,
        "weights": [1, 1, 1, 1, 1, 1, 1, 1],
    },
    "data_driven_decision_making_and_reasoning": {
        "buckets": [
            EXPENSE_MANAGEMENT_TASKS,
            MAXIMIZE_INVESTMENT_RETURN_TASKS,
            DASH_COMPUTE_MEAN_AND_ORDER,
            DASH_COMPUTE_MEDIAN_AND_ORDER,
            DASH_COMPUTE_MODE_AND_ORDER,
            DASH_COMPUTE_AND_CREATE_INCIDENT,
            DASH_COMPUTE_AND_CREATE_PROBLEM,
            DASH_COMPUTE_MEAN_FILTER_LIST,
            DASH_COMPUTE_MEDIAN_FILTER_LIST,
            DASH_COMPUTE_MODE_FILTER_LIST,
            DASH_COMPUTE_MEAN_AND_REQUEST,
            DASH_COMPUTE_MEDIAN_AND_REQUEST,
            DASH_COMPUTE_MODE_AND_REQUEST,
        ],
        "num_seeds": 1,
        "weights": [12, 28, 1, 1, 1, 3, 3, 1, 1, 1, 1, 1, 1],
    },
    "sophisticated_memory": {
        "buckets": [
            NAVIGATE_AND_CREATE_TASKS,
            NAVIGATE_AND_ORDER_TASKS,
            NAVIGATE_AND_FILTER_TASKS,
            NAVIGATE_AND_SORT_TASKS,
            OFFBOARD_USER_TASKS,
            ONBOARD_USER_TASKS,
        ],
        "num_seeds": 8,
        "weights": [1, 1, 1, 1, 1, 1],
    },
    "contextual_understanding_infeasible_tasks": {
        "buckets": [
            INFEASIBLE_NAVIGATE_AND_CREATE_WITH_REASON,
            INFEASIBLE_NAVIGATE_AND_CREATE,
            INFEASIBLE_NAVIGATE_AND_ORDER_WITH_REASON,
            INFEASIBLE_NAVIGATE_AND_ORDER,
            INFEASIBLE_NAVIGATE_AND_FILTER_WITH_REASON,
            INFEASIBLE_NAVIGATE_AND_FILTER,
            INFEASIBLE_NAVIGATE_AND_SORT_WITH_REASON,
            INFEASIBLE_NAVIGATE_AND_SORT,
        ],
        "num_seeds": 4,
        "weights": [1, 1, 1, 1, 1, 1, 1, 1],
    },
}

HUMAN_CURRICULUM = {
    "planning_and_problem_solving": {
        "buckets": [
            MARK_DUPLICATE_PROBLEMS_TASKS,
            WORKLOAD_BALANCING_TASKS,
            WORK_ASSIGNMENT_TASKS,
            SMALL_BASE_SCHEDULING_TASKS,
            SMALL_TIGHT_SCHEDULING_TASKS,
        ],
        "num_seeds": 1,
        "weights": [
            3,
            1,
            2,
            1,
            1,
        ],
    },
    "information_retrieval": {
        "buckets": [
            DASH_AND_ORDER,
            DASH_AND_CREATE_INCIDENT,
            DASH_AND_CREATE_PROBLEM,
            DASH_COMPUTE_MIN_FILTER_LIST,
            DASH_COMPUTE_MAX_FILTER_LIST,
            DASH_AND_REQUEST,
            WARRANTY_CHECK_TASKS,
            FIND_AND_ORDER_ITEM_TASKS,
        ],
        "num_seeds": 1,
        "weights": [1, 1, 1, 1, 1, 1, 1, 1],
    },
    "data_driven_decision_making_and_reasoning": {
        "buckets": [
            EXPENSE_MANAGEMENT_TASKS,
            MAXIMIZE_INVESTMENT_RETURN_TASKS,  # Not splitting as small multiplier
            [
                *DASH_COMPUTE_MEAN_AND_ORDER,
                *DASH_COMPUTE_MEDIAN_AND_ORDER,
                *DASH_COMPUTE_MODE_AND_ORDER,
            ],
            [
                *DASH_COMPUTE_AND_CREATE_INCIDENT,
                *DASH_COMPUTE_AND_CREATE_PROBLEM,
                *DASH_COMPUTE_MEAN_AND_REQUEST,
            ],
            DASH_COMPUTE_MEAN_FILTER_LIST,
            [
                *DASH_COMPUTE_MEDIAN_FILTER_LIST,
                *DASH_COMPUTE_MODE_FILTER_LIST,
            ],
            [
                *DASH_COMPUTE_MEDIAN_AND_REQUEST,
                *DASH_COMPUTE_MODE_AND_REQUEST,
            ],
        ],
        "num_seeds": 1,
        "weights": [2, 6, 1, 1, 1, 1, 1],
    },
    "sophisticated_memory": {
        "buckets": [
            NAVIGATE_AND_CREATE_TASKS,
            NAVIGATE_AND_ORDER_TASKS,
            NAVIGATE_AND_FILTER_TASKS,
            NAVIGATE_AND_SORT_TASKS,
            OFFBOARD_USER_TASKS,
            ONBOARD_USER_TASKS,
        ],
        "num_seeds": 2,
        "weights": [1, 1, 1, 1, 1, 1],
    },
    "contextual_understanding_infeasible_tasks": {
        "buckets": [
            INFEASIBLE_NAVIGATE_AND_CREATE_WITH_REASON,
            INFEASIBLE_NAVIGATE_AND_CREATE,
            INFEASIBLE_NAVIGATE_AND_ORDER_WITH_REASON,
            INFEASIBLE_NAVIGATE_AND_ORDER,
            INFEASIBLE_NAVIGATE_AND_FILTER_WITH_REASON,
            INFEASIBLE_NAVIGATE_AND_FILTER,
            INFEASIBLE_NAVIGATE_AND_SORT_WITH_REASON,
            INFEASIBLE_NAVIGATE_AND_SORT,
        ],
        "num_seeds": 1,
        "weights": [1, 1, 1, 1, 1, 1, 1, 1],
    },
}


import json
import multiprocessing
import random
import re

from itertools import product, combinations
from browsergym.workarena.tasks.service_catalog import META_CONFIGS
from browsergym.workarena.config import (
    ORDER_DEVELOPER_LAPTOP_TASK_CONFIG_PATH,
    ORDER_IPAD_MINI_TASK_CONFIG_PATH,
    ORDER_IPAD_PRO_TASK_CONFIG_PATH,
    ORDER_SALES_LAPTOP_TASK_CONFIG_PATH,
    ORDER_STANDARD_LAPTOP_TASK_CONFIG_PATH,
    ORDER_APPLE_WATCH_TASK_CONFIG_PATH,
    ORDER_APPLE_MAC_BOOK_PRO15_TASK_CONFIG_PATH,
    ORDER_DEVELOPMENT_LAPTOP_PC_TASK_CONFIG_PATH,
    ORDER_LOANER_LAPTOP_TASK_CONFIG_PATH,
)
from browsergym.workarena.tasks.service_catalog import __TASKS__

# The number of configurations to generate per item, as described in the WorkArena paper
NUM_CONFIGS_PER_ITEM = {
    "Developer Laptop (Mac)": 1000,
    "iPad mini": 80,
    "iPad pro": 90,
    "Sales Laptop": 1000,
    "Standard Laptop": 1000,
    "Apple Watch": 10,
    "Apple MacBook Pro 15": 10,
    "Development Laptop (PC)": 40,
    "Loaner Laptop": 350,
}

placeholder_tasks = [task() for task in __TASKS__]
ITEM_TO_TASK = {task.fixed_request_item: task.__class__.__name__ for task in placeholder_tasks}

CONFIG_PATHS = [
    ORDER_DEVELOPER_LAPTOP_TASK_CONFIG_PATH,
    ORDER_IPAD_MINI_TASK_CONFIG_PATH,
    ORDER_IPAD_PRO_TASK_CONFIG_PATH,
    ORDER_SALES_LAPTOP_TASK_CONFIG_PATH,
    ORDER_STANDARD_LAPTOP_TASK_CONFIG_PATH,
    ORDER_APPLE_WATCH_TASK_CONFIG_PATH,
    ORDER_APPLE_MAC_BOOK_PRO15_TASK_CONFIG_PATH,
    ORDER_DEVELOPMENT_LAPTOP_PC_TASK_CONFIG_PATH,
    ORDER_LOANER_LAPTOP_TASK_CONFIG_PATH,
]
TASK_TO_CONFIG_PATH = dict(zip(__TASKS__, CONFIG_PATHS))

def generate_configs_for_all_items():
    """Create task configs for all items; one file per task"""
    for item_choice, configs in META_CONFIGS.items():
        all_configs_for_a_single_item = generate_all_item_configs(item_choice, configs)
        num_configs_for_item = NUM_CONFIGS_PER_ITEM[item_choice]
        if len(all_configs_for_a_single_item) > num_configs_for_item:
            all_configs_for_a_single_item = random.sample(
                all_configs_for_a_single_item, num_configs_for_item
            )
        name = ITEM_TO_TASK[item_choice]
        name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
        task_name = re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower()
        with open(
            f"browsergym/workarena/src/browsergym/workarena/data_files/task_configs/{task_name}.json",
            "w",
        ) as f:
            all_configs_for_a_single_item = sorted(
                all_configs_for_a_single_item,
                key=lambda x: x["item"] + str(x["quantity"]),
            )
            json.dump(all_configs_for_a_single_item, f, indent=4, sort_keys=True)

def generate_all_item_configs(item_choice, configs):
    """
    generates all possible configurations for a given item choice and its configuration options.
    """
    all_configs = []

    # Prepare the configuration options by maintaining their type and possible values
    config_options = {}
    for config_key, config_values in configs["options"].items():
        if config_values[0] in ["checkbox", "radio"]:
            # Store each option with its type and each possible value
            config_options[config_key] = [(config_values[0], value) for value in config_values[1]]
        elif config_values[0] == "textarea":
            text_options = config_values[1]
            if config_key == "Additional software requirements":
                config_options[config_key] = [
                    ("textarea", ", ".join(option))
                    for n in range(
                        1, len(text_options) + 1
                    )  # Changed to 1 to len to avoid empty selection
                    for option in combinations(text_options, n)
                ]
            else:
                config_options[config_key] = [("textarea", option) for option in text_options]

    # Iterate over all possible configurations
    for values in product(*config_options.values()):
        # Pair each configuration key with its corresponding (type, value) tuple
        config_dict = dict(zip(config_options.keys(), values))
        for quantity in range(1, 11):
            # Construct the full configuration dictionary for each combination
            task_config = {
                "item": item_choice,
                "description": configs["desc"],
                "quantity": quantity,  # Static quantity; can be adjusted if needed
                "configuration": config_dict,
            }
            all_configs.append(task_config)

    return all_configs

def count_all_item_occurrences(configs):
    counts = {}
    for config in configs:
        item_name = config["item"]
        if item_name in counts:
            counts[item_name] += 1
        else:
            counts[item_name] = 1
    return counts


    "Loaner Laptop": 350,
}

placeholder_tasks = [task() for task in __TASKS__]
ITEM_TO_TASK = {task.fixed_request_item: task.__class__.__name__ for task in placeholder_tasks}

CONFIG_PATHS = [
    ORDER_DEVELOPER_LAPTOP_TASK_CONFIG_PATH,
    ORDER_IPAD_MINI_TASK_CONFIG_PATH,
    ORDER_IPAD_PRO_TASK_CONFIG_PATH,
    ORDER_SALES_LAPTOP_TASK_CONFIG_PATH,
    ORDER_STANDARD_LAPTOP_TASK_CONFIG_PATH,
    ORDER_APPLE_WATCH_TASK_CONFIG_PATH,
    ORDER_APPLE_MAC_BOOK_PRO15_TASK_CONFIG_PATH,
    ORDER_DEVELOPMENT_LAPTOP_PC_TASK_CONFIG_PATH,
    ORDER_LOANER_LAPTOP_TASK_CONFIG_PATH,
]
TASK_TO_CONFIG_PATH = dict(zip(__TASKS__, CONFIG_PATHS))

def generate_configs_for_all_items():
    """Create task configs for all items; one file per task"""
    for item_choice, configs in META_CONFIGS.items():
        all_configs_for_a_single_item = generate_all_item_configs(item_choice, configs)
        num_configs_for_item = NUM_CONFIGS_PER_ITEM[item_choice]
        if len(all_configs_for_a_single_item) > num_configs_for_item:
            all_configs_for_a_single_item = random.sample(
                all_configs_for_a_single_item, num_configs_for_item
            )
        name = ITEM_TO_TASK[item_choice]
        name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
        task_name = re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower()
        with open(
            f"browsergym/workarena/src/browsergym/workarena/data_files/task_configs/{task_name}.json",
            "w",
        ) as f:
            all_configs_for_a_single_item = sorted(
                all_configs_for_a_single_item,
                key=lambda x: x["item"] + str(x["quantity"]),
            )
            json.dump(all_configs_for_a_single_item, f, indent=4, sort_keys=True)

def generate_all_item_configs(item_choice, configs):
    """
    generates all possible configurations for a given item choice and its configuration options.
    """
    all_configs = []

    # Prepare the configuration options by maintaining their type and possible values
    config_options = {}
    for config_key, config_values in configs["options"].items():
        if config_values[0] in ["checkbox", "radio"]:
            # Store each option with its type and each possible value
            config_options[config_key] = [(config_values[0], value) for value in config_values[1]]
        elif config_values[0] == "textarea":
            text_options = config_values[1]
            if config_key == "Additional software requirements":
                config_options[config_key] = [
                    ("textarea", ", ".join(option))
                    for n in range(


        all_configs_for_a_single_item = generate_all_item_configs(item_choice, configs)
        num_configs_for_item = NUM_CONFIGS_PER_ITEM[item_choice]
        if len(all_configs_for_a_single_item) > num_configs_for_item:
            all_configs_for_a_single_item = random.sample(
                all_configs_for_a_single_item, num_configs_for_item
            )
        name = ITEM_TO_TASK[item_choice]
        name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
        task_name = re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower()
        with open(
            f"browsergym/workarena/src/browsergym/workarena/data_files/task_configs/{task_name}.json",
            "w",
        ) as f:
            all_configs_for_a_single_item = sorted(
                all_configs_for_a_single_item,
                key=lambda x: x["item"] + str(x["quantity"]),
            )
            json.dump(all_configs_for_a_single_item, f, indent=4, sort_keys=True)

def generate_all_item_configs(item_choice, configs):
    """
    generates all possible configurations for a given item choice and its configuration options.
    """
    all_configs = []

    # Prepare the configuration options by maintaining their type and possible values
    config_options = {}
    for config_key, config_values in configs["options"].items():
        if config_values[0] in ["checkbox", "radio"]:
            # Store each option with its type and each possible value
            config_options[config_key] = [(config_values[0], value) for value in config_values[1]]
        elif config_values[0] == "textarea":
            text_options = config_values[1]
            if config_key == "Additional software requirements":
                config_options[config_key] = [
                    ("textarea", ", ".join(option))
                    for n in range(
                        1, len(text_options) + 1
                    )  # Changed to 1 to len to avoid empty selection
                    for option in combinations(text_options, n)
                ]
            else:
                config_options[config_key] = [("textarea", option) for option in text_options]

    # Iterate over all possible configurations
    for values in product(*config_options.values()):
        # Pair each configuration key with its corresponding (type, value) tuple
        config_dict = dict(zip(config_options.keys(), values))
        for quantity in range(1, 11):
            # Construct the full configuration dictionary for each combination
            task_config = {
                "item": item_choice,
                "description": configs["desc"],
                "quantity": quantity,  # Static quantity; can be adjusted if needed
                "configuration": config_dict,
            }
            all_configs.append(task_config)

    return all_configs

def count_all_item_occurrences(configs):
    counts = {}
    for config in configs:
        item_name = config["item"]
        if item_name in counts:
            counts[item_name] += 1
        else:
            counts[item_name] = 1
    return counts


            else:
                config_options[config_key] = [("textarea", option) for option in text_options]

    # Iterate over all possible configurations
    for values in product(*config_options.values()):
        # Pair each configuration key with its corresponding (type, value) tuple
        config_dict = dict(zip(config_options.keys(), values))
        for quantity in range(1, 11):
            # Construct the full configuration dictionary for each combination
            task_config = {
                "item": item_choice,
                "description": configs["desc"],
                "quantity": quantity,  # Static quantity; can be adjusted if needed
                "configuration": config_dict,
            }
            all_configs.append(task_config)

    return all_configs

def count_all_item_occurrences(configs):
    counts = {}
    for config in configs:
        item_name = config["item"]
        if item_name in counts:
            counts[item_name] += 1
        else:
            counts[item_name] = 1
    return counts


"""
Generate configurations for the report and dashboard tasks

Notes: sometimes it crashes (e.g., timeout, etc.). Just relaunch and it will be fine.

"""

import json
import multiprocessing
import random
import tenacity

from functools import partial
from playwright.sync_api import sync_playwright

from browsergym.workarena.api.utils import table_api_call, table_column_info
from browsergym.workarena.config import (
    REPORT_DATE_FILTER,
    REPORT_PATCH_FLAG,
    REPORT_RETRIEVAL_MINMAX_CONFIG_PATH,
    REPORT_RETRIEVAL_VALUE_CONFIG_PATH,
    DASHBOARD_RETRIEVAL_MINMAX_CONFIG_PATH,
    DASHBOARD_RETRIEVAL_VALUE_CONFIG_PATH,
)
from browsergym.workarena.instance import SNowInstance
from browsergym.workarena.tasks.dashboard import DashboardRetrievalTask

N_CPU = 20
MAX_CONFIGS = 1000
REPORT = False  # Set to True for reports, False for dashboards

class DummyDashboard(DashboardRetrievalTask):
    def all_configs(self):
        return [
            {
                "url": "",
                "chart_title": "",
                "chart_series": "",
                "question": "max",
            }
        ]

def get_report_urls(instance):
    # Generate a bunch of reports on the fly 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=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=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

    return [
        (
            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}','00:00:00')^EQ"
            if report.get("sys_id", None) is None
            else f"/now/nav/ui/classic/params/target/sys_report_template.do%3Fjvar_report_id={report['sys_id']}"
        )
        for report in REPORTS
    ]

def get_dashboard_urls(instance):
    # 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 (almost empty post deleting reports that rely on time)
        "6b706f448f231110953ddffc9071a4f3",  # Telemetry - Table growth
        # "15c5d2d377213010a435478c4f5a993c",  # Usage overview
        # "85a57f9677100110ba155631dc5a9905",  # Web api usage overview (empty post deleting reports that rely on time)
        "c38ca3a273031010ae8dd21efaf6a747",  # Data classification
        "3d48f669538223008329ddeeff7b1253",  # Problem overview
    ]
    return [
        f"/now/nav/ui/classic/params/target/%24pa_dashboard.do%3Fsysparm_dashboard%3D{dashboard}"
        for dashboard in DASHBOARDS
    ]

@tenacity.retry(
    wait=tenacity.wait_fixed(1),
    stop=tenacity.stop_after_attempt(10),
)
def get_all_configs_by_url(url, is_report):
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()
        task = DummyDashboard(
            instance=SNowInstance(),
            fixed_config={
                "url": url,
                "chart_title": "",
                "chart_series": "",
                "question": "max",
            },
            seed=0,
        )
        task.setup(page=page)

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

        # Find all the charts
        charts = task._get_charts(page)

        # Check enough charts
        if len(charts) == 0:
            return []

        questions = []
        for chart in charts:
            try:
                chart_title = chart[0] if not is_report else ""  # No title for reports
                _, chart_data, _ = task._get_chart_by_title(page, chart_title)

                # Select a series randomly
                for series_idx in range(len(chart_data)):
                    series_name = chart_data[series_idx]["name"] if len(chart_data) > 1 else ""
                    data = chart_data[series_idx]["data"]

                    # Check if the data is interesting
                    labels = [point["label"] for point in data]
                    if len(labels) <= 1:
                        continue

                    if any(l.isdigit() for l in labels):
                        continue

                    # Generate all value questions
                    for format in ["count", "percent"]:
                        for label in labels:
                            questions.append(
                                {
                                    "url": url,
                                    "chart_title": chart_title,
                                    "chart_series": series_name,
                                    "question": f"value; {format}; {label}",
                                }
                            )

                    # Generate all other questions
                    questions.append(
                        {
                            "url": url,
                            "chart_title": chart_title,
                            "chart_series": series_name,
                            "question": "min",
                        }
                    )
                    questions.append(
                        {
                            "url": url,
                            "chart_title": chart_title,
                            "chart_series": series_name,
                            "question": "max",
                        }
                    )
            except Exception as e:
                print("Exception in worker", url, chart_title, e)
                continue  # Skip this chart

        if len(questions) == 0:
            return []

        # Test out all questions and keep only those that work
        valid_questions = []
        for question in questions:
            chat_messages = []
            task.config = question

            try:
                task.cheat(page=page, chat_messages=chat_messages)
                valid = task.validate(page=page, chat_messages=chat_messages)[0]
            except Exception as e:
                # raise e
                print("Exception in worker config validations", url, question, e)
                valid = 0

            if valid == 1:
                valid_questions.append(question)
            else:
                print(f"Failed to validate question {question}")

    print("Worker found", len(valid_questions), "valid questions")
    return valid_questions

if __name__ == "__main__":
    instance = SNowInstance()
    reports = get_report_urls(instance)
    gen_func = partial(get_all_configs_by_url, is_report=REPORT)

    if REPORT:
        urls = get_report_urls(instance)
        output_by_question = {
            "value": REPORT_RETRIEVAL_VALUE_CONFIG_PATH,
            "min,max": REPORT_RETRIEVAL_MINMAX_CONFIG_PATH,
        }
    else:
        urls = get_dashboard_urls(instance)
        output_by_question = {
            "value": DASHBOARD_RETRIEVAL_VALUE_CONFIG_PATH,
            "min,max": DASHBOARD_RETRIEVAL_MINMAX_CONFIG_PATH,
        }

    print(f"Generating configs for {len(urls)} URLs")
    configs = []
    with multiprocessing.Pool(processes=N_CPU) as pool:
        results = pool.map(gen_func, urls)
        pool.close()
        pool.join()

        # Flatten results list into config
        for result in results:
            configs += result

    # Post-process the configs and save them
    for question_type in output_by_question:
        types = [x.strip() for x in question_type.split(",")]

        type_configs = [
            config for config in configs if config["question"].split(";")[0].strip() in types
        ]
        type_configs = set(
            json.dumps(c) for c in type_configs
        )  # Serialize to string to make unique
        type_configs = [json.loads(c) for c in type_configs]
        random.shuffle(type_configs)
        type_configs = type_configs[:MAX_CONFIGS]

        print("Saving", len(type_configs), "configs for", question_type)
        with open(output_by_question[question_type], "w") as f:
            json.dump(type_configs, f)


DummyDashboard

from playwright.sync_api import sync_playwright

from browsergym.workarena.api.utils import table_api_call, table_column_info
from browsergym.workarena.config import (
    REPORT_DATE_FILTER,
    REPORT_PATCH_FLAG,
    REPORT_RETRIEVAL_MINMAX_CONFIG_PATH,
    REPORT_RETRIEVAL_VALUE_CONFIG_PATH,
    DASHBOARD_RETRIEVAL_MINMAX_CONFIG_PATH,
    DASHBOARD_RETRIEVAL_VALUE_CONFIG_PATH,
)
from browsergym.workarena.instance import SNowInstance
from browsergym.workarena.tasks.dashboard import DashboardRetrievalTask

N_CPU = 20
MAX_CONFIGS = 1000
REPORT = False  # Set to True for reports, False for dashboards

class DummyDashboard(DashboardRetrievalTask):
    def all_configs(self):
        return [
            {
                "url": "",
                "chart_title": "",
                "chart_series": "",
                "question": "max",
            }
        ]

def get_report_urls(instance):
    # Generate a bunch of reports on the fly 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=instance, table=table).items()


from browsergym.workarena.tasks.dashboard import DashboardRetrievalTask

N_CPU = 20
MAX_CONFIGS = 1000
REPORT = False  # Set to True for reports, False for dashboards

class DummyDashboard(DashboardRetrievalTask):
    def all_configs(self):
        return [
            {
                "url": "",
                "chart_title": "",
                "chart_series": "",
                "question": "max",
            }
        ]

def get_report_urls(instance):
    # Generate a bunch of reports on the fly 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=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=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

    return [
        (
            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}','00:00:00')^EQ"
            if report.get("sys_id", None) is None
            else f"/now/nav/ui/classic/params/target/sys_report_template.do%3Fjvar_report_id={report['sys_id']}"
        )
        for report in REPORTS
    ]

def get_dashboard_urls(instance):
    # 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 (almost empty post deleting reports that rely on time)
        "6b706f448f231110953ddffc9071a4f3",  # Telemetry - Table growth
        # "15c5d2d377213010a435478c4f5a993c",  # Usage overview
        # "85a57f9677100110ba155631dc5a9905",  # Web api usage overview (empty post deleting reports that rely on time)
        "c38ca3a273031010ae8dd21efaf6a747",  # Data classification
        "3d48f669538223008329ddeeff7b1253",  # Problem overview
    ]
    return [


        instance=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

    return [
        (
            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}','00:00:00')^EQ"
            if report.get("sys_id", None) is None
            else f"/now/nav/ui/classic/params/target/sys_report_template.do%3Fjvar_report_id={report['sys_id']}"
        )
        for report in REPORTS
    ]

def get_dashboard_urls(instance):
    # 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 (almost empty post deleting reports that rely on time)
        "6b706f448f231110953ddffc9071a4f3",  # Telemetry - Table growth
        # "15c5d2d377213010a435478c4f5a993c",  # Usage overview
        # "85a57f9677100110ba155631dc5a9905",  # Web api usage overview (empty post deleting reports that rely on time)
        "c38ca3a273031010ae8dd21efaf6a747",  # Data classification
        "3d48f669538223008329ddeeff7b1253",  # Problem overview
    ]
    return [
        f"/now/nav/ui/classic/params/target/%24pa_dashboard.do%3Fsysparm_dashboard%3D{dashboard}"
        for dashboard in DASHBOARDS
    ]

@tenacity.retry(
    wait=tenacity.wait_fixed(1),
    stop=tenacity.stop_after_attempt(10),
)
def get_all_configs_by_url(url, is_report):
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()
        task = DummyDashboard(
            instance=SNowInstance(),
            fixed_config={
                "url": url,
                "chart_title": "",
                "chart_series": "",
                "question": "max",
            },
            seed=0,
        )


        "18b1f472533130104c90ddeeff7b12a6",  # Incident overview
        "287d07d1ff3130106c1ef9a7cddcbd5d",  # Request overview
        "7ab78953eb32011008f2951ff15228e6",  # Service catalog overview
        # "2d297c880f1130101527008c07767e27",  # Survey overview (almost empty post deleting reports that rely on time)
        "6b706f448f231110953ddffc9071a4f3",  # Telemetry - Table growth
        # "15c5d2d377213010a435478c4f5a993c",  # Usage overview
        # "85a57f9677100110ba155631dc5a9905",  # Web api usage overview (empty post deleting reports that rely on time)
        "c38ca3a273031010ae8dd21efaf6a747",  # Data classification
        "3d48f669538223008329ddeeff7b1253",  # Problem overview
    ]
    return [
        f"/now/nav/ui/classic/params/target/%24pa_dashboard.do%3Fsysparm_dashboard%3D{dashboard}"
        for dashboard in DASHBOARDS
    ]

@tenacity.retry(
    wait=tenacity.wait_fixed(1),
    stop=tenacity.stop_after_attempt(10),
)
def get_all_configs_by_url(url, is_report):
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()
        task = DummyDashboard(
            instance=SNowInstance(),
            fixed_config={
                "url": url,
                "chart_title": "",
                "chart_series": "",
                "question": "max",
            },
            seed=0,
        )
        task.setup(page=page)

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

        # Find all the charts
        charts = task._get_charts(page)

        # Check enough charts
        if len(charts) == 0:
            return []

        questions = []
        for chart in charts:
            try:
                chart_title = chart[0] if not is_report else ""  # No title for reports
                _, chart_data, _ = task._get_chart_by_title(page, chart_title)

                # Select a series randomly
                for series_idx in range(len(chart_data)):
                    series_name = chart_data[series_idx]["name"] if len(chart_data) > 1 else ""
                    data = chart_data[series_idx]["data"]

                    # Check if the data is interesting
                    labels = [point["label"] for point in data]
                    if len(labels) <= 1:
                        continue

                    if any(l.isdigit() for l in labels):
                        continue

                    # Generate all value questions
                    for format in ["count", "percent"]:
                        for label in labels:
                            questions.append(
                                {
                                    "url": url,
                                    "chart_title": chart_title,
                                    "chart_series": series_name,
                                    "question": f"value; {format}; {label}",
                                }
                            )

                    # Generate all other questions
                    questions.append(
                        {
                            "url": url,
                            "chart_title": chart_title,
                            "chart_series": series_name,
                            "question": "min",
                        }
                    )
                    questions.append(
                        {
                            "url": url,
                            "chart_title": chart_title,
                            "chart_series": series_name,
                            "question": "max",
                        }
                    )
            except Exception as e:
                print("Exception in worker", url, chart_title, e)
                continue  # Skip this chart

        if len(questions) == 0:
            return []

        # Test out all questions and keep only those that work
        valid_questions = []
        for question in questions:
            chat_messages = []
            task.config = question

            try:
                task.cheat(page=page, chat_messages=chat_messages)
                valid = task.validate(page=page, chat_messages=chat_messages)[0]
            except Exception as e:
                # raise e
                print("Exception in worker config validations", url, question, e)
                valid = 0

            if valid == 1:
                valid_questions.append(question)
            else:
                print(f"Failed to validate question {question}")

    print("Worker found", len(valid_questions), "valid questions")
    return valid_questions

if __name__ == "__main__":
    instance = SNowInstance()
    reports = get_report_urls(instance)
    gen_func = partial(get_all_configs_by_url, is_report=REPORT)

    if REPORT:
        urls = get_report_urls(instance)
        output_by_question = {
            "value": REPORT_RETRIEVAL_VALUE_CONFIG_PATH,
            "min,max": REPORT_RETRIEVAL_MINMAX_CONFIG_PATH,
        }
    else:
        urls = get_dashboard_urls(instance)
        output_by_question = {
            "value": DASHBOARD_RETRIEVAL_VALUE_CONFIG_PATH,
            "min,max": DASHBOARD_RETRIEVAL_MINMAX_CONFIG_PATH,
        }


from browsergym.workarena.api.utils import table_api_call, table_column_info
from browsergym.workarena.config import (
    REPORT_DATE_FILTER,
    REPORT_PATCH_FLAG,
    REPORT_RETRIEVAL_MINMAX_CONFIG_PATH,
    REPORT_RETRIEVAL_VALUE_CONFIG_PATH,
    DASHBOARD_RETRIEVAL_MINMAX_CONFIG_PATH,
    DASHBOARD_RETRIEVAL_VALUE_CONFIG_PATH,
)
from browsergym.workarena.instance import SNowInstance
from browsergym.workarena.tasks.dashboard import DashboardRetrievalTask

N_CPU = 20
MAX_CONFIGS = 1000
REPORT = False  # Set to True for reports, False for dashboards

class DummyDashboard(DashboardRetrievalTask):
    def all_configs(self):
        return [
            {
                "url": "",
                "chart_title": "",
                "chart_series": "",
                "question": "max",
            }
        ]

def get_report_urls(instance):
    # Generate a bunch of reports on the fly 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=instance, table=table).items()