Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1dc10b8
Add unit tests for atomic units and improve scratch directory handling
Felipedino Aug 1, 2026
fdfee4c
Update error message formatting in PrepareAndSplitUnit and add run_id…
Felipedino Aug 1, 2026
812dfe3
feat: Enhance dataset handling and add SaveDatasetUnit
Felipedino Aug 2, 2026
f5b26c6
feat: Improve error handling in ConverterJob and add test for dataset…
Felipedino Aug 2, 2026
39f556d
feat: Split ApplyConverterUnit into FitConverterUnit and TransformDat…
Felipedino Aug 3, 2026
d8f5c4c
test: Add the over-declared REQUIRES check to the contract audit
Felipedino Aug 5, 2026
946aa3a
Add contract tests for exploration and prediction units
Felipedino Aug 3, 2026
215a813
feat: Refactor manual prediction to use shared units and add comprehe…
Felipedino Aug 5, 2026
c043515
fix test python 3.10
Felipedino Aug 10, 2026
cb91ff8
remove lightgbm xgboost
Felipedino Aug 10, 2026
df47bc3
fix: route the SHAP predictor through predict_prepared
Felipedino Aug 14, 2026
ce22e7d
Add contract tests for dataset ingestion units and update unit schemas
Felipedino Aug 10, 2026
1b1ff8e
fix: Improve error messages for unknown explainers and data loaders
Felipedino Aug 13, 2026
3f648a6
test dag
Felipedino Aug 20, 2026
e028177
Add comprehensive tests for DAG tracking, unit evaluation, and model …
Felipedino Aug 22, 2026
9a7b2d2
Merge origin/develop into the atomized branch
Felipedino Sep 10, 2026
46024af
Split the preparing unit in two, over the registered splitters
Felipedino Sep 10, 2026
e215285
Let the fit decide what data it sees, and whether validation is part …
Felipedino Sep 10, 2026
4906f9f
Make the search measure an objective the fitting unit builds
Felipedino Sep 10, 2026
cb92af6
Ask the splitter which partitions a run has, when explaining it
Felipedino Sep 10, 2026
30a39bf
Replace the saved model only once the new one is complete
Felipedino Sep 10, 2026
7c708d4
Prepare the data through the units, on both training paths
Felipedino Sep 10, 2026
c761a00
Train a holdout run through the units, not the strategy
Felipedino Sep 10, 2026
9c6ac25
Extract what surrounds a fit, before there are two units doing it
Felipedino Sep 10, 2026
f8a3fc7
Train a cross-validated run through the units, unless it is nested
Felipedino Sep 10, 2026
77dbca1
Measure a nested run through the units too, and stop calling the stra…
Felipedino Sep 10, 2026
d476cf0
Leave the evaluation strategies declaring, and nothing else
Felipedino Sep 11, 2026
f30f3bc
Build the pruning tests' objective from the unit that fits
Felipedino Sep 11, 2026
8ce15eb
Fix what the review found, and teach the audit about inheritance
Felipedino Sep 11, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Merge the pipeline tracking and cross-validation heads.

Two lines of work reached the database at the same time and neither knew about
the other, so the revision graph ended with two heads and ``upgrade head``
refused to pick one:

``e7b4d1a9c206`` added the pipeline run tracking tables (``pipeline_run``,
``pipeline_node_run``, ``pipeline_node_artifact``); ``b7e4d2a19c63`` is the tip
of the branch that brought cross-validation and the evaluation reports, which
added ``model_session.evaluation_strategy``, ``run.nested``, the fold columns on
``metric`` and the ``report`` table.

There is nothing to do here. The two sides touch disjoint tables and disjoint
columns, so joining the graph is the whole of the change: this revision exists
to give the two heads a single successor.

Revision ID: 82fb7a6b8ac2
Revises: b7e4d2a19c63, e7b4d1a9c206
Create Date: 2026-09-10 13:03:45.322840

"""

from typing import Sequence, Union

# revision identifiers, used by Alembic.
revision: str = "82fb7a6b8ac2"
down_revision: Union[str, None] = ("b7e4d2a19c63", "e7b4d1a9c206")
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Join the two revision lines. Neither side needs anything applied."""


def downgrade() -> None:
"""Split them again, which is likewise nothing to undo."""
122 changes: 122 additions & 0 deletions DashAI/alembic/versions/e7b4d1a9c206_add_pipeline_run_tracking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""Add pipeline run tracking tables.

Separates the definition of a graph from its executions. ``Pipeline`` keeps the
definition, which changes as the user edits it; ``pipeline_run`` freezes the
steps and edges of one execution, ``pipeline_node_run`` tracks each node of it,
and ``pipeline_node_artifact`` holds what a node emitted, keyed by a key from
the unit's PROVIDES rather than by a column per node type.

The three JSON result columns on ``pipeline`` (exploration, train, prediction)
are deliberately left in place: the pipelines endpoints and the front's results
view still read them, so removing them belongs with rewriting those.

Revision ID: e7b4d1a9c206
Revises: d5b3c8f2a041
Create Date: 2026-08-20 00:00:00.000000
"""

from typing import Sequence, Union

import sqlalchemy as sa

from alembic import op

revision: str = "e7b4d1a9c206"
down_revision: Union[str, None] = "d5b3c8f2a041"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.create_table(
"pipeline_run",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("pipeline_id", sa.Integer(), nullable=False),
sa.Column("steps", sa.JSON(), nullable=True),
sa.Column("edges", sa.JSON(), nullable=True),
sa.Column("created", sa.DateTime(), nullable=False),
sa.Column("last_modified", sa.DateTime(), nullable=False),
sa.Column("delivery_time", sa.DateTime(), nullable=True),
sa.Column("start_time", sa.DateTime(), nullable=True),
sa.Column("end_time", sa.DateTime(), nullable=True),
sa.Column(
"status",
sa.Enum(
"NOT_STARTED",
"DELIVERED",
"STARTED",
"FINISHED",
"ERROR",
name="pipelinerunstatus",
),
nullable=False,
),
sa.Column("error_message", sa.String(), nullable=True),
sa.ForeignKeyConstraint(
["pipeline_id"],
["pipeline.id"],
name=op.f("fk_pipeline_run_pipeline_id_pipeline"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_pipeline_run")),
)

op.create_table(
"pipeline_node_run",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("pipeline_run_id", sa.Integer(), nullable=False),
sa.Column("node_id", sa.String(), nullable=False),
sa.Column("block_id", sa.String(), nullable=False),
sa.Column("node_type", sa.String(), nullable=False),
sa.Column("config", sa.JSON(), nullable=True),
sa.Column("input", sa.JSON(), nullable=True),
sa.Column("output", sa.JSON(), nullable=True),
sa.Column("created", sa.DateTime(), nullable=False),
sa.Column("last_modified", sa.DateTime(), nullable=False),
sa.Column("delivery_time", sa.DateTime(), nullable=True),
sa.Column("start_time", sa.DateTime(), nullable=True),
sa.Column("end_time", sa.DateTime(), nullable=True),
sa.Column(
"status",
sa.Enum(
"NOT_STARTED",
"DELIVERED",
"STARTED",
"FINISHED",
"ERROR",
"CANCELLED",
name="noderunstatus",
),
nullable=False,
),
sa.Column("error_message", sa.String(), nullable=True),
sa.ForeignKeyConstraint(
["pipeline_run_id"],
["pipeline_run.id"],
name=op.f("fk_pipeline_node_run_pipeline_run_id_pipeline_run"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_pipeline_node_run")),
)

op.create_table(
"pipeline_node_artifact",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("node_run_id", sa.Integer(), nullable=False),
sa.Column("key", sa.String(), nullable=False),
sa.Column("value", sa.JSON(), nullable=True),
sa.Column("created", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(
["node_run_id"],
["pipeline_node_run.id"],
name=op.f("fk_pipeline_node_artifact_node_run_id_pipeline_node_run"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_pipeline_node_artifact")),
)


def downgrade() -> None:
op.drop_table("pipeline_node_artifact")
op.drop_table("pipeline_node_run")
op.drop_table("pipeline_run")
2 changes: 0 additions & 2 deletions DashAI/back/api/api_v1/endpoints/predict.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,6 @@ async def delete_prediction(
@inject
async def preview_manual_prediction(
request: Request,
component_registry: "ComponentRegistry" = Depends(lambda: di["component_registry"]),
session_factory: "sessionmaker" = Depends(lambda: di["session_factory"]),
):
"""Run a synchronous manual prediction and return results without persisting.
Expand Down Expand Up @@ -413,7 +412,6 @@ async def preview_manual_prediction(
run_manual_prediction,
run_id=run_id_int,
manual_input_data=rows_data,
component_registry=component_registry,
session_factory=session_factory,
)
return {"columns": columns, "rows": rows}
21 changes: 21 additions & 0 deletions DashAI/back/core/enums/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,24 @@ class DatafileStatus(Enum):
DOWNLOADING = "downloading"
READY = "ready"
ERROR = "error"


class PipelineRunStatus(Enum):
NOT_STARTED = 0
DELIVERED = 1
STARTED = 2
FINISHED = 3
ERROR = 4


class NodeRunStatus(Enum):
NOT_STARTED = 0
DELIVERED = 1
STARTED = 2
FINISHED = 3
ERROR = 4
# A node that never ran because an earlier one failed. Distinct from
# NOT_STARTED, which is a node still waiting its turn: without the
# distinction a run that died halfway is indistinguishable from one still
# in flight.
CANCELLED = 5
28 changes: 28 additions & 0 deletions DashAI/back/dag/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""A sequential DAG engine over the atomic units.

Nothing here imports from ``DashAI/back/pipeline/``. That subsystem does not
run — its nodes implement two of ``BaseJob``'s four abstract methods, so they
cannot even be instantiated — and it is replaced rather than repaired.

The engine is deliberately sequential. Its predecessor was concurrent, and the
concurrency was not buying what it cost: the most expensive node in a pipeline
was already serialised behind an exclusive lock, every context and database
write was wrapped in another, and SQLite answered the remaining concurrent
writes with "database is locked" often enough to need retries with exponential
backoff. Units open a database session each, which is safe in sequence and was
the source of that contention in parallel.
"""

from DashAI.back.dag.graph import Edge, Graph, GraphError, Node, connect, sinks
from DashAI.back.dag.validate import resolve_unit_class, validate

__all__ = [
"Edge",
"Graph",
"GraphError",
"Node",
"connect",
"resolve_unit_class",
"sinks",
"validate",
]
Loading
Loading