Skip to content

Commit c518fc5

Browse files
Merge pull request #123 from askui/fix/persistence-in-chat-api
Fix/persistence in chat api
2 parents 7e6b32a + 2e70aab commit c518fc5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+5306
-996
lines changed

mypy.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@ namespace_packages = true
2424

2525
[mypy-jsonref.*]
2626
ignore_missing_imports = true
27+
28+
[mypy-bson.*]
29+
ignore_missing_imports = true

pdm.lock

Lines changed: 17 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ dependencies = [
2929
"filetype>=1.2.0",
3030
"markitdown[xls,xlsx,docx]>=0.1.2",
3131
"asyncer==0.0.8",
32+
"bson>=0.5.10",
3233
]
3334
requires-python = ">=3.10"
3435
readme = "README.md"
@@ -102,6 +103,7 @@ python_files = ["test_*.py"]
102103
python_functions = ["test_*"]
103104
testpaths = ["tests"]
104105
timeout = 60
106+
asyncio_default_fixture_loop_scope = "session"
105107

106108
[tool.ruff]
107109
# Exclude a variety of commonly ignored directories.

src/askui/chat/api/app.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
from contextlib import asynccontextmanager
22
from typing import AsyncGenerator
33

4-
from fastapi import APIRouter, FastAPI
4+
from fastapi import APIRouter, FastAPI, HTTPException, Request, status
55
from fastapi.middleware.cors import CORSMiddleware
6+
from fastapi.responses import JSONResponse
67

78
from askui.chat.api.assistants.dependencies import get_assistant_service
89
from askui.chat.api.assistants.router import router as assistants_router
910
from askui.chat.api.dependencies import SetEnvFromHeadersDep, get_settings
11+
from askui.chat.api.files.router import router as files_router
1012
from askui.chat.api.health.router import router as health_router
1113
from askui.chat.api.mcp_configs.router import router as mcp_configs_router
1214
from askui.chat.api.messages.router import router as messages_router
1315
from askui.chat.api.runs.router import router as runs_router
1416
from askui.chat.api.threads.router import router as threads_router
17+
from askui.utils.api_utils import (
18+
ConflictError,
19+
FileTooLargeError,
20+
LimitReachedError,
21+
NotFoundError,
22+
)
1523

1624

1725
@asynccontextmanager
@@ -38,12 +46,69 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: # noqa: ARG001
3846
allow_headers=["*"],
3947
)
4048

49+
50+
@app.exception_handler(NotFoundError)
51+
def not_found_error_handler(
52+
request: Request, # noqa: ARG001
53+
exc: NotFoundError,
54+
) -> JSONResponse:
55+
return JSONResponse(
56+
status_code=status.HTTP_404_NOT_FOUND, content={"detail": str(exc)}
57+
)
58+
59+
60+
@app.exception_handler(ConflictError)
61+
def conflict_error_handler(
62+
request: Request, # noqa: ARG001
63+
exc: ConflictError,
64+
) -> JSONResponse:
65+
return JSONResponse(
66+
status_code=status.HTTP_409_CONFLICT, content={"detail": str(exc)}
67+
)
68+
69+
70+
@app.exception_handler(LimitReachedError)
71+
def limit_reached_error_handler(
72+
request: Request, # noqa: ARG001
73+
exc: LimitReachedError,
74+
) -> JSONResponse:
75+
return JSONResponse(
76+
status_code=status.HTTP_400_BAD_REQUEST, content={"detail": str(exc)}
77+
)
78+
79+
80+
@app.exception_handler(FileTooLargeError)
81+
def file_too_large_error_handler(
82+
request: Request, # noqa: ARG001
83+
exc: FileTooLargeError,
84+
) -> JSONResponse:
85+
return JSONResponse(
86+
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
87+
content={"detail": str(exc)},
88+
)
89+
90+
91+
@app.exception_handler(Exception)
92+
def catch_all_exception_handler(
93+
request: Request, # noqa: ARG001
94+
exc: Exception,
95+
) -> JSONResponse:
96+
if isinstance(exc, HTTPException):
97+
raise exc
98+
99+
return JSONResponse(
100+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
101+
content={"detail": "Internal server error"},
102+
)
103+
104+
41105
# Include routers
42106
v1_router = APIRouter(prefix="/v1")
43107
v1_router.include_router(assistants_router)
44108
v1_router.include_router(threads_router)
45109
v1_router.include_router(messages_router)
46110
v1_router.include_router(runs_router)
47111
v1_router.include_router(mcp_configs_router)
112+
v1_router.include_router(files_router)
48113
v1_router.include_router(health_router)
49114
app.include_router(v1_router)
Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,53 @@
11
from typing import Literal
22

3-
from pydantic import BaseModel, Field
3+
from pydantic import BaseModel
44

5+
from askui.chat.api.models import AssistantId
6+
from askui.utils.api_utils import Resource
57
from askui.utils.datetime_utils import UnixDatetime, now
68
from askui.utils.id_utils import generate_time_ordered_id
9+
from askui.utils.not_given import NOT_GIVEN, BaseModelWithNotGiven, NotGiven
710

811

9-
class Assistant(BaseModel):
10-
"""An assistant that can be used in a thread."""
12+
class AssistantBase(BaseModel):
13+
"""Base assistant model."""
1114

12-
id: str = Field(default_factory=lambda: generate_time_ordered_id("asst"))
13-
created_at: UnixDatetime = Field(default_factory=now)
1415
name: str | None = None
1516
description: str | None = None
17+
avatar: str | None = None
18+
19+
20+
class AssistantCreateParams(AssistantBase):
21+
"""Parameters for creating an assistant."""
22+
23+
24+
class AssistantModifyParams(BaseModelWithNotGiven):
25+
"""Parameters for modifying an assistant."""
26+
27+
name: str | NotGiven = NOT_GIVEN
28+
description: str | NotGiven = NOT_GIVEN
29+
avatar: str | NotGiven = NOT_GIVEN
30+
31+
32+
class Assistant(AssistantBase, Resource):
33+
"""An assistant that can be used in a thread."""
34+
35+
id: AssistantId
1636
object: Literal["assistant"] = "assistant"
17-
avatar: str | None = Field(default=None, description="URL of the avatar image")
37+
created_at: UnixDatetime
38+
39+
@classmethod
40+
def create(cls, params: AssistantCreateParams) -> "Assistant":
41+
return cls(
42+
id=generate_time_ordered_id("asst"),
43+
created_at=now(),
44+
**params.model_dump(),
45+
)
46+
47+
def modify(self, params: AssistantModifyParams) -> "Assistant":
48+
return Assistant.model_validate(
49+
{
50+
**self.model_dump(),
51+
**params.model_dump(),
52+
}
53+
)
Lines changed: 29 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
from fastapi import APIRouter, HTTPException
1+
from fastapi import APIRouter, status
22

3-
# from fastapi import status
43
from askui.chat.api.assistants.dependencies import AssistantServiceDep
5-
from askui.chat.api.assistants.models import Assistant
6-
from askui.chat.api.assistants.service import (
7-
AssistantService, # AssistantModifyRequest, CreateAssistantRequest,
4+
from askui.chat.api.assistants.models import (
5+
Assistant,
6+
AssistantCreateParams,
7+
AssistantModifyParams,
88
)
9-
from askui.chat.api.models import ListQueryDep
9+
from askui.chat.api.assistants.service import AssistantService
10+
from askui.chat.api.dependencies import ListQueryDep
11+
from askui.chat.api.models import AssistantId
1012
from askui.utils.api_utils import ListQuery, ListResponse
1113

1214
router = APIRouter(prefix="/assistants", tags=["assistants"])
@@ -17,51 +19,37 @@ def list_assistants(
1719
query: ListQuery = ListQueryDep,
1820
assistant_service: AssistantService = AssistantServiceDep,
1921
) -> ListResponse[Assistant]:
20-
"""List all assistants."""
2122
return assistant_service.list_(query=query)
2223

2324

24-
# @router.post("", status_code=status.HTTP_201_CREATED)
25-
# def create_assistant(
26-
# request: CreateAssistantRequest,
27-
# assistant_service: AssistantService = AssistantServiceDep,
28-
# ) -> Assistant:
29-
# """Create a new assistant."""
30-
# return assistant_service.create(request)
25+
@router.post("", status_code=status.HTTP_201_CREATED)
26+
def create_assistant(
27+
params: AssistantCreateParams,
28+
assistant_service: AssistantService = AssistantServiceDep,
29+
) -> Assistant:
30+
return assistant_service.create(params)
3131

3232

3333
@router.get("/{assistant_id}")
3434
def retrieve_assistant(
35-
assistant_id: str,
35+
assistant_id: AssistantId,
3636
assistant_service: AssistantService = AssistantServiceDep,
3737
) -> Assistant:
38-
"""Get an assistant by ID."""
39-
try:
40-
return assistant_service.retrieve(assistant_id)
41-
except FileNotFoundError as e:
42-
raise HTTPException(status_code=404, detail=str(e)) from e
38+
return assistant_service.retrieve(assistant_id)
4339

4440

45-
# @router.post("/{assistant_id}")
46-
# def modify_assistant(
47-
# assistant_id: str,
48-
# request: AssistantModifyRequest,
49-
# assistant_service: AssistantService = AssistantServiceDep,
50-
# ) -> Assistant:
51-
# """Update an assistant."""
52-
# try:
53-
# return assistant_service.modify(assistant_id, request)
54-
# except FileNotFoundError as e:
55-
# raise HTTPException(status_code=404, detail=str(e)) from e
41+
@router.post("/{assistant_id}")
42+
def modify_assistant(
43+
assistant_id: AssistantId,
44+
params: AssistantModifyParams,
45+
assistant_service: AssistantService = AssistantServiceDep,
46+
) -> Assistant:
47+
return assistant_service.modify(assistant_id, params)
5648

5749

58-
# @router.delete("/{assistant_id}", status_code=status.HTTP_204_NO_CONTENT)
59-
# def delete_assistant(
60-
# assistant_id: str,
61-
# assistant_service: AssistantService = AssistantServiceDep,
62-
# ) -> None:
63-
# """Delete an assistant."""
64-
# try:
65-
# assistant_service.delete(assistant_id)
66-
# except FileNotFoundError as e:
67-
# raise HTTPException(status_code=404, detail=str(e)) from e
50+
@router.delete("/{assistant_id}", status_code=status.HTTP_204_NO_CONTENT)
51+
def delete_assistant(
52+
assistant_id: AssistantId,
53+
assistant_service: AssistantService = AssistantServiceDep,
54+
) -> None:
55+
assistant_service.delete(assistant_id)

0 commit comments

Comments
 (0)