cli.py
1 import json 2 import sys 3 from inspect import signature 4 5 import click 6 7 from mlflow.deployments import interface 8 from mlflow.mcp.decorator import mlflow_mcp 9 from mlflow.utils import cli_args 10 from mlflow.utils.proto_json_utils import NumpyEncoder, _get_jsonable_obj 11 12 13 def _user_args_to_dict(user_list): 14 # Similar function in mlflow.cli is throwing exception on import 15 user_dict = {} 16 for s in user_list: 17 try: 18 # Some configs may contain '=' in the value 19 name, value = s.split("=", 1) 20 except ValueError as exc: 21 # not enough values to unpack 22 raise click.BadOptionUsage( 23 "config", 24 "Config options must be a pair and should be " 25 "provided as ``-C key=value`` or " 26 "``--config key=value``", 27 ) from exc 28 if name in user_dict: 29 raise click.ClickException(f"Repeated parameter: '{name}'") 30 user_dict[name] = value 31 return user_dict 32 33 34 installed_targets = list(interface.plugin_store.registry) 35 if len(installed_targets) > 0: 36 supported_targets_msg = "Support is currently installed for deployment to: {targets}".format( 37 targets=", ".join(installed_targets) 38 ) 39 else: 40 supported_targets_msg = ( 41 "NOTE: you currently do not have support installed for any deployment targets." 42 ) 43 44 target_details = click.option( 45 "--target", 46 "-t", 47 required=True, 48 help=f""" 49 Deployment target URI. Run 50 `mlflow deployments help --target-name <target-name>` for 51 more details on the supported URI format and config options 52 for a given target. 53 {supported_targets_msg} 54 55 See all supported deployment targets and installation 56 instructions at 57 https://mlflow.org/docs/latest/plugins.html#community-plugins 58 """, 59 ) 60 deployment_name = click.option("--name", "name", required=True, help="Name of the deployment") 61 optional_deployment_name = click.option("--name", "name", help="Name of the deployment") 62 parse_custom_arguments = click.option( 63 "--config", 64 "-C", 65 metavar="NAME=VALUE", 66 multiple=True, 67 help="Extra target-specific config for the model " 68 "deployment, of the form -C name=value. See " 69 "documentation/help for your deployment target for a " 70 "list of supported config options.", 71 ) 72 73 parse_input = click.option( 74 "--input-path", 75 "-I", 76 required=True, 77 help="Path to input prediction payload file. The file can" 78 "be a JSON (Python Dict) or CSV (pandas DataFrame). If the file is a CSV, the user must specify" 79 "the --content-type csv option.", 80 ) 81 82 parse_output = click.option( 83 "--output-path", 84 "-O", 85 help="File to output results to as a JSON file. If not provided, prints output to stdout.", 86 ) 87 88 required_endpoint_param = click.option("--endpoint", required=True, help="Name of the endpoint") 89 optional_endpoint_param = click.option("--endpoint", help="Name of the endpoint") 90 91 92 @click.group( 93 "deployments", 94 help=f""" 95 Deploy MLflow models to custom targets. 96 Run `mlflow deployments help --target-name <target-name>` for 97 more details on the supported URI format and config options for a given target. 98 {supported_targets_msg} 99 100 See all supported deployment targets and installation instructions in 101 https://mlflow.org/docs/latest/plugins.html#community-plugins 102 103 You can also write your own plugin for deployment to a custom target. For instructions on 104 writing and distributing a plugin, see 105 https://mlflow.org/docs/latest/plugins.html#writing-your-own-mlflow-plugins. 106 """, 107 ) 108 def commands(): 109 """ 110 Deploy MLflow models to custom targets. Support is currently installed for 111 the following targets: {targets}. Run `mlflow deployments help --target-name <target-name>` for 112 more details on the supported URI format and config options for a given target. 113 114 To deploy to other targets, you must first install an 115 appropriate third-party Python plugin. See the list of known community-maintained plugins 116 at https://mlflow.org/docs/latest/plugins.html#community-plugins. 117 118 You can also write your own plugin for deployment to a custom target. For instructions on 119 writing and distributing a plugin, see 120 https://mlflow.org/docs/latest/plugins.html#writing-your-own-mlflow-plugins. 121 """ 122 123 124 @commands.command("create") 125 @mlflow_mcp(tool_name="create_deployment") 126 @optional_endpoint_param 127 @parse_custom_arguments 128 @deployment_name 129 @target_details 130 @cli_args.MODEL_URI 131 @click.option( 132 "--flavor", 133 "-f", 134 help="Which flavor to be deployed. This will be auto inferred if it's not given", 135 ) 136 def create_deployment(flavor, model_uri, target, name, config, endpoint): 137 """ 138 Deploy the model at ``model_uri`` to the specified target. 139 140 Additional plugin-specific arguments may also be passed to this command, via `-C key=value` 141 """ 142 config_dict = _user_args_to_dict(config) 143 client = interface.get_deploy_client(target) 144 145 sig = signature(client.create_deployment) 146 if "endpoint" in sig.parameters: 147 deployment = client.create_deployment( 148 name, model_uri, flavor, config=config_dict, endpoint=endpoint 149 ) 150 else: 151 deployment = client.create_deployment(name, model_uri, flavor, config=config_dict) 152 click.echo("\n{} deployment {} is created".format(deployment["flavor"], deployment["name"])) 153 154 155 @commands.command("update") 156 @mlflow_mcp(tool_name="update_deployment") 157 @optional_endpoint_param 158 @parse_custom_arguments 159 @deployment_name 160 @target_details 161 @click.option( 162 "--model-uri", 163 "-m", 164 default=None, 165 metavar="URI", 166 help="URI to the model. A local path, a 'runs:/' URI, or a" 167 " remote storage URI (e.g., an 's3://' URI). For more information" 168 " about supported remote URIs for model artifacts, see" 169 " https://mlflow.org/docs/latest/tracking.html" 170 "#artifact-stores", 171 ) 172 @click.option( 173 "--flavor", 174 "-f", 175 help="Which flavor to be deployed. This will be auto inferred if it's not given", 176 ) 177 def update_deployment(flavor, model_uri, target, name, config, endpoint): 178 """ 179 Update the deployment with ID `deployment_id` in the specified target. 180 You can update the URI of the model and/or the flavor of the deployed model (in which case the 181 model URI must also be specified). 182 183 Additional plugin-specific arguments may also be passed to this command, via `-C key=value`. 184 """ 185 config_dict = _user_args_to_dict(config) 186 client = interface.get_deploy_client(target) 187 188 sig = signature(client.update_deployment) 189 if "endpoint" in sig.parameters: 190 ret = client.update_deployment( 191 name, model_uri=model_uri, flavor=flavor, config=config_dict, endpoint=endpoint 192 ) 193 else: 194 ret = client.update_deployment(name, model_uri=model_uri, flavor=flavor, config=config_dict) 195 click.echo("Deployment {} is updated (with flavor {})".format(name, ret["flavor"])) 196 197 198 @commands.command("delete") 199 @mlflow_mcp(tool_name="delete_deployment") 200 @optional_endpoint_param 201 @parse_custom_arguments 202 @deployment_name 203 @target_details 204 def delete_deployment(target, name, config, endpoint): 205 """ 206 Delete the deployment with name given at `--name` from the specified target. 207 """ 208 client = interface.get_deploy_client(target) 209 210 sig = signature(client.delete_deployment) 211 if "config" in sig.parameters: 212 config_dict = _user_args_to_dict(config) 213 if "endpoint" in sig.parameters: 214 client.delete_deployment(name, config=config_dict, endpoint=endpoint) 215 else: 216 client.delete_deployment(name, config=config_dict) 217 else: 218 if "endpoint" in sig.parameters: 219 client.delete_deployment(name, endpoint=endpoint) 220 else: 221 client.delete_deployment(name) 222 223 click.echo(f"Deployment {name} is deleted") 224 225 226 @commands.command("list") 227 @mlflow_mcp(tool_name="list_deployments") 228 @optional_endpoint_param 229 @target_details 230 def list_deployment(target, endpoint): 231 """ 232 List the names of all model deployments in the specified target. These names can be used with 233 the `delete`, `update`, and `get` commands. 234 """ 235 client = interface.get_deploy_client(target) 236 237 sig = signature(client.list_deployments) 238 if "endpoint" in sig.parameters: 239 ids = client.list_deployments(endpoint=endpoint) 240 else: 241 ids = client.list_deployments() 242 click.echo(f"List of all deployments:\n{ids}") 243 244 245 @commands.command("get") 246 @mlflow_mcp(tool_name="get_deployment") 247 @optional_endpoint_param 248 @deployment_name 249 @target_details 250 def get_deployment(target, name, endpoint): 251 """ 252 Print a detailed description of the deployment with name given at ``--name`` in the specified 253 target. 254 """ 255 client = interface.get_deploy_client(target) 256 257 sig = signature(client.get_deployment) 258 if "endpoint" in sig.parameters: 259 desc = client.get_deployment(name, endpoint=endpoint) 260 else: 261 desc = client.get_deployment(name) 262 for key, val in desc.items(): 263 click.echo(f"{key}: {val}") 264 click.echo("\n") 265 266 267 @commands.command("help") 268 @target_details 269 def target_help(target): 270 """ 271 Display additional help for a specific deployment target, e.g. info on target-specific config 272 options and the target's URI format. 273 """ 274 click.echo(interface._target_help(target)) 275 276 277 @commands.command("run-local") 278 @mlflow_mcp(tool_name="run_deployment_locally") 279 @parse_custom_arguments 280 @deployment_name 281 @target_details 282 @cli_args.MODEL_URI 283 @click.option( 284 "--flavor", 285 "-f", 286 help="Which flavor to be deployed. This will be auto inferred if it's not given", 287 ) 288 def run_local(flavor, model_uri, target, name, config): 289 """ 290 Deploy the model locally. This has very similar signature to ``create`` API 291 """ 292 config_dict = _user_args_to_dict(config) 293 interface.run_local(target, name, model_uri, flavor, config_dict) 294 295 296 def predictions_to_json(raw_predictions, output): 297 predictions = _get_jsonable_obj(raw_predictions, pandas_orient="records") 298 json.dump(predictions, output, cls=NumpyEncoder) 299 300 301 @commands.command("predict") 302 @mlflow_mcp(tool_name="predict_with_deployment") 303 @click.option( 304 "--name", 305 "name", 306 help="Name of the deployment. Exactly one of --name or --endpoint must be specified.", 307 ) 308 @click.option( 309 "--endpoint", 310 help="Name of the endpoint. Exactly one of --name or --endpoint must be specified.", 311 ) 312 @target_details 313 @parse_input 314 @parse_output 315 def predict(target, name, input_path, output_path, endpoint): 316 """ 317 Predict the results for the deployed model for the given input(s) 318 """ 319 import pandas as pd 320 321 if (name, endpoint).count(None) != 1: 322 raise click.UsageError("Must specify exactly one of --name or --endpoint.") 323 324 df = pd.read_json(input_path) 325 client = interface.get_deploy_client(target) 326 327 sig = signature(client.predict) 328 if "endpoint" in sig.parameters: 329 result = client.predict(name, df, endpoint=endpoint) 330 else: 331 result = client.predict(name, df) 332 if output_path is not None: 333 result.to_json(output_path) 334 else: 335 click.echo(result.to_json()) 336 337 338 @commands.command("explain") 339 @mlflow_mcp(tool_name="explain_deployment") 340 @click.option( 341 "--name", 342 "name", 343 help="Name of the deployment. Exactly one of --name or --endpoint must be specified.", 344 ) 345 @click.option( 346 "--endpoint", 347 help="Name of the endpoint. Exactly one of --name or --endpoint must be specified.", 348 ) 349 @target_details 350 @parse_input 351 @parse_output 352 def explain(target, name, input_path, output_path, endpoint): 353 """ 354 Generate explanations of model predictions on the specified input for 355 the deployed model for the given input(s). Explanation output formats vary 356 by deployment target, and can include details like feature importance for 357 understanding/debugging predictions. Run `mlflow deployments help` or 358 consult the documentation for your plugin for details on explanation format. 359 For information about the input data formats accepted by this function, 360 see the following documentation: 361 https://www.mlflow.org/docs/latest/models.html#built-in-deployment-tools 362 """ 363 import pandas as pd 364 365 if (name, endpoint).count(None) != 1: 366 raise click.UsageError("Must specify exactly one of --name or --endpoint.") 367 368 df = pd.read_json(input_path) 369 client = interface.get_deploy_client(target) 370 371 sig = signature(client.explain) 372 if "endpoint" in sig.parameters: 373 result = client.explain(name, df, endpoint=endpoint) 374 else: 375 result = client.explain(name, df) 376 if output_path: 377 with open(output_path, "w") as fp: 378 predictions_to_json(result, fp) 379 else: 380 predictions_to_json(result, sys.stdout) 381 382 383 @commands.command("create-endpoint") 384 @mlflow_mcp(tool_name="create_deployment_endpoint") 385 @click.option( 386 "--config", 387 "-C", 388 metavar="NAME=VALUE", 389 multiple=True, 390 help="Extra target-specific config for the endpoint, " 391 "of the form -C name=value. See " 392 "documentation/help for your deployment target for a " 393 "list of supported config options.", 394 ) 395 @required_endpoint_param 396 @target_details 397 def create_endpoint(target, name, config): 398 """ 399 Create an endpoint with the specified name at the specified target. 400 401 Additional plugin-specific arguments may also be passed to this command, via `-C key=value` 402 """ 403 config_dict = _user_args_to_dict(config) 404 client = interface.get_deploy_client(target) 405 endpoint = client.create_endpoint(name, config=config_dict) 406 click.echo("\nEndpoint {} is created".format(endpoint["name"])) 407 408 409 @commands.command("update-endpoint") 410 @mlflow_mcp(tool_name="update_deployment_endpoint") 411 @click.option( 412 "--config", 413 "-C", 414 metavar="NAME=VALUE", 415 multiple=True, 416 help="Extra target-specific config for the endpoint, " 417 "of the form -C name=value. See " 418 "documentation/help for your deployment target for a " 419 "list of supported config options.", 420 ) 421 @required_endpoint_param 422 @target_details 423 def update_endpoint(target, endpoint, config): 424 """ 425 Update the specified endpoint at the specified target. 426 427 Additional plugin-specific arguments may also be passed to this command, via `-C key=value` 428 """ 429 config_dict = _user_args_to_dict(config) 430 client = interface.get_deploy_client(target) 431 client.update_endpoint(endpoint, config=config_dict) 432 click.echo(f"\nEndpoint {endpoint} is updated") 433 434 435 @commands.command("delete-endpoint") 436 @mlflow_mcp(tool_name="delete_deployment_endpoint") 437 @required_endpoint_param 438 @target_details 439 def delete_endpoint(target, endpoint): 440 """ 441 Delete the specified endpoint at the specified target 442 """ 443 client = interface.get_deploy_client(target) 444 client.delete_endpoint(endpoint) 445 click.echo(f"\nEndpoint {endpoint} is deleted") 446 447 448 @commands.command("list-endpoints") 449 @mlflow_mcp(tool_name="list_deployment_endpoints") 450 @target_details 451 def list_endpoints(target): 452 """ 453 List all endpoints at the specified target 454 """ 455 client = interface.get_deploy_client(target) 456 ids = client.list_endpoints() 457 click.echo(f"List of all endpoints:\n{ids}") 458 459 460 @commands.command("get-endpoint") 461 @mlflow_mcp(tool_name="get_deployment_endpoint") 462 @required_endpoint_param 463 @target_details 464 def get_endpoint(target, endpoint): 465 """ 466 Get details for the specified endpoint at the specified target 467 """ 468 client = interface.get_deploy_client(target) 469 desc = client.get_endpoint(endpoint) 470 for key, val in desc.items(): 471 click.echo(f"{key}: {val}") 472 click.echo("\n") 473 474 475 def validate_config_path(_ctx, _param, value): 476 from mlflow.gateway.config import _validate_config 477 478 try: 479 _validate_config(value) 480 return value 481 except Exception as e: 482 raise click.BadParameter(str(e))