Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ The model is **Claude Haiku 5.5 on Vertex AI** (`claude-haiku-5-5`) by default.
| `evals/sync_cases.py` | Keeps `evals/.cases/` in line with the `agent_feedback` table: imports the promoted cases from the private case bucket, removes the retired ones, and with `--backfill` indexes cases that have no row ([Sync promoted cases from the bucket](#sync-promoted-cases-from-the-bucket)) |
| `evals/eligibility.py` | The model-free sweep of the catalogue's files of every Python library with a code profile (matplotlib, seaborn, plotnine, altair, pygal, lets-plot): eligible, coupled and blocked pairs, and why |
| `evals/scope.py`, `evals/scope.evalset.json` | The judge-only scope eval and its 219 synthetic cases: in-scope, off-topic and adversarial messages and notes pasted with a table, scored for adversarial recall and false refusals |
| `evals/prep.py`, `evals/fixtures/prep/` | The live prep eval and its 58 German and English cases (54 data requests, 10 of them `hard`, 11 data-code cases and 3 requests for example data, and 4 step-1 summaries): each through `/v1` in process with the real model, scored on the `prepare_data` operations and `generate_data` calls the root chose, on the size of the table each `run_data_code` call stored (a `refuse` case passes when nothing was stored), and on the shape of the root's reply ([Run the prep eval](#run-the-prep-eval)) |
| `evals/prep.py`, `evals/fixtures/prep/` | The live prep eval and its 65 German and English cases (54 data requests, 10 of them `hard`, 11 data-code cases and 3 requests for example data, and 11 step-1 summaries): each through `/v1` in process with the real model, scored on the `prepare_data` operations and `generate_data` calls the root chose, on the size of the table each `run_data_code` call stored (a `refuse` case passes when nothing was stored), and on the shape of the root's reply ([Run the prep eval](#run-the-prep-eval)) |
| `evals/fixtures/cases/` | The eval cases: 360 generated (`<spec>-<library>-<perturbation>`), eleven hand-written, among them `scatter-basic-matplotlib`, `bar-grouped-seaborn`, `scatter-basic-matplotlib-german-message` (a German follow-up that sets the session language) and four `*-combined-*` cases whose follow-up asks for two changes at once, each measured by the case's `checks`, and 23 stress cases (`stress-<name>`) ([fixtures README](evals/fixtures/README.md)) |
| `evals/baselines/` | Committed baseline reports, one per model ([baselines README](evals/baselines/README.md)) |

Expand Down Expand Up @@ -816,7 +816,9 @@ The prep eval (`evals/prep.py`) measures the one non-deterministic part of the s

A case passes on exact operation match, order-insensitive, when every `prepare_data` and `generate_data` call of the turn succeeded (the operations apply in order, so a list the data refuses, such as a column named before its rename, is a miss however it reads, and so is a formula the server cannot compute): the operations of every call, taken together, equal the expected ones after both went through the `Operation` models (a default the model left out and one it wrote are the same; a `set_type` `decimal` equal to the dataset's own counts as left out; the column lists of `drop_columns` and `pivot_longer` and the conditions of `drop_rows` are sets). A `generate_data` call is one expected item `{"op": "generate_data", "variables": [...], "series": [...]}`, and two calls match when they compute the same table: the same names, points and values, so an endpoint written `-3.0` or `-3`, `tau` or `2*pi`, and an expression written `x*x` or `x**2` are the same choice (`comparison_key`). The report lists the call after the `Generate` model with its defaults left out (a variable named `x`, 200 points), the whitespace of every expression and endpoint removed, the series as a set and the variables in order. A case that expects no operation passes when the root made no data call, whether the scope judge refused the message or the root answered in words; the report lists every tool call and the refusal code. A reply in which the root talks about its own instructions (ADK's instruction markers, a system instruction or the session block) is a miss whatever the data calls did, and the summary counts such cases under `instruction_talk`. The gate is 80 % exact matches (`--min-exact`); a case whose turn ended in an `error` event counts in no rate, and more than 10 % of them (`--max-unavailable`) exit 4.

Since 2026-11-01 (owner decision: "short and to the point, no waffle, follow-up questions only when they really matter") the eval also measures every reply of the root: its words, its questions, and whether it closes with a question (`measure_reply`). A reply is a reply miss when it closes with a question the turn was not blocked for, or when it runs over 100 words (`MAX_REPLY_WORDS`, about the five sentences `root.md` allows at most). A turn is blocked when its case carries the tag `ask_back` (a request that can mean two different changes, or dates whose day and month order reads both ways), when a data call of the turn failed, or when the turn left a required role of the spec without a column (`roles_open`), the case `root.md` names for a question. The reply gate is at most 10 % reply misses among the answered cases with a reply (`--max-reply-misses`); the summary reports the median and the longest reply in words, the questions per reply and the closing questions. A case can send the page's step-1 summary action instead of a message (`"action": "summarize_data"`, with a dataset and no expected operation); the four summary cases (tag `summary`) are a German pie of pets, a German wide table of expenses that `.to_long()` fits, an English daily series, and English slash dates whose order reads both ways, the one that may close with its question. In the user test of 2026-11-01, 10 of 11 summaries closed with a question that changed nothing. The reply text never enters the report.
Since 2026-11-01 (owner decision: "short and to the point, no waffle, follow-up questions only when they really matter") the eval also measures every reply of the root: its words, its questions, and whether it closes with a question (`measure_reply`). A reply is a reply miss when it closes with a question the turn was not blocked for, or when it runs over 100 words (`MAX_REPLY_WORDS`, about the five sentences `root.md` allows at most). A turn is blocked when its case carries the tag `ask_back` (a request that can mean two different changes, or dates whose day and month order reads both ways), when a data call of the turn failed, or when the turn left a required role of the spec without a column (`roles_open`), the case `root.md` names for a question. The reply gate is at most 10 % reply misses among the answered cases with a reply (`--max-reply-misses`); the summary reports the median and the longest reply in words, the questions per reply and the closing questions. A case can send the page's step-1 summary action instead of a message (`"action": "summarize_data"`, with a dataset); the eight summary cases (tag `summary`) are a German pie of pets, a German wide table of expenses that `.to_long()` fits, an English daily series, English slash dates whose order reads both ways (it may close with its question), and, since the live test of 2026-11-01, the owner's RTX 4090 price table (`Nov 2026` to `Dec 2025`, newest first, a change column with `−` signs) and its German twin (`Okt. 2026`, prices in €), whose only expected operation is the `sort` of the month column, an English column of `8 Nov 2026`-style dates that converts for certain (the `set_type` into `Date (date)` and the sort, `data/conversions.py`), and German dates with two-digit years (`03.11.26`), an uncertain conversion that may close with its question and expects none. In the user test of 2026-11-01, 10 of 11 summaries closed with a question that changed nothing. The reply text never enters the report.

Since 2026-11-02 a summary case is also checked for the conversions it owes (`score_conversions`): before the turn the run computes in code what the session block will list for the open roles (`conversions.candidates`), and after it each `certain` line must be among the root's operations. A root that skips one is the original failure of the live test of 2026-11-01, so the case is a miss whatever its fixture expected, and any skip fails the run. The summary counts `conversions_owed` and `conversions_made`, which is how often the root applies a `certain` conversion. Three summary cases joined: a list of numbers on `line-basic` in English and German, which gets its row numbers (`row_number`), and a `Year` column for a date role, read as years.

1. Complete steps 1 and 2 of [Before you begin](#before-you-begin): the dependencies and Application Default Credentials. The eval reads no `.env` file.
2. Run the eval:
Expand Down
30 changes: 29 additions & 1 deletion agents/anyplot/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
* `root_agent` ("anyplot") is the only agent that talks to the user. Its policy is the
constant `static_instruction` (`policy.root_instruction`), never templated; the
session block `session_context` adds only server-validated values (spec id,
library, the session language and its source, dataset and binding status, whether
library, the session language and its source, dataset and binding status, the
conversions that would fill a required role left open (`data/conversions.py`, their
lines fenced, since they name the user's columns), whether
the user pasted notes with the data (a fixed line, never the notes), plot
versions and the latest version's theme, so a change keeps it). The root's
`before_model_callback` (`add_session_context`) appends it to the system
Expand Down Expand Up @@ -38,7 +40,9 @@
from google.adk.plugins.context_filter_plugin import ContextFilterPlugin
from google.genai import types

from . import telemetry
from .data.bindings import check_bindings
from .data.conversions import candidates
from .dev_fixture import DevFixturePlugin
from .language import english_name, same_language
from .models import make_content_config, make_model
Expand Down Expand Up @@ -93,6 +97,12 @@
so the root knows to read them again after the context filter dropped the turn that carried them."""
FILE_LINE = "yes; get_dataset_profile returns its name under file_name, data and never a request"
"""The session block's line when the data came from a dropped file: a fixed text, never the name itself."""
CONVERSIONS_HEADING = (
"Conversions for open roles: {count}, columns that a set_type turns into what the role takes, or the row "
"numbers when no column can, from a dry run on the data (see 'Step 1: the data')"
)
"""The session block's line over the conversion candidates of the roles left open (`data/conversions.py`), live test
of 2026-11-01: the root asked the user for a date column that a conversion of the `Month` text would have made."""
BINDINGS_HINT = "; get_dataset_profile lists the bindings in force under bindings"
"""Where the root reads the bindings themselves. The live tests of 2026-10-14/15 caught the summary
describing other bindings than the real ones twice ("Staatsangehörigkeit for series" while series was
Expand Down Expand Up @@ -156,6 +166,24 @@ async def session_context(context: ReadonlyContext) -> str:
f"- Binding warnings: {len(check.warnings)}, roles bound to a column that does not fit them as it is"
)
lines.append(fence("user_data", "\n".join(check.warnings)))
found = (
candidates(
view.bindings,
view.snapshot.roles(),
profile,
dataset.as_parsed(),
language=view.language,
keep_text=dataset.text_columns,
cache=dataset.dry_runs,
)
if check.missing_roles
else []
)
if found:
# A dry run in code of the conversions that would fill an open role; the lines name the user's columns.
lines.append(f"- {CONVERSIONS_HEADING.format(count=len(found))}")
lines.append(fence("user_data", "\n".join(candidate.line() for candidate in found)))
telemetry.note_conversion(session_id, "asked") # until `prepare_data` makes a column in this turn
if view.data_confirmed:
lines.append("- Data: confirmed by the user; the plot can be made")
else:
Expand Down
Loading
Loading