Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e02676c
build(agents): add the Claude backend to the agents extra
MarkusNeusinger Oct 9, 2026
1c745a3
feat(agents): provider setting with Claude Haiku 5.5 as the default
MarkusNeusinger Oct 9, 2026
5b3b39d
feat(agents): model factory for Claude on Vertex AI and Gemini
MarkusNeusinger Oct 9, 2026
bcd5836
feat(agents): prompts and the instruction composer
MarkusNeusinger Oct 9, 2026
8d738f0
feat(agents): render contract, harness, gates and backends
MarkusNeusinger Oct 9, 2026
ae827d9
feat(agents): ScopeGuard, Budget and ToolSafety plugins with the requ…
MarkusNeusinger Oct 9, 2026
671cb3a
feat(agents): root agent, adapters, reviewer and the plot pipeline
MarkusNeusinger Oct 9, 2026
6f0a8be
feat(agents): development fixture seed and two fixture cases
MarkusNeusinger Oct 9, 2026
e39273c
feat(agents): /v1 service and the anyplot/1 stream translator
MarkusNeusinger Oct 9, 2026
a1ef8fc
test(agents): end-to-end Create plot through /v1 for both providers
MarkusNeusinger Oct 9, 2026
6f48139
test(agents): models, prompts, render, plugins, registry, stream, can…
MarkusNeusinger Oct 9, 2026
cdee1b3
docs(agents): runtime layout, local run, settings and changelog fragment
MarkusNeusinger Oct 9, 2026
f1f7b56
fix(agents): a version that cannot be stored ends as failed, with pip…
MarkusNeusinger Oct 9, 2026
504601e
fix(agents): close the pipeline's isolation scope so the next turn re…
MarkusNeusinger Oct 9, 2026
3193513
fix(agents): repair a sub-agent answer that fails its schema, and ski…
MarkusNeusinger Oct 9, 2026
cdd06b2
fix(agents): disable Claude thinking, pin forced-tool models, fail cl…
MarkusNeusinger Oct 9, 2026
eb23cf6
fix(agents): expire abandoned runs, one run per user, refuse off-scop…
MarkusNeusinger Oct 9, 2026
441d79a
fix(agents): a malformed render probe adds no gate line instead of fa…
MarkusNeusinger Oct 9, 2026
d5c11df
fix(agents): fence untrusted text everywhere a model reads it, saniti…
MarkusNeusinger Oct 9, 2026
1e5e6d0
fix(agents): keep THEME, the theme tokens and the final savefig intac…
MarkusNeusinger Oct 9, 2026
9c57e7b
feat(agents): a plan may add at most 2,000 characters of new string l…
MarkusNeusinger Oct 9, 2026
ff0ae14
fix(agents): versions belong to their library, and Claude prompts get…
MarkusNeusinger Oct 9, 2026
c52c68c
fix(agents): the local renderer stops its container when a render is …
MarkusNeusinger Oct 9, 2026
188252f
fix(api): the agent BFF accepts every spec role and passes on retryab…
MarkusNeusinger Oct 9, 2026
7c5d6ed
docs(changelog): note the runtime hardening and the BFF role fix in t…
MarkusNeusinger Oct 9, 2026
4e04b7f
fix(agents): the root never narrates the session block
MarkusNeusinger Oct 9, 2026
8528e20
chore(changelog): reference #12111 in the runtime fragment
MarkusNeusinger Oct 9, 2026
1d86fb1
fix(agents): role bullet regexes run in linear time
MarkusNeusinger Oct 9, 2026
72a1c63
fix(agents): render gates ship both themes and never echo error messages
MarkusNeusinger Oct 9, 2026
4a38cbd
fix(agents): catalogue and model-written text stays out of instructio…
MarkusNeusinger Oct 9, 2026
57e9be0
docs(agents): align the design doc with the built runtime and the Cla…
MarkusNeusinger Oct 9, 2026
a4376cc
fix(agents): exact error-class allowlist, one-theme canvas label, ran…
MarkusNeusinger Oct 9, 2026
a8bd3c8
docs(agents): phase-0 spikes cover both model arms
MarkusNeusinger Oct 9, 2026
fe9ec90
fix(agents): the last-digits family split uses string operations
MarkusNeusinger Oct 9, 2026
8fd433f
fix(agents): refuse the fake renderer outside development and test, o…
MarkusNeusinger Oct 9, 2026
96180e6
docs(gcp): budget guard runbook names the deployed state and both mod…
MarkusNeusinger Oct 9, 2026
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
8 changes: 6 additions & 2 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,15 @@
!README.md

# The agent network's image (agents/, docs/concepts/agent-network.md) builds from
# the same root context. It also needs the two prompt sources its adapter reads
# verbatim. The API image does not grow: api/Dockerfile COPYs only api/ and core/.
# the same root context. It also needs the catalogue prompt sources its adapter
# and reviewer read verbatim (agents/anyplot/policy.py). The API image does not
# grow: api/Dockerfile COPYs only api/ and core/.
!agents
!prompts/default-style-guide.md
!prompts/library
!prompts/quality-criteria.md
!prompts/workflow-prompts/impl-repair-claude.md
!prompts/workflow-prompts/ai-quality-review.md
# Promoted feedback cases hold users' data; they are synced here for eval runs
# and are never committed, so they must never reach an image either.
agents/evals/.cases
Expand Down
2 changes: 1 addition & 1 deletion agentic/docs/project-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ Example: `plots/scatter-basic/` contains everything for the basic scatter plot.
- **`core/database/repositories.py`**: Data access layer
- **`api/`**: FastAPI backend (routers, schemas, dependencies, cache, analytics, MCP server)
- **`app/`**: React frontend (Vite 8 + TypeScript 6 + MUI 9)
- **`agents/`**: Agent network on Google ADK 2.11 and Gemini ("Use with my data"); settings and pipeline contracts so far, design in `docs/concepts/agent-network.md`
- **`agents/`**: Agent network on Google ADK 2.11 and Vertex AI ("Use with my data"; Claude Haiku 5.5 by default, Gemini 3.8 Flash behind `AGENT_PROVIDER`); the runtime core (agents, plot pipeline, plugins, render layer, private `/v1` service) runs locally, see `agents/README.md`; design in `docs/concepts/agent-network.md`
- **`agentic/`**: AI workflow layer (composable phases, prompt templates, runtime state)
- **`agentic/workflows/`**: Click CLI scripts (plan, build, test, review + orchestrators)
- **`agentic/commands/`**: Markdown prompt templates
Expand Down
131 changes: 103 additions & 28 deletions agents/README.md
Original file line number Diff line number Diff line change
@@ -1,56 +1,131 @@
# Agent network

This directory holds the anyplot agent network: the service that lets an admin paste their own data on a plot page and get that plot adapted, rendered and reviewed ("Use with my data"). It is built with [Google ADK](https://adk.dev) 2.11 on Gemini through Vertex AI. The design, including every contract and guardrail, is in [Agent network design](../docs/concepts/agent-network.md).
This directory holds the anyplot agent network: the service that lets an admin paste their own data on a plot page and get that plot adapted, rendered and reviewed ("Use with my data"). It is built with [Google ADK](https://adk.dev) 2.11. The design, including every contract and guardrail, is in [Agent network design](../docs/concepts/agent-network.md).

Nothing here runs yet. This package currently contains the settings, the Pydantic contracts of the plot pipeline, and its deterministic data and code layers; the agents, tools, plugins, renderer and service arrive in later pull requests, as listed in the design's roadmap.
## What is built

The runtime core runs locally: the agents, the plot pipeline, the guardrail plugins, the render layer and the private `/v1` service. Not built yet: the Cloud Run sandbox render backend (it waits for spike S), the container image, the deploy, the regression harness and the evals.

The model is **Claude Haiku 5.5 on Vertex AI** (`claude-haiku-5-5`) by default. **Gemini 3.8 Flash** is the second arm: set `AGENT_PROVIDER=gemini` together with Gemini model ids, so the two can be compared on price and quality later. Every agent and the scope judge run on the configured provider.

## What lives here

| Path | Contents |
|---|---|
| `anyplot/settings.py` | `AgentSettings`, read from `AGENT_*` environment variables (plus `ENVIRONMENT`), with the pinned model, judge model and location as defaults |
| `anyplot/schemas.py` | The pipeline contracts with their size limits: `ColumnProfile`, `DatasetProfile`, `Binding`, `PipelineArgs`, `AdaptRequest`, `Edit`, `AdaptPlan`, `ReviewRequest`, `Defect`, `Verdict` and `PlotResult` |
| `anyplot/data/` | The deterministic data layer, without ADK: `parse.py` (pasted text to a canonical `data.csv` and a `DatasetProfile`), `roles.py` (data roles from a spec's `## Data` bullets), `bindings.py` (default role-to-column bindings and their checks) and `store.py` (the session-scoped dataset store) |
| `anyplot/code/` | The deterministic code layer, without ADK: `validate.py` (the two-profile AST validator), `regions.py` (the protected AST regions), `normalise.py` (the canvas normaliser), `readiness.py` (the blocked, coupled or clean scan with adapter hints), `edits.py` (applies an `AdaptPlan`), `loader.py` (the data placeholder in its run form) and `export.py` (the downloaded `plot.py`) |
| `main.py` | The `anyplot-agents` FastAPI service: the `/v1` routes the BFF (`api/routers/agent.py`) calls, the caller check, in-memory session and artifact services, and the idle sweeper |
| `stream.py` | The `anyplot/1` stream translator: ADK events in, sanitised `ready`, `status`, `message`, `plot`, `refusal`, `error` and `done` events out |
| `anyplot/agent.py` | The root agent `anyplot`, the `ALL_AGENTS` registry and `app` (the ADK `App` with its plugins), which `adk web` loads |
| `anyplot/models.py` | The only place that builds a model or a model client: `make_model`, `make_content_config` and `make_judge_client`, for Claude on Vertex AI and for Gemini |
| `anyplot/policy.py` | Composes each agent's static instruction from `anyplot/prompts/` and the catalogue's prompt sources, read verbatim; the fixed refusals; the data fences |
| `anyplot/prompts/` | `root.md`, `adapter.md`, `reviewer.md`, `scope_judge.md`, `data_judge.md` and `refusals.yaml` (English and German) |
| `anyplot/pipeline.py` | The `plot_pipeline` node: adapt, check, render, review once, repair once, always a `PlotResult` |
| `anyplot/sub_agents/` | One single-turn adapter per enabled library and the tool-less reviewer |
| `anyplot/tools/session.py` | The root's tools: `get_dataset_profile`, `get_spec_brief`, `get_current_code`, `set_bindings` and the `plot_pipeline` workflow |
| `anyplot/plugins/` | `ScopeGuardPlugin`, `BudgetPlugin`, `ToolSafetyPlugin` and the request ledger they share |
| `anyplot/render/` | The render contract, the probe harness, the host gates (R1-R3, advisory G3/G5/G7/G8), PNG hardening, the render store, and the `fake`, `local` and `sandbox` backends |
| `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 |
| `anyplot/dev_fixture.py` | The development-only session seed from an eval fixture case |
| `anyplot/settings.py` | `AgentSettings`, read from `AGENT_*` environment variables |
| `anyplot/schemas.py` | The pipeline contracts with their size limits |
| `anyplot/data/`, `anyplot/code/` | The deterministic data and code layers, without ADK |
| `evals/fixtures/cases/` | Fixture cases: `scatter-basic-matplotlib` and `bar-grouped-seaborn` |

The unit tests are in `tests/unit/agents/`; `tests/unit/agents/runtime/` drives a full "Create plot" through `/v1` with a fake renderer and scripted models for both providers.

Three rules hold for everything here:

The unit tests are in `tests/unit/agents/`.
- **Relative imports inside `anyplot/`.** ADK's loader imports the package as the top-level module `anyplot`, while the service imports it as `agents.anyplot`. Absolute imports of `core` are fine.
- **Models only through `models.py`.** Model ids, the project and the location come from `AgentSettings`; the settings refuse `-latest` aliases, regional locations such as `europe-west4`, and model ids that do not match the provider.
- **No content in logs.** The attribution log carries ids, counts and verdicts, never message text, code or data; the service switches span content capture off.

## Settings

| Variable | Default | Meaning |
|---|---|---|
| `AGENT_PROVIDER` | `anthropic-vertex` | `anthropic-vertex` (Claude on Vertex AI) or `gemini` |
| `AGENT_MODEL` | `claude-haiku-5-5` | Model of every agent; a `gemini-` id, or for `anthropic-vertex` a model in `CLAUDE_MODELS` (forced tool use with thinking disabled) |
| `AGENT_JUDGE_MODEL` | `claude-haiku-5-5` | Model of the scope and dataset judge; same rule |
| `AGENT_LOCATION` | `eu` | Vertex AI location: `eu`, `us` or `global` |
| `AGENT_PROJECT`, else `GOOGLE_CLOUD_PROJECT` | `anyplot` | Project that serves and bills the model calls |
| `AGENT_LIBRARIES` | `matplotlib,seaborn` | Enabled libraries; each needs a phase-1 runtime (`matplotlib`, `seaborn`), others are refused at startup |
| `AGENT_RENDERER` | `sandbox` | `sandbox`, `local` (Docker, development only), `fake` (fixture PNGs, development and test only) or `remote` |
| `AGENT_RENDER_IMAGE` | `anyplot-agents:dev` | Image the `local` renderer runs |
| `AGENT_RENDER_CONCURRENCY` | `2` | Theme renders at the same time |
| `AGENT_MAX_LLM_CALLS` | `12` | LLM calls per request |
| `AGENT_REQUEST_TOKEN_BUDGET` | `80000` | Tokens per request |
| `AGENT_DAILY_TOKEN_BUDGET` | `1000000` | Tokens per user and day |
| `AGENT_DAILY_PIPELINE_RUNS` | `40` | Pipeline runs per user and day |
| `AGENT_GLOBAL_DAILY_TOKEN_BUDGET` | `3000000` | Tokens per day for the whole service |
| `AGENT_RENDER_TIMEOUT_S` | `60` | Seconds per theme render |
| `AGENT_REQUEST_DEADLINE_S` | `180` | Hard request deadline |
| `AGENT_SOFT_DEADLINE_S` | `140` | Soft deadline inside the pipeline |
| `AGENT_JUDGE_TIMEOUT_S` | `4` | Budget of one judge verdict, its one retry included |
| `AGENT_SESSION_IDLE_S` | `3600` | Idle time after which the sweeper drops a session |
| `AGENT_ALLOWED_CALLERS` | empty | Service-account emails allowed to call `/v1` outside development |
| `AGENT_SERVICE_URLS` | empty | ID-token audiences accepted on `/v1` outside development |
| `AGENT_DEV_FIXTURE` | unset | Development only: a fixture case id that seeds every new session |
| `ENVIRONMENT` | `production` | Shared with the API; `local` rendering, the fixture seed and skipping the caller check need `development`, which is refused on Cloud Run |

## Run it locally

### Before you begin

1. Install the dependencies, including the two plotting libraries the renderer uses:

Two rules hold for everything added here:
```bash
uv sync --extra agents --extra test --extra lib-matplotlib --extra lib-seaborn
```

- **Relative imports inside `anyplot/`.** ADK's loader imports the package as the top-level module `anyplot`, while the service imports it as `agents.anyplot`. Absolute imports of `core` are fine.
- **No floating model aliases.** Model ids come from `AgentSettings` only, and the settings refuse `-latest` aliases and regional locations such as `europe-west4`.
Never install `openai-agents` into this environment: it ships a top-level `agents` module that shadows this package.

## Install the dependencies
2. Authenticate to Google Cloud with your own account (Application Default Credentials):

ADK is an optional extra, pinned to an exact version:
```bash
gcloud auth application-default login
```

```bash
uv sync --extra agents --extra test
```
3. Export the environment for Vertex AI on the `eu` multi-region:

```bash
export ENVIRONMENT=development GOOGLE_GENAI_USE_ENTERPRISE=TRUE \
GOOGLE_CLOUD_PROJECT=anyplot GOOGLE_CLOUD_LOCATION=eu AGENT_LOCATION=eu \
AGENT_PROVIDER=anthropic-vertex AGENT_MODEL=claude-haiku-5-5 AGENT_JUDGE_MODEL=claude-haiku-5-5
```

The `agents-eval` extra adds ADK's evaluation dependencies for `adk eval`. Never install `openai-agents` into this environment: it ships a top-level `agents` module that shadows this package.
For the Gemini arm, set `AGENT_PROVIDER=gemini AGENT_MODEL=gemini-3.8-flash AGENT_JUDGE_MODEL=gemini-3.5-flash-lite` instead.

## Run the agent locally (once the agent exists)
4. Choose a renderer: `AGENT_RENDERER=local` renders in Docker with the image in `AGENT_RENDER_IMAGE` (the agents image is not built yet, so this needs an image with Python, matplotlib and seaborn at `/app/.venv/bin/python`); `AGENT_RENDERER=fake` returns fixture PNGs and runs no code.

After the agent module lands, you can try it in ADK's development UI:
### Try the agents in ADK's development UI

1. Authenticate to Google Cloud:
1. Seed every new session with a fixture case, so there is a plot, a dataset and bindings to work with:

```bash
gcloud auth application-default login
export AGENT_DEV_FIXTURE=scatter-basic-matplotlib AGENT_RENDERER=fake ADK_DISABLE_LOAD_DOTENV=1
```

2. Point ADK at Vertex AI on the `eu` multi-region:
`ADK_DISABLE_LOAD_DOTENV=1` matters: `adk web` otherwise walks up from `agents/anyplot/` and loads the first `.env` it finds, which is the repository's root `.env` with production values.

2. Start the development UI with in-memory sessions and artifacts:

```bash
export GOOGLE_GENAI_USE_ENTERPRISE=TRUE GOOGLE_CLOUD_PROJECT=anyplot GOOGLE_CLOUD_LOCATION=eu \
AGENT_LOCATION=eu AGENT_RENDERER=local
uv run --extra agents adk web agents --port 8002 --session_service_uri memory:// --artifact_service_uri memory://
```

3. Start the development UI with in-memory sessions and artifacts:
3. Open `http://localhost:8002`, choose `anyplot`, and send "Create the plot".

```bash
uv run adk web agents --port 8002 --session_service_uri memory:// --artifact_service_uri memory://
```
`adk web` and `adk api_server` are unauthenticated and let the client choose the user id, so run them only on your own machine. Every message costs model calls: a "Create plot" is about four (root twice, the adapter, the reviewer) plus one judge call for free text.

### Run the service

`adk web` and `adk api_server` are unauthenticated and let the client choose the user id, so run them only on your own machine. The deployed service embeds ADK's `Runner` in its own FastAPI routes instead.
```bash
uv run uvicorn agents.main:app --port 8001
```

The service needs the header `X-Anyplot-User` on every `/v1` route; outside `ENVIRONMENT=development` it also requires the IAM-forwarded ID token (`AGENT_SERVICE_URLS`, `AGENT_ALLOWED_CALLERS`). To drive it from the plot page, run the API with `AGENT_ENABLED=true AGENT_SERVICE_URL=http://localhost:8001` (see `api/routers/agent.py`).

### Test

```bash
uv run pytest tests/unit/agents -q
```
13 changes: 10 additions & 3 deletions agents/anyplot/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
"""The `anyplot` agent: settings and the Pydantic contracts of the plot pipeline.
"""The `anyplot` agent: the root agent, its tools, the pipeline, the plugins and the `App`.

ADK's loader imports this package as the top-level module `anyplot` (`adk web agents`),
and the service imports it as `agents.anyplot`; both work only because every import
inside the package is relative. The agent itself (`agent.py` with `root_agent` and
`app`) arrives in a later PR, together with the `from . import agent` that ADK needs.
inside the package is relative. `adk web` looks for `app` (else `root_agent`) in this
package or in `anyplot.agent`; importing `agent` here makes the package itself carry
both, so either lookup finds the same objects.
"""

from . import agent
from .agent import app, root_agent


__all__ = ["agent", "app", "root_agent"]
Loading
Loading