-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge remote-tracking branch 'origin/main' into 326-setup-release-please
- Loading branch information
Showing
83 changed files
with
4,603 additions
and
4,265 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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_}") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" } }] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.