diff --git a/vizro-ai/src/vizro_ai/dashboard/nodes/_pydantic_output.py b/vizro-ai/src/vizro_ai/dashboard/_pydantic_output.py similarity index 100% rename from vizro-ai/src/vizro_ai/dashboard/nodes/_pydantic_output.py rename to vizro-ai/src/vizro_ai/dashboard/_pydantic_output.py diff --git a/vizro-ai/src/vizro_ai/dashboard/constants.py b/vizro-ai/src/vizro_ai/dashboard/constants.py new file mode 100644 index 000000000..9616015c1 --- /dev/null +++ b/vizro-ai/src/vizro_ai/dashboard/constants.py @@ -0,0 +1,24 @@ +"""Define constants for the dashboard module.""" + +from typing import Literal + +# For unsupported component and control types, how to handle them? +# option 1. Ignore silently +# option 2. Raise a warning and add the warning message into langgraph state. This gives the user transparency on why +# a certain component or control was not created. +# option 3. Raise a warning and suggest additional reference material +component_type = Literal[ + "AgGrid", "Card", "Graph" +] # Complete list: ["AgGrid", "Button", "Card", "Container", "Graph", "Table", "Tabs"] +control_type = Literal["Filter"] # Complete list: ["Filter", "Parameter"] + +# For other models, like ["Accordion", "NavBar"], how to handle them? + + +IMPORT_STATEMENTS = ( + "import vizro.plotly.express as px\n" + "from vizro.models.types import capture\n" + "import plotly.graph_objects as go\n" + "from vizro.tables import dash_ag_grid\n" + "import vizro.models as vm\n" +) diff --git a/vizro-ai/src/vizro_ai/dashboard/nodes/data_summary.py b/vizro-ai/src/vizro_ai/dashboard/data_preprocess/df_info.py similarity index 100% rename from vizro-ai/src/vizro_ai/dashboard/nodes/data_summary.py rename to vizro-ai/src/vizro_ai/dashboard/data_preprocess/df_info.py diff --git a/vizro-ai/src/vizro_ai/dashboard/graph/dashboard_creation.py b/vizro-ai/src/vizro_ai/dashboard/graph/dashboard_creation.py index d7e1e7c00..3e125f30f 100644 --- a/vizro-ai/src/vizro_ai/dashboard/graph/dashboard_creation.py +++ b/vizro-ai/src/vizro_ai/dashboard/graph/dashboard_creation.py @@ -11,13 +11,11 @@ from langgraph.constants import END, Send from langgraph.graph import StateGraph from tqdm.auto import tqdm -from vizro_ai.dashboard.nodes._pydantic_output import _get_pydantic_output -from vizro_ai.dashboard.nodes.build import PageBuilder -from vizro_ai.dashboard.nodes.data_summary import DfInfo, _get_df_info, df_sum_prompt -from vizro_ai.dashboard.nodes.plan import ( - DashboardPlanner, - PagePlanner, -) +from vizro_ai.dashboard._pydantic_output import _get_pydantic_output +from vizro_ai.dashboard.build.page import PageBuilder +from vizro_ai.dashboard.data_preprocess.df_info import DfInfo, _get_df_info, df_sum_prompt +from vizro_ai.dashboard.plan.dashboard import DashboardPlanner +from vizro_ai.dashboard.plan.page import PagePlanner from vizro_ai.dashboard.utils import DfMetadata, MetadataContent, _execute_step try: diff --git a/vizro-ai/src/vizro_ai/dashboard/nodes/build.py b/vizro-ai/src/vizro_ai/dashboard/nodes/build.py deleted file mode 100644 index 1a1ed47bf..000000000 --- a/vizro-ai/src/vizro_ai/dashboard/nodes/build.py +++ /dev/null @@ -1,109 +0,0 @@ -"""Module that contains the builder functionality.""" - -import logging - -import vizro.models as vm -from tqdm.auto import tqdm, trange -from vizro_ai.dashboard.utils import _execute_step -from vizro_ai.utils.helper import DebugFailure - -logger = logging.getLogger(__name__) - - -class PageBuilder: - """Class to build a page.""" - - def __init__(self, model, df_metadata, page_plan): - """Initialize PageBuilder.""" - self._model = model - self._df_metadata = df_metadata - self._page_plan = page_plan - self._components = None - self._controls = None - self._page = None - self._layout = None - - @property - def components(self): - """Property to get components.""" - if self._components is None: - self._components = self._build_components() - return self._components - - def _build_components(self): - components = [] - logger.info(f"Building components of page: {self._page_plan.title}") - for i in trange( - len(self._page_plan.components_plan), - desc=f"Building components of page: {self._page_plan.title}", - leave=False, - ): - logger.info(f"{self._page_plan.title} -> Building component {self._page_plan.components_plan[i]}") - try: - components.append( - self._page_plan.components_plan[i].create(df_metadata=self._df_metadata, model=self._model) - ) - except DebugFailure as e: - components.append( - vm.Card(id=self._page_plan.components_plan[i].component_id, text=f"Failed to build component: {e}") - ) - return components - - @property - def layout(self): - """Property to get layout.""" - if self._layout is None: - self._layout = self._build_layout() - return self._layout - - def _build_layout(self): - if self._page_plan.layout_plan is None: - return None - logger.info(f"{self._page_plan.title} -> Building layout {self._page_plan.layout_plan}") - return self._page_plan.layout_plan.create(model=self._model) - - @property - def controls(self): - """Property to get controls.""" - if self._controls is None: - self._controls = self._build_controls() - return self._controls - - @property - def available_components(self): - """Property to get available components.""" - return [comp.id for comp in self.components if isinstance(comp, (vm.Graph, vm.AgGrid))] - - def _build_controls(self): - controls = [] - logger.info(f"Building controls of page: {self._page_plan.title}") - # Could potentially be parallelized or sent as a batch to the API - for i in trange( - len(self._page_plan.controls_plan), desc=f"Building controls of page: {self._page_plan.title}", leave=False - ): - logger.info(f"{self._page_plan.title} -> Building control {self._page_plan.controls_plan[i]}") - control = self._page_plan.controls_plan[i].create( - model=self._model, available_components=self.available_components, df_metadata=self._df_metadata - ) - if control: - controls.append(control) - - return controls - - @property - def page(self): - """Property to get page.""" - if self._page is None: - page_desc = f"Building page: {self._page_plan.title}" - logger.info(page_desc) - pbar = tqdm(total=5, desc=page_desc) - - title = _execute_step(pbar, page_desc + " --> add title", self._page_plan.title) - components = _execute_step(pbar, page_desc + " --> add components", self.components) - controls = _execute_step(pbar, page_desc + " --> add controls", self.controls) - layout = _execute_step(pbar, page_desc + " --> add layout", self.layout) - - self._page = vm.Page(title=title, components=components, controls=controls, layout=layout) - _execute_step(pbar, page_desc + " --> done", None) - pbar.close() - return self._page diff --git a/vizro-ai/src/vizro_ai/dashboard/nodes/plan.py b/vizro-ai/src/vizro_ai/dashboard/nodes/plan.py deleted file mode 100644 index 02a408569..000000000 --- a/vizro-ai/src/vizro_ai/dashboard/nodes/plan.py +++ /dev/null @@ -1,260 +0,0 @@ -"""Module containing the planner functionality.""" - -import logging -from typing import List, Literal, Union - -import vizro.models as vm -from langchain_openai import ChatOpenAI -from vizro.models.types import ComponentType -from vizro_ai.dashboard.utils import DfMetadata - -try: - from pydantic.v1 import BaseModel, Field, ValidationError, create_model, validator -except ImportError: # pragma: no cov - from pydantic import BaseModel, Field, ValidationError, create_model, validator -import numpy as np -from vizro.models._layout import _get_grid_lines, _get_unique_grid_component_ids, _validate_grid_areas -from vizro.tables import dash_ag_grid -from vizro_ai.dashboard.nodes._pydantic_output import _get_pydantic_output - -logger = logging.getLogger(__name__) - -# For unsupported component and control types, how to handle them? -# option 1. Ignore silently -# option 2. Raise a warning and add the warning message into langgraph state. This gives the user transparency on why -# a certain component or control was not created. -# option 3. Raise a warning and suggest additional reference material -component_type = Literal[ - "AgGrid", "Card", "Graph" -] # Complete list: ["AgGrid", "Button", "Card", "Container", "Graph", "Table", "Tabs"] -control_type = Literal["Filter"] # Complete list: ["Filter", "Parameter"] - -# For other models, like ["Accordion", "NavBar"], how to handle them? - - -class ComponentPlan(BaseModel): - """Component plan model.""" - - component_type: component_type - component_description: str = Field( - ..., - description="Description of the component. Include everything that relates to this component. " - "Be as detailed as possible." - "Keep the original relevant description AS IS. Keep any links as original links.", - ) - component_id: str = Field( - pattern=r"^[a-z]+(_[a-z]+)?$", description="Small snake case description of this component." - ) - page_id: str = Field(..., description="The page id where this component will be placed.") - df_name: str = Field( - ..., - description="The name of the dataframe that this component will use. If no dataframe is " - "used, please specify that as N/A.", - ) - - def create(self, model, df_metadata) -> Union[ComponentType, None]: - """Create the component.""" - from vizro_ai import VizroAI - - vizro_ai = VizroAI(model=model) - - if self.component_type == "Graph": - return vm.Graph( - id=self.component_id + "_" + self.page_id, - figure=vizro_ai.plot(df=df_metadata.get_df(self.df_name), user_input=self.component_description), - ) - elif self.component_type == "AgGrid": - return vm.AgGrid(id=self.component_id + "_" + self.page_id, figure=dash_ag_grid(data_frame=self.df_name)) - elif self.component_type == "Card": - return _get_pydantic_output( - query=self.component_description, llm_model=model, result_model=vm.Card, df_info=None - ) - - -# TODO: This is a very basic implementation of the filter proxy model. It needs to be improved. -# TODO: Try use `df_sample` to inform pydantic models like `OptionsType` about available choices. -# Caution: If just use `df_sample` to inform the pydantic model, the choices might not be exhaustive. -def create_filter_proxy(df_cols, available_components) -> BaseModel: - """Create a filter proxy model.""" - - def validate_targets(v): - """Validate the targets.""" - if v not in available_components: - raise ValueError(f"targets must be one of {available_components}") - return v - - def validate_column(v): - """Validate the column.""" - if v not in df_cols: - raise ValueError(f"column must be one of {df_cols}") - return v - - # TODO: properly check this - e.g. what is the best way to ideally dynamically include the available components - # even in the schema - return create_model( - "FilterProxy", - targets=( - List[str], - Field( - ..., - description="Target component to be affected by filter. " - f"Must be one of {available_components}. ALWAYS REQUIRED.", - ), - ), - column=(str, Field(..., description="Column name of DataFrame to filter. ALWAYS REQUIRED.")), - __validators__={ - "validator1": validator("targets", pre=True, each_item=True, allow_reuse=True)(validate_targets), - "validator2": validator("column", allow_reuse=True)(validate_column), - }, - __base__=vm.Filter, - ) - - -class ControlPlan(BaseModel): - """Control plan model.""" - - control_type: control_type - control_description: str = Field( - ..., - description="Description of the control. Include everything that seems to relate to this control." - "Be as detailed as possible. Keep the original relevant description AS IS. If this control is used" - "to control a specific component, include the relevant component details.", - ) - df_name: str = Field( - ..., - description="The name of the dataframe that this component will use. " - "If the dataframe is not used, please specify that.", - ) - - # TODO: there is definitely room for dynamic model creation, e.g. with literals for targets - def create(self, model, available_components, df_metadata): - """Create the control.""" - filter_prompt = ( - f"Create a filter from the following instructions: {self.control_description}. Do not make up " - f"things that are optional and DO NOT configure actions, action triggers or action chains." - f" If no options are specified, leave them out." - ) - try: - _df_schema = df_metadata.get_df_schema(self.df_name) - _df_cols = list(_df_schema.keys()) - # when wrong dataframe name is given - except KeyError: - logger.info(f"Dataframe {self.df_name} not found in metadata, returning default values.") - return None - - try: - result_proxy = create_filter_proxy(df_cols=_df_cols, available_components=available_components) - proxy = _get_pydantic_output( - query=filter_prompt, llm_model=model, result_model=result_proxy, df_info=_df_schema - ) - logger.info( - f"`Control` proxy: {proxy.dict()}" - ) # when wrong column name is given, `AttributeError: 'ValidationError' object has no attribute 'dict'`` - actual = vm.Filter.parse_obj( - proxy.dict( - exclude={"selector": {"id": True, "actions": True, "_add_key": True}, "id": True, "type": True} - ) - ) - # del model_manager._ModelManager__models[proxy.id] # TODO: This is very wrong and needs to change - - except ValidationError as e: - logger.info(f"Build failed for `Control`, returning default values. Error details: {e}") - return None - - return actual - - -# TODO: try switch to inherit from Layout directly, like FilterProxy -class LayoutProxyModel(BaseModel): - """Proxy model for Layout.""" - - grid: List[List[int]] = Field(..., description="Grid specification to arrange components on screen.") - - @validator("grid") - def validate_grid(cls, grid): - """Validate the grid.""" - if len({len(row) for row in grid}) > 1: - raise ValueError("All rows must be of same length.") - - # Validate grid type and values - unique_grid_idx = _get_unique_grid_component_ids(grid) - if 0 not in unique_grid_idx or not np.array_equal(unique_grid_idx, np.arange((unique_grid_idx.max() + 1))): - raise ValueError("Grid must contain consecutive integers starting from 0.") - - # Validates grid areas spanned by components and spaces - component_grid_lines, space_grid_lines = _get_grid_lines(grid) - _validate_grid_areas(component_grid_lines + space_grid_lines) - return grid - - -class LayoutPlan(BaseModel): - """Layout plan model, which only applies to Vizro Components(Graph, AgGrid, Card).""" - - layout_description: str = Field( - ..., - description="Description of the layout of Vizro Components(Graph, AgGrid, Card). " - "Include everything that seems to relate" - " to this layout AS IS. If layout not specified, describe layout as `N/A`.", - ) - layout_grid_template_areas: List[str] = Field( - [], - description="Grid template areas for the layout, which adhere to the grid-template-areas CSS property syntax." - "Each unique string should be used to represent a unique component. If no grid template areas are provided, " - "leave this as an empty list.", - ) - - def create(self, model) -> Union[vm.Layout, None]: - """Create the layout.""" - layout_prompt = ( - f"Create a layout from the following instructions: {self.layout_description}. Do not make up " - f"a layout if not requested. If a layout_grid_template_areas is provided, translate it into " - f"a matrix of integers where each integer represents a component (starting from 0). replace " - f"'.' with -1 to represent empty spaces. Here is the grid template areas: {self.layout_grid_template_areas}" - ) - if self.layout_description == "N/A": - return None - - try: - proxy = _get_pydantic_output( - query=layout_prompt, llm_model=model, result_model=LayoutProxyModel, df_info=None - ) - actual = vm.Layout.parse_obj(proxy.dict(exclude={})) - except (ValidationError, AttributeError) as e: - logger.info(f"Build failed for `Layout`, returning default values. Error details: {e}") - actual = None - - return actual - - -class PagePlanner(BaseModel): - """Page plan model.""" - - title: str = Field( - ..., - description="Title of the page. If no description is provided, " - "make a short and concise title from the components.", - ) - components_plan: List[ComponentPlan] - controls_plan: List[ControlPlan] = Field([], description="Controls of the page.") - layout_plan: LayoutPlan = Field(None, description="Layout of the page.") - - -class DashboardPlanner(BaseModel): - """Dashboard plan model.""" - - title: str = Field( - ..., - description="Title of the dashboard. If no description is provided," - " make a short and concise title from the content of the pages.", - ) - pages: List[PagePlanner] - - -def _get_dashboard_plan( - query: str, - model: Union[ChatOpenAI], - df_metadata: DfMetadata, -) -> DashboardPlanner: - return _get_pydantic_output( - query=query, llm_model=model, result_model=DashboardPlanner, df_info=df_metadata.get_schemas_and_samples() - ) diff --git a/vizro-ai/src/vizro_ai/dashboard/plan/components.py b/vizro-ai/src/vizro_ai/dashboard/plan/components.py new file mode 100644 index 000000000..4662d3a9d --- /dev/null +++ b/vizro-ai/src/vizro_ai/dashboard/plan/components.py @@ -0,0 +1,56 @@ +"""Component plan model.""" + +import logging +from typing import Union + +import vizro.models as vm +from vizro.models.types import ComponentType + +try: + from pydantic.v1 import BaseModel, Field +except ImportError: # pragma: no cov + from pydantic import BaseModel, Field +from vizro.tables import dash_ag_grid +from vizro_ai.dashboard._pydantic_output import _get_pydantic_output +from vizro_ai.dashboard.constants import component_type + +logger = logging.getLogger(__name__) + + +class ComponentPlan(BaseModel): + """Component plan model.""" + + component_type: component_type + component_description: str = Field( + ..., + description="Description of the component. Include everything that relates to this component. " + "Be as detailed as possible." + "Keep the original relevant description AS IS. Keep any links as original links.", + ) + component_id: str = Field( + pattern=r"^[a-z]+(_[a-z]+)?$", description="Small snake case description of this component." + ) + page_id: str = Field(..., description="The page id where this component will be placed.") + df_name: str = Field( + ..., + description="The name of the dataframe that this component will use. If no dataframe is " + "used, please specify that as N/A.", + ) + + def create(self, model, df_metadata) -> Union[ComponentType, None]: + """Create the component.""" + from vizro_ai import VizroAI + + vizro_ai = VizroAI(model=model) + + if self.component_type == "Graph": + return vm.Graph( + id=self.component_id + "_" + self.page_id, + figure=vizro_ai.plot(df=df_metadata.get_df(self.df_name), user_input=self.component_description), + ) + elif self.component_type == "AgGrid": + return vm.AgGrid(id=self.component_id + "_" + self.page_id, figure=dash_ag_grid(data_frame=self.df_name)) + elif self.component_type == "Card": + return _get_pydantic_output( + query=self.component_description, llm_model=model, result_model=vm.Card, df_info=None + ) diff --git a/vizro-ai/src/vizro_ai/dashboard/plan/controls.py b/vizro-ai/src/vizro_ai/dashboard/plan/controls.py new file mode 100644 index 000000000..e1cb14be8 --- /dev/null +++ b/vizro-ai/src/vizro_ai/dashboard/plan/controls.py @@ -0,0 +1,104 @@ +"""Controls plan model.""" + +import logging +from typing import List + +import vizro.models as vm + +try: + from pydantic.v1 import BaseModel, Field, ValidationError, create_model, validator +except ImportError: # pragma: no cov + from pydantic import BaseModel, Field, ValidationError, create_model, validator +from vizro_ai.dashboard._pydantic_output import _get_pydantic_output +from vizro_ai.dashboard.constants import control_type + +logger = logging.getLogger(__name__) + + +def create_filter_proxy(df_cols, available_components) -> BaseModel: + """Create a filter proxy model.""" + + def validate_targets(v): + """Validate the targets.""" + if v not in available_components: + raise ValueError(f"targets must be one of {available_components}") + return v + + def validate_column(v): + """Validate the column.""" + if v not in df_cols: + raise ValueError(f"column must be one of {df_cols}") + return v + + # TODO: properly check this - e.g. what is the best way to ideally dynamically include the available components + # even in the schema + return create_model( + "FilterProxy", + targets=( + List[str], + Field( + ..., + description="Target component to be affected by filter. " + f"Must be one of {available_components}. ALWAYS REQUIRED.", + ), + ), + column=(str, Field(..., description="Column name of DataFrame to filter. ALWAYS REQUIRED.")), + __validators__={ + "validator1": validator("targets", pre=True, each_item=True, allow_reuse=True)(validate_targets), + "validator2": validator("column", allow_reuse=True)(validate_column), + }, + __base__=vm.Filter, + ) + + +class ControlPlan(BaseModel): + """Control plan model.""" + + control_type: control_type + control_description: str = Field( + ..., + description="Description of the control. Include everything that seems to relate to this control." + "Be as detailed as possible. Keep the original relevant description AS IS. If this control is used" + "to control a specific component, include the relevant component details.", + ) + df_name: str = Field( + ..., + description="The name of the dataframe that this component will use. " + "If the dataframe is not used, please specify that.", + ) + + # TODO: there is definitely room for dynamic model creation, e.g. with literals for targets + def create(self, model, available_components, df_metadata): + """Create the control.""" + filter_prompt = ( + f"Create a filter from the following instructions: {self.control_description}. Do not make up " + f"things that are optional and DO NOT configure actions, action triggers or action chains." + f" If no options are specified, leave them out." + ) + try: + _df_schema = df_metadata.get_df_schema(self.df_name) + _df_cols = list(_df_schema.keys()) + # when wrong dataframe name is given + except KeyError: + logger.info(f"Dataframe {self.df_name} not found in metadata, returning default values.") + return None + + try: + result_proxy = create_filter_proxy(df_cols=_df_cols, available_components=available_components) + proxy = _get_pydantic_output( + query=filter_prompt, llm_model=model, result_model=result_proxy, df_info=_df_schema + ) + logger.info( + f"`Control` proxy: {proxy.dict()}" + ) # when wrong column name is given, `AttributeError: 'ValidationError' object has no attribute 'dict'`` + actual = vm.Filter.parse_obj( + proxy.dict( + exclude={"selector": {"id": True, "actions": True, "_add_key": True}, "id": True, "type": True} + ) + ) + + except ValidationError as e: + logger.info(f"Build failed for `Control`, returning default values. Error details: {e}") + return None + + return actual diff --git a/vizro-ai/src/vizro_ai/dashboard/plan/dashboard.py b/vizro-ai/src/vizro_ai/dashboard/plan/dashboard.py new file mode 100644 index 000000000..b20b64b55 --- /dev/null +++ b/vizro-ai/src/vizro_ai/dashboard/plan/dashboard.py @@ -0,0 +1,23 @@ +"""Dashboard plan model.""" + +import logging +from typing import List + +try: + from pydantic.v1 import BaseModel, Field +except ImportError: # pragma: no cov + from pydantic import BaseModel, Field +from vizro_ai.dashboard.plan.page import PagePlanner + +logger = logging.getLogger(__name__) + + +class DashboardPlanner(BaseModel): + """Dashboard plan model.""" + + title: str = Field( + ..., + description="Title of the dashboard. If no description is provided," + " make a short and concise title from the content of the pages.", + ) + pages: List[PagePlanner] diff --git a/vizro-ai/src/vizro_ai/dashboard/plan/layout.py b/vizro-ai/src/vizro_ai/dashboard/plan/layout.py new file mode 100644 index 000000000..7790273d0 --- /dev/null +++ b/vizro-ai/src/vizro_ai/dashboard/plan/layout.py @@ -0,0 +1,78 @@ +"""Layout plan model.""" + +import logging +from typing import List, Union + +import vizro.models as vm + +try: + from pydantic.v1 import BaseModel, Field, ValidationError, validator +except ImportError: # pragma: no cov + from pydantic import BaseModel, Field, ValidationError, validator +import numpy as np +from vizro.models._layout import _get_grid_lines, _get_unique_grid_component_ids, _validate_grid_areas +from vizro_ai.dashboard._pydantic_output import _get_pydantic_output + +logger = logging.getLogger(__name__) + + +# TODO: try switch to inherit from Layout directly, like FilterProxy +class LayoutProxyModel(BaseModel): + """Proxy model for Layout.""" + + grid: List[List[int]] = Field(..., description="Grid specification to arrange components on screen.") + + @validator("grid") + def validate_grid(cls, grid): + """Validate the grid.""" + if len({len(row) for row in grid}) > 1: + raise ValueError("All rows must be of same length.") + + # Validate grid type and values + unique_grid_idx = _get_unique_grid_component_ids(grid) + if 0 not in unique_grid_idx or not np.array_equal(unique_grid_idx, np.arange((unique_grid_idx.max() + 1))): + raise ValueError("Grid must contain consecutive integers starting from 0.") + + # Validates grid areas spanned by components and spaces + component_grid_lines, space_grid_lines = _get_grid_lines(grid) + _validate_grid_areas(component_grid_lines + space_grid_lines) + return grid + + +class LayoutPlan(BaseModel): + """Layout plan model, which only applies to Vizro Components(Graph, AgGrid, Card).""" + + layout_description: str = Field( + ..., + description="Description of the layout of Vizro Components(Graph, AgGrid, Card). " + "Include everything that seems to relate" + " to this layout AS IS. If layout not specified, describe layout as `N/A`.", + ) + layout_grid_template_areas: List[str] = Field( + [], + description="Grid template areas for the layout, which adhere to the grid-template-areas CSS property syntax." + "Each unique string should be used to represent a unique component. If no grid template areas are provided, " + "leave this as an empty list.", + ) + + def create(self, model) -> Union[vm.Layout, None]: + """Create the layout.""" + layout_prompt = ( + f"Create a layout from the following instructions: {self.layout_description}. Do not make up " + f"a layout if not requested. If a layout_grid_template_areas is provided, translate it into " + f"a matrix of integers where each integer represents a component (starting from 0). replace " + f"'.' with -1 to represent empty spaces. Here is the grid template areas: {self.layout_grid_template_areas}" + ) + if self.layout_description == "N/A": + return None + + try: + proxy = _get_pydantic_output( + query=layout_prompt, llm_model=model, result_model=LayoutProxyModel, df_info=None + ) + actual = vm.Layout.parse_obj(proxy.dict(exclude={})) + except (ValidationError, AttributeError) as e: + logger.info(f"Build failed for `Layout`, returning default values. Error details: {e}") + actual = None + + return actual diff --git a/vizro-ai/src/vizro_ai/dashboard/plan/page.py b/vizro-ai/src/vizro_ai/dashboard/plan/page.py new file mode 100644 index 000000000..56a7c4601 --- /dev/null +++ b/vizro-ai/src/vizro_ai/dashboard/plan/page.py @@ -0,0 +1,27 @@ +"""Page plan model.""" + +import logging +from typing import List + +try: + from pydantic.v1 import BaseModel, Field +except ImportError: # pragma: no cov + from pydantic import BaseModel, Field +from vizro_ai.dashboard.plan.components import ComponentPlan +from vizro_ai.dashboard.plan.controls import ControlPlan +from vizro_ai.dashboard.plan.layout import LayoutPlan + +logger = logging.getLogger(__name__) + + +class PagePlanner(BaseModel): + """Page plan model.""" + + title: str = Field( + ..., + description="Title of the page. If no description is provided, " + "make a short and concise title from the components.", + ) + components_plan: List[ComponentPlan] + controls_plan: List[ControlPlan] = Field([], description="Controls of the page.") + layout_plan: LayoutPlan = Field(None, description="Layout of the page.") diff --git a/vizro-ai/src/vizro_ai/dashboard/utils.py b/vizro-ai/src/vizro_ai/dashboard/utils.py index 5071108ea..34f53d55d 100644 --- a/vizro-ai/src/vizro_ai/dashboard/utils.py +++ b/vizro-ai/src/vizro_ai/dashboard/utils.py @@ -8,14 +8,7 @@ import pandas as pd import tqdm.std import vizro.models as vm - -IMPORT_STATEMENTS = ( - "import vizro.plotly.express as px\n" - "from vizro.models.types import capture\n" - "import plotly.graph_objects as go\n" - "from vizro.tables import dash_ag_grid\n" - "import vizro.models as vm\n" -) +from vizro_ai.dashboard.constants import IMPORT_STATEMENTS @dataclass diff --git a/vizro-ai/src/vizro_ai/py.typed b/vizro-ai/src/vizro_ai/py.typed new file mode 100644 index 000000000..512ec7cb8 --- /dev/null +++ b/vizro-ai/src/vizro_ai/py.typed @@ -0,0 +1 @@ + # Marker file for PEP 561 diff --git a/vizro-ai/tests/unit/vizro-ai/dashboard/nodes/conftest.py b/vizro-ai/tests/unit/vizro-ai/dashboard/nodes/conftest.py index 197746759..6c281b0ad 100644 --- a/vizro-ai/tests/unit/vizro-ai/dashboard/nodes/conftest.py +++ b/vizro-ai/tests/unit/vizro-ai/dashboard/nodes/conftest.py @@ -3,7 +3,7 @@ import pytest from langchain.output_parsers import PydanticOutputParser from langchain_community.llms.fake import FakeListLLM -from vizro_ai.dashboard.nodes.plan import ComponentPlan +from vizro_ai.dashboard.plan.components import ComponentPlan class FakeListLLM(FakeListLLM): diff --git a/vizro-ai/tests/unit/vizro-ai/dashboard/nodes/test_model.py b/vizro-ai/tests/unit/vizro-ai/dashboard/nodes/test_model.py index e4fa08621..af3ee05e2 100644 --- a/vizro-ai/tests/unit/vizro-ai/dashboard/nodes/test_model.py +++ b/vizro-ai/tests/unit/vizro-ai/dashboard/nodes/test_model.py @@ -1,5 +1,5 @@ import vizro.models as vm -from vizro_ai.dashboard.nodes._pydantic_output import _get_pydantic_output +from vizro_ai.dashboard._pydantic_output import _get_pydantic_output def test_get_pydantic_output(component_description, fake_llm): diff --git a/vizro-ai/tests/unit/vizro-ai/dashboard/nodes/test_plan.py b/vizro-ai/tests/unit/vizro-ai/dashboard/nodes/test_plan.py index 1bd1e03ae..1fb500209 100644 --- a/vizro-ai/tests/unit/vizro-ai/dashboard/nodes/test_plan.py +++ b/vizro-ai/tests/unit/vizro-ai/dashboard/nodes/test_plan.py @@ -1,5 +1,7 @@ import pytest -from vizro_ai.dashboard.nodes.plan import DashboardPlanner, PagePlanner, create_filter_proxy +from vizro_ai.dashboard.plan.controls import create_filter_proxy +from vizro_ai.dashboard.plan.dashboard import DashboardPlanner +from vizro_ai.dashboard.plan.page import PagePlanner try: from pydantic.v1 import ValidationError