Skip to content

Commit f25bb71

Browse files
author
Andrej Simurka
committed
Refactored status codes, updated tests
1 parent 721c9f0 commit f25bb71

File tree

9 files changed

+735
-281
lines changed

9 files changed

+735
-281
lines changed

docs/openapi.json

Lines changed: 184 additions & 96 deletions
Large diffs are not rendered by default.

src/app/endpoints/conversations.py

Lines changed: 106 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,16 @@
1919
ConversationResponse,
2020
ConversationsListResponse,
2121
UnauthorizedResponse,
22+
NotFoundResponse,
23+
AccessDeniedResponse,
24+
BadRequestResponse,
25+
ServiceUnavailableResponse,
2226
)
2327
from utils.endpoints import (
2428
check_configuration_loaded,
2529
delete_conversation,
26-
validate_conversation_ownership,
30+
can_access_conversation,
31+
retrieve_conversation,
2732
)
2833
from utils.suid import check_suid
2934

@@ -32,102 +37,70 @@
3237

3338
conversation_responses: dict[int | str, dict[str, Any]] = {
3439
200: {
35-
"conversation_id": "123e4567-e89b-12d3-a456-426614174000",
36-
"chat_history": [
37-
{
38-
"messages": [
39-
{"content": "Hi", "type": "user"},
40-
{"content": "Hello!", "type": "assistant"},
41-
],
42-
"started_at": "2024-01-01T00:00:00Z",
43-
"completed_at": "2024-01-01T00:00:05Z",
44-
}
45-
],
40+
"model": ConversationResponse,
41+
"description": "Conversation retrieved successfully",
4642
},
4743
400: {
48-
"description": "Missing or invalid credentials provided by client",
49-
"model": UnauthorizedResponse,
44+
"model": BadRequestResponse,
45+
"description": "Invalid request",
5046
},
5147
401: {
52-
"description": "Unauthorized: Invalid or missing Bearer token",
5348
"model": UnauthorizedResponse,
49+
"description": "Unauthorized: Invalid or missing Bearer token",
50+
},
51+
403: {
52+
"model": AccessDeniedResponse,
53+
"description": "Client does not have permission to access conversation",
5454
},
5555
404: {
56-
"detail": {
57-
"response": "Conversation not found",
58-
"cause": "The specified conversation ID does not exist.",
59-
}
56+
"model": NotFoundResponse,
57+
"description": "Conversation not found",
6058
},
6159
503: {
62-
"detail": {
63-
"response": "Unable to connect to Llama Stack",
64-
"cause": "Connection error.",
65-
}
60+
"model": ServiceUnavailableResponse,
61+
"description": "Service unavailable",
6662
},
6763
}
6864

6965
conversation_delete_responses: dict[int | str, dict[str, Any]] = {
7066
200: {
71-
"conversation_id": "123e4567-e89b-12d3-a456-426614174000",
72-
"success": True,
73-
"message": "Conversation deleted successfully",
67+
"model": ConversationDeleteResponse,
68+
"description": "Conversation deleted successfully",
7469
},
7570
400: {
76-
"description": "Missing or invalid credentials provided by client",
77-
"model": UnauthorizedResponse,
71+
"model": BadRequestResponse,
72+
"description": "Invalid request",
7873
},
7974
401: {
80-
"description": "Unauthorized: Invalid or missing Bearer token",
8175
"model": UnauthorizedResponse,
76+
"description": "Unauthorized: Invalid or missing Bearer token",
77+
},
78+
403: {
79+
"model": AccessDeniedResponse,
80+
"description": "Client does not have permission to access conversation",
8281
},
8382
404: {
84-
"detail": {
85-
"response": "Conversation not found",
86-
"cause": "The specified conversation ID does not exist.",
87-
}
83+
"model": NotFoundResponse,
84+
"description": "Conversation not found",
8885
},
8986
503: {
90-
"detail": {
91-
"response": "Unable to connect to Llama Stack",
92-
"cause": "Connection error.",
93-
}
87+
"model": ServiceUnavailableResponse,
88+
"description": "Service unavailable",
9489
},
9590
}
9691

9792
conversations_list_responses: dict[int | str, dict[str, Any]] = {
9893
200: {
99-
"conversations": [
100-
{
101-
"conversation_id": "123e4567-e89b-12d3-a456-426614174000",
102-
"created_at": "2024-01-01T00:00:00Z",
103-
"last_message_at": "2024-01-01T00:05:00Z",
104-
"last_used_model": "gemini/gemini-1.5-flash",
105-
"last_used_provider": "gemini",
106-
"message_count": 5,
107-
},
108-
{
109-
"conversation_id": "456e7890-e12b-34d5-a678-901234567890",
110-
"created_at": "2024-01-01T01:00:00Z",
111-
"last_message_at": "2024-01-01T01:02:00Z",
112-
"last_used_model": "gemini/gemini-2.0-flash",
113-
"last_used_provider": "gemini",
114-
"message_count": 2,
115-
},
116-
]
117-
},
118-
400: {
119-
"description": "Missing or invalid credentials provided by client",
120-
"model": UnauthorizedResponse,
94+
"model": ConversationsListResponse,
95+
"description": "List of conversations retrieved successfully",
12196
},
12297
401: {
123-
"description": "Unauthorized: Invalid or missing Bearer token",
12498
"model": UnauthorizedResponse,
99+
"description": "Unauthorized: Invalid or missing Bearer token",
125100
},
126101
503: {
127-
"detail": {
128-
"response": "Unable to connect to Llama Stack",
129-
"cause": "Connection error.",
130-
}
102+
"model": ServiceUnavailableResponse,
103+
"description": "Service unavailable",
131104
},
132105
}
133106

@@ -267,34 +240,42 @@ async def get_conversation_endpoint_handler(
267240
logger.error("Invalid conversation ID format: %s", conversation_id)
268241
raise HTTPException(
269242
status_code=status.HTTP_400_BAD_REQUEST,
270-
detail={
271-
"response": "Invalid conversation ID format",
272-
"cause": f"Conversation ID {conversation_id} is not a valid UUID",
273-
},
243+
detail=BadRequestResponse(
244+
resource="conversation", resource_id=conversation_id
245+
).dump_detail(),
274246
)
275247

276248
user_id = auth[0]
277-
278-
user_conversation = validate_conversation_ownership(
279-
user_id=user_id,
280-
conversation_id=conversation_id,
249+
if not can_access_conversation(
250+
conversation_id,
251+
user_id,
281252
others_allowed=(
282253
Action.READ_OTHERS_CONVERSATIONS in request.state.authorized_actions
283254
),
284-
)
285-
286-
if user_conversation is None:
255+
):
287256
logger.warning(
288-
"User %s attempted to read conversation %s they don't own",
257+
"User %s attempted to read conversation %s they don't have access to",
289258
user_id,
290259
conversation_id,
291260
)
292261
raise HTTPException(
293262
status_code=status.HTTP_403_FORBIDDEN,
294-
detail={
295-
"response": "Access denied",
296-
"cause": "You do not have permission to read this conversation",
297-
},
263+
detail=AccessDeniedResponse(
264+
user_id=user_id,
265+
resource="conversation",
266+
resource_id=conversation_id,
267+
action="read",
268+
).dump_detail(),
269+
)
270+
271+
# If reached this, user is authorized to retreive this conversation
272+
conversation = retrieve_conversation(conversation_id)
273+
if conversation is None:
274+
raise HTTPException(
275+
status_code=status.HTTP_404_NOT_FOUND,
276+
detail=NotFoundResponse(
277+
resource="conversation", resource_id=conversation_id
278+
).dump_detail(),
298279
)
299280

300281
agent_id = conversation_id
@@ -308,10 +289,9 @@ async def get_conversation_endpoint_handler(
308289
logger.error("No sessions found for conversation %s", conversation_id)
309290
raise HTTPException(
310291
status_code=status.HTTP_404_NOT_FOUND,
311-
detail={
312-
"response": "Conversation not found",
313-
"cause": f"Conversation {conversation_id} could not be retrieved.",
314-
},
292+
detail=NotFoundResponse(
293+
resource="conversation", resource_id=conversation_id
294+
).dump_detail(),
315295
)
316296
session_id = str(agent_sessions[0].get("session_id"))
317297

@@ -334,22 +314,23 @@ async def get_conversation_endpoint_handler(
334314
logger.error("Unable to connect to Llama Stack: %s", e)
335315
raise HTTPException(
336316
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
337-
detail={
338-
"response": "Unable to connect to Llama Stack",
339-
"cause": str(e),
340-
},
317+
detail=ServiceUnavailableResponse(
318+
backend_name="Llama Stack", cause=str(e)
319+
).dump_detail(),
341320
) from e
321+
342322
except NotFoundError as e:
343323
logger.error("Conversation not found: %s", e)
344324
raise HTTPException(
345325
status_code=status.HTTP_404_NOT_FOUND,
346-
detail={
347-
"response": "Conversation not found",
348-
"cause": f"Conversation {conversation_id} could not be retrieved: {str(e)}",
349-
},
326+
detail=NotFoundResponse(
327+
resource="conversation", resource_id=conversation_id
328+
).dump_detail(),
350329
) from e
330+
351331
except HTTPException:
352332
raise
333+
353334
except Exception as e:
354335
# Handle case where session doesn't exist or other errors
355336
logger.exception("Error retrieving conversation %s: %s", conversation_id, e)
@@ -389,34 +370,42 @@ async def delete_conversation_endpoint_handler(
389370
logger.error("Invalid conversation ID format: %s", conversation_id)
390371
raise HTTPException(
391372
status_code=status.HTTP_400_BAD_REQUEST,
392-
detail={
393-
"response": "Invalid conversation ID format",
394-
"cause": f"Conversation ID {conversation_id} is not a valid UUID",
395-
},
373+
detail=BadRequestResponse(
374+
resource="conversation", resource_id=conversation_id
375+
).dump_detail(),
396376
)
397377

398378
user_id = auth[0]
399-
400-
user_conversation = validate_conversation_ownership(
401-
user_id=user_id,
402-
conversation_id=conversation_id,
379+
if not can_access_conversation(
380+
conversation_id,
381+
user_id,
403382
others_allowed=(
404383
Action.DELETE_OTHERS_CONVERSATIONS in request.state.authorized_actions
405384
),
406-
)
407-
408-
if user_conversation is None:
385+
):
409386
logger.warning(
410-
"User %s attempted to delete conversation %s they don't own",
387+
"User %s attempted to delete conversation %s they don't have access to",
411388
user_id,
412389
conversation_id,
413390
)
414391
raise HTTPException(
415392
status_code=status.HTTP_403_FORBIDDEN,
416-
detail={
417-
"response": "Access denied",
418-
"cause": "You do not have permission to delete this conversation",
419-
},
393+
detail=AccessDeniedResponse(
394+
user_id=user_id,
395+
resource="conversation",
396+
resource_id=conversation_id,
397+
action="delete",
398+
).dump_detail(),
399+
)
400+
401+
# If reached this, user is authorized to retreive this conversation
402+
conversation = retrieve_conversation(conversation_id)
403+
if conversation is None:
404+
raise HTTPException(
405+
status_code=status.HTTP_404_NOT_FOUND,
406+
detail=NotFoundResponse(
407+
resource="conversation", resource_id=conversation_id
408+
).dump_detail(),
420409
)
421410

422411
agent_id = conversation_id
@@ -452,25 +441,24 @@ async def delete_conversation_endpoint_handler(
452441
)
453442

454443
except APIConnectionError as e:
455-
logger.error("Unable to connect to Llama Stack: %s", e)
456444
raise HTTPException(
457445
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
458-
detail={
459-
"response": "Unable to connect to Llama Stack",
460-
"cause": str(e),
461-
},
446+
detail=ServiceUnavailableResponse(
447+
backend_name="Llama Stack", cause=str(e)
448+
).dump_detail(),
462449
) from e
450+
463451
except NotFoundError as e:
464-
logger.error("Conversation not found: %s", e)
465452
raise HTTPException(
466453
status_code=status.HTTP_404_NOT_FOUND,
467-
detail={
468-
"response": "Conversation not found",
469-
"cause": f"Conversation {conversation_id} could not be deleted: {str(e)}",
470-
},
454+
detail=NotFoundResponse(
455+
resource="conversation", resource_id=conversation_id
456+
).dump_detail(),
471457
) from e
458+
472459
except HTTPException:
473460
raise
461+
474462
except Exception as e:
475463
# Handle case where session doesn't exist or other errors
476464
logger.exception("Error deleting conversation %s: %s", conversation_id, e)

0 commit comments

Comments
 (0)