Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
df37077
fix: stop throttling probes, silently dropping batches, and crashing …
claude Aug 25, 2026
7b30880
fix: make the shipped containers actually deployable
claude Aug 25, 2026
8bf4db0
fix: let orchestrators probe readiness, and name unconfigured connect…
claude Aug 25, 2026
64d99c4
fix(pipelines): say which step broke when a pipeline fails to load
claude Aug 25, 2026
c2b7736
docs: explain how to verify a running deployment
claude Aug 25, 2026
42c6673
perf(storage): stop re-reading every row the database just accepted
claude Aug 25, 2026
39c706a
test(e2e): stop the harness's own traffic from failing its rate-limit…
claude Aug 25, 2026
b65d187
fix(studio): make the shipped UI self-contained and stop it reporting…
claude Aug 25, 2026
53f9a06
test(optimize): let the live optimization suites run without a cloud key
claude Aug 25, 2026
694abfc
fix(pipelines): make sub-pipelines usable, and stop broken conditions…
claude Aug 25, 2026
e000346
fix(deploy): install the project's dependencies, with uv, and never f…
claude Aug 25, 2026
4206c8b
fix(optimize): honour --prompt in every optimization mode
claude Aug 25, 2026
0d14330
fix(platform): warn when a MEMORY connection overrides DATABASE_URL
claude Aug 25, 2026
d8691b0
fix(templates): make the chatbot template actually use conversation h…
claude Aug 25, 2026
d7dfa4a
test(e2e): verify every pipeline, request isolation, and durability
claude Aug 25, 2026
214b013
fix(optimize): let fit() over an AgentDataset actually improve
claude Aug 25, 2026
69ed89a
fix(optimize): never report a score when nothing was evaluated
claude Aug 25, 2026
c877213
fix(langchain): bind the optional BaseMessage import through its own …
claude Aug 25, 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
5 changes: 5 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ README*
CHANGELOG*
TODO*

# …except README.md: pyproject.toml declares `readme = "README.md"`, so the
# build backend needs it present to install the project. Excluding it made
# `uv sync` in the Dockerfiles die with "Readme file does not exist".
!README.md

# Development and build tools
Makefile
docker-compose*.yml
Expand Down
28 changes: 22 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,39 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*

# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
# Install uv from PyPI at a pinned version.
#
# This used to be `COPY --from=ghcr.io/astral-sh/uv:latest`, which pulled
# an unpinned tag: image contents changed under you between builds, and a
# breaking uv release could break the build with no diff to show for it.
# PyPI is already required by every other layer here, so sourcing uv from
# it also drops a second registry from the build's dependency set.
ARG UV_VERSION=0.8.17
RUN pip install --no-cache-dir "uv==${UV_VERSION}"

# Set working directory
WORKDIR /app

# Copy dependency files first for cache efficiency
COPY pyproject.toml uv.lock ./
# Copy dependency files first for cache efficiency.
# README.md is required: pyproject.toml declares it as the project readme,
# so the build backend fails without it when uv installs the project below.
COPY pyproject.toml uv.lock README.md ./

# Install dependencies (without the project itself)
# Extras matter here: a bare `uv sync` installs only the core dependencies, so
# the image shipped without sqlalchemy, langgraph, prometheus-client or pyjwt —
# /metrics served nothing, DATABASE_URL failed with "No module named
# 'sqlalchemy'", and JWT auth could not be enabled at all. `all` restores those
# and matches what `agentomatic deploy` builds. `db-postgres` is named
# separately because `all` deliberately carries only the SQLite driver, and
# this image is a deployment: the compose stack beside it offers Postgres.
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-install-project --no-dev
uv sync --frozen --no-install-project --no-dev --extra all --extra db-postgres

# Copy source code and install the project
COPY src/ ./src/
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev
uv sync --frozen --no-dev --extra all --extra db-postgres

# Production stage
FROM python:3.12-slim
Expand Down
69 changes: 36 additions & 33 deletions Dockerfile.distroless
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
# built-in ``nonroot`` account (numeric UID 65532) so images honour
# Kubernetes ``runAsNonRoot`` admission policies out of the box.

# Build stage
FROM python:3.12-slim AS builder
# ---- Build stage ------------------------------------------------------------
# Python 3.11 on purpose: ``distroless/python3-debian12`` ships Debian 12's
# Python 3.11, and dependencies must be built for the interpreter that will
# actually import them. Building on 3.12 produced an image that could not start
# at all — the venv's ``bin/python`` symlinked to the builder's 3.12 binary,
# which does not exist in the runtime stage, so the ENTRYPOINT was a dangling
# symlink; and even resolved, cp312 wheels cannot be imported under 3.11.
FROM python:3.11-slim AS builder

# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy

# Install system dependencies
Expand All @@ -18,58 +22,57 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*

# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
# Install uv from PyPI at a pinned version. An unpinned `uv:latest` image made
# builds irreproducible, and PyPI is already required by every other layer.
ARG UV_VERSION=0.8.17
RUN pip install --no-cache-dir "uv==${UV_VERSION}"

# Set working directory
WORKDIR /app

# Copy dependency files
COPY pyproject.toml uv.lock ./

# Install dependencies
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-install-project --no-dev

# Copy source code and install
# README.md is required: pyproject.toml declares it as the project readme, so
# installing the project below fails without it.
COPY pyproject.toml uv.lock README.md ./
COPY src/ ./src/
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev

# Ensure the runtime user (uid 65532 / nonroot) can read the venv and sources
RUN chown -R 65532:65532 /app
# ``--target`` instead of a virtualenv: the runtime stage runs the distroless
# image's own interpreter, which cannot use a venv built around a different
# Python binary. A plain directory on ``PYTHONPATH`` works with any 3.11.
#
# Extras matter here: a bare install ships core dependencies only, leaving the
# image without sqlalchemy, langgraph, prometheus-client or pyjwt — /metrics
# served nothing, DATABASE_URL failed with "No module named 'sqlalchemy'", and
# JWT auth could not be enabled. ``db-postgres`` is named separately because
# ``all`` deliberately carries only the SQLite driver.
RUN --mount=type=cache,target=/root/.cache/uv \
uv pip install --target=/app/deps ".[all,db-postgres]"

# Production stage — distroless, non-root by default
# ---- Runtime stage ----------------------------------------------------------
FROM gcr.io/distroless/python3-debian12:nonroot

# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/app/.venv/bin:$PATH" \
VIRTUAL_ENV="/app/.venv"
PYTHONPATH="/app/deps"

WORKDIR /app

# Copy virtual environment and application from builder
COPY --from=builder --chown=65532:65532 /app/.venv /app/.venv
COPY --from=builder --chown=65532:65532 /app/deps /app/deps
COPY --from=builder --chown=65532:65532 /app/src /app/src
COPY --from=builder --chown=65532:65532 /app/pyproject.toml /app/

# Explicit numeric UID so Kubernetes ``runAsNonRoot`` admission does not
# have to introspect the base image at admit-time.
USER 65532:65532

# Expose port
EXPOSE 8000

# Distroless has no shell, but it can execute binaries directly. We
# invoke the ``agentomatic`` console script (installed by uv into
# ``/app/.venv/bin``) via the venv Python so both the plain and
# distroless images boot through the same ``agentomatic run`` entrypoint.
ENTRYPOINT ["/app/.venv/bin/python", "/app/.venv/bin/agentomatic"]
# Distroless has no shell, so run the CLI as a module through the base image's
# own interpreter; dependencies come from PYTHONPATH. Invoking the console
# script directly would go through its shebang, which points at the builder's
# interpreter and is not present here.
ENTRYPOINT ["/usr/bin/python3", "-m", "agentomatic.cli.commands"]
CMD ["run", "--agents-dir", "agents", "--host", "0.0.0.0", "--port", "8000"]

# No shell and no curl in this image — hit /health with the venv Python
# No shell and no curl in this image — hit /health with the base interpreter
# instead (exec form, so no shell is needed to run this CMD either).
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD ["/app/.venv/bin/python", "-c", "import urllib.request as u; u.urlopen('http://localhost:8000/health', timeout=5)"]
CMD ["/usr/bin/python3", "-c", "import urllib.request as u; u.urlopen('http://localhost:8000/health', timeout=5)"]
111 changes: 70 additions & 41 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,52 +1,81 @@
# =============================================================================
# Agentomatic — local production-shaped stack
#
# docker compose up --build # platform on http://localhost:8000
# docker compose --profile db up -d # …with Postgres-backed persistence
#
# One platform process serves every agent it discovers under ./agents — that
# is the point of the product, so there is no per-agent service here. Features
# are driven entirely by AGENTOMATIC_* env vars, exactly as in the image that
# `agentomatic deploy` generates, so what you exercise locally is what ships.
# =============================================================================

services:
# Alpha Agent
alpha-agent:
build: .
container_name: alpha-agent
platform:
build:
context: .
dockerfile: Dockerfile
image: agentomatic:latest
container_name: agentomatic-platform
restart: unless-stopped
ports:
- "8001:8000"
- "${AGENTOMATIC_PORT:-8000}:8000"
environment:
- AGENT_NAME=alpha
- PORT=8000
- OPENAI_API_KEY=${OPENAI_API_KEY}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
command: ["agentomatic", "run", "--agents-dir", "agents", "--host", "0.0.0.0", "--port", "8000"]
AGENTOMATIC_TITLE: "${AGENTOMATIC_TITLE:-Agentomatic Platform}"
AGENTOMATIC_LOG_LEVEL: "${AGENTOMATIC_LOG_LEVEL:-INFO}"
# Studio, docs, health and metrics are on by default.
AGENTOMATIC_ENABLE_STUDIO: "${AGENTOMATIC_ENABLE_STUDIO:-1}"
AGENTOMATIC_ENABLE_METRICS: "${AGENTOMATIC_ENABLE_METRICS:-1}"
AGENTOMATIC_ENABLE_CONTROL_PLANE: "${AGENTOMATIC_ENABLE_CONTROL_PLANE:-1}"
# Opt-in hardening — set these in .env before exposing the port.
AGENTOMATIC_ENABLE_AUTH: "${AGENTOMATIC_ENABLE_AUTH:-0}"
AGENTOMATIC_API_KEY: "${AGENTOMATIC_API_KEY:-}"
AGENTOMATIC_CONTROL_TOKEN: "${AGENTOMATIC_CONTROL_TOKEN:-}"
AGENTOMATIC_ENABLE_RATE_LIMIT: "${AGENTOMATIC_ENABLE_RATE_LIMIT:-0}"
# Durable invocation history. Empty DATABASE_URL keeps it in-memory.
AGENTOMATIC_LOGS_HISTORY: "${AGENTOMATIC_LOGS_HISTORY:-0}"
DATABASE_URL: "${DATABASE_URL:-}"
# Bring your own model provider; nothing vendor-specific is baked in.
OPENAI_API_KEY: "${OPENAI_API_KEY:-}"
ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}"
volumes:
- ./agents:/app/agents
networks:
- agentomatic-network
# Drop agents in and restart — no rebuild needed for iteration.
- ./agents:/app/agents:ro
- agentomatic-data:/app/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
networks: [agentomatic]

# Beta Agent
beta-agent:
build: .
container_name: beta-agent
ports:
- "8002:8000"
# Enable with: docker compose --profile db up -d
# then point the platform at it by setting, in .env:
# DATABASE_URL=postgresql+asyncpg://agentomatic:agentomatic@db:5432/agentomatic
# AGENTOMATIC_LOGS_HISTORY=1
db:
profiles: ["db"]
image: postgres:16-alpine
container_name: agentomatic-db
restart: unless-stopped
environment:
- AGENT_NAME=beta
- PORT=8000
- OPENAI_API_KEY=${OPENAI_API_KEY}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
command: ["agentomatic", "run", "--agents-dir", "agents", "--host", "0.0.0.0", "--port", "8000"]
POSTGRES_USER: "${POSTGRES_USER:-agentomatic}"
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:-agentomatic}"
POSTGRES_DB: "${POSTGRES_DB:-agentomatic}"
volumes:
- ./agents:/app/agents
networks:
- agentomatic-network
- agentomatic-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-agentomatic}"]
interval: 5s
timeout: 5s
retries: 20
networks: [agentomatic]

# Nginx reverse proxy
nginx:
image: nginx:alpine
container_name: agentomatic-proxy
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- alpha-agent
- beta-agent
networks:
- agentomatic-network
volumes:
agentomatic-data:
agentomatic-db:

networks:
agentomatic-network:
agentomatic:
driver: bridge
2 changes: 1 addition & 1 deletion docs/FRONTEND_API_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ the frontend submits work, then **polls** or **streams** progress.
**Submit** (returns immediately with `202` and a task record):

- Agent: `POST /api/v1/{agent}/invoke/async` (single) · `/invoke/batch` (many)
- Plugin: `POST /api/v1/{plugin}/predict/async` · `/predict/batch`
- Plugin: `POST /api/v1/plugins/{plugin}/predict/async` · `/predict/batch`
- Pipeline: `POST /api/v1/pipelines/{name}/run/async` · `/run/batch`
- Endpoint: `POST /api/v1/endpoints/{name}{path}/async` · `.../batch`
- Ingestor: `POST /api/v1/ingestion/{name}/run/async` · `/run/batch`
Expand Down
44 changes: 44 additions & 0 deletions docs/guide/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,50 @@ Everything above is configured declaratively — agents in folders, connections
in `connections.py`, endpoints in `endpoint.py`, and a handful of platform
flags. No custom FastAPI wiring is required.

## Project dependencies in the generated image

`agentomatic deploy` builds with **uv**, pinned via `ARG UV_VERSION` so the
image is reproducible, and installs your project's own dependencies as well
as agentomatic itself.

Declare what *your* project needs — a vendor LLM driver, a vector client, an
in-house package — in `requirements.txt` next to `main.py`:

```txt title="requirements.txt"
agentomatic[all]==1.10.0
# This project's agents talk to a local OpenAI-compatible model server,
# so it needs that client library.
langchain-openai>=0.3
```

The generated Dockerfile installs agentomatic first (at its pinned version)
and your requirements second, so a looser pin in your file cannot downgrade
the framework.

!!! warning "This is how a provider driver reaches the image"

The `all` extra deliberately excludes the vendor LLM drivers — `openai`,
`azure`, `vertex` — because the platform is provider-agnostic: you install
the SDK for the backend you actually use. `requirements.txt` is where that
happens. A stack configured for `openai_compatible` (which is what oMLX,
llama.cpp, vLLM and LM Studio all speak) needs `langchain-openai` there,
or the platform will refuse to start rather than answer with a fake model.

### Reproducible builds with a lockfile

If your project keeps a `pyproject.toml` and a `uv.lock`, the generated
Dockerfile installs from the lock instead:

```dockerfile
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --inexact
```

That installs the exact resolved versions the lock pins rather than
re-resolving at build time. Regenerate the lock with `uv lock` whenever you
change a dependency.


## 1. Install for production

Install only the extras you use. Common production combinations:
Expand Down
Loading
Loading