Skip to content

Commit 2370427

Browse files
Replace Streamlit by Reflex for playground (#493)
* wip 0 * add role page * add user and organization * lint * hide somes page for master user * remove password dialog * clean key page * clean usage page * create role * wip * clean role * wip * finish * add doc * Update pyproject.toml version * Update coverage badge --------- Co-authored-by: GitHub Actions <actions@github.com> Co-authored-by: leoguillaume <leoguillaume@users.noreply.github.com>
1 parent 7e5812f commit 2370427

143 files changed

Lines changed: 7402 additions & 3222 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/badges/coverage.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"schemaVersion":1,"label":"coverage","message":"72.71%","color":"yellow"}
1+
{"schemaVersion":1,"label":"coverage","message":"72.77%","color":"yellow"}
7.23 KB
Binary file not shown.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
settings:
2+
app_title: Albert API
3+
auth_key_max_expiration_days: 365
4+
playground_opengatellm_url: ${OPENGATELLM_URL}
5+
playground_default_model: ${PLAYGROUND_DEFAULT_MODEL}
6+
playground_theme_accent_color: indigo

.github/workflows/build_and_deploy.yml

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ on:
1212

1313
jobs:
1414
build-api:
15-
name: Build and push from ${{ github.ref_name }}/${{ github.sha }}
15+
name: Build and push OpenGateLLM API image
1616
runs-on: ubuntu-latest
1717
env:
1818
API_IMAGE_NAME: ghcr.io/etalab-ia/opengatellm/api
@@ -36,7 +36,7 @@ jobs:
3636
- name: Set up Docker Buildx
3737
uses: docker/setup-buildx-action@v3
3838

39-
- name: Build and push api
39+
- name: Build and push OpenGateLLM API image to GitHub
4040
uses: docker/build-push-action@v6
4141
with:
4242
context: .
@@ -47,11 +47,13 @@ jobs:
4747
cache-from: type=registry,ref=${{ env.API_IMAGE_NAME }}:cache
4848
cache-to: type=registry,ref=${{ env.API_IMAGE_NAME }}:cache,mode=max
4949

50-
build-playground:
51-
name: Build and push Playground image
50+
build-opengatellm-playground:
51+
name: Build and push OpenGateLLM playground image
5252
runs-on: ubuntu-latest
5353
env:
54-
PLAYGROUND_IMAGE_NAME: ghcr.io/etalab-ia/opengatellm/playground
54+
GITHUB_PLAYGROUND_IMAGE_NAME: ghcr.io/etalab-ia/opengatellm/playground
55+
GITLAB_PLAYGROUND_IMAGE_NAME: registry.gitlab.com/${{ secrets.GITLAB_PROJECT_PATH }}/playground
56+
PLAYGROUND_URL: https://albert.playground.env.etalab.gouv.fr
5557
IMAGE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || 'latest' }}
5658
steps:
5759
- name: Checkout repository
@@ -67,29 +69,78 @@ jobs:
6769
- name: Set up Docker Buildx
6870
uses: docker/setup-buildx-action@v3
6971

70-
- name: Build and push Playground image
72+
- name: Build and push OpenGateLLM playground image to GitHub
7173
uses: docker/build-push-action@v6
7274
with:
7375
context: .
76+
build-args: |
77+
CONFIG_FILE=.github/workflows/assets/playground.config.yml
78+
FAVICON=.github/workflows/assets/favicon.ico
7479
file: ./playground/Dockerfile
7580
platforms: linux/amd64,linux/arm64
7681
push: true
77-
tags: ${{ env.PLAYGROUND_IMAGE_NAME }}:${{ env.IMAGE_TAG }}
78-
cache-from: type=registry,ref=${{ env.PLAYGROUND_IMAGE_NAME }}:cache
79-
cache-to: type=registry,ref=${{ env.PLAYGROUND_IMAGE_NAME }}:cache,mode=max
82+
tags: ${{ env.GITHUB_PLAYGROUND_IMAGE_NAME }}:${{ env.IMAGE_TAG }}
83+
cache-from: type=registry,ref=${{ env.GITHUB_PLAYGROUND_IMAGE_NAME }}:cache
84+
cache-to: type=registry,ref=${{ env.GITHUB_PLAYGROUND_IMAGE_NAME }}:cache,mode=max
85+
86+
build-albert-playground:
87+
name: Build and push Albert playground image
88+
runs-on: ubuntu-latest
89+
env:
90+
GITLAB_PLAYGROUND_IMAGE_NAME: registry.gitlab.com/${{ secrets.GITLAB_PROJECT_PATH }}/playground
91+
IMAGE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || 'latest' }}
92+
strategy:
93+
matrix:
94+
environment: [dev, staging, prod]
95+
include:
96+
- environment: prod
97+
url: https://albert.playground.etalab.gouv.fr
98+
- environment: staging
99+
url: https://albert.playground.staging.etalab.gouv.fr
100+
- environment: dev
101+
url: https://albert.playground.dev.etalab.gouv.fr
102+
103+
steps:
104+
- name: Checkout repository
105+
uses: actions/checkout@v4
106+
107+
- name: Log in to GitLab Container Registry
108+
uses: docker/login-action@v3
109+
with:
110+
registry: registry.gitlab.com
111+
username: ${{ secrets.GITLAB_USERNAME }}
112+
password: ${{ secrets.GITLAB_TOKEN }}
113+
114+
- name: Set up Docker Buildx
115+
uses: docker/setup-buildx-action@v3
116+
117+
- name: Build and push Albert playground images to GitLab
118+
uses: docker/build-push-action@v6
119+
with:
120+
context: .
121+
build-args: |
122+
REFLEX_BACKEND_URL=${{ matrix.url }}
123+
REFLEX_FRONTEND_URL=${{ matrix.url }}
124+
CONFIG_FILE=.github/workflows/assets/playground.config.yml
125+
FAVICON=.github/workflows/assets/favicon.ico
126+
file: ./playground/Dockerfile
127+
platforms: linux/amd64,linux/arm64
128+
push: true
129+
tags: ${{ env.GITLAB_PLAYGROUND_IMAGE_NAME }}/${{ matrix.environment }}:${{ env.IMAGE_TAG }}
130+
cache-from: type=registry,ref=${{ env.GITHUB_PLAYGROUND_IMAGE_NAME }}:cache
80131

81132
deploy-dev:
82133
if: github.event_name == 'push' # Only deploy on push to main
83134
name: Deploy from ${{ github.ref_name }}/${{ github.sha }}
84135
runs-on: ubuntu-latest
85-
needs: [build-api, build-playground]
136+
needs: [build-api, build-opengatellm-playground, build-albert-playground]
86137
steps:
87138
- name: Trigger dev deployment
88139
run: |
89140
RESPONSE="$(curl --request POST \
90141
--form token=${{ secrets.GITLAB_CI_TOKEN }} \
91142
--form ref=main \
92-
--form 'variables[pipeline_name]=${{ github.event.repository.name }} - ${{ needs.build-and-push.outputs.commit_title }}' \
143+
--form 'variables[pipeline_name]=${{ github.event.repository.name }} - ${{ needs.build-api.outputs.commit_title }}' \
93144
--form 'variables[docker_image_tag]=latest' \
94145
--form 'variables[application_to_deploy]=albert-api' \
95146
--form 'variables[deployment_environment]=dev' \

.github/workflows/generate_documentation.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ on:
55
branches: [ main ]
66
pull_request:
77
branches: [ main ]
8+
release:
9+
types:
10+
- published
11+
- edited
812
workflow_call: # Add this to make the workflow reusable
913
workflow_dispatch: # Add this to allow manual triggering
1014

@@ -24,7 +28,7 @@ jobs:
2428
- name: Install dependencies
2529
run: |
2630
python -m pip install --upgrade pip
27-
pip install ".[api,dev]"
31+
pip install ".[api,playground,dev]"
2832
2933
- name: Generate documentation
3034
run: |

Makefile

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,22 @@ help:
4747

4848
.start-api:
4949
@bash -c 'set -a; . $(env); \
50-
SERVER=uvicorn \
51-
SERVER_CMD_ARGS="--reload --log-level debug" \
52-
./scripts/startup_api.sh &' \
53-
&& sleep 2 \
54-
&& open http://localhost:8000/docs
50+
trap "trap - SIGTERM && kill -- -$$$$" SIGINT SIGTERM EXIT; \
51+
python -m alembic -c api/alembic.ini upgrade head \
52+
&& uvicorn api.main:app --host 0.0.0.0 --port 8000 --reload --log-level debug & \
53+
sleep 10; \
54+
open http://localhost:8000/docs; \
55+
wait'
56+
5557

5658
.start-playground:
57-
@mkdir -p ~/.streamlit/
58-
@echo "[general]" > ~/.streamlit/credentials.toml
59-
@echo "email = \"\"" >> ~/.streamlit/credentials.toml
60-
@bash -c 'set -a; . $(env); ./scripts/startup_ui.sh'
59+
@bash -c 'set -a; . $(env); \
60+
trap "trap - SIGTERM && kill -- -$$$$" SIGINT SIGTERM EXIT; \
61+
cd ./playground \
62+
&& CONFIG_FILE=../$${CONFIG_FILE} API_URL="http://localhost:8500" reflex run --env dev --loglevel debug & \
63+
sleep 10; \
64+
open http://localhost:8501; \
65+
wait'
6166

6267
.pre-checks:
6368
@if [ ! -f $(env) ]; then \

api/Dockerfile

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
# First, build the application in the `/api` directory.
2-
# See `Dockerfile` for details.
31
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder
42
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
53

@@ -8,16 +6,17 @@ ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
86
# copied from the build image into the final image; see `standalone.Dockerfile`
97
# for an example.
108
ENV UV_PYTHON_DOWNLOADS=0
11-
# Install build dependencies
9+
1210
RUN apt-get update && apt-get install -y \
1311
libpq-dev \
1412
gcc \
1513
python3-dev \
1614
&& rm -rf /var/lib/apt/lists/*
1715

1816
WORKDIR /
19-
# Copy project files
17+
2018
COPY ./api/ /api
19+
2120
RUN --mount=type=cache,target=/root/.cache/uv \
2221
uv venv
2322
RUN --mount=type=cache,target=/root/.cache/uv \

api/factory.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def create_app(db_func=None, *args, **kwargs) -> FastAPI:
3131
sentry_sdk.init(**configuration.dependencies.sentry.model_dump())
3232

3333
app = FastAPI(
34-
title=configuration.settings.swagger_title,
34+
title=configuration.settings.app_title,
3535
summary=configuration.settings.swagger_summary,
3636
version=configuration.settings.swagger_version,
3737
description=configuration.settings.swagger_description,

api/helpers/_identityaccessmanager.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
InvalidCurrentPasswordException,
2929
InvalidTokenExpirationException,
3030
OrganizationNotFoundException,
31+
ReservedEmailException,
3132
RoleAlreadyExistsException,
3233
RoleNotFoundException,
3334
TokenNotFoundException,
@@ -42,9 +43,9 @@ class IdentityAccessManager:
4243
TOKEN_PREFIX = "sk-"
4344
PLAYGROUND_KEY_NAME = "playground"
4445

45-
def __init__(self, master_key: str, max_token_expiration_days: int | None = None, playground_session_duration: int = 3600):
46+
def __init__(self, master_key: str, key_max_expiration_days: int | None = None, playground_session_duration: int = 3600):
4647
self.master_key = master_key
47-
self.max_token_expiration_days = max_token_expiration_days
48+
self.key_max_expiration_days = key_max_expiration_days
4849
self.playground_session_duration = playground_session_duration
4950

5051
def _hash_password(self, password: str) -> str:
@@ -239,6 +240,9 @@ async def create_user(
239240
expires_at: int | None = None,
240241
priority: int = 0,
241242
) -> int:
243+
if email == "master":
244+
raise ReservedEmailException()
245+
242246
expires_at = func.to_timestamp(expires_at) if expires_at is not None else None
243247

244248
# check if role exists
@@ -337,6 +341,10 @@ async def update_user(
337341

338342
# update the user
339343
email = email if email is not None else user.email
344+
345+
if email == "master":
346+
raise ReservedEmailException()
347+
340348
name = name if name is not None else user.name
341349
iss = iss if iss is not None else user.iss
342350
sub = sub if sub is not None else user.sub
@@ -499,11 +507,11 @@ async def get_organizations(
499507
return organizations
500508

501509
async def create_token(self, session: AsyncSession, user_id: int, name: str, expires_at: int | None = None) -> tuple[int, str]:
502-
if self.max_token_expiration_days:
510+
if self.key_max_expiration_days:
503511
if expires_at is None:
504-
expires_at = int(dt.datetime.now(tz=dt.UTC).timestamp()) + self.max_token_expiration_days * 86400
505-
elif expires_at > int(dt.datetime.now(tz=dt.UTC).timestamp()) + self.max_token_expiration_days * 86400:
506-
raise InvalidTokenExpirationException(detail=f"Token expiration timestamp cannot be greater than {self.max_token_expiration_days} days from now.") # fmt: off
512+
expires_at = int(dt.datetime.now(tz=dt.UTC).timestamp()) + self.key_max_expiration_days * 86400
513+
elif expires_at > int(dt.datetime.now(tz=dt.UTC).timestamp()) + self.key_max_expiration_days * 86400:
514+
raise InvalidTokenExpirationException(detail=f"Token expiration timestamp cannot be greater than {self.key_max_expiration_days} days from now.") # fmt: off
507515

508516
result = await session.execute(statement=select(UserTable).where(UserTable.id == user_id))
509517
try:

api/schemas/core/configuration.py

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,7 @@
1212
import yaml
1313

1414
from api.schemas.models import ModelType
15-
from api.utils.variables import (
16-
DEFAULT_APP_NAME,
17-
DEFAULT_TIMEOUT,
18-
ROUTER__ADMIN,
19-
ROUTER__AUTH,
20-
ROUTERS,
21-
)
15+
from api.utils.variables import DEFAULT_APP_NAME, DEFAULT_TIMEOUT, ROUTER__ADMIN, ROUTER__AUTH, ROUTERS
2216

2317
# utils ----------------------------------------------------------------------------------------------------------------------------------------------
2418

@@ -414,9 +408,10 @@ class Tokenizer(str, Enum):
414408

415409
@custom_validation_error(url="https://github.com/etalab-ia/opengatellm/blob/main/docs/configuration.md#settings")
416410
class Settings(ConfigBaseModel):
417-
# other
411+
# general
418412
disabled_routers: list[Routers] = Field(default_factory=list, description="Disabled routers to limits services of the API.", examples=[["embeddings"]]) # fmt: off
419413
hidden_routers: list[Routers] = Field(default_factory=list, description="Routers are enabled but hidden in the swagger and the documentation of the API.", examples=[["admin"]]) # fmt: off
414+
app_title: str | None = Field(default="Albert API", description="Display title of your API in swagger UI, see https://fastapi.tiangolo.com/tutorial/metadata for more information.", examples=["Albert API"]) # fmt: off
420415

421416
# metrics
422417
metrics_retention_ms: int = Field(default=40000, ge=1, description="Retention time for metrics in milliseconds.") # fmt: off
@@ -429,7 +424,6 @@ class Settings(ConfigBaseModel):
429424
log_format: str | None = Field(default="[%(asctime)s][%(process)d:%(name)s][%(levelname)s] %(client_ip)s - %(message)s", description="Logging format of the API.") # fmt: off
430425

431426
# swagger
432-
swagger_title: str | None = Field(default="Albert API", description="Display title of your API in swagger UI, see https://fastapi.tiangolo.com/tutorial/metadata for more information.", examples=["Albert API"]) # fmt: off
433427
swagger_summary: str | None = Field(default="Albert API connect to your models. You can configuration this swagger UI in the configuration file, like hide routes or change the title.", description="Display summary of your API in swagger UI, see https://fastapi.tiangolo.com/tutorial/metadata for more information.", examples=["Albert API connect to your models."]) # fmt: off
434428
swagger_version: str | None = Field(default="latest", description="Display version of your API in swagger UI, see https://fastapi.tiangolo.com/tutorial/metadata for more information.", examples=["2.5.0"]) # fmt: off
435429
swagger_description: str | None = Field(default="[See documentation](https://github.com/etalab-ia/opengatellm/blob/main/README.md)", description="Display description of your API in swagger UI, see https://fastapi.tiangolo.com/tutorial/metadata for more information.", examples=["[See documentation](https://github.com/etalab-ia/opengatellm/blob/main/README.md)"]) # fmt: off
@@ -443,7 +437,7 @@ class Settings(ConfigBaseModel):
443437

444438
# auth
445439
auth_master_key: constr(strip_whitespace=True, min_length=1) = Field(default="changeme", description="Master key for the API. It should be a random string with at least 32 characters. This key has all permissions and cannot be modified or deleted. This key is used to create the first role and the first user. This key is also used to encrypt user tokens, watch out if you modify the master key, you'll need to update all user API keys.") # fmt: off
446-
auth_max_token_expiration_days: int | None = Field(default=None, ge=1, description="Maximum number of days for a token to be valid.") # fmt: off
440+
auth_key_max_expiration_days: int | None = Field(default=None, ge=1, description="Maximum number of days for a new API key to be valid.") # fmt: off
447441
auth_playground_session_duration: int = Field(default=3600, ge=1, description="Duration of the playground session in seconds.") # fmt: off
448442

449443
# rate_limiting
@@ -528,13 +522,9 @@ def validate_model(cls, values) -> Any:
528522
# load config ----------------------------------------------------------------------------------------------------------------------------------------
529523
@custom_validation_error(url="https://github.com/etalab-ia/opengatellm/blob/main/docs/configuration.md#all-configuration")
530524
class ConfigFile(ConfigBaseModel):
531-
"""
532-
Refer to the [configuration example file](https://github.com/etalab-ia/OpenGateLLM/blob/main/config.example.yml) for an example of configuration.
533-
"""
534-
535525
models: list[Model] = Field(min_length=1, description="Models used by the API. At least one model must be defined.") # fmt: off
536526
dependencies: Dependencies = Field(default_factory=Dependencies, description="Dependencies used by the API.") # fmt: off
537-
settings: Settings = Field(default_factory=Settings, description="Settings used by the API.") # fmt: off
527+
settings: Settings = Field(default_factory=Settings, description="General settings configuration fields.") # fmt: off
538528

539529
@field_validator("settings", mode="before")
540530
def set_default_settings(cls, settings) -> Any:

0 commit comments

Comments
 (0)