Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ The model is **Claude Haiku 5.5 on Vertex AI** (`claude-haiku-5-5`) by default.
| `anyplot/data/generate.py` | The formula source behind `generate_data`: the `Generate` spec (one or two variable ranges, one to ten series), the whitelist evaluator over Python's `ast` (NumPy float64, never `eval`), the canonical CSV it writes, and `looks_like_formula`, the dataset route's `422 formula` guard |
| `anyplot/plugins/` | `ScopeGuardPlugin`, `BudgetPlugin`, `ToolSafetyPlugin` and the request ledger they share |
| `anyplot/output_guard.py` | The output sanitiser of every root message and plot-event line (links, HTML and long code blocks out, at most 1,200 characters) and the reply cap the ScopeGuard runs on the root's final reply before it is stored: a reply over 1,200 characters becomes the fixed refusal, the same in every language, or is cut at 1,200 characters when the turn ran the plot pipeline |
| `anyplot/render/` | The render contract, the probe harness, the host gates (R1-R3, advisory G3/G5/G7/G8/G9/G10), PNG hardening, the render store, the `remote` (the renderer service, phase 1), `local`, `fake` and `sandbox` (an unused in-process stub) backends, `serial.py`, the one render semaphore in front of every backend, and `watermark.py`, the footer strip the artifact route and the feedback bundle add to the raw render |
| `anyplot/render/` | The render contract, the probe harness, the host gates (R1-R3, advisory G3/G5/G7/G8/G9/G10/G11/G12), PNG hardening, the render store, the `remote` (the renderer service, phase 1), `local`, `fake` and `sandbox` (an unused in-process stub) backends, `serial.py`, the one render semaphore in front of every backend, and `watermark.py`, the footer strip the artifact route and the feedback bundle add to the raw render |
| `anyplot/render/fonts/` | JetBrains Mono 2.304 Regular and Bold for the footer strip, unmodified, with `OFL.txt` (SIL Open Font License 1.1) and a notice with their SHA-256 values ([fonts README](anyplot/render/fonts/README.md)) |
| `renderer/` | The `anyplot-renderer` service: `main.py` (`POST /render`, `GET /status`, one render slot), `executor.py` (one `sandbox do` per theme with the kill path, the byte watchdog and the launcher retry), `auth.py` (the caller check), `settings.py` (`RENDERER_*`), `wire.py` (the JSON contract the `remote` backend shares), `Dockerfile` and `cloudbuild.yaml`. No ADK import and no `__init__.py`, so `adk web agents` does not list it as an agent |
| `anyplot/opening.py`, `session_state.py`, `services.py`, `briefs.py` | Opening a session and taking in a dataset, the server-set session state, the process-wide stores, and the spec and dataset briefs |
Expand Down Expand Up @@ -403,7 +403,7 @@ Before the first case the harness renders the catalogue file of the first case o
--renderer remote --render-url https://RENDERER_URL --gcloud-token
```

- The full matrix is the 123 cases that are not stress cases. Cap the spend with `--budget-usd`:
- The full matrix is the 127 cases that are not stress cases. Cap the spend with `--budget-usd`:

```bash
uv run --extra agents python -m agents.evals.matrix --cases full --budget-usd 4 \
Expand Down
48 changes: 48 additions & 0 deletions agents/anyplot/language.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"""

import re
from dataclasses import dataclass
from typing import Literal


Expand Down Expand Up @@ -74,6 +75,53 @@
"""The English name of each code in `_NAMES`: server text, so a prompt may name the language outside a fence."""


@dataclass(frozen=True)
class NumberFormat:
"""How the text in a plot writes numbers in one language: its decimal and thousands marks and its short words
for large numbers, each as `(power of ten, word)`, smallest first."""

decimal: str
thousands: str
suffixes: tuple[tuple[int, str], ...]


NUMBER_FORMATS: dict[str, NumberFormat] = {
"en": NumberFormat(".", ",", ((3, "k"), (6, "M"), (9, "B"))),
"de": NumberFormat(",", ".", ((3, "Tsd."), (6, "Mio."), (9, "Mrd."))),
"fr": NumberFormat(",", " ", ((3, "k"), (6, "M"), (9, "Md"))),
"es": NumberFormat(",", ".", ((3, "mil"), (6, "M"), (9, "mil M"))),
"it": NumberFormat(",", ".", ((3, "mila"), (6, "Mln"), (9, "Mld"))),
"pt": NumberFormat(",", ".", ((3, "mil"), (6, "mi"), (9, "bi"))),
"nl": NumberFormat(",", ".", ((3, "k"), (6, "mln"), (9, "mld"))),
"pl": NumberFormat(",", " ", ((3, "tys."), (6, "mln"), (9, "mld"))),
"ru": NumberFormat(",", " ", ((3, "тыс."), (6, "млн"), (9, "млрд"))),
"vi": NumberFormat(",", ".", ((3, "N"), (6, "Tr"), (9, "Tỷ"))),
"zh": NumberFormat(".", ",", ((4, "万"), (8, "亿"))),
"ja": NumberFormat(".", ",", ((4, "万"), (8, "億"))),
}
"""The number format of each language in `ENGLISH_NAMES`, the server's own table (Q4 of the live test of 2026-10-14:
a German plot showed "6.0 M" and "5.30"). `locale` is a banned import in plot code, so the adapter formats with
`FuncFormatter` or an f-string and these marks (`number_format_table`), and gate G12 measures the decimal mark.
Chinese and Japanese group large numbers by ten thousand (万) and a hundred million (亿, 億). A language
outside the table has no number rule and no G12."""


def number_format(language: str) -> NumberFormat | None:
"""The number format of a session language (`de`, `Deutsch` and `German` share one), or None outside the table."""
return NUMBER_FORMATS.get(canonical(language))


def number_format_table() -> str:
"""The table as Markdown rows for the adapter's static instruction: code, language, marks and words."""
names = {" ": "space", " ": "narrow no-break space", " ": "no-break space"}
rows = ["| Plot language | Decimal mark | Thousands mark | Large numbers |", "|---|---|---|---|"]
for code, spec in NUMBER_FORMATS.items():
words = ", ".join(f"10^{power}: {word}" for power, word in spec.suffixes)
thousands = names.get(spec.thousands, f"`{spec.thousands}`")
rows.append(f"| `{code}` ({ENGLISH_NAMES[code]}) | `{spec.decimal}` | {thousands} | {words} |")
return "\n".join(rows)


def normalise(value: str) -> str:
"""The stored form of a language.

Expand Down
8 changes: 5 additions & 3 deletions agents/anyplot/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,8 @@ class Candidate:
adaptation_rules: list[str] = field(default_factory=list)
"""The ADAPTATION validator rule ids behind `adaptation_lines`, for the attribution log."""
advisory_gates: list[str] = field(default_factory=list)
"""The advisory probe gate ids (`G3`, `G5`, `G7`, `G8`, `G9`, `G10`) behind `advisory_lines`, for the attribution log."""
"""The advisory probe gate ids (`G3`, `G5`, `G7`, `G8`, `G9`, `G10`, `G11`, `G12`) behind `advisory_lines`, for the
attribution log."""
review_score: int | None = None
"""The catalogue review's score of this render, when it had one (`AGENT_REVIEWER_MODE=catalogue`)."""
review_lines: list[str] = field(default_factory=list)
Expand Down Expand Up @@ -933,7 +934,8 @@ def finish(run: Run) -> PlotResult:
if shipped.canvas_line:
residual.append(shipped.canvas_line)
# The measured findings are facts of the shipped render, reviewed or not: the ADAPTATION findings and
# every advisory gate line (clipping G3, annotations outside G5, overlap G7, G8, G9, missing glyphs G10).
# every advisory gate line (clipping G3, annotations outside G5, overlap G7, G8, G9, missing glyphs G10,
# crowded per-mark labels G11, the plot language's decimal mark G12).
# No review score hides them (until 2026-10-14 only G10 stayed above the ship bar).
residual += shipped.adaptation_lines
residual += shipped.advisory_lines
Expand Down Expand Up @@ -1436,7 +1438,7 @@ async def _attempts(
started = time.monotonic()
rendered = await services.backend.render(job)
render_s = time.monotonic() - started
report = evaluate(rendered, themes=job.themes, library=view.library, rows=rows)
report = evaluate(rendered, themes=job.themes, library=view.library, rows=rows, plot_language=run.language)
if report.passed_host_gates:
run.repair_hints = list(report.repair_hints)
attribution(
Expand Down
3 changes: 3 additions & 0 deletions agents/anyplot/policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@

import yaml

from .language import number_format_table


PACKAGE_DIR = Path(__file__).resolve().parent
PROMPTS_DIR = PACKAGE_DIR / "prompts"
Expand Down Expand Up @@ -263,6 +265,7 @@ def adapter_instruction(library: str) -> str:
# The rubric holds the VQ-03 marker-size and alpha table (`vq03_table`), so it is not repeated.
return _join(
own_prompt("adapter.md"),
"## Number formats by plot language\n\n" + number_format_table(),
highlights,
"## Excerpt: which weaknesses to fix\n\n" + weaknesses,
"## Excerpt: visual-sizing fixes\n\n" + sizing,
Expand Down
2 changes: 2 additions & 0 deletions agents/anyplot/prompts/adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ These rules replace the catalogue's own rules where they differ:
- **Keep the plot.** Same plot type, same variant, same encodings and the same style. Change only what the user's data, the hints, the feedback and the change request need. Add no encodings, reference lines, highlights or callouts that nobody asked for.
- **Callouts and statistics.** A label or callout that states a number must compute it from `df` in an f-string (`f"r = {df[x].corr(df[y]):.2f}"`, written with the column variables of the code). A callout target comes from a rule over `df` (the maximum, the latest row), and a conditional claim is drawn only when `df` meets it. Remove a callout that cannot be derived.
- **Density.** Adapt marker sizes, line widths and alpha to the user's row count (see the VQ-03 table below).
- **Per-mark labels and decorations.** "The same style" covers elements whose count stays fixed (a title, a legend, one callout), not elements the catalogue draws once per bar or point of its few example marks: value labels, dots on bar ends, shadows. Their count grows with the user's data, so keep them only while each label fits the space of its mark and the plot stays legible, which rarely holds past about 25 marks; with more marks, drop them, or label only the extremes (the largest and the smallest) when the plot needs numbers. The server measures this (a `VQ-03` gate line counts the labels, the decorations and the pixels each label has).
- **Numbers.** Write every number in the plot text (tick labels, value labels, annotations, a number in the title) with the plot language's decimal and thousands marks and its words for large numbers, from the table "Number formats by plot language" below: `de` writes `5,30` and `6,5 Mio.`, `en` writes `5.30` and `6.5 M`. Python's `locale` is not available: format with an f-string, which writes `,` for thousands and `.` for decimals, then swap both marks through a placeholder: `f"{v:,.2f}".replace(",", "_").replace(".", DECIMAL).replace("_", THOUSANDS)`, with `DECIMAL` and `THOUSANDS` set once to the table's marks (`de`: `","` and `"."`; `fr`: `","` and `" "`; `en` keeps the f-string as it is), through a `matplotlib.ticker.FuncFormatter` for the axes and the same function for value labels (`ax.bar_label(container, labels=[...])`, never `fmt="%.2f"`, which always writes a point). Years, dates, identifiers and the cells of the user's data stay as they are. A plot language the table does not list keeps the catalogue's formats. The server checks the decimal mark of every numeric tick label and annotation (a `VQ-06` gate line).
- **Dates.** Use `matplotlib.dates.AutoDateLocator` with `ConciseDateFormatter` instead of a fixed locator, unless the change request asks for a specific one.
- **Safe code.** Use only the imports the code already has, plus, when needed, `numpy`, `pandas`, the plotting library, scipy's computing modules (`scipy.stats`, `scipy.interpolate`, `scipy.signal`, `scipy.optimize`, `scipy.fft`, `scipy.linalg`, ...) and these standard modules: `math`, `cmath`, `statistics`, `decimal`, `fractions`, `numbers`, `datetime`, `calendar`, `zoneinfo`, `collections`, `itertools`, `functools`, `operator`, `bisect`, `heapq`, `typing`, `warnings`, `textwrap`, `colorsys` and `re`. Never import `io`, `sys`, `pathlib`, `random`, `locale` or `string`. No file or network access, no `os` beyond the THEME line, no `.format(...)` (use f-strings), no `eval`, `exec`, `getattr`, `operator.attrgetter` or other reflection, no names with double underscores or private names of a module (`module._name`), no `class` statements, and no string literal longer than 200 characters.

Expand Down
2 changes: 1 addition & 1 deletion agents/anyplot/prompts/reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Do not judge anything else: not the catalogue title format, not the realism of t

These are never defects:

- The house style: a size, color, font or spacing the style guide prescribes, and a styling element the catalogue plot already had, such as a shadow, an outline, a grid or a muted reference line. Data claims are not styling: a callout at an example value is still DQ-03.
- The house style: a size, color, font or spacing the style guide prescribes, and a styling element the catalogue plot already had, such as a shadow, an outline, a grid or a muted reference line. Data claims are not styling: a callout at an example value is still DQ-03. Nor is an element drawn once per bar or point: value labels, dots on bar ends or shadows that the catalogue drew on a few example marks and the user's data repeats on many are judged like any other element (VQ-03 when they crowd the marks).
- Column names, units and category labels written the way the user's data writes them, in every plot language; a text whose language you cannot judge.
- A gate note on its own (see "The request").
- Something that could be better but fails no criterion above.
Expand Down
2 changes: 1 addition & 1 deletion agents/anyplot/prompts/scorer.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ The rubric below was written for catalogue implementations that generate their o
3. **No catalogue title.** The mandated catalogue title (`{spec-id} · {language} · {library} · anyplot.ai`) does not apply: a title that names the user's data is correct, and the 5d note on the long mandated title is about that format only. SC-04 asks whether the title and the legend describe the user's data. The adapter keeps the user's column names on purpose: an axis title, legend title or label that is a bound column name (the bindings block lists them) or a category from the data, exactly as the data writes it, is correct and never a VQ-06 or SC-04 deduction, even when it is terse, abbreviated, has no unit or is in another language than the plot language. Units and category labels keep the spelling of the user's data too. Only a text the code added, a sentence or phrase rather than a name from the data, that is plainly in another language than the plot language is a VQ-06 defect.
4. **One theme.** Apply the theme-readability checklist (5c) only for the attached render's theme, and name that theme, or `code`, in every defect line instead of `both`.

The house style is never a defect: a size, color, font or spacing the style guide prescribes, and a styling element the catalogue plot already had.
The house style is never a defect: a size, color, font or spacing the style guide prescribes, and a styling element the catalogue plot already had. This covers elements whose count stays fixed, not those drawn once per bar or point: value labels, dots on bar ends or shadows that the catalogue drew on its few example marks and the user's data repeats on many are judged like any other element (VQ-03 when they crowd the marks, VQ-02 when they overlap).

## Your answer

Expand Down
Loading
Loading