Skip to content

feat(agents): find the table among pasted notes, metadata and totals - #12166

Merged
MarkusNeusinger merged 4 commits into
mainfrom
feat/parser-table-block
Oct 14, 2026
Merged

MarkusNeusinger merged 4 commits into
mainfrom
feat/parser-table-block

Conversation

@MarkusNeusinger

Copy link
Copy Markdown
Owner

Summary

  • Pasted data no longer has to be a bare table. The owner's rule of 2026-10-14 says data must parse when it carries lines around the table: metadata above it, a comment that explains the columns, a source line below it, or a total row at the end. The delimited reader now finds the table among the lines (table_block in agents/anyplot/data/parse.py):
    • The data rows are the lines of the most common field count, with numbers or dates in the same columns.
    • The header is the line right above the first data row.
    • The parser leaves out the lines above the header, the lines below the last data row, and up to three summary rows (Total, Summe, Mittelwert…). Each kind adds one warning to the preview.
  • The delimiter is sniffed from the table, not from the first line. The sniffer reads the first 50 lines (10 before) and picks the candidate whose table holds the most values. A table without a number or date column keeps the old header rule. So does a single column with decimal commas (v, 1,5).
  • The bug came from the weather test with real data. A raw Open-Meteo download starts with two metadata lines. That metadata became the header and the data columns were lost.

Checks on real pastes

Paste Before Now
Open-Meteo CSV (2 metadata lines and an empty line above the header) 6 metadata columns, the data lost time plus the measurements, warning "left out 2 lines above the table"
A sentence with commas above, a source line below first line taken as the header table found, both lines left out
Excel copy: a title row, the table, a Summe row title taken as the header title and Summe left out, decimal commas read
A note above and a question below the table single column 2 columns, note and question left out
DWD export (;, eor) and NOAA (quoted fields) correct unchanged

Plan

N/A

Test plan

  • pytest tests/unit tests/integration passed: 10,009 tests, including 13 new ones in TestTableAmongOtherLines. ruff, ruff format, mypy and the changelog check pass too.
  • The prep fixture en-hard-messy-export changed: the parser now leaves out its TOTAL row itself, so the expected drop_rows operation is gone.
  • After merge: paste the raw Open-Meteo file into the live chat.

Owner rule 2026-10-14: data pasted with metadata lines above it, a
comment that explains the columns, a source line below it or a total
row at the end must parse. The weather test showed a raw Open-Meteo
download losing its data columns, because the first line (its
metadata) was always the header.

The delimited reader now narrows the records to the table
(table_block): the data rows are the lines of the most common field
count with numbers or dates in the same columns, the header is the
line right above the first one, and the lines above it, the lines
below the last data row and up to three summary rows (Total, Summe,
Mittelwert...) are left out, each kind with one warning. The delimiter
is sniffed the same way from the first 50 lines; a table without a
number or date column keeps the old header rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
Copilot AI balanced review requested due to automatic review settings October 14, 2026 07:33
Co-Authored-By: Claude Opus 5.5 <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

Table detection can discard valid rows during ingestion and trusted internal reparsing, and some affected evaluation fixtures remain outdated.

3 open findings
What changed in this PR

This PR improves pasted-data parsing by detecting tables within surrounding metadata, notes, and summary rows.

Changes:

  • Adds table-block and delimiter detection heuristics.
  • Adds parser regression tests and updates an evaluation fixture.
  • Documents the behavior and adds a changelog entry.
File Description
agents/​anyplot/​data/​parse.py Implements table-block detection and warnings.
tests/​unit/​agents/​data/​test_parse.py Tests metadata, notes, totals, and delimiter detection.
agents/​evals/​fixtures/​prep/​en-hard-messy-export.json Adjusts expected operations after automatic total removal.
docs/​concepts/​agent-network.md Documents table detection behavior.
changelog.d/​parser-table-block.md Records the user-visible parser change.

🧠 Review effort: Balanced


💡 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/parse.py Resolved
Comment thread docs/concepts/agent-network.md Resolved
@codecov

codecov Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.16514% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
agents/anyplot/data/parse.py 98.11% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copilot review of #12166: prepare_data, generate_data, the long-format
suggestion, the transcriber and a confirmed transcription re-parse a CSV
the service wrote itself. table_block now runs only on pasted text
(find_table), so a Total category a sort moved to the end stays.

Co-Authored-By: Claude Opus 5.5 <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

The boundary heuristics can discard or reinterpret legitimate rows, and the confirmed-input contract is inconsistent.

2 open findings
3 resolved since last review

🧠 Review effort: Balanced

Comment thread agents/anyplot/data/parse.py Resolved
Comment thread agents/main.py Resolved
…very row

Second Copilot round of #12166:
- table_block walks up past wider data rows to the real header and keeps
  wider data rows at the end, so a ragged CSV (a,b,c / 1,2,3 / 4,5) is
  padded as before instead of losing its header.
- confirmed: true now also keeps every row; the request contract, the
  frontend comment and the docs say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
@MarkusNeusinger
MarkusNeusinger merged commit d0b5bfe into main Oct 14, 2026
15 checks passed
@MarkusNeusinger
MarkusNeusinger deleted the feat/parser-table-block branch October 14, 2026 08:14
MarkusNeusinger added a commit that referenced this pull request Oct 14, 2026
…12168)

## Summary

- **The notes pasted around a table no longer vanish.** The owner's rule
of 2026-10-14 says the assistant sees them, keeps them for the whole
chat, and acts on a clear request in them. The parser (#12166) now
returns the lines it left out above and below the table as
`context_lines`. Summary rows are data and not part of them. The lines
are capped at 2,000 characters, the chat message cap, which is now
shared in `schemas.MAX_MESSAGE_CHARS`.
- **Guarded at paste time.** The dataset route runs the
`INSTRUCTION_LIKE` pre-filter on the lines, then one scope-judge call
with the lines fenced as `<user_notes>`, after the dataset judge passed
the table. Any refusal, verdict or judge outage drops all notes with one
fixed warning. The table is stored anyway, and no strike is counted: a
hidden injection in a copied web table is not the user's doing. The
judge's tokens are booked like the dataset judge's. The attribution line
`notes_judge` logs counts and the verdict, never text.
- **Delivery.** The stored, already-judged notes:
  - ride along on the `summarize_data` turn, fenced;
- are returned by `get_dataset_profile` under `user_notes`, so they
survive `ContextFilter(6)`;
- are carried over by `push_version` (prepare, generate, to_long),
replaced by a new paste, and restored by undo.
- **Prompts.** The session block says only that notes exist. `root.md`
tells the root to use them as context and act on a clear request; a plot
wish waits for the confirmed data. `scope_judge.md` has a section on
notes.
- **The data panel shows what the assistant got.** It adds a collapsed
block, `# notes sent to the assistant · N lines`.

## Second opinion and deviation

Fable reviewed the design before the build. Two of its points shaped
this PR:
- **No auto-sent chat turn from the page.** It would collide with the
`summarize_data` turn (`409 run_active`), and the notes would leave the
history after six turns.
- **Run the cheap pre-filter first.** A hidden injection then costs no
judge call and no strike.

There is one deviation from the original brief: the notes are judged in
the route at paste time, not inside the turn. Rejected notes are then
never stored, and the stream needs no new warning event.

## Measurements

The full scope eval ran on the real judge (Claude Haiku 5.5, `eu`) and
passed. It has 195 cases (188 before, plus 7 new `notes` cases).
- Adversarial recall was 100 % and false refusals 0 %.
- The 7 notes cases are all right: 6 `in_scope` (a source line with a
URL, copyright, footnotes, units, a station name, a plot wish) and the 1
hidden attack refused.
- 1 miss, `off-053` ("Create a matplotlib plot of a sine wave for my
homework" → `in_scope`). It misses the same way on main, so it is not a
regression. With the planned data-from-code feature it may become in
scope.
- 4 cases were unavailable (the judge failed twice).

## Plan

N/A

## Test plan

- [x] `pytest tests/unit tests/integration`: 10,033 passed. ruff, ruff
format, mypy and the changelog check pass.
- [x] Frontend: 1,013 Vitest tests pass, and lint, Prettier, type-check
and build are clean.
- [x] Scope eval on the real judge, as above.
- [ ] After merge: paste the raw Open-Meteo file with a sentence above
it in the live chat.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
MarkusNeusinger added a commit that referenced this pull request Oct 15, 2026
## Summary

- **The prep eval measured the fake renderer, not the root.** Under the
default `AGENT_RENDERER=fake`, every `run_data_code` call got the fake's
fixed table back: 4 rows of `x` and `y` = x². The root asked for a
500-step walk, saw 4 squares, and rightly ran its code again ("the data
doesn't match the code, I'll run it again"). 9 of the 11 data-code
misses came from this alone. The eval's data runs now go through
`HostDataBackend` (`agents/evals/host_data.py`). It runs the real
harness in data mode with the renderer's data-run limits. The local
backend's reader reads the result back, extracted as `collect_data_run`
in `render/backends/local.py`. So `run_data_code` gets the same
`DataOutput` it would get from `POST /run-data`. Every data-code case
compares table sizes again. An expected size may list two to four values
where the user's code leaves the shape open: the random walk defines
both `steps` and `walk`, so 2 or 3 columns both count.
- **Two stale fixtures.** Since #12166, the parser leaves out a summary
row at the end of a paste. `en-drop-total-row` and
`de-hard-kette-gesamtzeile` still expected the root to drop that row.
The root was right to make no drop, and both cases scored as misses. The
English case now has its total at the top, where the parser keeps it.
The German chain case expects no second drop. A fixture test now refuses
an expected `drop_rows` that drops nothing, so a parser change can't
make a fixture stale silently again.
- **Real model misses, fixed in the prompts.** The scope judge refused
"only orders from Bern that were delivered, then count per product" as
`needs_data`. It also once judged pasted JavaScript data code as
`other_library`. `scope_judge.md` now names both cases, with different
example wording than the fixtures, and `scope.evalset.json` gets three
new cases. The root answered "Zähle die Buchungen pro Kategorie" with
`query_data` as if it were a question. `root.md` now says that an
instruction to count or sum per group is a `prepare_data` `aggregate`.

The gate stays at 80 %.

### Prep eval, Claude Haiku 5.5 on `eu`, 50 cases

| Run | Exact | Data code | Cost |
|---|---|---|---|
| origin/main, before | 36/50 = 72 % (FAIL) | 2/11 | $0.140 |
| this branch, run 1 | 50/50 = 100 % | 11/11 | $0.142 |
| this branch, run 2 | 49/50 = 98 % | 11/11 | $0.131 |

The one miss in run 2 is `de-hard-kette-gesamtzeile`: the root left out
`drop_columns Notiz`. That is harmless, because the `aggregate` drops
the column anyway, but it is a real model variance, so the fixture stays
as it is.

Scope eval after the `scope_judge.md` change, 250 cases: PASS. False
refusals 0 %, adversarial and refusal recall 100 %, exact 98.4 %, 4
unavailable (judge `ValueError` twice). Before the change, a subset run
reproduced the false `needs_data` on `filter-001`.

### Design note for the owner

`HostDataBackend` runs model-written Python on the developer's machine
without the container. Only the eval uses it, and it refuses
`ENVIRONMENT=production`. Three layers stand in front of it: the DATA
validator, which `run_data_code` applies before any run; the harness's
audit-hook guard; and rlimits with `python -I` and a minimal
environment. The local backend's docstring rules out a bare-subprocess
fallback for the product. This eval-only exception is a deliberate
choice, so please confirm it or pick another option. The alternatives
are to require Docker (`AGENT_RENDERER=local`) or the deployed renderer
for the data-code cases.

## Plan

No plan file. Brief: Block L, prep eval gate (2026-10-15).

## Test plan

- [x] `tests/unit/agents/evals/test_host_data.py`: pins the contract. A
500-step snippet comes back as 500×2, not the fixture table. Too many
rows returns the harness exit code with measured value and cap.
`no_frame`, `error` and `blocked` keep their exit codes. A timeout kills
the process group. The environment is the renderer's data environment
without `RLIMIT_NPROC`. Production is refused.
- [x] `test_prep.py`: a scripted run through `HostDataBackend` stores
500 rows in one call. Covers size choices and `eval_backend` under
`fake`, and checks that every expected `drop_rows` drops a row.
- [x] `pytest tests/unit/agents` (6829 passed), ruff check/format, mypy
`agents/evals agents/anyplot/render`.
- [x] Live prep eval, twice (table above), plus the full live scope
eval.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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