Skip to main content

guml

Compile GUML — an intermediate representation for LLM-generated user interfaces — to HTML, React, Svelte and more. The real Rust compiler, built as a native extension. No Node, no Rust toolchain, no build step.

pip install guml
import guml

src = '''page "Dashboard"
card
  h Revenue
  metric $48,120
  p Up 12% on last month
'''

html = guml.render(src)      # a complete page: no JavaScript, nothing to serve alongside it
guml.check(src)              # []

pip install guml also puts a guml command on your PATH.


Two things this is for

1. Getting a model to build UI

This is what GUML exists for. A model writing React emits JSX, hooks, effect dependencies, Tailwind class strings and ARIA attributes — most of it mechanically derivable. GUML moves that to the compiler, so the model writes what it actually decided and everything conventional is generated.

import guml, anthropic

client = anthropic.Anthropic()

prompt = guml.SPEC + "\n\n" + guml.registry(["card", "btn", "list", "metric"])

reply = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=2000,
    system=prompt,
    messages=[{"role": "user", "content": "A dashboard showing revenue and recent orders"}],
)

source = reply.content[0].text

# Free round. Strips a markdown fence, formats, applies every unambiguous fix.
# Costs nothing and resolves a surprising share of what a first generation gets wrong.
source = guml.repair(source).text

problems = guml.check(source)
if problems:
    # Every problem in one pass, never just the first — each repair round is a full generation.
    feedback = "\n".join(f"{d.code} line {d.line}: {d.message}" for d in problems)
    ...

html = guml.render(source)
  • guml.SPEC — the language spec, sized to sit in a system prompt. Rules, not vocabulary.
  • guml.registry([...]) — the vocabulary, as a prompt-sized slice. Ask for the dozen tags a task needs rather than all 49.
  • guml.repair() — everything a repair loop can do without another model call.
  • guml.check() — every diagnostic in one pass. Codes are append-only and never renumbered, so a loop can key on them.

2. Serving pages from Python

render() produces HTML with no JavaScript and no build step, so it drops into any framework.

Flask

@app.get("/dashboard")
def dashboard():
    return guml.render(SOURCE)

FastAPI

from fastapi.responses import HTMLResponse

@app.get("/dashboard", response_class=HTMLResponse)
def dashboard():
    return guml.render(SOURCE)

Django

from django.http import HttpResponse

def dashboard(request):
    return HttpResponse(guml.render(SOURCE))

Jinja — the one integration shipped rather than documented, because it is the one that is annoying to write correctly:

pip install 'guml[jinja]'
from guml.jinja import GumlExtension
app.jinja_env.add_extension(GumlExtension)
<div class="panel">{{ source | guml }}</div>

It defaults to fragment=True — no doctype, no <head>, no <main> — and marks the result safe so Jinja does not escape it into visible angle brackets.


Security

js and raw blocks compile through unchanged. That is GUML's documented escape hatch and its security boundary at the same time: everything outside them is constrained, everything inside them is the author's own code.

So render() defaults to level="core" — markup only, no state, no data, no actions, no js:

guml.render(source)                 # core: safe for a document you did not write
guml.render(source, level="app")    # full: executes any `js` the author included

This deliberately differs from the guml build CLI, which defaults to app. Different threat model: the CLI compiles a file you wrote, and render() very often does not.

Before rendering something at app level, capabilities() tells you what it will actually do:

caps = guml.capabilities(source)
if caps.uses_escape_hatch:
    raise ValueError("this document contains js")

response.headers["Content-Security-Policy"] = caps.csp

API

render(src, *, level, style, fragment) GUML → HTML
compile(src, backend, *, level) any of react, svelte, html, html-bare, html-fragment, html-cdn, wc, json, a2ui, mcp-ui
check(src, *, level) list[Diagnostic], every problem in one pass
raise_for_errors(src) the raising version, for try/except
repair(src) / fix(src) mechanical repair, no model call
format(src) / canonical(src) idempotent formatting / normalisation for comparison
capabilities(src, backend) what it does, plus a matching CSP
registry(tags) prompt-sized vocabulary slice
SPEC the language spec, for a system prompt
BACKENDS every backend name, from the compiler itself

Diagnostic is a frozen dataclass — d.code, d.line, d.column, d.message, d.help, d.suggestion, d.is_error — not a dict. Fully typed, py.typed included.

Threading

The API is synchronous, deliberately: compiling is CPU-bound and takes single-digit milliseconds, and an async def wrapper would be fake async that still blocks the loop. The GIL is released for the duration of every compile, so Flask on threads and FastAPI's threadpool genuinely parallelise. For a very large document in an async handler, await asyncio.to_thread(guml.render, src).


Command line

guml build app.guml --backend html
guml check app.guml
guml fmt app.guml --write
guml capabilities app.guml

A subset — enough that compiling a .guml file does not require a Rust toolchain. The full CLI (source maps, custom themes, registry validation, token estimates) is cargo install guml-cli.


Also available


MIT.

Download files

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

Source Distribution

guml-0.1.0.tar.gz (402.1 kB view details)

Uploaded Source

Built Distribution

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

guml-0.1.0-cp39-abi3-win_amd64.whl (728.9 kB view details)

Uploaded CPython 3.9+Windows x86-64

File details

Details for the file guml-0.1.0.tar.gz.

File metadata

  • Download URL: guml-0.1.0.tar.gz
  • Upload date:
  • Size: 402.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.9

File hashes

Hashes for guml-0.1.0.tar.gz
Algorithm Hash digest
SHA256 024d840a9c3fb208ca6887f503bf725887a5fd7c74f657bd2fe550014fcb5551
MD5 aae6f458d046ea4e250146c3398b47bf
BLAKE2b-256 35c37a4b7c1a180b13e37e61e9807257abd9bf45a2b22bec473e7eea50f8487c

See more details on using hashes here.

File details

Details for the file guml-0.1.0-cp39-abi3-win_amd64.whl.

File metadata

  • Download URL: guml-0.1.0-cp39-abi3-win_amd64.whl
  • Upload date:
  • Size: 728.9 kB
  • Tags: CPython 3.9+, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.9

File hashes

Hashes for guml-0.1.0-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 257e0b1f4ec6ec39a69921b9c4bfd890bbdbfeb3419f9b37d7575efff95565e8
MD5 4e802a8dba22849ed5501257a7180b1f
BLAKE2b-256 8470a1bee6c8fb026419fcce9b155e09c4a819d7454d9f1059f5e9b73f18e0dc

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