--- 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` | Unused. Docker installs from `uv.lock` directly | No (gitignored) | | `.venv/` | Local virtual environment created by `uv sync` | No (gitignored) | ### Key Commands ```bash make setup # Install all deps (uv sync --frozen) uv add # Add production dep uv add --dev # Add dev-only dep uv remove # Remove dep uv lock # Re-resolve after manual pyproject.toml edits uv run # Run inside the managed venv ``` After changing deps, **commit both** `pyproject.toml` and `uv.lock`. ## Project Structure ```markdown 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/ # API docs sync, curation, metadata helpers │ └── 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` | Sync API docs, then build the 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 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 image is a two-stage build. The dependency stage copies a digest-pinned uv 0.11.17 binary and runs `uv sync --frozen --no-dev --no-install-project`, so packages come from `uv.lock` with their recorded hashes. The runtime stage copies only that virtualenv and `api/`; it does not contain `uv`. `make build` syncs the Python API docs, then builds. The Docker build context is the **repo root** (not `opentrons-ai-server/`). Docs come from the pinned `DOCS_TAG` Makefile variable and must be present under `api/storage/api_docs/docs/v2`. Entrypoint: `uvicorn api.handler.fast:app` (3 workers, port 8000). ## Python API docs curation The helper model that selects relevant docs reads `api/storage/api_docs/api_docs_struct.md`. Rich `` routing text is **not** taken from synced markdown alone; it comes from committed curation. | File | Edit? | Purpose | | ---------------------------- | ------ | ------------------------------------------ | | `api_docs_struct_about.md` | Yes | Source of truth for curated `` text | | `api_docs_struct.md` | **No** | Generated on `make sync-api-docs` | | `docs/v2/` (synced markdown) | **No** | Gitignored; fetched from `DOCS_TAG` | ```bash make sync-api-docs # regenerate api_docs_struct.md from curated about file make check-api-docs-curation # fail if curated entries and synced docs diverge ``` Full details: `opentrons-ai-server/docs/API_DOCS_CURATION.md` ## 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 1. Add the field to the `Settings` class in `api/settings.py` (use `SecretStr` for secrets) 2. Add the value to your local `.env` file 3. Before deploying: add the value in **AWS Secrets Manager** under the environment's secret name 4. Re-deploy — the deploy script maps Settings fields to ECS container env vars automatically