Skip to content

feat(agents): prepare_data changes the data in step 1, with undo - #12133

Merged
MarkusNeusinger merged 10 commits into
mainfrom
feat/agents-prepare-data
Oct 11, 2026
Merged

MarkusNeusinger merged 10 commits into
mainfrom
feat/agents-prepare-data

Conversation

@MarkusNeusinger

Copy link
Copy Markdown
Owner

Summary

  • The assistant changes your data in step 1. The root agent's new prepare_data tool chooses from a fixed set of pandas operations (rename, drop_columns, drop_rows, set_type, aggregate, sort, pivot_longer, pivot_wider, at most five per call) that the agents service applies to the stored canonical CSV and stores as the next dataset version (the last five kept for POST .../dataset/undo). The model never writes data rows; ToolSafety validates the operations against the same models first. The turn's done event carries the new version with one change line per operation, unconfirmed, and the BFF relays a done of up to 512 KiB so a wide dataset's change list always reaches the page.
  • Step-1 summary and the page. After every stored parse the page sends the action summarize_data, so the assistant's two-to-four-sentence description of the data (the profile now carries mean and median) is the first turn in the thread; data requests in words go through the composer before the confirmation. A done with dataset replaces the preview and the bindings, takes the page back to step 1 and shows a # changed block with the change lines and .undo(). Two enum-only analytics events: agent_data_prepared{changes}, agent_data_undo{version}.
  • Run queue. A turn that starts while the data is unconfirmed cannot render (the pipeline answers not_ready, nothing can confirm during a turn), so it takes the slot but no start in the one-a-minute window: the summary, a data request and the plot after the confirmation no longer wait a minute for each other, while whole pipeline runs keep the minute rule.
  • Prep eval. python -m agents.evals.prep drives 17 German and English data requests through /v1 with the real model and scores the root's prepare_data calls by exact operation match (80 % gate). Three runs on 2026-10-11 with Claude Haiku 5.5 on eu: 16/17 before the review fixes, 17/17 after them, 17/17 after the merge with feat(agents): the parser reads units, flat objects and Python literals #12132 (about $0.03 each).

Built with a workflow (implementer, two adversarial verifiers, fix pass) plus two delegates (frontend; the merge follow-up for #12132's parser units). The verifiers' findings that hold are all fixed in 4fa98ae85: the done event cap, the MM/DD/YYYY swap scoped to parser-flagged ambiguous columns, 12,5 never read as 125, JSON numbers as condition values with validation lines on refusals, the long-format size cap, set_type text kept through the re-parse (text_columns), the idle sweep per session, the parser's per-column time flag carried through the operations.

Left as designed, for the owner's eye: a rename's name is exempt from the link rule like a condition's value (a column may be renamed to what the user's table holds); set_type datetime without a format uses pandas' per-cell inference; a year period re-parses as an integer column. Two parser follow-ups found after the merge are a Todoist task (claude label): thousands commas behind a currency ($1,200 reads as 1.2 since #12132's unit rule, and a numeric column cannot be re-read), and the unit staying out of the column name after set_type number on a mixed column.

Plan

Design: Two steps: data first, then the plot. Order agreed with the owner on 2026-10-11: (1) the confirm gate (#12131), (2) this PR, (3) the chat-first data layout, (4) the .save() export.

Test plan

  • ruff check, ruff format --check, mypy api core agents, pytest tests/unit tests/integration (8,981 passed), tools.changelog check
  • cd app && yarn lint && yarn fm:check && yarn type-check && yarn test (805 passed) && yarn build
  • Live prep eval 17/17 on the branch head (report in the session scratchpad; reports are gitignored)
  • Browser walk-through against the mock BFF (desktop 1280 and phone 390, light and dark): paste → summary as the first turn → "das ist prozent" → # changed · version 2 with the change line → .undo() → # undone · back to version 1 → "gruppier nach x" → .confirm() → .create_plot() renders
  • After the deploy: paste the owner's CSV in production, ask "das ist Prozent, nicht Anzahl" and "gruppier nach Monat", undo, confirm, create the plot

MarkusNeusinger and others added 7 commits October 11, 2026 15:10
Adds agents/anyplot/data/prepare.py (no ADK import): the Pydantic operation
models the model may choose from (rename, drop_columns, drop_rows, set_type,
aggregate, sort, pivot_longer, pivot_wider, every string bounded, unknown
fields refused) and apply_operations, which applies them in order with pandas
to a ParsedDataset's canonical data.csv, writes canonical CSV text back and
returns one English change line per operation with its counts. Errors are
PrepareError(code, message) with the codes unknown_column, type_mismatch,
empty_result, too_wide and invalid_operation; messages name columns, never
cells. The caller re-parses the result with parse_dataset.

Tests cover every operation on small fixtures (a decimal-comma column with a
unit, DD.MM.YYYY dates, a total row, two unequal series, a sum per month),
the error codes, the change lines, the bounds and the round trip, which is
idempotent: the prepared CSV re-parses to itself.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
…res it

The root's new `prepare_data` tool takes at most five operations of the fixed
set (`data/prepare.py`); the server applies them to the stored canonical CSV,
re-parses the result and stores it as the next dataset version
(`dataset_versions.py`: the last five kept for undo, the bindings carried over
by `carry_bindings`, the confirmation dropped). The turn's `done` event carries
the new version with one change line per operation, `POST
/v1/sessions/{sid}/dataset/undo` restores the previous one (the BFF mirrors it,
`422 nothing_to_undo`), and the messages action `summarize_data` asks for the
step-1 summary without the scope judge; root.md gets the "Step 1: the data"
section with the operation reference. ToolSafety validates the operations
against the same models and exempts a condition's value and a rename's name
from the link rule, because they are compared or written, never followed.

The profile carries `mean` and `median` for numeric columns, and the
model-facing profile JSON leaves empty fields out; the stress-case profile
sizes are re-measured. The live prep eval (`evals/prep.py`, 17 German and
English fixtures) scores the root's choices by exact operation match with an
80 % gate; the mock BFF plays the summary, the three data changes and the undo.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
… review

Two verifiers read the operations and the wiring (commits 97bf8df and
9b12e36); this commit answers every finding that holds.

- A `done` event that carries `dataset` for a wide table exceeded the BFF's
  64 KiB event cap and was dropped whole, so the page saw `error upstream`
  instead of the change list: `done` now has its own cap of 512 KiB
  (`_MAX_DONE_EVENT_CHARS`), and `agents/stream.py` proves the largest
  `done` the service can send fits (`MAX_DONE_EVENT_CHARS`, tested).
- `set_type datetime MM/DD/YYYY` swapped day and month on ISO and dotted
  columns the parser never guessed: the swap applies only to a slash column
  the parser flagged (`AMBIGUOUS_DATE_ORDER` in the version's warnings),
  carried through renames and drops.
- `set_type number` removed every opposite mark as a thousands separator
  (`12,5` became 125): the cells are read as the parser reads a number, a
  thousands mark only between three-digit groups; when more cells read with
  the other decimal mark, the whole column is read that way and the change
  line says so.
- A JSON number as a condition value was refused as `invalid_arguments` with
  no hint: `Condition.value` coerces numbers to text, and ToolSafety's refusal
  of a `prepare_data` call carries the validation lines (`validation_lines`,
  shared with the tool's own `invalid_operations`).
- `pivot_longer` past the 200 KB input cap answered `internal`: the written
  size is checked and refused as `invalid_operation` with a hint.
- `set_type text` claimed a change the re-parse undid: `parse_dataset` takes
  `text_columns`, the store keeps them with every version, and the column
  stays text in every later version.
- The idle sweep took a session's undo history while the session lived, so
  `version > 1` and `422 nothing_to_undo` disagreed: the sweep counts
  idleness per session, so the history leaves with the current version.
- A datetime column with explicit midnight times lost `T00:00:00` on any
  operation, and a melt of a datetime column next to another kind dropped
  the time of day: the parser's per-column time flag travels with the frame
  (`_Carried`), so untouched columns are written back exactly as stored.

Left as designed: a rename's `name` stays exempt from the link rule like a
condition's `value` (a column may be renamed to what the user's table
holds); `set_type datetime` without a format uses pandas' per-cell
inference; a `year` period re-parses as an integer column.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
…ch change with .undo()

The frontend half of prepare_data on /debug/agent:

- Every parse that stores a dataset (a paste, or a transcription's
  .use()) is followed by the turn {action: "summarize_data"}, shown as
  `.summarize_data()` in the thread, so the assistant's summary is the
  first turn. No summary after an undo, a library switch or a
  transcription that still waits for its decision.
- A turn whose done carries `dataset` replaces the preview, the profile
  and the bindings, drops the confirmation (back to step 1) and shows a
  `# changed · version N` block above the preview with the change lines
  and `.undo()`. done's own bindings still apply after it.
- `.undo()` posts the undo route: the restored version replaces the data,
  unconfirmed, and the block reads `# undone · back to version N`. It is
  offered while the version is above 1 and not while anything runs; a
  422 nothing_to_undo disables it, a 409 run_active can be retried.
- agent.ts: ColumnProfile mean/median, DatasetParsed.version,
  DatasetChanged, done.dataset (validated, dropped whole when malformed,
  unknown fields left out), TurnAction/TurnBody, undoDataset, countBucket.
- The binding call's in-flight flag is a ref now, so the summary turn
  that follows the parse's own PUT never reads a stale bindingsBusy.
- Analytics: agent_data_prepared{changes: 1|2|3+} and
  agent_data_undo{version: 1|2|3+}, documented in plausible.md.
- The mock groups by the column a message names ("gruppier nach x").
- Step-aware composer placeholder and empty-thread hint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
…'s minute

The page now sends the step-1 summary right after a paste, and the user's data
requests follow it, so with one start a minute the plot after the confirmation
waited for the summary's minute to pass. A turn that starts while the data is
unconfirmed cannot render: the pipeline answers `not_ready`, and no confirmation
can land while the turn is active (`409 run_active`). The route therefore
submits such a turn as not `rated`: it takes the concurrency slot but no place
in the sliding window and never waits for it, while whole pipeline runs keep the
minute rule. The order stays first come first served, so an unrated entry behind
a rated one that waits for the window waits with it.

Also records the two live prep eval runs of 2026-10-11 in `agents/README.md`
(16 of 17 before the review fixes, 17 of 17 after them).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
…/agents-prepare-data

The merged parser types a unit column at parse time and names columns after
typing, so the text_columns request is matched by the names without a unit.
The prepare tests and fixtures that pasted unit columns as text follow in the
next commit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
Since #12132 the parser types a column whose every cell carries the same unit
at the paste (`Anteil (%)`, numbers), so `set_type number` answers "already
numbers" on it. The unit columns in the prepare tests and the two percent
cases of the prep eval keep one bare cell (`7`) and stay text, so they still
exercise `set_type number`; a new test documents the division of labour.

- tests: SALES and the inline `p`/`share` fixtures get one bare cell, with
  parse assertions that the column is text; change lines and CSVs follow.
- prep eval: de-percent-not-count gets a bare cell and the decimal_comma tag;
  en-decimal-comma-unit becomes en-percent-unit-mixed (dot decimals, one bare
  cell, no explicit decimal expected).
- root prompt and design doc: `set_type number` is for the columns the profile
  still shows as text.
- agents/README.md: the third prep eval run, 17 of 17.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
Copilot AI balanced review requested due to automatic review settings October 11, 2026 15:21
@codecov

codecov Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.14530% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
app/src/hooks/useAgentSession.ts 98.83% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Preparation ordering, binding carryover, numeric profiling, and persistent text typing still contain correctness defects.

6 open findings
What changed in this PR

Adds assistant-driven dataset preparation, versioned undo, step-1 summaries, queue handling, frontend controls, analytics, documentation, and evaluation coverage.

Changes:

  • Adds validated pandas preparation operations with version history and undo.
  • Integrates summaries, changed-data previews, analytics, and queue behavior across the API and frontend.
  • Adds extensive unit tests and a 17-case bilingual prep evaluation.
File Description
agents/​anyplot/​data/​prepare.py Implements preparation operations.
agents/​anyplot/​dataset_versions.py Manages dataset versions and undo.
agents/​anyplot/​data/​parse.py Adds statistics and persistent text typing.
agents/​anyplot/​data/​store.py Stores text hints and sweeps by session.
agents/​anyplot/​data/​bindings.py Carries bindings between versions.
agents/​anyplot/​tools/​session.py Exposes prepare_data.
agents/​anyplot/​session_state.py Adds dataset history state.
agents/​anyplot/​schemas.py Adds numeric profile statistics.
agents/​anyplot/​run_queue.py Adds unrated step-1 turns.
agents/​anyplot/​prompts/​root.md Instructs data preparation and summaries.
agents/​anyplot/​plugins/​tool_safety.py Validates preparation calls.
agents/​anyplot/​plugins/​ledger.py Records dataset changes.
agents/​anyplot/​opening.py Resets history on new paste.
agents/​anyplot/​briefs.py Trims empty profile fields.
agents/​anyplot/​agent.py Documents the new root tool.
agents/​evals/​prep.py Implements the prep evaluation.
agents/​evals/​fixtures/​prep/​*.json Adds 17 bilingual evaluation cases.
agents/​evals/​fixtures/​README.md Updates measured profile sizes.
agents/​main.py Adds undo, summaries, version sync, and queue integration.
agents/​stream.py Carries changed datasets in done.
agents/​README.md Documents preparation and evaluation workflows.
api/​routers/​agent.py Relays undo, summaries, and larger done events.
app/​scripts/​agent-bff-mock.mjs Mocks preparation, summaries, and undo.
app/​src/​lib/​agent.ts Adds frontend contracts and event parsing.
app/​src/​lib/​agent.test.ts Tests contracts, undo, and analytics buckets.
app/​src/​hooks/​useAgentSession.ts Integrates summary, preparation, and undo state.
app/​src/​hooks/​useAgentSession.test.ts Tests the new session flows.
app/​src/​pages/​AgentChatPage.tsx Exposes data requests and undo.
app/​src/​pages/​AgentChatPage.test.tsx Tests the complete preparation flow.
app/​src/​sections/​agent-chat/​DataPanel.tsx Displays changes and undo controls.
app/​src/​sections/​agent-chat/​DataPanel.test.tsx Tests change and undo states.
app/​src/​sections/​agent-chat/​ChatThread.tsx Displays structured actions.
app/​src/​sections/​agent-chat/​ChatThread.test.tsx Tests action rendering.
app/​src/​sections/​agent-chat/​StepHeader.tsx Documents step resets.
app/​src/​sections/​agent-chat/​messages.ts Adds undo failure messaging.
docs/​concepts/​agent-network.md Documents the preparation design.
docs/​reference/​api.md Documents new routes and stream fields.
docs/​reference/​plausible.md Documents preparation analytics.
changelog.d/​agents-prepare-data.md Records the feature.
tests/​unit/​api/​test_agent_router.py Tests BFF routing and event limits.
tests/​unit/​agents/​runtime/​test_stream.py Tests changed-dataset events.
tests/​unit/​agents/​runtime/​test_service_flow.py Tests preparation and undo end to end.
tests/​unit/​agents/​runtime/​test_run_queue.py Tests unrated queue entries.
tests/​unit/​agents/​runtime/​test_run_queue_flow.py Tests queue behavior through the service.
tests/​unit/​agents/​runtime/​test_registry.py Verifies root tool registration.
tests/​unit/​agents/​runtime/​test_plugins.py Tests preparation safety validation.
tests/​unit/​agents/​runtime/​test_dataset_versions.py Tests history and undo behavior.
tests/​unit/​agents/​data/​test_prepare.py Covers preparation operations.
tests/​unit/​agents/​data/​test_store.py Tests session-level history lifetime.
tests/​unit/​agents/​data/​test_parse.py Tests statistics and text hints.
tests/​unit/​agents/​data/​test_bindings.py Tests carried bindings.
tests/​unit/​agents/​evals/​test_prep.py Tests the prep evaluator.
tests/​unit/​agents/​evals/​test_stress.py Updates expected profile size.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread agents/anyplot/data/parse.py Resolved
Comment thread agents/anyplot/data/bindings.py Resolved
Comment thread agents/anyplot/data/prepare.py Resolved
Comment thread agents/evals/prep.py Resolved
Comment thread app/scripts/agent-bff-mock.mjs Resolved
Comment thread docs/reference/api.md Resolved
- Profile statistics never fail a parse: `fsum` of two `1e308` values raised
  `OverflowError`; the mean and the median are computed within the float range
  or left out (`_statistics`).
- `carry_bindings` computes the defaults for the open roles on the columns no
  kept binding uses, so a role whose first choice a kept binding holds takes
  its next candidate; a family keeps its members and gains the defaults' ones
  under the next free numbers. The mock's `carryBindings` does the same.
- `pivot_longer` keeps the melted column text when a source was kept text or
  unlike kinds were written as text; `pivot_wider` keeps the new columns text
  when the values column was.
- The prep eval counts a case as a miss when a `prepare_data` call of the turn
  failed on the data, so an operation list in an order the data refuses never
  passes on its shape alone (`failed_calls`).
- The API reference says the undo history holds the five versions before the
  current one, so it can end above version 1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Prompt validation, summary ambiguity handling, sticky text typing, and final dataset synchronization contain unresolved correctness issues.

2 open findings
6 resolved since last review
Previously missed (2)

In code that hasn't changed since last review

Medium severity Preserve parser warnings for root summary profiles

agents/​anyplot/​briefs.py:64

The new summary brief requires the agent to ask about parser-detected ambiguities such as day/month order, but every model-facing profile has warnings removed here. Once slash dates have been normalized to ISO values, that warning is the only remaining signal, so summarize_data cannot satisfy the brief. Preserve bounded parser warnings for the root/summary profile while omitting them only where they are genuinely unnecessary.

Medium severity Document Boolean condition values as strings

agents/​anyplot/​prompts/​root.md:49

This syntax tells the root to send JSON booleans, but Condition rejects booleans and _matches expects the strings "true" or "false". A model following this instruction gets invalid_arguments instead of filtering Boolean rows. Document the values as strings so the prompt matches the validated tool contract.

🧠 Review effort: Balanced

Comment thread agents/main.py Resolved
Comment thread agents/anyplot/data/prepare.py Resolved
- The `done` event's state sync runs while the run is still active, so no
  undo or paste can slip in between the run's end and the `done` that reports
  its dataset; the run is released right after, before `done` is sent, and
  at once when the stream is closed or cancelled early.
- `Prepared.text_columns` carries the names the re-parse gives the columns
  (`canonical_names`: cleaned, shortened, suffixed), so a pivot's long value
  or a long new name keeps its text hint through the re-parse.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
@MarkusNeusinger
MarkusNeusinger enabled auto-merge (squash) October 11, 2026 16:20
@MarkusNeusinger
MarkusNeusinger merged commit d27794f into main Oct 11, 2026
14 checks passed
@MarkusNeusinger
MarkusNeusinger deleted the feat/agents-prepare-data branch October 11, 2026 16:25
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