Skip to content

Commit c441403

Browse files
committed
Implementation of /v2/conversations endpoints
1 parent 9bf87a4 commit c441403

File tree

4 files changed

+221
-2
lines changed

4 files changed

+221
-2
lines changed
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
"""Handler for REST API calls to manage conversation history."""
2+
3+
import logging
4+
from typing import Any
5+
6+
from fastapi import APIRouter, Request, Depends, HTTPException, status
7+
8+
from configuration import configuration
9+
from authentication import get_auth_dependency
10+
from authorization.middleware import authorize
11+
from models.cache_entry import CacheEntry
12+
from models.config import Action
13+
from models.responses import (
14+
ConversationsListResponseV2,
15+
ConversationResponse,
16+
ConversationDeleteResponse,
17+
UnauthorizedResponse,
18+
)
19+
from utils.endpoints import check_configuration_loaded
20+
from utils.suid import check_suid
21+
22+
logger = logging.getLogger("app.endpoints.handlers")
23+
router = APIRouter(tags=["conversations_v2"])
24+
auth_dependency = get_auth_dependency()
25+
26+
27+
conversation_responses: dict[int | str, dict[str, Any]] = {
28+
200: {
29+
"conversation_id": "123e4567-e89b-12d3-a456-426614174000",
30+
"chat_history": [
31+
{
32+
"messages": [
33+
{"content": "Hi", "type": "user"},
34+
{"content": "Hello!", "type": "assistant"},
35+
],
36+
"started_at": "2024-01-01T00:00:00Z",
37+
"completed_at": "2024-01-01T00:00:05Z",
38+
"provider": "provider ID",
39+
"model": "model ID",
40+
}
41+
],
42+
},
43+
400: {
44+
"description": "Missing or invalid credentials provided by client",
45+
"model": UnauthorizedResponse,
46+
},
47+
401: {
48+
"description": "Unauthorized: Invalid or missing Bearer token",
49+
"model": UnauthorizedResponse,
50+
},
51+
404: {
52+
"detail": {
53+
"response": "Conversation not found",
54+
"cause": "The specified conversation ID does not exist.",
55+
}
56+
},
57+
}
58+
59+
conversation_delete_responses: dict[int | str, dict[str, Any]] = {
60+
200: {
61+
"conversation_id": "123e4567-e89b-12d3-a456-426614174000",
62+
"success": True,
63+
"message": "Conversation deleted successfully",
64+
},
65+
400: {
66+
"description": "Missing or invalid credentials provided by client",
67+
"model": UnauthorizedResponse,
68+
},
69+
401: {
70+
"description": "Unauthorized: Invalid or missing Bearer token",
71+
"model": UnauthorizedResponse,
72+
},
73+
404: {
74+
"detail": {
75+
"response": "Conversation not found",
76+
"cause": "The specified conversation ID does not exist.",
77+
}
78+
},
79+
}
80+
81+
conversations_list_responses: dict[int | str, dict[str, Any]] = {
82+
200: {
83+
"conversations": [
84+
{
85+
"conversation_id": "123e4567-e89b-12d3-a456-426614174000",
86+
}
87+
]
88+
}
89+
}
90+
91+
92+
@router.get("/conversations", responses=conversations_list_responses)
93+
@authorize(Action.LIST_CONVERSATIONS)
94+
async def get_conversations_list_endpoint_handler(
95+
request: Request,
96+
auth: Any = Depends(auth_dependency),
97+
) -> ConversationsListResponseV2:
98+
"""Handle request to retrieve all conversations for the authenticated user."""
99+
check_configuration_loaded(configuration)
100+
101+
user_id = auth[0]
102+
103+
logger.info("Retrieving conversations for user %s", user_id)
104+
conversations = configuration.conversation_cache.list(user_id, False)
105+
logger.info("Conversations for user %s: %s", user_id, len(conversations))
106+
107+
return ConversationsListResponseV2(conversations=conversations)
108+
109+
110+
@router.get("/conversations/{conversation_id}", responses=conversation_responses)
111+
@authorize(Action.GET_CONVERSATION)
112+
async def get_conversation_endpoint_handler(
113+
request: Request,
114+
conversation_id: str,
115+
auth: Any = Depends(auth_dependency),
116+
) -> ConversationResponse:
117+
"""Handle request to retrieve a conversation by ID."""
118+
check_configuration_loaded(configuration)
119+
check_valid_conversation_id(conversation_id)
120+
121+
user_id = auth[0]
122+
logger.info("Retrieving conversation %s for user %s", conversation_id, user_id)
123+
124+
check_conversation_existence(user_id, conversation_id)
125+
126+
conversation = configuration.conversation_cache.get(user_id, conversation_id, False)
127+
chat_history = [transform_chat_message(entry) for entry in conversation]
128+
129+
return ConversationResponse(
130+
conversation_id=conversation_id, chat_history=chat_history
131+
)
132+
133+
134+
@router.delete(
135+
"/conversations/{conversation_id}", responses=conversation_delete_responses
136+
)
137+
@authorize(Action.DELETE_CONVERSATION)
138+
async def delete_conversation_endpoint_handler(
139+
request: Request,
140+
conversation_id: str,
141+
auth: Any = Depends(auth_dependency),
142+
) -> ConversationDeleteResponse:
143+
"""Handle request to delete a conversation by ID."""
144+
check_configuration_loaded(configuration)
145+
check_valid_conversation_id(conversation_id)
146+
147+
user_id = auth[0]
148+
logger.info("Deleting conversation %s for user %s", conversation_id, user_id)
149+
150+
check_conversation_existence(user_id, conversation_id)
151+
152+
logger.info("Deleting conversation %s for user %s", conversation_id, user_id)
153+
deleted = configuration.conversation_cache.delete(user_id, conversation_id, False)
154+
155+
if deleted:
156+
return ConversationDeleteResponse(
157+
conversation_id=conversation_id,
158+
success=True,
159+
response="Conversation deleted successfully",
160+
)
161+
return ConversationDeleteResponse(
162+
conversation_id=conversation_id,
163+
success=True,
164+
response="Conversation can not be deleted",
165+
)
166+
167+
168+
def check_valid_conversation_id(conversation_id: str) -> None:
169+
"""Check validity of conversation ID format."""
170+
if not check_suid(conversation_id):
171+
logger.error("Invalid conversation ID format: %s", conversation_id)
172+
raise HTTPException(
173+
status_code=status.HTTP_400_BAD_REQUEST,
174+
detail={
175+
"response": "Invalid conversation ID format",
176+
"cause": f"Conversation ID {conversation_id} is not a valid UUID",
177+
},
178+
)
179+
180+
181+
def check_conversation_existence(user_id: str, conversation_id: str) -> None:
182+
"""Check if conversation exists."""
183+
conversations = configuration.conversation_cache.list(user_id, False)
184+
if conversation_id not in conversations:
185+
logger.error("No conversation found for conversation ID %s", conversation_id)
186+
raise HTTPException(
187+
status_code=status.HTTP_404_NOT_FOUND,
188+
detail={
189+
"response": "Conversation not found",
190+
"cause": f"Conversation {conversation_id} could not be retrieved.",
191+
},
192+
)
193+
194+
195+
def transform_chat_message(entry: CacheEntry) -> dict[str, Any]:
196+
return {
197+
"provider": entry.provider,
198+
"model": entry.model,
199+
"query": entry.query,
200+
"response": entry.response,
201+
"messages": [
202+
{"content": entry.query, "type": "user"},
203+
{"content": entry.response, "type": "assistant"},
204+
],
205+
}

src/app/routers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
streaming_query,
1414
authorized,
1515
conversations,
16+
conversations_v2,
1617
metrics,
1718
)
1819

@@ -31,6 +32,7 @@ def include_routers(app: FastAPI) -> None:
3132
app.include_router(config.router, prefix="/v1")
3233
app.include_router(feedback.router, prefix="/v1")
3334
app.include_router(conversations.router, prefix="/v1")
35+
app.include_router(conversations_v2.router, prefix="/v2")
3436

3537
# road-core does not version these endpoints
3638
app.include_router(health.router)

src/models/responses.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,16 @@ class ConversationsListResponse(BaseModel):
667667
}
668668

669669

670+
class ConversationsListResponseV2(BaseModel):
671+
"""Model representing a response for listing conversations of a user.
672+
673+
Attributes:
674+
conversations: List of conversation IDs associated with the user.
675+
"""
676+
677+
conversations: list[str]
678+
679+
670680
class ErrorResponse(BaseModel):
671681
"""Model representing error response for query endpoint."""
672682

tests/unit/app/test_routers.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from app.endpoints import (
1010
conversations,
11+
conversations_v2,
1112
root,
1213
info,
1314
models,
@@ -60,7 +61,7 @@ def test_include_routers() -> None:
6061
include_routers(app)
6162

6263
# are all routers added?
63-
assert len(app.routers) == 11
64+
assert len(app.routers) == 12
6465
assert root.router in app.get_routers()
6566
assert info.router in app.get_routers()
6667
assert models.router in app.get_routers()
@@ -80,7 +81,7 @@ def test_check_prefixes() -> None:
8081
include_routers(app)
8182

8283
# are all routers added?
83-
assert len(app.routers) == 11
84+
assert len(app.routers) == 12
8485
assert app.get_router_prefix(root.router) == ""
8586
assert app.get_router_prefix(info.router) == "/v1"
8687
assert app.get_router_prefix(models.router) == "/v1"
@@ -92,3 +93,4 @@ def test_check_prefixes() -> None:
9293
assert app.get_router_prefix(authorized.router) == ""
9394
assert app.get_router_prefix(conversations.router) == "/v1"
9495
assert app.get_router_prefix(metrics.router) == ""
96+
assert app.get_router_prefix(conversations_v2.router) == "/v2"

0 commit comments

Comments
 (0)