Skip to content

Commit 5cbc4e7

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 5cbc4e7

File tree

6 files changed

+343
-2
lines changed

6 files changed

+343
-2
lines changed

src/app/endpoints/conversations.py

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
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+
def simplify_session_data(session_data: Any) -> list[dict[str, Any]]:
68+
"""Simplify session data to include only essential conversation information.
69+
70+
Args:
71+
session_data: The full session data from llama-stack
72+
73+
Returns:
74+
Simplified session data with only input_messages and output_message per turn
75+
"""
76+
session_dict = session_data.model_dump()
77+
# Create simplified structure
78+
chat_history = []
79+
80+
# Extract only essential data from each turn
81+
for turn in session_dict.get("turns", []):
82+
# Clean up input messages
83+
cleaned_messages = []
84+
for msg in turn.get("input_messages", []):
85+
cleaned_msg = {
86+
"content": msg.get("content"),
87+
"type": msg.get("role"), # Rename role to type
88+
}
89+
cleaned_messages.append(cleaned_msg)
90+
91+
# Clean up output message
92+
output_msg = turn.get("output_message", {})
93+
cleaned_messages.append(
94+
{
95+
"content": output_msg.get("content"),
96+
"type": output_msg.get("role"), # Rename role to type
97+
}
98+
)
99+
100+
simplified_turn = {
101+
"messages": cleaned_messages,
102+
"started_at": turn.get("started_at"),
103+
"completed_at": turn.get("completed_at"),
104+
}
105+
chat_history.append(simplified_turn)
106+
107+
return chat_history
108+
109+
110+
@router.get("/conversations/{conversation_id}", responses=conversation_responses)
111+
def get_conversation_endpoint_handler(
112+
conversation_id: str,
113+
_auth: Any = Depends(auth_dependency),
114+
) -> ConversationResponse:
115+
"""Handle request to retrieve a conversation by ID."""
116+
check_configuration_loaded(configuration)
117+
118+
# Validate conversation ID format
119+
if not check_suid(conversation_id):
120+
logger.error("Invalid conversation ID format: %s", conversation_id)
121+
raise HTTPException(
122+
status_code=status.HTTP_400_BAD_REQUEST,
123+
detail={
124+
"response": "Invalid conversation ID format",
125+
"cause": f"Conversation ID {conversation_id} is not a valid UUID",
126+
},
127+
)
128+
129+
agent_id = conversation_id_to_agent_id.get(conversation_id)
130+
if not agent_id:
131+
logger.error("Agent ID not found for conversation %s", conversation_id)
132+
raise HTTPException(
133+
status_code=status.HTTP_404_NOT_FOUND,
134+
detail={
135+
"response": "conversation ID not found",
136+
"cause": f"conversation ID {conversation_id} not found!",
137+
},
138+
)
139+
140+
logger.info("Retrieving conversation %s", conversation_id)
141+
142+
try:
143+
client = LlamaStackClientHolder().get_client()
144+
145+
session_data = client.agents.session.retrieve(
146+
agent_id=agent_id, session_id=conversation_id
147+
)
148+
149+
logger.info("Successfully retrieved conversation %s", conversation_id)
150+
151+
# Simplify the session data to include only essential conversation information
152+
chat_history = simplify_session_data(session_data)
153+
154+
return ConversationResponse(
155+
conversation_id=conversation_id,
156+
chat_history=chat_history,
157+
)
158+
159+
except APIConnectionError as e:
160+
logger.error("Unable to connect to Llama Stack: %s", e)
161+
raise HTTPException(
162+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
163+
detail={
164+
"response": "Unable to connect to Llama Stack",
165+
"cause": str(e),
166+
},
167+
) from e
168+
except Exception as e:
169+
# Handle case where session doesn't exist or other errors
170+
logger.error("Error retrieving conversation %s: %s", conversation_id, e)
171+
raise HTTPException(
172+
status_code=status.HTTP_404_NOT_FOUND,
173+
detail={
174+
"response": "Conversation not found",
175+
"cause": f"Conversation {conversation_id} could not be retrieved: {str(e)}",
176+
},
177+
) from e
178+
179+
180+
@router.delete(
181+
"/conversations/{conversation_id}", responses=conversation_delete_responses
182+
)
183+
def delete_conversation_endpoint_handler(
184+
conversation_id: str,
185+
_auth: Any = Depends(auth_dependency),
186+
) -> ConversationDeleteResponse:
187+
"""Handle request to delete a conversation by ID."""
188+
check_configuration_loaded(configuration)
189+
190+
# Validate conversation ID format
191+
if not check_suid(conversation_id):
192+
logger.error("Invalid conversation ID format: %s", conversation_id)
193+
raise HTTPException(
194+
status_code=status.HTTP_400_BAD_REQUEST,
195+
detail={
196+
"response": "Invalid conversation ID format",
197+
"cause": f"Conversation ID {conversation_id} is not a valid UUID",
198+
},
199+
)
200+
agent_id = conversation_id_to_agent_id.get(conversation_id)
201+
if not agent_id:
202+
logger.error("Agent ID not found for conversation %s", conversation_id)
203+
raise HTTPException(
204+
status_code=status.HTTP_404_NOT_FOUND,
205+
detail={
206+
"response": "conversation ID not found",
207+
"cause": f"conversation ID {conversation_id} not found!",
208+
},
209+
)
210+
logger.info("Deleting conversation %s", conversation_id)
211+
212+
try:
213+
# Get Llama Stack client
214+
client = LlamaStackClientHolder().get_client()
215+
# Delete session using the conversation_id as session_id
216+
# In this implementation, conversation_id and session_id are the same
217+
client.agents.session.delete(agent_id=agent_id, session_id=conversation_id)
218+
219+
logger.info("Successfully deleted conversation %s", conversation_id)
220+
221+
return ConversationDeleteResponse(
222+
conversation_id=conversation_id,
223+
success=True,
224+
response="Conversation deleted successfully",
225+
)
226+
227+
except APIConnectionError as e:
228+
logger.error("Unable to connect to Llama Stack: %s", e)
229+
raise HTTPException(
230+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
231+
detail={
232+
"response": "Unable to connect to Llama Stack",
233+
"cause": str(e),
234+
},
235+
) from e
236+
except Exception as e:
237+
# Handle case where session doesn't exist or other errors
238+
logger.error("Error deleting conversation %s: %s", conversation_id, e)
239+
raise HTTPException(
240+
status_code=status.HTTP_404_NOT_FOUND,
241+
detail={
242+
"response": "Conversation not found",
243+
"cause": f"Conversation {conversation_id} could not be deleted: {str(e)}",
244+
},
245+
) 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: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,3 +298,89 @@ 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+
chat_history: The simplified chat history as a list of conversation turns.
309+
310+
Example:
311+
```python
312+
conversation_response = ConversationResponse(
313+
conversation_id="123e4567-e89b-12d3-a456-426614174000",
314+
chat_history=[
315+
{
316+
"messages": [
317+
{"content": "Hello", "type": "user"},
318+
{"content": "Hi there!", "type": "assistant"}
319+
],
320+
"started_at": "2024-01-01T00:01:00Z",
321+
"completed_at": "2024-01-01T00:01:05Z"
322+
}
323+
]
324+
)
325+
```
326+
"""
327+
328+
conversation_id: str
329+
chat_history: list[dict[str, Any]]
330+
331+
# provides examples for /docs endpoint
332+
model_config = {
333+
"json_schema_extra": {
334+
"examples": [
335+
{
336+
"conversation_id": "123e4567-e89b-12d3-a456-426614174000",
337+
"chat_history": [
338+
{
339+
"messages": [
340+
{"content": "Hello", "type": "user"},
341+
{"content": "Hi there!", "type": "assistant"},
342+
],
343+
"started_at": "2024-01-01T00:01:00Z",
344+
"completed_at": "2024-01-01T00:01:05Z",
345+
}
346+
],
347+
}
348+
]
349+
}
350+
}
351+
352+
353+
class ConversationDeleteResponse(BaseModel):
354+
"""Model representing a response for deleting a conversation.
355+
356+
Attributes:
357+
conversation_id: The conversation ID (UUID) that was deleted.
358+
success: Whether the deletion was successful.
359+
response: A message about the deletion result.
360+
361+
Example:
362+
```python
363+
delete_response = ConversationDeleteResponse(
364+
conversation_id="123e4567-e89b-12d3-a456-426614174000",
365+
success=True,
366+
response="Conversation deleted successfully"
367+
)
368+
```
369+
"""
370+
371+
conversation_id: str
372+
success: bool
373+
response: str
374+
375+
# provides examples for /docs endpoint
376+
model_config = {
377+
"json_schema_extra": {
378+
"examples": [
379+
{
380+
"conversation_id": "123e4567-e89b-12d3-a456-426614174000",
381+
"success": True,
382+
"response": "Conversation deleted successfully",
383+
}
384+
]
385+
}
386+
}

0 commit comments

Comments
 (0)