You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(agents): prepare_data changes the data in step 1, with undo - #12133
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
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
- 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
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.
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.
- 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
prepare_datatool 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 forPOST .../dataset/undo). The model never writes data rows;ToolSafetyvalidates the operations against the same models first. The turn'sdoneevent carries the new version with one change line per operation, unconfirmed, and the BFF relays adoneof up to 512 KiB so a wide dataset's change list always reaches the page.summarize_data, so the assistant's two-to-four-sentence description of the data (the profile now carriesmeanandmedian) is the first turn in the thread; data requests in words go through the composer before the confirmation. Adonewithdatasetreplaces the preview and the bindings, takes the page back to step 1 and shows a# changedblock with the change lines and.undo(). Two enum-only analytics events:agent_data_prepared{changes},agent_data_undo{version}.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.python -m agents.evals.prepdrives 17 German and English data requests through/v1with the real model and scores the root'sprepare_datacalls by exact operation match (80 % gate). Three runs on 2026-10-11 with Claude Haiku 5.5 oneu: 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: thedoneevent cap, theMM/DD/YYYYswap scoped to parser-flagged ambiguous columns,12,5never read as 125, JSON numbers as condition values with validation lines on refusals, the long-format size cap,set_type textkept 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
nameis exempt from the link rule like a condition'svalue(a column may be renamed to what the user's table holds);set_type datetimewithout a format uses pandas' per-cell inference; ayearperiod re-parses as an integer column. Two parser follow-ups found after the merge are a Todoist task (claudelabel): thousands commas behind a currency ($1,200reads 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 afterset_type numberon 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 checkcd app && yarn lint && yarn fm:check && yarn type-check && yarn test(805 passed)&& yarn build# changed · version 2with the change line →.undo()→# undone · back to version 1→ "gruppier nach x" →.confirm()→.create_plot()renders