Skip to content

Store dataset generation settings and reuse them - #10

Merged
duncankmckinnon merged 2 commits into
mainfrom
dataset-generation-settings
Aug 7, 2026
Merged

Store dataset generation settings and reuse them#10
duncankmckinnon merged 2 commits into
mainfrom
dataset-generation-settings

Conversation

@duncankmckinnon

Copy link
Copy Markdown
Owner

Follow-up to #9.

Why

Generation steering was used for one call and then discarded. Dataset persists only name, description, columns, and label_schema — so nothing could show how a dataset's rows came to be, and repeating an ask meant reconstructing the instructions, column notes, and label mix by hand.

Where the settings live

A separate DatasetGeneration table, not columns on Dataset. That's load-bearing: init_db is a bare create_all, which creates missing tables but never missing columns. New fields on Dataset would leave every existing SQLite database raising no such column on any dataset query; a new table is created on next start and existing data is untouched. test_init_db_adds_the_generation_table_to_an_existing_database drops the table and re-runs init_db to hold that property.

It also keeps provenance out of the dataset's own metadata — an uploaded dataset simply has no row, which is why the read returns null rather than an empty object.

Both generate routes record what they were asked for. set_generation replaces rather than accumulates: the settings describe the most recent ask, which is what a form repopulates from. source_version_id is provenance only — nothing reads through it to derive shape, since the version may since have changed or been deleted.

Three consumers

Read the settings. GET /api/datasets/{id}/generation returns them, or null for a dataset that was never generated. A collapsed panel on the dataset detail page renders them — the mix converted back to percentages, and the source version as an id rather than a link that could 404.

Top up an existing dataset. POST /api/datasets/{id}/generate-rows appends rows and falls back to the stored settings, so repeating an ask needs only {"count": 10}. Anything you do pass overrides the stored value and becomes the new stored ask, so the next top-up repeats what actually ran.

Shape is deliberately not overridable — the dataset's own columns and label space always apply, which is what keeps new rows compatible with the existing rows and with any evaluator already running against them. A dataset with an empty label schema gets no suggested labels and rejects label_guidance, matching the existing rule on /generate-from-version. Deleting a dataset drops its generation record, which would otherwise be unreachable.

The modal prefills from the stored settings, and prunes any note whose column has since been removed by an edit — sending it would fail the server's unknown-column check.

Duplicate a dataset. An action on the datasets list opens the generate form seeded from an existing dataset's shape and settings, so you can change one thing and produce a fresh dataset.

Shared plumbing

labelMix.fromProportions inverts toProportions so a stored mix repopulates the percent editor; both prefill paths use it.

DatasetGenerateForm takes an optional initial, read once at mount, with callers remounting via key to change it. That keeps the fields plain state rather than props that would have to be synced back on every keystroke.

Verification

  • Python: 583 passed, 1 skipped (was 563); ruff check and ruff format --check clean
  • Web: 247 tests passed (was 219); tsc --noEmit and production build clean

New coverage: the store round-trip and replace-not-accumulate behaviour, cascade on delete, the create_all migration property, settings stored by both generate routes, null for an uploaded dataset, fallback and override precedence on top-up, index continuation, shape being fixed by the dataset, and the prefill in both forms.

Note that web/dist is gitignored, so reviewing the UI from a checkout needs cd web && npm run build first.

🤖 Generated with Claude Code

Generation steering was used for one call and discarded, so nothing could show how a
dataset's rows came to be, and repeating an ask meant reconstructing it by hand.

Settings live in their own `DatasetGeneration` table rather than as columns on `Dataset`.
`init_db` is a bare `create_all`, which creates missing tables but never missing columns,
so fields here reach an existing database while new `Dataset` fields would leave one
raising `no such column` on every dataset query. It also keeps provenance out of the
dataset's own metadata: an uploaded dataset simply has no row. A test drops the table and
re-runs `init_db` to hold that property.

Both generate routes record what they were asked for, and `set_generation` replaces rather
than accumulates — the settings describe the most recent ask, which is what a form
repopulates from. `source_version_id` is provenance only; nothing reads through it to
derive shape, since the version may have changed or been deleted.

Three consumers:

- `GET /{id}/generation` returns the settings, or null for a dataset that was never
  generated. A collapsed panel on the dataset detail page renders them, with the mix shown
  as percentages and the source version as an id rather than a link that could 404.
- `POST /{id}/generate-rows` appends more rows, falling back to the stored settings so
  repeating an ask needs only a count. Overrides win and become the new stored ask.
  Shape is never overridable: the dataset's own columns and label space apply, which is
  what keeps new rows compatible with the existing ones. Deleting a dataset drops its
  generation record, which would otherwise be unreachable.
- A Duplicate action on the datasets list opens the generate form seeded from an existing
  dataset's shape and settings.

`labelMix.fromProportions` inverts `toProportions` so a stored mix repopulates the percent
editor; both prefill paths use it. `DatasetGenerateForm` takes an optional `initial` read
once at mount, with callers remounting via `key` to change it — that keeps the fields plain
state instead of props needing to be synced back on every edit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Every conflict resolved to main's side, because this branch has nothing left to
contribute. Its commit 912d6f1 and main's ed5915f are the same work committed
twice — identical patch-ids, and 912d6f1's tree is byte-identical to ed5915f's.
The web-ui-redesign branch was cut on top of ed5915f, so PR #11 already shipped
the generation-settings work; the merge base falls back to 4997006, which is the
only reason git saw seven conflicts at all.

The resulting tree equals main's exactly, so PR #10's diff is now empty.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@duncankmckinnon
duncankmckinnon merged commit 7411d5c into main Aug 7, 2026
6 checks passed
@duncankmckinnon
duncankmckinnon deleted the dataset-generation-settings branch August 7, 2026 23:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants