Skip to content

Add pelican_svg_env: blind SVG drawing, scored in three layers - #1019

Merged
sergiopaniego merged 13 commits into
mainfrom
add-pelican-svg-env
Jul 30, 2026
Merged

Add pelican_svg_env: blind SVG drawing, scored in three layers#1019
sergiopaniego merged 13 commits into
mainfrom
add-pelican-svg-env

Conversation

@sergiopaniego

@sergiopaniego sergiopaniego commented Jul 29, 2026

Copy link
Copy Markdown
Member

Summary

Simon Willison's "generate an SVG of a pelican riding a bicycle" as an environment that can be evaluated and trained against. The model never sees its own output, so it has to hold the spatial arrangement in its head and emit coordinates for it. Ships with two examples: an eval script over HF Inference Providers and a GRPO training script for TRL on HF Jobs.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • New environment
  • Refactoring

Alignment Checklist

Before submitting, verify:

  • I have read .claude/docs/PRINCIPLES.md and this PR aligns with our principles
  • I have checked .claude/docs/INVARIANTS.md and no invariants are violated
  • I have run /pre-submit-pr (or bash .claude/hooks/lint.sh and tests) and addressed all issues

RFC Status

  • Not required (bug fix, docs, minor refactoring)
  • RFC exists: #___
  • RFC needed (will create before merge)

Notes: rewards live entirely inside the environment (three layers: source gate, deterministic geometry, vision judge composed via the RFC 004 rubric tree), and the client never imports from server/.

Test Plan

  • PYTHONPATH=src:envs uv run pytest tests/envs/test_pelican_svg_env.py tests/envs/test_pelican_svg_training_contract.py -v — 132 tests, no network. The judge is exercised through a stub client, including the failure path where a broken judge must not inflate the reward.
  • The environment is deployed and serving at https://huggingface.co/spaces/sergiopaniego/pelican-svg-envcurl .../health, then reset/step over the WebSocket client.
  • End-to-end artifacts produced with this exact code: 139 scored samples from 7 frontier models (dataset, includes all 30 subject-vehicle tasks as a second config), and two GRPO checkpoints trained against the live environment on HF Jobs (judge off, judge on, curves).
  • fixtures/ holds 12 hand-written SVGs with verdicts fixed in advance (honest attempts, two deliberate cheats, degenerate cases) that run as regression tests for the gate.

Claude Code Review

N/A


Note

Medium Risk
Large new surface area scoring untrusted model SVG (parsing, rasterization) and optional external vision API calls; reward logic is intricate but isolated to the new env package.

Overview
Adds pelican_svg_env, a single-turn OpenEnv where the policy outputs SVG for “animal riding vehicle” tasks without seeing the render. Scoring runs in sequence: a source gate (cheats, malformed SVG, blank canvas), deterministic structure (wheels, frame, rider geometry; wheel count adapts per vehicle), then an optional HF vision judge (blind caption + per-task checklist). Rewards are composed via the RFC 004 rubric (Gate → weighted structure/semantic).

The env ships with a FastAPI server, Docker image, openenv.yaml Space config, client/models, 12 SVG fixtures, and docs (new pelican_svg page plus catalog/toctree entries). Task catalog is 6×5 combinations with task_id pinning; judge can be disabled or forced offline via env vars without renormalizing failed judge calls into higher rewards.

Reviewed by Cursor Bugbot for commit 34e828a. Bugbot is set up for automated code reviews on this repo. Configure here.

Copilot AI review requested due to automatic review settings July 29, 2026 16:40

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@burtenshaw burtenshaw added environment size: extra-large Extra-large pull request labels Jul 29, 2026 — with Cursor
@bot-ci-comment

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

@sergiopaniego
sergiopaniego requested a review from burtenshaw July 29, 2026 16:41
Comment thread envs/pelican_svg_env/server/vision_judge.py Fixed
Comment thread examples/pelican_svg_eval.py Fixed
Comment thread envs/pelican_svg_env/server/pelican_svg_environment.py
Copilot AI review requested due to automatic review settings July 29, 2026 16:45

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

version = "0.1.2"
description = "Pelican SVG Environment for OpenEnv"
requires-python = ">=3.10"
dependencies = [

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tier 1 (blocking CI): this env ships without a uv.lock — the only one of 37 envs missing it. The validate-env-locks CI job runs uv sync --frozen --all-groups --all-extras --dry-run --no-install-project here (triggered by this added pyproject.toml) and fails:

error: Unable to find lockfile at `uv.lock`, but `--frozen` was provided.

(reproduced on a clean checkout of this PR head). Fix: cd envs/pelican_svg_env && uv lock and commit the resulting uv.lock. The Docker build's uv sync --frozen also needs it to pin dependencies (reproducibility).

Comment thread envs/pelican_svg_env/pyproject.toml Outdated
"fastapi>=0.115.0",
"pydantic>=2.0.0",
"uvicorn[standard]>=0.24.0",
"requests>=2.31.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: requests is declared but never imported anywhere in the env (grep finds it only here). Safe to drop unless an external consumer relies on it — the judge uses huggingface_hub and the client uses openenv.core.

# synchronous `step()` path runs each call under its own short-lived
# loop, so a cached client fails with "Event loop is closed" on every
# request after the first.
async with AsyncInferenceClient(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alignment ✓ (rewards inside the environment): this external VLM call runs server-side inside the reward path (step_async → evaluate_submission → VisionJudge.evaluate), so domain knowledge stays encapsulated per INVARIANTS "Rewards in environment" — it is not external reward computation. Nicely done too: the token comes from an env var and is never logged, building the client per-call sidesteps the "event loop is closed" reuse bug, and a judge failure returns error/scores 0 (sample marked unjudged) instead of raising — which test_judge_failure_does_not_raise_the_reward pins so "break the judge" can't inflate reward.

Copilot AI review requested due to automatic review settings July 29, 2026 17:09

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings July 30, 2026 07:52

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sergiopaniego

Copy link
Copy Markdown
Member Author

Addressed the review findings in ae7633e:

  • Judge without credentials (cursor, high): real bug, fixed. _judge_from_env now only configures a judge when a token actually exists, checking the env vars and the ambient huggingface_hub token. Without one it returns None, so the rubric weights collapse to the deterministic layer instead of silently capping every reward at 0.35.
  • Missing uv.lock (cursor, tier 1): added. For the record, validate-env-locks was green on the previous commits too, so the job tolerates a missing lock, but every other env ships one and now this one does as well.
  • Unused requests dependency (cursor, minor): dropped, it was never imported.
  • Dead ... after a Protocol docstring and mixed implicit/explicit returns (code-quality): both fixed, the eval retry loop now has a single explicit failure return.

132 env tests, lint and the docs sync check all pass.

Comment thread envs/pelican_svg_env/server/pelican_svg_environment.py Outdated
Copilot AI review requested due to automatic review settings July 30, 2026 08:20

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sergiopaniego

Copy link
Copy Markdown
Member Author

Bugbot's second-pass finding was real too, fixed in 59157ba: _pick_task only honoured a pin when both subject and vehicle were set, so pinning just one was silently ignored and reset fell through to sampling or the canonical pair. A partial pin now fills the unpinned half from the canonical pelican-bicycle counterpart, the docstring says so, and test_partial_pin_is_honoured covers both directions.

Comment thread envs/pelican_svg_env/server/geometry.py
@sergiopaniego

Copy link
Copy Markdown
Member Author

One more edge, raised by an external review of 59157ba and fixed in the follow-up commit: the partial-pin fill could resolve to the canonical pair under held_out_only=True, quietly breaking that flag's promise. And it turned out a full canonical pin had always slipped past it too, since pins resolve before the held-out branch.

Decision taken: held_out_only is a hard promise. Any pin that resolves to the canonical task now raises ValueError with a message naming the conflict, held-out pins still work, and pins take precedence over sample_tasks explicitly. Docstrings updated and five new tests pin the precedence rules: reset-time partial pins, pin-beats-sampling, both rejection paths, and a held-out pin that must keep working. 137 tests green.

Copilot AI review requested due to automatic review settings July 30, 2026 08:33

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sergiopaniego

Copy link
Copy Markdown
Member Author

Follow-up on the arc-flag finding, fixed in the latest commit: the path tokenizer was context-free, so compact arc flags (A 25 25 0 0175 50, which the spec reads as 0, 1, 75, 50) merged into a single number, threw the 7-argument count off and silently dropped the arc. The tokenizer now tracks the active command and consumes exactly one character at an arc's two flag positions.

Verified against the literal reported case, a negative coordinate glued to the flags (a25 25 0 10-50 0), and a full wheel drawn as two compact arcs, which comes out with circularity 1.0. The circle-spelling agreement test gains an arc_compact_flags variant that asserts the compact form measures identically to the spaced one. 138 tests green.

Copilot AI review requested due to automatic review settings July 30, 2026 08:57

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread envs/pelican_svg_env/server/vision_judge.py Outdated
Simon Willison's "generate an SVG of a pelican riding a bicycle" as an
environment that can be evaluated and trained against. The model never sees its
own output, so it has to hold the spatial arrangement in its head and emit
coordinates for it.

Scoring runs cheapest-first, so a submission that is not an honest attempt never
reaches a paid call:

- a source gate rejecting embedded rasters, text labels, external references and
  degenerate documents, before anything is rendered
- seven deterministic geometry checks over the flattened shapes, weight 0.35
- a vision judge, blind caption plus a per-task feature checklist, weight 0.65

6 animals by 5 vehicles, 30 tasks. The judge's checklist, the anti-cheat terms
and the expected wheel count all derive from the task rather than being
hardcoded for the pelican.

Also adds two examples: pelican_svg_eval.py scores any set of models through HF
Inference Providers, and pelican_svg_grpo.py trains against the environment with
TRL and is runnable on HF Jobs.
…mode, row ranking

- _parse_checklist only counts a JSON true. bool() coerced strings like
  "false" or "no" from a judge that ignored the schema into approval.
- sample_completions applies the chat template with the same enable_thinking
  the training dataset used, so the probe measures the mode the policy was
  trained under.
- find_wheels ranks candidate rows by bottom edge, centre plus radius, which
  is what touches the ground. Ranking by centre let a pedal drawn below the
  axle displace a single expected wheel, and made the hub-versus-rim tie
  depend on rounding.
Copilot AI review requested due to automatic review settings July 30, 2026 10:11

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sergiopaniego

Copy link
Copy Markdown
Member Author

Second Bugbot pass addressed in 23e3a60, all three were real:

  • Falsy strings inflate checklist: _parse_checklist coerced with bool(), so a judge that ignored the schema and answered "false" or "no" as strings read as approval. Only a JSON true counts now, consistent with the parser's own rule that ambiguity counts against the submission.
  • Probe ignores thinking flag: sample_completions now applies the chat template with the same enable_thinking the training dataset used, at both probe call sites. Probing in a different template mode than the policy was trained under measures the wrong thing.
  • Unicycle prefers hub over wheels: candidate rows are now ranked by bottom edge (centre plus radius), which is what touches the ground. That fixes the reported case, a pedal below the axle displacing a single expected wheel, and it also removes the hub-versus-rim tie that previously leaned on height quantisation.

Two regression tests added (test_truthy_strings_do_not_pass_the_checklist, test_a_pedal_below_the_axle_does_not_displace_a_single_wheel), 140 tests green.

The expected count outranked the bottom edge, so with a single expected
wheel a lone pedal, which matches the count, beat two real wheels that
reach the ground lower. A bicycle could pass as a unicycle. Where a row
sits is a fact about the drawing and the expected count is only a
preference, so the ground now votes first. Regression: two wheels plus a
pedal scored as a unicycle finds two wheels and fails wheel_count.
Copilot AI review requested due to automatic review settings July 30, 2026 10:19

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sergiopaniego

Copy link
Copy Markdown
Member Author

One more from the external deep review, fixed in 477a1ab: the row ranking put the expected wheel count above the bottom edge, so with expected=1 a lone pedal, which matches the count, still beat two real wheels that reach the ground lower. A bicycle drawing could pass as a unicycle.

The key is reordered so the ground votes first: bottom edge, then expected count, then radius. Where a row sits is a fact about the drawing, the expected count is only our preference. Regression added: two wheels plus a pedal, scored as a unicycle, must find two wheels and fail wheel_count honestly instead of adopting the pedal. 141 tests green, and the earlier hub, rider-size and single-wheel-pedal regressions all still pass under the new ordering.

Comment thread envs/pelican_svg_env/server/structure.py
Copilot AI review requested due to automatic review settings July 30, 2026 10:27

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread envs/pelican_svg_env/server/geometry.py
A typo'd task_id arrives over the wire and used to surface as a bare
KeyError inside an EXECUTION_ERROR envelope. The error now says which ids
exist.
_frame_spans_wheels took the two leftmost wheel candidates as the hub
span while wheels_apart already used the outermost pair, so a stray
circle to the left of the bicycle shrank the span to a gap the frame
never crosses. The span now runs between the outermost pair and only
that pair is excluded from the coverage count, so a middle wheel still
counts as bridging geometry.
Copilot AI review requested due to automatic review settings July 30, 2026 10:43

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sergiopaniego

Copy link
Copy Markdown
Member Author

Fixed in 0f15db5. _frame_spans_wheels now takes the outermost pair as the span, matching wheels_apart, and only that pair is excluded from the coverage count so a middle wheel candidate still counts as bridging geometry. Added a regression test with a stray circle to the left of the bicycle: it failed the frame check before the fix and passes now.

Note on the branch: the previous head (8d591e2) accidentally swept in unrelated README formatting churn, which is what broke check-env-docs. That commit was replaced, the branch now only touches pelican files again.

flatten_path kept one last_control for cubics and quadratics, so S
reflected a quadratic control and T a cubic one. Per the SVG spec S may
reflect only after C/S and T only after Q/T, otherwise the first control
is the current point. The control point is now tracked per curve type.
Copilot AI review requested due to automatic review settings July 30, 2026 10:55

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sergiopaniego

Copy link
Copy Markdown
Member Author

Fixed in 52b19eb. flatten_path now tracks the previous control point per curve type: S reflects only after C/S and T only after Q/T, falling back to the current point otherwise, as the spec requires. Two regression tests cover the mixed sequences (Q then S, C then T): both dipped below the baseline with the shared control and stay on it now.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 52b19eb. Configure here.

Comment thread envs/pelican_svg_env/server/geometry.py
extract_shapes walked every drawable element, so a bicycle hidden with
display none, visibility hidden, opacity 0, an unpainted fill and
stroke, or coordinates outside the viewBox still earned the full
structural reward while a visible decoy satisfied the gate. The walk now
skips hidden subtrees, tracks inherited paint, and drops shapes whose
bounding box never intersects the canvas.
Copilot AI review requested due to automatic review settings July 30, 2026 11:38

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sergiopaniego

Copy link
Copy Markdown
Member Author

Fixed in 34e828a. extract_shapes now skips subtrees hidden with display: none, visibility: hidden or opacity: 0 (attribute or inline style), tracks inherited fill and stroke so an outline whose paint resolves to none/none contributes nothing, and drops shapes whose bounding box never intersects the viewBox. Verified the exploit end to end: a hidden bicycle plus a visible decoy scored structure 1.0 before and 0.0 now. Ten regression tests cover each hiding channel plus the keep-cases (stroked outline with no fill, a child repainting inside an unpainted group, partially off-canvas shapes).

@sergiopaniego
sergiopaniego merged commit 5298e0d into main Jul 30, 2026
9 checks passed
@sergiopaniego
sergiopaniego deleted the add-pelican-svg-env branch July 30, 2026 12:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

environment size: extra-large Extra-large pull request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants