Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
b6e8e81
feat: implement dataset column rebuilding for transformed datasets
Creylay Sep 8, 2026
26cd0b3
feat: add column reference models and tests for session-level preproc…
Creylay Sep 8, 2026
dfdb054
feat: implement SessionPreprocessor for dataset transformation and fi…
Creylay Sep 8, 2026
1e6f63b
feat: dispatch a PreprocessingJob when a session has converters
Creylay Sep 9, 2026
a3b5e5e
feat: apply persisted preprocessing to training, prediction and expla…
Creylay Sep 9, 2026
7777eb6
fix: exclude a converter's untouched passthrough column from its outp…
Creylay Sep 9, 2026
6c88e19
feat: expose a converter's concrete output dtype in its metadata
Creylay Sep 9, 2026
74573be
feat: build the session preprocessing wizard, mirroring Notebooks' co…
Creylay Sep 9, 2026
a2e21a5
feat: track session preprocessing status via the shared job-polling m…
Creylay Sep 9, 2026
f8c2b73
fix: surface the real backend error when creating a Run fails
Creylay Sep 9, 2026
c697cdf
feat: Enhance session preprocessing to support output slots for mixed…
Creylay Sep 10, 2026
ec78672
feat: Implement rawColumnsNeededFor function to resolve dataset colum…
Creylay Sep 10, 2026
de0fd15
feat: Default input selection to last preprocessing step's output gro…
Creylay Sep 10, 2026
137b249
feat(i18n): update translations for experiments and models
Creylay Sep 11, 2026
656a35d
Merge remote-tracking branch 'origin/develop' into feat/session-prepr…
Creylay Sep 11, 2026
bfb15d6
fix: update down_revision to correct migration reference
Creylay Sep 11, 2026
8dd8e2c
feat: enhance converter output display with disambiguation for duplic…
Creylay Sep 11, 2026
ad36abb
feat: add convertersMeta prop to enhance converter output handling ac…
Creylay Sep 11, 2026
8ed098e
feat: enhance InfoModal layout with flexible sizing and overflow hand…
Creylay Sep 11, 2026
a2f506a
feat: add appliedConverters label to multiple language files and enha…
Creylay Sep 11, 2026
2014a6b
feat: add LEARNS_FROM_DATA flag to converters to indicate data leakag…
Creylay Sep 22, 2026
9133346
feat: add data leakage warning to ParameterStepConverter and localize…
Creylay Sep 22, 2026
49f41eb
feat: add tests for learns_from_data metadata in converters
Creylay Sep 22, 2026
9373426
fix: update dill package version to 0.4.1 for compatibility
Creylay Sep 22, 2026
465f133
fix: increase padding in ModelsRightBar for improved UI spacing
Creylay Sep 22, 2026
46b85bf
fix: update translation keys for input and output column labels in Di…
Creylay Sep 22, 2026
8d9a9c7
fix: replace useEffect with useLayoutEffect to prevent flashing of in…
Creylay Sep 22, 2026
813af1f
fix: update applyPreprocessing label to include "(advanced)" in multi…
Creylay Sep 28, 2026
991f801
feat: implement session preprocessing structure types and infer_outpu…
Creylay Sep 28, 2026
512d81a
feat: add COLUMN_OPERATION attribute to various converters for enhanc…
Creylay Sep 28, 2026
71daad1
feat: enhance output column inference across various converters with …
Creylay Sep 28, 2026
6ae36ae
Enhance converters with infer_output_columns method and structure est…
Creylay Sep 28, 2026
6f67a16
feat: implement column name planning and enhance session preprocessin…
Creylay Sep 28, 2026
e17b5b3
feat: add infer_structure function and corresponding tests for sessio…
Creylay Sep 28, 2026
a04dc5c
feat: implement preprocessing structure estimation and validation end…
Creylay Sep 28, 2026
f6d582f
feat: add preprocessing structure estimation and related hooks, types…
Creylay Sep 28, 2026
18f2964
feat: add BaseColumnsStep component and integrate it into session cre…
Creylay Sep 28, 2026
330f15a
Refactor session preprocessing components to improve structure estima…
Creylay Sep 28, 2026
0ee3f79
feat: update session preprocessing structure and validation logic
Creylay Sep 28, 2026
0b5246e
feat: add leakage warning toggle to ParameterStepConverter and update…
Creylay Sep 29, 2026
b938b2f
Merge remote-tracking branch 'origin' into feat/session-preprocessing…
Creylay Sep 30, 2026
02d4ae7
Merge develop into feat/session-preprocessing-structure
cristian-tamblay Oct 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 188 additions & 41 deletions DashAI/back/api/api_v1/endpoints/model_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@
ColumnsValidationParams,
ModelSessionBulkDeleteParams,
ModelSessionParams,
PreprocessingStructureParams,
)
from DashAI.back.api.utils import remove_path
from DashAI.back.dependencies.database.models import Dataset, ModelSession, Run
from DashAI.back.job.preprocessing_job import PreprocessingJob
from DashAI.back.preprocessing.column_ref import ConverterSequence, RawColumnRef
from DashAI.back.preprocessing.structure import (
StructureError,
infer_structure,
resolve_state_refs,
)
from DashAI.back.splitters.splits_payload import (
META_KEYS,
normalize_splits_payload,
Expand Down Expand Up @@ -121,40 +127,119 @@ async def get_model_session(
return model_session


@router.post("/validation")
@inject
async def validate_columns(
params: ColumnsValidationParams,
component_registry: "ComponentRegistry" = Depends(lambda: di["component_registry"]),
session_factory: "sessionmaker" = Depends(lambda: di["session_factory"]),
):
"""Validate if dataset columns are compatible with a task."""
def _load_sample_dataset(db, dataset_id: int):
"""Load the first rows of a dataset, enough for its columns and types.

Parameters
----------
db : Session
An open database session.
dataset_id : int
The dataset to load.

Returns
-------
DashAIDataset
At most 5 rows of the dataset, with every column and its type.

Raises
------
HTTPException
404 if the dataset is not registered.
"""
import os

import pyarrow as pa
import pyarrow.ipc as ipc

from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset

dataset = db.get(Dataset, dataset_id)
if not dataset:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Dataset not found",
)
data_filepath = os.path.join(f"{dataset.file_path}/dataset", "data.arrow")
with pa.OSFile(data_filepath, "rb") as source:
reader = ipc.open_file(source)
batch = reader.get_batch(0)
sample_batch = batch.slice(0, min(5, batch.num_rows))
return DashAIDataset(pa.Table.from_batches([sample_batch]))


def _type_allowed(type_name, allowed_types) -> bool:
"""Whether a column type is one of a task's allowed input types.

A task declaring the DashAIValue wildcard accepts every concrete value
type; an unknown type (None) is let through, since only a fit can tell.
"""
if not allowed_types or type_name is None:
return True
return type_name in allowed_types or (
"DashAIValue" in allowed_types and type_name in _DASHAI_VALUE_TYPE_NAMES
)


@router.post("/preprocessing/structure")
@inject
async def get_preprocessing_structure(
params: PreprocessingStructureParams,
component_registry: "ComponentRegistry" = Depends(lambda: di["component_registry"]),
session_factory: "sessionmaker" = Depends(lambda: di["session_factory"]),
):
"""Estimate the dataset structure after every step of a converter chain.

Read only: nothing is fit and nothing is stored. The session wizard calls
it on every change to the chain, to offer only columns that exist at each
step and to flag steps that cannot work.

Parameters
----------
params : PreprocessingStructureParams
The dataset, the candidate and target columns, and the chain.

Returns
-------
StructureResult
The state after every step, their errors and warnings, and the final
state.
"""
with session_factory() as db:
try:
dataset = db.get(Dataset, params.dataset_id)
if not dataset:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Dataset not found",
)
sample = _load_sample_dataset(db, params.dataset_id)
except exc.SQLAlchemyError as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal database error",
) from e
return infer_structure(
dataset_types=sample.types,
candidates=params.candidates,
target=params.output_columns,
steps=params.steps,
component_registry=component_registry,
)

dataset_path = f"{dataset.file_path}/dataset"
data_filepath = os.path.join(dataset_path, "data.arrow")
with pa.OSFile(data_filepath, "rb") as source:
reader = ipc.open_file(source)
batch = reader.get_batch(0)
sample_size = min(5, batch.num_rows)
sample_batch = batch.slice(0, sample_size)

table = pa.Table.from_batches([sample_batch])
minimal_dataset = DashAIDataset(table)
@router.post("/validation")
@inject
async def validate_columns(
params: ColumnsValidationParams,
component_registry: "ComponentRegistry" = Depends(lambda: di["component_registry"]),
session_factory: "sessionmaker" = Depends(lambda: di["session_factory"]),
):
"""Validate if dataset columns are compatible with a task.

With `preprocessing`, every input ref is resolved against the chain's
estimated final structure and its type checked against the task's input
types; the original columns among them also go through the task's own
validation, as without preprocessing.
"""
with session_factory() as db:
try:
minimal_dataset = _load_sample_dataset(db, params.dataset_id)

column_names = minimal_dataset.column_names

Expand Down Expand Up @@ -192,31 +277,42 @@ async def validate_columns(

task: "BaseTask" = component_registry[params.task_name]["class"]()

if group_refs:
declared_types = params.converter_output_types or {}
task_metadata = task.get_metadata()
allowed_input_types = set(task_metadata.get("inputs_types", []))
for ref in group_refs:
# A step with a heterogeneous scope (see SessionPreprocessor.
# _classify_by_type) can declare more than one type, one per
# slot — "{step}:{slot}" disambiguates which one a slotted ref
# means; an unslotted ref (the whole step) keeps the plain
# "{step}" key, unchanged from before slots existed.
key = str(ref.step) if ref.slot is None else f"{ref.step}:{ref.slot}"
declared_type = declared_types.get(key)
type_ok = declared_type in allowed_input_types or (
"DashAIValue" in allowed_input_types
and declared_type in _DASHAI_VALUE_TYPE_NAMES
if params.preprocessing:
allowed_input_types = set(task.get_metadata().get("inputs_types", []))
structure = infer_structure(
dataset_types=minimal_dataset.types,
candidates=list(column_names),
target=outputs_names,
steps=params.preprocessing,
component_registry=component_registry,
)
if not structure.valid:
return {
"dataset_status": "invalid",
"error": "The preprocessing chain has an invalid step.",
}
try:
items = resolve_state_refs(
params.input_refs or [], structure.final, set(outputs_names)
)
if allowed_input_types and not type_ok:
except StructureError as e:
return {"dataset_status": "invalid", "error": e.message.code}
for item in items:
if not _type_allowed(item.type, allowed_input_types):
return {
"dataset_status": "invalid",
"error": (
f"Converter step {ref.step} declares output type "
f"'{declared_type}', which is not one of the task's "
f"Input of type '{item.type}' is not one of the task's "
f"allowed input types {sorted(allowed_input_types)}."
),
}
elif group_refs:
# A ref to a converter's output only has a type through the chain's
# estimated structure, which needs the preprocessing steps.
return {
"dataset_status": "invalid",
"error": "References to converter outputs need the preprocessing steps.",
}

validation_response = {}

Expand Down Expand Up @@ -286,6 +382,52 @@ def _validate_splits(splits: str, component_registry: "ComponentRegistry") -> No
) from e


def _check_preprocessing_structure(
db, params, sequence, input_column_refs, component_registry
) -> None:
"""Reject a session whose chain or inputs cannot work, before creating it.

Without this, the session would be created and its PreprocessingJob
would only fail afterwards, with an error like a bare missing column
name.

Raises
------
HTTPException
422 with the estimated structure when a step is invalid, or with the
error when an input ref points at something the chain does not
produce (or consumed).
"""
sample = _load_sample_dataset(db, params.dataset_id)
structure = infer_structure(
dataset_types=sample.types,
candidates=list(sample.column_names),
target=params.output_columns,
steps=sequence.steps,
component_registry=component_registry,
)
if not structure.valid:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail={
"message": "The preprocessing chain has an invalid step.",
"structure": structure.model_dump(mode="json"),
},
)
try:
resolve_state_refs(
input_column_refs, structure.final, set(params.output_columns)
)
except StructureError as e:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail={
"message": "An input column does not exist after preprocessing.",
"error": e.message.model_dump(mode="json"),
},
) from e


@router.post("/", status_code=status.HTTP_201_CREATED)
@inject
async def create_model_session(
Expand Down Expand Up @@ -362,6 +504,11 @@ async def create_model_session(
schema = reader.schema
column_names = schema.names

if has_preprocessing:
_check_preprocessing_structure(
db, params, sequence, input_column_refs, component_registry
)

# When preprocessing is configured, input_columns is only a
# placeholder until PreprocessingJob resolves the real ones, so
# it cannot be checked against the raw dataset's column count.
Expand Down
14 changes: 12 additions & 2 deletions DashAI/back/api/api_v1/schemas/model_sessions_params.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, List, Optional
from typing import List, Optional

from pydantic import BaseModel, Field

Expand Down Expand Up @@ -26,7 +26,17 @@ class ColumnsValidationParams(BaseModel):
inputs_columns: List[str]
outputs_columns: List[str]
input_refs: Optional[List[ColumnRef]] = None
converter_output_types: Optional[Dict[str, str]] = None
# With preprocessing, the type of every input ref is taken from the
# chain's estimated structure (see infer_structure).
preprocessing: Optional[List[ConverterStep]] = None


class PreprocessingStructureParams(BaseModel):
dataset_id: int
# Original columns the user may feed into the chain.
candidates: List[str]
output_columns: List[str]
steps: List[ConverterStep] = Field(default_factory=list)


class ModelSessionBulkDeleteParams(BaseModel):
Expand Down
Loading
Loading