@@ -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