Skip to content

Commit 8f6a0e8

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

File tree

9 files changed

+723
-288
lines changed

9 files changed

+723
-288
lines changed

docs/openapi.json

Lines changed: 178 additions & 102 deletions
Large diffs are not rendered by default.

src/app/endpoints/conversations.py

Lines changed: 104 additions & 119 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

@@ -261,40 +234,46 @@ async def get_conversation_endpoint_handler(
261234
ID and simplified chat history.
262235
"""
263236
check_configuration_loaded(configuration)
264-
237+
265238
# Validate conversation ID format
266239
if not check_suid(conversation_id):
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+
conversation = retrieve_conversation(conversation_id, user_id)
271+
if conversation is None:
272+
raise HTTPException(
273+
status_code=status.HTTP_404_NOT_FOUND,
274+
detail=NotFoundResponse(
275+
resource="conversation", resource_id=conversation_id
276+
).dump_detail(),
298277
)
299278

300279
agent_id = conversation_id
@@ -308,10 +287,9 @@ async def get_conversation_endpoint_handler(
308287
logger.error("No sessions found for conversation %s", conversation_id)
309288
raise HTTPException(
310289
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-
},
290+
detail=NotFoundResponse(
291+
resource="conversation", resource_id=conversation_id
292+
).dump_detail(),
315293
)
316294
session_id = str(agent_sessions[0].get("session_id"))
317295

@@ -334,22 +312,23 @@ async def get_conversation_endpoint_handler(
334312
logger.error("Unable to connect to Llama Stack: %s", e)
335313
raise HTTPException(
336314
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
337-
detail={
338-
"response": "Unable to connect to Llama Stack",
339-
"cause": str(e),
340-
},
315+
detail=ServiceUnavailableResponse(
316+
backend_name="Llama Stack", cause=str(e)
317+
).dump_detail(),
341318
) from e
319+
342320
except NotFoundError as e:
343321
logger.error("Conversation not found: %s", e)
344322
raise HTTPException(
345323
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-
},
324+
detail=NotFoundResponse(
325+
resource="conversation", resource_id=conversation_id
326+
).dump_detail(),
350327
) from e
328+
351329
except HTTPException:
352330
raise
331+
353332
except Exception as e:
354333
# Handle case where session doesn't exist or other errors
355334
logger.exception("Error retrieving conversation %s: %s", conversation_id, e)
@@ -389,34 +368,41 @@ async def delete_conversation_endpoint_handler(
389368
logger.error("Invalid conversation ID format: %s", conversation_id)
390369
raise HTTPException(
391370
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-
},
371+
detail=BadRequestResponse(
372+
resource="conversation", resource_id=conversation_id
373+
).dump_detail(),
396374
)
397375

398376
user_id = auth[0]
399-
400-
user_conversation = validate_conversation_ownership(
401-
user_id=user_id,
402-
conversation_id=conversation_id,
377+
if not can_access_conversation(
378+
conversation_id,
379+
user_id,
403380
others_allowed=(
404381
Action.DELETE_OTHERS_CONVERSATIONS in request.state.authorized_actions
405382
),
406-
)
407-
408-
if user_conversation is None:
383+
):
409384
logger.warning(
410-
"User %s attempted to delete conversation %s they don't own",
385+
"User %s attempted to delete conversation %s they don't have access to",
411386
user_id,
412387
conversation_id,
413388
)
414389
raise HTTPException(
415390
status_code=status.HTTP_403_FORBIDDEN,
416-
detail={
417-
"response": "Access denied",
418-
"cause": "You do not have permission to delete this conversation",
419-
},
391+
detail=AccessDeniedResponse(
392+
user_id=user_id,
393+
resource="conversation",
394+
resource_id=conversation_id,
395+
action="delete",
396+
).dump_detail(),
397+
)
398+
399+
conversation = retrieve_conversation(conversation_id, user_id)
400+
if conversation is None:
401+
raise HTTPException(
402+
status_code=status.HTTP_404_NOT_FOUND,
403+
detail=NotFoundResponse(
404+
resource="conversation", resource_id=conversation_id
405+
).dump_detail(),
420406
)
421407

422408
agent_id = conversation_id
@@ -452,25 +438,24 @@ async def delete_conversation_endpoint_handler(
452438
)
453439

454440
except APIConnectionError as e:
455-
logger.error("Unable to connect to Llama Stack: %s", e)
456441
raise HTTPException(
457442
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
458-
detail={
459-
"response": "Unable to connect to Llama Stack",
460-
"cause": str(e),
461-
},
443+
detail=ServiceUnavailableResponse(
444+
backend_name="Llama Stack", cause=str(e)
445+
).dump_detail(),
462446
) from e
447+
463448
except NotFoundError as e:
464-
logger.error("Conversation not found: %s", e)
465449
raise HTTPException(
466450
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-
},
451+
detail=NotFoundResponse(
452+
resource="conversation", resource_id=conversation_id
453+
).dump_detail(),
471454
) from e
455+
472456
except HTTPException:
473457
raise
458+
474459
except Exception as e:
475460
# Handle case where session doesn't exist or other errors
476461
logger.exception("Error deleting conversation %s: %s", conversation_id, e)

0 commit comments

Comments
 (0)