Skip to main content

An HTTP service that compiles Asymptote (*.asy) sources into rendered output (PDF, SVG, EPS, PNG, JPG).

Project description

asyagent

An HTTP service that compiles Asymptote (*.asy) vector-graphics source code into rendered output (PDF, SVG, EPS, PNG, JPG) and returns it either inline (raw binary or base64 JSON) or as an object-storage URL.

AsyAgent is a proxy that wraps a local tool and exposes it as an API with optional object-storage upload for Asymptote: the asy compiler doesn't have a server mode, so asyagent wraps it in an HTTP API.

Key characteristics

  • Zero third-party dependencies — pure Python 3.10+ standard library. No pip install, no virtualenv required.
  • Header-driven control plane — all request behaviour (output format, response mode, encoding, DPI, timeout, storage prefix) is controlled by X-Asy-* request headers.
  • Dual response modes — return compiled output inline (binary or base64 JSON) or upload to storage and return a URL.
  • Multiple storage backends — local filesystem (zero-config), S3-compatible object storage (hand-rolled AWS Signature V4), or disabled.
  • Multi-format — native pdf/svg/eps, plus png/jpg via Ghostscript rasterization. Multi-output (multiple shipout() calls) is auto-bundled as a ZIP.

Quick start

# Start the server (zero config — uses local storage)
python3 -m asyagent

# Compile an asy source, get PDF inline
curl -s http://127.0.0.1:8787/v1/render \
  --data-binary 'size(5cm); draw(unitcircle);' \
  -H 'Content-Type: text/plain' \
  -o circle.pdf

# Get a PNG instead
curl -s http://127.0.0.1:8787/v1/render \
  --data-binary 'size(5cm); draw(unitcircle);' \
  -H 'Content-Type: text/plain' \
  -H 'X-Asy-Format: png' \
  -o circle.png

# Return base64 JSON
curl -s http://127.0.0.1:8787/v1/render \
  -d '{"source": "size(5cm); draw(unitcircle);"}' \
  -H 'Content-Type: application/json' \
  -H 'X-Asy-Encoding: base64'

# Upload to storage, return URL
curl -s http://127.0.0.1:8787/v1/render \
  -d '{"source": "size(5cm); draw(unitcircle);"}' \
  -H 'Content-Type: application/json' \
  -H 'X-Asy-Mode: url'

API

POST /v1/render

Accepts an Asymptote source and returns the compiled output.

Request body (one of):

Body type Content-Type Description
Raw source text text/plain (or any non-JSON) The .asy source code directly. Auto-detection: if the body is a single line starting with http:// or https://, it's treated as a URL.
JSON object application/json {"source": "..."} or {"url": "https://..."}

Control headers:

Header Values Default Description
X-Asy-Format pdf svg eps png jpg jpeg pdf (or inferred from Accept) Output format
X-Asy-Mode inline url inline inline = return binary/base64; url = upload to storage, return URL
X-Asy-Encoding binary base64 binary Only for inline mode. binary returns raw bytes with proper Content-Type; base64 returns JSON with base64-encoded data
X-Asy-Input auto source url auto How to interpret a non-JSON body
X-Asy-Dpi 14096 150 DPI for raster formats (png/jpg)
X-Asy-Timeout seconds 60 Compile timeout (capped by ASYAGENT_MAX_TIMEOUT)
X-Asy-Filename string Suggested filename for Content-Disposition / storage key
X-Asy-Disposition inline attachment inline Content-Disposition value
X-Asy-Storage-Prefix string env S3_PREFIX Override storage key prefix for this request
X-Asy-Storage-Bucket string env S3_BUCKET Override S3 bucket for this request
Accept MIME type Alternative to X-Asy-Format (e.g. Accept: image/png)

Response (inline/binary): Raw bytes with Content-Type matching the format (e.g. application/pdf, image/png).

Response (inline/base64):

{
  "ok": true,
  "mode": "inline",
  "encoding": "base64",
  "format": "pdf",
  "mime": "application/pdf",
  "size": 5989,
  "data": "JVBERi0xLjU..."
}

Response (url mode):

{
  "ok": true,
  "mode": "url",
  "format": "pdf",
  "mime": "application/pdf",
  "size": 5989,
  "url": "http://host/files/...",
  "key": "files/abc123.pdf",
  "urls": [{"url": "...", "key": "...", "mime": "...", "size": 5989}]
}

GET /

Service info (version, formats, storage health, defaults).

GET /healthz

Health check — returns 200 if storage is writable, 503 otherwise.

Skill distribution

asyagent vendors the Asymptote skill (an OpenCode agent skill for writing .asy source) and serves it through three self-describing endpoints. An LLM agent that can reach the server can discover the skill, install it, and learn how to call the render API — all from GET /v1/skill.

GET /v1/skill

Returns a JSON manifest with four sections:

  • skill — name, version, description, license, compatibility (parsed from the skill's SKILL.md frontmatter).
  • installmethod: "archive", the archive_url, exact steps (curl + tar), and a note that skillutils.asy is provided server-side.
  • files — every file in the bundle with its path, fetch url, size, and mime (for lazy per-file download).
  • render_api — the POST /v1/render contract: body forms, X-Asy-* headers, response modes, and a ready-to-copy curl example pointing at this server.

GET /v1/skill/files/{path}

Serves a single skill file by its relative path (e.g. SKILL.md, docs/01-basics.md, lib/skillutils.asy, scripts/asy_render.py). Path traversal outside the bundle root returns 404.

GET /v1/skill/archive[.tar.gz|.zip]

Downloads the entire skill as a tar.gz (default) or zip archive. Use this to install the skill in one step:

curl -sL http://127.0.0.1:8787/v1/skill/archive.tar.gz | tar xz -C ~/.config/opencode/skills

skillutils.asy is bundled inside the archive and is also placed on the server's Asymptote module path (via the Docker image), so import skillutils; works server-side with no client setup.

GET /files/{key}

Serves files from local storage (only available when ASYAGENT_STORAGE=local).

Configuration

All configuration via environment variables:

Server

Variable Default Description
ASYAGENT_HOST 0.0.0.0 Listen address
ASYAGENT_PORT 8787 Listen port
ASYAGENT_MAX_WORKERS 16 Max concurrent compiles (semaphore)
ASYAGENT_COMPILE_TIMEOUT 60 Default compile timeout (s)
ASYAGENT_MAX_TIMEOUT 300 Max allowed client-requested timeout
ASYAGENT_FETCH_TIMEOUT 20 URL fetch timeout (s)
ASYAGENT_MAX_SOURCE_BYTES 1048576 Max request body size
ASYAGENT_MAX_FETCH_BYTES 5242880 Max remote file size for URL input
ASYAGENT_ASY_BIN asy Path to asy binary
ASYAGENT_GS_BIN gs Path to ghostscript binary
ASYAGENT_DEFAULT_FORMAT pdf Default output format
ASYAGENT_DEFAULT_MODE inline Default response mode
ASYAGENT_DEFAULT_ENCODING binary Default inline encoding
ASYAGENT_DEFAULT_DPI 150 Default raster DPI
ASYAGENT_TMP_DIR Override temp directory for compile working dirs
ASYAGENT_SKILL_DIR package _skill/ Override the skill bundle directory served by /v1/skill/* endpoints. Defaults to the _skill/ directory shipped inside the asyagent package. Set to an external skill directory to test changes before vendoring.

Storage

Variable Default Description
ASYAGENT_STORAGE local Backend: local, s3, or none
ASYAGENT_LOCAL_DIR ./storage Local storage directory
ASYAGENT_LOCAL_BASE_URL auto Base URL for local file serving

S3 / Object Storage

Variable Default Description
S3_BUCKET Bucket name (required for S3)
S3_PREFIX asyagent/ Key prefix
S3_ENDPOINT auto Custom endpoint (e.g. http://minio:9000)
S3_URL_STYLE path path or virtual (virtual-hosted)
S3_PUBLIC_BASE_URL Override the public URL base (e.g. CDN domain)
S3_PRESIGN false Return presigned URLs instead of public URLs
S3_PRESIGN_EXPIRES 3600 Presigned URL expiry (s)
S3_USE_TLS true Use HTTPS for S3 API calls
AWS_ACCESS_KEY_ID Access key
AWS_SECRET_ACCESS_KEY Secret key
AWS_SESSION_TOKEN STS session token (optional)
AWS_REGION us-east-1 Region

Architecture

asyagent/
  __init__.py          # package metadata
  __main__.py          # python -m asyagent entry point
  config.py            # Settings dataclass, env-driven
  errors.py            # typed exception hierarchy
  sigv4.py             # AWS Signature V4 (sign + presign), zero-dep
  fetcher.py           # URL -> source text fetcher
  compiler.py          # asy invocation + gs rasterization + ZIP bundling
  storage.py           # Local / S3 / None storage backends
  server.py            # ThreadingHTTPServer, header-driven rendering
tests/
  test_sigv4.py        # KAT against AWS test vector (AKIDEXAMPLE)
  test_compiler.py     # all formats, multi-shipout, errors
  test_server.py       # end-to-end integration tests
examples/
  unit_circle.asy      # example source
  function_plot.asy    # example source
  multi_page.asy       # multi-shipout example
  client.py            # example client script

Request flow

Client ──POST /v1/render──▶ server.py
  │                           │
  │  Headers: X-Asy-Format,   │  RenderContext (parses all X-Asy-* headers)
  │  X-Asy-Mode, etc.         │
  │                           ├── text/plain body ──▶ _resolve_source (auto-detect url/source)
  │                           ├── application/json ──▶ _resolve_source (json.source or json.url)
  │                           │                          └── fetcher.py (if url)
  │                           │
  │                           ├── compile_source (semaphore-gated)
  │                           │   ├── asy -f pdf -o out input.asy    (native: pdf/svg/eps)
  │                           │   ├── gs -sDEVICE=pngalpha ...        (raster: png/jpg)
  │                           │   └── select_or_bundle (ZIP if multiple outputs)
  │                           │
  │                           ├── mode=inline,encoding=binary ──▶ raw bytes + Content-Type
  │                           ├── mode=inline,encoding=base64 ──▶ JSON {data: base64...}
  │                           └── mode=url ──▶ storage.upload ──▶ JSON {url: ...}
  │
◀── response (binary / JSON) ─┘

Response mode comparison

Mode Encoding Response body Content-Type Use case
inline binary Raw compiled bytes matches format Direct download, browser display
inline base64 JSON {data: "..."} application/json API compositing, embedding in JSON workflows
url JSON {url: "..."} application/json Large files, CDN delivery, async workflows

Running tests

python3 -m unittest discover -s tests -v

Docker

docker build -t asyagent .
docker run -p 8787:8787 asyagent

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

asyagent-0.4.0.tar.gz (95.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

asyagent-0.4.0-py3-none-any.whl (101.3 kB view details)

Uploaded Python 3

File details

Details for the file asyagent-0.4.0.tar.gz.

File metadata

  • Download URL: asyagent-0.4.0.tar.gz
  • Upload date:
  • Size: 95.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for asyagent-0.4.0.tar.gz
Algorithm Hash digest
SHA256 aa80bcac343d5529c05ececcb9b5b5ebdaa8b5dfefde44a1437eda51728054c6
MD5 735139e37dbd9f73080599d51ec80b4a
BLAKE2b-256 079ad826d8295c2902292b40ae039d9f9eca9a1741cd1a245f048a1d9943b244

See more details on using hashes here.

File details

Details for the file asyagent-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: asyagent-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 101.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for asyagent-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6721069e8aaa04edca42aaa808ff75d7601050c22af0648f9592fd15f1d4009b
MD5 f0f31f3873fddf1f2b5dcc65be0eaf77
BLAKE2b-256 e9408c36f3ec83213e3179e41f4cb863edf8a944fd0f13cba8a7b9336ad958f5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page