Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 3 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@
ANTHROPIC_API_KEY=your_api_key_here
ANTHROPIC_MODEL=claude-sonnet-4-6

# Signs resumable gateway proposals carried in assistant history. Generate a
# stable random value of at least 32 bytes; this is integrity protection, not
# encryption.
GATEWAY_PROPOSAL_SIGNING_KEY=replace_with_a_stable_random_signing_key

# Token protecting the server's deployed evaluation route. Use a long random
# value and configure the same value as EVAL_RUN_TOKEN when invoking evals.
UK_CHAT_EVAL_TOKEN=replace_with_a_long_random_token
Expand Down Expand Up @@ -39,6 +34,9 @@ BILLING_ENABLED=false
DB_NAME=microsim
DB_USERNAME=postgres
DB_PASSWORD=postgres
# Host migration commands require an explicit PostgreSQL URL with schema-change
# permission. Docker Compose derives its migration URL from the values above.
# ALEMBIC_DATABASE_URL=postgresql://postgres:postgres@localhost:5433/microsim

# Supabase (frontend auth)
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
Expand Down
6 changes: 6 additions & 0 deletions .github/scripts/cleanup-modal-preview.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,11 @@ set -euo pipefail
: "${MODAL_SECRET_NAME:?MODAL_SECRET_NAME is required}"

script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -n "${DATABASE_SCHEMA:-}" ]]; then
export POLICYENGINE_UK_CHAT_MODAL_APP_NAME="$MODAL_APP_NAME"
export POLICYENGINE_UK_CHAT_MODAL_SECRET_NAME="$MODAL_SECRET_NAME"
migration_run_name="${MODAL_APP_NAME}-schema-cleanup"
modal run --name "$migration_run_name" modal_app.py::remove_preview_database_schema
fi
"$script_dir/stop-modal-app.sh"
modal secret delete "$MODAL_SECRET_NAME" --yes --allow-missing
12 changes: 12 additions & 0 deletions .github/scripts/run-modal-migration.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env bash

set -euo pipefail

if [[ -n "${MODAL_APP_NAME:-}" ]]; then
: "${MODAL_SECRET_NAME:?MODAL_SECRET_NAME is required with MODAL_APP_NAME}"
export POLICYENGINE_UK_CHAT_MODAL_APP_NAME="$MODAL_APP_NAME"
export POLICYENGINE_UK_CHAT_MODAL_SECRET_NAME="$MODAL_SECRET_NAME"
fi

migration_run_name="${MODAL_APP_NAME:-policyengine-uk-chat}-migration"
modal run --name "$migration_run_name" modal_app.py::migrate
8 changes: 6 additions & 2 deletions .github/scripts/sync-modal-secret.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ set -euo pipefail
: "${HOSTNAMES:?HOSTNAMES is required}"
: "${PUBLIC_BASE_URL:?PUBLIC_BASE_URL is required}"
: "${UK_CHAT_EVAL_TOKEN:?UK_CHAT_EVAL_TOKEN is required}"
: "${GATEWAY_PROPOSAL_SIGNING_KEY:?GATEWAY_PROPOSAL_SIGNING_KEY is required}"
: "${ALEMBIC_DATABASE_URL:?ALEMBIC_DATABASE_URL is required}"

billing_enabled="${BILLING_ENABLED:-false}"

Expand All @@ -17,11 +17,11 @@ secret_args=(
"ANTHROPIC_COMPLEX_MODEL=claude-sonnet-4-6"
"ANTHROPIC_TITLE_MODEL=claude-haiku-4-5"
"ANTHROPIC_DEFAULT_MODEL=claude-haiku-4-5"
"GATEWAY_PROPOSAL_SIGNING_KEY=$GATEWAY_PROPOSAL_SIGNING_KEY"
"UK_CHAT_EVAL_TOKEN=$UK_CHAT_EVAL_TOKEN"
"POLICYENGINE_UK_DATA_TOKEN=$POLICYENGINE_UK_DATA_TOKEN"
"HUGGING_FACE_TOKEN=$HUGGING_FACE_TOKEN"
"DATABASE_URL=$DATABASE_URL"
"ALEMBIC_DATABASE_URL=$ALEMBIC_DATABASE_URL"
"BILLING_ENABLED=$billing_enabled"
"OBSERVABILITY_ENVIRONMENT=$OBSERVABILITY_ENVIRONMENT"
"OBSERVABILITY_GOOGLE_CLOUD_PROJECT=$OBSERVABILITY_GOOGLE_CLOUD_PROJECT"
Expand All @@ -32,6 +32,10 @@ secret_args=(
"PUBLIC_BASE_URL=$PUBLIC_BASE_URL"
)

if [[ -n "${DATABASE_SCHEMA:-}" ]]; then
secret_args+=("DATABASE_SCHEMA=$DATABASE_SCHEMA")
fi

if [[ "$billing_enabled" == "true" ]]; then
: "${SUPABASE_URL:?SUPABASE_URL is required when billing is enabled}"
: "${SUPABASE_SERVICE_ROLE_KEY:?SUPABASE_SERVICE_ROLE_KEY is required when billing is enabled}"
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,12 @@ jobs:
MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GATEWAY_PROPOSAL_SIGNING_KEY: ${{ secrets.GATEWAY_PROPOSAL_SIGNING_KEY }}
UK_CHAT_EVAL_TOKEN: ${{ secrets.UK_CHAT_EVAL_TOKEN }}
POLICYENGINE_UK_DATA_TOKEN: ${{ secrets.POLICYENGINE_UK_DATA_TOKEN }}
# TEMPORARY: data access for the policyengine.py engine override.
HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }}
DATABASE_URL: ${{ secrets.POLICYENGINE_UK_CHAT_DATABASE_URL }}
ALEMBIC_DATABASE_URL: ${{ secrets.POLICYENGINE_UK_CHAT_DATABASE_URL }}
BILLING_ENABLED: "false"
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
Expand All @@ -130,6 +130,12 @@ jobs:
PUBLIC_BASE_URL: https://policyengine-uk-chat.vercel.app/uk/chat
run: .github/scripts/sync-modal-secret.sh

- name: Upgrade database schema
env:
MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}
run: .github/scripts/run-modal-migration.sh

- name: Deploy to Modal
env:
MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
Expand Down
12 changes: 11 additions & 1 deletion .github/workflows/pr-beta-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ env:
# short and stable rather than using branch names that Modal may truncate.
MODAL_PREVIEW_APP_NAME: pe-uk-chat-${{ github.event.pull_request.number }}
MODAL_PREVIEW_SECRET_NAME: pe-uk-chat-${{ github.event.pull_request.number }}-secrets
DATABASE_SCHEMA: uk_chat_pr_${{ github.event.pull_request.number }}

jobs:
deploy:
Expand Down Expand Up @@ -64,11 +65,11 @@ jobs:
MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GATEWAY_PROPOSAL_SIGNING_KEY: ${{ secrets.GATEWAY_PROPOSAL_SIGNING_KEY }}
UK_CHAT_EVAL_TOKEN: ${{ secrets.UK_CHAT_EVAL_TOKEN }}
POLICYENGINE_UK_DATA_TOKEN: ${{ secrets.POLICYENGINE_UK_DATA_TOKEN }}
HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }}
DATABASE_URL: ${{ secrets.POLICYENGINE_UK_CHAT_DATABASE_URL }}
ALEMBIC_DATABASE_URL: ${{ secrets.POLICYENGINE_UK_CHAT_DATABASE_URL }}
BILLING_ENABLED: "false"
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
Expand All @@ -84,6 +85,14 @@ jobs:
PUBLIC_BASE_URL: ${{ steps.names.outputs.frontend_url }}/uk/chat
run: .github/scripts/sync-modal-secret.sh

- name: Upgrade database schema
env:
MODAL_APP_NAME: ${{ env.MODAL_PREVIEW_APP_NAME }}
MODAL_SECRET_NAME: ${{ env.MODAL_PREVIEW_SECRET_NAME }}
MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}
run: .github/scripts/run-modal-migration.sh

- name: Deploy backend preview to Modal
id: modal_deploy
env:
Expand Down Expand Up @@ -176,6 +185,7 @@ jobs:
env:
MODAL_APP_NAME: ${{ env.MODAL_PREVIEW_APP_NAME }}
MODAL_SECRET_NAME: ${{ env.MODAL_PREVIEW_SECRET_NAME }}
DATABASE_SCHEMA: ${{ env.DATABASE_SCHEMA }}
MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}
run: .github/scripts/cleanup-modal-preview.sh
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,22 @@ jobs:
permissions:
contents: read
id-token: write
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: capability_test
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres -d capability_test"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
CAPABILITY_TEST_POSTGRES_URL: postgresql://postgres:postgres@localhost:5432/capability_test

steps:
- name: Checkout
Expand Down
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ When adding, moving, or reviewing manual AI eval cases or harness code, read
When changing the chat model pathway, system prompts, tool definitions, or
calculation boundaries, read `docs/engineering/skills/uk-chat-runtime.md`.

When changing SQLModel schema, Alembic revisions, database adoption, or
migration behavior, read `docs/engineering/skills/database-migrations.md`.

Keep this file thin. Do not duplicate durable engineering guidance here; update
the canonical docs first, then adjust this adapter only when an entry point
needs to point at new guidance.
29 changes: 19 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: up down build logs restart shell-backend shell-frontend test test-backend test-frontend sync-policyengine-uk-evals check-policyengine-uk-evals eval-ai-offline eval-ai-live eval-ai-live-uk-population eval-ai-deployed-uk-population
.PHONY: up down build logs restart shell-backend shell-frontend migrate migration-check migration-current test test-backend test-frontend typecheck-backend sync-policyengine-uk-evals check-policyengine-uk-evals eval-ai-offline eval-ai-live eval-ai-deployed-uk-population

# Start all services in dev mode (live reload)
up:
Expand Down Expand Up @@ -39,6 +39,15 @@ shell-backend:
shell-frontend:
docker compose exec frontend sh

migrate:
python -m alembic -c backend/alembic.ini upgrade head

migration-check:
python -m alembic -c backend/alembic.ini check

migration-current:
python -m alembic -c backend/alembic.ini current

# One-time setup: copy .env.example to .env
init:
@if [ ! -f .env ]; then cp .env.example .env && echo "Created .env — fill in your ANTHROPIC_API_KEY"; else echo ".env already exists"; fi
Expand All @@ -47,26 +56,26 @@ init:
test: test-backend test-frontend

test-backend:
PYTHONPATH=backend python -m pytest backend/tests --cov --cov-config=.coveragerc --cov-report=term-missing --cov-report=xml --cov-fail-under=80
PYTHONPATH=backend:. python -m pytest backend/tests --cov --cov-config=.coveragerc --cov-report=term-missing --cov-report=xml --cov-fail-under=80

test-frontend:
cd frontend && npm run test:coverage
cd frontend && npm run build

typecheck-backend:
PYTHONPATH=backend:. python -m mypy --config-file backend/mypy.ini

sync-policyengine-uk-evals:
PYTHONPATH=backend python -m eval.sync_policyengine_uk --sync
PYTHONPATH=backend:. python -m eval.sync_policyengine_uk --sync

check-policyengine-uk-evals:
PYTHONPATH=backend python -m eval.sync_policyengine_uk --check
PYTHONPATH=backend:. python -m eval.sync_policyengine_uk --check

eval-ai-offline: check-policyengine-uk-evals
PYTHONPATH=backend python -m eval.run --mode offline
PYTHONPATH=backend:. python -m eval.run --mode offline

eval-ai-live: check-policyengine-uk-evals
PYTHONPATH=backend python -m eval.run --mode live --provider anthropic

eval-ai-live-uk-population: check-policyengine-uk-evals
RUN_DATA_EVALS=1 PYTHONPATH=backend python -m eval.run --suite tool_loop --mode live --provider anthropic
PYTHONPATH=backend:. python -m eval.run --mode live --provider anthropic

eval-ai-deployed-uk-population:
PYTHONPATH=backend python -m eval.run_deployed --case-file evals/cases/tool_loop/uk_population_live.yaml
PYTHONPATH=backend:. python -m eval.run_deployed --case-file evals/cases/tool_loop/uk_population_live.yaml
146 changes: 146 additions & 0 deletions backend/alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Alembic configuration for SQLModel-owned UK Chat tables.

[alembic]
# path to migration scripts.
# this is typically a path given in POSIX (e.g. forward slashes)
# format, relative to the token %(here)s which refers to the location of this
# ini file
script_location = %(here)s/migrations

# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s

# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory. for multiple paths, the path separator
# is defined by "path_separator" below.
prepend_sys_path = %(here)s


# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the tzdata library which can be installed by adding
# `alembic[tz]` to the pip requirements.
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =

# max length of characters to apply to the "slug" field
# truncate_slug_length = 40

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false

# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false

# version location specification; This defaults
# to <script_location>/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "path_separator"
# below.
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions

# path_separator; This indicates what character is used to split lists of file
# paths, including version_locations and prepend_sys_path within configparser
# files such as alembic.ini.
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
# to provide os-dependent path splitting.
#
# Note that in order to support legacy alembic.ini files, this default does NOT
# take place if path_separator is not present in alembic.ini. If this
# option is omitted entirely, fallback logic is as follows:
#
# 1. Parsing of the version_locations option falls back to using the legacy
# "version_path_separator" key, which if absent then falls back to the legacy
# behavior of splitting on spaces and/or commas.
# 2. Parsing of the prepend_sys_path option falls back to the legacy
# behavior of splitting on spaces, commas, or colons.
#
# Valid values for path_separator are:
#
# path_separator = :
# path_separator = ;
# path_separator = space
# path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
path_separator = os

# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false

# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8

# env.py requires ALEMBIC_DATABASE_URL and never reads a committed URL.


[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples

# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME

# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
# hooks = ruff
# ruff.type = module
# ruff.module = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME

# Alternatively, use the exec runner to execute a binary found on your PATH
# hooks = ruff
# ruff.type = exec
# ruff.executable = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME

# Logging configuration. This is also consumed by the user-maintained
# env.py script only.
[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARNING
handlers = console
qualname =

[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
Loading
Loading