Source code for sktime_mcp.tools.save_model
"""
save_model tool for sktime MCP.
Saves estimator instances via sktime's MLflow integration.
"""
from collections.abc import Callable
from pathlib import Path
from typing import Any
from sktime_mcp.runtime.handles import get_handle_manager
# MLflow URI schemes that must be passed through untouched
_MLFLOW_URI_PREFIXES = ("runs:/", "models:/", "mlflow-artifacts:/")
def resolve_model_path(path: str) -> str:
"""Expand ~ and resolve local filesystem paths to absolute form.
MLflow URIs (runs:/, models:/, mlflow-artifacts:/, scheme://...) are
returned unchanged. Without this, "~/model" creates a literal "~"
directory in the server cwd, and relative paths land in the server's
working directory with no way for the caller to learn where.
"""
if path.startswith(_MLFLOW_URI_PREFIXES) or "://" in path:
return path
return str(Path(path).expanduser().resolve())
def _get_mlflow_save_model() -> Callable[..., Any]:
"""Resolve sktime MLflow save utility lazily for better runtime compatibility."""
try:
from sktime.utils.mlflow_sktime import save_model as mlflow_save_model
except Exception as exc:
raise ImportError(
"Unable to import sktime MLflow save_model utility. "
"Ensure sktime and MLflow dependencies are installed."
) from exc
return mlflow_save_model
[docs]
def save_model_tool(
estimator_handle: str,
path: str,
mlflow_params: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""
Save an instantiated estimator to a local path or URI using sktime+MLflow.
Args:
estimator_handle: Handle ID from instantiate
path: Local directory or URI where the model should be saved
mlflow_params: Optional extra keyword arguments for sktime MLflow save_model
Returns:
Dictionary with success status and confirmation message/path.
"""
handle_manager = get_handle_manager()
try:
estimator = handle_manager.get_instance(estimator_handle)
except KeyError:
return {"success": False, "error": handle_manager.describe_missing(estimator_handle)}
if not handle_manager.is_fitted(estimator_handle):
handle_info = handle_manager.get_info(estimator_handle)
return {
"success": False,
"error": (
f"Estimator '{handle_info.estimator_name}' (handle: {estimator_handle}) "
"has not been fitted. Call fit before saving."
),
}
if mlflow_params is not None and not isinstance(mlflow_params, dict):
return {"success": False, "error": "mlflow_params must be a dictionary"}
resolved_path = resolve_model_path(path)
try:
save_model = _get_mlflow_save_model()
save_model(sktime_model=estimator, path=resolved_path, **(mlflow_params or {}))
return {
"success": True,
"estimator_handle": estimator_handle,
"saved_path": resolved_path,
"message": f"Model saved successfully to '{resolved_path}'",
}
except Exception as exc:
return {"success": False, "error": str(exc)}