Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into 326-setup-release-please
Browse files Browse the repository at this point in the history
  • Loading branch information
jalling97 committed May 2, 2024
2 parents 0fe0411 + c269eee commit 95af26d
Show file tree
Hide file tree
Showing 83 changed files with 4,603 additions and 4,265 deletions.
26 changes: 26 additions & 0 deletions src/leapfrogai_api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# LeapfrogAI API

A mostly OpenAI compliant API surface.

## Requirements

- Supabase

## Local Development

Create a local Supabase instance (requires [[Supabase CLI](https://supabase.com/docs/guides/cli/getting-started)):

``` bash
supabase start # from /leapfrogai

supabase db reset # clears all data and reinitializes migrations

supabase status # to check status and see your keys
```

Setup environment variables:

``` bash
export SUPABASE_URL="http://localhost:54321" # or whatever you configured it as in your Supabase config.toml
export SUPABASE_SERVICE_KEY="<YOUR_KEY>" # supabase status will show you the keys
```
Empty file.
28 changes: 28 additions & 0 deletions src/leapfrogai_api/data/crud_file_bucket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""CRUD Operations for the Files Bucket."""

from supabase_py_async import AsyncClient
from fastapi import UploadFile


class CRUDFileBucket:
"""CRUD Operations for FileBucket."""

def __init__(self, model: type[UploadFile]):
self.model = model

async def upload(self, client: AsyncClient, file: UploadFile, id_: str):
"""Upload a file to the file bucket."""

return await client.storage.from_("file_bucket").upload(
file=file.file.read(), path=f"{id_}"
)

async def download(self, client: AsyncClient, id_: str):
"""Get a file from the file bucket."""

return await client.storage.from_("file_bucket").download(path=f"{id_}")

async def delete(self, client: AsyncClient, id_: str):
"""Delete a file from the file bucket."""

return await client.storage.from_("file_bucket").remove(paths=f"{id_}")
77 changes: 77 additions & 0 deletions src/leapfrogai_api/data/crud_file_object.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""CRUD Operations for FileObject"""

from supabase_py_async import AsyncClient
from openai.types import FileObject, FileDeleted


class CRUDFileObject:
"""CRUD Operations for FileObject"""

def __init__(self, model: type[FileObject]):
self.model = model

async def create(
self, client: AsyncClient, file_object: FileObject
) -> FileObject | None:
"""Create a new file object."""
file_object_dict = file_object.model_dump()
if file_object_dict.get("id") == "":
del file_object_dict["id"]
data, _count = (
await client.table("file_objects").insert(file_object_dict).execute()
)

_, response = data

if response:
return self.model(**response[0])
return None

async def get(self, client: AsyncClient, file_id: str) -> FileObject | None:
"""Get a file object by its ID."""
data, _count = (
await client.table("file_objects").select("*").eq("id", file_id).execute()
)

_, response = data

if response:
return self.model(**response[0])
return None

async def list(self, client: AsyncClient) -> list[FileObject] | None:
"""List all file objects."""
data, _count = await client.table("file_objects").select("*").execute()

_, response = data

if response:
return [self.model(**item) for item in response]
return None

async def update(
self, client: AsyncClient, file_id: str, file_object: FileObject
) -> FileObject | None:
"""Update a file object by its ID."""
data, _count = (
await client.table("file_objects")
.update(file_object.model_dump())
.eq("id", file_id)
.execute()
)

_, response = data

if response:
return self.model(**response[0])
return None

async def delete(self, client: AsyncClient, file_id: str) -> FileDeleted:
"""Delete a file object by its ID."""
data, _count = (
await client.table("file_objects").delete().eq("id", file_id).execute()
)

_, response = data

return FileDeleted(id=file_id, deleted=bool(response), object="file")
3 changes: 2 additions & 1 deletion src/leapfrogai_api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ version = "0.6.1"

dependencies = [
"fastapi >= 0.109.1",
"openai >= 1.21.1",
"openai == 1.21.1",
"uvicorn >= 0.23.2",
"pydantic >= 2.0.0",
"python-multipart >= 0.0.7", #indirect dep of FastAPI to receive form data for file uploads
"watchfiles >= 0.21.0",
"leapfrogai_sdk",
"supabase-py-async",
]
requires-python = "~=3.11"

Expand Down
97 changes: 80 additions & 17 deletions src/leapfrogai_api/routers/openai/files.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,107 @@
"""OpenAI compliant Files API Router."""
"""OpenAI Compliant Files API Router."""

import time

from fastapi import APIRouter, Depends, HTTPException, UploadFile
from openai.types import FileDeleted, FileObject

from fastapi import Depends, APIRouter, HTTPException
from openai.types import FileObject, FileDeleted
from leapfrogai_api.backend.types import UploadFileRequest
from leapfrogai_api.data.crud_file_object import CRUDFileObject
from leapfrogai_api.data.crud_file_bucket import CRUDFileBucket
from leapfrogai_api.routers.supabase_session import Session

router = APIRouter(prefix="/openai/v1/files", tags=["openai/files"])


@router.post("")
async def upload_file(
client: Session,
request: UploadFileRequest = Depends(UploadFileRequest.as_form),
) -> FileObject:
"""Upload a file."""
# TODO: Implement this function
raise HTTPException(status_code=501, detail="Not implemented")

try:
file_object = FileObject(
id="", # This is set by the database to prevent conflicts
bytes=request.file.size,
created_at=int(time.time()),
filename=request.file.filename,
object="file", # Per OpenAI Spec this should always be file
purpose="assistants", # we only support assistants for now
status="uploaded",
status_details=None,
)
except Exception as exc:
raise HTTPException(status_code=500, detail="Failed to parse file") from exc

try:
crud_file_object = CRUDFileObject(model=FileObject)
file_object = await crud_file_object.create(
file_object=file_object, client=client
)

if not file_object:
raise HTTPException(status_code=500, detail="Failed to create file object")

crud_file_bucket = CRUDFileBucket(model=UploadFile)
await crud_file_bucket.upload(
client=client, file=request.file, id_=file_object.id
)

except Exception as exc:
raise HTTPException(status_code=500, detail="Failed to store file") from exc

return file_object


@router.get("")
async def list_files():
async def list_files(session: Session) -> list[FileObject] | None:
"""List all files."""
# TODO: Implement this function
raise HTTPException(status_code=501, detail="Not implemented")
try:
crud_file = CRUDFileObject(model=FileObject)
return await crud_file.list(client=session)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail="No file objects found") from exc


@router.get("/{file_id}")
async def retrieve_file(file_id: str) -> FileObject:
async def retrieve_file(client: Session, file_id: str) -> FileObject | None:
"""Retrieve a file."""
# TODO: Implement this function
raise HTTPException(status_code=501, detail="Not implemented")
try:
crud_file = CRUDFileObject(model=FileObject)
return await crud_file.get(file_id=file_id, client=client)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail="File not found") from exc
except ValueError as exc:
raise HTTPException(
status_code=500, detail="Multiple files found with same id"
) from exc


@router.delete("/{file_id}")
async def delete_file(file_id: str) -> FileDeleted:
async def delete_file(session: Session, file_id: str) -> FileDeleted:
"""Delete a file."""
# TODO: Implement this function
raise HTTPException(status_code=501, detail="Not implemented")
try:
crud_file_object = CRUDFileObject(model=FileObject)
file_deleted = await crud_file_object.delete(file_id=file_id, client=session)

crud_file_bucket = CRUDFileBucket(model=UploadFile)
await crud_file_bucket.delete(client=session, id_=file_id)

return file_deleted
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail="File not found") from exc


@router.get("/{file_id}/content")
async def retrieve_file_content(file_id: str):
async def retrieve_file_content(session: Session, file_id: str):
"""Retrieve the content of a file."""
# TODO: Implement this function
raise HTTPException(status_code=501, detail="Not implemented")
try:
crud_file_bucket = CRUDFileBucket(model=UploadFile)
return await crud_file_bucket.download(client=session, id_=file_id)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail="File not found") from exc
except ValueError as exc:
raise HTTPException(
status_code=500, detail="Multiple files found with same id"
) from exc
17 changes: 17 additions & 0 deletions src/leapfrogai_api/routers/supabase_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Supabase session dependency."""

import os
from typing import Annotated
from fastapi import Depends
from supabase_py_async import AsyncClient, create_client


async def init_supabase_client() -> AsyncClient:
"""Initialize a Supabase client."""
return await create_client(
supabase_key=os.getenv("SUPABASE_SERVICE_KEY"),
supabase_url=os.getenv("SUPABASE_URL"),
)


Session = Annotated[AsyncClient, Depends(init_supabase_client)]
62 changes: 31 additions & 31 deletions src/leapfrogai_ui/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
/** @type { import("eslint").Linter.Config } */
module.exports = {
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
],
rules: {
'no-undef': 'off'
}
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
],
rules: {
'no-undef': 'off'
}
};
14 changes: 7 additions & 7 deletions src/leapfrogai_ui/.prettierrc
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"useTabs": true,
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
"useTabs": false,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}
6 changes: 3 additions & 3 deletions src/leapfrogai_ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ 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://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"
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.

Expand Down
Loading

0 comments on commit 95af26d

Please sign in to comment.