diff --git a/vizro-ai/changelog.d/20240626_224646_anna_xiong_azure_openai.md b/vizro-ai/changelog.d/20240626_224646_anna_xiong_azure_openai.md new file mode 100644 index 000000000..f1f65e73c --- /dev/null +++ b/vizro-ai/changelog.d/20240626_224646_anna_xiong_azure_openai.md @@ -0,0 +1,48 @@ + + + + + + + + + diff --git a/vizro-ai/changelog.d/20240705_153310_lingyi_zhang_vizroai_models.md b/vizro-ai/changelog.d/20240705_153310_lingyi_zhang_vizroai_models.md new file mode 100644 index 000000000..f1f65e73c --- /dev/null +++ b/vizro-ai/changelog.d/20240705_153310_lingyi_zhang_vizroai_models.md @@ -0,0 +1,48 @@ + + + + + + + + + diff --git a/vizro-ai/docs/pages/user-guides/customize-vizro-ai.md b/vizro-ai/docs/pages/user-guides/customize-vizro-ai.md index 89050991e..963a746d7 100644 --- a/vizro-ai/docs/pages/user-guides/customize-vizro-ai.md +++ b/vizro-ai/docs/pages/user-guides/customize-vizro-ai.md @@ -3,20 +3,35 @@ ## Supported models Vizro-AI currently supports [OpenAI models](https://platform.openai.com/docs/models) as follows, although we are working on supporting more vendors: -- `gpt-3.5-turbo` (default model) -- `gpt-4-turbo` (recommended for best model performance) -- `gpt-3.5-turbo-0125` -- `gpt-3.5-turbo-1106` -- `gpt-4-0613` -- `gpt-4-1106-preview` -- `gpt-3.5-turbo-0613` (to be deprecated on June 13, 2024) +=== "OpenAI" + + - gpt-3.5-turbo `default` + - gpt-4-turbo + - gpt-4o + - gpt-4 + - gpt-4-0613 + - gpt-4-1106-preview + - gpt-4-turbo-2024-04-09 + - gpt-4-turbo-preview + - gpt-4-0125-preview + - gpt-3.5-turbo-1106 + - gpt-3.5-turbo-0125 + - gpt-4o-2024-05-13 + +=== "Anthropic" + + :octicons-hourglass-24: In development + +=== "MistralAI" + + :octicons-hourglass-24: In development These models offer different levels of performance and cost to Vizro-AI users: -* The **gpt-3.5** model series have lower price point and higher speeds for providing answers, but do not offer sophisticated charting. +* The **gpt-3.5** model series have lower price points and faster speeds for providing answers, but do not offer sophisticated charting. -* Consider upgrading to the **gpt-4** models for more demanding tasks. While they are part of a more capable GPT model series, their response time is slower than gpt-3.5 models, and they come at a higher cost. +* Consider upgrading to the **gpt-4** and **gpt-4o** model series for more demanding tasks. While they are part of a more capable GPT model series, they come at a higher cost. Refer to the [OpenAI documentation for more about model capabilities](https://platform.openai.com/docs/models/overview) and [pricing](https://openai.com/pricing). @@ -36,10 +51,10 @@ The example below uses the OpenAI model name in a string form: ```py linenums="1" from vizro_ai import VizroAI - vizro_ai = VizroAI(model="gpt-3.5-turbo-0125") + vizro_ai = VizroAI(model="gpt-4-turbo") ``` -The example below customizes the `ChatOpenAI` instance further beyond the chosen default from the string instantiation. We pass the `"gpt-3.5-turbo-0125"` model from OpenAI as `model_name` for `ChatOpenAI`, which offers improved response accuracy, we also want to increase maximum number of retries. +The example below customizes the `ChatOpenAI` instance further beyond the chosen default from the string instantiation. We pass the `"gpt-4-turbo"` model from OpenAI as `model_name` for `ChatOpenAI`, which offers improved response accuracy, we also want to increase maximum number of retries. It's important to mention that any parameter that could be used in the `openai.create` call is also usable in `ChatOpenAI`. For more customization options for `ChatOpenAI`, refer to the [LangChain ChatOpenAI docs](https://api.python.langchain.com/en/latest/chat_models/langchain_openai.chat_models.base.ChatOpenAI.html) @@ -57,7 +72,7 @@ To ensure a deterministic answer to our queries, we've set the temperature to 0. df = px.data.gapminder() llm = ChatOpenAI( - model_name="gpt-3.5-turbo-0125", + model_name="gpt-4-turbo", temperature=0, max_retries=5, ) diff --git a/vizro-ai/src/vizro_ai/chains/_llm_models.py b/vizro-ai/src/vizro_ai/chains/_llm_models.py index 7c70952f2..9014ad17c 100644 --- a/vizro-ai/src/vizro_ai/chains/_llm_models.py +++ b/vizro-ai/src/vizro_ai/chains/_llm_models.py @@ -3,41 +3,29 @@ from langchain_core.language_models.chat_models import BaseChatModel from langchain_openai import ChatOpenAI -# TODO constant of model inventory, can be converted to yaml and link to docs -PREDEFINED_MODELS: Dict[str, Dict[str, Union[int, BaseChatModel]]] = { - "gpt-3.5-turbo-0613": { - "max_tokens": 4096, - "wrapper": ChatOpenAI, - }, - "gpt-4-0613": { - "max_tokens": 8192, - "wrapper": ChatOpenAI, - }, - "gpt-3.5-turbo-1106": { - "max_tokens": 16385, - "wrapper": ChatOpenAI, - }, - "gpt-4-1106-preview": { - "max_tokens": 128000, - "wrapper": ChatOpenAI, - }, - "gpt-3.5-turbo-0125": { - "max_tokens": 16385, - "wrapper": ChatOpenAI, - }, - "gpt-3.5-turbo": { - "max_tokens": 16385, - "wrapper": ChatOpenAI, - }, - "gpt-4-turbo": { - "max_tokens": 128000, - "wrapper": ChatOpenAI, - }, +SUPPORTED_MODELS = { + "OpenAI": [ + "gpt-4-0613", + "gpt-4", + "gpt-4-1106-preview", + "gpt-4-turbo", + "gpt-4-turbo-2024-04-09", + "gpt-4-turbo-preview", + "gpt-4-0125-preview", + "gpt-3.5-turbo-1106", + "gpt-3.5-turbo-0125", + "gpt-3.5-turbo", + "gpt-4o-2024-05-13", + "gpt-4o", + ] } +DEFAULT_WRAPPER_MAP: Dict[str, BaseChatModel] = {"OpenAI": ChatOpenAI} DEFAULT_MODEL = "gpt-3.5-turbo" DEFAULT_TEMPERATURE = 0 +model_to_vendor = {model: key for key, models in SUPPORTED_MODELS.items() for model in models} + def _get_llm_model(model: Optional[Union[ChatOpenAI, str]] = None) -> BaseChatModel: """Fetches and initializes an instance of the LLM. @@ -54,14 +42,21 @@ def _get_llm_model(model: Optional[Union[ChatOpenAI, str]] = None) -> BaseChatMo """ if not model: return ChatOpenAI(model_name=DEFAULT_MODEL, temperature=DEFAULT_TEMPERATURE) - if isinstance(model, ChatOpenAI): + + if isinstance(model, BaseChatModel): return model - if isinstance(model, str) and model in PREDEFINED_MODELS: - return PREDEFINED_MODELS.get(model)["wrapper"](model_name=model, temperature=DEFAULT_TEMPERATURE) + + if isinstance(model, str): + if any(model in model_list for model_list in SUPPORTED_MODELS.values()): + vendor = model_to_vendor[model] + return DEFAULT_WRAPPER_MAP.get(vendor)(model_name=model, temperature=DEFAULT_TEMPERATURE) + raise ValueError( - f"Model {model} not found! List of available model can be found at https://vizro.readthedocs.io/projects/vizro-ai/en/latest/pages/explanation/faq/#which-llms-are-supported-by-vizro-ai" + f"Model {model} not found! List of available model can be found at https://vizro.readthedocs.io/projects/vizro-ai/en/latest/pages/user-guides/customize-vizro-ai/#supported-models" ) if __name__ == "__main__": - llm_chat_openai = _get_llm_model() + llm_chat_openai = _get_llm_model(model="gpt-3.5-turbo") + print(repr(llm_chat_openai)) # noqa: T201 + print(llm_chat_openai.model_name) # noqa: T201 diff --git a/vizro-core/changelog.d/20240703_091955_huong_li_nguyen_fix_selector_return_type.md b/vizro-core/changelog.d/20240703_091955_huong_li_nguyen_fix_selector_return_type.md new file mode 100644 index 000000000..b1e16724c --- /dev/null +++ b/vizro-core/changelog.d/20240703_091955_huong_li_nguyen_fix_selector_return_type.md @@ -0,0 +1,47 @@ + + + + + + + + +### Fixed + +- Ensure that categorical selectors always return a list of values. ([#562](https://github.com/mckinsey/vizro/pull/562)) + + diff --git a/vizro-core/changelog.d/20240708_105029_huong_li_nguyen_fix_default_nav_items.md b/vizro-core/changelog.d/20240708_105029_huong_li_nguyen_fix_default_nav_items.md new file mode 100644 index 000000000..5beb71ca9 --- /dev/null +++ b/vizro-core/changelog.d/20240708_105029_huong_li_nguyen_fix_default_nav_items.md @@ -0,0 +1,47 @@ + + + + + + + + +### Fixed + +- Remove default icon provision for `vm.NavLink` when the icon count exceeds 9 and a user icon is provided.([#571](https://github.com/mckinsey/vizro/pull/571)) + + diff --git a/vizro-core/examples/_dev/app.py b/vizro-core/examples/_dev/app.py index a69ca412a..77a1e670d 100644 --- a/vizro-core/examples/_dev/app.py +++ b/vizro-core/examples/_dev/app.py @@ -1,69 +1,38 @@ """Dev app to try things out.""" -from typing import Optional - -import dash_bootstrap_components as dbc -import pandas as pd import vizro.models as vm -import vizro.plotly.express as px -from dash import html from vizro import Vizro -from vizro.figures import kpi_card -from vizro.models.types import capture - -tips = px.data.tips - - -@capture("figure") # (1)! -def custom_kpi_card( # noqa: PLR0913 - data_frame: pd.DataFrame, - value_column: str, - *, - value_format: str = "{value}", - agg_func: str = "sum", - title: Optional[str] = None, - icon: Optional[str] = None, -) -> dbc.Card: # (2)! - """Creates a custom KPI card.""" - title = title or f"{agg_func} {value_column}".title() - value = data_frame[value_column].agg(agg_func) - header = dbc.CardHeader( - [ - html.H2(title), - html.P(icon, className="material-symbols-outlined") if icon else None, # (3)! - ] - ) - body = dbc.CardBody([value_format.format(value=value)]) - return dbc.Card([header, body], className="card-kpi") - - -page = vm.Page( - title="Create your own KPI card", - layout=vm.Layout(grid=[[0, 1, -1, -1]] + [[-1, -1, -1, -1]] * 3), # (4)! - components=[ - vm.Figure( - figure=kpi_card( # (5)! - data_frame=tips, - value_column="tip", - value_format="${value:.2f}", - icon="shopping_cart", - title="Default KPI card", - ) - ), - vm.Figure( - figure=custom_kpi_card( # (6)! - data_frame=tips, - value_column="tip", - value_format="${value:.2f}", - icon="payment", - title="Custom KPI card", - ) - ), - ], +page_1 = vm.Page(title="Page 1", components=[vm.Card(text="Placeholder")]) +page_2 = vm.Page(title="Page 2", components=[vm.Card(text="Placeholder")]) +page_3 = vm.Page(title="Page 3", components=[vm.Card(text="Placeholder")]) +page_4 = vm.Page(title="Page 4", components=[vm.Card(text="Placeholder")]) +page_5 = vm.Page(title="Page 5", components=[vm.Card(text="Placeholder")]) +page_6 = vm.Page(title="Page 6", components=[vm.Card(text="Placeholder")]) +page_7 = vm.Page(title="Page 7", components=[vm.Card(text="Placeholder")]) +page_8 = vm.Page(title="Page 8", components=[vm.Card(text="Placeholder")]) +page_9 = vm.Page(title="Page 9", components=[vm.Card(text="Placeholder")]) +page_10 = vm.Page(title="Page 10", components=[vm.Card(text="Placeholder")]) + +dashboard = vm.Dashboard( + pages=[page_1, page_2, page_3, page_4, page_5, page_6, page_7, page_8, page_9, page_10], + navigation=vm.Navigation( + nav_selector=vm.NavBar( + items=[ + vm.NavLink(label="Page 1", pages=["Page 1"], icon="Home"), + vm.NavLink(label="Page 1", pages=["Page 2"], icon="Home"), + vm.NavLink(label="Page 1", pages=["Page 3"], icon="Home"), + vm.NavLink(label="Page 1", pages=["Page 4"], icon="Home"), + vm.NavLink(label="Page 1", pages=["Page 5"], icon="Home"), + vm.NavLink(label="Page 1", pages=["Page 6"], icon="Home"), + vm.NavLink(label="Page 1", pages=["Page 7"], icon="Home"), + vm.NavLink(label="Page 1", pages=["Page 8"], icon="Home"), + vm.NavLink(label="Page 1", pages=["Page 9"], icon="Home"), + vm.NavLink(label="Page 1", pages=["Page 10"], icon="Home"), + ] + ) + ), ) -dashboard = vm.Dashboard(pages=[page]) - if __name__ == "__main__": Vizro().build(dashboard).run() diff --git a/vizro-core/src/vizro/actions/_actions_utils.py b/vizro-core/src/vizro/actions/_actions_utils.py index 6f0c327bb..e7a532f99 100644 --- a/vizro-core/src/vizro/actions/_actions_utils.py +++ b/vizro-core/src/vizro/actions/_actions_utils.py @@ -138,12 +138,19 @@ def _get_parametrized_config(target: ModelID, ctd_parameters: List[CallbackTrigg config["data_frame"] = {} for ctd in ctd_parameters: - selector_value = ctd[ - "value" - ] # TODO: needs to be refactored so that it is independent of implementation details + # TODO: needs to be refactored so that it is independent of implementation details + selector_value = ctd["value"] + if hasattr(selector_value, "__iter__") and ALL_OPTION in selector_value: # type: ignore[operator] selector: SelectorType = model_manager[ctd["id"]] - selector_value = selector.options + + # Even if options are provided as List[Dict], the Dash component only returns a List of values. + # So we need to ensure that we always return a List only as well to provide consistent types. + if all(isinstance(option, dict) for option in selector.options): + selector_value = [option["value"] for option in selector.options] + else: + selector_value = selector.options + selector_value = _validate_selector_value_none(selector_value) selector_actions = _get_component_actions(model_manager[ctd["id"]]) diff --git a/vizro-core/src/vizro/models/_action/_action.py b/vizro-core/src/vizro/models/_action/_action.py index 0285b8459..3d9c1c665 100644 --- a/vizro-core/src/vizro/models/_action/_action.py +++ b/vizro-core/src/vizro/models/_action/_action.py @@ -105,7 +105,7 @@ def _action_callback_function( ) -> Any: logger.debug("===== Running action with id %s, function %s =====", self.id, self.function._function.__name__) if logger.isEnabledFor(logging.DEBUG): - logger.debug("Action inputs:\n%s", pformat(inputs, depth=2, width=200)) + logger.debug("Action inputs:\n%s", pformat(inputs, depth=3, width=200)) logger.debug("Action outputs:\n%s", pformat(outputs, width=200)) if isinstance(inputs, Mapping): diff --git a/vizro-core/src/vizro/models/_navigation/nav_bar.py b/vizro-core/src/vizro/models/_navigation/nav_bar.py index 0f41b94e3..4802b306e 100644 --- a/vizro-core/src/vizro/models/_navigation/nav_bar.py +++ b/vizro-core/src/vizro/models/_navigation/nav_bar.py @@ -48,9 +48,10 @@ def pre_build(self): ] for position, item in enumerate(self.items, 1): - # The filter icons are named filter_1, filter_2, etc. up to filter_9. If there are more than 9 items, then - # the 10th and all subsequent items are named filter_9+. - item.icon = item.icon or f"filter_{position}" if position <= 9 else "filter_9+" # noqa: PLR2004 + # The default icons are named filter_1, filter_2, etc. up to filter_9. + # If there are more than 9 items, then the 10th and all subsequent items are named filter_9+. + icon_default = f"filter_{position}" if position <= 9 else "filter_9+" # noqa: PLR2004 + item.icon = item.icon or icon_default # Since models instantiated in pre_build do not themselves have pre_build called on them, we call it manually # here. diff --git a/vizro-core/tests/unit/vizro/actions/conftest.py b/vizro-core/tests/unit/vizro/actions/conftest.py index 2adf94ae8..d8ecbdaa2 100644 --- a/vizro-core/tests/unit/vizro/actions/conftest.py +++ b/vizro-core/tests/unit/vizro/actions/conftest.py @@ -10,6 +10,11 @@ def gapminder_2007(gapminder): return gapminder.query("year == 2007") +@pytest.fixture +def iris(): + return px.data.iris() + + @pytest.fixture def gapminder_dynamic_first_n_last_n_function(gapminder): return lambda first_n=None, last_n=None: ( @@ -44,6 +49,16 @@ def scatter_chart(gapminder_2007, scatter_params): return px.scatter(gapminder_2007, **scatter_params).update_layout(margin_t=24) +@pytest.fixture +def scatter_matrix_params(): + return {"dimensions": ["sepal_width", "sepal_length", "petal_width", "petal_length"]} + + +@pytest.fixture +def scatter_matrix_chart(iris, scatter_matrix_params): + return px.scatter_matrix(iris, **scatter_matrix_params).update_layout(margin_t=24) + + @pytest.fixture def scatter_chart_dynamic_data_frame(scatter_params): return px.scatter("gapminder_dynamic_first_n_last_n", **scatter_params).update_layout(margin_t=24) @@ -110,3 +125,16 @@ def managers_one_page_two_graphs_one_table_one_aggrid_one_button( ], ) Vizro._pre_build() + + +@pytest.fixture +def managers_one_page_one_graph_with_dict_param_input(scatter_matrix_chart): + """Instantiates a model_manager and data_manager with a page and a graph that requires a list input.""" + vm.Page( + id="test_page", + title="My first dashboard", + components=[ + vm.Graph(id="scatter_matrix_chart", figure=scatter_matrix_chart), + ], + ) + Vizro._pre_build() diff --git a/vizro-core/tests/unit/vizro/actions/test_parameter_action.py b/vizro-core/tests/unit/vizro/actions/test_parameter_action.py index d4677e865..4db90f879 100644 --- a/vizro-core/tests/unit/vizro/actions/test_parameter_action.py +++ b/vizro-core/tests/unit/vizro/actions/test_parameter_action.py @@ -15,6 +15,13 @@ def target_scatter_parameter_y(request, gapminder_2007, scatter_params): return px.scatter(gapminder_2007, **scatter_params).update_layout(margin_t=24) +@pytest.fixture +def target_scatter_matrix_parameter_dimensions(request, iris, scatter_matrix_params): + dimensions = request.param + scatter_matrix_params["dimensions"] = dimensions + return px.scatter_matrix(iris, **scatter_matrix_params).update_layout(margin_t=24) + + @pytest.fixture def target_scatter_parameter_hover_data(request, gapminder_2007, scatter_params): hover_data = request.param @@ -95,6 +102,38 @@ def ctx_parameter_y(request): return context_value +@pytest.fixture +def ctx_parameter_dimensions(request): + """Mock dash.ctx that represents `dimensions` Parameter value selection.""" + y = request.param + mock_ctx = { + "args_grouping": { + "external": { + "filter_interaction": [], + "filters": [], + "parameters": [ + CallbackTriggerDict( + id="dimensions_parameter", + property="value", + value=y, + str_id="dimensions_parameter", + triggered=False, + ) + ], + "theme_selector": CallbackTriggerDict( + id="theme_selector", + property="checked", + value=False, + str_id="theme_selector", + triggered=False, + ), + } + } + } + context_value.set(AttributeDict(**mock_ctx)) + return context_value + + @pytest.fixture def ctx_parameter_hover_data(request): """Mock dash.ctx that represents hover_data Parameter value selection.""" @@ -497,3 +536,36 @@ def test_data_frame_parameters_multiple_targets( } assert result == expected + + @pytest.mark.usefixtures("managers_one_page_one_graph_with_dict_param_input") + @pytest.mark.parametrize( + "ctx_parameter_dimensions, target_scatter_matrix_parameter_dimensions", + [("ALL", ["sepal_length", "sepal_width", "petal_length", "petal_width"]), (["sepal_width"], ["sepal_width"])], + indirect=True, + ) + def test_one_parameter_with_dict_input_as_options( + self, ctx_parameter_dimensions, target_scatter_matrix_parameter_dimensions + ): + # If the options are provided as a list of dictionaries, the value should be correctly passed to the + # target as a list. So when "ALL" is selected, a list of all possible values should be returned. + dimensions_parameter = vm.Parameter( + id="test_parameter_dimensions", + targets=["scatter_matrix_chart.dimensions"], + selector=vm.RadioItems( + id="dimensions_parameter", + options=[ + {"label": "sepal_length", "value": "sepal_length"}, + {"label": "sepal_width", "value": "sepal_width"}, + {"label": "petal_length", "value": "petal_length"}, + {"label": "petal_width", "value": "petal_width"}, + ], + ), + ) + model_manager["test_page"].controls = [dimensions_parameter] + dimensions_parameter.pre_build() + + # Run action by picking the above added action function and executing it with () + result = model_manager[f"{PARAMETER_ACTION_PREFIX}_test_parameter_dimensions"].function() + expected = {"scatter_matrix_chart": target_scatter_matrix_parameter_dimensions} + + assert result == expected