aminghadersohi commented on code in PR #41611: URL: https://github.com/apache/superset/pull/41611#discussion_r3537920585
########## superset/mcp_service/semantic_layer/tool/get_compatible_metrics.py: ########## @@ -0,0 +1,230 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""MCP tool: get_compatible_metrics + +Returns metrics compatible with the current dimension/metric selection. +""" + +import logging + +from fastmcp import Context +from superset_core.mcp.decorators import tool, ToolAnnotations + +from superset.extensions import event_logger +from superset.mcp_service.privacy import ( + DATA_MODEL_METADATA_ERROR_TYPE, + requires_data_model_metadata_access, + user_can_view_data_model_metadata, +) +from superset.mcp_service.semantic_layer.schemas import ( + CompatibleMetricsResponse, + GetCompatibleMetricsRequest, + MetricInfo, + SemanticLayerError, +) + +logger = logging.getLogger(__name__) + + +@tool( + tags=["data", "semantic"], + class_permission_name="Dataset", + annotations=ToolAnnotations( + title="Get compatible metrics", + readOnlyHint=True, + destructiveHint=False, + ), +) +@requires_data_model_metadata_access +async def get_compatible_metrics( + request: GetCompatibleMetricsRequest, + ctx: Context, +) -> CompatibleMetricsResponse | SemanticLayerError: + """Return metrics compatible with the current dimension/metric selection. + + Used to progressively refine a query: given a set of already-selected + metrics and dimensions, returns the additional metrics that can be + combined without breaking the underlying semantic constraints. + + Provide exactly one of ``dataset_id`` (built-in) or ``view_id`` (external). + + For built-in datasets, all metrics from the dataset are considered + compatible (SQL GROUP BY imposes no metric-level constraints). + + For external semantic views, delegates to the view's + ``get_compatible_metrics`` implementation. + + Example: + ```json + { + "selected_metrics": [], + "selected_dimensions": ["region"], + "view_id": 5 + } + ``` + """ + await ctx.info( + "Getting compatible metrics: dataset_id=%s, view_id=%s, " + "metrics=%s, dims=%s" + % ( + request.dataset_id, + request.view_id, + request.selected_metrics, + request.selected_dimensions, + ) + ) + + if not user_can_view_data_model_metadata(): + return SemanticLayerError.create( + error="You don't have permission to access dataset details for your role.", + error_type=DATA_MODEL_METADATA_ERROR_TYPE, + ) + + if request.dataset_id is None and request.view_id is None: + return SemanticLayerError.create( + error="Provide either dataset_id (built-in) or view_id (external).", + error_type="ValidationError", + ) + if request.dataset_id is not None and request.view_id is not None: + return SemanticLayerError.create( + error="Provide only one of dataset_id or view_id, not both.", + error_type="ValidationError", + ) + + try: + # ------------------------------------------------------------------ + # Built-in dataset path + # ------------------------------------------------------------------ + if request.dataset_id is not None: + from sqlalchemy.orm import subqueryload + + from superset.connectors.sqla.models import SqlaTable + from superset.daos.dataset import DatasetDAO + + with event_logger.log_context(action="mcp.get_compatible_metrics.builtin"): + dataset = DatasetDAO.find_by_id( + request.dataset_id, + query_options=[ + subqueryload(SqlaTable.columns), + subqueryload(SqlaTable.metrics), + ], + ) Review Comment: Fixed — annotated as `dataset: SqlaTable | None`. ########## tests/unit_tests/mcp_service/semantic_layer/tool/test_list_metrics.py: ########## @@ -0,0 +1,305 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Unit tests for the list_metrics MCP tool.""" + +from __future__ import annotations + +import importlib +from collections.abc import Generator +from types import ModuleType +from unittest.mock import call, MagicMock, Mock, patch + +import pytest +from fastmcp import Client, FastMCP + +from superset.errors import ErrorLevel, SupersetError, SupersetErrorType +from superset.exceptions import SupersetSecurityException +from superset.mcp_service.app import mcp +from superset.utils import json + +list_metrics_module: ModuleType = importlib.import_module( + "superset.mcp_service.semantic_layer.tool.list_metrics" +) + + [email protected] +def mcp_server() -> FastMCP: + return mcp + + [email protected](autouse=True) +def mock_auth() -> Generator[MagicMock, None, None]: + with ( + patch("superset.mcp_service.auth.get_user_from_request") as mock_get_user, + patch.object( + list_metrics_module, + "user_can_view_data_model_metadata", + return_value=True, + ), + ): + mock_user = Mock() Review Comment: Fixed — annotated as `mock_user: Mock`. ########## tests/unit_tests/mcp_service/semantic_layer/tool/test_get_compatible_metrics.py: ########## @@ -0,0 +1,247 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Unit tests for the get_compatible_metrics MCP tool.""" + +from __future__ import annotations + +import importlib +from collections.abc import Generator +from types import ModuleType +from unittest.mock import MagicMock, Mock, patch + +import pytest +from fastmcp import Client, FastMCP + +from superset.errors import ErrorLevel, SupersetError, SupersetErrorType +from superset.exceptions import SupersetSecurityException +from superset.mcp_service.app import mcp +from superset.utils import json + +get_compatible_metrics_module: ModuleType = importlib.import_module( + "superset.mcp_service.semantic_layer.tool.get_compatible_metrics" +) + + [email protected] +def mcp_server() -> FastMCP: + return mcp + + [email protected](autouse=True) +def mock_auth() -> Generator[MagicMock, None, None]: + with ( + patch("superset.mcp_service.auth.get_user_from_request") as mock_get_user, + patch.object( + get_compatible_metrics_module, + "user_can_view_data_model_metadata", + return_value=True, + ), + ): + mock_user = Mock() + mock_user.id = 1 + mock_user.username = "admin" + mock_get_user.return_value = mock_user + yield mock_get_user + + +def _make_metric(name: str, expression: str = "COUNT(*)") -> MagicMock: + m = MagicMock() + m.metric_name = name + m.verbose_name = None + m.expression = expression + m.description = None + m.d3format = None + m.warning_text = None + return m + + +def _make_dataset(dataset_id: int = 42) -> MagicMock: + ds = MagicMock() + ds.id = dataset_id + ds.table_name = f"table_{dataset_id}" + ds.columns = [] + ds.metrics = [_make_metric("count"), _make_metric("revenue", "SUM(revenue)")] + return ds + + +def _make_view(view_id: int = 5) -> MagicMock: + view = MagicMock() Review Comment: Fixed — annotated as `view: MagicMock`. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
