Skip to content

Commit 800bc9b

Browse files
committed
Fix Chat History persistence across all conversation flows
- Add message saving to /api/brief/parse endpoint (user message + parsing response) - Add confirmation message saving to /api/brief/confirm endpoint - Add message saving to /api/generate endpoint (request + generated content) - Update frontend API to pass conversationId and userId to parseBrief - Update streamGenerateContent to accept and send userId - Fix assistant response saving condition (requires_user_input OR is_final) - Add delete_conversation method to cosmos_service - Update ChatHistory component with currentMessages prop and refresh trigger
1 parent cb0438b commit 800bc9b

5 files changed

Lines changed: 288 additions & 52 deletions

File tree

content-gen/src/app.py

Lines changed: 126 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -95,22 +95,23 @@ async def generate():
9595
):
9696
yield f"data: {json.dumps(response)}\n\n"
9797

98-
# Try to save assistant responses
99-
if response.get("is_final"):
100-
try:
101-
cosmos_service = await get_cosmos_service()
102-
await cosmos_service.add_message_to_conversation(
103-
conversation_id=conversation_id,
104-
user_id=user_id,
105-
message={
106-
"role": "assistant",
107-
"content": response.get("content", ""),
108-
"agent": response.get("agent", ""),
109-
"timestamp": datetime.now(timezone.utc).isoformat()
110-
}
111-
)
112-
except Exception as e:
113-
logger.warning(f"Failed to save response to CosmosDB: {e}")
98+
# Save assistant responses when final OR when requiring user input
99+
if response.get("is_final") or response.get("requires_user_input"):
100+
if response.get("content"):
101+
try:
102+
cosmos_service = await get_cosmos_service()
103+
await cosmos_service.add_message_to_conversation(
104+
conversation_id=conversation_id,
105+
user_id=user_id,
106+
message={
107+
"role": "assistant",
108+
"content": response.get("content", ""),
109+
"agent": response.get("agent", ""),
110+
"timestamp": datetime.now(timezone.utc).isoformat()
111+
}
112+
)
113+
except Exception as e:
114+
logger.warning(f"Failed to save response to CosmosDB: {e}")
114115
except Exception as e:
115116
logger.exception(f"Error in orchestrator: {e}")
116117
yield f"data: {json.dumps({'type': 'error', 'content': str(e), 'is_final': True})}\n\n"
@@ -136,24 +137,60 @@ async def parse_brief():
136137
137138
Request body:
138139
{
139-
"brief_text": "Free-form creative brief text"
140+
"brief_text": "Free-form creative brief text",
141+
"conversation_id": "optional-uuid",
142+
"user_id": "user identifier"
140143
}
141144
142145
Returns:
143146
Structured CreativeBrief JSON for user confirmation.
144147
"""
145148
data = await request.get_json()
146149
brief_text = data.get("brief_text", "")
150+
conversation_id = data.get("conversation_id") or str(uuid.uuid4())
151+
user_id = data.get("user_id", "anonymous")
147152

148153
if not brief_text:
149154
return jsonify({"error": "Brief text is required"}), 400
150155

156+
# Save the user's brief text as a message to CosmosDB
157+
try:
158+
cosmos_service = await get_cosmos_service()
159+
await cosmos_service.add_message_to_conversation(
160+
conversation_id=conversation_id,
161+
user_id=user_id,
162+
message={
163+
"role": "user",
164+
"content": brief_text,
165+
"timestamp": datetime.now(timezone.utc).isoformat()
166+
}
167+
)
168+
except Exception as e:
169+
logger.warning(f"Failed to save brief message to CosmosDB: {e}")
170+
151171
orchestrator = get_orchestrator()
152172
parsed_brief = await orchestrator.parse_brief(brief_text)
153173

174+
# Save the assistant's parsing response
175+
try:
176+
cosmos_service = await get_cosmos_service()
177+
await cosmos_service.add_message_to_conversation(
178+
conversation_id=conversation_id,
179+
user_id=user_id,
180+
message={
181+
"role": "assistant",
182+
"content": "I've parsed your creative brief. Please review and confirm the details before we proceed.",
183+
"agent": "PlanningAgent",
184+
"timestamp": datetime.now(timezone.utc).isoformat()
185+
}
186+
)
187+
except Exception as e:
188+
logger.warning(f"Failed to save parsing response to CosmosDB: {e}")
189+
154190
return jsonify({
155191
"brief": parsed_brief.model_dump(),
156192
"requires_confirmation": True,
193+
"conversation_id": conversation_id,
157194
"message": "Please review and confirm the parsed creative brief"
158195
})
159196

@@ -183,13 +220,26 @@ async def confirm_brief():
183220
except Exception as e:
184221
return jsonify({"error": f"Invalid brief format: {str(e)}"}), 400
185222

186-
# Try to save the confirmed brief to CosmosDB, but don't fail if unavailable
223+
# Try to save the confirmed brief to CosmosDB, preserving existing messages
187224
try:
188225
cosmos_service = await get_cosmos_service()
226+
227+
# Get existing conversation to preserve messages
228+
existing = await cosmos_service.get_conversation(conversation_id, user_id)
229+
existing_messages = existing.get("messages", []) if existing else []
230+
231+
# Add confirmation message
232+
existing_messages.append({
233+
"role": "assistant",
234+
"content": "Great! Your creative brief has been confirmed. Now you can select products to feature and generate content.",
235+
"agent": "TriageAgent",
236+
"timestamp": datetime.now(timezone.utc).isoformat()
237+
})
238+
189239
await cosmos_service.save_conversation(
190240
conversation_id=conversation_id,
191241
user_id=user_id,
192-
messages=[],
242+
messages=existing_messages,
193243
brief=brief,
194244
metadata={"status": "brief_confirmed"}
195245
)
@@ -227,12 +277,29 @@ async def generate_content():
227277
products_data = data.get("products", [])
228278
generate_images = data.get("generate_images", True)
229279
conversation_id = data.get("conversation_id") or str(uuid.uuid4())
280+
user_id = data.get("user_id", "anonymous")
230281

231282
try:
232283
brief = CreativeBrief(**brief_data)
233284
except Exception as e:
234285
return jsonify({"error": f"Invalid brief format: {str(e)}"}), 400
235286

287+
# Save user request for content generation
288+
try:
289+
cosmos_service = await get_cosmos_service()
290+
product_names = [p.get("product_name", "product") for p in products_data[:3]]
291+
await cosmos_service.add_message_to_conversation(
292+
conversation_id=conversation_id,
293+
user_id=user_id,
294+
message={
295+
"role": "user",
296+
"content": f"Generate content for: {', '.join(product_names) if product_names else 'the campaign'}",
297+
"timestamp": datetime.now(timezone.utc).isoformat()
298+
}
299+
)
300+
except Exception as e:
301+
logger.warning(f"Failed to save generation request to CosmosDB: {e}")
302+
236303
orchestrator = get_orchestrator()
237304

238305
async def generate():
@@ -256,6 +323,24 @@ async def generate():
256323
except Exception as e:
257324
logger.warning(f"Failed to save image to blob storage: {e}")
258325

326+
# Save generated content to conversation
327+
try:
328+
cosmos_service = await get_cosmos_service()
329+
text_content = response.get("text_content", {})
330+
headline = text_content.get("headline", "") if isinstance(text_content, dict) else ""
331+
await cosmos_service.add_message_to_conversation(
332+
conversation_id=conversation_id,
333+
user_id=user_id,
334+
message={
335+
"role": "assistant",
336+
"content": f"Content generated successfully! {f'Headline: "{headline}"' if headline else ''}",
337+
"agent": "ContentAgent",
338+
"timestamp": datetime.now(timezone.utc).isoformat()
339+
}
340+
)
341+
except Exception as e:
342+
logger.warning(f"Failed to save generated content to CosmosDB: {e}")
343+
259344
# Format response to match what frontend expects
260345
yield f"data: {json.dumps({'type': 'agent_response', 'content': json.dumps(response), 'is_final': True})}\n\n"
261346
except Exception as e:
@@ -441,6 +526,28 @@ async def get_conversation(conversation_id: str):
441526
return jsonify(conversation)
442527

443528

529+
@app.route("/api/conversations/<conversation_id>", methods=["DELETE"])
530+
async def delete_conversation(conversation_id: str):
531+
"""
532+
Delete a specific conversation.
533+
534+
Query params:
535+
user_id: User identifier (required)
536+
"""
537+
user_id = request.args.get("user_id")
538+
539+
if not user_id:
540+
return jsonify({"error": "user_id is required"}), 400
541+
542+
try:
543+
cosmos_service = await get_cosmos_service()
544+
await cosmos_service.delete_conversation(conversation_id, user_id)
545+
return jsonify({"success": True, "message": "Conversation deleted"})
546+
except Exception as e:
547+
logger.warning(f"Failed to delete conversation: {e}")
548+
return jsonify({"error": "Failed to delete conversation"}), 500
549+
550+
444551
# ==================== Brand Guidelines Endpoints ====================
445552

446553
@app.route("/api/brand-guidelines", methods=["GET"])

content-gen/src/backend/services/cosmos_service.py

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -325,20 +325,20 @@ async def get_user_conversations(
325325
limit: int = 20
326326
) -> List[dict]:
327327
"""
328-
Get all conversations for a user.
328+
Get all conversations for a user with summary data.
329329
330330
Args:
331331
user_id: User ID
332332
limit: Maximum number of conversations
333333
334334
Returns:
335-
List of conversations
335+
List of conversation summaries
336336
"""
337337
await self.initialize()
338338

339+
# Get conversations with messages to extract title and last message
339340
query = """
340-
SELECT TOP @limit c.id, c.user_id, c.updated_at,
341-
ARRAY_LENGTH(c.messages) as message_count
341+
SELECT TOP @limit c.id, c.user_id, c.updated_at, c.messages, c.brief
342342
FROM c
343343
WHERE c.user_id = @user_id
344344
ORDER BY c.updated_at DESC
@@ -353,9 +353,61 @@ async def get_user_conversations(
353353
query=query,
354354
parameters=params
355355
):
356-
conversations.append(item)
356+
messages = item.get("messages", [])
357+
brief = item.get("brief", {})
358+
359+
# Extract title from brief overview or first user message
360+
title = "Untitled Conversation"
361+
if brief and brief.get("overview"):
362+
title = brief["overview"][:50]
363+
elif messages:
364+
for msg in messages:
365+
if msg.get("role") == "user":
366+
title = msg.get("content", "")[:50]
367+
break
368+
369+
# Get last message preview
370+
last_message = ""
371+
if messages:
372+
last_msg = messages[-1]
373+
last_message = last_msg.get("content", "")[:100]
374+
375+
conversations.append({
376+
"id": item["id"],
377+
"title": title,
378+
"lastMessage": last_message,
379+
"timestamp": item.get("updated_at", ""),
380+
"messageCount": len(messages)
381+
})
357382

358383
return conversations
384+
385+
async def delete_conversation(
386+
self,
387+
conversation_id: str,
388+
user_id: str
389+
) -> bool:
390+
"""
391+
Delete a conversation.
392+
393+
Args:
394+
conversation_id: Unique conversation identifier
395+
user_id: User ID for partition key
396+
397+
Returns:
398+
True if deleted successfully
399+
"""
400+
await self.initialize()
401+
402+
try:
403+
await self._conversations_container.delete_item(
404+
item=conversation_id,
405+
partition_key=user_id
406+
)
407+
return True
408+
except Exception as e:
409+
logger.warning(f"Failed to delete conversation {conversation_id}: {e}")
410+
raise
359411

360412

361413
# Singleton instance

content-gen/src/frontend/src/App.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,26 @@ function App() {
3333
// Generated content
3434
const [generatedContent, setGeneratedContent] = useState<GeneratedContent | null>(null);
3535

36+
// Trigger for refreshing chat history
37+
const [historyRefreshTrigger, setHistoryRefreshTrigger] = useState(0);
38+
3639
// Handle selecting a conversation from history
3740
const handleSelectConversation = useCallback(async (selectedConversationId: string) => {
3841
setIsLoading(true);
3942
try {
40-
const response = await fetch(`/api/conversations/${selectedConversationId}`);
43+
const response = await fetch(`/api/conversations/${selectedConversationId}?user_id=${encodeURIComponent(userId)}`);
4144
if (response.ok) {
4245
const data = await response.json();
4346
setConversationId(selectedConversationId);
44-
setMessages(data.messages || []);
47+
// Map messages to ChatMessage format
48+
const loadedMessages: ChatMessage[] = (data.messages || []).map((msg: { role: string; content: string; timestamp?: string; agent?: string }, index: number) => ({
49+
id: `${selectedConversationId}-${index}`,
50+
role: msg.role as 'user' | 'assistant',
51+
content: msg.content,
52+
timestamp: msg.timestamp || new Date().toISOString(),
53+
agent: msg.agent,
54+
}));
55+
setMessages(loadedMessages);
4556
setPendingBrief(null);
4657
setConfirmedBrief(data.brief || null);
4758
setGeneratedContent(null);
@@ -52,7 +63,7 @@ function App() {
5263
} finally {
5364
setIsLoading(false);
5465
}
55-
}, []);
66+
}, [userId]);
5667

5768
// Handle starting a new conversation
5869
const handleNewConversation = useCallback(() => {
@@ -85,7 +96,7 @@ function App() {
8596

8697
if (isBriefLike && !confirmedBrief) {
8798
// Parse as a creative brief
88-
const parsed = await parseBrief(content);
99+
const parsed = await parseBrief(content, conversationId, userId);
89100
setPendingBrief(parsed.brief);
90101

91102
const assistantMessage: ChatMessage = {
@@ -143,6 +154,8 @@ function App() {
143154
setMessages(prev => [...prev, errorMessage]);
144155
} finally {
145156
setIsLoading(false);
157+
// Trigger refresh of chat history after message is sent
158+
setHistoryRefreshTrigger(prev => prev + 1);
146159
}
147160
}, [conversationId, userId, confirmedBrief]);
148161

@@ -188,7 +201,8 @@ function App() {
188201
confirmedBrief,
189202
selectedProducts,
190203
true,
191-
conversationId
204+
conversationId,
205+
userId
192206
)) {
193207
if (response.is_final && response.type !== 'error') {
194208
try {
@@ -292,8 +306,11 @@ function App() {
292306
<div className="history-panel">
293307
<ChatHistory
294308
currentConversationId={conversationId}
309+
userId={userId}
310+
currentMessages={messages}
295311
onSelectConversation={handleSelectConversation}
296312
onNewConversation={handleNewConversation}
313+
refreshTrigger={historyRefreshTrigger}
297314
/>
298315
</div>
299316

0 commit comments

Comments
 (0)