Repository navigation
feat(agent): text operations in the data step, bound-pair rule - #12556
Merged
Merged
Conversation
…n the binder prepare_data gains split, extract, strip and replace, a closed set with literal texts only. Each writes new columns beside its source, typed as set_type reads them, and the steps chain in one call. The binder never binds a lower and upper bound pair whose lower column starts above the upper one's maximum; the pair stays open, and a text column of ranges becomes a certain split conversion that fills it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
…plits Fixes from the Gemini Flash check before the PR: extract keeps the minus of `$-100`, `-$100`, `USD-20` and `ca.-15`; split keeps the sign of `-.5`; the two parts of one split fill a bound pair only when the first neither starts nor ends above the second, so `22-18` cells leave the pair open. The root prompt quotes the separators, tells `number` from `digits`, and its chain example no longer extracts a part that already reads as a number. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Three moderate active findings remain: range pairing can mix unrelated operations, number extraction misclassifies labels such as Q1, and extracted leading-zero values bypass the documented typing guard.
3 open issues
What changed in this PR
This pull request adds literal text operations to the agent data-preparation workflow and improves lower/upper bound-pair handling, including automatic range-text splitting.
Changes:
- Added chainable
split,extract,strip, andreplaceoperations with validation, typing, undo support, and query support. - Added bound-pair discovery, inverted-pair warnings, and certain range-column conversions.
- Expanded prompts, telemetry, documentation, changelog data, evaluations, and regression tests.
| File | Description |
|---|---|
tests/unit/agents/runtime/test_plugins.py |
Tests URL/path safety exemptions for text-operation literals and names. |
tests/unit/agents/runtime/test_dataset_versions.py |
Tests range splitting, binding carry-over, and undo behavior. |
tests/unit/agents/evals/test_prep.py |
Updates operation and fixture-count assertions and tests operation descriptions. |
tests/unit/agents/data/test_warning_codes.py |
Adds warning-code coverage for inverted bound pairs. |
tests/unit/agents/data/test_prepare.py |
Adds comprehensive tests for text operations, typing, refusals, and operation chains. |
tests/unit/agents/data/test_conversions.py |
Tests automatic range conversion candidates and language-specific names. |
tests/unit/agents/data/test_computed.py |
Corrects Bollinger test data ordering for lower and upper bands. |
tests/unit/agents/data/test_bounds.py |
Adds bound-pair discovery, inversion, ordering, and warning tests. |
tests/unit/agents/data/test_bindings.py |
Tests inverted-pair defaults, carried bindings, and split-pair assignment. |
docs/concepts/agent-network.md |
Documents text operations and bound-pair conversion behavior. |
changelog.d/agent-text-operations.md |
Records the new data operations and bound-pair fixes. |
agents/README.md |
Updates prep-evaluation counts and documents new fixtures. |
agents/evals/prep.py |
Adds canonical descriptions for the four new operations. |
agents/evals/fixtures/prep/en-summary-range-column.json |
Adds an English range-column summary case. |
agents/evals/fixtures/prep/en-strip-a-prefix.json |
Adds an English prefix-stripping case. |
agents/evals/fixtures/prep/en-keep-the-number.json |
Adds an English number-extraction case. |
agents/evals/fixtures/prep/en-hard-text-chain.json |
Adds a chained strip-and-split case. |
agents/evals/fixtures/prep/de-text-ersetzen.json |
Adds a German replacement case. |
agents/evals/fixtures/prep/de-spalte-am-bindestrich-teilen.json |
Adds a German range-splitting case. |
agents/anyplot/tools/session.py |
Exposes the new operations in the model-facing tool documentation. |
agents/anyplot/telemetry.py |
Adds the new operations to telemetry’s preparation-operation set. |
agents/anyplot/prompts/root.md |
Teaches the root agent when and how to use text operations and range conversions. |
agents/anyplot/plugins/tool_safety.py |
Exempts literal text-operation arguments from URL/path validation. |
agents/anyplot/data/warning_codes.py |
Adds the bound_pair_inverted warning code. |
agents/anyplot/data/prepare.py |
Implements text operations, typed output columns, range detection, and parsing helpers. |
agents/anyplot/data/conversions.py |
Adds automatic RangeSplit conversion candidates and localized range names. |
agents/anyplot/data/bounds.py |
Defines bound-pair discovery, inversion checks, ordering checks, and warning text. |
agents/anyplot/data/bindings.py |
Prevents inverted defaults and carries split columns into lower/upper roles. |
Review effort: Lite
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
A hand binding of an inverted lower and upper bound pair stays valid, blocks nothing, survives every change of the data and every undo, and carries one neutral warning line that asks for nothing; the root mentions it once and then follows the user. Only two neighbouring parts of one split fill an open pair (Prepared.splits), never two extracts of one column. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
Resolves the agents/README.md conflict: the prep eval's 71 cases from this branch beside main's 900 generated eval cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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_data(andquery_data) take four more operations, literal texts only, never a pattern:split(column,separator,names: 2 to 10),extract(keep:number,digitsorletters),strip(prefix,suffix) andreplace(old,new). Each reads the source's canonical cell texts and writes new columns beside it; the source stays, so.undo()and the user's check work as for every other operation. A new column is typed asset_typereads one (prepare._typed): numbers with the dataset's decimal mark, its thousands rules and at most a tenth of failing cells, which become missing and are counted; else dates; else text, kept text through the re-parse. A number counts only in a cell the parser reads as one (10 kg, neverQ1), leading zeros keep a column text, and a column with a cell of digits alone is never read as dates. The steps chain in one call.agents/anyplot/data/bounds.py). Lower and upper bound pairs come from the specs' role names (lower/upper,low/high,min/max,lcl/ucl:min_value/max_value,ci_lower/ci_upper,low/high,lower_80/upper_80, ...;error_lower/error_upperand the tornado chart'slow_value/high_valueare excluded, since their values are no bounds). The default bindings never bind a pair whose lower column starts above the upper column's maximum: both roles stay open. A pair bound so by hand is valid and carries a binding warning (telemetry codebound_pair_inverted).conversions.RangeSplit,prepare.range_run): when both roles of a pair are open and every cell of a free text column is two plain numbers around one dash, nothing guessed, the first never above the second, the session block lists thesplitascertain; its two new columns (<column> (min)and<column> (max)in the session language) are bound bycarry_bindings, the first to the lower role whatever order the spec lists the roles in. Two parts of one split fill a pair only when the first neither starts nor ends above the second (bounds.ordered), so22-18cells leave the pair open, never swapped.This closes the failure classes
range-text-splitandbinding-min-max-invertedof the agent feedback triage (arange-intervaltable with its ranges in one text column, and a count and an average bound as the minimum and the maximum).Owner input on this PR: a hand-bound inverted pair is never blocked
A person may bind high and low "the wrong way round" on purpose. Checked and changed in d09a036:
check_bindings: a hand binding of an inverted pair stays valid and complete (no error, no missing role, so.confirm()is never disabled), with one short, neutral warning line that asks for nothing. The line lost its instruction ("bind the two columns that hold each range's ends").carry_bindings: a binding that stays valid is never dropped, so a pair the user bound so survives everyprepare_datachange and every undo; only an inverted bindingcarry_bindingswould add itself is left out. New tests:test_a_pair_bound_high_to_low_by_hand_survives_a_change_of_the_data(a rename that sends the defaults in, a sort, a split whose parts would fit the pair) andtest_a_pair_the_user_bound_high_to_low_survives_prepare_data_and_undo(throughpush_versionandundo_version).root.md: the warning is mentioned once in the session, in one neutral sentence, with what would fit instead; after that the root follows the user, never changes the binding unasked and never mentions it again.Copilot's round
extracts) could fill a pair as if they were a split.Prepared.splitsnow records each split's parts,push_versionandcarry_bindingspass them on, and only two neighbouring parts of one split fill a pair.extractnumberofQ1gives 1 (an explicit request, asA-12gives 12; theQ1rule is for the automatic typing), and of#00123gives 123 (numberis for amounts;digitskeeps the zeros as text).Decided along the way
-or−that starts a part right before a number is that part's sign:-5-10is -5 and 10.name,names): the source is never changed in place.ToolSafetyexempts the literals (separator,prefix,suffix,old,new) andnamesfrom the link rule, as it exempts a condition's value and a label: they are compared with the cells or written into them. A column reference stays checked.1.200) make no certain range split, as for any number reading that guesses its mark; asked for, the split reads them the file's own way (1200 in a decimal-comma file).y_lower1,y_upper1) is no pair: its members bind in column order.TestBollingerhad every lower band above every upper band (the helper fills each column above the one before it); the columns now stand lower band first. The rule caught it rightly; no production code was changed to make a test pass.Plan
Agent case triage round 1, general fixes D and E (owner decisions 2026-11-04).
Test plan
uv run ruff check .,uv run ruff format --check .uv run --extra typecheck --extra agents mypy api core agents --prettyuv run pytest tests/unit tests/integration -n 8: 26,603 passed before the Flash fixes;tests/unit/agents20,023 passed on the head. New:test_bounds.py, andTestSplit,TestExtract,TestStripAndReplace,TestTextChains(prepare),TestBoundPairs(bindings),TestRanges(conversions), a push-and-undo test (dataset versions), the link-rule literals (plugins), the warning code, the eval fixtures anddescribe.-5stays a number, a decimal comma1,5and thousands1.200keep their reading, ids and codes (A-12, a column namedid) and spans of years stay text unless the user asks,Q1stays a label,007keeps its zeros.uv run python -m tools.changelog check --base origin/maineupassed the gates, 65 of 71 exact (91.5 %), 0 unavailable, 200 model calls, $0.18; the certain conversions were made 5 of 5, the range split among them; replies median 52 words, 1 reply miss. The six new text cases: 5 of 6, the miss chosedigitsfor "the number" of a ticket id (#4711, the same table either way); the prompt now tellsnumber(an amount) fromdigits(an id or a code), and the case asks for an amount. The other misses:en-percent-unit-mixedalso misses onmain(a baseline run of its tags),de-klasse-aus-vorzeichenanden-code-strip-plot-callsare model variance, andde-datum-tag-monat-jahrandde-hard-kette-gesamtzeileadded arow_numberfrom the session block's row-number line in a message turn (both matched onmainin one baseline run each). A rerun after the Flash fixes (tagstext,date_format,summary-row, 9 cases, $0.035): 8 of 9, the six text cases 6 of 6,de-datum-tag-monat-jahrmatched without arow_number, andde-hard-kette-gesamtzeilemissed only by leaving outdrop_columns Notiz, the variance the README already names for that case. Total eval spend: $0.24.extractdropped the minus of$-100and-$100; medium:-.5-10was cut at its sign, and a manual high-to-low split could fill a pair the wrong way; low: two prompt wordings and an internal count).Texts to read
agents/anyplot/prompts/root.mdlower bound is above its upper boundnames a pair of roles such asmin_valueandmax_valuebound to two columns whose values do not overlap, the lower above the upper. The binding is valid and may be meant: mention it once in the session, in one neutral sentence, with what would fit instead (other columns, or asplitof a column that holds the ranges as text, such as10–25). After that it is the user's choice: never change it unasked, never mention it again, and draw the plot as bound."10–20or of names into two or more columns, keep only the number, the digits or the letters of each cell, take a prefix or a suffix such as#orCHFoff the cells, replace a text in the cells), or says how a column is meant to be read"row_number:{"op": "split", "column": ..., "separator": ..., "names": [2 to 10 new names]}(cuts each cell at the textseparator, such as"-","–",",",";","/"," to "or a space" ", into new columns namednames, in order:10–25gives 10 and 25; a space cuts at every run of spaces; a-that starts a part right before a digit is its minus sign, so-5-10gives -5 and 10; a cell without the separator stays whole in the first new column, and the last one keeps the rest of a cell with more separators, whichchangescounts){"op": "extract", "column": ..., "keep": "number" | "digits" | "letters", "name": ...}(adds a column withnumber, the one number of each cell, for an amount or a measure, read with the dataset's decimal mark:CHF 1'200gives 1200,ca. -3.5 °Cgives -3.5; a cell with no number or with two, such as10–20, is left empty, and more than a tenth of such cells refuse it; withdigits, every digit in order, for an id or a code:+41 79gives4179,#00123keeps its zeros; withletters, only the letters:12 kggiveskg){"op": "strip", "column": ..., "prefix": ..., "suffix": ..., "name": ...}(adds a column with the textprefixtaken off the start andsuffixoff the end of each cell that has them; one of the two is enough){"op": "replace", "column": ..., "old": ..., "new": ..., "name": ...}(adds a column with everyoldin each cell replaced bynew; leavenewout to removeold)split,extract,strip,replace) take their texts literally, never as a pattern, add their new columns last and keepcolumnas it is. Each new column is read asset_typereads one: numbers with the dataset's decimal mark, else dates, else text, andchangessays which, with the cells left empty. They combine in one call, each step naming the column an earlier step made:#A-12takes astripof the prefix#, then asplitof the new column at-, whose part12is already a number;vol. 3; ed. 2takes asplitat;, then anextractof the number of each part. Ids and codes such asA-12stay as they are unless the user asks for them to be split."min_value and max_value) names a text column of ranges such as10–25and itssplitinto two new columns, which the server binds to the two roles: say which column you split, with the count and the two new columns' names."prepare_datacan do it, and never userun_data_codefor it, ... When no operation fits, say which change is needed and that it has to come from the file."prepare_datacan do it, also when it takes several operations in one call, and never userun_data_codefor it, ... When no operation and no chain of them fits, say which change is needed and that it has to come from the file."The
prepare_datatool description (agents/anyplot/tools/session.py, the docstring the model reads) names the four operations the same way.User-visible and model-visible strings (all new)
# changedblock of the data panel):new)too_wide"the data has N columns; K more would exceed the 50 that fit".Not done here (for a follow-up)
app/src/sections/agent-chat/bindings.ts,signHint,siblingHint); it has no hint for an inverted pair yet. The server warning reaches the root through the session block and thePUT /bindingsanswer.https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8