mirror of
https://github.com/apache/superset.git
synced 2026-05-12 19:35:17 +00:00
feat(mcp): add Handlebars chart type support to MCP service (#38402)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@ from superset.mcp_service.chart.schemas import (
|
||||
ChartSemantics,
|
||||
ColumnRef,
|
||||
FilterConfig,
|
||||
HandlebarsChartConfig,
|
||||
MixedTimeseriesChartConfig,
|
||||
PieChartConfig,
|
||||
PivotTableChartConfig,
|
||||
@@ -309,7 +310,8 @@ def map_config_to_form_data(
|
||||
| XYChartConfig
|
||||
| PieChartConfig
|
||||
| PivotTableChartConfig
|
||||
| MixedTimeseriesChartConfig,
|
||||
| MixedTimeseriesChartConfig
|
||||
| HandlebarsChartConfig,
|
||||
dataset_id: int | str | None = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Map chart config to Superset form_data."""
|
||||
@@ -323,6 +325,8 @@ def map_config_to_form_data(
|
||||
return map_pivot_table_config(config)
|
||||
elif isinstance(config, MixedTimeseriesChartConfig):
|
||||
return map_mixed_timeseries_config(config, dataset_id=dataset_id)
|
||||
elif isinstance(config, HandlebarsChartConfig):
|
||||
return map_handlebars_config(config)
|
||||
else:
|
||||
raise ValueError(f"Unsupported config type: {type(config)}")
|
||||
|
||||
@@ -638,6 +642,44 @@ def map_pie_config(config: PieChartConfig) -> Dict[str, Any]:
|
||||
return form_data
|
||||
|
||||
|
||||
def map_handlebars_config(config: HandlebarsChartConfig) -> Dict[str, Any]:
|
||||
"""Map handlebars chart config to Superset form_data."""
|
||||
form_data: Dict[str, Any] = {
|
||||
"viz_type": "handlebars",
|
||||
"handlebars_template": config.handlebars_template,
|
||||
"row_limit": config.row_limit,
|
||||
"order_desc": config.order_desc,
|
||||
}
|
||||
|
||||
if config.style_template:
|
||||
form_data["styleTemplate"] = config.style_template
|
||||
|
||||
if config.query_mode == "raw":
|
||||
form_data["query_mode"] = "raw"
|
||||
if config.columns:
|
||||
form_data["all_columns"] = [col.name for col in config.columns]
|
||||
else:
|
||||
form_data["query_mode"] = "aggregate"
|
||||
if config.groupby:
|
||||
form_data["groupby"] = [col.name for col in config.groupby]
|
||||
if config.metrics:
|
||||
form_data["metrics"] = [create_metric_object(col) for col in config.metrics]
|
||||
if config.filters:
|
||||
form_data["adhoc_filters"] = [
|
||||
{
|
||||
"clause": "WHERE",
|
||||
"expressionType": "SIMPLE",
|
||||
"subject": filter_config.column,
|
||||
"operator": map_filter_operator(filter_config.op),
|
||||
"comparator": filter_config.value,
|
||||
}
|
||||
for filter_config in config.filters
|
||||
if filter_config is not None
|
||||
]
|
||||
|
||||
return form_data
|
||||
|
||||
|
||||
def map_pivot_table_config(config: PivotTableChartConfig) -> Dict[str, Any]:
|
||||
"""Map pivot table config to Superset form_data."""
|
||||
if not config.rows:
|
||||
@@ -908,12 +950,28 @@ def _mixed_timeseries_what(config: MixedTimeseriesChartConfig) -> str:
|
||||
return f"{primary} + {secondary}"
|
||||
|
||||
|
||||
def _handlebars_chart_what(config: HandlebarsChartConfig) -> str:
|
||||
"""Build the 'what' portion for a handlebars chart name.
|
||||
|
||||
Uses parentheses instead of en-dash to avoid collision with
|
||||
``generate_chart_name``'s ``\u2013`` context separator.
|
||||
"""
|
||||
if config.query_mode == "raw" and config.columns:
|
||||
cols = ", ".join(col.name for col in config.columns[:3])
|
||||
return f"Handlebars ({cols})"
|
||||
elif config.metrics:
|
||||
metrics = ", ".join(col.name for col in config.metrics[:3])
|
||||
return f"Handlebars ({metrics})"
|
||||
return "Handlebars Chart"
|
||||
|
||||
|
||||
def generate_chart_name(
|
||||
config: TableChartConfig
|
||||
| XYChartConfig
|
||||
| PieChartConfig
|
||||
| PivotTableChartConfig
|
||||
| MixedTimeseriesChartConfig,
|
||||
| MixedTimeseriesChartConfig
|
||||
| HandlebarsChartConfig,
|
||||
dataset_name: str | None = None,
|
||||
) -> str:
|
||||
"""Generate a descriptive chart name following a standard format.
|
||||
@@ -944,6 +1002,9 @@ def generate_chart_name(
|
||||
elif isinstance(config, MixedTimeseriesChartConfig):
|
||||
what = _mixed_timeseries_what(config)
|
||||
context = _summarize_filters(config.filters)
|
||||
elif isinstance(config, HandlebarsChartConfig):
|
||||
what = _handlebars_chart_what(config)
|
||||
context = _summarize_filters(getattr(config, "filters", None))
|
||||
else:
|
||||
return "Chart"
|
||||
|
||||
@@ -953,6 +1014,31 @@ def generate_chart_name(
|
||||
return _truncate(name)
|
||||
|
||||
|
||||
def _resolve_viz_type(config: Any) -> str:
|
||||
"""Resolve the Superset viz_type from a chart config object."""
|
||||
chart_type = getattr(config, "chart_type", "unknown")
|
||||
if chart_type == "xy":
|
||||
kind = getattr(config, "kind", "line")
|
||||
viz_type_map = {
|
||||
"line": "echarts_timeseries_line",
|
||||
"bar": "echarts_timeseries_bar",
|
||||
"area": "echarts_area",
|
||||
"scatter": "echarts_timeseries_scatter",
|
||||
}
|
||||
return viz_type_map.get(kind, "echarts_timeseries_line")
|
||||
elif chart_type == "table":
|
||||
return getattr(config, "viz_type", "table")
|
||||
elif chart_type == "pie":
|
||||
return "pie"
|
||||
elif chart_type == "pivot_table":
|
||||
return "pivot_table_v2"
|
||||
elif chart_type == "mixed_timeseries":
|
||||
return "mixed_timeseries"
|
||||
elif chart_type == "handlebars":
|
||||
return "handlebars"
|
||||
return "unknown"
|
||||
|
||||
|
||||
def analyze_chart_capabilities(chart: Any | None, config: Any) -> ChartCapabilities:
|
||||
"""Analyze chart capabilities based on type and configuration."""
|
||||
if chart:
|
||||
@@ -1003,29 +1089,6 @@ def analyze_chart_capabilities(chart: Any | None, config: Any) -> ChartCapabilit
|
||||
)
|
||||
|
||||
|
||||
def _resolve_viz_type(config: Any) -> str:
|
||||
"""Resolve viz_type from a chart config object."""
|
||||
chart_type = getattr(config, "chart_type", "unknown")
|
||||
if chart_type == "xy":
|
||||
kind = getattr(config, "kind", "line")
|
||||
viz_type_map = {
|
||||
"line": "echarts_timeseries_line",
|
||||
"bar": "echarts_timeseries_bar",
|
||||
"area": "echarts_area",
|
||||
"scatter": "echarts_timeseries_scatter",
|
||||
}
|
||||
return viz_type_map.get(kind, "echarts_timeseries_line")
|
||||
elif chart_type == "table":
|
||||
return getattr(config, "viz_type", "table")
|
||||
elif chart_type == "pie":
|
||||
return "pie"
|
||||
elif chart_type == "pivot_table":
|
||||
return "pivot_table_v2"
|
||||
elif chart_type == "mixed_timeseries":
|
||||
return "mixed_timeseries"
|
||||
return "unknown"
|
||||
|
||||
|
||||
def analyze_chart_semantics(chart: Any | None, config: Any) -> ChartSemantics:
|
||||
"""Generate semantic understanding of the chart."""
|
||||
if chart:
|
||||
@@ -1052,6 +1115,10 @@ def analyze_chart_semantics(chart: Any | None, config: Any) -> ChartSemantics:
|
||||
"Combines two different chart types on the same time axis "
|
||||
"for comparing related metrics with different scales"
|
||||
),
|
||||
"handlebars": (
|
||||
"Renders data using a custom Handlebars HTML template for "
|
||||
"fully flexible layouts like KPI cards, leaderboards, and reports"
|
||||
),
|
||||
}
|
||||
|
||||
primary_insight = insights_map.get(
|
||||
|
||||
Reference in New Issue
Block a user