Skip to content

Commit a7d77ba

Browse files
committed
Add /conversations endpoint for conversation history management
- Add GET /v1/conversations/{conversation_id} to retrieve conversation history - Add DELETE /v1/conversations/{conversation_id} to delete conversations - Use llama-stack client.agents.session.retrieve and .delete methods - Map conversation ID to agent ID for LlamaStack operations - Add ConversationResponse and ConversationDeleteResponse models - Include conversations router in main app routing - Maintain consistent error handling and authentication patterns
1 parent 8833716 commit a7d77ba

File tree

6 files changed

+287
-2
lines changed

6 files changed

+287
-2
lines changed

src/app/endpoints/conversations.py

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
"""Handler for REST API calls to manage conversation history."""
2+
3+
import logging
4+
from typing import Any
5+
6+
from llama_stack_client import APIConnectionError
7+
8+
from fastapi import APIRouter, HTTPException, status, Depends
9+
10+
from client import LlamaStackClientHolder
11+
from configuration import configuration
12+
from models.responses import ConversationResponse, ConversationDeleteResponse
13+
from auth import get_auth_dependency
14+
from utils.endpoints import check_configuration_loaded
15+
from utils.suid import check_suid
16+
17+
logger = logging.getLogger("app.endpoints.handlers")
18+
router = APIRouter(tags=["conversations"])
19+
auth_dependency = get_auth_dependency()
20+
21+
conversation_id_to_agent_id: dict[str, str] = {}
22+
23+
conversation_responses: dict[int | str, dict[str, Any]] = {
24+
200: {
25+
"conversation_id": "123e4567-e89b-12d3-a456-426614174000",
26+
"session_data": {
27+
"session_id": "123e4567-e89b-12d3-a456-426614174000",
28+
"turns": [],
29+
"started_at": "2024-01-01T00:00:00Z",
30+
},
31+
},
32+
404: {
33+
"detail": {
34+
"response": "Conversation not found",
35+
"cause": "The specified conversation ID does not exist.",
36+
}
37+
},
38+
503: {
39+
"detail": {
40+
"response": "Unable to connect to Llama Stack",
41+
"cause": "Connection error.",
42+
}
43+
},
44+
}
45+
46+
conversation_delete_responses: dict[int | str, dict[str, Any]] = {
47+
200: {
48+
"conversation_id": "123e4567-e89b-12d3-a456-426614174000",
49+
"success": True,
50+
"message": "Conversation deleted successfully",
51+
},
52+
404: {
53+
"detail": {
54+
"response": "Conversation not found",
55+
"cause": "The specified conversation ID does not exist.",
56+
}
57+
},
58+
503: {
59+
"detail": {
60+
"response": "Unable to connect to Llama Stack",
61+
"cause": "Connection error.",
62+
}
63+
},
64+
}
65+
66+
67+
@router.get("/conversations/{conversation_id}", responses=conversation_responses)
68+
def get_conversation_endpoint_handler(
69+
conversation_id: str,
70+
_auth: Any = Depends(auth_dependency),
71+
) -> ConversationResponse:
72+
"""Handle request to retrieve a conversation by ID."""
73+
check_configuration_loaded(configuration)
74+
75+
# Validate conversation ID format
76+
if not check_suid(conversation_id):
77+
logger.error("Invalid conversation ID format: %s", conversation_id)
78+
raise HTTPException(
79+
status_code=status.HTTP_400_BAD_REQUEST,
80+
detail={
81+
"response": "Invalid conversation ID format",
82+
"cause": f"Conversation ID {conversation_id} is not a valid UUID",
83+
},
84+
)
85+
86+
agent_id = conversation_id_to_agent_id.get(conversation_id)
87+
if not agent_id:
88+
logger.error("Agent ID not found for conversation %s", conversation_id)
89+
raise HTTPException(
90+
status_code=status.HTTP_404_NOT_FOUND,
91+
detail={
92+
"response": "conversation ID not found",
93+
"cause": f"conversation ID {conversation_id} not found!",
94+
},
95+
)
96+
97+
logger.info("Retrieving conversation %s", conversation_id)
98+
99+
try:
100+
client = LlamaStackClientHolder().get_client()
101+
102+
session_data = client.agents.session.retrieve(
103+
agent_id=agent_id, session_id=conversation_id
104+
)
105+
106+
logger.info("Successfully retrieved conversation %s", conversation_id)
107+
108+
return ConversationResponse(
109+
conversation_id=conversation_id,
110+
session_data=(
111+
session_data.model_dump()
112+
if hasattr(session_data, "model_dump")
113+
else dict(session_data)
114+
),
115+
)
116+
117+
except APIConnectionError as e:
118+
logger.error("Unable to connect to Llama Stack: %s", e)
119+
raise HTTPException(
120+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
121+
detail={
122+
"response": "Unable to connect to Llama Stack",
123+
"cause": str(e),
124+
},
125+
) from e
126+
except Exception as e:
127+
# Handle case where session doesn't exist or other errors
128+
logger.error("Error retrieving conversation %s: %s", conversation_id, e)
129+
raise HTTPException(
130+
status_code=status.HTTP_404_NOT_FOUND,
131+
detail={
132+
"response": "Conversation not found",
133+
"cause": f"Conversation {conversation_id} could not be retrieved: {str(e)}",
134+
},
135+
) from e
136+
137+
138+
@router.delete(
139+
"/conversations/{conversation_id}", responses=conversation_delete_responses
140+
)
141+
def delete_conversation_endpoint_handler(
142+
conversation_id: str,
143+
_auth: Any = Depends(auth_dependency),
144+
) -> ConversationDeleteResponse:
145+
"""Handle request to delete a conversation by ID."""
146+
check_configuration_loaded(configuration)
147+
148+
# Validate conversation ID format
149+
if not check_suid(conversation_id):
150+
logger.error("Invalid conversation ID format: %s", conversation_id)
151+
raise HTTPException(
152+
status_code=status.HTTP_400_BAD_REQUEST,
153+
detail={
154+
"response": "Invalid conversation ID format",
155+
"cause": f"Conversation ID {conversation_id} is not a valid UUID",
156+
},
157+
)
158+
agent_id = conversation_id_to_agent_id.get(conversation_id)
159+
if not agent_id:
160+
logger.error("Agent ID not found for conversation %s", conversation_id)
161+
raise HTTPException(
162+
status_code=status.HTTP_404_NOT_FOUND,
163+
detail={
164+
"response": "conversation ID not found",
165+
"cause": f"conversation ID {conversation_id} not found!",
166+
},
167+
)
168+
logger.info("Deleting conversation %s", conversation_id)
169+
170+
try:
171+
# Get Llama Stack client
172+
client = LlamaStackClientHolder().get_client()
173+
# Delete session using the conversation_id as session_id
174+
# In this implementation, conversation_id and session_id are the same
175+
client.agents.session.delete(agent_id=agent_id, session_id=conversation_id)
176+
177+
logger.info("Successfully deleted conversation %s", conversation_id)
178+
179+
return ConversationDeleteResponse(
180+
conversation_id=conversation_id,
181+
success=True,
182+
message="Conversation deleted successfully",
183+
)
184+
185+
except APIConnectionError as e:
186+
logger.error("Unable to connect to Llama Stack: %s", e)
187+
raise HTTPException(
188+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
189+
detail={
190+
"response": "Unable to connect to Llama Stack",
191+
"cause": str(e),
192+
},
193+
) from e
194+
except Exception as e:
195+
# Handle case where session doesn't exist or other errors
196+
logger.error("Error deleting conversation %s: %s", conversation_id, e)
197+
raise HTTPException(
198+
status_code=status.HTTP_404_NOT_FOUND,
199+
detail={
200+
"response": "Conversation not found",
201+
"cause": f"Conversation {conversation_id} could not be deleted: {str(e)}",
202+
},
203+
) from e

src/app/endpoints/query.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
from client import LlamaStackClientHolder
2525
from configuration import configuration
26+
from app.endpoints.conversations import conversation_id_to_agent_id
2627
from models.responses import QueryResponse, UnauthorizedResponse, ForbiddenResponse
2728
from models.requests import QueryRequest, Attachment
2829
import constants
@@ -97,6 +98,8 @@ def get_agent(
9798
)
9899
conversation_id = agent.create_session(get_suid())
99100
_agent_cache[conversation_id] = agent
101+
conversation_id_to_agent_id[conversation_id] = agent.agent_id
102+
100103
return agent, conversation_id
101104

102105

src/app/endpoints/streaming_query.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from utils.suid import get_suid
2727
from utils.types import GraniteToolParser
2828

29+
from app.endpoints.conversations import conversation_id_to_agent_id
2930
from app.endpoints.query import (
3031
get_rag_toolgroups,
3132
is_transcripts_enabled,
@@ -67,6 +68,7 @@ async def get_agent(
6768
)
6869
conversation_id = await agent.create_session(get_suid())
6970
_agent_cache[conversation_id] = agent
71+
conversation_id_to_agent_id[conversation_id] = agent.agent_id
7072
return agent, conversation_id
7173

7274

src/app/routers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
feedback,
1313
streaming_query,
1414
authorized,
15+
conversations,
1516
)
1617

1718

@@ -28,6 +29,7 @@ def include_routers(app: FastAPI) -> None:
2829
app.include_router(streaming_query.router, prefix="/v1")
2930
app.include_router(config.router, prefix="/v1")
3031
app.include_router(feedback.router, prefix="/v1")
32+
app.include_router(conversations.router, prefix="/v1")
3133

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

src/models/responses.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,3 +298,75 @@ class ForbiddenResponse(UnauthorizedResponse):
298298
]
299299
}
300300
}
301+
302+
303+
class ConversationResponse(BaseModel):
304+
"""Model representing a response for retrieving a conversation.
305+
306+
Attributes:
307+
conversation_id: The conversation ID (UUID).
308+
session_data: The session data retrieved from llama-stack.
309+
310+
Example:
311+
```python
312+
conversation_response = ConversationResponse(
313+
conversation_id="123e4567-e89b-12d3-a456-426614174000",
314+
session_data={"turns": [...], "created_at": "..."}
315+
)
316+
```
317+
"""
318+
319+
conversation_id: str
320+
session_data: dict[str, Any]
321+
322+
# provides examples for /docs endpoint
323+
model_config = {
324+
"json_schema_extra": {
325+
"examples": [
326+
{
327+
"conversation_id": "123e4567-e89b-12d3-a456-426614174000",
328+
"session_data": {
329+
"session_id": "123e4567-e89b-12d3-a456-426614174000",
330+
"turns": [],
331+
"started_at": "2024-01-01T00:00:00Z",
332+
},
333+
}
334+
]
335+
}
336+
}
337+
338+
339+
class ConversationDeleteResponse(BaseModel):
340+
"""Model representing a response for deleting a conversation.
341+
342+
Attributes:
343+
conversation_id: The conversation ID (UUID) that was deleted.
344+
success: Whether the deletion was successful.
345+
message: A message about the deletion result.
346+
347+
Example:
348+
```python
349+
delete_response = ConversationDeleteResponse(
350+
conversation_id="123e4567-e89b-12d3-a456-426614174000",
351+
success=True,
352+
message="Conversation deleted successfully"
353+
)
354+
```
355+
"""
356+
357+
conversation_id: str
358+
success: bool
359+
message: str
360+
361+
# provides examples for /docs endpoint
362+
model_config = {
363+
"json_schema_extra": {
364+
"examples": [
365+
{
366+
"conversation_id": "123e4567-e89b-12d3-a456-426614174000",
367+
"success": True,
368+
"message": "Conversation deleted successfully",
369+
}
370+
]
371+
}
372+
}

tests/unit/app/test_routers.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from app.routers import include_routers # noqa:E402
66

77
from app.endpoints import (
8+
conversations,
89
root,
910
info,
1011
models,
@@ -43,7 +44,7 @@ def test_include_routers() -> None:
4344
include_routers(app)
4445

4546
# are all routers added?
46-
assert len(app.routers) == 9
47+
assert len(app.routers) == 10
4748
assert root.router in app.get_routers()
4849
assert info.router in app.get_routers()
4950
assert models.router in app.get_routers()
@@ -53,6 +54,7 @@ def test_include_routers() -> None:
5354
assert feedback.router in app.get_routers()
5455
assert health.router in app.get_routers()
5556
assert authorized.router in app.get_routers()
57+
assert conversations.router in app.get_routers()
5658

5759

5860
def test_check_prefixes() -> None:
@@ -61,7 +63,7 @@ def test_check_prefixes() -> None:
6163
include_routers(app)
6264

6365
# are all routers added?
64-
assert len(app.routers) == 9
66+
assert len(app.routers) == 10
6567
assert app.get_router_prefix(root.router) is None
6668
assert app.get_router_prefix(info.router) == "/v1"
6769
assert app.get_router_prefix(models.router) == "/v1"
@@ -71,3 +73,4 @@ def test_check_prefixes() -> None:
7173
assert app.get_router_prefix(feedback.router) == "/v1"
7274
assert app.get_router_prefix(health.router) is None
7375
assert app.get_router_prefix(authorized.router) is None
76+
assert app.get_router_prefix(conversations.router) == "/v1"

0 commit comments

Comments
 (0)