From 6ff292f0d3462b0a12cb0bd032ed9bdaa4077d69 Mon Sep 17 00:00:00 2001 From: Gregory Horvath Date: Wed, 24 Jul 2024 16:11:29 -0400 Subject: [PATCH 01/76] chore(api)!: updating api endpoints (#817) * (breaking) updating api key endpoint names * updates UI with the new api key endpoint conventions * moving leapfrogai models endpoint into leapfrogai namespace * changes rag endpoint to leapfrogai/vector_stores/search * temporarily disables the playwright tests that keep failing in the e2e pipeline --------- Co-authored-by: Andrew Risse --- .github/workflows/e2e.yaml | 8 ++-- src/leapfrogai_api/Makefile | 10 +++-- src/leapfrogai_api/backend/types.py | 16 ++++---- src/leapfrogai_api/main.py | 25 ++++++------- src/leapfrogai_api/routers/base.py | 8 +--- src/leapfrogai_api/routers/leapfrogai/auth.py | 37 +++++++++++++++---- .../routers/leapfrogai/models.py | 10 +++++ .../leapfrogai/{rag.py => vector_stores.py} | 20 +++++----- .../run_create_params_request_base.py | 8 ++-- src/leapfrogai_ui/playwright.config.ts | 2 + .../src/lib/mocks/api-key-mocks.ts | 10 ++--- .../src/routes/api/api-keys/delete/+server.ts | 2 +- .../chat/(settings)/api-keys/+page.server.ts | 4 +- src/leapfrogai_ui/tests/helpers/apiHelpers.ts | 26 +++++-------- tests/integration/api/test_auth.py | 10 ++--- tests/pytest/leapfrogai_api/test_api.py | 8 ++-- 16 files changed, 118 insertions(+), 86 deletions(-) create mode 100644 src/leapfrogai_api/routers/leapfrogai/models.py rename src/leapfrogai_api/routers/leapfrogai/{rag.py => vector_stores.py} (61%) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index adae970fe..1b3da9fc3 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -129,9 +129,11 @@ jobs: # Run the playwright UI tests using the deployed Supabase endpoint - name: UI/API/Supabase E2E Playwright Tests - run: | - cp src/leapfrogai_ui/.env.example src/leapfrogai_ui/.env - TEST_ENV=CI PUBLIC_DISABLE_KEYCLOAK=true PUBLIC_SUPABASE_ANON_KEY=$ANON_KEY npm --prefix src/leapfrogai_ui run test:integration:ci + run: | + echo "skip" +# run: | +# cp src/leapfrogai_ui/.env.example src/leapfrogai_ui/.env +# TEST_ENV=CI PUBLIC_DISABLE_KEYCLOAK=true PUBLIC_SUPABASE_ANON_KEY=$ANON_KEY npm --prefix src/leapfrogai_ui run test:integration:ci # The UI can be removed after the Playwright tests are finished - name: Cleanup UI diff --git a/src/leapfrogai_api/Makefile b/src/leapfrogai_api/Makefile index 470f885db..db315c61e 100644 --- a/src/leapfrogai_api/Makefile +++ b/src/leapfrogai_api/Makefile @@ -1,14 +1,18 @@ +MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) SHELL := /bin/bash export SUPABASE_URL=$(shell supabase status | grep -oP '(?<=API URL: ).*') export SUPABASE_ANON_KEY=$(shell supabase status | grep -oP '(?<=anon key: ).*') -install: +install-api: + @cd ${MAKEFILE_DIR} && \ python -m pip install ../../src/leapfrogai_sdk + @cd ${MAKEFILE_DIR} && \ python -m pip install -e . -dev: +dev-run-api: + @cd ${MAKEFILE_DIR} && \ python -m uvicorn main:app --port 3000 --reload --log-level info define get_jwt_token @@ -36,4 +40,4 @@ env: $(call get_jwt_token,"${SUPABASE_URL}/auth/v1/token?grant_type=password") test-integration: - cd ../../ && python -m pytest tests/integration/api/ -vv -s + @cd ${MAKEFILE_DIR} && python -m pytest ../../tests/integration/api/ -vv -s diff --git a/src/leapfrogai_api/backend/types.py b/src/leapfrogai_api/backend/types.py index 8024f9da1..59011003c 100644 --- a/src/leapfrogai_api/backend/types.py +++ b/src/leapfrogai_api/backend/types.py @@ -682,31 +682,31 @@ class ModifyMessageRequest(BaseModel): ################ -# LEAPFROGAI RAG +# LEAPFROGAI Vector Stores ################ -class RAGItem(BaseModel): - """Object representing a single item in a Retrieval-Augmented Generation (RAG) result.""" +class SearchItem(BaseModel): + """Object representing a single item in a search result.""" - id: str = Field(..., description="Unique identifier for the RAG item.") + id: str = Field(..., description="Unique identifier for the search item.") vector_store_id: str = Field( ..., description="ID of the vector store containing this item." ) file_id: str = Field(..., description="ID of the file associated with this item.") - content: str = Field(..., description="The actual content of the RAG item.") + content: str = Field(..., description="The actual content of the item.") metadata: dict = Field( - ..., description="Additional metadata associated with the RAG item." + ..., description="Additional metadata associated with the item." ) similarity: float = Field( ..., description="Similarity score of this item to the query." ) -class RAGResponse(BaseModel): +class SearchResponse(BaseModel): """Response object for RAG queries.""" - data: list[RAGItem] = Field( + data: list[SearchItem] = Field( ..., description="List of RAG items returned as a result of the query.", min_length=0, diff --git a/src/leapfrogai_api/main.py b/src/leapfrogai_api/main.py index d3207169b..fa0a1e056 100644 --- a/src/leapfrogai_api/main.py +++ b/src/leapfrogai_api/main.py @@ -3,32 +3,30 @@ import asyncio import logging from contextlib import asynccontextmanager + from fastapi import FastAPI +from fastapi.exception_handlers import request_validation_exception_handler +from fastapi.exceptions import RequestValidationError from leapfrogai_api.routers.base import router as base_router -from leapfrogai_api.routers.leapfrogai import ( - auth, - rag, -) +from leapfrogai_api.routers.leapfrogai import auth +from leapfrogai_api.routers.leapfrogai import models as lfai_models +from leapfrogai_api.routers.leapfrogai import vector_stores as lfai_vector_stores from leapfrogai_api.routers.openai import ( + assistants, audio, - completions, chat, + completions, embeddings, - models, - assistants, files, - threads, messages, + models, runs, runs_steps, + threads, vector_stores, ) from leapfrogai_api.utils import get_model_config -from fastapi.exception_handlers import ( - request_validation_exception_handler, -) -from fastapi.exceptions import RequestValidationError # handle startup & shutdown tasks @@ -71,7 +69,8 @@ async def validation_exception_handler(request, exc): app.include_router(runs.router) app.include_router(messages.router) app.include_router(runs_steps.router) -app.include_router(rag.router) +app.include_router(lfai_vector_stores.router) +app.include_router(lfai_models.router) # This should be at the bottom to prevent it preempting more specific runs endpoints # https://fastapi.tiangolo.com/tutorial/path-params/#order-matters app.include_router(threads.router) diff --git a/src/leapfrogai_api/routers/base.py b/src/leapfrogai_api/routers/base.py index 150a53f90..58032a62c 100644 --- a/src/leapfrogai_api/routers/base.py +++ b/src/leapfrogai_api/routers/base.py @@ -1,7 +1,7 @@ """Base router for the API.""" from fastapi import APIRouter -from leapfrogai_api.utils import get_model_config + router = APIRouter(tags=["/"]) @@ -10,9 +10,3 @@ async def healthz(): """Health check endpoint.""" return {"status": "ok"} - - -@router.get("/models") -async def models(): - """List all the models.""" - return get_model_config() diff --git a/src/leapfrogai_api/routers/leapfrogai/auth.py b/src/leapfrogai_api/routers/leapfrogai/auth.py index 9ef989d75..897f23a8b 100644 --- a/src/leapfrogai_api/routers/leapfrogai/auth.py +++ b/src/leapfrogai_api/routers/leapfrogai/auth.py @@ -42,7 +42,7 @@ class ModifyAPIKeyRequest(BaseModel): ) -@router.post("/create-api-key") +@router.post("/api-keys") async def create_api_key( session: Session, request: CreateAPIKeyRequest, @@ -50,6 +50,8 @@ async def create_api_key( """ Create an API key. + Accessible only with a valid JWT, not an API key. + WARNING: The API key is only returned once. Store it securely. """ @@ -71,24 +73,32 @@ async def create_api_key( return await crud_api_key.create(new_api_key) -@router.get("/list-api-keys") +@router.get("/api-keys") async def list_api_keys( session: Session, ) -> list[APIKeyItem]: - """List all API keys.""" + """ + List all API keys. + + Accessible only with a valid JWT, not an API key. + """ crud_api_key = CRUDAPIKey(session) return await crud_api_key.list() -@router.post("/update-api-key/{api_key_id}") +@router.patch("/api-keys/{api_key_id}") async def update_api_key( session: Session, api_key_id: Annotated[str, Field(description="The UUID of the API key.")], request: ModifyAPIKeyRequest, ) -> APIKeyItem: - """Update an API key.""" + """ + Update an API key. + + Accessible only with a valid JWT, not an API key. + """ crud_api_key = CRUDAPIKey(session) @@ -100,6 +110,15 @@ async def update_api_key( detail="API key not found.", ) + if request.expires_at and ( + request.expires_at > api_key.expires_at + or request.expires_at <= int(time.time()) + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid expiration time. New expiration must be in the future but less than the current expiration.", + ) + updated_api_key = APIKeyItem( name=request.name if request.name else api_key.name, id=api_key_id, @@ -113,12 +132,16 @@ async def update_api_key( return await crud_api_key.update(api_key_id, updated_api_key) -@router.delete("/revoke-api-key/{api_key_id}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete("/api-keys/{api_key_id}", status_code=status.HTTP_204_NO_CONTENT) async def revoke_api_key( session: Session, api_key_id: Annotated[str, Field(description="The UUID of the API key.")], ): - """Revoke an API key.""" + """ + Revoke an API key. + + Accessible only with a valid JWT, not an API key. + """ crud_api_key = CRUDAPIKey(session) diff --git a/src/leapfrogai_api/routers/leapfrogai/models.py b/src/leapfrogai_api/routers/leapfrogai/models.py new file mode 100644 index 000000000..27b750a2a --- /dev/null +++ b/src/leapfrogai_api/routers/leapfrogai/models.py @@ -0,0 +1,10 @@ +from fastapi import APIRouter +from leapfrogai_api.utils import get_model_config + +router = APIRouter(prefix="/leapfrogai/v1/models", tags=["leapfrogai/models"]) + + +@router.get("") +async def models(): + """List all the models.""" + return get_model_config() diff --git a/src/leapfrogai_api/routers/leapfrogai/rag.py b/src/leapfrogai_api/routers/leapfrogai/vector_stores.py similarity index 61% rename from src/leapfrogai_api/routers/leapfrogai/rag.py rename to src/leapfrogai_api/routers/leapfrogai/vector_stores.py index 99da0d558..8d3db3ec3 100644 --- a/src/leapfrogai_api/routers/leapfrogai/rag.py +++ b/src/leapfrogai_api/routers/leapfrogai/vector_stores.py @@ -3,21 +3,23 @@ from fastapi import APIRouter from postgrest.base_request_builder import SingleAPIResponse from leapfrogai_api.backend.rag.query import QueryService -from leapfrogai_api.backend.types import RAGResponse +from leapfrogai_api.backend.types import SearchResponse from leapfrogai_api.routers.supabase_session import Session -router = APIRouter(prefix="/leapfrogai/v1/rag", tags=["leapfrogai/rag"]) +router = APIRouter( + prefix="/leapfrogai/v1/vector_stores", tags=["leapfrogai/vector_stores"] +) -@router.post("") -async def query_rag( +@router.post("/search") +async def search( session: Session, query: str, vector_store_id: str, k: int = 5, -) -> RAGResponse: +) -> SearchResponse: """ - Query the RAG (Retrieval-Augmented Generation). + Performs a similarity search of the vector store. Args: session (Session): The database session. @@ -26,13 +28,13 @@ async def query_rag( k (int, optional): The number of results to retrieve. Defaults to 5. Returns: - RAGResponse: The response from the RAG. + SearchResponse: The search response from the vector store. """ query_service = QueryService(db=session) - result: SingleAPIResponse[RAGResponse] = await query_service.query_rag( + result: SingleAPIResponse[SearchResponse] = await query_service.query_rag( query=query, vector_store_id=vector_store_id, k=k, ) - return RAGResponse(data=result.data) + return SearchResponse(data=result.data) diff --git a/src/leapfrogai_api/routers/openai/requests/run_create_params_request_base.py b/src/leapfrogai_api/routers/openai/requests/run_create_params_request_base.py index eeefba78f..3f9834340 100644 --- a/src/leapfrogai_api/routers/openai/requests/run_create_params_request_base.py +++ b/src/leapfrogai_api/routers/openai/requests/run_create_params_request_base.py @@ -50,7 +50,7 @@ from leapfrogai_api.backend.rag.query import QueryService from leapfrogai_api.backend.types import ( ChatMessage, - RAGResponse, + SearchResponse, ChatCompletionResponse, ChatCompletionRequest, ChatChoice, @@ -263,12 +263,14 @@ def sort_by_created_at(msg: Message): for vector_store_id in vector_store_ids: rag_results_raw: SingleAPIResponse[ - RAGResponse + SearchResponse ] = await query_service.query_rag( query=first_message.content, vector_store_id=vector_store_id, ) - rag_responses: RAGResponse = RAGResponse(data=rag_results_raw.data) + rag_responses: SearchResponse = SearchResponse( + data=rag_results_raw.data + ) # Insert the RAG response messages just before the user's query for count, rag_response in enumerate(rag_responses.data): diff --git a/src/leapfrogai_ui/playwright.config.ts b/src/leapfrogai_ui/playwright.config.ts index eb599e436..47f5dc2df 100644 --- a/src/leapfrogai_ui/playwright.config.ts +++ b/src/leapfrogai_ui/playwright.config.ts @@ -62,6 +62,8 @@ const devConfig: PlaywrightTestConfig = { port: 4173, stderr: 'pipe' }, + testDir: 'tests', + testMatch: /(.+\.)?(test|spec)\.[jt]s/, use: { baseURL: 'http://localhost:4173' } diff --git a/src/leapfrogai_ui/src/lib/mocks/api-key-mocks.ts b/src/leapfrogai_ui/src/lib/mocks/api-key-mocks.ts index be4b8c96e..bf01f2eb9 100644 --- a/src/leapfrogai_ui/src/lib/mocks/api-key-mocks.ts +++ b/src/leapfrogai_ui/src/lib/mocks/api-key-mocks.ts @@ -5,7 +5,7 @@ import { faker } from '@faker-js/faker'; export const mockGetKeys = (keys: APIKeyRow[]) => { server.use( - http.get(`${process.env.LEAPFROGAI_API_BASE_URL}/leapfrogai/v1/auth/list-api-keys`, () => + http.get(`${process.env.LEAPFROGAI_API_BASE_URL}/leapfrogai/v1/auth/api-keys`, () => HttpResponse.json(keys) ) ); @@ -41,7 +41,7 @@ export const mockCreateApiKeyFormAction = (key: APIKeyRow) => { export const mockCreateApiKey = (api_key = `lfai_${faker.string.uuid()}`) => { server.use( http.post( - `${process.env.LEAPFROGAI_API_BASE_URL}/leapfrogai/v1/auth/create-api-key`, + `${process.env.LEAPFROGAI_API_BASE_URL}/leapfrogai/v1/auth/api-keys`, async ({ request }) => { const reqJson = (await request.json()) as NewApiKeyInput; const key: APIKeyRow = { @@ -61,7 +61,7 @@ export const mockCreateApiKey = (api_key = `lfai_${faker.string.uuid()}`) => { export const mockCreateApiKeyError = () => { server.use( http.post( - `${process.env.LEAPFROGAI_API_BASE_URL}/leapfrogai/v1/auth/create-api-key`, + `${process.env.LEAPFROGAI_API_BASE_URL}/leapfrogai/v1/auth/api-keys`, async () => new HttpResponse(null, { status: 500 }) ) ); @@ -70,7 +70,7 @@ export const mockCreateApiKeyError = () => { export const mockRevokeApiKey = () => { server.use( http.delete( - `${process.env.LEAPFROGAI_API_BASE_URL}/leapfrogai/v1/auth/revoke-api-key/:id`, + `${process.env.LEAPFROGAI_API_BASE_URL}/leapfrogai/v1/auth/api-keys/:id`, () => new HttpResponse(null, { status: 204 }) ) ); @@ -79,7 +79,7 @@ export const mockRevokeApiKey = () => { export const mockRevokeApiKeyError = () => { server.use( http.delete( - `${process.env.LEAPFROGAI_API_BASE_URL}/leapfrogai/v1/auth/revoke-api-key/:id`, + `${process.env.LEAPFROGAI_API_BASE_URL}/leapfrogai/v1/auth/api-keys/:id`, () => new HttpResponse(null, { status: 500 }) ) ); diff --git a/src/leapfrogai_ui/src/routes/api/api-keys/delete/+server.ts b/src/leapfrogai_ui/src/routes/api/api-keys/delete/+server.ts index d5b81633a..785c289ac 100644 --- a/src/leapfrogai_ui/src/routes/api/api-keys/delete/+server.ts +++ b/src/leapfrogai_ui/src/routes/api/api-keys/delete/+server.ts @@ -25,7 +25,7 @@ export const DELETE: RequestHandler = async ({ request, locals: { session } }) = const promises: Promise[] = []; for (const id of requestData.ids) { promises.push( - fetch(`${env.LEAPFROGAI_API_BASE_URL}/leapfrogai/v1/auth/revoke-api-key/${id}`, { + fetch(`${env.LEAPFROGAI_API_BASE_URL}/leapfrogai/v1/auth/api-keys/${id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${session.access_token}`, diff --git a/src/leapfrogai_ui/src/routes/chat/(settings)/api-keys/+page.server.ts b/src/leapfrogai_ui/src/routes/chat/(settings)/api-keys/+page.server.ts index 838468361..c9127e7a6 100644 --- a/src/leapfrogai_ui/src/routes/chat/(settings)/api-keys/+page.server.ts +++ b/src/leapfrogai_ui/src/routes/chat/(settings)/api-keys/+page.server.ts @@ -21,7 +21,7 @@ export const load: PageServerLoad = async ({ depends, locals: { session } }) => let keys: APIKeyRow[] = []; - const res = await fetch(`${env.LEAPFROGAI_API_BASE_URL}/leapfrogai/v1/auth/list-api-keys`, { + const res = await fetch(`${env.LEAPFROGAI_API_BASE_URL}/leapfrogai/v1/auth/api-keys`, { headers: { Authorization: `Bearer ${session.access_token}` } @@ -53,7 +53,7 @@ export const actions: Actions = { return fail(400, { form }); } - const res = await fetch(`${env.LEAPFROGAI_API_BASE_URL}/leapfrogai/v1/auth/create-api-key`, { + const res = await fetch(`${env.LEAPFROGAI_API_BASE_URL}/leapfrogai/v1/auth/api-keys`, { headers: { Authorization: `Bearer ${session.access_token}`, 'Content-Type': 'application/json' diff --git a/src/leapfrogai_ui/tests/helpers/apiHelpers.ts b/src/leapfrogai_ui/tests/helpers/apiHelpers.ts index 3ae672d21..db67856c4 100644 --- a/src/leapfrogai_ui/tests/helpers/apiHelpers.ts +++ b/src/leapfrogai_ui/tests/helpers/apiHelpers.ts @@ -2,29 +2,23 @@ import { getToken } from '../fixtures'; export const deleteAllTestAPIKeys = async () => { const token = getToken(); - const res = await fetch( - `${process.env.LEAPFROGAI_API_BASE_URL}/leapfrogai/v1/auth/list-api-keys`, - { - headers: { - Authorization: `Bearer ${token}` - } + const res = await fetch(`${process.env.LEAPFROGAI_API_BASE_URL}/leapfrogai/v1/auth/api-keys`, { + headers: { + Authorization: `Bearer ${token}` } - ); + }); const keys = await res.json(); const promises: Promise[] = []; for (const key of keys) { if (key.name.includes('test')) { promises.push( - fetch( - `${process.env.LEAPFROGAI_API_BASE_URL}/leapfrogai/v1/auth/revoke-api-key/${key.id}`, - { - method: 'DELETE', - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json' - } + fetch(`${process.env.LEAPFROGAI_API_BASE_URL}/leapfrogai/v1/auth/api-keys/${key.id}`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' } - ) + }) ); } } diff --git a/tests/integration/api/test_auth.py b/tests/integration/api/test_auth.py index 554ab53b4..834dc8756 100644 --- a/tests/integration/api/test_auth.py +++ b/tests/integration/api/test_auth.py @@ -35,7 +35,7 @@ def create_api_key(): "expires_at": int(time.time()) + THIRTY_DAYS, } - response = client.post("/leapfrogai/v1/auth/create-api-key", json=request) + response = client.post("/leapfrogai/v1/auth/api-keys", json=request) return response @@ -58,7 +58,7 @@ def test_list_api_keys(create_api_key): id_ = create_api_key.json()["id"] - response = client.get("/leapfrogai/v1/auth/list-api-keys") + response = client.get("/leapfrogai/v1/auth/api-keys") assert response.status_code is status.HTTP_200_OK assert len(response.json()) > 0, "List should return at least one API key." for api_key in response.json(): @@ -79,7 +79,7 @@ def test_update_api_key(create_api_key): "expires_at": int(time.time()) + 100, } - response = client.post(f"/leapfrogai/v1/auth/update-api-key/{id_}", json=request) + response = client.patch(f"/leapfrogai/v1/auth/api-keys/{id_}", json=request) assert response.status_code is status.HTTP_200_OK assert APIKeyItem.model_validate(response.json()), "API key should be valid." assert response.json()["id"] == id_, "Update should return the created API key." @@ -90,9 +90,9 @@ def test_revoke_api_key(create_api_key): api_key_id = create_api_key.json()["id"] - response = client.delete(f"/leapfrogai/v1/auth/revoke-api-key/{api_key_id}") + response = client.delete(f"/leapfrogai/v1/auth/api-keys/{api_key_id}") assert response.status_code is status.HTTP_204_NO_CONTENT with pytest.raises(HTTPException): - response = client.delete(f"/leapfrogai/v1/auth/revoke-api-key/{api_key_id}") + response = client.delete(f"/leapfrogai/v1/auth/api-keys/{api_key_id}") assert response.status_code is status.HTTP_404_NOT_FOUND diff --git a/tests/pytest/leapfrogai_api/test_api.py b/tests/pytest/leapfrogai_api/test_api.py index 935cc3b0c..a80df6b6c 100644 --- a/tests/pytest/leapfrogai_api/test_api.py +++ b/tests/pytest/leapfrogai_api/test_api.py @@ -70,7 +70,7 @@ def dummy_auth_middleware(): def test_config_load(): """Test that the config is loaded correctly.""" with TestClient(app) as client: - response = client.get("/models") + response = client.get("/leapfrogai/v1/models") assert response.status_code == 200 assert response.json() == { @@ -89,7 +89,7 @@ def test_config_delete(tmp_path): with TestClient(app) as client: # ensure the API loads the temp config - response = client.get("/models") + response = client.get("/leapfrogai/v1/models") assert response.status_code == 200 assert response.json() == { @@ -102,7 +102,7 @@ def test_config_delete(tmp_path): # wait for the api to be able to detect the change time.sleep(0.5) # assert response is now empty - response = client.get("/models") + response = client.get("/leapfrogai/v1/models") assert response.status_code == 200 assert response.json() == {"config_sources": {}, "models": {}} @@ -114,7 +114,7 @@ def test_routes(): expected_routes = { "/docs": ["GET", "HEAD"], "/healthz": ["GET"], - "/models": ["GET"], + "/leapfrogai/v1/models": ["GET"], "/openai/v1/models": ["GET"], "/openai/v1/chat/completions": ["POST"], "/openai/v1/embeddings": ["POST"], From 71a261fde9473cfb315e80d6a8f4fb7a84034452 Mon Sep 17 00:00:00 2001 From: Andrew Risse <52644157+andrewrisse@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:22:20 -0600 Subject: [PATCH 02/76] Allows Playwright e2es to run locally again From fd1e3dd3148581307d9ccf021c94d4208d839c56 Mon Sep 17 00:00:00 2001 From: Andrew Risse <52644157+andrewrisse@users.noreply.github.com> Date: Thu, 25 Jul 2024 16:06:23 -0600 Subject: [PATCH 03/76] fix(ui): playwright login without keycloak (#833) * fix playwright login without keycloak --------- Co-authored-by: John Alling --- .github/workflows/e2e-shim.yaml | 4 -- .github/workflows/e2e.yaml | 12 ++--- src/leapfrogai_ui/README.md | 25 ++--------- src/leapfrogai_ui/package-lock.json | 36 --------------- src/leapfrogai_ui/package.json | 2 - src/leapfrogai_ui/src/lib/schemas/auth.ts | 6 +++ src/leapfrogai_ui/src/routes/+page.server.ts | 7 ++- src/leapfrogai_ui/src/routes/+page.svelte | 45 ++++++++++++++----- .../src/routes/auth/+page.server.ts | 45 ++++++++++++++++++- .../src/routes/auth/confirm/+server.ts | 31 +++++++++++++ .../src/routes/auth/error/+page.svelte | 1 + src/leapfrogai_ui/supabase/config.toml | 2 +- src/leapfrogai_ui/tests/global.setup.ts | 18 ++++---- 13 files changed, 140 insertions(+), 94 deletions(-) create mode 100644 src/leapfrogai_ui/src/lib/schemas/auth.ts create mode 100644 src/leapfrogai_ui/src/routes/auth/confirm/+server.ts create mode 100644 src/leapfrogai_ui/src/routes/auth/error/+page.svelte diff --git a/.github/workflows/e2e-shim.yaml b/.github/workflows/e2e-shim.yaml index 83899dcce..95ec044e1 100644 --- a/.github/workflows/e2e-shim.yaml +++ b/.github/workflows/e2e-shim.yaml @@ -23,10 +23,6 @@ on: # Catch pytests - "tests/pytest/**" - # Catch LFAI-UI things - - "src/leapfrogai_ui/**" - - "packages/ui/**" - # Catch changes to the repeater model - "packages/repeater/**" diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 1b3da9fc3..11ac50a61 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -32,10 +32,6 @@ on: # Ignore non e2e tests - "!tests/pytest/**" - # Ignore LFAI-UI things (for now?) - - "!src/leapfrogai_ui/**" - - "!packages/ui/**" - # Ignore changes to the repeater model - "!packages/repeater/**" @@ -129,11 +125,9 @@ jobs: # Run the playwright UI tests using the deployed Supabase endpoint - name: UI/API/Supabase E2E Playwright Tests - run: | - echo "skip" -# run: | -# cp src/leapfrogai_ui/.env.example src/leapfrogai_ui/.env -# TEST_ENV=CI PUBLIC_DISABLE_KEYCLOAK=true PUBLIC_SUPABASE_ANON_KEY=$ANON_KEY npm --prefix src/leapfrogai_ui run test:integration:ci + run: | + cp src/leapfrogai_ui/.env.example src/leapfrogai_ui/.env + TEST_ENV=CI PUBLIC_DISABLE_KEYCLOAK=true PUBLIC_SUPABASE_ANON_KEY=$ANON_KEY npm --prefix src/leapfrogai_ui run test:integration:ci # The UI can be removed after the Playwright tests are finished - name: Cleanup UI diff --git a/src/leapfrogai_ui/README.md b/src/leapfrogai_ui/README.md index 3ef22293c..88f6e3613 100644 --- a/src/leapfrogai_ui/README.md +++ b/src/leapfrogai_ui/README.md @@ -63,7 +63,7 @@ run the UI outside of UDS on localhost (e.g. for development work), there are so The Supabase UDS package has a ConfigMap called "supabase-auth-default". Add these values to the "GOTRUE_URI_ALLOW_LIST" (no spaces!). This variable may not exist and you will need to add it. Restart the supabase-auth pod after updating the config: - `http://localhost:5173/auth/callback,http://localhost:5173,http://localhost:4173/auth/callback,http://localhost:4173` + `http://localhost:5173/auth/callback,http://localhost:4173,http://localhost:4173/auth/callback` Note - Port 4173 is utilized by Playwright for E2E tests. You do not need this if you are not concerned about Playwright. ###### With Keycloak authentication @@ -180,7 +180,6 @@ The variables that had to be overridden were: ``` [auth] site_url = "http://localhost:5173" -additional_redirect_urls = ["http://localhost:5173/auth/callback"] [auth.external.keycloak] enabled = true @@ -197,9 +196,8 @@ Under a realm in Keycloak that is not the master realm (if using UDS, its "uds") 1. Create a new client (the client ID you use will be used in the env variables below) 2. Turn on "Client Authentication" 3. For "Valid redirect URLs", you need to put: - 1. `http://localhost:5173/auth/callback` (or the URL for the frontend app callback) - 2. `http://127.0.0.1:54321/auth/v1/callback` (or the URL for the Supabase callback, for locally running Supabase, DO NOT USE LOCALHOST, use 127.0.0.1) - 3. Put the same two URLs in for "Web Origins" + 1. `http://127.0.0.1:54321/auth/v1/callback` (or the URL for the Supabase callback, for locally running Supabase, DO NOT USE LOCALHOST, use 127.0.0.1) + 2. Put the same two URLs in for "Web Origins" 4. Copy the Client Secret under the Clients -> Credentials tab and use in the env variables below 5. You can create users under the "Users" tab and either have them verify their email (if you setup SMTP), or manually mark them as verified. @@ -256,23 +254,6 @@ variables will also not do anything because they are only used for locally runni You will instead need to modify the Auth provider settings directly in the Supabase dashboard and set the appropriate redirect URLs and client ID/secret. -If you use the component from @supabase/auth-ui-shared, you must ensure you supply provider and scope props: -ex: - -``` - -``` - If you do not use this component and need to login with a Supabase auth command directly, ensure you provide the "openid" scope with the options parameter. diff --git a/src/leapfrogai_ui/package-lock.json b/src/leapfrogai_ui/package-lock.json index 973169fe4..056e5c65b 100644 --- a/src/leapfrogai_ui/package-lock.json +++ b/src/leapfrogai_ui/package-lock.json @@ -14,8 +14,6 @@ "@carbon/layout": "^11.23.0", "@carbon/themes": "^11.37.0", "@carbon/type": "^11.28.0", - "@supabase/auth-ui-shared": "^0.1.8", - "@supabase/auth-ui-svelte": "^0.2.9", "@supabase/ssr": "^0.4.0", "@supabase/supabase-js": "^2.44.4", "@sveltejs/vite-plugin-svelte": "^3.1.1", @@ -2052,11 +2050,6 @@ "integrity": "sha512-lWb0Wiz8KZ9ip/dY1eUqt7fhTPmL24p6Hmv5Fd9pzlzAdw/YNcWZr+tiCT4oZ4Zyxzi9+1X4zv82o7jYvcFxYA==", "optional": true }, - "node_modules/@stitches/core": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@stitches/core/-/core-1.2.8.tgz", - "integrity": "sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==" - }, "node_modules/@supabase/auth-js": { "version": "2.64.4", "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.64.4.tgz", @@ -2065,35 +2058,6 @@ "@supabase/node-fetch": "^2.6.14" } }, - "node_modules/@supabase/auth-ui-shared": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@supabase/auth-ui-shared/-/auth-ui-shared-0.1.8.tgz", - "integrity": "sha512-ouQ0DjKcEFg+0gZigFIEgu01V3e6riGZPzgVD0MJsCBNsMsiDT74+GgCEIElMUpTGkwSja3xLwdFRFgMNFKcjg==", - "peerDependencies": { - "@supabase/supabase-js": "^2.21.0" - } - }, - "node_modules/@supabase/auth-ui-svelte": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@supabase/auth-ui-svelte/-/auth-ui-svelte-0.2.9.tgz", - "integrity": "sha512-oc+SRS7ykc5FCssoqT0IiK5KF/obwnWko5ePaaMTDwUEQOavv+O8/poAh2lRTKRJCqJMqOOpMS/lUE6pHcma3g==", - "dependencies": { - "@stitches/core": "^1.2.8", - "@supabase/auth-ui-shared": "0.1.8", - "svelte": "^3.55.1" - }, - "peerDependencies": { - "@supabase/supabase-js": "^2.21.0" - } - }, - "node_modules/@supabase/auth-ui-svelte/node_modules/svelte": { - "version": "3.59.2", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.59.2.tgz", - "integrity": "sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==", - "engines": { - "node": ">= 8" - } - }, "node_modules/@supabase/functions-js": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.1.tgz", diff --git a/src/leapfrogai_ui/package.json b/src/leapfrogai_ui/package.json index b4fd46228..4a41576e2 100644 --- a/src/leapfrogai_ui/package.json +++ b/src/leapfrogai_ui/package.json @@ -75,8 +75,6 @@ "@carbon/layout": "^11.23.0", "@carbon/themes": "^11.37.0", "@carbon/type": "^11.28.0", - "@supabase/auth-ui-shared": "^0.1.8", - "@supabase/auth-ui-svelte": "^0.2.9", "@supabase/ssr": "^0.4.0", "@supabase/supabase-js": "^2.44.4", "@sveltejs/vite-plugin-svelte": "^3.1.1", diff --git a/src/leapfrogai_ui/src/lib/schemas/auth.ts b/src/leapfrogai_ui/src/lib/schemas/auth.ts new file mode 100644 index 000000000..512801154 --- /dev/null +++ b/src/leapfrogai_ui/src/lib/schemas/auth.ts @@ -0,0 +1,6 @@ +import { object, string } from 'yup'; + +export const emailPasswordSchema = object({ + email: string().email().required(), + password: string().required() +}); diff --git a/src/leapfrogai_ui/src/routes/+page.server.ts b/src/leapfrogai_ui/src/routes/+page.server.ts index f598cdcda..d44e7310c 100644 --- a/src/leapfrogai_ui/src/routes/+page.server.ts +++ b/src/leapfrogai_ui/src/routes/+page.server.ts @@ -1,5 +1,10 @@ import type { PageServerLoad } from './$types'; +import { superValidate } from 'sveltekit-superforms'; +import { yup } from 'sveltekit-superforms/adapters'; +import { emailPasswordSchema } from '$schemas/auth'; export const load: PageServerLoad = async ({ url }) => { - return { url: url.origin }; + const form = await superValidate(yup(emailPasswordSchema)); + + return { url: url.origin, form }; }; diff --git a/src/leapfrogai_ui/src/routes/+page.svelte b/src/leapfrogai_ui/src/routes/+page.svelte index 454a33b46..7f1fe2ded 100644 --- a/src/leapfrogai_ui/src/routes/+page.svelte +++ b/src/leapfrogai_ui/src/routes/+page.svelte @@ -1,9 +1,8 @@ {#if env.PUBLIC_DISABLE_KEYCLOAK === 'true'} - +
+
+ + + + + + + + {#if $errors.email} + {$errors.email} + {/if} +
+
+ - -
- - - - - - - - -
- {#if avatarToShow} -
-
+
+
+
+ (selectedRadioButton = 'pictogram')} + class="dark:text-gray-400">Pictogram + (selectedRadioButton = 'upload')} + class="dark:text-gray-400">Upload +
+
+
- {/if} - -
-
Upload image
-
Supported file types are .jpg and .png.
- +
+
+
+ 0 ? filteredPictograms : pictogramNames} + bind:selectedPictogramName={tempPictogram} /> -
- {#if hideUploader} -
- + {#if avatarToShow} + + {/if} + +
+

Upload image

+

Supported file types are .jpg and .png.

+ -
- {/if} + Upload from computer + - {#if shouldValidate && (fileNotUploaded || fileTooBig)} -
-
{errorMsg}
+
- {/if} -
-
-
- - + +
+ + + { + const file = e.currentTarget.files[0]; + $form.avatarFile = file ?? null; + }} + multiple={false} + type="file" + tabindex="-1" + accept={['.jpg', '.jpeg', '.png']} + name="avatarFile" + class="sr-only" + /> + +
diff --git a/src/leapfrogai_ui/src/lib/components/AssistantCard.svelte b/src/leapfrogai_ui/src/lib/components/AssistantCard.svelte new file mode 100644 index 000000000..36cc4fd96 --- /dev/null +++ b/src/leapfrogai_ui/src/lib/components/AssistantCard.svelte @@ -0,0 +1,101 @@ + + +
+ +
+ + + goto(`/chat/assistants-management/edit/${assistant.id}`)} + >Edit + (deleteModalOpen = true)}>Delete + +
+ +
+ {#if assistant.metadata.avatar} + + {:else} + + {/if} + +
+ {assistant.name && assistant.name.length > 20 + ? `${assistant.name.slice(0, 20)}...` + : assistant.name} +
+ + {assistant.description && assistant.description.length > 75 + ? `${assistant.description?.slice(0, 75)}...` + : assistant.description} +
+
+
+ +
+ +

+ Are you sure you want to delete your + {assistant.name} + assistant? +

+
+ + +
+
diff --git a/src/leapfrogai_ui/src/lib/components/AssistantFileDropdown.svelte b/src/leapfrogai_ui/src/lib/components/AssistantFileDropdown.svelte new file mode 100644 index 000000000..3c903f0a9 --- /dev/null +++ b/src/leapfrogai_ui/src/lib/components/AssistantFileDropdown.svelte @@ -0,0 +1,96 @@ + + +
+ + +
+
+ { + const fileList = e.detail; + filesStore.setUploading(true); + filesStore.addUploadingFiles(fileList, { autoSelectUploadedFiles: true }); + submit(); + }}>Upload new data source +
+
+
+ {#each $filesStore.files?.map( (file) => ({ id: file.id, text: file.filename }) ) as file (file.id)} +
  • + handleClick(file.id)} + checked={$filesStore.selectedAssistantFileIds.includes(file.id)} + class="overflow-hidden text-ellipsis whitespace-nowrap">{file.text} +
  • + {/each} +
    +
    +
    diff --git a/src/leapfrogai_ui/src/lib/components/AssistantFileSelect.svelte b/src/leapfrogai_ui/src/lib/components/AssistantFileSelect.svelte index b3b065e2b..868593e3f 100644 --- a/src/leapfrogai_ui/src/lib/components/AssistantFileSelect.svelte +++ b/src/leapfrogai_ui/src/lib/components/AssistantFileSelect.svelte @@ -1,10 +1,10 @@ -
    - ({ id: file.id, text: file.filename }))} - direction="top" - accept={ACCEPTED_FILE_TYPES} - bind:selectedIds={$filesStore.selectedAssistantFileIds} - {filesForm} - /> -
    + -
    +
    {#each filteredStoreFiles as file} -
    +
    { filesStore.setSelectedAssistantFileIds( $filesStore.selectedAssistantFileIds.filter((id) => id !== file.id) @@ -50,25 +39,3 @@
    - - diff --git a/src/leapfrogai_ui/src/lib/components/AssistantFileSelect.test.ts b/src/leapfrogai_ui/src/lib/components/AssistantFileSelect.test.ts index 5c938b175..6bb15f2ae 100644 --- a/src/leapfrogai_ui/src/lib/components/AssistantFileSelect.test.ts +++ b/src/leapfrogai_ui/src/lib/components/AssistantFileSelect.test.ts @@ -56,6 +56,7 @@ describe('AssistantFileSelect', () => { screen.queryByTestId(`${mockFiles[0].filename}-${mockFiles[0].status}-uploader-item`) ).not.toBeInTheDocument(); + await userEvent.click(screen.getByTestId('file-select-dropdown-btn')); await userEvent.click(screen.getByText(mockFiles[0].filename)); screen.getByTestId(`${mockFiles[0].filename}-${mockFiles[0].status}-uploader-item`); }); diff --git a/src/leapfrogai_ui/src/lib/components/AssistantForm.svelte b/src/leapfrogai_ui/src/lib/components/AssistantForm.svelte index b96c19dcb..2b738982c 100644 --- a/src/leapfrogai_ui/src/lib/components/AssistantForm.svelte +++ b/src/leapfrogai_ui/src/lib/components/AssistantForm.svelte @@ -7,15 +7,17 @@ import { superForm } from 'sveltekit-superforms'; import { page } from '$app/stores'; import { beforeNavigate, goto } from '$app/navigation'; - import { Button, Modal, Slider, TextArea, TextInput } from 'carbon-components-svelte'; - import AssistantAvatar from '$components/AssistantAvatar.svelte'; + import { Button, Modal, P } from 'flowbite-svelte'; + import Slider from '$components/Slider.svelte'; import { yup } from 'sveltekit-superforms/adapters'; import { filesStore, toastStore } from '$stores'; - import InputTooltip from '$components/InputTooltip.svelte'; import { assistantInputSchema, editAssistantInputSchema } from '$lib/schemas/assistants'; import type { NavigationTarget } from '@sveltejs/kit'; import { onMount } from 'svelte'; import AssistantFileSelect from '$components/AssistantFileSelect.svelte'; + import LFInput from '$components/LFInput.svelte'; + import LFLabel from '$components/LFLabel.svelte'; + import AssistantAvatar from '$components/AssistantAvatar.svelte'; export let data; @@ -53,8 +55,6 @@ }); let cancelModalOpen = false; - let files: File[] = []; - let selectedPictogramName = isEditMode ? $form.pictogram : 'default'; let navigateTo: NavigationTarget; let leavePageConfirmed = false; @@ -86,171 +86,110 @@ }); -
    -
    -
    -
    -
    {`${isEditMode ? 'Edit' : 'New'} Assistant`}
    - -
    - - - - - - - - - -