Skip to content

Commit 38b05d0

Browse files
committed
Refactor tests to support group-atoms and improve converter handling
- Updated `test_runs_api.py` to utilize group-atoms for input and output columns in model sessions, ensuring converters are correctly applied and persisted. - Added new tests for predicting with session converters and validating the behavior of group atoms in input/output columns. - Enhanced `test_session_preprocessing_job.py` to verify that preprocessing correctly saves separate x and y files, and resolves group atoms per fold in cross-validation. - Introduced tests for handling output columns as group atoms, ensuring proper error handling when such configurations are attempted. - Improved `test_dashai_dataset.py` to ensure that column selection correctly prunes saved type metadata, preventing stale schema information from persisting after dataset modifications.
1 parent 8d0a9a3 commit 38b05d0

54 files changed

Lines changed: 4353 additions & 862 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

DashAI/back/api/api_v1/endpoints/explainers.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,10 +1023,22 @@ async def validate_dataset(
10231023
detail="Internal database error",
10241024
) from e
10251025

1026+
from DashAI.back.job.base_job import JobError
1027+
from DashAI.back.job.session_preprocessing_job import (
1028+
get_real_input_output_columns,
1029+
)
1030+
10261031
validation_response = {}
1027-
input_columns = model_session.input_columns
1028-
output_columns = model_session.output_columns
1029-
required_columns = input_columns + output_columns
1032+
try:
1033+
real_input_columns, real_output_columns = get_real_input_output_columns(
1034+
model_session
1035+
)
1036+
except JobError as e:
1037+
raise HTTPException(
1038+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
1039+
detail=str(e),
1040+
) from e
1041+
required_columns = real_input_columns + real_output_columns
10301042

10311043
instances_columns = list(instances.features)
10321044

@@ -1122,14 +1134,29 @@ async def valid_datasets(
11221134
detail="Internal database error",
11231135
) from e
11241136

1137+
from DashAI.back.job.base_job import JobError
1138+
from DashAI.back.job.session_preprocessing_job import (
1139+
get_real_input_output_columns,
1140+
)
1141+
1142+
try:
1143+
real_input_columns, real_output_columns = get_real_input_output_columns(
1144+
model_session
1145+
)
1146+
except JobError as e:
1147+
raise HTTPException(
1148+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
1149+
detail=str(e),
1150+
) from e
1151+
11251152
# Fallback only: a converter that adds/renames input columns
11261153
# (BagOfWords' `bow_<word>`, PCA's `pca0`/`pca1`) means
11271154
# `model_session.input_columns` only exist in the session's
11281155
# preprocessed data, never in a raw dataset — every candidate used to
11291156
# fail this check. Replaced below with the training dataset's own raw
11301157
# schema whenever it's readable; kept here only for the rare case that
11311158
# read fails, so this doesn't silently accept every dataset.
1132-
required_columns = model_session.input_columns + model_session.output_columns
1159+
required_columns = real_input_columns + real_output_columns
11331160

11341161
training_types = {}
11351162
training_spec = {}
@@ -1144,8 +1171,8 @@ async def valid_datasets(
11441171

11451172
if training_spec:
11461173
required_columns = [
1147-
col for col in training_spec if col not in model_session.output_columns
1148-
] + model_session.output_columns
1174+
col for col in training_spec if col not in real_output_columns
1175+
] + real_output_columns
11491176

11501177
valid_dataset_ids = []
11511178
for dataset in datasets:

DashAI/back/api/api_v1/endpoints/model_sessions.py

Lines changed: 397 additions & 112 deletions
Large diffs are not rendered by default.

DashAI/back/api/api_v1/endpoints/predict.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
Prediction,
1515
Run,
1616
)
17+
from DashAI.back.job.base_job import JobError
1718
from DashAI.back.job.predict_job import run_manual_prediction
1819

1920
if TYPE_CHECKING:
@@ -154,6 +155,9 @@ async def filter_datasets_endpoint(
154155
``{"valid_dataset_ids": [...]}`` with the ids of the matching datasets.
155156
"""
156157
from DashAI.back.dataloaders.classes.dashai_dataset import get_columns_spec
158+
from DashAI.back.job.session_preprocessing_job import (
159+
get_real_input_output_columns,
160+
)
157161

158162
try:
159163
with session_factory() as db:
@@ -188,7 +192,10 @@ async def filter_datasets_endpoint(
188192
trained_columns_spec = get_columns_spec(
189193
f"{trained_dataset.file_path}/dataset"
190194
)
191-
output_columns = set(model_session.output_columns)
195+
_real_input_columns, real_output_columns = get_real_input_output_columns(
196+
model_session
197+
)
198+
output_columns = set(real_output_columns)
192199
input_columns = [
193200
col for col in trained_columns_spec if col not in output_columns
194201
]
@@ -209,6 +216,14 @@ async def filter_datasets_endpoint(
209216
except HTTPException:
210217
# Re-raise HTTPExceptions as-is
211218
raise
219+
except JobError as e:
220+
# A session that isn't fully configured yet (e.g. no real output
221+
# column resolved) — a client-actionable state, not an unexpected
222+
# internal error.
223+
raise HTTPException(
224+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
225+
detail=str(e),
226+
) from e
212227
except Exception as e:
213228
logger.exception("Error filtering datasets: %s", str(e))
214229
raise HTTPException(

DashAI/back/api/api_v1/schemas/model_sessions_params.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,52 @@
1-
from typing import Any, Dict, List, Optional
1+
from typing import Any, Dict, List, Literal, Optional
22

3-
from pydantic import BaseModel
3+
from pydantic import BaseModel, field_validator
4+
5+
6+
class ColumnAtom(BaseModel):
7+
"""A selectable unit in the session wizard: either a literal column of
8+
the raw dataset, or the whole output group (one declared slot) of a
9+
converter already configured earlier in the same converters list.
10+
11+
A `group` atom always selects the entire slot: there is no way to pick
12+
a subset of a slot's real columns, since their exact names/count are
13+
not known until the real fit runs (see the design spec, section B).
14+
"""
15+
16+
kind: Literal["column", "group"]
17+
name: Optional[str] = None
18+
converter_id: Optional[str] = None
19+
slot: Optional[int] = None
20+
21+
22+
def _coerce_atom(value: Any) -> Any:
23+
"""Coerce a bare column-name string into a literal `column` atom.
24+
25+
Kept for backward compatibility: callers that predate the atom-based
26+
column scope (e.g. `input_columns=["SepalLengthCm"]`) still POST plain
27+
strings. Anything that isn't a string (a dict, or an already-built
28+
`ColumnAtom`) is passed through unchanged for pydantic to validate
29+
normally.
30+
"""
31+
if isinstance(value, str):
32+
return ColumnAtom(kind="column", name=value)
33+
return value
434

535

636
class SessionConverterParams(BaseModel):
37+
id: str
738
converter: str
839
params: Dict[str, Any] = {}
9-
columns: List[str] = []
40+
input_scope: List[ColumnAtom] = []
1041
target_column: Optional[str] = None
1142

43+
@field_validator("input_scope", mode="before")
44+
@classmethod
45+
def _coerce_input_scope(cls, value: Any) -> Any:
46+
if isinstance(value, list):
47+
return [_coerce_atom(item) for item in value]
48+
return value
49+
1250

1351
class UpdateConvertersParams(BaseModel):
1452
converters: List[SessionConverterParams] = []
@@ -18,15 +56,22 @@ class ModelSessionParams(BaseModel):
1856
dataset_id: int
1957
task_name: str
2058
name: str
21-
input_columns: List[str] = []
22-
output_columns: List[str] = []
59+
input_columns: List[ColumnAtom] = []
60+
output_columns: List[ColumnAtom] = []
2361
train_metrics: List[str]
2462
validation_metrics: List[str]
2563
test_metrics: List[str]
2664
evaluation_strategy: str
2765
splits: str
2866
converters: List[SessionConverterParams] = []
2967

68+
@field_validator("input_columns", "output_columns", mode="before")
69+
@classmethod
70+
def _coerce_columns(cls, value: Any) -> Any:
71+
if isinstance(value, list):
72+
return [_coerce_atom(item) for item in value]
73+
return value
74+
3075

3176
class ColumnsValidationParams(BaseModel):
3277
task_name: str

DashAI/back/dataloaders/classes/dashai_dataset.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,24 @@ def select_columns(self, column_names: Union[str, List[str]]) -> "DashAIDataset"
631631
col: self._types[col] for col in column_names if col in self._types
632632
}
633633

634+
# `pa.Table.select()` only prunes columns; it does not touch the
635+
# table's own schema-level `dashai_types` metadata blob, which
636+
# would otherwise still list every column from the *original*
637+
# table, not just the ones kept here. Left uncorrected, saving
638+
# this table to disk and reloading it (`save_dataset`/
639+
# `load_dataset`, which derive `.types` from that same blob when
640+
# no explicit `types=` is given) would silently reintroduce
641+
# phantom type entries for columns that no longer exist in the
642+
# actual data — crashing the first thing that iterates `.types`
643+
# expecting it to match the real columns (e.g.
644+
# `categorical_label_encoder`).
645+
from DashAI.back.types.utils import save_types_in_arrow_metadata
646+
647+
subset_table = save_types_in_arrow_metadata(
648+
subset_table,
649+
{col: t.to_string() for col, t in subset_types.items()},
650+
)
651+
634652
return DashAIDataset(table=subset_table, splits=self.splits, types=subset_types)
635653

636654
def __getitem__(self, key):

0 commit comments

Comments
 (0)