| name | ai-server |
| description | Conventions for the opentrons-ai-server FastAPI service — project structure, uv dependency management, settings, testing, Docker, and deployment. Use when working with files in opentrons-ai-server/ or discussing the AI server API. |
AI Server Instructions
Overview
opentrons-ai-server is a standalone FastAPI service for Opentrons AI — protocol generation, chat completions, and related AI features. It is not part of the monorepo build system; it has its own dependency management, CI workflows, and deployment pipeline.
Deployed environments: staging (staging.opentrons.ai) and prod (ai.opentrons.com), running on AWS ECS Fargate behind CloudFront.
API Endpoints
The server exposes four chat endpoints that return JSON responses:
| Endpoint | Purpose |
|---|
POST /api/chat/completion | General chat (no file attachments) |
POST /api/chat/completion-multipart | Chat with file attachments (multipart form) |
POST /api/chat/create-protocol | Generate a new protocol |
POST /api/chat/update-protocol | Update an existing protocol |
All endpoints require a Bearer token in Authorization. Setting "fake": true in the request body bypasses the LLM and returns a canned response from api/domain/fake_responses.py — useful for local development without Anthropic API calls.
Package Manager — uv
This project uses uv for Python dependency management (not pipenv, pip-tools, or poetry).
| File | Role | Committed? |
|---|
pyproject.toml | Single source of truth for dependencies AND all tool config | Yes |
uv.lock | Locked dependency graph | Yes |
requirements.txt | Generated pip-format file for Docker builds | No (gitignored) |
.venv/ | Local virtual environment created by uv sync | No (gitignored) |
Key Commands
make setup
uv add <package>
uv add --dev <package>
uv remove <package>
uv lock
uv run <command>
After changing deps, commit both pyproject.toml and uv.lock.
Project Structure
opentrons-ai-server/
├── api/ # Application source code
│ ├── handler/ # FastAPI app, routes, middleware (fast.py entrypoint)
│ ├── domain/ # Business logic — LLM prediction (Anthropic, OpenAI)
│ ├── models/ # Pydantic request/response models
│ ├── services/ # File processing and other services
│ ├── integration/ # External integrations (Auth0, Google Sheets, AWS)
│ ├── constants/ # Shared constants
│ ├── data/ # Static data files
│ ├── storage/ # Stored API docs, indexes
│ ├── utils/ # Markdown conversion, index creation
│ └── settings.py # Pydantic Settings — all env vars and secrets
├── tests/
│ ├── conftest.py # Pytest fixtures and --env option
│ ├── helpers/ # Client, token helpers for live testing
│ └── test\_\*.py # Unit and live tests
├── deploy.py # ECS Fargate deployment script
├── Dockerfile
├── Makefile
├── pyproject.toml
└── uv.lock
Configuration & Settings
All runtime configuration lives in api/settings.py via pydantic-settings:
- Locally: values come from a
.env file (gitignored)
- Deployed: values come from AWS Secrets Manager, loaded into ECS by
deploy.py
- Every new env var or secret must be added as a field on the
Settings class
- Secrets use
SecretStr type; non-secret vars are plain strings with defaults
- Always import settings via the
get_settings() singleton (not Settings() directly) to avoid re-parsing .env on every import
Notable settings:
allowed_origins — comma-separated CORS origins (must be explicit; wildcard * is invalid with allow_credentials=True)
request_timeout_seconds — request timeout in seconds (default "300"); production proxies must be configured to allow at least this duration
anthropic_max_tokens — stored as a string, cast to int when used
Generate a template .env from defaults: make gen-env
Tool Configuration
All config is in pyproject.toml — no separate config files:
| Tool | Section | Purpose |
|---|
| ruff | [tool.ruff], [tool.ruff.lint], [tool.ruff.format] | Linting AND formatting |
| mypy | [tool.mypy], [[tool.mypy.overrides]] | Strict type checking with pydantic plugin |
| pytest | [tool.pytest.ini_options] | Test runner config, markers: unit, live |
Line length: 140. Target: Python 3.12. Mypy is in strict mode.
Makefile Targets
All targets run from opentrons-ai-server/.
Development
| Target | Description |
|---|
make setup | Install all deps (uv sync --frozen --extra dev) |
make teardown | Delete .venv/ |
make format | Auto-fix lint + format with ruff, then prettier for .md/.json |
make lint | Check lint (ruff) + type check (mypy) — no auto-fix |
make prep | format then lint then unit-test |
make unit-test | Run unit tests (pytest tests -m unit) |
Running Locally
| Target | Description |
|---|
make local-run | Run FastAPI with uvicorn (hot reload, no Docker) |
make build | Generate requirements.txt, build Docker image |
make run | Run the Docker container (requires .env file) |
make rebuild | clean + build + run |
make live-test | Run live tests against a running server (ENV=local default) |
make live-client | Interactive client for testing the API |
Deployment
| Target | Description |
|---|
make gen-requirements | Export uv.lock to requirements.txt (production deps only) |
make deploy ENV=staging | Build, push to ECR, update ECS service |
make dry-deploy ENV=staging | Retrieve AWS data but make no changes |
make build-only ENV=staging | Build Docker image only, no push/deploy |
Docker Build
The container does not use uv internally:
make build calls make gen-requirements → uv export --no-hashes --no-dev -o requirements.txt
- Dockerfile installs with plain
pip
- Copies
api/ source and Opentrons API docs
- Entrypoint:
uvicorn api.handler.fast:app (3 workers, port 8000)
Docker build context is the repo root (not opentrons-ai-server/) so the Dockerfile can copy api/docs/v2 from the sibling api/ package.
Testing
- Unit tests (
@pytest.mark.unit): run offline → make unit-test
- Live tests (
@pytest.mark.live): run against a real server → make live-test ENV=local
- The
--env pytest option selects the target environment (local/staging/prod)
- Test helpers in
tests/helpers/ handle Auth0 token caching and HTTP client setup
Authentication
Auth0 JWT verification via api/integration/auth.py. Config: auth0_domain, auth0_api_audience, auth0_issuer, auth0_algorithms in Settings.
Code Conventions
- Formatting and linting: ruff only (no black).
make format to auto-fix
- Type annotations: required everywhere — mypy strict mode
- Pydantic models for all request/response schemas (in
api/models/)
- Structured logging via
structlog
- Import sorting handled by ruff's
I rule (isort-compatible)
Adding a New Env Var or Secret
- Add the field to the
Settings class in api/settings.py (use SecretStr for secrets)
- Add the value to your local
.env file
- Before deploying: add the value in AWS Secrets Manager under the environment's secret name
- Re-deploy — the deploy script maps Settings fields to ECS container env vars automatically