Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): assistants endpoint #424

Merged
merged 90 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from 82 commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
35e19be
moving over assistants stuff
gphorvath Apr 19, 2024
ebea2d4
adding in some files endpoints stuff
gphorvath Apr 22, 2024
d06166b
Addressing formatting issues
gphorvath Apr 22, 2024
8a0240d
add stubs for vector store
gphorvath Apr 22, 2024
ce14cdf
add threads stubs
gphorvath Apr 22, 2024
2f5cc51
stub messages and vector store files
gphorvath Apr 22, 2024
1e816fd
fix openai typing refs
gphorvath Apr 22, 2024
ef39d18
stubbing out runs and runs steps
gphorvath Apr 22, 2024
33f4d33
Merge branch 'main' into 292-featapi-assistants-endpoints
gphorvath Apr 22, 2024
bf2cb8c
typing issues, and removing tests
gphorvath Apr 22, 2024
c6d49f7
only include endpoints if Supabase is available
gphorvath Apr 22, 2024
b691ce2
fix formatting issue
gphorvath Apr 22, 2024
a94cdcb
more robustly handle missing supabase
gphorvath Apr 22, 2024
b914c74
refactor supabase_wrapper
gphorvath Apr 22, 2024
51ba2fd
snazzy tests
gphorvath Apr 22, 2024
3ea2ef3
refactor Supabase Wrapper
gphorvath Apr 23, 2024
1946e3c
Merge branch 'main' into 292-featapi-assistants-endpoints
gphorvath Apr 23, 2024
6011e59
remove supabase-jwt secret (for now)
gphorvath Apr 23, 2024
80173b2
update stubs to async defs
gphorvath Apr 24, 2024
a4bed09
pull in main
gphorvath Apr 24, 2024
b2cd0bc
Merge branch 'main' of github.com:defenseunicorns/leapfrogai into 292…
gphorvath Apr 30, 2024
d3dda52
revert overarching pyproject.toml
gphorvath Apr 30, 2024
30ae75f
resolving more conflicts from main
gphorvath Apr 30, 2024
7fb9c23
working on endpoints and migrations
gphorvath Apr 30, 2024
d4933fa
move to async supabase client
gphorvath Apr 30, 2024
f14149e
[WIP] refactor assistants and files
gphorvath Apr 30, 2024
c3c098f
remove redundant /
gphorvath Apr 30, 2024
cae2e02
[WIP] CRUD for Threads/Messages
gphorvath Apr 30, 2024
385ec34
Merge branch 'main' into 292-featapi-assistants-endpoints
gphorvath May 1, 2024
598151c
finishing up files
gphorvath May 1, 2024
9e8358e
remove unused utils
gphorvath May 1, 2024
0617189
Merge branch 'main' of github.com:defenseunicorns/leapfrogai into 292…
gphorvath May 1, 2024
57a4203
removing threads/messages stuff (future PR)
gphorvath May 1, 2024
f20c3d4
remove env.example
gphorvath May 1, 2024
42f18e4
remove Supabase deployment variables (future PR)
gphorvath May 1, 2024
1226062
ironing out remaining issues with assistant crud
gphorvath May 1, 2024
ebf1ef2
update test to include assistants
gphorvath May 1, 2024
08605f4
add a comment about the uuid
gphorvath May 1, 2024
66db8aa
thank god for linters...
gphorvath May 1, 2024
762a5dc
add UUID4 type validation to endpoints
gphorvath May 1, 2024
503f4f3
adding back tests, but they still require Supabase connection
gphorvath May 1, 2024
6758a3f
working on integration tests
gphorvath May 2, 2024
31de889
improving integration tests
gphorvath May 2, 2024
82a7d4a
documenting integration tests
gphorvath May 2, 2024
c1dc712
tweaking some naming and comments
gphorvath May 3, 2024
8ac7f64
fixing list files and list assistants endpoints
gphorvath May 6, 2024
32898ba
update tests
gphorvath May 6, 2024
7f51af9
improve list type hints and modify assistants
gphorvath May 6, 2024
ad27579
handle errors in list endpoints
gphorvath May 6, 2024
e48d779
amp up test messages
gphorvath May 6, 2024
27d6b71
amp up test messages
gphorvath May 6, 2024
1156d3f
Merge branch 'main' into 292-featapi-assistants-endpoints
gphorvath May 6, 2024
f9cbe52
update tool validation
gphorvath May 9, 2024
309e8c8
update return type
gphorvath May 9, 2024
665b9d3
update return type
gphorvath May 9, 2024
8975705
update return type
gphorvath May 9, 2024
9c4d013
fix validate tools typed dict
gphorvath May 9, 2024
a2abd6c
Merge branch 'main' into 292-featapi-assistants-endpoints
gphorvath May 9, 2024
60b1fa0
Merge branch 'main' into 292-featapi-assistants-endpoints
gphorvath May 15, 2024
ca3c3ff
Merge branch 'main' into 292-featapi-assistants-endpoints
gphorvath May 15, 2024
74b4425
address return ListFilesResponse instead of none
gphorvath May 15, 2024
50fca73
add a reference in README/Integration Tests to README/Local Development
gphorvath May 15, 2024
15a8aef
fixed typo
gphorvath May 15, 2024
11ab054
add comment to CRUD operations about why we delete the id field
gphorvath May 15, 2024
ce01502
ignore config.yaml s
gphorvath May 15, 2024
e9b8818
beefing up api endpoint tests
gphorvath May 15, 2024
a1ec9d4
break up assistants tests
gphorvath May 15, 2024
38ddc01
update files tests
gphorvath May 16, 2024
650031a
Merge branch 'main' into 292-featapi-assistants-endpoints
gphorvath May 16, 2024
e668b9e
Merge branch '292-featapi-assistants-endpoints' of github.com:defense…
gphorvath May 16, 2024
2a903a6
updating status codes
gphorvath May 16, 2024
29d4234
refactoring a bit to remove code duplication
gphorvath May 16, 2024
1f5c651
a couple schema tweaks
gphorvath May 16, 2024
1b4303e
Merge branch 'main' into 292-featapi-assistants-endpoints
gphorvath May 16, 2024
99619ae
fix some shindig int he file upload endpoint
gphorvath May 16, 2024
0f53ffe
Merge branch '292-featapi-assistants-endpoints' of github.com:defense…
gphorvath May 16, 2024
15a5519
update modify assistant to clarify what can be modified
gphorvath May 16, 2024
08f9a9b
need to create file_object before adding to bucket so we have file_id
gphorvath May 16, 2024
0e592d5
Adds supabase env variables to api deployment
CollectiveUnicorn May 16, 2024
d57451e
fix a couple minor issues
gphorvath May 17, 2024
8294ece
fixes the ninja Aleks error
gphorvath May 17, 2024
c1ac5f8
removed the openai utilpre-commitpre-commitpre-commit!
gphorvath May 17, 2024
f50ada9
changing config.yaml ignore to be specific to API (there is one in pa…
gphorvath May 20, 2024
b4f758a
Merge branch 'main' into 292-featapi-assistants-endpoints
gphorvath May 20, 2024
933d086
Merge branch 'main' into 292-featapi-assistants-endpoints
gphorvath May 20, 2024
eac365e
addressing some comments
gphorvath May 20, 2024
08e9182
addressed the last comment
gphorvath May 20, 2024
3ee46d5
missed some lint
gphorvath May 20, 2024
97852bf
Merge branch 'main' into 292-featapi-assistants-endpoints
gphorvath May 20, 2024
19ee1fe
addressed comment
gphorvath May 20, 2024
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ build/
.ruff_cache
.branches
.temp
config.yaml
gphorvath marked this conversation as resolved.
Show resolved Hide resolved

# local model and tokenizer files
*.bin
Expand Down
8 changes: 8 additions & 0 deletions packages/api/chart/templates/api/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ spec:
value: "*.toml"
- name: PORT
value: "{{ .Values.api.port }}"
- name: SUPABASE_URL
value: "{{ .Values.supabase.url }}"
- name: SUPABASE_SERVICE_KEY
valueFrom:
secretKeyRef:
name: supabase-bootstrap-jwt
key: service-key
optional: true
ports:
- containerPort: 8080
livenessProbe:
Expand Down
3 changes: 3 additions & 0 deletions packages/api/chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ image:
# x-release-please-end
kiwigridTag: 1.23.3

supabase:
url: "https://supabase-kong.###ZARF_VAR_HOSTED_DOMAIN###"

api:
replicas: 1
port: 8080
Expand Down
2 changes: 2 additions & 0 deletions packages/api/zarf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ variables:
- name: EXPOSE_OPENAPI_SCHEMA
default: "false"
description: "Flag to expose the OpenAPI schema for debugging."
- name: HOSTED_DOMAIN
default: "uds.dev"

components:
- name: leapfrogai
Expand Down
3 changes: 3 additions & 0 deletions src/leapfrogai_api/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ install:
dev:
make install
python -m uvicorn main:app --port 3000 --reload

test-integration:
cd ../../ && python -m pytest tests/integration/api
gphorvath marked this conversation as resolved.
Show resolved Hide resolved
19 changes: 18 additions & 1 deletion src/leapfrogai_api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,21 @@ 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
```
```

## Integration Tests

The integration tests serve to identify any mismatches between components:

- Check all API routes
- Validate Request/Response types
- DB CRUD operations
- Schema mismatches

Integration tests require a Supabase instance and environment variables configured (see [Local Development](#local-development)).

From this directory run:

``` bash
make test-integration
```
23 changes: 19 additions & 4 deletions src/leapfrogai_api/backend/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
from typing import Literal
from pydantic import BaseModel
from fastapi import UploadFile, Form, File
from openai.types import FileObject
from openai.types.beta import Assistant, AssistantTool
from openai.types.beta.assistant import ToolResources

##########
# GENERIC
Expand Down Expand Up @@ -229,6 +232,13 @@ def as_form(
return cls(file=file, purpose=purpose)


class ListFilesResponse(BaseModel):
"""Response object for listing files."""

object: str = Literal["list"]
data: list[FileObject] = []


#############
# ASSISTANTS
#############
Expand All @@ -241,10 +251,8 @@ class CreateAssistantRequest(BaseModel):
name: str | None = "Froggy Assistant"
description: str | None = "A helpful assistant."
instructions: str | None = "You are a helpful assistant."
tools: list[dict[Literal["type"], Literal["file_search"]]] | None = [
{"type": "file_search"}
] # This is all we support right now
tool_resources: object | None = {}
tools: list[AssistantTool] | None = [] # This is all we support right now
tool_resources: ToolResources | None = ToolResources()
metadata: object | None = {}
temperature: float | None = 1.0
top_p: float | None = 1.0
Expand All @@ -253,3 +261,10 @@ class CreateAssistantRequest(BaseModel):

class ModifyAssistantRequest(CreateAssistantRequest):
"""Request object for modifying an assistant."""


class ListAssistantsResponse(BaseModel):
"""Response object for listing files."""

object: str = Literal["list"]
data: list[Assistant] = []
34 changes: 34 additions & 0 deletions src/leapfrogai_api/data/crud_assistant_object.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""CRUD Operations for Assistant."""

from openai.types.beta import Assistant
from supabase_py_async import AsyncClient
from leapfrogai_api.data.crud_base import CRUDBase


class CRUDAssistant(CRUDBase[Assistant]):
"""CRUD Operations for Assistant"""

def __init__(self, model: type[Assistant], table_name: str = "assistant_objects"):
super().__init__(model=model, table_name=table_name)

async def create(self, db: AsyncClient, object_: Assistant) -> Assistant | None:
"""Create a new assistant."""
return await super().create(db=db, object_=object_)

async def get(self, id_: str, db: AsyncClient) -> Assistant | None:
"""Get an assistant by its ID."""
return await super().get(db=db, id_=id_)

async def list(self, db: AsyncClient) -> list[Assistant] | None:
"""List all assistants."""
return await super().list(db=db)

async def update(
self, id_: str, db: AsyncClient, object_: Assistant
) -> Assistant | None:
"""Update an assistant by its ID."""
return await super().update(id_=id_, db=db, object_=object_)

async def delete(self, id_: str, db: AsyncClient) -> bool:
"""Delete an assistant by its ID."""
return await super().delete(id_=id_, db=db)
75 changes: 75 additions & 0 deletions src/leapfrogai_api/data/crud_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""CRUD Operations for VectorStore."""

from typing import Generic, TypeVar
from supabase_py_async import AsyncClient
from pydantic import BaseModel

ModelType = TypeVar("ModelType", bound=BaseModel)


class CRUDBase(Generic[ModelType]):
"""CRUD Operations"""

def __init__(self, model: type[ModelType], table_name: str):
self.model = model
self.table_name = table_name

async def create(self, db: AsyncClient, object_: ModelType) -> ModelType | None:
"""Create new row."""
dict_ = object_.model_dump()
del dict_["id"] # Ensure this is created by the database
del dict_["created_at"] # Ensure this is created by the database
data, _count = await db.table(self.table_name).insert(dict_).execute()

_, response = data

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

async def get(self, id_: str, db: AsyncClient) -> ModelType | None:
"""Get row by ID."""
data, _count = (
await db.table(self.table_name).select("*").eq("id", id_).execute()
)

_, response = data

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

async def list(self, db: AsyncClient) -> list[ModelType] | None:
"""List all rows."""
data, _count = await db.table(self.table_name).select("*").execute()

_, response = data

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

async def update(
self, id_: str, db: AsyncClient, object_: ModelType
) -> ModelType | None:
"""Update a vector store by its ID."""
data, _count = (
await db.table(self.table_name)
.update(object_.model_dump())
.eq("id", id_)
.execute()
)

_, response = data

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

async def delete(self, id_: str, db: AsyncClient) -> bool:
"""Delete a vector store by its ID."""
data, _count = await db.table(self.table_name).delete().eq("id", id_).execute()

_, response = data

return bool(response)
73 changes: 15 additions & 58 deletions src/leapfrogai_api/data/crud_file_object.py
Original file line number Diff line number Diff line change
@@ -1,77 +1,34 @@
"""CRUD Operations for FileObject"""

from openai.types import FileObject
from supabase_py_async import AsyncClient
from openai.types import FileObject, FileDeleted
from leapfrogai_api.data.crud_base import CRUDBase


class CRUDFileObject:
class CRUDFileObject(CRUDBase[FileObject]):
"""CRUD Operations for FileObject"""

def __init__(self, model: type[FileObject]):
self.model = model
def __init__(self, model: type[FileObject], table_name: str = "file_objects"):
super().__init__(model=model, table_name=table_name)

async def create(
self, client: AsyncClient, file_object: FileObject
) -> FileObject | None:
async def create(self, db: AsyncClient, 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
return await super().create(db=db, object_=object_)

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

_, response = data

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

async def list(self, client: AsyncClient) -> list[FileObject] | None:
async def list(self, db: 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
return await super().list(db=db)

async def update(
self, client: AsyncClient, file_id: str, file_object: FileObject
self, id_: str, db: AsyncClient, 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
return await super().update(id_=id_, db=db, object_=object_)

async def delete(self, client: AsyncClient, file_id: str) -> FileDeleted:
async def delete(self, id_: str, db: AsyncClient) -> bool:
"""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")
return await super().delete(id_=id_, db=db)
Loading