vizro-mcp 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,180 @@
1
+ """Prompts for the Vizro MCP."""
2
+ # ruff: noqa: E501 #Ignore line length only in prompts
3
+
4
+ from typing import Literal, Optional
5
+
6
+ import vizro
7
+ import vizro.models as vm
8
+
9
+ from vizro_mcp._utils.configs import SAMPLE_DASHBOARD_CONFIG
10
+
11
+ CHART_INSTRUCTIONS = """
12
+ IMPORTANT:
13
+ - ALWAYS VALIDATE:if you iterate over a valid produced solution, make sure to ALWAYS call the `validate_chart_code` tool to validate the chart code, display the figure code to the user
14
+ - DO NOT modify the background (with plot_bgcolor) or color sequences unless explicitly asked for
15
+
16
+ Instructions for creating a Vizro chart:
17
+ - analyze the datasets needed for the chart using the `load_and_analyze_data` tool OR by any other data analysis means available to you; the most important information here are the column names and column types
18
+ - if the user provides no data, but you need to display a chart or table, use the `get_sample_data_info` tool to get sample data information
19
+ - create a chart using plotly express and/or plotly graph objects, and call the function `custom_chart`
20
+ - call the validate_chart_code tool to validate the chart code, display the figure code to the user (as artifact)
21
+ - do NOT call any other tool after, especially do NOT create a dashboard
22
+ """
23
+
24
+ STANDARD_INSTRUCTIONS = """
25
+ IMPORTANT:
26
+ - ALWAYS VALIDATE: if you iterate over a valid produced solution, make sure to ALWAYS call the `validate_dashboard_config` tool again to ensure the solution is still valid
27
+ - DO NOT show any code or config to the user until you have validated the solution, do not say you are preparing a solution, just do it and validate it
28
+ - ALWAYS CHECK SCHEMA: to start with, or when stuck, try enquiring the schema of the component in question with the `get_model_json_schema` tool (available models see below)
29
+
30
+ - IF the user has no plan (ie no components or pages), use the config at the bottom of this prompt, OTHERWISE:
31
+ - make a plan of what components you would like to use, then request all necessary schemas using the `get_model_json_schema` tool (start with `Dashboard`, and don't forget `Graph`)
32
+ - assemble your components into a page, then add the page or pages to a dashboard, DO NOT show config or code to the user until you have validated the solution
33
+ - ALWAYS validate the dashboard configuration using the `validate_dashboard_config` tool
34
+ - using `custom_chart` is encouraged for advanced visualizations, no need to call the planner tool in advanced mode
35
+ """
36
+
37
+ IDE_INSTRUCTIONS = """
38
+ - after validation, add the python code to `app.py` with the following code:
39
+ ```python
40
+ app = Vizro().build(dashboard)
41
+ if __name__ == "__main__":
42
+ app.run(debug=True, port=8050)
43
+ - you MUST use the code from the validation tool, do not modify it, watch out for differences to previous `app.py`
44
+ """
45
+
46
+ GENERIC_HOST_INSTRUCTIONS = """
47
+ - you should call the `validate_dashboard_config` tool to validate the solution, and unless
48
+ otherwise specified, open the dashboard in the browser
49
+ - if you cannot open the dashboard in the browser, communicate this to the user, provide them with the python code
50
+ instead and explain how to run it
51
+ """
52
+
53
+ # This dict is used to give the model and overview of what is available in the vizro.models namespace.
54
+ # It helps it to narrow down the choices when asking for a model.
55
+ MODEL_GROUPS: dict[str, list[type[vm.VizroBaseModel]]] = {
56
+ "main": [vm.Dashboard, vm.Page],
57
+ "components": [vm.Card, vm.Button, vm.Text, vm.Container, vm.Tabs, vm.Graph, vm.AgGrid], #'Figure', 'Table'
58
+ "layouts": [vm.Grid, vm.Flex],
59
+ "controls": [vm.Filter, vm.Parameter],
60
+ "selectors": [
61
+ vm.Dropdown,
62
+ vm.RadioItems,
63
+ vm.Checklist,
64
+ vm.DatePicker,
65
+ vm.Slider,
66
+ vm.RangeSlider,
67
+ vm.DatePicker,
68
+ ],
69
+ "navigation": [vm.Navigation, vm.NavBar, vm.NavLink],
70
+ "additional_info": [vm.Tooltip],
71
+ }
72
+
73
+
74
+ def get_overview_vizro_models() -> dict[str, list[dict[str, str]]]:
75
+ """Get all available models in the vizro.models namespace.
76
+
77
+ Returns:
78
+ Dictionary with categories of models and their descriptions
79
+ """
80
+ result: dict[str, list[dict[str, str]]] = {}
81
+ for category, models_list in MODEL_GROUPS.items():
82
+ result[category] = [
83
+ {
84
+ "name": model_class.__name__,
85
+ "description": (model_class.__doc__ or "No description available").split("\n")[0],
86
+ }
87
+ for model_class in models_list
88
+ ]
89
+ return result
90
+
91
+
92
+ def get_dashboard_instructions(
93
+ advanced_mode: bool = False, user_host: Literal["generic_host", "ide"] = "generic_host"
94
+ ) -> str:
95
+ """Get instructions for creating a Vizro dashboard in an IDE/editor."""
96
+ if not advanced_mode:
97
+ return f"""
98
+ {STANDARD_INSTRUCTIONS}
99
+
100
+ {IDE_INSTRUCTIONS if user_host == "ide" else GENERIC_HOST_INSTRUCTIONS}
101
+
102
+ Models you can use:
103
+ {get_overview_vizro_models()}
104
+
105
+ Very simple dashboard config:
106
+ {SAMPLE_DASHBOARD_CONFIG}
107
+ """
108
+ else:
109
+ return f"""
110
+ Instructions for going beyond the basic dashboard:
111
+ - ensure that you have called the `get_vizro_chart_or_dashboard_plan` tool with advanced_mode=False first
112
+ - communicate to the user that you are going to use Python code to create the dashboard, and that they will have to run the code themselves
113
+ - search the web for more information about the components you are using, if you cannot search the web communicate this to the user, and tell them that this is a current limitation of the tool
114
+ - good websites for searching are:
115
+ - general overview on how to extend Vizro: https://vizro.readthedocs.io/en/{vizro.__version__}/pages/user-guides/extensions/
116
+ - custom (Dash) components: https://vizro.readthedocs.io/en/{vizro.__version__}/pages/user-guides/custom-components/
117
+ - custom CSS (for enhanced styling): https://vizro.readthedocs.io/en/{vizro.__version__}/pages/user-guides/custom-css/
118
+ - if stuck, return to a JSON based config, and call the `validate_dashboard_config` tool to validate the solution!!
119
+ """
120
+
121
+
122
+ def get_starter_dashboard_prompt() -> str:
123
+ """Get a prompt for creating a super simple Vizro dashboard."""
124
+ return f"""
125
+ Create a super simple Vizro dashboard with one page and one chart and one filter:
126
+ - No need to call any tools except for `validate_dashboard_config`
127
+ - Call this tool with the precise config as shown below
128
+ - The PyCafe link will be automatically opened in your default browser
129
+ - THEN show the python code after validation, but do not show the PyCafe link
130
+ - Be concise, do not explain anything else, just create the dashboard
131
+ - Finally ask the user what they would like to do next, then you can call other tools to get more information, you should then start with the `get_chart_or_dashboard_plan` tool
132
+
133
+ {SAMPLE_DASHBOARD_CONFIG}
134
+ """
135
+
136
+
137
+ def get_dashboard_prompt(file_path_or_url: str, user_context: Optional[str] = None) -> str:
138
+ """Get a prompt for creating a Vizro dashboard."""
139
+ USER_INSTRUCTIONS = f"""
140
+ 3. Create a Vizro dashboard that follows the user context:
141
+ `{user_context}`
142
+ You MUST follow the user context. If you diverge or add, then communicate this to the user.
143
+ """
144
+ FALLBACK_INSTRUCTIONS = """
145
+ 3. Create a Vizro dashboard that follows the below specifications:
146
+ - Make a homepage that uses the Card component to create navigation to the other pages.
147
+ - Overview page: Summary of the dataset using the Text component and the dataset itself using the plain AgGrid component.
148
+ - Distribution page: Visualizing the distribution of all numeric columns using the Graph component with a histogram.
149
+ - use a Parameter that targets the Graph component and the x argument, and you can select the column to be displayed
150
+ - IMPORTANT:remember that you target the chart like: <graph_id>.x and NOT <graph_id>.figure.x
151
+ - do not use any color schemes etc.
152
+ - add filters for all categorical columns
153
+ - Advanced analysis page:
154
+ - use the `custom_charts` feature of the `validate_dashboard_config` tool to create 4 interesting charts
155
+ - put them in a 2x2 Layout, and ensure they look good
156
+ - do not use any color schemes, but ensure that if you use hover, that it works on explicitly light and dark mode
157
+ - use the `Graph` model `title`, but do NOT give the charts a title, that would be redundant
158
+ - Finally, ensure that the Navigation is with a Navbar, and that you select nice icons for the Navbar.
159
+ """
160
+ return f"""
161
+ Create a dashboard based on the following dataset: `{file_path_or_url}`. Proceed as follows:
162
+ 1. Analyze the data using the `load_and_analyze_data` tool first, passing the file path or github url `{file_path_or_url}` to the tool OR by any other data analysis means available to you.
163
+ 2. Get some knowledge about the Vizro dashboard process by calling the `get_vizro_chart_or_dashboard_plan` tool AND the `get_model_json_schema` (start with `Graph`, `AgGrid`, `Card`, `Navigation`) tool.
164
+ {USER_INSTRUCTIONS if user_context else FALLBACK_INSTRUCTIONS}
165
+ """
166
+
167
+
168
+ def get_chart_prompt(file_path_or_url: str, user_context: Optional[str] = None) -> str:
169
+ """Get a prompt for creating a Vizro chart."""
170
+ FALLBACK_INSTRUCTIONS = """
171
+ - Think what chart could reflect this data-set best, ideally the chart shows some insights about the data-set
172
+ - come up with a few options, but only write code for the best one, communicate the others as options to the user
173
+ """
174
+ return f"""
175
+ Create a Vizro chart using the following instructins:
176
+ - Make sure to analyze the data using the `load_and_analyze_data` tool using the file path or github url `{file_path_or_url}` OR by any other data analysis means available to you.
177
+ - Create a chart using the following description:
178
+ {user_context if user_context else FALLBACK_INSTRUCTIONS}
179
+ - Then you MUST use the `validate_chart_code` tool to validate the chart code.
180
+ """
vizro_mcp/_utils/utils.py CHANGED
@@ -7,11 +7,19 @@ import json
7
7
  import re
8
8
  from dataclasses import dataclass
9
9
  from pathlib import Path
10
- from typing import Any, Literal, Optional, Union
10
+ from typing import TYPE_CHECKING, Literal, Optional, Union
11
11
  from urllib.parse import quote, urlencode
12
12
 
13
13
  import pandas as pd
14
+ import vizro
14
15
  import vizro.models as vm
16
+ from pydantic.json_schema import GenerateJsonSchema
17
+ from vizro.models._base import _format_and_lint
18
+
19
+ from vizro_mcp._utils.configs import DFInfo, DFMetaData
20
+
21
+ if TYPE_CHECKING:
22
+ from vizro_mcp._schemas.schemas import ChartPlan
15
23
 
16
24
  # PyCafe URL for Vizro snippets
17
25
  PYCAFE_URL = "https://py.cafe"
@@ -23,139 +31,6 @@ class VizroCodeAndPreviewLink:
23
31
  pycafe_url: str
24
32
 
25
33
 
26
- @dataclass
27
- class DFMetaData:
28
- file_name: str
29
- file_path_or_url: str
30
- file_location_type: Literal["local", "remote"]
31
- read_function_string: Literal["pd.read_csv", "pd.read_json", "pd.read_html", "pd.read_parquet", "pd.read_excel"]
32
- column_names_types: Optional[dict[str, str]] = None
33
-
34
-
35
- @dataclass
36
- class DFInfo:
37
- general_info: str
38
- sample: dict[str, Any]
39
-
40
-
41
- IRIS = DFMetaData(
42
- file_name="iris_data",
43
- file_path_or_url="https://raw.githubusercontent.com/plotly/datasets/master/iris-id.csv",
44
- file_location_type="remote",
45
- read_function_string="pd.read_csv",
46
- column_names_types={
47
- "sepal_length": "float",
48
- "sepal_width": "float",
49
- "petal_length": "float",
50
- "petal_width": "float",
51
- "species": "str",
52
- },
53
- )
54
-
55
- TIPS = DFMetaData(
56
- file_name="tips_data",
57
- file_path_or_url="https://raw.githubusercontent.com/plotly/datasets/master/tips.csv",
58
- file_location_type="remote",
59
- read_function_string="pd.read_csv",
60
- column_names_types={
61
- "total_bill": "float",
62
- "tip": "float",
63
- "sex": "str",
64
- "smoker": "str",
65
- "day": "str",
66
- "time": "str",
67
- "size": "int",
68
- },
69
- )
70
-
71
- STOCKS = DFMetaData(
72
- file_name="stocks_data",
73
- file_path_or_url="https://raw.githubusercontent.com/plotly/datasets/master/stockdata.csv",
74
- file_location_type="remote",
75
- read_function_string="pd.read_csv",
76
- column_names_types={
77
- "Date": "str",
78
- "IBM": "float",
79
- "MSFT": "float",
80
- "SBUX": "float",
81
- "AAPL": "float",
82
- "GSPC": "float",
83
- },
84
- )
85
-
86
- GAPMINDER = DFMetaData(
87
- file_name="gapminder_data",
88
- file_path_or_url="https://raw.githubusercontent.com/plotly/datasets/master/gapminder_unfiltered.csv",
89
- file_location_type="remote",
90
- read_function_string="pd.read_csv",
91
- column_names_types={
92
- "country": "str",
93
- "continent": "str",
94
- "year": "int",
95
- "lifeExp": "float",
96
- "pop": "int",
97
- "gdpPercap": "float",
98
- },
99
- )
100
-
101
- SAMPLE_DASHBOARD_CONFIG = """
102
- {
103
- `config`: {
104
- `pages`: [
105
- {
106
- `title`: `Iris Data Analysis`,
107
- `controls`: [
108
- {
109
- `id`: `species_filter`,
110
- `type`: `filter`,
111
- `column`: `species`,
112
- `targets`: [
113
- `scatter_plot`
114
- ],
115
- `selector`: {
116
- `type`: `dropdown`,
117
- `multi`: true
118
- }
119
- }
120
- ],
121
- `components`: [
122
- {
123
- `id`: `scatter_plot`,
124
- `type`: `graph`,
125
- `title`: `Sepal Dimensions by Species`,
126
- `figure`: {
127
- `x`: `sepal_length`,
128
- `y`: `sepal_width`,
129
- `color`: `species`,
130
- `_target_`: `scatter`,
131
- `data_frame`: `iris_data`,
132
- `hover_data`: [
133
- `petal_length`,
134
- `petal_width`
135
- ]
136
- }
137
- }
138
- ]
139
- }
140
- ],
141
- `theme`: `vizro_dark`,
142
- `title`: `Iris Dashboard`
143
- },
144
- `data_infos`: `
145
- [
146
- {
147
- \"file_name\": \"iris_data\",
148
- \"file_path_or_url\": \"https://raw.githubusercontent.com/plotly/datasets/master/iris-id.csv\",
149
- \"file_location_type\": \"remote\",
150
- \"read_function_string\": \"pd.read_csv\",
151
- }
152
- ]
153
- `
154
- }
155
-
156
- """
157
-
158
-
159
34
  def convert_github_url_to_raw(path_or_url: str) -> str:
160
35
  """Convert a GitHub URL to a raw URL if it's a GitHub URL, otherwise return the original path or URL."""
161
36
  github_pattern = r"https?://(?:www\.)?github\.com/([^/]+)/([^/]+)/(?:blob|raw)/([^/]+)/(.+)"
@@ -241,7 +116,7 @@ def create_pycafe_url(python_code: str) -> str:
241
116
  # Create JSON object for py.cafe
242
117
  json_object = {
243
118
  "code": python_code,
244
- "requirements": "vizro==0.1.38",
119
+ "requirements": f"vizro=={vizro.__version__}",
245
120
  "files": [],
246
121
  }
247
122
 
@@ -255,26 +130,38 @@ def create_pycafe_url(python_code: str) -> str:
255
130
  return pycafe_url
256
131
 
257
132
 
133
+ # TODO: is this still needed after 0.1.42
134
+ def remove_figure_quotes(code_string: str) -> str:
135
+ """Remove quotes around all figure argument values."""
136
+ return _format_and_lint(re.sub(r'figure="([^"]*)"', r"figure=\1", code_string))
137
+
138
+
258
139
  def get_python_code_and_preview_link(
259
- model_object: vm.VizroBaseModel, data_infos: list[DFMetaData]
140
+ model_object: vm.VizroBaseModel,
141
+ data_infos: list[DFMetaData],
142
+ custom_charts: list["ChartPlan"],
260
143
  ) -> VizroCodeAndPreviewLink:
261
144
  """Get the Python code and preview link for a Vizro model object."""
262
145
  # Get the Python code
263
- python_code = model_object._to_python()
146
+ python_code = model_object._to_python(
147
+ extra_callable_defs={custom_chart.get_chart_code(vizro=True) for custom_chart in custom_charts}
148
+ )
264
149
 
265
- # Add imports after the first empty line
150
+ # Gather all imports (static + custom), deduplicate, and insert at the first empty line
151
+ static_imports = [
152
+ "from vizro import Vizro",
153
+ "import pandas as pd",
154
+ "from vizro.managers import data_manager",
155
+ ]
156
+ custom_imports = [
157
+ imp for custom_chart in custom_charts for imp in custom_chart.get_imports(vizro=True).split("\n") if imp.strip()
158
+ ]
159
+ all_imports = list(dict.fromkeys(static_imports + custom_imports))
266
160
  lines = python_code.splitlines()
267
161
  for i, line in enumerate(lines):
268
162
  if not line.strip():
269
- # Found first empty line, insert imports here
270
- imports_to_add = [
271
- "from vizro import Vizro",
272
- "import pandas as pd",
273
- "from vizro.managers import data_manager",
274
- ]
275
- lines[i:i] = imports_to_add
163
+ lines[i:i] = all_imports
276
164
  break
277
-
278
165
  python_code = "\n".join(lines)
279
166
 
280
167
  # Prepare data loading code
@@ -295,6 +182,36 @@ def get_python_code_and_preview_link(
295
182
  # Add final run line
296
183
  python_code += "\n\nVizro().build(model).run()"
297
184
 
185
+ python_code = remove_figure_quotes(python_code)
186
+
298
187
  pycafe_url = create_pycafe_url(python_code)
299
188
 
300
189
  return VizroCodeAndPreviewLink(python_code=python_code, pycafe_url=pycafe_url)
190
+
191
+
192
+ class NoDefsGenerateJsonSchema(GenerateJsonSchema):
193
+ """Custom schema generator that handles reference cases appropriately."""
194
+
195
+ def generate(self, schema, mode="validation"):
196
+ """Generate schema and resolve references if needed."""
197
+ json_schema = super().generate(schema, mode=mode)
198
+
199
+ # If schema is a reference (has $ref but no properties)
200
+ if "$ref" in json_schema and "properties" not in json_schema:
201
+ # Extract the reference path - typically like "#/$defs/ModelName"
202
+ ref_path = json_schema["$ref"]
203
+ if ref_path.startswith("#/$defs/"):
204
+ model_name = ref_path.split("/")[-1]
205
+ # Get the referenced definition from $defs
206
+ # Simply copy the referenced definition content to the top level
207
+ json_schema.update(json_schema["$defs"][model_name])
208
+ # Remove the $ref since we've resolved it
209
+ json_schema.pop("$ref", None)
210
+
211
+ # Remove the $defs section if it exists
212
+ json_schema.pop("$defs", None)
213
+ return json_schema
214
+
215
+
216
+ # if __name__ == "__main__":
217
+ # print(vm.Dashboard.model_json_schema(schema_generator=NoDefsGenerateJsonSchema).keys())