Skip to content

Commit 9ffb208

Browse files
nbowen3dionhaefner
andauthored
feat: add ability to specify mlflow tag for tesseract (#426)
<!-- Please use a PR title that conforms to *conventional commits*: "<commit_type>: Describe your change"; for example: "fix: prevent race condition". Some other commit types are: fix, feat, ci, doc, refactor... For a full list of commit types visit https://www.conventionalcommits.org/en/v1.0.0/ --> #### Relevant issue or PR <!-- If the changes resolve an issue or follow some other PR, link to them here. Only link something if it is directly relevant. --> #### Description of changes <!-- Add a high-level description of changes, focusing on the *what* and *why*. --> This adds the ability to specify mlflow tags for Tesseract runs via the `TESSERACT_MLFLOW_TAGS` env variable. This is useful to embed additional metadata into the MLFlow run. n.b. As we want to ensure that all runs have the tags if set, we need to make a small change here to check if there is an active run. Previously, if there was no an active run and you logged something, it would implicitly create one. That is a problem as it will not include the tags. Instead we now explicitly check if there an active run; and, if not, we create one with the tags. #### Testing done <!-- Describe how the changes were tested; e.g., "CI passes", "Tested manually in stagingrepo#123", screenshots of a terminal session that verify the changes, or any other evidence of testing the changes. --> Local and unit testing --------- Co-authored-by: Dion Häfner <dion.haefner@simulation.science>
1 parent 1181ed1 commit 9ffb208

5 files changed

Lines changed: 89 additions & 4 deletions

File tree

docs/content/using-tesseracts/advanced.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,30 @@ $ tesseract serve --env=TESSERACT_MLFLOW_TRACKING_URI="..." \
7575
metrics
7676
````
7777
78+
If you wish to pass additional parameters to the MLflow run (such as tags, run name, or description), you can do so via the `TESSERACT_MLFLOW_RUN_EXTRA_ARGS` environment variable. This accepts a Python dictionary string that is passed directly to `mlflow.start_run()`. See supported
79+
parameters in the [mlflow documentation](https://mlflow.org/docs/latest/api_reference/python_api/mlflow.html#mlflow.start_run).
80+
81+
**Example: Setting tags only**
82+
```bash
83+
$ tesseract serve --env=TESSERACT_MLFLOW_TRACKING_URI="..." \
84+
--env=TESSERACT_MLFLOW_RUN_EXTRA_ARGS='{"tags": {"key1": "value1", "key2": "value2"}}' \
85+
metrics
86+
```
87+
88+
**Example: Setting run name and tags**
89+
```bash
90+
$ tesseract serve --env=TESSERACT_MLFLOW_TRACKING_URI="..." \
91+
--env=TESSERACT_MLFLOW_RUN_EXTRA_ARGS='{"run_name": "my_experiment", "tags": {"env": "production"}}' \
92+
metrics
93+
```
94+
95+
**Example: Multiple parameters**
96+
```bash
97+
$ tesseract serve --env=TESSERACT_MLFLOW_TRACKING_URI="..." \
98+
--env=TESSERACT_MLFLOW_RUN_EXTRA_ARGS='{"run_name": "test_run", "description": "Testing new feature", "tags": {"version": "1.0"}}' \
99+
metrics
100+
```
101+
78102
## Volume mounts and user permissions
79103
80104
When mounting a volume into a Tesseract container, default behavior depends on the Docker engine being used. Specifically, Docker Desktop, Docker Engine, and Podman have different ways of handling user permissions for mounted volumes.

tesseract_core/runtime/cli.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ def main_callback(**kwargs: Any) -> None:
141141
if get_origin(field_type) is Literal:
142142
field_type = make_choice_enum(f"{field_name}Choices", get_args(field_type))
143143

144+
if get_origin(field_type) is dict:
145+
# Dicts are parsed as strings
146+
field_type = str
147+
144148
params.append(
145149
inspect.Parameter(
146150
field_name,

tesseract_core/runtime/config.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
# Copyright 2025 Pasteur Labs. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
33

4+
import ast
45
import os
56
from pathlib import Path
6-
from typing import Any
7+
from typing import Annotated, Any
78

8-
from pydantic import BaseModel, ConfigDict, FilePath
9+
from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, FilePath
910

1011
from tesseract_core.runtime.file_interactions import supported_format_type
1112

1213

14+
def _eval_str(obj: Any) -> Any:
15+
"""Evaluate a string into the corresponding Python object."""
16+
if isinstance(obj, str):
17+
try:
18+
return ast.literal_eval(obj)
19+
except SyntaxError as exc:
20+
raise ValueError("Could not parse string as Python object") from exc
21+
return obj
22+
23+
1324
class RuntimeConfig(BaseModel):
1425
"""Available runtime configuration."""
1526

@@ -25,6 +36,9 @@ class RuntimeConfig(BaseModel):
2536
mlflow_tracking_uri: str = ""
2637
mlflow_tracking_username: str = ""
2738
mlflow_tracking_password: str = ""
39+
mlflow_run_extra_args: Annotated[dict[str, Any], BeforeValidator(_eval_str)] = (
40+
Field(default_factory=dict)
41+
)
2842

2943
model_config = ConfigDict(frozen=True, extra="forbid")
3044

tesseract_core/runtime/mpa.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,10 @@ def log_artifact(self, local_path: str) -> None:
215215
self.mlflow.log_artifact(local_path)
216216

217217
def start_run(self) -> None:
218-
"""Start a new MLflow run."""
219-
self.mlflow.start_run()
218+
"""Start a new MLflow run with optional extra arguments from config."""
219+
config = get_config()
220+
run_extra_args = config.mlflow_run_extra_args
221+
self.mlflow.start_run(**run_extra_args)
220222

221223
def end_run(self) -> None:
222224
"""End the current MLflow run."""

tests/runtime_tests/test_mpa.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import sqlite3
1010

1111
import pytest
12+
from pydantic import ValidationError
1213

1314
from tesseract_core.runtime import mpa
1415
from tesseract_core.runtime.config import update_config
@@ -293,3 +294,43 @@ def test_build_tracking_uri_sqlite_ignores_credentials():
293294
assert "testuser" not in tracking_uri
294295
assert "testpass" not in tracking_uri
295296
assert tracking_uri.startswith("sqlite:///")
297+
298+
299+
def test_mlflow_run_extra_args(mocker):
300+
"""Test passing a dict with basic tags."""
301+
pytest.importorskip("mlflow")
302+
303+
kwargs = {"tags": {"env": "prod", "team": "ml"}}
304+
kwargs_str = repr(kwargs)
305+
306+
update_config(mlflow_run_extra_args=kwargs_str)
307+
mocked_mlflow = mocker.Mock()
308+
309+
backend = mpa.MLflowBackend()
310+
backend.mlflow = mocked_mlflow
311+
312+
# Make sure kwargs are forwarded correctly to mlflow.start_run
313+
backend.start_run()
314+
mocked_mlflow.start_run.assert_called_with(**kwargs)
315+
316+
317+
def test_mlflow_run_extra_args_parsing():
318+
# This is actually a test for config.py but we add it here for now
319+
320+
with pytest.raises(ValidationError):
321+
# Not a valid Python object
322+
update_config(mlflow_run_extra_args="{'unbalanced dict': True")
323+
324+
with pytest.raises(ValidationError):
325+
# Not a dict
326+
update_config(mlflow_run_extra_args="['this is a list']")
327+
328+
with pytest.raises(ValidationError):
329+
# Not str keys
330+
update_config(mlflow_run_extra_args="{0: 'hey there'}")
331+
332+
# All good
333+
update_config(mlflow_run_extra_args="{'hey there': 'general kenobi'}")
334+
335+
# Passing dicts directly is fine, too
336+
update_config(mlflow_run_extra_args={"hey there": "general kenobi"})

0 commit comments

Comments
 (0)