Skip to content

Commit 516c17a

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 516c17a

File tree

6 files changed

+361
-2
lines changed

6 files changed

+361
-2
lines changed

src/app/endpoints/conversations.py

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
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, NotFoundError
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 NotFoundError as e:
169+
logger.error("Conversation not found: %s", e)
170+
raise HTTPException(
171+
status_code=status.HTTP_404_NOT_FOUND,
172+
detail={
173+
"response": "Conversation not found",
174+
"cause": f"Conversation {conversation_id} could not be retrieved: {str(e)}",
175+
},
176+
) from e
177+
except Exception as e:
178+
# Handle case where session doesn't exist or other errors
179+
logger.error("Error retrieving conversation %s: %s", conversation_id, e)
180+
raise HTTPException(
181+
status_code=status.HTTP_404_NOT_FOUND,
182+
detail={
183+
"response": "Conversation not found",
184+
"cause": f"Conversation {conversation_id} could not be retrieved: {str(e)}",
185+
},
186+
) from e
187+
188+
189+
@router.delete(
190+
"/conversations/{conversation_id}", responses=conversation_delete_responses
191+
)
192+
def delete_conversation_endpoint_handler(
193+
conversation_id: str,
194+
_auth: Any = Depends(auth_dependency),
195+
) -> ConversationDeleteResponse:
196+
"""Handle request to delete a conversation by ID."""
197+
check_configuration_loaded(configuration)
198+
199+
# Validate conversation ID format
200+
if not check_suid(conversation_id):
201+
logger.error("Invalid conversation ID format: %s", conversation_id)
202+
raise HTTPException(
203+
status_code=status.HTTP_400_BAD_REQUEST,
204+
detail={
205+
"response": "Invalid conversation ID format",
206+
"cause": f"Conversation ID {conversation_id} is not a valid UUID",
207+
},
208+
)
209+
agent_id = conversation_id_to_agent_id.get(conversation_id)
210+
if not agent_id:
211+
logger.error("Agent ID not found for conversation %s", conversation_id)
212+
raise HTTPException(
213+
status_code=status.HTTP_404_NOT_FOUND,
214+
detail={
215+
"response": "conversation ID not found",
216+
"cause": f"conversation ID {conversation_id} not found!",
217+
},
218+
)
219+
logger.info("Deleting conversation %s", conversation_id)
220+
221+
try:
222+
# Get Llama Stack client
223+
client = LlamaStackClientHolder().get_client()
224+
# Delete session using the conversation_id as session_id
225+
# In this implementation, conversation_id and session_id are the same
226+
client.agents.session.delete(agent_id=agent_id, session_id=conversation_id)
227+
228+
logger.info("Successfully deleted conversation %s", conversation_id)
229+
230+
return ConversationDeleteResponse(
231+
conversation_id=conversation_id,
232+
success=True,
233+
response="Conversation deleted successfully",
234+
)
235+
236+
except APIConnectionError as e:
237+
logger.error("Unable to connect to Llama Stack: %s", e)
238+
raise HTTPException(
239+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
240+
detail={
241+
"response": "Unable to connect to Llama Stack",
242+
"cause": str(e),
243+
},
244+
) from e
245+
except NotFoundError as e:
246+
logger.error("Conversation not found: %s", e)
247+
raise HTTPException(
248+
status_code=status.HTTP_404_NOT_FOUND,
249+
detail={
250+
"response": "Conversation not found",
251+
"cause": f"Conversation {conversation_id} could not be deleted: {str(e)}",
252+
},
253+
) from e
254+
except Exception as e:
255+
# Handle case where session doesn't exist or other errors
256+
logger.error("Error deleting conversation %s: %s", conversation_id, e)
257+
raise HTTPException(
258+
status_code=status.HTTP_404_NOT_FOUND,
259+
detail={
260+
"response": "Conversation not found",
261+
"cause": f"Conversation {conversation_id} could not be deleted: {str(e)}",
262+
},
263+
) 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)

0 commit comments

Comments
 (0)