ds-mcp-server 0.1.0__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,2 @@
1
+ """ds-mcp-server — Data Science MCP Server and multi-provider client."""
2
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,106 @@
1
+ """
2
+ Data Exploration Module for the AI Visualization Engine.
3
+ Provides tools for the LLM to inspect dataset columns before plotting.
4
+ """
5
+
6
+ import pandas as pd
7
+
8
+ from .viz_config import MAX_ROWS
9
+ from .viz_utils import load_data_safely, was_last_load_truncated
10
+
11
+
12
+ def get_column_summary_impl(data_file_path: str, column: str) -> str:
13
+ """
14
+ Analyzes a specific column in the dataset and returns a statistical summary.
15
+
16
+ For numeric columns, it calculates the minimum, maximum, mean, and null count.
17
+ For categorical columns, it calculates the number of unique values, lists
18
+ the top 10 unique values, and the null count.
19
+
20
+ Args:
21
+ data_file_path: The absolute path to the data file.
22
+ column: The name of the column to analyze.
23
+
24
+ Returns:
25
+ A formatted string containing the column statistics, or an error message
26
+ if the column cannot be found or analyzed.
27
+ """
28
+ try:
29
+ df = load_data_safely(data_file_path)
30
+
31
+ truncation_note = ""
32
+ if was_last_load_truncated(data_file_path):
33
+ truncation_note = (
34
+ f"\n\nNote: dataset was truncated to the first {MAX_ROWS:,} rows for "
35
+ "memory safety. Mention this in your final summary."
36
+ )
37
+
38
+ if column not in df.columns:
39
+ return f"Error: Column '{column}' not found in the dataset."
40
+
41
+ col_data = df[column]
42
+ null_count = col_data.isna().sum()
43
+
44
+ if pd.api.types.is_numeric_dtype(col_data):
45
+ min_val = col_data.min()
46
+ max_val = col_data.max()
47
+ mean_val = col_data.mean()
48
+
49
+ return (
50
+ f"Numeric Column '{column}': Min={min_val}, Max={max_val}, "
51
+ f"Mean={mean_val:.2f}, Nulls={null_count}{truncation_note}"
52
+ )
53
+ else:
54
+ unique_vals = col_data.unique()
55
+ total_unique = len(unique_vals)
56
+
57
+ if total_unique > 10:
58
+ top_vals = ", ".join(map(str, unique_vals[:10]))
59
+ val_str = f"{top_vals}... (+ {total_unique - 10} more)"
60
+ else:
61
+ val_str = ", ".join(map(str, unique_vals))
62
+
63
+ return (
64
+ f"Categorical Column '{column}': {total_unique} unique values. "
65
+ f"Top values: {val_str}. Nulls={null_count}{truncation_note}"
66
+ )
67
+
68
+ except Exception as e:
69
+ return f"Error analyzing column '{column}': {str(e)}"
70
+
71
+
72
+ def get_all_columns_summary_impl(data_file_path: str) -> str:
73
+ """
74
+ Returns a compact schema of every column: name and type only.
75
+ Intentionally terse to minimise token load on the model.
76
+ Use get_column_summary for detailed stats on a specific column.
77
+ """
78
+ try:
79
+ df = load_data_safely(data_file_path)
80
+
81
+ truncation_note = ""
82
+ if was_last_load_truncated(data_file_path):
83
+ truncation_note = f" (truncated to {MAX_ROWS:,} rows)"
84
+
85
+ numeric_cols = [c for c in df.columns if pd.api.types.is_numeric_dtype(df[c])]
86
+ datetime_cols = [c for c in df.columns if pd.api.types.is_datetime64_any_dtype(df[c])]
87
+ categorical_cols = [c for c in df.columns if c not in numeric_cols and c not in datetime_cols]
88
+
89
+ lines = [
90
+ f"Dataset: {len(df):,} rows × {len(df.columns)} columns{truncation_note}",
91
+ f"Numeric columns ({len(numeric_cols)}): {', '.join(numeric_cols)}",
92
+ ]
93
+ if categorical_cols:
94
+ cat_details = []
95
+ for c in categorical_cols:
96
+ unique_vals = df[c].dropna().unique()
97
+ sample = ", ".join(str(v) for v in sorted(unique_vals, key=str)[:5])
98
+ cat_details.append(f"{c} [{sample}]")
99
+ lines.append(f"Categorical columns ({len(categorical_cols)}): {'; '.join(cat_details)}")
100
+ if datetime_cols:
101
+ lines.append(f"Datetime columns ({len(datetime_cols)}): {', '.join(datetime_cols)}")
102
+
103
+ return "\n".join(lines)
104
+
105
+ except Exception as e:
106
+ return f"Error summarizing columns: {str(e)}"
@@ -0,0 +1,419 @@
1
+ """
2
+ Interactive Plotting Module for the AI Visualization Engine.
3
+ Generates web-ready interactive Plotly charts (.json).
4
+ """
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ import plotly.express as px
9
+ import plotly.graph_objects as go
10
+
11
+ from .viz_utils import (
12
+ _strip_show_calls,
13
+ generate_code_snippet,
14
+ get_plot_path,
15
+ load_data_safely,
16
+ )
17
+
18
+
19
+ def plot_histogram_impl(
20
+ data_file_path: str, column: str, title: str, color_column: str | None = None
21
+ ) -> str:
22
+ """
23
+ Generates and saves an interactive Plotly histogram.
24
+ """
25
+ try:
26
+ df = load_data_safely(data_file_path)
27
+ if column not in df.columns:
28
+ return f"Error: Column '{column}' not found in data."
29
+
30
+ if color_column and color_column not in df.columns:
31
+ color_column = None
32
+
33
+ fig = px.histogram(
34
+ df,
35
+ x=column,
36
+ color=color_column,
37
+ title=title,
38
+ template="plotly_white"
39
+ )
40
+
41
+ plot_path = get_plot_path(data_file_path, f"hist_{column}", ext=".html")
42
+ fig.write_html(plot_path, include_plotlyjs="cdn")
43
+
44
+ color_arg = f", color='{color_column}'" if color_column else ""
45
+ code_logic = (
46
+ f"fig = px.histogram(df, x='{column}'{color_arg}, "
47
+ f"title='{title}', template='plotly_white')"
48
+ )
49
+ code = generate_code_snippet(code_logic, data_file_path)
50
+
51
+ return f"{plot_path}|||{code}"
52
+ except Exception as e:
53
+ return f"Error plotting interactive histogram: {str(e)}"
54
+
55
+
56
+ def plot_scatterplot_impl(
57
+ data_file_path: str,
58
+ x_column: str,
59
+ y_column: str,
60
+ title: str,
61
+ color_column: str | None = None,
62
+ ) -> str:
63
+ """
64
+ Generates and saves an interactive Plotly scatter plot.
65
+ """
66
+ try:
67
+ df = load_data_safely(data_file_path)
68
+ if x_column not in df.columns or y_column not in df.columns:
69
+ return f"Error: Columns '{x_column}' or '{y_column}' not found."
70
+
71
+ if color_column and color_column not in df.columns:
72
+ color_column = None
73
+
74
+ fig = px.scatter(
75
+ df,
76
+ x=x_column,
77
+ y=y_column,
78
+ color=color_column,
79
+ title=title,
80
+ template="plotly_white",
81
+ )
82
+
83
+ plot_path = get_plot_path(
84
+ data_file_path, f"scatter_{x_column}_vs_{y_column}", ext=".html"
85
+ )
86
+ fig.write_html(plot_path, include_plotlyjs="cdn")
87
+
88
+ color_arg = f", color='{color_column}'" if color_column else ""
89
+ code_logic = (
90
+ f"fig = px.scatter(df, x='{x_column}', y='{y_column}'{color_arg}, "
91
+ f"title='{title}', template='plotly_white')"
92
+ )
93
+ code = generate_code_snippet(code_logic, data_file_path)
94
+
95
+ return f"{plot_path}|||{code}"
96
+ except Exception as e:
97
+ return f"Error plotting interactive scatterplot: {str(e)}"
98
+
99
+
100
+ def plot_boxplot_impl(
101
+ data_file_path: str,
102
+ x_column: str,
103
+ y_column: str,
104
+ title: str,
105
+ color_column: str | None = None,
106
+ ) -> str:
107
+ """
108
+ Generates and saves an interactive Plotly box plot.
109
+ """
110
+ try:
111
+ df = load_data_safely(data_file_path)
112
+ if x_column not in df.columns or y_column not in df.columns:
113
+ return f"Error: Columns '{x_column}' or '{y_column}' not found."
114
+
115
+ if color_column and color_column not in df.columns:
116
+ color_column = None
117
+
118
+ fig = px.box(
119
+ df,
120
+ x=x_column,
121
+ y=y_column,
122
+ color=color_column,
123
+ title=title,
124
+ template="plotly_white",
125
+ )
126
+
127
+ plot_path = get_plot_path(
128
+ data_file_path, f"boxplot_{y_column}_by_{x_column}", ext=".html"
129
+ )
130
+ fig.write_html(plot_path, include_plotlyjs="cdn")
131
+
132
+ color_arg = f", color='{color_column}'" if color_column else ""
133
+ code_logic = (
134
+ f"fig = px.box(df, x='{x_column}', y='{y_column}'{color_arg}, "
135
+ f"title='{title}', template='plotly_white')"
136
+ )
137
+ code = generate_code_snippet(code_logic, data_file_path)
138
+
139
+ return f"{plot_path}|||{code}"
140
+ except Exception as e:
141
+ return f"Error plotting interactive boxplot: {str(e)}"
142
+
143
+
144
+ def plot_lineplot_impl(
145
+ data_file_path: str,
146
+ x_column: str,
147
+ y_column: str,
148
+ title: str,
149
+ color_column: str | None = None,
150
+ ) -> str:
151
+ """
152
+ Generates and saves an interactive Plotly line plot.
153
+ """
154
+ try:
155
+ df = load_data_safely(data_file_path)
156
+ if x_column not in df.columns or y_column not in df.columns:
157
+ return f"Error: Columns '{x_column}' or '{y_column}' not found."
158
+
159
+ if color_column and color_column not in df.columns:
160
+ color_column = None
161
+
162
+ fig = px.line(
163
+ df,
164
+ x=x_column,
165
+ y=y_column,
166
+ color=color_column,
167
+ title=title,
168
+ template="plotly_white",
169
+ )
170
+
171
+ plot_path = get_plot_path(
172
+ data_file_path, f"lineplot_{y_column}_over_{x_column}", ext=".html"
173
+ )
174
+ fig.write_html(plot_path, include_plotlyjs="cdn")
175
+
176
+ color_arg = f", color='{color_column}'" if color_column else ""
177
+ code_logic = (
178
+ f"fig = px.line(df, x='{x_column}', y='{y_column}'{color_arg}, "
179
+ f"title='{title}', template='plotly_white')"
180
+ )
181
+ code = generate_code_snippet(code_logic, data_file_path)
182
+
183
+ return f"{plot_path}|||{code}"
184
+ except Exception as e:
185
+ return f"Error plotting interactive lineplot: {str(e)}"
186
+
187
+
188
+ def plot_barchart_impl(
189
+ data_file_path: str,
190
+ x_column: str,
191
+ y_column: str,
192
+ title: str,
193
+ color_column: str | None = None,
194
+ aggregation: str = "mean",
195
+ ) -> str:
196
+ """
197
+ Generates and saves an interactive Plotly bar chart.
198
+
199
+ Args:
200
+ aggregation: How to aggregate y values per x category ('mean', 'sum', 'count', 'median').
201
+ """
202
+ try:
203
+ df = load_data_safely(data_file_path)
204
+ if x_column not in df.columns or y_column not in df.columns:
205
+ return f"Error: Columns '{x_column}' or '{y_column}' not found."
206
+
207
+ if color_column and color_column not in df.columns:
208
+ color_column = None
209
+
210
+ valid_aggs = {"mean", "sum", "count", "median"}
211
+ if aggregation not in valid_aggs:
212
+ aggregation = "mean"
213
+
214
+ agg_fn = getattr(df.groupby([x_column] + ([color_column] if color_column else []))[y_column], aggregation)
215
+ agg_df = agg_fn().reset_index()
216
+
217
+ color_arg = f", color='{color_column}'" if color_column else ""
218
+ fig = px.bar(
219
+ agg_df,
220
+ x=x_column,
221
+ y=y_column,
222
+ color=color_column,
223
+ barmode="group" if color_column else "relative",
224
+ title=title,
225
+ template="plotly_white",
226
+ )
227
+
228
+ plot_path = get_plot_path(
229
+ data_file_path, f"bar_{y_column}_by_{x_column}", ext=".html"
230
+ )
231
+ fig.write_html(plot_path, include_plotlyjs="cdn")
232
+
233
+ code_logic = (
234
+ f"agg_df = df.groupby(['{x_column}'{(', ' + repr(color_column)) if color_column else ''}])"
235
+ f"['{y_column}'].{aggregation}().reset_index()\n"
236
+ f"fig = px.bar(agg_df, x='{x_column}', y='{y_column}'{color_arg},\n"
237
+ f" barmode='group', title='{title}', template='plotly_white')"
238
+ )
239
+ code = generate_code_snippet(code_logic, data_file_path)
240
+ return f"{plot_path}|||{code}"
241
+ except Exception as e:
242
+ return f"Error plotting interactive bar chart: {str(e)}"
243
+
244
+
245
+ def plot_scatter_matrix_impl(
246
+ data_file_path: str,
247
+ columns: str,
248
+ title: str,
249
+ color_column: str | None = None,
250
+ ) -> str:
251
+ """
252
+ Generates and saves an interactive Plotly scatter matrix (pair plot equivalent).
253
+
254
+ Args:
255
+ columns: Comma-separated list of numeric column names to include.
256
+ color_column: Optional categorical column to colour points by (e.g. 'diagnosis').
257
+ """
258
+ try:
259
+ df = load_data_safely(data_file_path)
260
+
261
+ col_list = [c.strip() for c in columns.split(",") if c.strip()]
262
+ missing = [c for c in col_list if c not in df.columns]
263
+ if missing:
264
+ return f"Error: columns not found in data: {', '.join(missing)}"
265
+
266
+ if color_column and color_column not in df.columns:
267
+ color_column = None
268
+
269
+ fig = px.scatter_matrix(
270
+ df,
271
+ dimensions=col_list,
272
+ color=color_column,
273
+ title=title,
274
+ template="plotly_white",
275
+ )
276
+ fig.update_traces(diagonal_visible=False, showupperhalf=False)
277
+
278
+ plot_path = get_plot_path(
279
+ data_file_path, f"scatter_matrix_{'_'.join(col_list[:3])}", ext=".html"
280
+ )
281
+ fig.write_html(plot_path, include_plotlyjs="cdn")
282
+
283
+ color_arg = f", color='{color_column}'" if color_column else ""
284
+ code_logic = (
285
+ f"cols = {col_list!r}\n"
286
+ f"fig = px.scatter_matrix(df, dimensions=cols{color_arg},\n"
287
+ f" title='{title}', template='plotly_white')\n"
288
+ "fig.update_traces(diagonal_visible=False, showupperhalf=False)"
289
+ )
290
+ code = generate_code_snippet(code_logic, data_file_path)
291
+ return f"{plot_path}|||{code}"
292
+ except Exception as e:
293
+ return f"Error plotting scatter matrix: {str(e)}"
294
+
295
+
296
+ def plot_correlation_heatmap_impl(
297
+ data_file_path: str,
298
+ title: str,
299
+ method: str = "pearson",
300
+ column_filter: str | None = None,
301
+ ) -> str:
302
+ """
303
+ Generates an interactive Plotly correlation heatmap.
304
+
305
+ Args:
306
+ column_filter: Optional comma-separated column names or suffix patterns
307
+ (e.g. "_mean" to select only columns ending in _mean). When omitted,
308
+ all numeric columns are used.
309
+ """
310
+ try:
311
+ df = load_data_safely(data_file_path)
312
+ numeric_df = df.select_dtypes(include="number")
313
+
314
+ if column_filter:
315
+ filters = [f.strip() for f in column_filter.split(",") if f.strip()]
316
+ selected = [
317
+ c for c in numeric_df.columns
318
+ if c in filters or any(c.endswith(f) for f in filters)
319
+ ]
320
+ if len(selected) < 2:
321
+ return (
322
+ f"Error: column_filter '{column_filter}' matched fewer than 2 columns. "
323
+ f"Available numeric columns: {', '.join(numeric_df.columns)}"
324
+ )
325
+ numeric_df = numeric_df[selected]
326
+
327
+ if numeric_df.shape[1] < 2:
328
+ return "Error: Need at least 2 numeric columns to generate a correlation heatmap."
329
+
330
+ corr_matrix = numeric_df.corr(method=method).round(2)
331
+
332
+ fig = px.imshow(
333
+ corr_matrix,
334
+ text_auto=True,
335
+ aspect="auto",
336
+ color_continuous_scale="RdBu_r",
337
+ zmin=-1,
338
+ zmax=1,
339
+ title=title,
340
+ )
341
+ fig.update_layout(template="plotly_white")
342
+
343
+ safe_filter = column_filter.replace(",", "_").replace(" ", "") if column_filter else "all"
344
+ plot_path = get_plot_path(data_file_path, f"corr_heatmap_{safe_filter}_{method}", ext=".html")
345
+ fig.write_html(plot_path, include_plotlyjs="cdn")
346
+
347
+ filter_code = ""
348
+ if column_filter:
349
+ filter_code = (
350
+ f"filters = {repr([f.strip() for f in column_filter.split(',') if f.strip()])}\n"
351
+ f"numeric_df = df.select_dtypes(include='number')\n"
352
+ f"numeric_df = numeric_df[[c for c in numeric_df.columns if c in filters or any(c.endswith(f) for f in filters)]]\n"
353
+ )
354
+ else:
355
+ filter_code = "numeric_df = df.select_dtypes(include='number')\n"
356
+
357
+ code_logic = (
358
+ f"{filter_code}"
359
+ f"corr_matrix = numeric_df.corr(method='{method}').round(2)\n"
360
+ f"fig = px.imshow(corr_matrix, text_auto=True, aspect='auto',\n"
361
+ f" color_continuous_scale='RdBu_r', zmin=-1, zmax=1, title='{title}')\n"
362
+ "fig.update_layout(template='plotly_white')"
363
+ )
364
+ code = generate_code_snippet(code_logic, data_file_path)
365
+ return f"{plot_path}|||{code}"
366
+ except Exception as e:
367
+ return f"Error plotting correlation heatmap: {str(e)}"
368
+
369
+
370
+ def generate_custom_plotly_impl(
371
+ data_file_path: str, python_code: str, plot_filename_keyword: str
372
+ ) -> str:
373
+ """
374
+ Executes custom Python code to generate complex interactive Plotly charts.
375
+ """
376
+ try:
377
+ df = load_data_safely(data_file_path)
378
+
379
+ # Local scope for `exec`.
380
+ # Pass as a single dict (used as both globals and locals) so that nested
381
+ # scopes like list comprehensions can also resolve `df`, `pd`, etc.
382
+ # Using exec(code, {}, locals) would make injected names invisible inside
383
+ # comprehensions and function defs due to Python 3's exec scoping rules.
384
+ local_scope = {
385
+ "pd": pd,
386
+ "px": px,
387
+ "go": go,
388
+ "np": np,
389
+ "df": df,
390
+ # data_file_path is intentionally NOT exposed: the model should use df
391
+ # directly and must not call pd.read_csv() or reference file paths.
392
+ }
393
+
394
+ # Strip markdown formatting and any fig.show() / plt.show() calls before exec.
395
+ clean_code = python_code.replace("```python", "").replace("```", "").strip()
396
+ clean_code = _strip_show_calls(clean_code)
397
+
398
+ # Execute the custom code
399
+ exec(clean_code, local_scope)
400
+
401
+ # The system prompt enforces that the LLM must assign the output to 'fig'
402
+ if "fig" not in local_scope:
403
+ return (
404
+ "Error: Your code must assign the Plotly object to a variable "
405
+ "named 'fig'."
406
+ )
407
+
408
+ fig = local_scope["fig"]
409
+ plot_path = get_plot_path(
410
+ data_file_path, f"custom_{plot_filename_keyword}", ext=".html"
411
+ )
412
+ fig.write_html(plot_path, include_plotlyjs="cdn")
413
+
414
+ full_user_code = generate_code_snippet(clean_code, data_file_path)
415
+
416
+ return f"{plot_path}|||{full_user_code}"
417
+
418
+ except Exception as e:
419
+ return f"Error executing custom plotly code: {str(e)}"