Source code for sktime_mcp.tools.codegen
"""
Code generation tool for sktime MCP.
Generates Python code to recreate estimators and pipelines.
"""
import json
import keyword
from typing import Any
from sktime_mcp.runtime.executor import _get_demo_datasets
from sktime_mcp.runtime.handles import get_handle_manager
def _format_value(value: Any) -> str:
"""Format a parameter value for Python code generation."""
if isinstance(value, str):
# json.dumps escapes embedded quotes/backslashes; its output is also
# a valid Python string literal
return json.dumps(value)
elif isinstance(value, (list, tuple)):
if isinstance(value, tuple):
items = ", ".join(_format_value(v) for v in value)
return f"({items})" if len(value) != 1 else f"({items},)"
else:
items = ", ".join(_format_value(v) for v in value)
return f"[{items}]"
elif isinstance(value, dict):
items = ", ".join(f"{_format_value(k)}: {_format_value(v)}" for k, v in value.items())
return f"{{{items}}}"
elif isinstance(value, bool):
return str(value)
elif value is None:
return "None"
elif isinstance(value, (int, float)):
return str(value)
else:
# For complex objects, try to represent as str
return repr(value)
def _is_valid_var_name(var_name: str) -> bool:
"""Return True when var_name is a valid non-keyword Python identifier."""
return isinstance(var_name, str) and var_name.isidentifier() and not keyword.iskeyword(var_name)
def _loader_for(dataset: str, demo_datasets: dict) -> tuple[str, str]:
"""Return (module, func) for a demo dataset name, defaulting to load_airline."""
if dataset in demo_datasets:
module_path = demo_datasets[dataset]
module, func = module_path.rsplit(".", 1)
return module, func
return "sktime.datasets", "load_airline"
def _fit_example(
var_name: str,
obj_type: str,
dataset: str | None,
handle_info: Any,
demo_datasets: dict,
) -> str:
"""Build a runnable fit/predict example matching the estimator's scitype.
A forecaster-shaped example (`fit(y)` / `predict(fh)`) is wrong for
transformers, splitters, and classifiers, which raise AttributeError when
the generated code runs (BUG-03).
"""
if obj_type in ("classifier", "regressor"):
# Panel X + label/target y — use a classification demo dataset.
ds = dataset or "arrow_head"
module, func = _loader_for(ds, demo_datasets)
verb = "class" if obj_type == "classifier" else "value"
return f"""
# Example usage ({obj_type}):
from {module} import {func}
X, y = {func}(return_X_y=True)
{var_name}.fit(X, y)
predictions = {var_name}.predict(X) # predicted {verb} per instance
print(predictions)
"""
if obj_type == "transformer":
ds = dataset or handle_info.metadata.get("training_dataset") or "airline"
module, func = _loader_for(ds, demo_datasets)
return f"""
# Example usage (transformer):
from {module} import {func}
y = {func}()
y_transformed = {var_name}.fit_transform(y)
print(y_transformed)
"""
if obj_type == "splitter":
ds = dataset or "airline"
module, func = _loader_for(ds, demo_datasets)
return f"""
# Example usage (splitter):
from {module} import {func}
y = {func}()
for train_idx, test_idx in {var_name}.split(y):
print("train:", train_idx, "test:", test_idx)
"""
# Default: forecaster.
ds = dataset or handle_info.metadata.get("training_dataset") or "airline"
module, func = _loader_for(ds, demo_datasets)
return f"""
# Example usage (forecaster):
from {module} import {func}
y = {func}()
{var_name}.fit(y)
fh = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] # 12-step ahead forecast
predictions = {var_name}.predict(fh=fh)
print(predictions)
"""
[docs]
def export_code_tool(
handle: str,
var_name: str = "model",
include_fit_example: bool = False,
dataset: str | None = None,
) -> dict[str, Any]:
"""
Export an estimator or pipeline as executable Python code.
Args:
handle: The handle ID of the estimator/pipeline to export
var_name: Variable name to use in generated code (default: "model")
include_fit_example: Whether to include a fit/predict example (default: False)
dataset: Optional dataset name for the fit example (default: None, falls back to airline)
Returns:
Dictionary with:
- success: bool
- code: Generated Python code string
- estimator_name: Name of the estimator/pipeline
- is_pipeline: Whether this is a pipeline
Example:
>>> # First create an estimator
>>> result = instantiate_tool("ARIMA", {"order": [1, 1, 1]})
>>> handle = result["handle"]
>>>
>>> # Export as code
>>> export_code_tool(handle, var_name="arima_model")
{
"success": True,
"code": "from sktime.forecasting.arima import ARIMA\\n\\narima_model = ARIMA(order=[1, 1, 1])",
"estimator_name": "ARIMA",
"is_pipeline": False
}
"""
handle_manager = get_handle_manager()
# Get handle info
try:
handle_info = handle_manager.get_info(handle)
except KeyError:
return {"success": False, "error": handle_manager.describe_missing(handle)}
if not _is_valid_var_name(var_name):
return {
"success": False,
"error": "var_name must be a valid Python identifier and not a keyword.",
}
estimator_name = handle_info.estimator_name
params = handle_info.params
spec = params.get("spec")
instance = handle_manager.get_instance(handle)
get_tag = getattr(instance, "get_class_tag", None)
obj_type = get_tag("object_type", "") if callable(get_tag) else ""
# is_pipeline from the instance, not a spec substring — "[" in spec
# false-positived on any list argument (BUG-04).
is_pipeline = bool(spec and "*" in spec) or hasattr(instance, "steps")
if spec:
code = f"from sktime.registry import craft\n\n{var_name} = craft({_format_value(spec)})"
elif handle_info.metadata.get("source") == "loaded" and handle_info.metadata.get("path"):
# Loaded models carry no craft spec; emit a load_model snippet instead of
# failing with "No craft spec found" (NB-17).
model_path = handle_info.metadata["path"]
code = (
"from sktime.utils.mlflow_sktime import load_model\n\n"
f"{var_name} = load_model({_format_value(model_path)})"
)
else:
return {"success": False, "error": "No craft spec found in handle parameters."}
# Optionally add a scitype-appropriate fit example (BUG-03).
if include_fit_example:
demo_datasets = _get_demo_datasets()
if dataset is not None and dataset not in demo_datasets:
return {
"success": False,
"error": (
f"Unknown dataset '{dataset}' for the fit example. Use a demo dataset "
"name (see list_available_data) or omit dataset to use a default."
),
}
example = _fit_example(var_name, obj_type, dataset, handle_info, demo_datasets)
code += example
return {
"success": True,
"code": code,
"estimator_name": estimator_name,
"is_pipeline": is_pipeline,
"handle": handle,
}