Home Ai Tools Article 1
Part 1 Generative AI

Beuys: Every Agent is an Artist

Image: AI-generated


Beuys: Every Agent is an Artist

Small, specialized tools as the ideal extension for AI agents.


1. The Problem: The Media Break in the Agent Workflow

AI agents like Claude Code operate primarily in a text-based environment (terminal, editor). The need for visual output often leads to a significant media break in practice:

  1. Context switch: The developer must leave the editor and open the browser.
  2. Manual process: Logging in to portals like Midjourney or DALL-E.
  3. Integration: Downloading and placing the generated images into the project directory is done manually.

This break prevents the full automation of workflows where visual material (e.g., icons for documentation or UI elements) should be generated directly by the agent.

"A tool should be transparent. It should not be something you think about. It should be something you think with." — Bjarne Stroustrup: The C++ Programming Language

For an agent, a tool is "transparent" when it can be called via a standardized interface (CLI) and delivers deterministic results.

2. The Solution: Image Generation via CLI

beuys is a minimalist tool that integrates image generation into the terminal workflow. It reduces interaction to a single command:

beuys "A minimalist icon for a terminal tool" --size 1024x1024

Advantages for the agent and the developer: - Context preservation: The call is made directly from the working directory. - Automatability: The agent can call the tool independently, capture the path of the generated image, and process it further. - Minimalism: Focus on a single task according to the Unix principle.

→ GitHub: github.com/mhennemeyer/beuys

Installation

# Option A: Copy the binary
cp scripts/dist/beuys /usr/local/bin/

# Option B: Run directly as a script
./scripts/beuys "A flower in sunlight"

3. Implementation: Zero-Dependency Approach

Modern software stacks often suffer from "dependency bloat". A CLI tool for agents should be lightweight and runnable on any system without complex installation (pip install ...).

❌ Impure/Heavy: OpenAI Client Library

Using the official client brings transitive dependencies and requires version maintenance.

from openai import OpenAI
client = OpenAI()

response = client.images.generate(
  model="dall-e-3",
  prompt="...",
  n=1,
)
image_url = response.data[0].url

✅ Pure/Light: Direct API Call via urllib

beuys uses only the Python standard library. This makes the tool immune to changes in third-party libraries and ensures immediate readiness for use.

import json
import urllib.request

def call_openai(prompt, api_key):
    payload = json.dumps({
        "model": "dall-e-3",
        "prompt": prompt,
        "size": "1024x1024",
        "n": 1,
    }).encode("utf-8")

    req = urllib.request.Request(
        "https://api.openai.com/v1/images/generations",
        data=payload,
        headers={
            "Content-Type": "application/json",
            "Authorization": f"Bearer {api_key}",
        },
        method="POST",
    )

    with urllib.request.urlopen(req) as resp:
        body = json.loads(resp.read().decode("utf-8"))
        return body["data"][0]["url"]

4. Architecture: Specialization vs. Integration

The decision against direct integration of image generation as a plugin follows architectural considerations. Providing it as an independent, small tool offers several advantages:

Feature Specialized Tool (Beuys) Integrated Function
Testability Testable in isolation in the terminal Requires complete agent setup
Maintainability ~180 lines of code, fully comprehensible Part of a complex codebase
Portability Usable as a binary everywhere Bound to the agent
Unix Principle "Do one thing and do it well" "Swiss Army Knife" (Complexity)

This specialization allows image generation to be versioned independently of the rest of the system. If OpenAI's API changes, only this one tiny tool needs to be adapted.

Try It Yourself

The core building block of the beuys CLI is a pure function that turns input parameters into an API-conforming JSON payload – no network, no side effects. That is exactly what makes the tool testable and API updates trivial: what gets sent to OpenAI can be verified in isolation.

import json

def build_payload(prompt: str, size: str = "1024x1024", n: int = 1) -> dict:
    """Pure payload builder for the OpenAI Image API."""
    if not prompt.strip():
        raise ValueError("prompt must not be empty")
    if size not in {"256x256", "512x512", "1024x1024", "1792x1024", "1024x1792"}:
        raise ValueError(f"Invalid size: {size}")
    return {
        "model": "dall-e-3",
        "prompt": prompt.strip(),
        "size": size,
        "n": n,
    }

def encode_payload(payload: dict) -> bytes:
    """Serialization is separated from building – single responsibility."""
    return json.dumps(payload).encode("utf-8")

examples = [
    ("A minimalist icon for a terminal tool", "1024x1024"),
    ("A flower in sunlight",                  "512x512"),
]

for prompt, size in examples:
    payload = build_payload(prompt, size=size)
    body = encode_payload(payload)
    print(f"prompt={prompt!r:45}  size={size:9}  bytes={len(body)}")
    print(f"  payload = {payload}")

Extension idea: Add a pure validate_prompt helper (e.g., minimum length, forbidden terms) and combine it with build_payload into a small pipeline – the function stays pure and testable without any network.

That is exactly what we do next: a pure validate_prompt helper plus a small prepare pipeline that chains validation and payload building. The result is a tuple ("ok", payload) or ("err", reason) – no exceptions, no side effects, easy to use in tests.

FORBIDDEN = {"nsfw", "violence", "blood"}

def validate_prompt(prompt: str, min_len: int = 8):
    """Pure validator: (ok, prompt) or (err, reason)."""
    p = prompt.strip()
    if len(p) < min_len:
        return ("err", f"prompt too short (<{min_len} chars)")
    lowered = p.lower()
    hit = next((w for w in FORBIDDEN if w in lowered), None)
    if hit:
        return ("err", f"forbidden term: {hit!r}")
    return ("ok", p)

def build_payload(prompt: str, size: str = "1024x1024", n: int = 1) -> dict:
    return {"model": "dall-e-3", "prompt": prompt, "size": size, "n": n}

def prepare(prompt: str, size: str = "1024x1024"):
    """Pipeline: validate → build_payload. Stays pure."""
    status, value = validate_prompt(prompt)
    if status == "err":
        return ("err", value)
    return ("ok", build_payload(value, size=size))

cases = [
    "A minimalist icon for a terminal tool",
    "short",                             # too short
    "An image with NSFW content please", # forbidden term
    "A flower in sunlight",
]
for c in cases:
    print(f"{c!r:40} → {prepare(c)}")

5. Conclusion

beuys demonstrates that functional extensions for AI agents do not require complex frameworks. A deep understanding of standard protocols (HTTP/JSON) and the avoidance of unnecessary dependencies lead to more robust and maintainable systems.


Sources & Resources

📄 As PDF