From 24d9074d94ba3b77ab906805620053e09af96419 Mon Sep 17 00:00:00 2001 From: alekst23 Date: Wed, 28 Aug 2024 17:15:54 -0400 Subject: [PATCH 01/17] chore: New 'types' module Migrate Request types for run.create methods to new types module --- src/leapfrogai_api/backend/types.py | 9 +---- src/leapfrogai_api/data/crud_api_key.py | 2 +- src/leapfrogai_api/routers/leapfrogai/auth.py | 39 +++---------------- .../thread_run_create_params_request.py | 2 +- src/leapfrogai_api/routers/openai/runs.py | 2 +- src/leapfrogai_api/types/__init__.py | 1 + src/leapfrogai_api/types/constants.py | 6 +++ src/leapfrogai_api/types/model_types.py | 10 +++++ src/leapfrogai_api/types/request/__init__.py | 8 ++++ .../types/request/auth_types.py | 36 +++++++++++++++++ .../request/run_create_params.py} | 2 +- .../request/run_create_params_base.py} | 6 ++- src/leapfrogai_api/utils/config.py | 13 +------ tests/integration/api/test_runs.py | 3 +- 14 files changed, 79 insertions(+), 60 deletions(-) create mode 100644 src/leapfrogai_api/types/__init__.py create mode 100644 src/leapfrogai_api/types/constants.py create mode 100644 src/leapfrogai_api/types/model_types.py create mode 100644 src/leapfrogai_api/types/request/__init__.py create mode 100644 src/leapfrogai_api/types/request/auth_types.py rename src/leapfrogai_api/{routers/openai/requests/run_create_params_request.py => types/request/run_create_params.py} (98%) rename src/leapfrogai_api/{routers/openai/requests/run_create_params_request_base.py => types/request/run_create_params_base.py} (99%) diff --git a/src/leapfrogai_api/backend/types.py b/src/leapfrogai_api/backend/types.py index 59011003c..b2e0beb78 100644 --- a/src/leapfrogai_api/backend/types.py +++ b/src/leapfrogai_api/backend/types.py @@ -20,14 +20,7 @@ from openai.types.beta.vector_store import ExpiresAfter from pydantic import BaseModel, Field -########## -# DEFAULTS -########## - - -DEFAULT_MAX_COMPLETION_TOKENS = 4096 -DEFAULT_MAX_PROMPT_TOKENS = 4096 - +from leapfrogai_api.types.constants import DEFAULT_MAX_COMPLETION_TOKENS ########## # GENERIC diff --git a/src/leapfrogai_api/data/crud_api_key.py b/src/leapfrogai_api/data/crud_api_key.py index eea3718c8..24c90d99c 100644 --- a/src/leapfrogai_api/data/crud_api_key.py +++ b/src/leapfrogai_api/data/crud_api_key.py @@ -7,7 +7,7 @@ from leapfrogai_api.data.crud_base import CRUDBase from leapfrogai_api.backend.security.api_key import APIKey, KEY_PREFIX -THIRTY_DAYS = 60 * 60 * 24 * 30 # in seconds +from leapfrogai_api.types.constants import THIRTY_DAYS class APIKeyItem(BaseModel): diff --git a/src/leapfrogai_api/routers/leapfrogai/auth.py b/src/leapfrogai_api/routers/leapfrogai/auth.py index 897f23a8b..69fc80fbc 100644 --- a/src/leapfrogai_api/routers/leapfrogai/auth.py +++ b/src/leapfrogai_api/routers/leapfrogai/auth.py @@ -3,43 +3,14 @@ import time from typing import Annotated from fastapi import APIRouter, HTTPException, status -from pydantic import BaseModel, Field -from leapfrogai_api.routers.supabase_session import Session -from leapfrogai_api.data.crud_api_key import APIKeyItem, CRUDAPIKey, THIRTY_DAYS - -router = APIRouter(prefix="/leapfrogai/v1/auth", tags=["leapfrogai/auth"]) - - -class CreateAPIKeyRequest(BaseModel): - """Request body for creating an API key.""" - - name: str | None = Field( - default=None, - description="The name of the API key.", - examples=["API Key 1"], - ) - - expires_at: int = Field( - default=int(time.time()) + THIRTY_DAYS, - description="The time at which the API key expires, in seconds since the Unix epoch.", - examples=[int(time.time()) + THIRTY_DAYS], - ) +from pydantic import Field +from leapfrogai_api.routers.supabase_session import Session +from leapfrogai_api.data.crud_api_key import APIKeyItem, CRUDAPIKey +from leapfrogai_api.types.request import CreateAPIKeyRequest, ModifyAPIKeyRequest -class ModifyAPIKeyRequest(BaseModel): - """Request body for modifying an API key.""" - name: str | None = Field( - default=None, - description="The name of the API key. If not provided, the name will not be changed.", - examples=["API Key 1"], - ) - - expires_at: int | None = Field( - default=None, - description="The time at which the API key expires, in seconds since the Unix epoch. If not provided, the expiration time will not be changed.", - examples=[int(time.time()) + THIRTY_DAYS], - ) +router = APIRouter(prefix="/leapfrogai/v1/auth", tags=["leapfrogai/auth"]) @router.post("/api-keys") diff --git a/src/leapfrogai_api/routers/openai/requests/thread_run_create_params_request.py b/src/leapfrogai_api/routers/openai/requests/thread_run_create_params_request.py index eb38b8406..6d6a32d5d 100644 --- a/src/leapfrogai_api/routers/openai/requests/thread_run_create_params_request.py +++ b/src/leapfrogai_api/routers/openai/requests/thread_run_create_params_request.py @@ -24,7 +24,7 @@ from leapfrogai_api.routers.openai.requests.create_message_request import ( CreateMessageRequest, ) -from leapfrogai_api.routers.openai.requests.run_create_params_request_base import ( +from leapfrogai_api.types.request import ( RunCreateParamsRequestBase, ) from leapfrogai_api.data.crud_run import CRUDRun diff --git a/src/leapfrogai_api/routers/openai/runs.py b/src/leapfrogai_api/routers/openai/runs.py index 9a4835115..2f8fc2c9f 100644 --- a/src/leapfrogai_api/routers/openai/runs.py +++ b/src/leapfrogai_api/routers/openai/runs.py @@ -11,7 +11,7 @@ from leapfrogai_api.routers.openai.requests.thread_run_create_params_request import ( ThreadRunCreateParamsRequest, ) -from leapfrogai_api.routers.openai.requests.run_create_params_request import ( +from leapfrogai_api.types.request import ( RunCreateParamsRequest, ) from leapfrogai_api.data.crud_run import CRUDRun diff --git a/src/leapfrogai_api/types/__init__.py b/src/leapfrogai_api/types/__init__.py new file mode 100644 index 000000000..6a0b5f8a1 --- /dev/null +++ b/src/leapfrogai_api/types/__init__.py @@ -0,0 +1 @@ +from model_types import Model as Model diff --git a/src/leapfrogai_api/types/constants.py b/src/leapfrogai_api/types/constants.py new file mode 100644 index 000000000..6a6340152 --- /dev/null +++ b/src/leapfrogai_api/types/constants.py @@ -0,0 +1,6 @@ +# This file defines application constants not set by environment variables or configuration files. + +THIRTY_DAYS = 60 * 60 * 24 * 30 # in seconds + +DEFAULT_MAX_COMPLETION_TOKENS = 4096 +DEFAULT_MAX_PROMPT_TOKENS = 4096 diff --git a/src/leapfrogai_api/types/model_types.py b/src/leapfrogai_api/types/model_types.py new file mode 100644 index 000000000..c4f5519b4 --- /dev/null +++ b/src/leapfrogai_api/types/model_types.py @@ -0,0 +1,10 @@ +from typing import List + + +class Model: + name: str + backend: str + + def __init__(self, name: str, backend: str, capabilities: List[str] | None = None): + self.name = name + self.backend = backend diff --git a/src/leapfrogai_api/types/request/__init__.py b/src/leapfrogai_api/types/request/__init__.py new file mode 100644 index 000000000..315bfe1b6 --- /dev/null +++ b/src/leapfrogai_api/types/request/__init__.py @@ -0,0 +1,8 @@ +from auth_types import ( + CreateAPIKeyRequest as CreateAPIKeyRequest, + ModifyAPIKeyRequest as ModifyAPIKeyRequest, +) +from run_create_params_base import ( + RunCreateParamsRequestBase as RunCreateParamsRequestBase, +) +from run_create_params import RunCreateParamsRequest as RunCreateParamsRequest diff --git a/src/leapfrogai_api/types/request/auth_types.py b/src/leapfrogai_api/types/request/auth_types.py new file mode 100644 index 000000000..ce21a1601 --- /dev/null +++ b/src/leapfrogai_api/types/request/auth_types.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel, Field +import time + +from leapfrogai_api.types.constants import THIRTY_DAYS + + +class CreateAPIKeyRequest(BaseModel): + """Request body for creating an API key.""" + + name: str | None = Field( + default=None, + description="The name of the API key.", + examples=["API Key 1"], + ) + + expires_at: int = Field( + default=int(time.time()) + THIRTY_DAYS, + description="The time at which the API key expires, in seconds since the Unix epoch.", + examples=[int(time.time()) + THIRTY_DAYS], + ) + + +class ModifyAPIKeyRequest(BaseModel): + """Request body for modifying an API key.""" + + name: str | None = Field( + default=None, + description="The name of the API key. If not provided, the name will not be changed.", + examples=["API Key 1"], + ) + + expires_at: int | None = Field( + default=None, + description="The time at which the API key expires, in seconds since the Unix epoch. If not provided, the expiration time will not be changed.", + examples=[int(time.time()) + THIRTY_DAYS], + ) diff --git a/src/leapfrogai_api/routers/openai/requests/run_create_params_request.py b/src/leapfrogai_api/types/request/run_create_params.py similarity index 98% rename from src/leapfrogai_api/routers/openai/requests/run_create_params_request.py rename to src/leapfrogai_api/types/request/run_create_params.py index 2ea23555b..f15f51d6a 100644 --- a/src/leapfrogai_api/routers/openai/requests/run_create_params_request.py +++ b/src/leapfrogai_api/types/request/run_create_params.py @@ -10,7 +10,7 @@ ) from pydantic import Field from starlette.responses import StreamingResponse -from leapfrogai_api.routers.openai.requests.run_create_params_request_base import ( +from leapfrogai_api.types.request.run_create_params_base import ( RunCreateParamsRequestBase, ) from leapfrogai_api.routers.openai.requests.create_message_request import ( diff --git a/src/leapfrogai_api/routers/openai/requests/run_create_params_request_base.py b/src/leapfrogai_api/types/request/run_create_params_base.py similarity index 99% rename from src/leapfrogai_api/routers/openai/requests/run_create_params_request_base.py rename to src/leapfrogai_api/types/request/run_create_params_base.py index cb552bc29..c422c480e 100644 --- a/src/leapfrogai_api/routers/openai/requests/run_create_params_request_base.py +++ b/src/leapfrogai_api/types/request/run_create_params_base.py @@ -42,6 +42,10 @@ from postgrest.base_request_builder import SingleAPIResponse from pydantic import BaseModel, Field, ValidationError +from leapfrogai_api.types.constants import ( + DEFAULT_MAX_COMPLETION_TOKENS, + DEFAULT_MAX_PROMPT_TOKENS, +) from leapfrogai_api.backend.converters import ( from_assistant_stream_event_to_str, from_text_to_message, @@ -54,8 +58,6 @@ ChatCompletionResponse, ChatCompletionRequest, ChatChoice, - DEFAULT_MAX_COMPLETION_TOKENS, - DEFAULT_MAX_PROMPT_TOKENS, ) from leapfrogai_api.data.crud_assistant import CRUDAssistant, FilterAssistant from leapfrogai_api.data.crud_message import CRUDMessage diff --git a/src/leapfrogai_api/utils/config.py b/src/leapfrogai_api/utils/config.py index 60edfb3e8..598e0b575 100644 --- a/src/leapfrogai_api/utils/config.py +++ b/src/leapfrogai_api/utils/config.py @@ -2,23 +2,14 @@ import glob import logging import os -from typing import List - import toml import yaml from watchfiles import Change, awatch - -logger = logging.getLogger(__name__) +from leapfrogai_api.types import Model -class Model: - name: str - backend: str - - def __init__(self, name: str, backend: str, capabilities: List[str] | None = None): - self.name = name - self.backend = backend +logger = logging.getLogger(__name__) class Config: diff --git a/tests/integration/api/test_runs.py b/tests/integration/api/test_runs.py index 1ad4ef64b..c42d3fece 100644 --- a/tests/integration/api/test_runs.py +++ b/tests/integration/api/test_runs.py @@ -7,6 +7,7 @@ from openai.types.beta import Assistant, Thread, AssistantDeleted, ThreadDeleted from openai.types.beta.thread import ToolResources, ToolResourcesFileSearch from openai.types.beta.threads import Message, Text, TextContentBlock, Run + from leapfrogai_api.main import app from leapfrogai_api.routers.openai.requests.create_modify_assistant_request import ( CreateAssistantRequest, @@ -17,7 +18,7 @@ from leapfrogai_api.routers.openai.requests.create_thread_request import ( CreateThreadRequest, ) -from leapfrogai_api.routers.openai.requests.run_create_params_request import ( +from leapfrogai_api.types.request import ( RunCreateParamsRequest, ) from leapfrogai_api.routers.openai.requests.thread_run_create_params_request import ( From e1fa5dfaa79cef63c47d3e3599660ed227a6de05 Mon Sep 17 00:00:00 2001 From: alekst23 Date: Thu, 29 Aug 2024 12:11:41 -0400 Subject: [PATCH 02/17] Moving from `types` to `typedef` due to overloaded module name --- src/leapfrogai_api/backend/types.py | 2 +- src/leapfrogai_api/data/crud_api_key.py | 2 +- src/leapfrogai_api/routers/leapfrogai/auth.py | 2 +- .../openai/requests/thread_run_create_params_request.py | 2 +- src/leapfrogai_api/routers/openai/runs.py | 2 +- src/leapfrogai_api/typedef/__init__.py | 1 + src/leapfrogai_api/{types => typedef}/constants.py | 0 src/leapfrogai_api/{types => typedef}/model_types.py | 0 src/leapfrogai_api/{types => typedef}/request/__init__.py | 6 +++--- src/leapfrogai_api/{types => typedef}/request/auth_types.py | 2 +- .../{types => typedef}/request/run_create_params.py | 2 +- .../{types => typedef}/request/run_create_params_base.py | 2 +- src/leapfrogai_api/types/__init__.py | 1 - src/leapfrogai_api/utils/config.py | 2 +- tests/integration/api/test_runs.py | 2 +- 15 files changed, 14 insertions(+), 14 deletions(-) create mode 100644 src/leapfrogai_api/typedef/__init__.py rename src/leapfrogai_api/{types => typedef}/constants.py (100%) rename src/leapfrogai_api/{types => typedef}/model_types.py (100%) rename src/leapfrogai_api/{types => typedef}/request/__init__.py (52%) rename src/leapfrogai_api/{types => typedef}/request/auth_types.py (94%) rename src/leapfrogai_api/{types => typedef}/request/run_create_params.py (98%) rename src/leapfrogai_api/{types => typedef}/request/run_create_params_base.py (99%) delete mode 100644 src/leapfrogai_api/types/__init__.py diff --git a/src/leapfrogai_api/backend/types.py b/src/leapfrogai_api/backend/types.py index b2e0beb78..0520283d2 100644 --- a/src/leapfrogai_api/backend/types.py +++ b/src/leapfrogai_api/backend/types.py @@ -20,7 +20,7 @@ from openai.types.beta.vector_store import ExpiresAfter from pydantic import BaseModel, Field -from leapfrogai_api.types.constants import DEFAULT_MAX_COMPLETION_TOKENS +from leapfrogai_api.typedef.constants import DEFAULT_MAX_COMPLETION_TOKENS ########## # GENERIC diff --git a/src/leapfrogai_api/data/crud_api_key.py b/src/leapfrogai_api/data/crud_api_key.py index 24c90d99c..a460e8553 100644 --- a/src/leapfrogai_api/data/crud_api_key.py +++ b/src/leapfrogai_api/data/crud_api_key.py @@ -7,7 +7,7 @@ from leapfrogai_api.data.crud_base import CRUDBase from leapfrogai_api.backend.security.api_key import APIKey, KEY_PREFIX -from leapfrogai_api.types.constants import THIRTY_DAYS +from leapfrogai_api.typedef.constants import THIRTY_DAYS class APIKeyItem(BaseModel): diff --git a/src/leapfrogai_api/routers/leapfrogai/auth.py b/src/leapfrogai_api/routers/leapfrogai/auth.py index 69fc80fbc..3e5b406ca 100644 --- a/src/leapfrogai_api/routers/leapfrogai/auth.py +++ b/src/leapfrogai_api/routers/leapfrogai/auth.py @@ -7,7 +7,7 @@ from leapfrogai_api.routers.supabase_session import Session from leapfrogai_api.data.crud_api_key import APIKeyItem, CRUDAPIKey -from leapfrogai_api.types.request import CreateAPIKeyRequest, ModifyAPIKeyRequest +from leapfrogai_api.typedef.request import CreateAPIKeyRequest, ModifyAPIKeyRequest router = APIRouter(prefix="/leapfrogai/v1/auth", tags=["leapfrogai/auth"]) diff --git a/src/leapfrogai_api/routers/openai/requests/thread_run_create_params_request.py b/src/leapfrogai_api/routers/openai/requests/thread_run_create_params_request.py index 6d6a32d5d..af70d1df6 100644 --- a/src/leapfrogai_api/routers/openai/requests/thread_run_create_params_request.py +++ b/src/leapfrogai_api/routers/openai/requests/thread_run_create_params_request.py @@ -24,7 +24,7 @@ from leapfrogai_api.routers.openai.requests.create_message_request import ( CreateMessageRequest, ) -from leapfrogai_api.types.request import ( +from leapfrogai_api.typedef.request import ( RunCreateParamsRequestBase, ) from leapfrogai_api.data.crud_run import CRUDRun diff --git a/src/leapfrogai_api/routers/openai/runs.py b/src/leapfrogai_api/routers/openai/runs.py index 2f8fc2c9f..5249e6ad2 100644 --- a/src/leapfrogai_api/routers/openai/runs.py +++ b/src/leapfrogai_api/routers/openai/runs.py @@ -11,7 +11,7 @@ from leapfrogai_api.routers.openai.requests.thread_run_create_params_request import ( ThreadRunCreateParamsRequest, ) -from leapfrogai_api.types.request import ( +from leapfrogai_api.typedef.request import ( RunCreateParamsRequest, ) from leapfrogai_api.data.crud_run import CRUDRun diff --git a/src/leapfrogai_api/typedef/__init__.py b/src/leapfrogai_api/typedef/__init__.py new file mode 100644 index 000000000..27dbd81b5 --- /dev/null +++ b/src/leapfrogai_api/typedef/__init__.py @@ -0,0 +1 @@ +from .model_types import Model as Model diff --git a/src/leapfrogai_api/types/constants.py b/src/leapfrogai_api/typedef/constants.py similarity index 100% rename from src/leapfrogai_api/types/constants.py rename to src/leapfrogai_api/typedef/constants.py diff --git a/src/leapfrogai_api/types/model_types.py b/src/leapfrogai_api/typedef/model_types.py similarity index 100% rename from src/leapfrogai_api/types/model_types.py rename to src/leapfrogai_api/typedef/model_types.py diff --git a/src/leapfrogai_api/types/request/__init__.py b/src/leapfrogai_api/typedef/request/__init__.py similarity index 52% rename from src/leapfrogai_api/types/request/__init__.py rename to src/leapfrogai_api/typedef/request/__init__.py index 315bfe1b6..75e30f7e0 100644 --- a/src/leapfrogai_api/types/request/__init__.py +++ b/src/leapfrogai_api/typedef/request/__init__.py @@ -1,8 +1,8 @@ -from auth_types import ( +from .auth_types import ( CreateAPIKeyRequest as CreateAPIKeyRequest, ModifyAPIKeyRequest as ModifyAPIKeyRequest, ) -from run_create_params_base import ( +from .run_create_params_base import ( RunCreateParamsRequestBase as RunCreateParamsRequestBase, ) -from run_create_params import RunCreateParamsRequest as RunCreateParamsRequest +from .run_create_params import RunCreateParamsRequest as RunCreateParamsRequest diff --git a/src/leapfrogai_api/types/request/auth_types.py b/src/leapfrogai_api/typedef/request/auth_types.py similarity index 94% rename from src/leapfrogai_api/types/request/auth_types.py rename to src/leapfrogai_api/typedef/request/auth_types.py index ce21a1601..f3f335867 100644 --- a/src/leapfrogai_api/types/request/auth_types.py +++ b/src/leapfrogai_api/typedef/request/auth_types.py @@ -1,7 +1,7 @@ from pydantic import BaseModel, Field import time -from leapfrogai_api.types.constants import THIRTY_DAYS +from leapfrogai_api.typedef.constants import THIRTY_DAYS class CreateAPIKeyRequest(BaseModel): diff --git a/src/leapfrogai_api/types/request/run_create_params.py b/src/leapfrogai_api/typedef/request/run_create_params.py similarity index 98% rename from src/leapfrogai_api/types/request/run_create_params.py rename to src/leapfrogai_api/typedef/request/run_create_params.py index f15f51d6a..9963a9e4a 100644 --- a/src/leapfrogai_api/types/request/run_create_params.py +++ b/src/leapfrogai_api/typedef/request/run_create_params.py @@ -10,7 +10,7 @@ ) from pydantic import Field from starlette.responses import StreamingResponse -from leapfrogai_api.types.request.run_create_params_base import ( +from leapfrogai_api.typedef.request.run_create_params_base import ( RunCreateParamsRequestBase, ) from leapfrogai_api.routers.openai.requests.create_message_request import ( diff --git a/src/leapfrogai_api/types/request/run_create_params_base.py b/src/leapfrogai_api/typedef/request/run_create_params_base.py similarity index 99% rename from src/leapfrogai_api/types/request/run_create_params_base.py rename to src/leapfrogai_api/typedef/request/run_create_params_base.py index c422c480e..fb518e7e5 100644 --- a/src/leapfrogai_api/types/request/run_create_params_base.py +++ b/src/leapfrogai_api/typedef/request/run_create_params_base.py @@ -42,7 +42,7 @@ from postgrest.base_request_builder import SingleAPIResponse from pydantic import BaseModel, Field, ValidationError -from leapfrogai_api.types.constants import ( +from leapfrogai_api.typedef.constants import ( DEFAULT_MAX_COMPLETION_TOKENS, DEFAULT_MAX_PROMPT_TOKENS, ) diff --git a/src/leapfrogai_api/types/__init__.py b/src/leapfrogai_api/types/__init__.py deleted file mode 100644 index 6a0b5f8a1..000000000 --- a/src/leapfrogai_api/types/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from model_types import Model as Model diff --git a/src/leapfrogai_api/utils/config.py b/src/leapfrogai_api/utils/config.py index 598e0b575..f7d6b9d47 100644 --- a/src/leapfrogai_api/utils/config.py +++ b/src/leapfrogai_api/utils/config.py @@ -6,7 +6,7 @@ import yaml from watchfiles import Change, awatch -from leapfrogai_api.types import Model +from leapfrogai_api.typedef import Model logger = logging.getLogger(__name__) diff --git a/tests/integration/api/test_runs.py b/tests/integration/api/test_runs.py index c42d3fece..1d88ec7c3 100644 --- a/tests/integration/api/test_runs.py +++ b/tests/integration/api/test_runs.py @@ -18,7 +18,7 @@ from leapfrogai_api.routers.openai.requests.create_thread_request import ( CreateThreadRequest, ) -from leapfrogai_api.types.request import ( +from leapfrogai_api.typedef.request import ( RunCreateParamsRequest, ) from leapfrogai_api.routers.openai.requests.thread_run_create_params_request import ( From 912289314591c5938632d217a7cd55d5f01437b9 Mon Sep 17 00:00:00 2001 From: alekst23 Date: Thu, 29 Aug 2024 15:43:17 -0400 Subject: [PATCH 03/17] fix: resolve circular dependency in thread runs --- src/leapfrogai_api/backend/grpc_client.py | 4 +- src/leapfrogai_api/backend/rag/index.py | 6 +- src/leapfrogai_api/backend/thread_runner.py | 398 ++++++++++++++++++ src/leapfrogai_api/backend/types.py | 131 ------ .../routers/openai/assistants.py | 2 +- src/leapfrogai_api/routers/openai/audio.py | 2 +- src/leapfrogai_api/routers/openai/runs.py | 17 +- src/leapfrogai_api/typedef/__init__.py | 1 + src/leapfrogai_api/typedef/assistant_types.py | 13 + .../typedef/request/__init__.py | 7 + .../typedef/request/audio_types.py | 117 +++++ .../typedef/request/run_create_params.py | 26 -- .../typedef/request/run_create_params_base.py | 337 +-------------- .../typedef/request/run_modify.py | 7 + 14 files changed, 562 insertions(+), 506 deletions(-) create mode 100644 src/leapfrogai_api/backend/thread_runner.py create mode 100644 src/leapfrogai_api/typedef/assistant_types.py create mode 100644 src/leapfrogai_api/typedef/request/audio_types.py create mode 100644 src/leapfrogai_api/typedef/request/run_modify.py diff --git a/src/leapfrogai_api/backend/grpc_client.py b/src/leapfrogai_api/backend/grpc_client.py index 9dbe782de..96d23d58c 100644 --- a/src/leapfrogai_api/backend/grpc_client.py +++ b/src/leapfrogai_api/backend/grpc_client.py @@ -12,9 +12,11 @@ CompletionChoice, CompletionResponse, CreateEmbeddingResponse, - CreateTranscriptionResponse, EmbeddingResponseData, Usage, +) +from leapfrogai_api.typedef.request import ( + CreateTranscriptionResponse, CreateTranslationResponse, ) from leapfrogai_sdk.chat.chat_pb2 import ( diff --git a/src/leapfrogai_api/backend/rag/index.py b/src/leapfrogai_api/backend/rag/index.py index 6014a9d9a..987ed10a6 100644 --- a/src/leapfrogai_api/backend/rag/index.py +++ b/src/leapfrogai_api/backend/rag/index.py @@ -55,10 +55,12 @@ async def index_file(self, vector_store_id: str, file_id: str) -> VectorStoreFil crud_vector_store_file = CRUDVectorStoreFile(db=self.db) crud_vector_store = CRUDVectorStore(db=self.db) - if file_existing := await crud_vector_store_file.get( + if existing_file := await crud_vector_store_file.get( filters=FilterVectorStoreFile(vector_store_id=vector_store_id, id=file_id) ): - return file_existing + logger.warning("File already indexed: %s", file_id) + return existing_file + if not ( await crud_vector_store.get(filters=FilterVectorStore(id=vector_store_id)) ): diff --git a/src/leapfrogai_api/backend/thread_runner.py b/src/leapfrogai_api/backend/thread_runner.py new file mode 100644 index 000000000..6ef654616 --- /dev/null +++ b/src/leapfrogai_api/backend/thread_runner.py @@ -0,0 +1,398 @@ +from __future__ import annotations + +import time +import uuid +from typing import cast, AsyncGenerator, Any +from uuid import UUID + +from fastapi import HTTPException, status +from openai.types.beta.assistant import ToolResources as BetaAssistantToolResources +from openai.types.beta.threads import Run +from openai.types.beta import ( + Thread, +) +from openai.types.beta.assistant_stream_event import ( + ThreadMessageCreated, + ThreadMessageInProgress, + ThreadMessageCompleted, +) +from openai.types.beta.thread import ( + ToolResources as BetaThreadToolResources, + ToolResourcesFileSearch as BetaThreadToolResourcesFileSearch, +) +from openai.types.beta.threads import ( + Message, + TextContentBlock, +) +from postgrest.base_request_builder import SingleAPIResponse +from pydantic import BaseModel +from starlette.responses import StreamingResponse + +from leapfrogai_api.backend.converters import ( + from_assistant_stream_event_to_str, + from_text_to_message, + from_chat_completion_choice_to_thread_message_delta, +) +from leapfrogai_api.backend.rag.query import QueryService +from leapfrogai_api.data.crud_assistant import CRUDAssistant, FilterAssistant +from leapfrogai_api.data.crud_message import CRUDMessage +from leapfrogai_api.routers.openai.chat import chat_complete, chat_complete_stream_raw +from leapfrogai_api.routers.openai.requests.create_message_request import ( + CreateMessageRequest, +) +from leapfrogai_api.routers.supabase_session import Session +from leapfrogai_api.utils import get_model_config +from leapfrogai_sdk.chat.chat_pb2 import ( + ChatCompletionResponse as ProtobufChatCompletionResponse, +) +from leapfrogai_api.backend.types import ( + ChatMessage, + SearchResponse, + ChatCompletionResponse, + ChatCompletionRequest, + ChatChoice, +) +from leapfrogai_api.typedef.request import RunCreateParamsRequest + + +class ThreadRunner(BaseModel): + @staticmethod + async def list_messages(thread_id: str, session: Session) -> list[Message]: + """List all the messages in a thread.""" + try: + crud_message = CRUDMessage(db=session) + messages: list[Message] | None = await crud_message.list( + filters={"thread_id": thread_id} + ) + + if messages is None: + return [] + + return messages + except Exception as exc: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to list messages", + ) from exc + + async def create_chat_messages( + self, + request: RunCreateParamsRequest, + session: Session, + thread: Thread, + additional_instructions: str | None, + tool_resources: BetaThreadToolResources | None = None, + ) -> tuple[list[ChatMessage], list[str]]: + # Get existing messages + thread_messages: list[Message] = await self.list_messages(thread.id, session) + + if len(thread_messages) == 0: + return [], [] + + def sort_by_created_at(msg: Message): + return msg.created_at + + # The messages are not guaranteed to come out of the DB sorted, so they are sorted here + thread_messages.sort(key=sort_by_created_at) + + chat_thread_messages = [] + + for message in thread_messages: + if isinstance(message.content[0], TextContentBlock): + for annotation in message.content[0].text.annotations: + # The LLM may hallucinate if we leave the annotations in when we pass them into the LLM, so they are removed + message.content[0].text.value = message.content[ + 0 + ].text.value.replace(annotation.text, "") + chat_thread_messages.append( + ChatMessage( + role=message.role, content=message.content[0].text.value + ) + ) + + first_message: ChatMessage = chat_thread_messages[0] + + # Holds the converted thread's messages, this will be built up with a series of push operations + chat_messages: list[ChatMessage] = [] + + # 1 - Model instructions (system message) + if request.instructions: + chat_messages.append( + ChatMessage(role="system", content=request.instructions) + ) + + # 2 - Additional model instructions (system message) + if additional_instructions: + chat_messages.append( + ChatMessage(role="system", content=additional_instructions) + ) + + # 3 - The existing messages with everything after the first message + for message in chat_thread_messages: + chat_messages.append(message) + + use_rag: bool = request.can_use_rag(tool_resources) + + rag_message: str = "Here are relevant docs needed to reply:\n" + + # 4 - The RAG results are appended behind the user's query + file_ids: set[str] = set() + if use_rag: + query_service = QueryService(db=session) + file_search: BetaThreadToolResourcesFileSearch = cast( + BetaThreadToolResourcesFileSearch, tool_resources.file_search + ) + vector_store_ids: list[str] = cast(list[str], file_search.vector_store_ids) + + for vector_store_id in vector_store_ids: + rag_results_raw: SingleAPIResponse[ + SearchResponse + ] = await query_service.query_rag( + query=first_message.content, + vector_store_id=vector_store_id, + ) + 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): + file_ids.add(rag_response.file_id) + response_with_instructions: str = f"{rag_response.content}" + rag_message += f"{response_with_instructions}\n" + + chat_messages.insert( + len(chat_messages) - 1, # Insert right before the user message + ChatMessage(role="user", content=rag_message), + ) # TODO: Should this go in user or something else like function? + + return chat_messages, list(file_ids) + + async def generate_message_for_thread( + self, + request: RunCreateParamsRequest, + session: Session, + thread: Thread, + run_id: str, + additional_instructions: str | None = None, + tool_resources: BetaThreadToolResources | None = None, + ): + # If no tools resources are passed in, try the tools in the assistant + if not tool_resources: + crud_assistant = CRUDAssistant(session) + assistant = await crud_assistant.get( + filters=FilterAssistant(id=request.assistant_id) + ) + + if ( + assistant + and assistant.tool_resources + and isinstance(assistant.tool_resources, BetaAssistantToolResources) + ): + tool_resources = BetaThreadToolResources.model_validate( + assistant.tool_resources.model_dump() + ) + else: + tool_resources = None + + chat_messages, file_ids = await self.create_chat_messages( + request, session, thread, additional_instructions, tool_resources + ) + + # Generate a new message and add it to the thread creation request + chat_response: ChatCompletionResponse = await chat_complete( + req=ChatCompletionRequest( + model=str(request.model), + messages=chat_messages, + functions=None, + temperature=request.temperature, + top_p=request.top_p, + stream=request.stream, + stop=None, + max_tokens=request.max_completion_tokens, + ), + model_config=get_model_config(), + session=session, + ) + + choice: ChatChoice = cast(ChatChoice, chat_response.choices[0]) + + message = from_text_to_message(choice.message.content, file_ids) + + create_message_request = CreateMessageRequest( + role=message.role, + content=message.content, + attachments=message.attachments, + metadata=message.metadata.__dict__ if message.metadata else None, + ) + + await create_message_request.create_message( + session=session, + thread_id=thread.id, + run_id=run_id, + assistant_id=request.assistant_id, + ) + + async def stream_generate_message_for_thread( + self, + request: RunCreateParamsRequest, + session: Session, + initial_messages: list[str], + thread: Thread, + ending_messages: list[str], + run_id: str, + additional_instructions: str | None = None, + tool_resources: BetaThreadToolResources | None = None, + ) -> AsyncGenerator[str, Any]: + # If no tools resources are passed in, try the tools in the assistant + if not tool_resources: + crud_assistant = CRUDAssistant(session) + assistant = await crud_assistant.get( + filters=FilterAssistant(id=request.assistant_id) + ) + + if ( + assistant + and assistant.tool_resources + and isinstance(assistant.tool_resources, BetaAssistantToolResources) + ): + tool_resources = BetaThreadToolResources.model_validate( + assistant.tool_resources.model_dump() + ) + else: + tool_resources = None + + chat_messages, file_ids = await self.create_chat_messages( + request, session, thread, additional_instructions, tool_resources + ) + + chat_response: AsyncGenerator[ProtobufChatCompletionResponse, Any] = ( + chat_complete_stream_raw( + req=ChatCompletionRequest( + model=str(request.model), + messages=chat_messages, + functions=None, + temperature=request.temperature, + top_p=request.top_p, + stream=request.stream, + stop=None, + max_tokens=request.max_completion_tokens, + ), + model_config=get_model_config(), + ) + ) + + for message in initial_messages: + yield message + yield "\n\n" + + # Create an empty message + new_message: Message = from_text_to_message("", []) + + create_message_request = CreateMessageRequest( + role=new_message.role, + content=new_message.content, + attachments=new_message.attachments, + metadata=new_message.metadata.__dict__ if new_message.metadata else None, + ) + + new_message = await create_message_request.create_message( + session=session, + thread_id=thread.id, + run_id=run_id, + assistant_id=request.assistant_id, + ) + + yield from_assistant_stream_event_to_str( + ThreadMessageCreated(data=new_message, event="thread.message.created") + ) + yield "\n\n" + + yield from_assistant_stream_event_to_str( + ThreadMessageInProgress( + data=new_message, event="thread.message.in_progress" + ) + ) + yield "\n\n" + + # The accumulated streaming response + response: str = "" + + index: int = 0 + async for streaming_response in chat_response: + random_uuid: UUID = uuid.uuid4() + # Build up the llm response so that it can be committed to the db as a new message + response += streaming_response.choices[0].chat_item.content + thread_message_event = ( + await from_chat_completion_choice_to_thread_message_delta( + index, random_uuid, streaming_response + ) + ) + yield from_assistant_stream_event_to_str(thread_message_event) + yield "\n\n" + index += 1 + + new_message.content = from_text_to_message(response, file_ids).content + new_message.created_at = int(time.time()) + + crud_message = CRUDMessage(db=session) + + if not ( + updated_message := await crud_message.update( + id_=new_message.id, object_=new_message + ) + ): + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to update message during streaming", + ) + + yield from_assistant_stream_event_to_str( + ThreadMessageCompleted( + data=updated_message, event="thread.message.completed" + ) + ) + yield "\n\n" + + for message in ending_messages: + yield message + yield "\n\n" + + yield "event: done\ndata: [DONE]" + + async def generate_response( + self, + request: RunCreateParamsRequest, + existing_thread, + new_run: Run, + session: Session, + ): + """Generate a new response based on the existing thread""" + if request.stream: + initial_messages: list[str] = ( + RunCreateParamsRequest.get_initial_messages_base(run=new_run) + ) + ending_messages: list[str] = ( + RunCreateParamsRequest.get_ending_messages_base(run=new_run) + ) + stream: AsyncGenerator[str, Any] = self.stream_generate_message_for_thread( + request=request, + session=session, + initial_messages=initial_messages, + thread=existing_thread, + ending_messages=ending_messages, + run_id=new_run.id, + additional_instructions=request.additional_instructions, + ) + + return StreamingResponse(stream, media_type="text/event-stream") + else: + await self.generate_message_for_thread( + request=request, + session=session, + thread=existing_thread, + run_id=new_run.id, + additional_instructions=request.additional_instructions, + ) + + return new_run diff --git a/src/leapfrogai_api/backend/types.py b/src/leapfrogai_api/backend/types.py index 0520283d2..1900cabfb 100644 --- a/src/leapfrogai_api/backend/types.py +++ b/src/leapfrogai_api/backend/types.py @@ -8,7 +8,6 @@ from fastapi import UploadFile, Form, File from openai.types import FileObject -from openai.types.beta import Assistant from openai.types.beta import VectorStore from openai.types.beta.thread import ToolResources as BetaThreadToolResources @@ -355,120 +354,6 @@ class CreateEmbeddingResponse(BaseModel): ########## -class CreateTranscriptionRequest(BaseModel): - """Request object for creating a transcription.""" - - file: UploadFile = Field( - ..., - description="The audio file to transcribe. Supports any audio format that ffmpeg can handle. For a complete list of supported formats, see: https://ffmpeg.org/ffmpeg-formats.html", - ) - model: str = Field(..., description="ID of the model to use.") - language: str = Field( - default="", - description="The language of the input audio. Supplying the input language in ISO-639-1 format will improve accuracy and latency.", - ) - prompt: str = Field( - default="", - description="An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language.", - ) - response_format: str = Field( - default="json", - description="The format of the transcript output, in one of these options: json, text, srt, verbose_json, or vtt.", - ) - temperature: float = Field( - default=1.0, - ge=0, - le=1, - description="The sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit.", - ) - timestamp_granularities: list[Literal["word", "segment"]] | None = Field( - default=None, - description="The timestamp granularities to populate for this transcription. response_format must be set to verbose_json to use timestamp granularities. Either or both of these options are supported: word, or segment. Note: There is no additional latency for segment timestamps, but generating word timestamps incurs additional latency.", - ) - - @classmethod - def as_form( - cls, - file: UploadFile = File(...), - model: str = Form(...), - language: str | None = Form(""), - prompt: str | None = Form(""), - response_format: str | None = Form(""), - temperature: float | None = Form(1.0), - timestamp_granularities: list[Literal["word", "segment"]] | None = Form(None), - ) -> CreateTranscriptionRequest: - return cls( - file=file, - model=model, - language=language, - prompt=prompt, - response_format=response_format, - temperature=temperature, - timestamp_granularities=timestamp_granularities, - ) - - -class CreateTranscriptionResponse(BaseModel): - """Response object for transcription.""" - - text: str = Field( - ..., - description="The transcribed text.", - examples=["Hello, this is a transcription of the audio file."], - ) - - -class CreateTranslationRequest(BaseModel): - """Request object for creating a translation.""" - - file: UploadFile = Field( - ..., - description="The audio file to translate. Supports any audio format that ffmpeg can handle. For a complete list of supported formats, see: https://ffmpeg.org/ffmpeg-formats.html", - ) - model: str = Field(..., description="ID of the model to use.") - prompt: str = Field( - default="", - description="An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language.", - ) - response_format: str = Field( - default="json", - description="The format of the transcript output, in one of these options: json, text, srt, verbose_json, or vtt.", - ) - temperature: float = Field( - default=1.0, - ge=0, - le=1, - description="The sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit.", - ) - - @classmethod - def as_form( - cls, - file: UploadFile = File(...), - model: str = Form(...), - prompt: str | None = Form(""), - response_format: str | None = Form(""), - temperature: float | None = Form(1.0), - ) -> CreateTranslationRequest: - return cls( - file=file, - model=model, - prompt=prompt, - response_format=response_format, - temperature=temperature, - ) - - -class CreateTranslationResponse(BaseModel): - """Response object for translation.""" - - text: str = Field( - ..., - description="The translated text.", - examples=["Hello, this is a translation of the audio file."], - ) - - ############# # FILES ############# @@ -514,16 +399,6 @@ class ListFilesResponse(BaseModel): ############# -class ListAssistantsResponse(BaseModel): - """Response object for listing assistants.""" - - object: Literal["list"] = Field( - default="list", - description="The type of object. Always 'list' for this response.", - ) - data: list[Assistant] = Field(description="A list of Assistant objects.") - - ################ # VECTOR STORES ################ @@ -653,12 +528,6 @@ class ListVectorStoresResponse(BaseModel): ################ -class ModifyRunRequest(BaseModel): - """Request object for modifying a run.""" - - metadata: dict[str, str] | None = Field(default=None, examples=[{}]) - - class ModifyThreadRequest(BaseModel): """Request object for modifying a thread.""" diff --git a/src/leapfrogai_api/routers/openai/assistants.py b/src/leapfrogai_api/routers/openai/assistants.py index 01d290a28..9606cbb3b 100644 --- a/src/leapfrogai_api/routers/openai/assistants.py +++ b/src/leapfrogai_api/routers/openai/assistants.py @@ -3,7 +3,7 @@ from fastapi import HTTPException, APIRouter, status from openai.types.beta import Assistant, AssistantDeleted from leapfrogai_api.backend.helpers import object_or_default -from leapfrogai_api.backend.types import ListAssistantsResponse +from leapfrogai_api.typedef import ListAssistantsResponse from leapfrogai_api.routers.openai.requests.create_modify_assistant_request import ( CreateAssistantRequest, ModifyAssistantRequest, diff --git a/src/leapfrogai_api/routers/openai/audio.py b/src/leapfrogai_api/routers/openai/audio.py index aae238cb5..844e42ab2 100644 --- a/src/leapfrogai_api/routers/openai/audio.py +++ b/src/leapfrogai_api/routers/openai/audio.py @@ -5,7 +5,7 @@ from fastapi import HTTPException, APIRouter, Depends from leapfrogai_api.backend.grpc_client import create_transcription, create_translation from leapfrogai_api.backend.helpers import read_chunks -from leapfrogai_api.backend.types import ( +from leapfrogai_api.typedef.request import ( CreateTranscriptionRequest, CreateTranscriptionResponse, CreateTranslationRequest, diff --git a/src/leapfrogai_api/routers/openai/runs.py b/src/leapfrogai_api/routers/openai/runs.py index 5249e6ad2..33a0b7c06 100644 --- a/src/leapfrogai_api/routers/openai/runs.py +++ b/src/leapfrogai_api/routers/openai/runs.py @@ -5,15 +5,9 @@ from fastapi.responses import StreamingResponse from openai.types.beta.threads import Run from openai.pagination import SyncCursorPage -from leapfrogai_api.backend.types import ( - ModifyRunRequest, -) from leapfrogai_api.routers.openai.requests.thread_run_create_params_request import ( ThreadRunCreateParamsRequest, ) -from leapfrogai_api.typedef.request import ( - RunCreateParamsRequest, -) from leapfrogai_api.data.crud_run import CRUDRun from leapfrogai_api.data.crud_thread import CRUDThread from leapfrogai_api.routers.supabase_session import Session @@ -21,6 +15,8 @@ validate_assistant_tool, validate_assistant_tool_choice_option, ) +from leapfrogai_api.typedef.request import RunCreateParamsRequest, ModifyRunRequest +from leapfrogai_api.backend.thread_runner import ThreadRunner router = APIRouter(prefix="/openai/v1/threads", tags=["openai/threads/runs"]) @@ -60,7 +56,12 @@ async def create_run( ) try: - return await request.generate_response(existing_thread, new_run, session) + return await ThreadRunner().generate_response( + request=request, + existing_thread=existing_thread, + new_run=new_run, + session=session, + ) except Exception as exc: traceback.print_exc() raise HTTPException( @@ -84,7 +85,7 @@ async def create_thread_and_run( ) try: - return await request.generate_response(new_run, new_thread, session) + return await ThreadRunner().generate_response(new_run, new_thread, session) except Exception as exc: traceback.print_exc() raise HTTPException( diff --git a/src/leapfrogai_api/typedef/__init__.py b/src/leapfrogai_api/typedef/__init__.py index 27dbd81b5..2e246394c 100644 --- a/src/leapfrogai_api/typedef/__init__.py +++ b/src/leapfrogai_api/typedef/__init__.py @@ -1 +1,2 @@ from .model_types import Model as Model +from .assistant_types import ListAssistantsResponse as ListAssistantsResponse diff --git a/src/leapfrogai_api/typedef/assistant_types.py b/src/leapfrogai_api/typedef/assistant_types.py new file mode 100644 index 000000000..8c3217dae --- /dev/null +++ b/src/leapfrogai_api/typedef/assistant_types.py @@ -0,0 +1,13 @@ +from typing import Literal +from pydantic import BaseModel, Field +from openai.types.beta import Assistant + + +class ListAssistantsResponse(BaseModel): + """Response object for listing assistants.""" + + object: Literal["list"] = Field( + default="list", + description="The type of object. Always 'list' for this response.", + ) + data: list[Assistant] = Field(description="A list of Assistant objects.") diff --git a/src/leapfrogai_api/typedef/request/__init__.py b/src/leapfrogai_api/typedef/request/__init__.py index 75e30f7e0..15312fbaf 100644 --- a/src/leapfrogai_api/typedef/request/__init__.py +++ b/src/leapfrogai_api/typedef/request/__init__.py @@ -6,3 +6,10 @@ RunCreateParamsRequestBase as RunCreateParamsRequestBase, ) from .run_create_params import RunCreateParamsRequest as RunCreateParamsRequest +from .run_modify import ModifyRunRequest as ModifyRunRequest +from .audio_types import ( + CreateTranscriptionRequest as CreateTranscriptionRequest, + CreateTranscriptionResponse as CreateTranscriptionResponse, + CreateTranslationRequest as CreateTranslationRequest, + CreateTranslationResponse as CreateTranslationResponse, +) diff --git a/src/leapfrogai_api/typedef/request/audio_types.py b/src/leapfrogai_api/typedef/request/audio_types.py new file mode 100644 index 000000000..0191f671b --- /dev/null +++ b/src/leapfrogai_api/typedef/request/audio_types.py @@ -0,0 +1,117 @@ +from typing import Literal +from pydantic import BaseModel, Field +from fastapi import UploadFile, Form, File + + +class CreateTranscriptionRequest(BaseModel): + """Request object for creating a transcription.""" + + file: UploadFile = Field( + ..., + description="The audio file to transcribe. Supports any audio format that ffmpeg can handle. For a complete list of supported formats, see: https://ffmpeg.org/ffmpeg-formats.html", + ) + model: str = Field(..., description="ID of the model to use.") + language: str = Field( + default="", + description="The language of the input audio. Supplying the input language in ISO-639-1 format will improve accuracy and latency.", + ) + prompt: str = Field( + default="", + description="An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language.", + ) + response_format: str = Field( + default="json", + description="The format of the transcript output, in one of these options: json, text, srt, verbose_json, or vtt.", + ) + temperature: float = Field( + default=1.0, + ge=0, + le=1, + description="The sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit.", + ) + timestamp_granularities: list[Literal["word", "segment"]] | None = Field( + default=None, + description="The timestamp granularities to populate for this transcription. response_format must be set to verbose_json to use timestamp granularities. Either or both of these options are supported: word, or segment. Note: There is no additional latency for segment timestamps, but generating word timestamps incurs additional latency.", + ) + + @classmethod + def as_form( + cls, + file: UploadFile = File(...), + model: str = Form(...), + language: str | None = Form(""), + prompt: str | None = Form(""), + response_format: str | None = Form(""), + temperature: float | None = Form(1.0), + timestamp_granularities: list[Literal["word", "segment"]] | None = Form(None), + ): + return cls( + file=file, + model=model, + language=language, + prompt=prompt, + response_format=response_format, + temperature=temperature, + timestamp_granularities=timestamp_granularities, + ) + + +class CreateTranscriptionResponse(BaseModel): + """Response object for transcription.""" + + text: str = Field( + ..., + description="The transcribed text.", + examples=["Hello, this is a transcription of the audio file."], + ) + + +class CreateTranslationRequest(BaseModel): + """Request object for creating a translation.""" + + file: UploadFile = Field( + ..., + description="The audio file to translate. Supports any audio format that ffmpeg can handle. For a complete list of supported formats, see: https://ffmpeg.org/ffmpeg-formats.html", + ) + model: str = Field(..., description="ID of the model to use.") + prompt: str = Field( + default="", + description="An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language.", + ) + response_format: str = Field( + default="json", + description="The format of the transcript output, in one of these options: json, text, srt, verbose_json, or vtt.", + ) + temperature: float = Field( + default=1.0, + ge=0, + le=1, + description="The sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit.", + ) + + @classmethod + def as_form( + cls, + file: UploadFile = File(...), + model: str = Form(...), + prompt: str | None = Form(""), + response_format: str | None = Form(""), + temperature: float | None = Form(1.0), + ): + return cls( + file=file, + model=model, + prompt=prompt, + response_format=response_format, + temperature=temperature, + ) + + +class CreateTranslationResponse(BaseModel): + """Response object for translation.""" + + text: str = Field( + ..., + description="The translated text.", + examples=["Hello, this is a translation of the audio file."], + ) diff --git a/src/leapfrogai_api/typedef/request/run_create_params.py b/src/leapfrogai_api/typedef/request/run_create_params.py index 9963a9e4a..33da59fef 100644 --- a/src/leapfrogai_api/typedef/request/run_create_params.py +++ b/src/leapfrogai_api/typedef/request/run_create_params.py @@ -1,7 +1,6 @@ from __future__ import annotations import logging -from typing import AsyncGenerator, Any from openai.types.beta.threads import Run from openai.types.beta.threads.run_create_params import ( AdditionalMessage, @@ -9,7 +8,6 @@ AdditionalMessageAttachmentToolFileSearch, ) from pydantic import Field -from starlette.responses import StreamingResponse from leapfrogai_api.typedef.request.run_create_params_base import ( RunCreateParamsRequestBase, ) @@ -107,27 +105,3 @@ async def create_run(self, session, thread_id): **create_params.__dict__, ) return await crud_run.create(object_=run) - - async def generate_response(self, existing_thread, new_run: Run, session: Session): - """Generate a new response based on the existing thread""" - if self.stream: - initial_messages: list[str] = ( - RunCreateParamsRequestBase.get_initial_messages_base(run=new_run) - ) - ending_messages: list[str] = ( - RunCreateParamsRequestBase.get_ending_messages_base(run=new_run) - ) - stream: AsyncGenerator[str, Any] = ( - super().stream_generate_message_for_thread(session=session, initial_messages=initial_messages, thread=existing_thread, ending_messages=ending_messages, run_id=new_run.id, additional_instructions=self.additional_instructions) - ) - - return StreamingResponse(stream, media_type="text/event-stream") - else: - await super().generate_message_for_thread( - session=session, - thread=existing_thread, - run_id=new_run.id, - additional_instructions=self.additional_instructions, - ) - - return new_run diff --git a/src/leapfrogai_api/typedef/request/run_create_params_base.py b/src/leapfrogai_api/typedef/request/run_create_params_base.py index fb518e7e5..e593c8024 100644 --- a/src/leapfrogai_api/typedef/request/run_create_params_base.py +++ b/src/leapfrogai_api/typedef/request/run_create_params_base.py @@ -1,45 +1,25 @@ from __future__ import annotations import logging -import time import traceback -import uuid -from typing import cast, AsyncGenerator, Any -from uuid import UUID -from fastapi import HTTPException, status -from openai.types.beta.assistant import ToolResources as BetaAssistantToolResources from openai.types.beta import ( AssistantResponseFormatOption, FileSearchTool, Assistant, - Thread, AssistantToolChoiceOption, AssistantTool, AssistantToolChoice, ) -from openai.types.beta.assistant_stream_event import ( - ThreadMessageCreated, - ThreadMessageInProgress, - ThreadMessageCompleted, -) from openai.types.beta.assistant_stream_event import ( ThreadRunCreated, ThreadRunQueued, ThreadRunInProgress, ThreadRunCompleted, ) -from openai.types.beta.thread import ( - ToolResources as BetaThreadToolResources, - ToolResourcesFileSearch as BetaThreadToolResourcesFileSearch, -) -from openai.types.beta.threads import ( - Message, - TextContentBlock, -) +from openai.types.beta.thread import ToolResources as BetaThreadToolResources from openai.types.beta.threads import Run from openai.types.beta.threads.run_create_params import TruncationStrategy -from postgrest.base_request_builder import SingleAPIResponse from pydantic import BaseModel, Field, ValidationError from leapfrogai_api.typedef.constants import ( @@ -48,28 +28,9 @@ ) from leapfrogai_api.backend.converters import ( from_assistant_stream_event_to_str, - from_text_to_message, - from_chat_completion_choice_to_thread_message_delta, -) -from leapfrogai_api.backend.rag.query import QueryService -from leapfrogai_api.backend.types import ( - ChatMessage, - SearchResponse, - ChatCompletionResponse, - ChatCompletionRequest, - ChatChoice, ) from leapfrogai_api.data.crud_assistant import CRUDAssistant, FilterAssistant -from leapfrogai_api.data.crud_message import CRUDMessage -from leapfrogai_api.routers.openai.chat import chat_complete, chat_complete_stream_raw -from leapfrogai_api.routers.openai.requests.create_message_request import ( - CreateMessageRequest, -) from leapfrogai_api.routers.supabase_session import Session -from leapfrogai_api.utils import get_model_config -from leapfrogai_sdk.chat.chat_pb2 import ( - ChatCompletionResponse as ProtobufChatCompletionResponse, -) logger = logging.getLogger(__name__) @@ -180,299 +141,3 @@ def can_use_rag( return False return False - - async def list_messages(self, thread_id: str, session: Session) -> list[Message]: - """List all the messages in a thread.""" - try: - crud_message = CRUDMessage(db=session) - messages: list[Message] | None = await crud_message.list( - filters={"thread_id": thread_id} - ) - - if messages is None: - return [] - - return messages - except Exception as exc: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to list messages", - ) from exc - - async def create_chat_messages( - self, - session: Session, - thread: Thread, - additional_instructions: str | None, - tool_resources: BetaThreadToolResources | None = None, - ) -> tuple[list[ChatMessage], list[str]]: - # Get existing messages - thread_messages: list[Message] = await self.list_messages(thread.id, session) - - if len(thread_messages) == 0: - return [], [] - - def sort_by_created_at(msg: Message): - return msg.created_at - - # The messages are not guaranteed to come out of the DB sorted, so they are sorted here - thread_messages.sort(key=sort_by_created_at) - - chat_thread_messages = [] - - for message in thread_messages: - if isinstance(message.content[0], TextContentBlock): - for annotation in message.content[0].text.annotations: - # The LLM may hallucinate if we leave the annotations in when we pass them into the LLM, so they are removed - message.content[0].text.value = message.content[ - 0 - ].text.value.replace(annotation.text, "") - chat_thread_messages.append( - ChatMessage( - role=message.role, content=message.content[0].text.value - ) - ) - - # Holds the converted thread's messages, this will be built up with a series of push operations - chat_messages: list[ChatMessage] = [] - - # 1 - Model instructions (system message) - if self.instructions: - chat_messages.append(ChatMessage(role="system", content=self.instructions)) - - # 2 - Additional model instructions (system message) - if additional_instructions: - chat_messages.append( - ChatMessage(role="system", content=additional_instructions) - ) - - # 3 - The existing messages with everything after the first message - for message in chat_thread_messages: - chat_messages.append(message) - - # 4 - The RAG results are appended behind the user's query - if self.can_use_rag(tool_resources): - rag_message: str = "Here are relevant docs needed to reply:\n" - - query_message: ChatMessage = chat_thread_messages[-1] - - query_service = QueryService(db=session) - file_search: BetaThreadToolResourcesFileSearch = cast( - BetaThreadToolResourcesFileSearch, tool_resources.file_search - ) - - vector_store_ids: list[str] = cast(list[str], file_search.vector_store_ids) - file_ids: set[str] = set() - for vector_store_id in vector_store_ids: - rag_results_raw: SingleAPIResponse[ - SearchResponse - ] = await query_service.query_rag( - query=query_message.content, - vector_store_id=vector_store_id, - ) - 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): - file_ids.add(rag_response.file_id) - response_with_instructions: str = f"{rag_response.content}" - rag_message += f"{response_with_instructions}\n" - - chat_messages.insert( - len(chat_messages) - 1, # Insert right before the user message - ChatMessage(role="user", content=rag_message), - ) # TODO: Should this go in user or something else like function? - - return chat_messages, list(file_ids) - - async def generate_message_for_thread( - self, - session: Session, - thread: Thread, - run_id: str, - additional_instructions: str | None = None, - tool_resources: BetaThreadToolResources | None = None, - ): - # If no tools resources are passed in, try the tools in the assistant - if not tool_resources: - crud_assistant = CRUDAssistant(session) - assistant = await crud_assistant.get( - filters=FilterAssistant(id=self.assistant_id) - ) - - if ( - assistant - and assistant.tool_resources - and isinstance(assistant.tool_resources, BetaAssistantToolResources) - ): - tool_resources = BetaThreadToolResources.model_validate( - assistant.tool_resources.model_dump() - ) - else: - tool_resources = None - - chat_messages, file_ids = await self.create_chat_messages( - session, thread, additional_instructions, tool_resources - ) - - # Generate a new message and add it to the thread creation request - chat_response: ChatCompletionResponse = await chat_complete( - req=ChatCompletionRequest( - model=str(self.model), - messages=chat_messages, - functions=None, - temperature=self.temperature, - top_p=self.top_p, - stream=self.stream, - stop=None, - max_tokens=self.max_completion_tokens, - ), - model_config=get_model_config(), - session=session, - ) - - choice: ChatChoice = cast(ChatChoice, chat_response.choices[0]) - - message = from_text_to_message(choice.message.content, file_ids) - - create_message_request = CreateMessageRequest( - role=message.role, - content=message.content, - attachments=message.attachments, - metadata=message.metadata.__dict__ if message.metadata else None, - ) - - await create_message_request.create_message( - session=session, - thread_id=thread.id, - run_id=run_id, - assistant_id=self.assistant_id, - ) - - async def stream_generate_message_for_thread( - self, - session: Session, - initial_messages: list[str], - thread: Thread, - ending_messages: list[str], - run_id: str, - additional_instructions: str | None = None, - tool_resources: BetaThreadToolResources | None = None, - ) -> AsyncGenerator[str, Any]: - # If no tools resources are passed in, try the tools in the assistant - if not tool_resources: - crud_assistant = CRUDAssistant(session) - assistant = await crud_assistant.get( - filters=FilterAssistant(id=self.assistant_id) - ) - - if ( - assistant - and assistant.tool_resources - and isinstance(assistant.tool_resources, BetaAssistantToolResources) - ): - tool_resources = BetaThreadToolResources.model_validate( - assistant.tool_resources.model_dump() - ) - else: - tool_resources = None - - chat_messages, file_ids = await self.create_chat_messages( - session, thread, additional_instructions, tool_resources - ) - - chat_response: AsyncGenerator[ProtobufChatCompletionResponse, Any] = ( - chat_complete_stream_raw( - req=ChatCompletionRequest( - model=str(self.model), - messages=chat_messages, - functions=None, - temperature=self.temperature, - top_p=self.top_p, - stream=self.stream, - stop=None, - max_tokens=self.max_completion_tokens, - ), - model_config=get_model_config(), - ) - ) - - for message in initial_messages: - yield message - yield "\n\n" - - # Create an empty message - new_message: Message = from_text_to_message("", []) - - create_message_request = CreateMessageRequest( - role=new_message.role, - content=new_message.content, - attachments=new_message.attachments, - metadata=new_message.metadata.__dict__ if new_message.metadata else None, - ) - - new_message = await create_message_request.create_message( - session=session, - thread_id=thread.id, - run_id=run_id, - assistant_id=self.assistant_id, - ) - - yield from_assistant_stream_event_to_str( - ThreadMessageCreated(data=new_message, event="thread.message.created") - ) - yield "\n\n" - - yield from_assistant_stream_event_to_str( - ThreadMessageInProgress( - data=new_message, event="thread.message.in_progress" - ) - ) - yield "\n\n" - - # The accumulated streaming response - response: str = "" - - index: int = 0 - async for streaming_response in chat_response: - random_uuid: UUID = uuid.uuid4() - # Build up the llm response so that it can be committed to the db as a new message - response += streaming_response.choices[0].chat_item.content - thread_message_event = ( - await from_chat_completion_choice_to_thread_message_delta( - index, random_uuid, streaming_response - ) - ) - yield from_assistant_stream_event_to_str(thread_message_event) - yield "\n\n" - index += 1 - - new_message.content = from_text_to_message(response, file_ids).content - new_message.created_at = int(time.time()) - - crud_message = CRUDMessage(db=session) - - if not ( - updated_message := await crud_message.update( - id_=new_message.id, object_=new_message - ) - ): - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to update message during streaming", - ) - - yield from_assistant_stream_event_to_str( - ThreadMessageCompleted( - data=updated_message, event="thread.message.completed" - ) - ) - yield "\n\n" - - for message in ending_messages: - yield message - yield "\n\n" - - yield "event: done\ndata: [DONE]" diff --git a/src/leapfrogai_api/typedef/request/run_modify.py b/src/leapfrogai_api/typedef/request/run_modify.py new file mode 100644 index 000000000..2e60e040d --- /dev/null +++ b/src/leapfrogai_api/typedef/request/run_modify.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel, Field + + +class ModifyRunRequest(BaseModel): + """Request object for modifying a run.""" + + metadata: dict[str, str] | None = Field(default=None, examples=[{}]) From f65186847f9cb04e4fa6ea629284a132d86074cf Mon Sep 17 00:00:00 2001 From: alekst23 Date: Fri, 30 Aug 2024 11:16:01 -0400 Subject: [PATCH 04/17] test: Testing for routes affected by refactoring --- src/leapfrogai_api/routers/openai/runs.py | 4 +- tests/conformance/test_completions.py | 66 ++++++++++++++++++++++ tests/conformance/test_conformance_runs.py | 62 ++++++++++++++++++++ tests/conformance/test_files.py | 16 ------ 4 files changed, 131 insertions(+), 17 deletions(-) create mode 100644 tests/conformance/test_completions.py create mode 100644 tests/conformance/test_conformance_runs.py diff --git a/src/leapfrogai_api/routers/openai/runs.py b/src/leapfrogai_api/routers/openai/runs.py index 33a0b7c06..6424a85ec 100644 --- a/src/leapfrogai_api/routers/openai/runs.py +++ b/src/leapfrogai_api/routers/openai/runs.py @@ -85,7 +85,9 @@ async def create_thread_and_run( ) try: - return await ThreadRunner().generate_response(new_run, new_thread, session) + return await ThreadRunner().generate_response( + request=request, exising_thread=new_thread, new_run=new_run, session=session + ) except Exception as exc: traceback.print_exc() raise HTTPException( diff --git a/tests/conformance/test_completions.py b/tests/conformance/test_completions.py new file mode 100644 index 000000000..6e53dfdcc --- /dev/null +++ b/tests/conformance/test_completions.py @@ -0,0 +1,66 @@ +import pytest +from openai.types.beta.threads import Run, Message, TextContentBlock, Text + +from .utils import client_config_factory + + +def make_mock_message_object(role, message_text): + Message( + id="", + thread_id="", + created_at=0, + object="thread.message", + status="in_progress", + role=role, + content=[ + TextContentBlock(text=Text(value=message_text, annotations=[]), type="text") + ], + ) + + +def make_mock_message_simple(role, message_text): + return dict(role=role, content=message_text) + + +mock_message = make_mock_message_simple(role="user", message_text="Hello world!") + + +@pytest.mark.parametrize( + "client_name, test_messages", + [ + ("openai", []), + ("openai", [mock_message]), + ("leapfrogai", []), + ("leapfrogai", [mock_message]), + ], +) +def test_run_completion(client_name, test_messages): + # Setup + config = client_config_factory(client_name) + client = config["client"] + + assistant = client.beta.assistants.create( + name="Test Assistant", + instructions="You must provide a response based on the attached files.", + model=config["model"], + ) + thread = client.beta.threads.create() + + for m in test_messages: + client.beta.threads.messages.create( + thread_id=thread.id, + role=m["role"], + content=m["content"], + ) + + # Run the test + run = client.beta.threads.runs.create_and_poll( + assistant_id=assistant.id, thread_id=thread.id + ) + + # Check results + messages = client.beta.threads.messages.list( + thread_id=thread.id, run_id=run.id + ).data + assert len(messages) >= 1 + assert isinstance(run, Run) diff --git a/tests/conformance/test_conformance_runs.py b/tests/conformance/test_conformance_runs.py new file mode 100644 index 000000000..7a4447bfc --- /dev/null +++ b/tests/conformance/test_conformance_runs.py @@ -0,0 +1,62 @@ +import pytest +from openai.types.beta.threads import Run, Message, TextContentBlock, Text + +from .utils import client_config_factory + + +def make_mock_message_object(role, message_text): + Message( + id="", + thread_id="", + created_at=0, + object="thread.message", + status="in_progress", + role=role, + content=[ + TextContentBlock(text=Text(value=message_text, annotations=[]), type="text") + ], + ) + + +def make_mock_message_simple(role, message_text): + return dict(role=role, content=message_text) + + +mock_message = make_mock_message_simple(role="user", message_text="Hello world!") + + +@pytest.mark.parametrize( + "client_name, test_messages", + [ + ("openai", []), + ("openai", [mock_message]), + ("leapfrogai", []), + ("leapfrogai", [mock_message]), + ], +) +def test_run_create(client_name, test_messages): + # Setup + config = client_config_factory(client_name) + client = config["client"] + + assistant = client.beta.assistants.create( + name="Test Assistant", + instructions="You must provide a response based on the attached files.", + model=config["model"], + ) + thread = client.beta.threads.create() + + for m in test_messages: + client.beta.threads.messages.create( + thread_id=thread.id, + role=m["role"], + content=m["content"], + ) + + # Run the test + run = client.beta.threads.runs.create( + assistant_id=assistant.id, thread_id=thread.id + ) + + # Check results + assert isinstance(run, Run) diff --git a/tests/conformance/test_files.py b/tests/conformance/test_files.py index a1510b790..02b67530a 100644 --- a/tests/conformance/test_files.py +++ b/tests/conformance/test_files.py @@ -24,22 +24,6 @@ def test_file_upload(client_name): assert isinstance(vector_store_file, VectorStoreFile) -@pytest.mark.parametrize("client_name", ["openai", "leapfrogai"]) -def test_file_upload_batches(client_name): - config = client_config_factory(client_name) - client = config.client # shorthand - - vector_store = client.beta.vector_stores.create(name="Test data") - - file_streams = [open(text_file_path(), "rb")] - - client.beta.vector_stores.file_batches.upload_and_poll( - vector_store_id=vector_store.id, files=file_streams - ) - - assert isinstance(vector_store, VectorStore) - - @pytest.mark.parametrize("client_name", ["openai", "leapfrogai"]) def test_file_delete(client_name): config = client_config_factory(client_name) From 2fcd87e713f37ee6fcfb35c045d8a9a17ffcc75b Mon Sep 17 00:00:00 2001 From: alekst23 Date: Fri, 30 Aug 2024 11:59:21 -0400 Subject: [PATCH 05/17] Rename 'request' to 'requests' --- src/leapfrogai_api/backend/grpc_client.py | 2 +- src/leapfrogai_api/backend/thread_runner.py | 2 +- src/leapfrogai_api/routers/leapfrogai/auth.py | 2 +- src/leapfrogai_api/routers/openai/audio.py | 2 +- .../routers/openai/requests/thread_run_create_params_request.py | 2 +- src/leapfrogai_api/routers/openai/runs.py | 2 +- src/leapfrogai_api/typedef/{request => requests}/__init__.py | 0 src/leapfrogai_api/typedef/{request => requests}/audio_types.py | 0 src/leapfrogai_api/typedef/{request => requests}/auth_types.py | 0 .../typedef/{request => requests}/run_create_params.py | 2 +- .../typedef/{request => requests}/run_create_params_base.py | 0 src/leapfrogai_api/typedef/{request => requests}/run_modify.py | 0 tests/integration/api/test_runs.py | 2 +- 13 files changed, 8 insertions(+), 8 deletions(-) rename src/leapfrogai_api/typedef/{request => requests}/__init__.py (100%) rename src/leapfrogai_api/typedef/{request => requests}/audio_types.py (100%) rename src/leapfrogai_api/typedef/{request => requests}/auth_types.py (100%) rename src/leapfrogai_api/typedef/{request => requests}/run_create_params.py (98%) rename src/leapfrogai_api/typedef/{request => requests}/run_create_params_base.py (100%) rename src/leapfrogai_api/typedef/{request => requests}/run_modify.py (100%) diff --git a/src/leapfrogai_api/backend/grpc_client.py b/src/leapfrogai_api/backend/grpc_client.py index 96d23d58c..b4f4de366 100644 --- a/src/leapfrogai_api/backend/grpc_client.py +++ b/src/leapfrogai_api/backend/grpc_client.py @@ -15,7 +15,7 @@ EmbeddingResponseData, Usage, ) -from leapfrogai_api.typedef.request import ( +from leapfrogai_api.typedef.requests import ( CreateTranscriptionResponse, CreateTranslationResponse, ) diff --git a/src/leapfrogai_api/backend/thread_runner.py b/src/leapfrogai_api/backend/thread_runner.py index 6ef654616..effb27648 100644 --- a/src/leapfrogai_api/backend/thread_runner.py +++ b/src/leapfrogai_api/backend/thread_runner.py @@ -52,7 +52,7 @@ ChatCompletionRequest, ChatChoice, ) -from leapfrogai_api.typedef.request import RunCreateParamsRequest +from leapfrogai_api.typedef.requests import RunCreateParamsRequest class ThreadRunner(BaseModel): diff --git a/src/leapfrogai_api/routers/leapfrogai/auth.py b/src/leapfrogai_api/routers/leapfrogai/auth.py index 3e5b406ca..b34e7539d 100644 --- a/src/leapfrogai_api/routers/leapfrogai/auth.py +++ b/src/leapfrogai_api/routers/leapfrogai/auth.py @@ -7,7 +7,7 @@ from leapfrogai_api.routers.supabase_session import Session from leapfrogai_api.data.crud_api_key import APIKeyItem, CRUDAPIKey -from leapfrogai_api.typedef.request import CreateAPIKeyRequest, ModifyAPIKeyRequest +from leapfrogai_api.typedef.requests import CreateAPIKeyRequest, ModifyAPIKeyRequest router = APIRouter(prefix="/leapfrogai/v1/auth", tags=["leapfrogai/auth"]) diff --git a/src/leapfrogai_api/routers/openai/audio.py b/src/leapfrogai_api/routers/openai/audio.py index 844e42ab2..6ef5d4436 100644 --- a/src/leapfrogai_api/routers/openai/audio.py +++ b/src/leapfrogai_api/routers/openai/audio.py @@ -5,7 +5,7 @@ from fastapi import HTTPException, APIRouter, Depends from leapfrogai_api.backend.grpc_client import create_transcription, create_translation from leapfrogai_api.backend.helpers import read_chunks -from leapfrogai_api.typedef.request import ( +from leapfrogai_api.typedef.requests import ( CreateTranscriptionRequest, CreateTranscriptionResponse, CreateTranslationRequest, diff --git a/src/leapfrogai_api/routers/openai/requests/thread_run_create_params_request.py b/src/leapfrogai_api/routers/openai/requests/thread_run_create_params_request.py index af70d1df6..74bf6ff74 100644 --- a/src/leapfrogai_api/routers/openai/requests/thread_run_create_params_request.py +++ b/src/leapfrogai_api/routers/openai/requests/thread_run_create_params_request.py @@ -24,7 +24,7 @@ from leapfrogai_api.routers.openai.requests.create_message_request import ( CreateMessageRequest, ) -from leapfrogai_api.typedef.request import ( +from leapfrogai_api.typedef.requests import ( RunCreateParamsRequestBase, ) from leapfrogai_api.data.crud_run import CRUDRun diff --git a/src/leapfrogai_api/routers/openai/runs.py b/src/leapfrogai_api/routers/openai/runs.py index 6424a85ec..9c55eaa24 100644 --- a/src/leapfrogai_api/routers/openai/runs.py +++ b/src/leapfrogai_api/routers/openai/runs.py @@ -15,7 +15,7 @@ validate_assistant_tool, validate_assistant_tool_choice_option, ) -from leapfrogai_api.typedef.request import RunCreateParamsRequest, ModifyRunRequest +from leapfrogai_api.typedef.requests import RunCreateParamsRequest, ModifyRunRequest from leapfrogai_api.backend.thread_runner import ThreadRunner router = APIRouter(prefix="/openai/v1/threads", tags=["openai/threads/runs"]) diff --git a/src/leapfrogai_api/typedef/request/__init__.py b/src/leapfrogai_api/typedef/requests/__init__.py similarity index 100% rename from src/leapfrogai_api/typedef/request/__init__.py rename to src/leapfrogai_api/typedef/requests/__init__.py diff --git a/src/leapfrogai_api/typedef/request/audio_types.py b/src/leapfrogai_api/typedef/requests/audio_types.py similarity index 100% rename from src/leapfrogai_api/typedef/request/audio_types.py rename to src/leapfrogai_api/typedef/requests/audio_types.py diff --git a/src/leapfrogai_api/typedef/request/auth_types.py b/src/leapfrogai_api/typedef/requests/auth_types.py similarity index 100% rename from src/leapfrogai_api/typedef/request/auth_types.py rename to src/leapfrogai_api/typedef/requests/auth_types.py diff --git a/src/leapfrogai_api/typedef/request/run_create_params.py b/src/leapfrogai_api/typedef/requests/run_create_params.py similarity index 98% rename from src/leapfrogai_api/typedef/request/run_create_params.py rename to src/leapfrogai_api/typedef/requests/run_create_params.py index 33da59fef..e6fe1bd40 100644 --- a/src/leapfrogai_api/typedef/request/run_create_params.py +++ b/src/leapfrogai_api/typedef/requests/run_create_params.py @@ -8,7 +8,7 @@ AdditionalMessageAttachmentToolFileSearch, ) from pydantic import Field -from leapfrogai_api.typedef.request.run_create_params_base import ( +from leapfrogai_api.typedef.requests.run_create_params_base import ( RunCreateParamsRequestBase, ) from leapfrogai_api.routers.openai.requests.create_message_request import ( diff --git a/src/leapfrogai_api/typedef/request/run_create_params_base.py b/src/leapfrogai_api/typedef/requests/run_create_params_base.py similarity index 100% rename from src/leapfrogai_api/typedef/request/run_create_params_base.py rename to src/leapfrogai_api/typedef/requests/run_create_params_base.py diff --git a/src/leapfrogai_api/typedef/request/run_modify.py b/src/leapfrogai_api/typedef/requests/run_modify.py similarity index 100% rename from src/leapfrogai_api/typedef/request/run_modify.py rename to src/leapfrogai_api/typedef/requests/run_modify.py diff --git a/tests/integration/api/test_runs.py b/tests/integration/api/test_runs.py index 1d88ec7c3..5220fe7fd 100644 --- a/tests/integration/api/test_runs.py +++ b/tests/integration/api/test_runs.py @@ -18,7 +18,7 @@ from leapfrogai_api.routers.openai.requests.create_thread_request import ( CreateThreadRequest, ) -from leapfrogai_api.typedef.request import ( +from leapfrogai_api.typedef.requests import ( RunCreateParamsRequest, ) from leapfrogai_api.routers.openai.requests.thread_run_create_params_request import ( From de211e48eecda53deb7608ddfae7a895ef351452 Mon Sep 17 00:00:00 2001 From: alekst23 Date: Fri, 30 Aug 2024 15:52:11 -0400 Subject: [PATCH 06/17] chore: move thread create and modify request types --- src/leapfrogai_api/backend/types.py | 10 ---------- src/leapfrogai_api/routers/openai/threads.py | 5 +---- src/leapfrogai_api/typedef/requests/__init__.py | 9 +++++++-- .../requests/{run_create_params.py => run_create.py} | 2 +- .../{run_create_params_base.py => run_create_base.py} | 0 .../requests/thread_create.py} | 0 src/leapfrogai_api/typedef/requests/thread_modify.py | 11 +++++++++++ 7 files changed, 20 insertions(+), 17 deletions(-) rename src/leapfrogai_api/typedef/requests/{run_create_params.py => run_create.py} (98%) rename src/leapfrogai_api/typedef/requests/{run_create_params_base.py => run_create_base.py} (100%) rename src/leapfrogai_api/{routers/openai/requests/create_thread_request.py => typedef/requests/thread_create.py} (100%) create mode 100644 src/leapfrogai_api/typedef/requests/thread_modify.py diff --git a/src/leapfrogai_api/backend/types.py b/src/leapfrogai_api/backend/types.py index 1900cabfb..dd3dd359d 100644 --- a/src/leapfrogai_api/backend/types.py +++ b/src/leapfrogai_api/backend/types.py @@ -10,7 +10,6 @@ from openai.types import FileObject from openai.types.beta import VectorStore -from openai.types.beta.thread import ToolResources as BetaThreadToolResources from openai.types.beta.thread_create_params import ( ToolResourcesFileSearchVectorStoreChunkingStrategy, ToolResourcesFileSearchVectorStoreChunkingStrategyAuto, @@ -528,15 +527,6 @@ class ListVectorStoresResponse(BaseModel): ################ -class ModifyThreadRequest(BaseModel): - """Request object for modifying a thread.""" - - tool_resources: BetaThreadToolResources | None = Field( - default=None, examples=[None] - ) - metadata: dict | None = Field(default=None, examples=[{}]) - - class ModifyMessageRequest(BaseModel): """Request object for modifying a message.""" diff --git a/src/leapfrogai_api/routers/openai/threads.py b/src/leapfrogai_api/routers/openai/threads.py index 01c7bf7a3..c1e92ac09 100644 --- a/src/leapfrogai_api/routers/openai/threads.py +++ b/src/leapfrogai_api/routers/openai/threads.py @@ -2,10 +2,7 @@ from fastapi import HTTPException, APIRouter, status from openai.types.beta import Thread, ThreadDeleted -from leapfrogai_api.backend.types import ModifyThreadRequest -from leapfrogai_api.routers.openai.requests.create_thread_request import ( - CreateThreadRequest, -) +from leapfrogai_api.typedef.requests import ModifyThreadRequest, CreateThreadRequest from leapfrogai_api.data.crud_thread import CRUDThread from leapfrogai_api.routers.supabase_session import Session from leapfrogai_api.utils.validate_tools import ( diff --git a/src/leapfrogai_api/typedef/requests/__init__.py b/src/leapfrogai_api/typedef/requests/__init__.py index 15312fbaf..3d6c04f8e 100644 --- a/src/leapfrogai_api/typedef/requests/__init__.py +++ b/src/leapfrogai_api/typedef/requests/__init__.py @@ -2,11 +2,16 @@ CreateAPIKeyRequest as CreateAPIKeyRequest, ModifyAPIKeyRequest as ModifyAPIKeyRequest, ) -from .run_create_params_base import ( + +from .run_create_base import ( RunCreateParamsRequestBase as RunCreateParamsRequestBase, ) -from .run_create_params import RunCreateParamsRequest as RunCreateParamsRequest +from .run_create import RunCreateParamsRequest as RunCreateParamsRequest from .run_modify import ModifyRunRequest as ModifyRunRequest + +from .thread_create import CreateThreadRequest as CreateThreadRequest +from .thread_modify import ModifyThreadRequest as ModifyThreadRequest + from .audio_types import ( CreateTranscriptionRequest as CreateTranscriptionRequest, CreateTranscriptionResponse as CreateTranscriptionResponse, diff --git a/src/leapfrogai_api/typedef/requests/run_create_params.py b/src/leapfrogai_api/typedef/requests/run_create.py similarity index 98% rename from src/leapfrogai_api/typedef/requests/run_create_params.py rename to src/leapfrogai_api/typedef/requests/run_create.py index e6fe1bd40..8e5430faa 100644 --- a/src/leapfrogai_api/typedef/requests/run_create_params.py +++ b/src/leapfrogai_api/typedef/requests/run_create.py @@ -8,7 +8,7 @@ AdditionalMessageAttachmentToolFileSearch, ) from pydantic import Field -from leapfrogai_api.typedef.requests.run_create_params_base import ( +from leapfrogai_api.typedef.requests.run_create_base import ( RunCreateParamsRequestBase, ) from leapfrogai_api.routers.openai.requests.create_message_request import ( diff --git a/src/leapfrogai_api/typedef/requests/run_create_params_base.py b/src/leapfrogai_api/typedef/requests/run_create_base.py similarity index 100% rename from src/leapfrogai_api/typedef/requests/run_create_params_base.py rename to src/leapfrogai_api/typedef/requests/run_create_base.py diff --git a/src/leapfrogai_api/routers/openai/requests/create_thread_request.py b/src/leapfrogai_api/typedef/requests/thread_create.py similarity index 100% rename from src/leapfrogai_api/routers/openai/requests/create_thread_request.py rename to src/leapfrogai_api/typedef/requests/thread_create.py diff --git a/src/leapfrogai_api/typedef/requests/thread_modify.py b/src/leapfrogai_api/typedef/requests/thread_modify.py new file mode 100644 index 000000000..5586b89d2 --- /dev/null +++ b/src/leapfrogai_api/typedef/requests/thread_modify.py @@ -0,0 +1,11 @@ +from openai.types.beta.thread import ToolResources as BetaThreadToolResources +from pydantic import BaseModel, Field + + +class ModifyThreadRequest(BaseModel): + """Request object for modifying a thread.""" + + tool_resources: BetaThreadToolResources | None = Field( + default=None, examples=[None] + ) + metadata: dict | None = Field(default=None, examples=[{}]) From 8cbccd3be395a46e716993ec090e3997b3e40e93 Mon Sep 17 00:00:00 2001 From: alekst23 Date: Fri, 30 Aug 2024 17:03:53 -0400 Subject: [PATCH 07/17] fix to import --- .../openai/requests/thread_run_create_params_request.py | 4 +--- src/leapfrogai_api/routers/openai/threads.py | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/leapfrogai_api/routers/openai/requests/thread_run_create_params_request.py b/src/leapfrogai_api/routers/openai/requests/thread_run_create_params_request.py index 74bf6ff74..2ad6cddca 100644 --- a/src/leapfrogai_api/routers/openai/requests/thread_run_create_params_request.py +++ b/src/leapfrogai_api/routers/openai/requests/thread_run_create_params_request.py @@ -32,9 +32,7 @@ from_content_param_to_content, from_assistant_stream_event_to_str, ) -from leapfrogai_api.routers.openai.requests.create_thread_request import ( - CreateThreadRequest, -) +from leapfrogai_api.typedef.requests import CreateThreadRequest from leapfrogai_api.routers.supabase_session import Session diff --git a/src/leapfrogai_api/routers/openai/threads.py b/src/leapfrogai_api/routers/openai/threads.py index c1e92ac09..c0c542aa1 100644 --- a/src/leapfrogai_api/routers/openai/threads.py +++ b/src/leapfrogai_api/routers/openai/threads.py @@ -15,7 +15,6 @@ @router.post("") async def create_thread(request: CreateThreadRequest, session: Session) -> Thread: """Create a thread.""" - if request.tool_resources and not validate_tool_resources(request.tool_resources): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, From bf236f59d1264872cc3ec83a6ed71a805ef84573 Mon Sep 17 00:00:00 2001 From: alekst23 Date: Tue, 3 Sep 2024 15:16:20 -0400 Subject: [PATCH 08/17] chore: moving type files to sub-modules --- src/leapfrogai_api/backend/grpc_client.py | 25 +- src/leapfrogai_api/backend/helpers.py | 6 +- src/leapfrogai_api/backend/thread_runner.py | 6 +- src/leapfrogai_api/backend/types.py | 295 ------------------ src/leapfrogai_api/routers/leapfrogai/auth.py | 2 +- .../routers/openai/assistants.py | 3 +- src/leapfrogai_api/routers/openai/audio.py | 3 +- src/leapfrogai_api/routers/openai/chat.py | 2 +- .../routers/openai/completions.py | 4 +- .../routers/openai/embeddings.py | 7 +- src/leapfrogai_api/routers/openai/runs.py | 7 +- src/leapfrogai_api/routers/openai/threads.py | 3 +- src/leapfrogai_api/typedef/__init__.py | 3 +- .../typedef/assistants/__init__.py | 1 + .../{ => assistants}/assistant_types.py | 0 src/leapfrogai_api/typedef/audio/__init__.py | 6 + .../{requests => audio}/audio_types.py | 0 src/leapfrogai_api/typedef/auth/__init__.py | 4 + .../typedef/{requests => auth}/auth_types.py | 0 src/leapfrogai_api/typedef/chat/__init__.py | 9 + src/leapfrogai_api/typedef/chat/chat_types.py | 144 +++++++++ src/leapfrogai_api/typedef/common.py | 17 + .../typedef/completion/__init__.py | 5 + .../typedef/completion/completion_types.py | 68 ++++ .../typedef/embeddings/__init__.py | 5 + .../typedef/embeddings/embedding_types.py | 55 ++++ src/leapfrogai_api/typedef/models/__init__.py | 1 + .../typedef/{ => models}/model_types.py | 0 .../typedef/requests/__init__.py | 20 -- src/leapfrogai_api/typedef/runs/__init__.py | 3 + .../typedef/{requests => runs}/run_create.py | 3 +- .../{requests => runs}/run_create_base.py | 0 .../typedef/{requests => runs}/run_modify.py | 0 .../typedef/threads/__init__.py | 5 + .../{requests => threads}/thread_create.py | 0 .../{requests => threads}/thread_modify.py | 0 .../thread_run_create_params_request.py | 4 +- src/leapfrogai_api/utils/config.py | 2 +- tests/integration/api/test_runs.py | 4 +- 39 files changed, 371 insertions(+), 351 deletions(-) create mode 100644 src/leapfrogai_api/typedef/assistants/__init__.py rename src/leapfrogai_api/typedef/{ => assistants}/assistant_types.py (100%) create mode 100644 src/leapfrogai_api/typedef/audio/__init__.py rename src/leapfrogai_api/typedef/{requests => audio}/audio_types.py (100%) create mode 100644 src/leapfrogai_api/typedef/auth/__init__.py rename src/leapfrogai_api/typedef/{requests => auth}/auth_types.py (100%) create mode 100644 src/leapfrogai_api/typedef/chat/__init__.py create mode 100644 src/leapfrogai_api/typedef/chat/chat_types.py create mode 100644 src/leapfrogai_api/typedef/common.py create mode 100644 src/leapfrogai_api/typedef/completion/__init__.py create mode 100644 src/leapfrogai_api/typedef/completion/completion_types.py create mode 100644 src/leapfrogai_api/typedef/embeddings/__init__.py create mode 100644 src/leapfrogai_api/typedef/embeddings/embedding_types.py create mode 100644 src/leapfrogai_api/typedef/models/__init__.py rename src/leapfrogai_api/typedef/{ => models}/model_types.py (100%) delete mode 100644 src/leapfrogai_api/typedef/requests/__init__.py create mode 100644 src/leapfrogai_api/typedef/runs/__init__.py rename src/leapfrogai_api/typedef/{requests => runs}/run_create.py (98%) rename src/leapfrogai_api/typedef/{requests => runs}/run_create_base.py (100%) rename src/leapfrogai_api/typedef/{requests => runs}/run_modify.py (100%) create mode 100644 src/leapfrogai_api/typedef/threads/__init__.py rename src/leapfrogai_api/typedef/{requests => threads}/thread_create.py (100%) rename src/leapfrogai_api/typedef/{requests => threads}/thread_modify.py (100%) rename src/leapfrogai_api/{routers/openai/requests => typedef/threads}/thread_run_create_params_request.py (98%) diff --git a/src/leapfrogai_api/backend/grpc_client.py b/src/leapfrogai_api/backend/grpc_client.py index b4f4de366..bf9b98718 100644 --- a/src/leapfrogai_api/backend/grpc_client.py +++ b/src/leapfrogai_api/backend/grpc_client.py @@ -3,26 +3,31 @@ from typing import Iterator, AsyncGenerator, Any, List import grpc from fastapi.responses import StreamingResponse + import leapfrogai_sdk as lfai from leapfrogai_api.backend.helpers import recv_chat, recv_completion -from leapfrogai_api.backend.types import ( +from leapfrogai_sdk.chat.chat_pb2 import ( + ChatCompletionResponse as ProtobufChatCompletionResponse, +) +from leapfrogai_api.utils.config import Model +from leapfrogai_api.typedef.audio import ( + CreateTranscriptionResponse, + CreateTranslationResponse, +) +from leapfrogai_api.typedef.chat import ( ChatChoice, ChatCompletionResponse, ChatMessage, +) +from leapfrogai_api.typedef.completion import ( CompletionChoice, CompletionResponse, +) +from leapfrogai_api.typedef import Usage +from leapfrogai_api.typedef.embeddings import ( CreateEmbeddingResponse, EmbeddingResponseData, - Usage, -) -from leapfrogai_api.typedef.requests import ( - CreateTranscriptionResponse, - CreateTranslationResponse, ) -from leapfrogai_sdk.chat.chat_pb2 import ( - ChatCompletionResponse as ProtobufChatCompletionResponse, -) -from leapfrogai_api.utils.config import Model async def stream_completion(model: Model, request: lfai.CompletionRequest): diff --git a/src/leapfrogai_api/backend/helpers.py b/src/leapfrogai_api/backend/helpers.py index 2fec1f6eb..2007b9725 100644 --- a/src/leapfrogai_api/backend/helpers.py +++ b/src/leapfrogai_api/backend/helpers.py @@ -5,12 +5,16 @@ import grpc from typing import BinaryIO, Iterator, AsyncGenerator, Any import leapfrogai_sdk as lfai -from leapfrogai_api.backend.types import ( +from leapfrogai_api.typedef.chat import ( ChatCompletionResponse, ChatDelta, ChatStreamChoice, +) +from leapfrogai_api.typedef.completion import ( CompletionChoice, CompletionResponse, +) +from leapfrogai_api.typedef import ( Usage, ) diff --git a/src/leapfrogai_api/backend/thread_runner.py b/src/leapfrogai_api/backend/thread_runner.py index effb27648..fd0eb932f 100644 --- a/src/leapfrogai_api/backend/thread_runner.py +++ b/src/leapfrogai_api/backend/thread_runner.py @@ -45,14 +45,14 @@ from leapfrogai_sdk.chat.chat_pb2 import ( ChatCompletionResponse as ProtobufChatCompletionResponse, ) -from leapfrogai_api.backend.types import ( +from leapfrogai_api.typedef.chat import ( ChatMessage, - SearchResponse, ChatCompletionResponse, ChatCompletionRequest, ChatChoice, ) -from leapfrogai_api.typedef.requests import RunCreateParamsRequest +from leapfrogai_api.typedef.runs import RunCreateParamsRequest +from leapfrogai_api.backend.types import SearchResponse class ThreadRunner(BaseModel): diff --git a/src/leapfrogai_api/backend/types.py b/src/leapfrogai_api/backend/types.py index dd3dd359d..679725fe3 100644 --- a/src/leapfrogai_api/backend/types.py +++ b/src/leapfrogai_api/backend/types.py @@ -14,31 +14,9 @@ ToolResourcesFileSearchVectorStoreChunkingStrategy, ToolResourcesFileSearchVectorStoreChunkingStrategyAuto, ) -from openai.types.beta.threads.text_content_block_param import TextContentBlockParam from openai.types.beta.vector_store import ExpiresAfter from pydantic import BaseModel, Field -from leapfrogai_api.typedef.constants import DEFAULT_MAX_COMPLETION_TOKENS - -########## -# GENERIC -########## - - -class Usage(BaseModel): - """Usage object.""" - - prompt_tokens: int = Field( - ..., description="The number of tokens used in the prompt." - ) - completion_tokens: int | None = Field( - default=DEFAULT_MAX_COMPLETION_TOKENS, - description="The number of tokens generated in the completion.", - ) - total_tokens: int = Field( - ..., description="The total number of tokens used (prompt + completion)." - ) - ########## # MODELS @@ -80,279 +58,6 @@ class ModelResponse(BaseModel): ) -############ -# COMPLETION -############ - - -class CompletionRequest(BaseModel): - """Request object for completion.""" - - model: str = Field( - ..., - description="The ID of the model to use for completion.", - example="llama-cpp-python", - ) - prompt: str | list[int] = Field( - ..., - description="The prompt to generate completions for. Can be a string or a list of integers representing token IDs.", - examples=["Once upon a time,"], - ) - stream: bool = Field( - False, description="Whether to stream the results as they become available." - ) - max_tokens: int | None = Field( - default=DEFAULT_MAX_COMPLETION_TOKENS, - description="The maximum number of tokens to generate.", - ge=1, - ) - temperature: float | None = Field( - 1.0, - description="Sampling temperature to use. Higher values mean more random completions. Use lower values for more deterministic completions. The upper limit may vary depending on the backend used.", - ge=0.0, - ) - - -class CompletionChoice(BaseModel): - """Choice object for completion.""" - - index: int = Field(..., description="The index of this completion choice.") - text: str = Field(..., description="The generated text for this completion choice.") - logprobs: object | None = Field( - None, - description="Log probabilities for the generated tokens. Only returned if requested.", - ) - finish_reason: str = Field( - "", description="The reason why the completion finished.", example="length" - ) - - -class CompletionResponse(BaseModel): - """Response object for completion.""" - - id: str = Field("", description="A unique identifier for this completion response.") - object: Literal["completion"] = Field( - "completion", - description="The object type, which is always 'completion' for this response.", - ) - created: int = Field( - 0, - description="The Unix timestamp (in seconds) of when the completion was created.", - ) - model: str = Field("", description="The ID of the model used for the completion.") - choices: list[CompletionChoice] = Field( - ..., description="A list of generated completions." - ) - usage: Usage | None = Field( - None, description="Usage statistics for the completion request." - ) - - -########## -# CHAT -########## - - -class ChatFunction(BaseModel): - """Function object for chat completion.""" - - name: str - parameters: dict[str, object] - description: str - - -class ChatMessage(BaseModel): - """Message object for chat completion.""" - - role: Literal["user", "assistant", "system", "function"] = Field( - default="user", - description="The role of the message author.", - examples=["user", "assistant", "system", "function"], - ) - content: str | list[TextContentBlockParam] = Field( - default="", - description="The content of the message. Can be a string or a list of text content blocks.", - examples=[ - "Hello, how are you?", - [{"type": "text", "text": "Hello, how are you?"}], - ], - ) - - -class ChatDelta(BaseModel): - """Delta object for chat completion.""" - - role: str = Field( - default="", - description="The role of the author of this message delta.", - examples=["assistant"], - ) - content: str | None = Field( - default="", description="The content of this message delta." - ) - - -class ChatCompletionRequest(BaseModel): - """Request object for chat completion.""" - - model: str = Field( - default="", - description="The ID of the model to use for chat completion.", - examples=["llama-cpp-python"], - ) - messages: list[ChatMessage] = Field( - default=[], description="A list of messages comprising the conversation so far." - ) - functions: list | None = Field( - default=None, - description="A list of functions that the model may generate JSON inputs for.", - ) - temperature: float | None = Field( - default=1.0, - description="What sampling temperature to use. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. The upper limit may vary depending on the backend used.", - ge=0, - ) - top_p: float | None = Field( - default=1, - description="An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.", - gt=0, - le=1, - ) - stream: bool | None = Field( - default=False, - description="If set, partial message deltas will be sent. Tokens will be sent as data-only server-sent events as they become available, with the stream terminated by a data: [DONE] message.", - ) - stop: str | None = Field( - default=None, - description="Sequences that determine where the API will stop generating further tokens.", - ) - max_tokens: int | None = Field( - default=DEFAULT_MAX_COMPLETION_TOKENS, - description="The maximum number of tokens to generate in the chat completion.", - gt=0, - ) - - -class ChatChoice(BaseModel): - """Choice object for chat completion.""" - - index: int = Field( - default=0, description="The index of this choice among the list of choices." - ) - message: ChatMessage = Field( - default=ChatMessage(), description="The message content for this choice." - ) - finish_reason: str | None = Field( - default="", - description="The reason why the model stopped generating tokens.", - examples=["stop", "length"], - ) - - -class ChatStreamChoice(BaseModel): - """Stream choice object for chat completion.""" - - index: int = Field( - default=0, description="The index of this choice among the list of choices." - ) - delta: ChatDelta = Field( - default=ChatDelta(), description="The delta content for this streaming choice." - ) - finish_reason: str | None = Field( - default="", - description="The reason why the model stopped generating tokens.", - examples=["stop", "length"], - ) - - -class ChatCompletionResponse(BaseModel): - """Response object for chat completion.""" - - id: str = Field( - default="", description="A unique identifier for the chat completion." - ) - object: str = Field( - default="chat.completion", - description="The object type, which is always 'chat.completion' for this response.", - ) - created: int = Field( - default=0, - description="The Unix timestamp (in seconds) of when the chat completion was created.", - ) - model: str = Field( - default="", description="The ID of the model used for the chat completion." - ) - choices: list[ChatChoice] | list[ChatStreamChoice] = Field( - default=[], - description="A list of chat completion choices. Can be either ChatChoice or ChatStreamChoice depending on whether streaming is enabled.", - ) - usage: Usage | None = Field( - default=None, description="Usage statistics for the completion request." - ) - - -############# -# EMBEDDINGS -############# - - -class CreateEmbeddingRequest(BaseModel): - """Request object for creating embeddings.""" - - model: str = Field( - description="The ID of the model to use for generating embeddings.", - examples=["text-embeddings"], - ) - input: str | list[str] | list[int] | list[list[int]] = Field( - description="The text to generate embeddings for. Can be a string, array of strings, array of tokens, or array of token arrays.", - examples=["The quick brown fox jumps over the lazy dog", ["Hello", "World"]], - ) - - -class EmbeddingResponseData(BaseModel): - """Response object for embeddings.""" - - embedding: list[float] = Field( - default=[], - description="The embedding vector representing the input text.", - ) - index: int = Field( - default=0, - description="The index of the embedding in the list of generated embeddings.", - ) - object: str = Field( - default="embedding", - description="The object type, which is always 'embedding'.", - ) - - -class CreateEmbeddingResponse(BaseModel): - """Response object for embeddings.""" - - data: list[EmbeddingResponseData] = Field( - default=[], - description="A list of embedding objects.", - ) - model: str = Field( - default="", - examples=["text-embeddings"], - description="The ID of the model used for generating the embeddings.", - ) - object: str = Field( - default="list", - description="The object type, which is always 'list' for embedding responses.", - ) - usage: Usage | None = Field( - default=None, - description="Usage statistics for the API call.", - ) - - -########## -# AUDIO -########## - - ############# # FILES ############# diff --git a/src/leapfrogai_api/routers/leapfrogai/auth.py b/src/leapfrogai_api/routers/leapfrogai/auth.py index b34e7539d..e5aea8c49 100644 --- a/src/leapfrogai_api/routers/leapfrogai/auth.py +++ b/src/leapfrogai_api/routers/leapfrogai/auth.py @@ -7,7 +7,7 @@ from leapfrogai_api.routers.supabase_session import Session from leapfrogai_api.data.crud_api_key import APIKeyItem, CRUDAPIKey -from leapfrogai_api.typedef.requests import CreateAPIKeyRequest, ModifyAPIKeyRequest +from leapfrogai_api.typedef.auth import CreateAPIKeyRequest, ModifyAPIKeyRequest router = APIRouter(prefix="/leapfrogai/v1/auth", tags=["leapfrogai/auth"]) diff --git a/src/leapfrogai_api/routers/openai/assistants.py b/src/leapfrogai_api/routers/openai/assistants.py index 9606cbb3b..e1cb86643 100644 --- a/src/leapfrogai_api/routers/openai/assistants.py +++ b/src/leapfrogai_api/routers/openai/assistants.py @@ -2,8 +2,9 @@ from fastapi import HTTPException, APIRouter, status from openai.types.beta import Assistant, AssistantDeleted + from leapfrogai_api.backend.helpers import object_or_default -from leapfrogai_api.typedef import ListAssistantsResponse +from leapfrogai_api.typedef.assistants import ListAssistantsResponse from leapfrogai_api.routers.openai.requests.create_modify_assistant_request import ( CreateAssistantRequest, ModifyAssistantRequest, diff --git a/src/leapfrogai_api/routers/openai/audio.py b/src/leapfrogai_api/routers/openai/audio.py index 6ef5d4436..f60e7b0f7 100644 --- a/src/leapfrogai_api/routers/openai/audio.py +++ b/src/leapfrogai_api/routers/openai/audio.py @@ -3,9 +3,10 @@ from itertools import chain from typing import Annotated from fastapi import HTTPException, APIRouter, Depends + from leapfrogai_api.backend.grpc_client import create_transcription, create_translation from leapfrogai_api.backend.helpers import read_chunks -from leapfrogai_api.typedef.requests import ( +from leapfrogai_api.typedef.audio import ( CreateTranscriptionRequest, CreateTranscriptionResponse, CreateTranslationRequest, diff --git a/src/leapfrogai_api/routers/openai/chat.py b/src/leapfrogai_api/routers/openai/chat.py index 23d09beb4..a204dcbe4 100644 --- a/src/leapfrogai_api/routers/openai/chat.py +++ b/src/leapfrogai_api/routers/openai/chat.py @@ -9,7 +9,7 @@ stream_chat_completion_raw, ) from leapfrogai_api.backend.helpers import grpc_chat_role -from leapfrogai_api.backend.types import ChatCompletionRequest +from leapfrogai_api.typedef.chat import ChatCompletionRequest from leapfrogai_api.routers.supabase_session import Session from leapfrogai_api.utils import get_model_config from leapfrogai_api.utils.config import Config diff --git a/src/leapfrogai_api/routers/openai/completions.py b/src/leapfrogai_api/routers/openai/completions.py index 0c0e4fd76..97b1cf3dc 100644 --- a/src/leapfrogai_api/routers/openai/completions.py +++ b/src/leapfrogai_api/routers/openai/completions.py @@ -6,9 +6,7 @@ completion, stream_completion, ) -from leapfrogai_api.backend.types import ( - CompletionRequest, -) +from leapfrogai_api.typedef.completion import CompletionRequest from leapfrogai_api.routers.supabase_session import Session from leapfrogai_api.utils import get_model_config from leapfrogai_api.utils.config import Config diff --git a/src/leapfrogai_api/routers/openai/embeddings.py b/src/leapfrogai_api/routers/openai/embeddings.py index fd7a8c7fc..faeaa07f4 100644 --- a/src/leapfrogai_api/routers/openai/embeddings.py +++ b/src/leapfrogai_api/routers/openai/embeddings.py @@ -1,11 +1,14 @@ """FastAPI router for OpenAI embeddings API.""" from typing import Annotated - from fastapi import APIRouter, Depends, HTTPException, status + import leapfrogai_sdk as lfai from leapfrogai_api.backend.grpc_client import create_embeddings -from leapfrogai_api.backend.types import CreateEmbeddingRequest, CreateEmbeddingResponse +from leapfrogai_api.typedef.embeddings import ( + CreateEmbeddingRequest, + CreateEmbeddingResponse, +) from leapfrogai_api.routers.supabase_session import Session from leapfrogai_api.utils import get_model_config from leapfrogai_api.utils.config import Config diff --git a/src/leapfrogai_api/routers/openai/runs.py b/src/leapfrogai_api/routers/openai/runs.py index 9c55eaa24..20baf5b97 100644 --- a/src/leapfrogai_api/routers/openai/runs.py +++ b/src/leapfrogai_api/routers/openai/runs.py @@ -5,9 +5,7 @@ from fastapi.responses import StreamingResponse from openai.types.beta.threads import Run from openai.pagination import SyncCursorPage -from leapfrogai_api.routers.openai.requests.thread_run_create_params_request import ( - ThreadRunCreateParamsRequest, -) + from leapfrogai_api.data.crud_run import CRUDRun from leapfrogai_api.data.crud_thread import CRUDThread from leapfrogai_api.routers.supabase_session import Session @@ -15,7 +13,8 @@ validate_assistant_tool, validate_assistant_tool_choice_option, ) -from leapfrogai_api.typedef.requests import RunCreateParamsRequest, ModifyRunRequest +from leapfrogai_api.typedef.threads import ThreadRunCreateParamsRequest +from leapfrogai_api.typedef.runs import RunCreateParamsRequest, ModifyRunRequest from leapfrogai_api.backend.thread_runner import ThreadRunner router = APIRouter(prefix="/openai/v1/threads", tags=["openai/threads/runs"]) diff --git a/src/leapfrogai_api/routers/openai/threads.py b/src/leapfrogai_api/routers/openai/threads.py index c0c542aa1..3461ca27a 100644 --- a/src/leapfrogai_api/routers/openai/threads.py +++ b/src/leapfrogai_api/routers/openai/threads.py @@ -2,7 +2,8 @@ from fastapi import HTTPException, APIRouter, status from openai.types.beta import Thread, ThreadDeleted -from leapfrogai_api.typedef.requests import ModifyThreadRequest, CreateThreadRequest + +from leapfrogai_api.typedef.threads import ModifyThreadRequest, CreateThreadRequest from leapfrogai_api.data.crud_thread import CRUDThread from leapfrogai_api.routers.supabase_session import Session from leapfrogai_api.utils.validate_tools import ( diff --git a/src/leapfrogai_api/typedef/__init__.py b/src/leapfrogai_api/typedef/__init__.py index 2e246394c..d65f47391 100644 --- a/src/leapfrogai_api/typedef/__init__.py +++ b/src/leapfrogai_api/typedef/__init__.py @@ -1,2 +1 @@ -from .model_types import Model as Model -from .assistant_types import ListAssistantsResponse as ListAssistantsResponse +from .common import Usage as Usage diff --git a/src/leapfrogai_api/typedef/assistants/__init__.py b/src/leapfrogai_api/typedef/assistants/__init__.py new file mode 100644 index 000000000..0bb259a61 --- /dev/null +++ b/src/leapfrogai_api/typedef/assistants/__init__.py @@ -0,0 +1 @@ +from .assistant_types import ListAssistantsResponse as ListAssistantsResponse diff --git a/src/leapfrogai_api/typedef/assistant_types.py b/src/leapfrogai_api/typedef/assistants/assistant_types.py similarity index 100% rename from src/leapfrogai_api/typedef/assistant_types.py rename to src/leapfrogai_api/typedef/assistants/assistant_types.py diff --git a/src/leapfrogai_api/typedef/audio/__init__.py b/src/leapfrogai_api/typedef/audio/__init__.py new file mode 100644 index 000000000..7ff0f5d03 --- /dev/null +++ b/src/leapfrogai_api/typedef/audio/__init__.py @@ -0,0 +1,6 @@ +from .audio_types import ( + CreateTranscriptionRequest as CreateTranscriptionRequest, + CreateTranscriptionResponse as CreateTranscriptionResponse, + CreateTranslationRequest as CreateTranslationRequest, + CreateTranslationResponse as CreateTranslationResponse, +) diff --git a/src/leapfrogai_api/typedef/requests/audio_types.py b/src/leapfrogai_api/typedef/audio/audio_types.py similarity index 100% rename from src/leapfrogai_api/typedef/requests/audio_types.py rename to src/leapfrogai_api/typedef/audio/audio_types.py diff --git a/src/leapfrogai_api/typedef/auth/__init__.py b/src/leapfrogai_api/typedef/auth/__init__.py new file mode 100644 index 000000000..7356275ec --- /dev/null +++ b/src/leapfrogai_api/typedef/auth/__init__.py @@ -0,0 +1,4 @@ +from .auth_types import ( + CreateAPIKeyRequest as CreateAPIKeyRequest, + ModifyAPIKeyRequest as ModifyAPIKeyRequest, +) diff --git a/src/leapfrogai_api/typedef/requests/auth_types.py b/src/leapfrogai_api/typedef/auth/auth_types.py similarity index 100% rename from src/leapfrogai_api/typedef/requests/auth_types.py rename to src/leapfrogai_api/typedef/auth/auth_types.py diff --git a/src/leapfrogai_api/typedef/chat/__init__.py b/src/leapfrogai_api/typedef/chat/__init__.py new file mode 100644 index 000000000..3b7836b55 --- /dev/null +++ b/src/leapfrogai_api/typedef/chat/__init__.py @@ -0,0 +1,9 @@ +from .chat_types import ( + ChatFunction as ChatFunction, + ChatMessage as ChatMessage, + ChatDelta as ChatDelta, + ChatChoice as ChatChoice, + ChatStreamChoice as ChatStreamChoice, + ChatCompletionRequest as ChatCompletionRequest, + ChatCompletionResponse as ChatCompletionResponse, +) diff --git a/src/leapfrogai_api/typedef/chat/chat_types.py b/src/leapfrogai_api/typedef/chat/chat_types.py new file mode 100644 index 000000000..c851681ad --- /dev/null +++ b/src/leapfrogai_api/typedef/chat/chat_types.py @@ -0,0 +1,144 @@ +from pydantic import BaseModel, Field +from typing import Literal +from openai.types.beta.threads.text_content_block_param import TextContentBlockParam + +from ..common import Usage +from ..constants import DEFAULT_MAX_COMPLETION_TOKENS + + +class ChatFunction(BaseModel): + """Function object for chat completion.""" + + name: str + parameters: dict[str, object] + description: str + + +class ChatMessage(BaseModel): + """Message object for chat completion.""" + + role: Literal["user", "assistant", "system", "function"] = Field( + default="user", + description="The role of the message author.", + examples=["user", "assistant", "system", "function"], + ) + content: str | list[TextContentBlockParam] = Field( + default="", + description="The content of the message. Can be a string or a list of text content blocks.", + examples=[ + "Hello, how are you?", + [{"type": "text", "text": "Hello, how are you?"}], + ], + ) + + +class ChatDelta(BaseModel): + """Delta object for chat completion.""" + + role: str = Field( + default="", + description="The role of the author of this message delta.", + examples=["assistant"], + ) + content: str | None = Field( + default="", description="The content of this message delta." + ) + + +class ChatChoice(BaseModel): + """Choice object for chat completion.""" + + index: int = Field( + default=0, description="The index of this choice among the list of choices." + ) + message: ChatMessage = Field( + default=ChatMessage(), description="The message content for this choice." + ) + finish_reason: str | None = Field( + default="", + description="The reason why the model stopped generating tokens.", + examples=["stop", "length"], + ) + + +class ChatStreamChoice(BaseModel): + """Stream choice object for chat completion.""" + + index: int = Field( + default=0, description="The index of this choice among the list of choices." + ) + delta: ChatDelta = Field( + default=ChatDelta(), description="The delta content for this streaming choice." + ) + finish_reason: str | None = Field( + default="", + description="The reason why the model stopped generating tokens.", + examples=["stop", "length"], + ) + + +class ChatCompletionRequest(BaseModel): + """Request object for chat completion.""" + + model: str = Field( + default="", + description="The ID of the model to use for chat completion.", + examples=["llama-cpp-python"], + ) + messages: list[ChatMessage] = Field( + default=[], description="A list of messages comprising the conversation so far." + ) + functions: list | None = Field( + default=None, + description="A list of functions that the model may generate JSON inputs for.", + ) + temperature: float | None = Field( + default=1.0, + description="What sampling temperature to use. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. The upper limit may vary depending on the backend used.", + ge=0, + ) + top_p: float | None = Field( + default=1, + description="An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.", + gt=0, + le=1, + ) + stream: bool | None = Field( + default=False, + description="If set, partial message deltas will be sent. Tokens will be sent as data-only server-sent events as they become available, with the stream terminated by a data: [DONE] message.", + ) + stop: str | None = Field( + default=None, + description="Sequences that determine where the API will stop generating further tokens.", + ) + max_tokens: int | None = Field( + default=DEFAULT_MAX_COMPLETION_TOKENS, + description="The maximum number of tokens to generate in the chat completion.", + gt=0, + ) + + +class ChatCompletionResponse(BaseModel): + """Response object for chat completion.""" + + id: str = Field( + default="", description="A unique identifier for the chat completion." + ) + object: str = Field( + default="chat.completion", + description="The object type, which is always 'chat.completion' for this response.", + ) + created: int = Field( + default=0, + description="The Unix timestamp (in seconds) of when the chat completion was created.", + ) + model: str = Field( + default="", description="The ID of the model used for the chat completion." + ) + choices: list[ChatChoice] | list[ChatStreamChoice] = Field( + default=[], + description="A list of chat completion choices. Can be either ChatChoice or ChatStreamChoice depending on whether streaming is enabled.", + ) + usage: Usage | None = Field( + default=None, description="Usage statistics for the completion request." + ) diff --git a/src/leapfrogai_api/typedef/common.py b/src/leapfrogai_api/typedef/common.py new file mode 100644 index 000000000..d050636c1 --- /dev/null +++ b/src/leapfrogai_api/typedef/common.py @@ -0,0 +1,17 @@ +from pydantic import BaseModel, Field +from leapfrogai_api.typedef.constants import DEFAULT_MAX_COMPLETION_TOKENS + + +class Usage(BaseModel): + """Usage object.""" + + prompt_tokens: int = Field( + ..., description="The number of tokens used in the prompt." + ) + completion_tokens: int | None = Field( + default=DEFAULT_MAX_COMPLETION_TOKENS, + description="The number of tokens generated in the completion.", + ) + total_tokens: int = Field( + ..., description="The total number of tokens used (prompt + completion)." + ) diff --git a/src/leapfrogai_api/typedef/completion/__init__.py b/src/leapfrogai_api/typedef/completion/__init__.py new file mode 100644 index 000000000..b5f8d1f95 --- /dev/null +++ b/src/leapfrogai_api/typedef/completion/__init__.py @@ -0,0 +1,5 @@ +from .completion_types import ( + CompletionChoice as CompletionChoice, + CompletionRequest as CompletionRequest, + CompletionResponse as CompletionResponse, +) diff --git a/src/leapfrogai_api/typedef/completion/completion_types.py b/src/leapfrogai_api/typedef/completion/completion_types.py new file mode 100644 index 000000000..810920a60 --- /dev/null +++ b/src/leapfrogai_api/typedef/completion/completion_types.py @@ -0,0 +1,68 @@ +from pydantic import BaseModel, Field +from typing import Literal + +from ..common import Usage +from ..constants import DEFAULT_MAX_COMPLETION_TOKENS + + +class CompletionChoice(BaseModel): + """Choice object for completion.""" + + index: int = Field(..., description="The index of this completion choice.") + text: str = Field(..., description="The generated text for this completion choice.") + logprobs: object | None = Field( + None, + description="Log probabilities for the generated tokens. Only returned if requested.", + ) + finish_reason: str = Field( + "", description="The reason why the completion finished.", example="length" + ) + + +class CompletionRequest(BaseModel): + """Request object for completion.""" + + model: str = Field( + ..., + description="The ID of the model to use for completion.", + example="llama-cpp-python", + ) + prompt: str | list[int] = Field( + ..., + description="The prompt to generate completions for. Can be a string or a list of integers representing token IDs.", + examples=["Once upon a time,"], + ) + stream: bool = Field( + False, description="Whether to stream the results as they become available." + ) + max_tokens: int | None = Field( + default=DEFAULT_MAX_COMPLETION_TOKENS, + description="The maximum number of tokens to generate.", + ge=1, + ) + temperature: float | None = Field( + 1.0, + description="Sampling temperature to use. Higher values mean more random completions. Use lower values for more deterministic completions. The upper limit may vary depending on the backend used.", + ge=0.0, + ) + + +class CompletionResponse(BaseModel): + """Response object for completion.""" + + id: str = Field("", description="A unique identifier for this completion response.") + object: Literal["completion"] = Field( + "completion", + description="The object type, which is always 'completion' for this response.", + ) + created: int = Field( + 0, + description="The Unix timestamp (in seconds) of when the completion was created.", + ) + model: str = Field("", description="The ID of the model used for the completion.") + choices: list[CompletionChoice] = Field( + ..., description="A list of generated completions." + ) + usage: Usage | None = Field( + None, description="Usage statistics for the completion request." + ) diff --git a/src/leapfrogai_api/typedef/embeddings/__init__.py b/src/leapfrogai_api/typedef/embeddings/__init__.py new file mode 100644 index 000000000..78a246fb3 --- /dev/null +++ b/src/leapfrogai_api/typedef/embeddings/__init__.py @@ -0,0 +1,5 @@ +from .embedding_types import ( + EmbeddingResponseData as EmbeddingResponseData, + CreateEmbeddingRequest as CreateEmbeddingRequest, + CreateEmbeddingResponse as CreateEmbeddingResponse, +) diff --git a/src/leapfrogai_api/typedef/embeddings/embedding_types.py b/src/leapfrogai_api/typedef/embeddings/embedding_types.py new file mode 100644 index 000000000..c7a3ce581 --- /dev/null +++ b/src/leapfrogai_api/typedef/embeddings/embedding_types.py @@ -0,0 +1,55 @@ +from pydantic import BaseModel, Field + +from ..common import Usage + + +class EmbeddingResponseData(BaseModel): + """Response object for embeddings.""" + + embedding: list[float] = Field( + default=[], + description="The embedding vector representing the input text.", + ) + index: int = Field( + default=0, + description="The index of the embedding in the list of generated embeddings.", + ) + object: str = Field( + default="embedding", + description="The object type, which is always 'embedding'.", + ) + + +class CreateEmbeddingRequest(BaseModel): + """Request object for creating embeddings.""" + + model: str = Field( + description="The ID of the model to use for generating embeddings.", + examples=["text-embeddings"], + ) + input: str | list[str] | list[int] | list[list[int]] = Field( + description="The text to generate embeddings for. Can be a string, array of strings, array of tokens, or array of token arrays.", + examples=["The quick brown fox jumps over the lazy dog", ["Hello", "World"]], + ) + + +class CreateEmbeddingResponse(BaseModel): + """Response object for embeddings.""" + + data: list[EmbeddingResponseData] = Field( + default=[], + description="A list of embedding objects.", + ) + model: str = Field( + default="", + examples=["text-embeddings"], + description="The ID of the model used for generating the embeddings.", + ) + object: str = Field( + default="list", + description="The object type, which is always 'list' for embedding responses.", + ) + usage: Usage | None = Field( + default=None, + description="Usage statistics for the API call.", + ) diff --git a/src/leapfrogai_api/typedef/models/__init__.py b/src/leapfrogai_api/typedef/models/__init__.py new file mode 100644 index 000000000..27dbd81b5 --- /dev/null +++ b/src/leapfrogai_api/typedef/models/__init__.py @@ -0,0 +1 @@ +from .model_types import Model as Model diff --git a/src/leapfrogai_api/typedef/model_types.py b/src/leapfrogai_api/typedef/models/model_types.py similarity index 100% rename from src/leapfrogai_api/typedef/model_types.py rename to src/leapfrogai_api/typedef/models/model_types.py diff --git a/src/leapfrogai_api/typedef/requests/__init__.py b/src/leapfrogai_api/typedef/requests/__init__.py deleted file mode 100644 index 3d6c04f8e..000000000 --- a/src/leapfrogai_api/typedef/requests/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -from .auth_types import ( - CreateAPIKeyRequest as CreateAPIKeyRequest, - ModifyAPIKeyRequest as ModifyAPIKeyRequest, -) - -from .run_create_base import ( - RunCreateParamsRequestBase as RunCreateParamsRequestBase, -) -from .run_create import RunCreateParamsRequest as RunCreateParamsRequest -from .run_modify import ModifyRunRequest as ModifyRunRequest - -from .thread_create import CreateThreadRequest as CreateThreadRequest -from .thread_modify import ModifyThreadRequest as ModifyThreadRequest - -from .audio_types import ( - CreateTranscriptionRequest as CreateTranscriptionRequest, - CreateTranscriptionResponse as CreateTranscriptionResponse, - CreateTranslationRequest as CreateTranslationRequest, - CreateTranslationResponse as CreateTranslationResponse, -) diff --git a/src/leapfrogai_api/typedef/runs/__init__.py b/src/leapfrogai_api/typedef/runs/__init__.py new file mode 100644 index 000000000..0734ee139 --- /dev/null +++ b/src/leapfrogai_api/typedef/runs/__init__.py @@ -0,0 +1,3 @@ +from .run_create_base import RunCreateParamsRequestBase as RunCreateParamsRequestBase +from .run_create import RunCreateParamsRequest as RunCreateParamsRequest +from .run_modify import ModifyRunRequest as ModifyRunRequest diff --git a/src/leapfrogai_api/typedef/requests/run_create.py b/src/leapfrogai_api/typedef/runs/run_create.py similarity index 98% rename from src/leapfrogai_api/typedef/requests/run_create.py rename to src/leapfrogai_api/typedef/runs/run_create.py index 8e5430faa..917044a44 100644 --- a/src/leapfrogai_api/typedef/requests/run_create.py +++ b/src/leapfrogai_api/typedef/runs/run_create.py @@ -8,7 +8,8 @@ AdditionalMessageAttachmentToolFileSearch, ) from pydantic import Field -from leapfrogai_api.typedef.requests.run_create_base import ( + +from .run_create_base import ( RunCreateParamsRequestBase, ) from leapfrogai_api.routers.openai.requests.create_message_request import ( diff --git a/src/leapfrogai_api/typedef/requests/run_create_base.py b/src/leapfrogai_api/typedef/runs/run_create_base.py similarity index 100% rename from src/leapfrogai_api/typedef/requests/run_create_base.py rename to src/leapfrogai_api/typedef/runs/run_create_base.py diff --git a/src/leapfrogai_api/typedef/requests/run_modify.py b/src/leapfrogai_api/typedef/runs/run_modify.py similarity index 100% rename from src/leapfrogai_api/typedef/requests/run_modify.py rename to src/leapfrogai_api/typedef/runs/run_modify.py diff --git a/src/leapfrogai_api/typedef/threads/__init__.py b/src/leapfrogai_api/typedef/threads/__init__.py new file mode 100644 index 000000000..296605314 --- /dev/null +++ b/src/leapfrogai_api/typedef/threads/__init__.py @@ -0,0 +1,5 @@ +from .thread_create import CreateThreadRequest as CreateThreadRequest +from .thread_modify import ModifyThreadRequest as ModifyThreadRequest +from .thread_run_create_params_request import ( + ThreadRunCreateParamsRequest as ThreadRunCreateParamsRequest, +) diff --git a/src/leapfrogai_api/typedef/requests/thread_create.py b/src/leapfrogai_api/typedef/threads/thread_create.py similarity index 100% rename from src/leapfrogai_api/typedef/requests/thread_create.py rename to src/leapfrogai_api/typedef/threads/thread_create.py diff --git a/src/leapfrogai_api/typedef/requests/thread_modify.py b/src/leapfrogai_api/typedef/threads/thread_modify.py similarity index 100% rename from src/leapfrogai_api/typedef/requests/thread_modify.py rename to src/leapfrogai_api/typedef/threads/thread_modify.py diff --git a/src/leapfrogai_api/routers/openai/requests/thread_run_create_params_request.py b/src/leapfrogai_api/typedef/threads/thread_run_create_params_request.py similarity index 98% rename from src/leapfrogai_api/routers/openai/requests/thread_run_create_params_request.py rename to src/leapfrogai_api/typedef/threads/thread_run_create_params_request.py index 2ad6cddca..bbf17dd0e 100644 --- a/src/leapfrogai_api/routers/openai/requests/thread_run_create_params_request.py +++ b/src/leapfrogai_api/typedef/threads/thread_run_create_params_request.py @@ -24,7 +24,7 @@ from leapfrogai_api.routers.openai.requests.create_message_request import ( CreateMessageRequest, ) -from leapfrogai_api.typedef.requests import ( +from leapfrogai_api.typedef.runs import ( RunCreateParamsRequestBase, ) from leapfrogai_api.data.crud_run import CRUDRun @@ -32,7 +32,7 @@ from_content_param_to_content, from_assistant_stream_event_to_str, ) -from leapfrogai_api.typedef.requests import CreateThreadRequest +from leapfrogai_api.typedef.threads import CreateThreadRequest from leapfrogai_api.routers.supabase_session import Session diff --git a/src/leapfrogai_api/utils/config.py b/src/leapfrogai_api/utils/config.py index f7d6b9d47..9ef327f51 100644 --- a/src/leapfrogai_api/utils/config.py +++ b/src/leapfrogai_api/utils/config.py @@ -6,7 +6,7 @@ import yaml from watchfiles import Change, awatch -from leapfrogai_api.typedef import Model +from leapfrogai_api.typedef.models import Model logger = logging.getLogger(__name__) diff --git a/tests/integration/api/test_runs.py b/tests/integration/api/test_runs.py index 5220fe7fd..0134280a8 100644 --- a/tests/integration/api/test_runs.py +++ b/tests/integration/api/test_runs.py @@ -18,10 +18,10 @@ from leapfrogai_api.routers.openai.requests.create_thread_request import ( CreateThreadRequest, ) -from leapfrogai_api.typedef.requests import ( +from leapfrogai_api.typedef.runs import ( RunCreateParamsRequest, ) -from leapfrogai_api.routers.openai.requests.thread_run_create_params_request import ( +from leapfrogai_api.typedef.threads import ( ThreadRunCreateParamsRequest, ) From b221e0aa6ef734db53f905fe3500d2ae0ea3cca6 Mon Sep 17 00:00:00 2001 From: alekst23 Date: Tue, 3 Sep 2024 15:57:11 -0400 Subject: [PATCH 09/17] chore: message types --- src/leapfrogai_api/backend/thread_runner.py | 4 +--- src/leapfrogai_api/backend/types.py | 11 ----------- src/leapfrogai_api/routers/openai/messages.py | 7 +------ src/leapfrogai_api/typedef/messages/__init__.py | 4 ++++ .../messages/message_types.py} | 6 ++++++ src/leapfrogai_api/typedef/runs/run_create.py | 2 +- .../threads/thread_run_create_params_request.py | 7 +++---- tests/integration/api/test_messages.py | 2 +- tests/integration/api/test_runs.py | 2 +- tests/integration/api/test_threads.py | 2 +- 10 files changed, 19 insertions(+), 28 deletions(-) create mode 100644 src/leapfrogai_api/typedef/messages/__init__.py rename src/leapfrogai_api/{routers/openai/requests/create_message_request.py => typedef/messages/message_types.py} (94%) diff --git a/src/leapfrogai_api/backend/thread_runner.py b/src/leapfrogai_api/backend/thread_runner.py index fd0eb932f..b98f304e7 100644 --- a/src/leapfrogai_api/backend/thread_runner.py +++ b/src/leapfrogai_api/backend/thread_runner.py @@ -37,9 +37,6 @@ from leapfrogai_api.data.crud_assistant import CRUDAssistant, FilterAssistant from leapfrogai_api.data.crud_message import CRUDMessage from leapfrogai_api.routers.openai.chat import chat_complete, chat_complete_stream_raw -from leapfrogai_api.routers.openai.requests.create_message_request import ( - CreateMessageRequest, -) from leapfrogai_api.routers.supabase_session import Session from leapfrogai_api.utils import get_model_config from leapfrogai_sdk.chat.chat_pb2 import ( @@ -52,6 +49,7 @@ ChatChoice, ) from leapfrogai_api.typedef.runs import RunCreateParamsRequest +from leapfrogai_api.typedef.messages import CreateMessageRequest from leapfrogai_api.backend.types import SearchResponse diff --git a/src/leapfrogai_api/backend/types.py b/src/leapfrogai_api/backend/types.py index 679725fe3..81652affd 100644 --- a/src/leapfrogai_api/backend/types.py +++ b/src/leapfrogai_api/backend/types.py @@ -227,17 +227,6 @@ class ListVectorStoresResponse(BaseModel): ) -################ -# THREADS, RUNS, MESSAGES -################ - - -class ModifyMessageRequest(BaseModel): - """Request object for modifying a message.""" - - metadata: dict | None = Field(default=None, examples=[{}]) - - ################ # LEAPFROGAI Vector Stores ################ diff --git a/src/leapfrogai_api/routers/openai/messages.py b/src/leapfrogai_api/routers/openai/messages.py index d0fb41db0..e9826992d 100644 --- a/src/leapfrogai_api/routers/openai/messages.py +++ b/src/leapfrogai_api/routers/openai/messages.py @@ -3,12 +3,7 @@ from fastapi import HTTPException, APIRouter, status from openai.types.beta.threads import Message, MessageDeleted from openai.pagination import SyncCursorPage -from leapfrogai_api.backend.types import ( - ModifyMessageRequest, -) -from leapfrogai_api.routers.openai.requests.create_message_request import ( - CreateMessageRequest, -) +from leapfrogai_api.typedef.messages import CreateMessageRequest, ModifyMessageRequest from leapfrogai_api.data.crud_message import CRUDMessage from leapfrogai_api.routers.supabase_session import Session diff --git a/src/leapfrogai_api/typedef/messages/__init__.py b/src/leapfrogai_api/typedef/messages/__init__.py new file mode 100644 index 000000000..c29086c8b --- /dev/null +++ b/src/leapfrogai_api/typedef/messages/__init__.py @@ -0,0 +1,4 @@ +from .message_types import ( + CreateMessageRequest as CreateMessageRequest, + ModifyMessageRequest as ModifyMessageRequest, +) diff --git a/src/leapfrogai_api/routers/openai/requests/create_message_request.py b/src/leapfrogai_api/typedef/messages/message_types.py similarity index 94% rename from src/leapfrogai_api/routers/openai/requests/create_message_request.py rename to src/leapfrogai_api/typedef/messages/message_types.py index c32574d81..e1d8ffff1 100644 --- a/src/leapfrogai_api/routers/openai/requests/create_message_request.py +++ b/src/leapfrogai_api/typedef/messages/message_types.py @@ -73,3 +73,9 @@ async def create_message( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Unable to create message", ) from exc + + +class ModifyMessageRequest(BaseModel): + """Request object for modifying a message.""" + + metadata: dict | None = Field(default=None, examples=[{}]) diff --git a/src/leapfrogai_api/typedef/runs/run_create.py b/src/leapfrogai_api/typedef/runs/run_create.py index 917044a44..bedefa813 100644 --- a/src/leapfrogai_api/typedef/runs/run_create.py +++ b/src/leapfrogai_api/typedef/runs/run_create.py @@ -12,7 +12,7 @@ from .run_create_base import ( RunCreateParamsRequestBase, ) -from leapfrogai_api.routers.openai.requests.create_message_request import ( +from leapfrogai_api.typedef.messages import ( CreateMessageRequest, ) from leapfrogai_api.data.crud_run import CRUDRun diff --git a/src/leapfrogai_api/typedef/threads/thread_run_create_params_request.py b/src/leapfrogai_api/typedef/threads/thread_run_create_params_request.py index bbf17dd0e..b4ec02aa4 100644 --- a/src/leapfrogai_api/typedef/threads/thread_run_create_params_request.py +++ b/src/leapfrogai_api/typedef/threads/thread_run_create_params_request.py @@ -1,7 +1,8 @@ from __future__ import annotations - import logging from typing import Iterable, AsyncGenerator, Any +from pydantic import Field +from starlette.responses import StreamingResponse from openai.types.beta import Thread from openai.types.beta.assistant_stream_event import ThreadCreated @@ -18,10 +19,8 @@ ThreadMessageAttachmentToolFileSearch, ) from openai.types.beta.threads import MessageContent, Message, Run -from pydantic import Field -from starlette.responses import StreamingResponse -from leapfrogai_api.routers.openai.requests.create_message_request import ( +from leapfrogai_api.typedef.messages import ( CreateMessageRequest, ) from leapfrogai_api.typedef.runs import ( diff --git a/tests/integration/api/test_messages.py b/tests/integration/api/test_messages.py index 0018dd8a4..148b1889e 100644 --- a/tests/integration/api/test_messages.py +++ b/tests/integration/api/test_messages.py @@ -10,7 +10,7 @@ from leapfrogai_api.backend.types import ( ModifyMessageRequest, ) -from leapfrogai_api.routers.openai.requests.create_message_request import ( +from leapfrogai_api.typedef.messages.create_message_request import ( CreateMessageRequest, ) from leapfrogai_api.routers.openai.requests.create_thread_request import ( diff --git a/tests/integration/api/test_runs.py b/tests/integration/api/test_runs.py index 0134280a8..678586e1e 100644 --- a/tests/integration/api/test_runs.py +++ b/tests/integration/api/test_runs.py @@ -12,7 +12,7 @@ from leapfrogai_api.routers.openai.requests.create_modify_assistant_request import ( CreateAssistantRequest, ) -from leapfrogai_api.routers.openai.requests.create_message_request import ( +from leapfrogai_api.typedef.messages.create_message_request import ( CreateMessageRequest, ) from leapfrogai_api.routers.openai.requests.create_thread_request import ( diff --git a/tests/integration/api/test_threads.py b/tests/integration/api/test_threads.py index 41e93f4c1..95f8330a3 100644 --- a/tests/integration/api/test_threads.py +++ b/tests/integration/api/test_threads.py @@ -10,7 +10,7 @@ from leapfrogai_api.backend.types import ( ModifyThreadRequest, ) -from leapfrogai_api.routers.openai.requests.create_message_request import ( +from leapfrogai_api.typedef.messages.create_message_request import ( CreateMessageRequest, ) from leapfrogai_api.routers.openai.requests.create_thread_request import ( From 4a7728f5e6fccf129f4c0f84140d28d81d5cac13 Mon Sep 17 00:00:00 2001 From: alekst23 Date: Tue, 3 Sep 2024 15:59:44 -0400 Subject: [PATCH 10/17] chore: model types --- src/leapfrogai_api/backend/types.py | 40 ------------------- src/leapfrogai_api/routers/openai/models.py | 2 +- src/leapfrogai_api/typedef/models/__init__.py | 6 ++- .../typedef/models/model_types.py | 38 +++++++++++++++++- 4 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/leapfrogai_api/backend/types.py b/src/leapfrogai_api/backend/types.py index 81652affd..4b4117a15 100644 --- a/src/leapfrogai_api/backend/types.py +++ b/src/leapfrogai_api/backend/types.py @@ -18,46 +18,6 @@ from pydantic import BaseModel, Field -########## -# MODELS -########## - - -class ModelResponseModel(BaseModel): - """Represents a single model in the response.""" - - id: str = Field( - ..., - description="The unique identifier of the model.", - examples=["llama-cpp-python"], - ) - object: Literal["model"] = Field( - default="model", - description="The object type, which is always 'model' for this response.", - ) - created: int = Field( - default=0, - description="The Unix timestamp (in seconds) when the model was created. Always 0 for LeapfrogAI models.", - examples=[0], - ) - owned_by: Literal["leapfrogai"] = Field( - default="leapfrogai", - description="The organization that owns the model. Always 'leapfrogai' for LeapfrogAI models.", - ) - - -class ModelResponse(BaseModel): - """Response object for listing available models.""" - - object: Literal["list"] = Field( - default="list", - description="The object type, which is always 'list' for this response.", - ) - data: list[ModelResponseModel] = Field( - ..., description="A list of available models.", min_length=0 - ) - - ############# # FILES ############# diff --git a/src/leapfrogai_api/routers/openai/models.py b/src/leapfrogai_api/routers/openai/models.py index c71167d05..74ffc57e1 100644 --- a/src/leapfrogai_api/routers/openai/models.py +++ b/src/leapfrogai_api/routers/openai/models.py @@ -1,7 +1,7 @@ """OpenAI compliant models router.""" from fastapi import APIRouter -from leapfrogai_api.backend.types import ( +from leapfrogai_api.typedef.models import ( ModelResponse, ModelResponseModel, ) diff --git a/src/leapfrogai_api/typedef/models/__init__.py b/src/leapfrogai_api/typedef/models/__init__.py index 27dbd81b5..7be04f80b 100644 --- a/src/leapfrogai_api/typedef/models/__init__.py +++ b/src/leapfrogai_api/typedef/models/__init__.py @@ -1 +1,5 @@ -from .model_types import Model as Model +from .model_types import ( + Model as Model, + ModelResponse as ModelResponse, + ModelResponseModel as ModelResponseModel, +) diff --git a/src/leapfrogai_api/typedef/models/model_types.py b/src/leapfrogai_api/typedef/models/model_types.py index c4f5519b4..1aa0df535 100644 --- a/src/leapfrogai_api/typedef/models/model_types.py +++ b/src/leapfrogai_api/typedef/models/model_types.py @@ -1,4 +1,5 @@ -from typing import List +from typing import List, Literal +from pydantic import BaseModel, Field class Model: @@ -8,3 +9,38 @@ class Model: def __init__(self, name: str, backend: str, capabilities: List[str] | None = None): self.name = name self.backend = backend + + +class ModelResponseModel(BaseModel): + """Represents a single model in the response.""" + + id: str = Field( + ..., + description="The unique identifier of the model.", + examples=["llama-cpp-python"], + ) + object: Literal["model"] = Field( + default="model", + description="The object type, which is always 'model' for this response.", + ) + created: int = Field( + default=0, + description="The Unix timestamp (in seconds) when the model was created. Always 0 for LeapfrogAI models.", + examples=[0], + ) + owned_by: Literal["leapfrogai"] = Field( + default="leapfrogai", + description="The organization that owns the model. Always 'leapfrogai' for LeapfrogAI models.", + ) + + +class ModelResponse(BaseModel): + """Response object for listing available models.""" + + object: Literal["list"] = Field( + default="list", + description="The object type, which is always 'list' for this response.", + ) + data: list[ModelResponseModel] = Field( + ..., description="A list of available models.", min_length=0 + ) From 595c3acb1fd8c752ee66a91e9b9fb1a7b39f8209 Mon Sep 17 00:00:00 2001 From: alekst23 Date: Tue, 3 Sep 2024 16:52:26 -0400 Subject: [PATCH 11/17] chore: file types --- src/leapfrogai_api/backend/types.py | 47 ------------------- src/leapfrogai_api/routers/openai/files.py | 2 +- src/leapfrogai_api/typedef/files/__init__.py | 4 ++ .../typedef/files/file_types.py | 39 +++++++++++++++ 4 files changed, 44 insertions(+), 48 deletions(-) create mode 100644 src/leapfrogai_api/typedef/files/__init__.py create mode 100644 src/leapfrogai_api/typedef/files/file_types.py diff --git a/src/leapfrogai_api/backend/types.py b/src/leapfrogai_api/backend/types.py index 4b4117a15..506b149dd 100644 --- a/src/leapfrogai_api/backend/types.py +++ b/src/leapfrogai_api/backend/types.py @@ -6,8 +6,6 @@ from enum import Enum from typing import Literal -from fastapi import UploadFile, Form, File -from openai.types import FileObject from openai.types.beta import VectorStore from openai.types.beta.thread_create_params import ( @@ -18,51 +16,6 @@ from pydantic import BaseModel, Field -############# -# FILES -############# - - -class UploadFileRequest(BaseModel): - """Request object for uploading a file.""" - - file: UploadFile = Field( - ..., - description="The file to be uploaded. Must be a supported file type.", - ) - purpose: Literal["assistants"] | None = Field( - default="assistants", - description="The intended purpose of the uploaded file. Currently, only 'assistants' is supported.", - ) - - @classmethod - def as_form( - cls, - file: UploadFile = File(...), - purpose: str | None = Form("assistants"), - ) -> UploadFileRequest: - """Create an instance of the class from form data.""" - return cls(file=file, purpose=purpose) - - -class ListFilesResponse(BaseModel): - """Response object for listing files.""" - - object: Literal["list"] = Field( - default="list", - description="The type of object returned. Always 'list' for file listing.", - ) - data: list[FileObject] = Field( - default=[], - description="An array of File objects, each representing an uploaded file.", - ) - - -############# -# ASSISTANTS -############# - - ################ # VECTOR STORES ################ diff --git a/src/leapfrogai_api/routers/openai/files.py b/src/leapfrogai_api/routers/openai/files.py index 026ac8c0c..11e0f88dd 100644 --- a/src/leapfrogai_api/routers/openai/files.py +++ b/src/leapfrogai_api/routers/openai/files.py @@ -8,10 +8,10 @@ is_supported_mime_type, get_mime_type_from_filename, ) -from leapfrogai_api.backend.types import ListFilesResponse, UploadFileRequest from leapfrogai_api.data.crud_file_bucket import CRUDFileBucket from leapfrogai_api.data.crud_file_object import CRUDFileObject, FilterFileObject from leapfrogai_api.routers.supabase_session import Session +from leapfrogai_api.typedef.files import ListFilesResponse, UploadFileRequest router = APIRouter(prefix="/openai/v1/files", tags=["openai/files"]) diff --git a/src/leapfrogai_api/typedef/files/__init__.py b/src/leapfrogai_api/typedef/files/__init__.py new file mode 100644 index 000000000..07a4ba2c7 --- /dev/null +++ b/src/leapfrogai_api/typedef/files/__init__.py @@ -0,0 +1,4 @@ +from .file_types import ( + UploadFileRequest as UploadFileRequest, + ListFilesResponse as ListFilesResponse, +) diff --git a/src/leapfrogai_api/typedef/files/file_types.py b/src/leapfrogai_api/typedef/files/file_types.py new file mode 100644 index 000000000..5a9c94e0d --- /dev/null +++ b/src/leapfrogai_api/typedef/files/file_types.py @@ -0,0 +1,39 @@ +from pydantic import BaseModel, Field +from typing import Literal +from fastapi import UploadFile, Form, File +from openai.types import FileObject + + +class UploadFileRequest(BaseModel): + """Request object for uploading a file.""" + + file: UploadFile = Field( + ..., + description="The file to be uploaded. Must be a supported file type.", + ) + purpose: Literal["assistants"] | None = Field( + default="assistants", + description="The intended purpose of the uploaded file. Currently, only 'assistants' is supported.", + ) + + @classmethod + def as_form( + cls, + file: UploadFile = File(...), + purpose: str | None = Form("assistants"), + ) -> "UploadFileRequest": + """Create an instance of the class from form data.""" + return cls(file=file, purpose=purpose) + + +class ListFilesResponse(BaseModel): + """Response object for listing files.""" + + object: Literal["list"] = Field( + default="list", + description="The type of object returned. Always 'list' for file listing.", + ) + data: list[FileObject] = Field( + default=[], + description="An array of File objects, each representing an uploaded file.", + ) From b7a4dfe42603c2362d4182e006e3375d01f7e1da Mon Sep 17 00:00:00 2001 From: alekst23 Date: Tue, 3 Sep 2024 17:02:46 -0400 Subject: [PATCH 12/17] chore: vectore_store types --- src/leapfrogai_api/backend/rag/index.py | 3 +- src/leapfrogai_api/backend/thread_runner.py | 2 +- .../routers/leapfrogai/vector_stores.py | 2 +- .../create_modify_assistant_request.py | 19 +++++---- .../routers/openai/vector_stores.py | 2 +- .../typedef/vectorstores/__init__.py | 12 ++++++ .../typedef/vectorstores/search_types.py | 28 +++++++++++++ .../vectorstores/vectorstore_types.py} | 41 ------------------- 8 files changed, 55 insertions(+), 54 deletions(-) create mode 100644 src/leapfrogai_api/typedef/vectorstores/__init__.py create mode 100644 src/leapfrogai_api/typedef/vectorstores/search_types.py rename src/leapfrogai_api/{backend/types.py => typedef/vectorstores/vectorstore_types.py} (79%) diff --git a/src/leapfrogai_api/backend/rag/index.py b/src/leapfrogai_api/backend/rag/index.py index 987ed10a6..81f20e091 100644 --- a/src/leapfrogai_api/backend/rag/index.py +++ b/src/leapfrogai_api/backend/rag/index.py @@ -10,12 +10,13 @@ from openai.types.beta.vector_stores import VectorStoreFile from openai.types.beta.vector_stores.vector_store_file import LastError from supabase import AClient as AsyncClient + from leapfrogai_api.backend.rag.document_loader import load_file, split from leapfrogai_api.backend.rag.leapfrogai_embeddings import LeapfrogAIEmbeddings from leapfrogai_api.data.crud_file_bucket import CRUDFileBucket from leapfrogai_api.data.crud_file_object import CRUDFileObject, FilterFileObject from leapfrogai_api.data.crud_vector_store import CRUDVectorStore, FilterVectorStore -from leapfrogai_api.backend.types import ( +from leapfrogai_api.typedef.vectorstores import ( VectorStoreStatus, VectorStoreFileStatus, CreateVectorStoreRequest, diff --git a/src/leapfrogai_api/backend/thread_runner.py b/src/leapfrogai_api/backend/thread_runner.py index b98f304e7..850ba001b 100644 --- a/src/leapfrogai_api/backend/thread_runner.py +++ b/src/leapfrogai_api/backend/thread_runner.py @@ -50,7 +50,7 @@ ) from leapfrogai_api.typedef.runs import RunCreateParamsRequest from leapfrogai_api.typedef.messages import CreateMessageRequest -from leapfrogai_api.backend.types import SearchResponse +from leapfrogai_api.typedef.vectorstores import SearchResponse class ThreadRunner(BaseModel): diff --git a/src/leapfrogai_api/routers/leapfrogai/vector_stores.py b/src/leapfrogai_api/routers/leapfrogai/vector_stores.py index 8d3db3ec3..253405e30 100644 --- a/src/leapfrogai_api/routers/leapfrogai/vector_stores.py +++ b/src/leapfrogai_api/routers/leapfrogai/vector_stores.py @@ -3,7 +3,7 @@ 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 SearchResponse +from leapfrogai_api.typedef.vectorstores import SearchResponse from leapfrogai_api.routers.supabase_session import Session router = APIRouter( diff --git a/src/leapfrogai_api/routers/openai/requests/create_modify_assistant_request.py b/src/leapfrogai_api/routers/openai/requests/create_modify_assistant_request.py index 8a0791300..746147471 100644 --- a/src/leapfrogai_api/routers/openai/requests/create_modify_assistant_request.py +++ b/src/leapfrogai_api/routers/openai/requests/create_modify_assistant_request.py @@ -1,20 +1,11 @@ from __future__ import annotations import logging - from contextlib import suppress from fastapi import HTTPException, status from pydantic import BaseModel, Field from typing import Literal -from leapfrogai_api.backend.rag.index import IndexingService -from leapfrogai_api.backend.types import CreateVectorStoreRequest -from leapfrogai_api.data.crud_vector_store import CRUDVectorStore, FilterVectorStore -from leapfrogai_api.routers.supabase_session import Session -from leapfrogai_api.utils.validate_tools import ( - validate_assistant_tool, - validate_tool_resources, -) from openai.types.beta import AssistantTool from openai.types.beta.assistant import ( ToolResources as BetaAssistantToolResources, @@ -25,6 +16,16 @@ logger = logging.getLogger(__name__) +from leapfrogai_api.backend.rag.index import IndexingService +from leapfrogai_api.typedef.vectorstores import CreateVectorStoreRequest +from leapfrogai_api.data.crud_vector_store import CRUDVectorStore, FilterVectorStore +from leapfrogai_api.routers.supabase_session import Session +from leapfrogai_api.utils.validate_tools import ( + validate_assistant_tool, + validate_tool_resources, +) + + class CreateAssistantRequest(BaseModel): """Request object for creating an assistant.""" diff --git a/src/leapfrogai_api/routers/openai/vector_stores.py b/src/leapfrogai_api/routers/openai/vector_stores.py index 6f0ab4d26..dea997a88 100644 --- a/src/leapfrogai_api/routers/openai/vector_stores.py +++ b/src/leapfrogai_api/routers/openai/vector_stores.py @@ -8,7 +8,7 @@ from openai.types.beta import VectorStore, VectorStoreDeleted from openai.types.beta.vector_stores import VectorStoreFile, VectorStoreFileDeleted from leapfrogai_api.backend.rag.index import IndexingService -from leapfrogai_api.backend.types import ( +from leapfrogai_api.typedef.vectorstores import ( CreateVectorStoreFileRequest, CreateVectorStoreRequest, ListVectorStoresResponse, diff --git a/src/leapfrogai_api/typedef/vectorstores/__init__.py b/src/leapfrogai_api/typedef/vectorstores/__init__.py new file mode 100644 index 000000000..1491a9767 --- /dev/null +++ b/src/leapfrogai_api/typedef/vectorstores/__init__.py @@ -0,0 +1,12 @@ +from .vectorstore_types import ( + VectorStoreFileStatus as VectorStoreFileStatus, + VectorStoreStatus as VectorStoreStatus, + CreateVectorStoreFileRequest as CreateVectorStoreFileRequest, + CreateVectorStoreRequest as CreateVectorStoreRequest, + ModifyVectorStoreRequest as ModifyVectorStoreRequest, + ListVectorStoresResponse as ListVectorStoresResponse, +) +from .search_types import ( + SearchItem as SearchItem, + SearchResponse as SearchResponse, +) diff --git a/src/leapfrogai_api/typedef/vectorstores/search_types.py b/src/leapfrogai_api/typedef/vectorstores/search_types.py new file mode 100644 index 000000000..76abb0822 --- /dev/null +++ b/src/leapfrogai_api/typedef/vectorstores/search_types.py @@ -0,0 +1,28 @@ +from pydantic import BaseModel, Field + + +class SearchItem(BaseModel): + """Object representing a single item in a search result.""" + + 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 item.") + metadata: dict = Field( + ..., description="Additional metadata associated with the item." + ) + similarity: float = Field( + ..., description="Similarity score of this item to the query." + ) + + +class SearchResponse(BaseModel): + """Response object for RAG queries.""" + + 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/backend/types.py b/src/leapfrogai_api/typedef/vectorstores/vectorstore_types.py similarity index 79% rename from src/leapfrogai_api/backend/types.py rename to src/leapfrogai_api/typedef/vectorstores/vectorstore_types.py index 506b149dd..c6a014c75 100644 --- a/src/leapfrogai_api/backend/types.py +++ b/src/leapfrogai_api/typedef/vectorstores/vectorstore_types.py @@ -1,7 +1,3 @@ -"""Typing definitions for assistants API.""" - -from __future__ import annotations - import datetime from enum import Enum from typing import Literal @@ -16,11 +12,6 @@ from pydantic import BaseModel, Field -################ -# VECTOR STORES -################ - - class VectorStoreFileStatus(Enum): """Enum for the status of a vector store file.""" @@ -138,35 +129,3 @@ class ListVectorStoresResponse(BaseModel): default=[], description="A list of VectorStore objects.", ) - - -################ -# LEAPFROGAI Vector Stores -################ - - -class SearchItem(BaseModel): - """Object representing a single item in a search result.""" - - 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 item.") - metadata: dict = Field( - ..., description="Additional metadata associated with the item." - ) - similarity: float = Field( - ..., description="Similarity score of this item to the query." - ) - - -class SearchResponse(BaseModel): - """Response object for RAG queries.""" - - data: list[SearchItem] = Field( - ..., - description="List of RAG items returned as a result of the query.", - min_length=0, - ) From c585ef586f81992d913657bf5c4e3cb5e162855a Mon Sep 17 00:00:00 2001 From: alekst23 Date: Tue, 3 Sep 2024 17:11:08 -0400 Subject: [PATCH 13/17] chore: fix imports --- tests/integration/api/test_messages.py | 10 +++------- tests/integration/api/test_runs.py | 6 ++---- tests/integration/api/test_threads.py | 10 +++++----- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/tests/integration/api/test_messages.py b/tests/integration/api/test_messages.py index 148b1889e..c033321f1 100644 --- a/tests/integration/api/test_messages.py +++ b/tests/integration/api/test_messages.py @@ -7,13 +7,9 @@ from fastapi.testclient import TestClient from openai.types.beta import Thread, ThreadDeleted from openai.types.beta.threads import TextContentBlock, Text, Message, MessageDeleted -from leapfrogai_api.backend.types import ( - ModifyMessageRequest, -) -from leapfrogai_api.typedef.messages.create_message_request import ( - CreateMessageRequest, -) -from leapfrogai_api.routers.openai.requests.create_thread_request import ( + +from leapfrogai_api.typedef.messages import CreateMessageRequest, ModifyMessageRequest +from leapfrogai_api.typedef.threads import ( CreateThreadRequest, ) from leapfrogai_api.main import app diff --git a/tests/integration/api/test_runs.py b/tests/integration/api/test_runs.py index 678586e1e..81f48f15c 100644 --- a/tests/integration/api/test_runs.py +++ b/tests/integration/api/test_runs.py @@ -12,16 +12,14 @@ from leapfrogai_api.routers.openai.requests.create_modify_assistant_request import ( CreateAssistantRequest, ) -from leapfrogai_api.typedef.messages.create_message_request import ( +from leapfrogai_api.typedef.messages import ( CreateMessageRequest, ) -from leapfrogai_api.routers.openai.requests.create_thread_request import ( - CreateThreadRequest, -) from leapfrogai_api.typedef.runs import ( RunCreateParamsRequest, ) from leapfrogai_api.typedef.threads import ( + CreateThreadRequest, ThreadRunCreateParamsRequest, ) diff --git a/tests/integration/api/test_threads.py b/tests/integration/api/test_threads.py index 95f8330a3..64a4c64c9 100644 --- a/tests/integration/api/test_threads.py +++ b/tests/integration/api/test_threads.py @@ -4,18 +4,18 @@ import pytest from fastapi import HTTPException, status from fastapi.testclient import TestClient + from openai.types.beta import Thread, ThreadDeleted from openai.types.beta.thread import ToolResourcesCodeInterpreter, ToolResources from openai.types.beta.threads import TextContentBlock, Text -from leapfrogai_api.backend.types import ( + +from leapfrogai_api.typedef.threads import ( + CreateThreadRequest, ModifyThreadRequest, ) -from leapfrogai_api.typedef.messages.create_message_request import ( +from leapfrogai_api.typedef.messages import ( CreateMessageRequest, ) -from leapfrogai_api.routers.openai.requests.create_thread_request import ( - CreateThreadRequest, -) from leapfrogai_api.routers.openai.threads import router From 3cd7cad0d15b59e8e4b6571d614789f70860ee8e Mon Sep 17 00:00:00 2001 From: alekst23 Date: Wed, 4 Sep 2024 12:18:55 -0400 Subject: [PATCH 14/17] chore: assistant types --- .../routers/openai/assistants.py | 2 +- .../routers/openai/requests/__init__.py | 0 .../create_modify_assistant_request.py | 208 ------------------ .../typedef/assistants/__init__.py | 6 +- .../typedef/assistants/assistant_types.py | 204 +++++++++++++++++ 5 files changed, 210 insertions(+), 210 deletions(-) delete mode 100644 src/leapfrogai_api/routers/openai/requests/__init__.py delete mode 100644 src/leapfrogai_api/routers/openai/requests/create_modify_assistant_request.py diff --git a/src/leapfrogai_api/routers/openai/assistants.py b/src/leapfrogai_api/routers/openai/assistants.py index e1cb86643..3c44bf0a0 100644 --- a/src/leapfrogai_api/routers/openai/assistants.py +++ b/src/leapfrogai_api/routers/openai/assistants.py @@ -5,7 +5,7 @@ from leapfrogai_api.backend.helpers import object_or_default from leapfrogai_api.typedef.assistants import ListAssistantsResponse -from leapfrogai_api.routers.openai.requests.create_modify_assistant_request import ( +from leapfrogai_api.typedef.assistants import ( CreateAssistantRequest, ModifyAssistantRequest, ) diff --git a/src/leapfrogai_api/routers/openai/requests/__init__.py b/src/leapfrogai_api/routers/openai/requests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/leapfrogai_api/routers/openai/requests/create_modify_assistant_request.py b/src/leapfrogai_api/routers/openai/requests/create_modify_assistant_request.py deleted file mode 100644 index 746147471..000000000 --- a/src/leapfrogai_api/routers/openai/requests/create_modify_assistant_request.py +++ /dev/null @@ -1,208 +0,0 @@ -from __future__ import annotations - -import logging -from contextlib import suppress -from fastapi import HTTPException, status -from pydantic import BaseModel, Field -from typing import Literal - -from openai.types.beta import AssistantTool -from openai.types.beta.assistant import ( - ToolResources as BetaAssistantToolResources, - ToolResourcesFileSearch, -) -from openai.types.beta.assistant_tool import FileSearchTool - -logger = logging.getLogger(__name__) - - -from leapfrogai_api.backend.rag.index import IndexingService -from leapfrogai_api.typedef.vectorstores import CreateVectorStoreRequest -from leapfrogai_api.data.crud_vector_store import CRUDVectorStore, FilterVectorStore -from leapfrogai_api.routers.supabase_session import Session -from leapfrogai_api.utils.validate_tools import ( - validate_assistant_tool, - validate_tool_resources, -) - - -class CreateAssistantRequest(BaseModel): - """Request object for creating an assistant.""" - - model: str = Field( - default="llama-cpp-python", - examples=["llama-cpp-python"], - description="The model to be used by the assistant. Default is 'llama-cpp-python'.", - ) - name: str | None = Field( - default=None, - examples=["Froggy Assistant"], - description="The name of the assistant. Optional.", - ) - description: str | None = Field( - default=None, - examples=["A helpful assistant."], - description="A description of the assistant's purpose. Optional.", - ) - instructions: str | None = Field( - default=None, - examples=["You are a helpful assistant."], - description="Instructions that the assistant should follow. Optional.", - ) - tools: list[AssistantTool] | None = Field( - default=None, - examples=[[FileSearchTool(type="file_search")]], - description="List of tools the assistant can use. Optional.", - ) - tool_resources: BetaAssistantToolResources | None = Field( - default=None, - examples=[ - BetaAssistantToolResources( - file_search=ToolResourcesFileSearch(vector_store_ids=[]) - ) - ], - description="Resources for the tools used by the assistant. Optional.", - ) - metadata: dict | None = Field( - default={}, - examples=[{}], - description="Additional metadata for the assistant. Optional.", - ) - temperature: float | None = Field( - default=None, - examples=[1.0], - description="Sampling temperature for the model. Optional.", - ) - top_p: float | None = Field( - default=None, - examples=[1.0], - description="Nucleus sampling parameter. Optional.", - ) - response_format: Literal["auto"] | None = Field( - default=None, - examples=["auto"], - description="The format of the assistant's responses. Currently only 'auto' is supported. Optional.", - ) - - # helper function to check for unsupported tools/tool resources, and if a new vector store needs to be added - async def request_checks_and_modifications(self, session: Session): - """ - Performs checks and modifications that occur in both create and modify requests - - Checks performed: - - unsupported tools - - unsupported tool resources - - Modifications performed: - - vector store creation - """ - - async def new_vector_store_from_file_ids(): - logger.debug("Creating vector store for new assistant") - indexing_service = IndexingService(db=session) - vector_store_params_dict = vector_stores[0] - - if "file_ids" not in vector_store_params_dict: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="ToolResourcesFileSearchVectorStores found but no file ids were provided", - ) - - if not await indexing_service.file_ids_are_valid( - vector_store_params_dict["file_ids"] - ): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Invalid file ids attached to assistants request", - ) - - vector_store_request = CreateVectorStoreRequest( - file_ids=vector_store_params_dict["file_ids"], - name="{}_vector_store".format(self.name), - expires_after=None, - metadata={}, - ) - - vector_store = await indexing_service.create_new_vector_store( - vector_store_request - ) - - self.tool_resources.file_search.vector_store_ids = [vector_store.id] - - async def attach_existing_vector_store_from_id(): - logger.debug( - "Attaching vector store with id: {} to new assistant".format(ids[0]) - ) - crud_vector_store = CRUDVectorStore(db=session) - try: - existing_vector_store = await crud_vector_store.get( - filters=FilterVectorStore(id=ids[0]) - ) - if existing_vector_store is None: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Provided vector store id was not found", - ) - except Exception: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Invalid vector store id was provided", - ) - - # check for unsupported tools - if self.tools: - for tool in self.tools: - if not validate_assistant_tool(tool): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Unsupported tool type: {tool.type}", - ) - - # check for unsupported tool resources - if self.tool_resources and not validate_tool_resources(self.tool_resources): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Unsupported tool resource: {self.tool_resources}", - ) - - # check if a vector store needs to be built or added to this assistant - if self.tool_resources and self.tool_resources.file_search is not None: - ids = self.tool_resources.file_search.vector_store_ids or [] - vector_stores = ( - getattr(self.tool_resources.file_search, "vector_stores", []) or [] - ) - - ids_len = len(ids) - vector_stores_len = len(list(vector_stores)) - - # too many ids or vector_stores provided - if (ids_len + vector_stores_len) > 1: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="There can be a maximum of 1 vector store attached to the assistant", - ) - - # new vector store requested from file ids - elif vector_stores_len == 1: - await new_vector_store_from_file_ids() - - # attach already existing vector store from its id - elif ids_len == 1: - await attach_existing_vector_store_from_id() - - # nothing provided, no changes made - else: - logger.debug( - "No files or vector store id found; assistant will be created with no vector store" - ) - - # ensure the vector_stores field is removed regardless, if it exists - with suppress(AttributeError): - self.tool_resources.file_search.vector_stores = None - - -class ModifyAssistantRequest(CreateAssistantRequest): - """Request object for modifying an assistant.""" - - # Inherits all fields from CreateAssistantRequest - # All fields are optional for modification diff --git a/src/leapfrogai_api/typedef/assistants/__init__.py b/src/leapfrogai_api/typedef/assistants/__init__.py index 0bb259a61..57fc94ba1 100644 --- a/src/leapfrogai_api/typedef/assistants/__init__.py +++ b/src/leapfrogai_api/typedef/assistants/__init__.py @@ -1 +1,5 @@ -from .assistant_types import ListAssistantsResponse as ListAssistantsResponse +from .assistant_types import ( + CreateAssistantRequest as CreateAssistantRequest, + ModifyAssistantRequest as ModifyAssistantRequest, + ListAssistantsResponse as ListAssistantsResponse, +) diff --git a/src/leapfrogai_api/typedef/assistants/assistant_types.py b/src/leapfrogai_api/typedef/assistants/assistant_types.py index 8c3217dae..fec59ee10 100644 --- a/src/leapfrogai_api/typedef/assistants/assistant_types.py +++ b/src/leapfrogai_api/typedef/assistants/assistant_types.py @@ -1,6 +1,210 @@ +from __future__ import annotations + +import logging +from contextlib import suppress +from fastapi import HTTPException, status from typing import Literal from pydantic import BaseModel, Field + from openai.types.beta import Assistant +from openai.types.beta import AssistantTool +from openai.types.beta.assistant import ( + ToolResources as BetaAssistantToolResources, + ToolResourcesFileSearch, +) +from openai.types.beta.assistant_tool import FileSearchTool + +from leapfrogai_api.backend.rag.index import IndexingService +from leapfrogai_api.typedef.vectorstores import CreateVectorStoreRequest +from leapfrogai_api.data.crud_vector_store import CRUDVectorStore, FilterVectorStore +from leapfrogai_api.routers.supabase_session import Session +from leapfrogai_api.utils.validate_tools import ( + validate_assistant_tool, + validate_tool_resources, +) + +logger = logging.getLogger(__name__) + +class CreateAssistantRequest(BaseModel): + """Request object for creating an assistant.""" + + model: str = Field( + default="llama-cpp-python", + examples=["llama-cpp-python"], + description="The model to be used by the assistant. Default is 'llama-cpp-python'.", + ) + name: str | None = Field( + default=None, + examples=["Froggy Assistant"], + description="The name of the assistant. Optional.", + ) + description: str | None = Field( + default=None, + examples=["A helpful assistant."], + description="A description of the assistant's purpose. Optional.", + ) + instructions: str | None = Field( + default=None, + examples=["You are a helpful assistant."], + description="Instructions that the assistant should follow. Optional.", + ) + tools: list[AssistantTool] | None = Field( + default=None, + examples=[[FileSearchTool(type="file_search")]], + description="List of tools the assistant can use. Optional.", + ) + tool_resources: BetaAssistantToolResources | None = Field( + default=None, + examples=[ + BetaAssistantToolResources( + file_search=ToolResourcesFileSearch(vector_store_ids=[]) + ) + ], + description="Resources for the tools used by the assistant. Optional.", + ) + metadata: dict | None = Field( + default={}, + examples=[{}], + description="Additional metadata for the assistant. Optional.", + ) + temperature: float | None = Field( + default=None, + examples=[1.0], + description="Sampling temperature for the model. Optional.", + ) + top_p: float | None = Field( + default=None, + examples=[1.0], + description="Nucleus sampling parameter. Optional.", + ) + response_format: Literal["auto"] | None = Field( + default=None, + examples=["auto"], + description="The format of the assistant's responses. Currently only 'auto' is supported. Optional.", + ) + + # helper function to check for unsupported tools/tool resources, and if a new vector store needs to be added + async def request_checks_and_modifications(self, session: Session): + """ + Performs checks and modifications that occur in both create and modify requests + + Checks performed: + - unsupported tools + - unsupported tool resources + + Modifications performed: + - vector store creation + """ + + async def new_vector_store_from_file_ids(): + logger.debug("Creating vector store for new assistant") + indexing_service = IndexingService(db=session) + vector_store_params_dict = vector_stores[0] + + if "file_ids" not in vector_store_params_dict: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="ToolResourcesFileSearchVectorStores found but no file ids were provided", + ) + + if not await indexing_service.file_ids_are_valid( + vector_store_params_dict["file_ids"] + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid file ids attached to assistants request", + ) + + vector_store_request = CreateVectorStoreRequest( + file_ids=vector_store_params_dict["file_ids"], + name="{}_vector_store".format(self.name), + expires_after=None, + metadata={}, + ) + + vector_store = await indexing_service.create_new_vector_store( + vector_store_request + ) + + self.tool_resources.file_search.vector_store_ids = [vector_store.id] + + async def attach_existing_vector_store_from_id(): + logger.debug( + "Attaching vector store with id: {} to new assistant".format(ids[0]) + ) + crud_vector_store = CRUDVectorStore(db=session) + try: + existing_vector_store = await crud_vector_store.get( + filters=FilterVectorStore(id=ids[0]) + ) + if existing_vector_store is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Provided vector store id was not found", + ) + except Exception: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid vector store id was provided", + ) + + # check for unsupported tools + if self.tools: + for tool in self.tools: + if not validate_assistant_tool(tool): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Unsupported tool type: {tool.type}", + ) + + # check for unsupported tool resources + if self.tool_resources and not validate_tool_resources(self.tool_resources): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Unsupported tool resource: {self.tool_resources}", + ) + + # check if a vector store needs to be built or added to this assistant + if self.tool_resources and self.tool_resources.file_search is not None: + ids = self.tool_resources.file_search.vector_store_ids or [] + vector_stores = ( + getattr(self.tool_resources.file_search, "vector_stores", []) or [] + ) + + ids_len = len(ids) + vector_stores_len = len(list(vector_stores)) + + # too many ids or vector_stores provided + if (ids_len + vector_stores_len) > 1: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="There can be a maximum of 1 vector store attached to the assistant", + ) + + # new vector store requested from file ids + elif vector_stores_len == 1: + await new_vector_store_from_file_ids() + + # attach already existing vector store from its id + elif ids_len == 1: + await attach_existing_vector_store_from_id() + + # nothing provided, no changes made + else: + logger.debug( + "No files or vector store id found; assistant will be created with no vector store" + ) + + # ensure the vector_stores field is removed regardless, if it exists + with suppress(AttributeError): + self.tool_resources.file_search.vector_stores = None + + +class ModifyAssistantRequest(CreateAssistantRequest): + """Request object for modifying an assistant.""" + + # Inherits all fields from CreateAssistantRequest + # All fields are optional for modification class ListAssistantsResponse(BaseModel): From 350fed56bcfdb9298ab2a284a33b3c8f87c444d2 Mon Sep 17 00:00:00 2001 From: alekst23 Date: Wed, 4 Sep 2024 14:28:39 -0400 Subject: [PATCH 15/17] lint fix --- src/leapfrogai_api/typedef/assistants/assistant_types.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/leapfrogai_api/typedef/assistants/assistant_types.py b/src/leapfrogai_api/typedef/assistants/assistant_types.py index fec59ee10..168a0e357 100644 --- a/src/leapfrogai_api/typedef/assistants/assistant_types.py +++ b/src/leapfrogai_api/typedef/assistants/assistant_types.py @@ -23,8 +23,10 @@ validate_tool_resources, ) + logger = logging.getLogger(__name__) + class CreateAssistantRequest(BaseModel): """Request object for creating an assistant.""" From 8bbdf1a7cb94b53961ce9631b277d69cefa0e342 Mon Sep 17 00:00:00 2001 From: alekst23 Date: Wed, 4 Sep 2024 14:37:25 -0400 Subject: [PATCH 16/17] fixes to test imports --- tests/e2e/test_text_backend_full.py | 2 +- tests/integration/api/test_assistants.py | 4 ++-- tests/integration/api/test_vector_stores.py | 2 +- tests/pytest/leapfrogai_api/test_api.py | 13 +++++++------ .../leapfrogai_api/routers/openai/test_threads.py | 5 +---- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/tests/e2e/test_text_backend_full.py b/tests/e2e/test_text_backend_full.py index 3da7cdd0a..fdee17172 100644 --- a/tests/e2e/test_text_backend_full.py +++ b/tests/e2e/test_text_backend_full.py @@ -5,7 +5,7 @@ from openai import OpenAI from openai.types.beta.vector_store import VectorStore -from leapfrogai_api.backend.types import VectorStoreStatus +from leapfrogai_api.typedef.vectorstores import VectorStoreStatus def download_arxiv_pdf(): diff --git a/tests/integration/api/test_assistants.py b/tests/integration/api/test_assistants.py index e07fdd8dd..deb341904 100644 --- a/tests/integration/api/test_assistants.py +++ b/tests/integration/api/test_assistants.py @@ -13,11 +13,11 @@ from openai.types.beta.vector_store import ExpiresAfter import leapfrogai_api.backend.rag.index -from leapfrogai_api.backend.types import CreateVectorStoreRequest from leapfrogai_api.routers.openai.vector_stores import router as vector_store_router from leapfrogai_api.routers.openai.files import router as files_router from leapfrogai_api.routers.openai.assistants import router as assistants_router -from leapfrogai_api.routers.openai.requests.create_modify_assistant_request import ( +from leapfrogai_api.typedef.vectorstores import CreateVectorStoreRequest +from leapfrogai_api.typedef.assistants import ( CreateAssistantRequest, ModifyAssistantRequest, ) diff --git a/tests/integration/api/test_vector_stores.py b/tests/integration/api/test_vector_stores.py index 4a939cb21..f9c69f8e8 100644 --- a/tests/integration/api/test_vector_stores.py +++ b/tests/integration/api/test_vector_stores.py @@ -13,7 +13,7 @@ from langchain_core.embeddings.fake import FakeEmbeddings import leapfrogai_api.backend.rag.index -from leapfrogai_api.backend.types import ( +from leapfrogai_api.typedef.vectorstores import ( CreateVectorStoreRequest, ModifyVectorStoreRequest, ) diff --git a/tests/pytest/leapfrogai_api/test_api.py b/tests/pytest/leapfrogai_api/test_api.py index a80df6b6c..4eacd1085 100644 --- a/tests/pytest/leapfrogai_api/test_api.py +++ b/tests/pytest/leapfrogai_api/test_api.py @@ -10,7 +10,8 @@ from fastapi.testclient import TestClient from starlette.middleware.base import _CachedRequest from supabase import ClientOptions -import leapfrogai_api.backend.types as lfai_types +from leapfrogai_api.typedef.chat import ChatCompletionRequest, ChatMessage +from leapfrogai_api.typedef.embeddings import CreateEmbeddingRequest from leapfrogai_api.main import app from leapfrogai_api.routers.supabase_session import init_supabase_client @@ -209,7 +210,7 @@ def test_embedding(dummy_auth_middleware): with TestClient(app) as client: # Send request to client - embedding_request = lfai_types.CreateEmbeddingRequest( + embedding_request = CreateEmbeddingRequest( model="repeater", input="This is the embedding input text.", ) @@ -237,9 +238,9 @@ def test_chat_completion(dummy_auth_middleware): """Test the chat completion endpoint.""" with TestClient(app) as client: input_content = "this is the chat completion input." - chat_completion_request = lfai_types.ChatCompletionRequest( + chat_completion_request = ChatCompletionRequest( model="repeater", - messages=[lfai_types.ChatMessage(role="user", content=input_content)], + messages=[ChatMessage(role="user", content=input_content)], ) response = client.post( "/openai/v1/chat/completions", json=chat_completion_request.model_dump() @@ -284,9 +285,9 @@ def test_stream_chat_completion(dummy_auth_middleware): with TestClient(app) as client: input_content = "this is the stream chat completion input." - chat_completion_request = lfai_types.ChatCompletionRequest( + chat_completion_request = ChatCompletionRequest( model="repeater", - messages=[lfai_types.ChatMessage(role="user", content=input_content)], + messages=[ChatMessage(role="user", content=input_content)], stream=True, ) diff --git a/tests/unit/leapfrogai_api/routers/openai/test_threads.py b/tests/unit/leapfrogai_api/routers/openai/test_threads.py index c39f1ff84..c02853c87 100644 --- a/tests/unit/leapfrogai_api/routers/openai/test_threads.py +++ b/tests/unit/leapfrogai_api/routers/openai/test_threads.py @@ -9,10 +9,7 @@ ToolResourcesFileSearch, ) -from leapfrogai_api.routers.openai.requests.create_thread_request import ( - CreateThreadRequest, -) -from leapfrogai_api.backend.types import ModifyThreadRequest +from leapfrogai_api.typedef.threads import ModifyThreadRequest, CreateThreadRequest from leapfrogai_api.data.crud_thread import CRUDThread from leapfrogai_api.data.crud_message import CRUDMessage from leapfrogai_api.routers.openai.threads import ( From 39749c2f148a9cec6b456ef5c2db94ec210cae45 Mon Sep 17 00:00:00 2001 From: alekst23 Date: Fri, 6 Sep 2024 13:17:49 -0400 Subject: [PATCH 17/17] fixes from comments --- src/leapfrogai_api/backend/__init__.py | 5 +++++ .../backend/{thread_runner.py => composer.py} | 2 +- src/leapfrogai_api/{typedef => backend}/constants.py | 2 +- src/leapfrogai_api/data/crud_api_key.py | 4 ++-- src/leapfrogai_api/routers/openai/runs.py | 6 +++--- src/leapfrogai_api/typedef/auth/auth_types.py | 8 ++++---- src/leapfrogai_api/typedef/chat/chat_types.py | 2 +- src/leapfrogai_api/typedef/common.py | 2 +- src/leapfrogai_api/typedef/completion/completion_types.py | 2 +- src/leapfrogai_api/typedef/runs/run_create_base.py | 2 +- tests/conformance/test_conformance_runs.py | 2 +- tests/integration/api/test_auth.py | 8 ++++++-- 12 files changed, 27 insertions(+), 18 deletions(-) rename src/leapfrogai_api/backend/{thread_runner.py => composer.py} (99%) rename src/leapfrogai_api/{typedef => backend}/constants.py (76%) diff --git a/src/leapfrogai_api/backend/__init__.py b/src/leapfrogai_api/backend/__init__.py index e69de29bb..ca18ccd8d 100644 --- a/src/leapfrogai_api/backend/__init__.py +++ b/src/leapfrogai_api/backend/__init__.py @@ -0,0 +1,5 @@ +from .constants import ( + THIRTY_DAYS_SECONDS as THIRTY_DAYS_SECONDS, + DEFAULT_MAX_COMPLETION_TOKENS as DEFAULT_MAX_COMPLETION_TOKENS, + DEFAULT_MAX_PROMPT_TOKENS as DEFAULT_MAX_PROMPT_TOKENS, +) diff --git a/src/leapfrogai_api/backend/thread_runner.py b/src/leapfrogai_api/backend/composer.py similarity index 99% rename from src/leapfrogai_api/backend/thread_runner.py rename to src/leapfrogai_api/backend/composer.py index 850ba001b..c1cc31e56 100644 --- a/src/leapfrogai_api/backend/thread_runner.py +++ b/src/leapfrogai_api/backend/composer.py @@ -53,7 +53,7 @@ from leapfrogai_api.typedef.vectorstores import SearchResponse -class ThreadRunner(BaseModel): +class Composer(BaseModel): @staticmethod async def list_messages(thread_id: str, session: Session) -> list[Message]: """List all the messages in a thread.""" diff --git a/src/leapfrogai_api/typedef/constants.py b/src/leapfrogai_api/backend/constants.py similarity index 76% rename from src/leapfrogai_api/typedef/constants.py rename to src/leapfrogai_api/backend/constants.py index 6a6340152..3a647df54 100644 --- a/src/leapfrogai_api/typedef/constants.py +++ b/src/leapfrogai_api/backend/constants.py @@ -1,6 +1,6 @@ # This file defines application constants not set by environment variables or configuration files. -THIRTY_DAYS = 60 * 60 * 24 * 30 # in seconds +THIRTY_DAYS_SECONDS = 60 * 60 * 24 * 30 # in seconds DEFAULT_MAX_COMPLETION_TOKENS = 4096 DEFAULT_MAX_PROMPT_TOKENS = 4096 diff --git a/src/leapfrogai_api/data/crud_api_key.py b/src/leapfrogai_api/data/crud_api_key.py index a460e8553..5a9840e41 100644 --- a/src/leapfrogai_api/data/crud_api_key.py +++ b/src/leapfrogai_api/data/crud_api_key.py @@ -7,7 +7,7 @@ from leapfrogai_api.data.crud_base import CRUDBase from leapfrogai_api.backend.security.api_key import APIKey, KEY_PREFIX -from leapfrogai_api.typedef.constants import THIRTY_DAYS +from leapfrogai_api.backend.constants import THIRTY_DAYS_SECONDS class APIKeyItem(BaseModel): @@ -31,7 +31,7 @@ class APIKeyItem(BaseModel): ) expires_at: int = Field( description="The time at which the API key expires, in seconds since the Unix epoch.", - examples=[int(time.time()) + THIRTY_DAYS], + examples=[int(time.time()) + THIRTY_DAYS_SECONDS], ) diff --git a/src/leapfrogai_api/routers/openai/runs.py b/src/leapfrogai_api/routers/openai/runs.py index 20baf5b97..c52ee7bc9 100644 --- a/src/leapfrogai_api/routers/openai/runs.py +++ b/src/leapfrogai_api/routers/openai/runs.py @@ -15,7 +15,7 @@ ) from leapfrogai_api.typedef.threads import ThreadRunCreateParamsRequest from leapfrogai_api.typedef.runs import RunCreateParamsRequest, ModifyRunRequest -from leapfrogai_api.backend.thread_runner import ThreadRunner +from leapfrogai_api.backend.composer import Composer router = APIRouter(prefix="/openai/v1/threads", tags=["openai/threads/runs"]) @@ -55,7 +55,7 @@ async def create_run( ) try: - return await ThreadRunner().generate_response( + return await Composer().generate_response( request=request, existing_thread=existing_thread, new_run=new_run, @@ -84,7 +84,7 @@ async def create_thread_and_run( ) try: - return await ThreadRunner().generate_response( + return await Composer().generate_response( request=request, exising_thread=new_thread, new_run=new_run, session=session ) except Exception as exc: diff --git a/src/leapfrogai_api/typedef/auth/auth_types.py b/src/leapfrogai_api/typedef/auth/auth_types.py index f3f335867..f49dd5bac 100644 --- a/src/leapfrogai_api/typedef/auth/auth_types.py +++ b/src/leapfrogai_api/typedef/auth/auth_types.py @@ -1,7 +1,7 @@ from pydantic import BaseModel, Field import time -from leapfrogai_api.typedef.constants import THIRTY_DAYS +from leapfrogai_api.backend.constants import THIRTY_DAYS_SECONDS class CreateAPIKeyRequest(BaseModel): @@ -14,9 +14,9 @@ class CreateAPIKeyRequest(BaseModel): ) expires_at: int = Field( - default=int(time.time()) + THIRTY_DAYS, + default=int(time.time()) + THIRTY_DAYS_SECONDS, description="The time at which the API key expires, in seconds since the Unix epoch.", - examples=[int(time.time()) + THIRTY_DAYS], + examples=[int(time.time()) + THIRTY_DAYS_SECONDS], ) @@ -32,5 +32,5 @@ class ModifyAPIKeyRequest(BaseModel): expires_at: int | None = Field( default=None, description="The time at which the API key expires, in seconds since the Unix epoch. If not provided, the expiration time will not be changed.", - examples=[int(time.time()) + THIRTY_DAYS], + examples=[int(time.time()) + THIRTY_DAYS_SECONDS], ) diff --git a/src/leapfrogai_api/typedef/chat/chat_types.py b/src/leapfrogai_api/typedef/chat/chat_types.py index c851681ad..9f8e7028d 100644 --- a/src/leapfrogai_api/typedef/chat/chat_types.py +++ b/src/leapfrogai_api/typedef/chat/chat_types.py @@ -3,7 +3,7 @@ from openai.types.beta.threads.text_content_block_param import TextContentBlockParam from ..common import Usage -from ..constants import DEFAULT_MAX_COMPLETION_TOKENS +from ...backend.constants import DEFAULT_MAX_COMPLETION_TOKENS class ChatFunction(BaseModel): diff --git a/src/leapfrogai_api/typedef/common.py b/src/leapfrogai_api/typedef/common.py index d050636c1..879dc0855 100644 --- a/src/leapfrogai_api/typedef/common.py +++ b/src/leapfrogai_api/typedef/common.py @@ -1,5 +1,5 @@ from pydantic import BaseModel, Field -from leapfrogai_api.typedef.constants import DEFAULT_MAX_COMPLETION_TOKENS +from leapfrogai_api.backend.constants import DEFAULT_MAX_COMPLETION_TOKENS class Usage(BaseModel): diff --git a/src/leapfrogai_api/typedef/completion/completion_types.py b/src/leapfrogai_api/typedef/completion/completion_types.py index 810920a60..2e406db6a 100644 --- a/src/leapfrogai_api/typedef/completion/completion_types.py +++ b/src/leapfrogai_api/typedef/completion/completion_types.py @@ -2,7 +2,7 @@ from typing import Literal from ..common import Usage -from ..constants import DEFAULT_MAX_COMPLETION_TOKENS +from ...backend.constants import DEFAULT_MAX_COMPLETION_TOKENS class CompletionChoice(BaseModel): diff --git a/src/leapfrogai_api/typedef/runs/run_create_base.py b/src/leapfrogai_api/typedef/runs/run_create_base.py index e593c8024..cd83fda65 100644 --- a/src/leapfrogai_api/typedef/runs/run_create_base.py +++ b/src/leapfrogai_api/typedef/runs/run_create_base.py @@ -22,7 +22,7 @@ from openai.types.beta.threads.run_create_params import TruncationStrategy from pydantic import BaseModel, Field, ValidationError -from leapfrogai_api.typedef.constants import ( +from leapfrogai_api.backend.constants import ( DEFAULT_MAX_COMPLETION_TOKENS, DEFAULT_MAX_PROMPT_TOKENS, ) diff --git a/tests/conformance/test_conformance_runs.py b/tests/conformance/test_conformance_runs.py index 7a4447bfc..18b79b6b1 100644 --- a/tests/conformance/test_conformance_runs.py +++ b/tests/conformance/test_conformance_runs.py @@ -1,7 +1,7 @@ import pytest from openai.types.beta.threads import Run, Message, TextContentBlock, Text -from .utils import client_config_factory +from ..utils.client import client_config_factory def make_mock_message_object(role, message_text): diff --git a/tests/integration/api/test_auth.py b/tests/integration/api/test_auth.py index 834dc8756..ec4aca178 100644 --- a/tests/integration/api/test_auth.py +++ b/tests/integration/api/test_auth.py @@ -5,7 +5,11 @@ import pytest from fastapi import status, HTTPException from fastapi.testclient import TestClient -from leapfrogai_api.routers.leapfrogai.auth import router, THIRTY_DAYS, APIKeyItem +from leapfrogai_api.routers.leapfrogai.auth import ( + router, + THIRTY_DAYS_SECONDS, + APIKeyItem, +) from leapfrogai_api.backend.security.api_key import APIKey @@ -32,7 +36,7 @@ def create_api_key(): request = { "name": "API Keys Are Cool!", - "expires_at": int(time.time()) + THIRTY_DAYS, + "expires_at": int(time.time()) + THIRTY_DAYS_SECONDS, } response = client.post("/leapfrogai/v1/auth/api-keys", json=request)