runs.py
1 """ 2 CLI for runs 3 """ 4 5 import json 6 7 import click 8 9 import mlflow 10 from mlflow import MlflowClient 11 from mlflow.entities import RunStatus, ViewType 12 from mlflow.environment_variables import MLFLOW_EXPERIMENT_ID, MLFLOW_EXPERIMENT_NAME 13 from mlflow.exceptions import MlflowException 14 from mlflow.mcp.decorator import mlflow_mcp 15 from mlflow.tracking import _get_store 16 from mlflow.utils.string_utils import _create_table 17 from mlflow.utils.time import conv_longdate_to_str 18 19 RUN_ID = click.option("--run-id", type=click.STRING, required=True) 20 21 22 @click.group("runs") 23 def commands(): 24 """ 25 Manage runs. To manage runs of experiments associated with a tracking server, set the 26 MLFLOW_TRACKING_URI environment variable to the URL of the desired server. 27 """ 28 29 30 @commands.command("list") 31 @mlflow_mcp(tool_name="list_runs") 32 @click.option( 33 "--experiment-id", 34 envvar=MLFLOW_EXPERIMENT_ID.name, 35 type=click.STRING, 36 help="Specify the experiment ID for list of runs.", 37 required=True, 38 ) 39 @click.option( 40 "--view", 41 "-v", 42 default="active_only", 43 help="Select view type for list experiments. Valid view types are " 44 "'active_only' (default), 'deleted_only', and 'all'.", 45 ) 46 def list_run(experiment_id: str, view: str) -> None: 47 """ 48 List all runs of the specified experiment in the configured tracking server. 49 """ 50 store = _get_store() 51 view_type = ViewType.from_string(view) if view else ViewType.ACTIVE_ONLY 52 runs = store.search_runs([experiment_id], None, view_type) 53 table = [] 54 for run in runs: 55 run_name = run.info.run_name or "" 56 table.append([conv_longdate_to_str(run.info.start_time), run_name, run.info.run_id]) 57 click.echo(_create_table(sorted(table, reverse=True), headers=["Date", "Name", "ID"])) 58 59 60 @commands.command("delete") 61 @mlflow_mcp(tool_name="delete_run") 62 @RUN_ID 63 def delete_run(run_id: str) -> None: 64 """ 65 Mark a run for deletion. Return an error if the run does not exist or 66 is already marked. You can restore a marked run with ``restore_run``, 67 or permanently delete a run in the backend store. 68 """ 69 store = _get_store() 70 store.delete_run(run_id) 71 click.echo(f"Run with ID {run_id} has been deleted.") 72 73 74 @commands.command("restore") 75 @mlflow_mcp(tool_name="restore_run") 76 @RUN_ID 77 def restore_run(run_id: str) -> None: 78 """ 79 Restore a deleted run. 80 Returns an error if the run is active or has been permanently deleted. 81 """ 82 store = _get_store() 83 store.restore_run(run_id) 84 click.echo(f"Run with id {run_id} has been restored.") 85 86 87 @commands.command("describe") 88 @mlflow_mcp(tool_name="describe_run") 89 @RUN_ID 90 def describe_run(run_id: str) -> None: 91 """ 92 All of run details will print to the stdout as JSON format. 93 """ 94 store = _get_store() 95 run = store.get_run(run_id) 96 json_run = json.dumps(run.to_dictionary(), indent=4) 97 click.echo(json_run) 98 99 100 @commands.command("create") 101 @mlflow_mcp(tool_name="create_run") 102 @click.option( 103 "--experiment-id", 104 envvar=MLFLOW_EXPERIMENT_ID.name, 105 type=click.STRING, 106 help="ID of the experiment under which to create the run. " 107 "Must specify either this or --experiment-name.", 108 ) 109 @click.option( 110 "--experiment-name", 111 envvar=MLFLOW_EXPERIMENT_NAME.name, 112 type=click.STRING, 113 help="Name of the experiment under which to create the run. " 114 "Must specify either this or --experiment-id.", 115 ) 116 @click.option( 117 "--run-name", 118 type=click.STRING, 119 help="Optional human-readable name for the run (e.g., 'baseline-model-v1').", 120 ) 121 @click.option( 122 "--description", 123 type=click.STRING, 124 help="Optional longer description of what this run represents.", 125 ) 126 @click.option( 127 "--tags", 128 "-t", 129 multiple=True, 130 help="Key-value pairs to categorize and filter runs. Use multiple times for " 131 "multiple tags. Format: key=value (e.g., env=prod, model=xgboost, version=1.0).", 132 ) 133 @click.option( 134 "--status", 135 type=click.Choice(["FINISHED", "FAILED", "KILLED"], case_sensitive=False), 136 default="FINISHED", 137 help="Final status of the run. Options: FINISHED (default), FAILED, or KILLED.", 138 ) 139 @click.option( 140 "--parent-run-id", 141 type=click.STRING, 142 help="Optional ID of a parent run to create a nested run under.", 143 ) 144 def create_run( 145 experiment_id: str | None, 146 experiment_name: str | None, 147 run_name: str | None, 148 description: str | None, 149 tags: tuple[str, ...], 150 status: str, 151 parent_run_id: str | None, 152 ) -> None: 153 """ 154 Create a new MLflow run and immediately end it with the specified status. 155 156 This command is useful for creating runs programmatically for testing, scripting, 157 or recording completed experiments. The run will be created and immediately closed 158 with the specified status (FINISHED, FAILED, or KILLED). 159 """ 160 # Validate that exactly one of experiment_id or experiment_name is provided 161 if (experiment_id is not None and experiment_name is not None) or ( 162 experiment_id is None and experiment_name is None 163 ): 164 raise click.UsageError("Must specify exactly one of --experiment-id or --experiment-name.") 165 166 # Parse tags from key=value format 167 tags_dict = {} 168 if tags: 169 for tag in tags: 170 match tag.split("=", 1): 171 case [key, value]: 172 if key in tags_dict: 173 raise click.UsageError(f"Duplicate tag key: '{key}'") 174 tags_dict[key] = value 175 case _: 176 raise click.UsageError( 177 f"Invalid tag format: '{tag}'. Tags must be in key=value format." 178 ) 179 180 # Set the experiment if using experiment_name 181 if experiment_name: 182 experiment = mlflow.set_experiment(experiment_name=experiment_name) 183 experiment_id = experiment.experiment_id 184 185 # Start the run with the specified parameters 186 try: 187 # Start the run 188 active_run = mlflow.start_run( 189 experiment_id=experiment_id, 190 run_name=run_name, 191 nested=bool(parent_run_id), 192 parent_run_id=parent_run_id, 193 tags=tags_dict, 194 description=description, 195 ) 196 run_id = active_run.info.run_id 197 actual_experiment_id = active_run.info.experiment_id 198 199 # End the run with the specified status 200 mlflow.end_run(status=RunStatus.to_string(getattr(RunStatus, status.upper()))) 201 202 # Output the created run information 203 output = { 204 "run_id": run_id, 205 "experiment_id": actual_experiment_id, 206 "status": status.upper(), 207 "run_name": run_name, 208 } 209 210 click.echo(json.dumps(output, indent=2)) 211 212 except MlflowException as e: 213 raise click.ClickException(f"Failed to create run: {e.message}") 214 except Exception as e: 215 raise click.ClickException(f"Unexpected error creating run: {e!s}") 216 217 218 @commands.command("link-traces") 219 @mlflow_mcp(tool_name="link_traces_to_run") 220 @click.option( 221 "--run-id", 222 type=click.STRING, 223 required=True, 224 help="ID of the run to link traces to.", 225 ) 226 @click.option( 227 "trace_ids", 228 "--trace-id", 229 "-t", 230 multiple=True, 231 required=True, 232 help="Trace ID to link to the run. Can be specified multiple times (maximum 100 traces).", 233 ) 234 def link_traces(run_id: str, trace_ids: tuple[str, ...]) -> None: 235 """ 236 Link traces to a run. 237 238 This command links one or more traces to an existing run. Traces can be 239 linked to runs to establish relationships between traces and runs. 240 Maximum 100 traces can be linked in a single command. 241 """ 242 try: 243 client = MlflowClient() 244 client.link_traces_to_run(list(trace_ids), run_id) 245 246 # Output success message with count 247 click.echo(f"Successfully linked {len(trace_ids)} trace(s) to run '{run_id}'") 248 249 except MlflowException as e: 250 raise click.ClickException(f"Failed to link traces: {e.message}") 251 except Exception as e: 252 raise click.ClickException(f"Unexpected error linking traces: {e!s}")