Skip to content

Commit f327596

Browse files
committed
Merge fix-system-prompt-exposure: system prompt fix and image regeneration
2 parents 7a61b45 + 374ed0a commit f327596

4 files changed

Lines changed: 602 additions & 4 deletions

File tree

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

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,115 @@ function App() {
292292
timestamp: new Date().toISOString(),
293293
};
294294
setMessages(prev => [...prev, assistantMessage]);
295+
} else if (generatedContent && confirmedBrief) {
296+
// Content has been generated - check if user wants to modify the image
297+
const imageModificationKeywords = [
298+
'change', 'modify', 'update', 'replace', 'show', 'display', 'use',
299+
'instead', 'different', 'another', 'make it', 'make the',
300+
'kitchen', 'dining', 'living', 'bedroom', 'bathroom', 'outdoor', 'office',
301+
'room', 'scene', 'setting', 'background', 'style', 'color', 'lighting'
302+
];
303+
const isImageModification = imageModificationKeywords.some(kw => content.toLowerCase().includes(kw));
304+
305+
if (isImageModification) {
306+
// User wants to modify the image - use regeneration endpoint
307+
const { streamRegenerateImage } = await import('./api');
308+
309+
setGenerationStatus('Regenerating image with your changes...');
310+
311+
let responseData: GeneratedContent | null = null;
312+
let messageContent = '';
313+
314+
// Get previous prompt from image_content if available
315+
const previousPrompt = generatedContent.image_content?.prompt_used;
316+
317+
for await (const response of streamRegenerateImage(
318+
content,
319+
confirmedBrief,
320+
selectedProducts,
321+
previousPrompt,
322+
conversationId,
323+
userId,
324+
signal
325+
)) {
326+
if (response.type === 'heartbeat') {
327+
setGenerationStatus(response.message || 'Regenerating image...');
328+
} else if (response.type === 'agent_response' && response.is_final) {
329+
try {
330+
const parsedContent = JSON.parse(response.content);
331+
332+
// Update generatedContent with new image
333+
if (parsedContent.image_url || parsedContent.image_base64) {
334+
responseData = {
335+
...generatedContent,
336+
image_content: {
337+
...generatedContent.image_content,
338+
image_url: parsedContent.image_url || generatedContent.image_content?.image_url,
339+
image_base64: parsedContent.image_base64,
340+
prompt_used: parsedContent.image_prompt || generatedContent.image_content?.prompt_used,
341+
},
342+
};
343+
setGeneratedContent(responseData);
344+
messageContent = parsedContent.message || 'Image regenerated with your requested changes.';
345+
} else if (parsedContent.error) {
346+
messageContent = parsedContent.error;
347+
} else {
348+
messageContent = parsedContent.message || 'I processed your request.';
349+
}
350+
} catch {
351+
messageContent = response.content || 'Image regenerated.';
352+
}
353+
} else if (response.type === 'error') {
354+
messageContent = response.content || 'An error occurred while regenerating the image.';
355+
}
356+
}
357+
358+
setGenerationStatus('');
359+
360+
const assistantMessage: ChatMessage = {
361+
id: uuidv4(),
362+
role: 'assistant',
363+
content: messageContent,
364+
agent: 'ImageAgent',
365+
timestamp: new Date().toISOString(),
366+
};
367+
setMessages(prev => [...prev, assistantMessage]);
368+
} else {
369+
// General question after content generation - use regular chat
370+
let fullContent = '';
371+
let currentAgent = '';
372+
let messageAdded = false;
373+
374+
setGenerationStatus('Processing your request...');
375+
for await (const response of streamChat(content, conversationId, userId, signal)) {
376+
if (response.type === 'agent_response') {
377+
fullContent = response.content;
378+
currentAgent = response.agent || '';
379+
380+
if ((response.is_final || response.requires_user_input) && !messageAdded) {
381+
const assistantMessage: ChatMessage = {
382+
id: uuidv4(),
383+
role: 'assistant',
384+
content: fullContent,
385+
agent: currentAgent,
386+
timestamp: new Date().toISOString(),
387+
};
388+
setMessages(prev => [...prev, assistantMessage]);
389+
messageAdded = true;
390+
}
391+
} else if (response.type === 'error') {
392+
const errorMessage: ChatMessage = {
393+
id: uuidv4(),
394+
role: 'assistant',
395+
content: response.content || 'An error occurred.',
396+
timestamp: new Date().toISOString(),
397+
};
398+
setMessages(prev => [...prev, errorMessage]);
399+
messageAdded = true;
400+
}
401+
}
402+
setGenerationStatus('');
403+
}
295404
} else {
296405
// Check if this looks like a creative brief
297406
const briefKeywords = ['campaign', 'marketing', 'target audience', 'objective', 'deliverable'];

content-gen/src/app/frontend/src/api/index.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,3 +287,65 @@ export async function* streamGenerateContent(
287287

288288
throw new Error('Generation timed out after 10 minutes');
289289
}
290+
/**
291+
* Regenerate image with a modification request
292+
* Used when user wants to change the generated image after initial content generation
293+
*/
294+
export async function* streamRegenerateImage(
295+
modificationRequest: string,
296+
brief: CreativeBrief,
297+
products?: Product[],
298+
previousImagePrompt?: string,
299+
conversationId?: string,
300+
userId?: string,
301+
signal?: AbortSignal
302+
): AsyncGenerator<AgentResponse> {
303+
const response = await fetch(`${API_BASE}/regenerate`, {
304+
signal,
305+
method: 'POST',
306+
headers: { 'Content-Type': 'application/json' },
307+
body: JSON.stringify({
308+
modification_request: modificationRequest,
309+
brief,
310+
products: products || [],
311+
previous_image_prompt: previousImagePrompt,
312+
conversation_id: conversationId,
313+
user_id: userId || 'anonymous',
314+
}),
315+
});
316+
317+
if (!response.ok) {
318+
throw new Error(`Regeneration request failed: ${response.statusText}`);
319+
}
320+
321+
const reader = response.body?.getReader();
322+
if (!reader) {
323+
throw new Error('No response body');
324+
}
325+
326+
const decoder = new TextDecoder();
327+
let buffer = '';
328+
329+
while (true) {
330+
const { done, value } = await reader.read();
331+
if (done) break;
332+
333+
buffer += decoder.decode(value, { stream: true });
334+
const lines = buffer.split('\n\n');
335+
buffer = lines.pop() || '';
336+
337+
for (const line of lines) {
338+
if (line.startsWith('data: ')) {
339+
const data = line.slice(6);
340+
if (data === '[DONE]') {
341+
return;
342+
}
343+
try {
344+
yield JSON.parse(data) as AgentResponse;
345+
} catch {
346+
console.error('Failed to parse SSE data:', data);
347+
}
348+
}
349+
}
350+
}
351+
}

content-gen/src/backend/app.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -857,6 +857,170 @@ async def generate():
857857
)
858858

859859

860+
@app.route("/api/regenerate", methods=["POST"])
861+
async def regenerate_content():
862+
"""
863+
Regenerate image based on user modification request.
864+
865+
This endpoint is called when the user wants to modify the generated image
866+
after initial content generation (e.g., "show a kitchen instead of dining room").
867+
868+
Request body:
869+
{
870+
"modification_request": "User's modification request",
871+
"brief": { ... CreativeBrief fields ... },
872+
"products": [ ... Product list ... ],
873+
"previous_image_prompt": "Previous image prompt (optional)",
874+
"conversation_id": "uuid"
875+
}
876+
877+
Returns regenerated image with the modification applied.
878+
"""
879+
import asyncio
880+
881+
data = await request.get_json()
882+
883+
modification_request = data.get("modification_request", "")
884+
brief_data = data.get("brief", {})
885+
products_data = data.get("products", [])
886+
previous_image_prompt = data.get("previous_image_prompt")
887+
conversation_id = data.get("conversation_id") or str(uuid.uuid4())
888+
user_id = data.get("user_id", "anonymous")
889+
890+
if not modification_request:
891+
return jsonify({"error": "modification_request is required"}), 400
892+
893+
try:
894+
brief = CreativeBrief(**brief_data)
895+
except Exception as e:
896+
return jsonify({"error": f"Invalid brief format: {str(e)}"}), 400
897+
898+
# Save user request for regeneration
899+
try:
900+
cosmos_service = await get_cosmos_service()
901+
await cosmos_service.add_message_to_conversation(
902+
conversation_id=conversation_id,
903+
user_id=user_id,
904+
message={
905+
"role": "user",
906+
"content": f"Modify image: {modification_request}",
907+
"timestamp": datetime.now(timezone.utc).isoformat()
908+
}
909+
)
910+
except Exception as e:
911+
logger.warning(f"Failed to save regeneration request to CosmosDB: {e}")
912+
913+
orchestrator = get_orchestrator()
914+
915+
async def generate():
916+
"""Stream regeneration responses with keepalive heartbeats."""
917+
logger.info(f"Starting image regeneration for conversation {conversation_id}")
918+
regeneration_task = None
919+
920+
try:
921+
# Create a task for the regeneration
922+
regeneration_task = asyncio.create_task(
923+
orchestrator.regenerate_image(
924+
modification_request=modification_request,
925+
brief=brief,
926+
products=products_data,
927+
previous_image_prompt=previous_image_prompt
928+
)
929+
)
930+
931+
# Send keepalive heartbeats while regeneration is running
932+
heartbeat_count = 0
933+
while not regeneration_task.done():
934+
for _ in range(30): # 15 seconds
935+
if regeneration_task.done():
936+
break
937+
await asyncio.sleep(0.5)
938+
939+
if not regeneration_task.done():
940+
heartbeat_count += 1
941+
yield f"data: {json.dumps({'type': 'heartbeat', 'count': heartbeat_count, 'message': 'Regenerating image...'})}\n\n"
942+
943+
except asyncio.CancelledError:
944+
logger.warning(f"Regeneration cancelled for conversation {conversation_id}")
945+
if regeneration_task and not regeneration_task.done():
946+
regeneration_task.cancel()
947+
raise
948+
except GeneratorExit:
949+
logger.warning(f"Regeneration closed by client for conversation {conversation_id}")
950+
if regeneration_task and not regeneration_task.done():
951+
regeneration_task.cancel()
952+
return
953+
954+
# Get the result
955+
try:
956+
response = regeneration_task.result()
957+
logger.info(f"Regeneration complete. Response keys: {list(response.keys()) if response else 'None'}")
958+
959+
# Check for RAI block
960+
if response.get("rai_blocked"):
961+
yield f"data: {json.dumps({'type': 'error', 'content': response.get('error', 'Request blocked by content safety'), 'rai_blocked': True, 'is_final': True})}\n\n"
962+
yield "data: [DONE]\n\n"
963+
return
964+
965+
# Handle image URL from orchestrator's blob save
966+
if response.get("image_blob_url"):
967+
blob_url = response["image_blob_url"]
968+
parts = blob_url.split("/")
969+
filename = parts[-1]
970+
conv_folder = parts[-2]
971+
response["image_url"] = f"/api/images/{conv_folder}/{filename}"
972+
del response["image_blob_url"]
973+
elif response.get("image_base64"):
974+
# Save to blob storage
975+
try:
976+
blob_service = await get_blob_service()
977+
blob_url = await blob_service.save_generated_image(
978+
conversation_id=conversation_id,
979+
image_base64=response["image_base64"]
980+
)
981+
if blob_url:
982+
parts = blob_url.split("/")
983+
filename = parts[-1]
984+
response["image_url"] = f"/api/images/{conversation_id}/{filename}"
985+
del response["image_base64"]
986+
except Exception as e:
987+
logger.warning(f"Failed to save regenerated image to blob: {e}")
988+
989+
# Save assistant response
990+
try:
991+
cosmos_service = await get_cosmos_service()
992+
await cosmos_service.add_message_to_conversation(
993+
conversation_id=conversation_id,
994+
user_id=user_id,
995+
message={
996+
"role": "assistant",
997+
"content": response.get("message", "Image regenerated based on your request."),
998+
"agent": "ImageAgent",
999+
"timestamp": datetime.now(timezone.utc).isoformat()
1000+
}
1001+
)
1002+
except Exception as e:
1003+
logger.warning(f"Failed to save regeneration response to CosmosDB: {e}")
1004+
1005+
yield f"data: {json.dumps({'type': 'agent_response', 'content': json.dumps(response), 'is_final': True})}\n\n"
1006+
except Exception as e:
1007+
logger.exception(f"Error in regeneration: {e}")
1008+
yield f"data: {json.dumps({'type': 'error', 'content': str(e), 'is_final': True})}\n\n"
1009+
1010+
yield "data: [DONE]\n\n"
1011+
1012+
return Response(
1013+
generate(),
1014+
mimetype="text/event-stream",
1015+
headers={
1016+
"Cache-Control": "no-cache, no-store, must-revalidate",
1017+
"X-Accel-Buffering": "no",
1018+
"Connection": "keep-alive",
1019+
"Content-Type": "text/event-stream; charset=utf-8",
1020+
}
1021+
)
1022+
1023+
8601024
# ==================== Image Proxy Endpoints ====================
8611025

8621026
@app.route("/api/images/<conversation_id>/<filename>", methods=["GET"])

0 commit comments

Comments
 (0)