Fix all async LLM routes: bypass GCP 30s load balancer timeout
Convert 6 synchronous LLM routes to async 202+WebSocket pattern: - generate-response (focus_group_ai): persona chat response - generate-key-themes (focus_group_ai): discussion analysis - modify-with-ai (personas): AI persona modification - export-profile (personas): markdown profile export - describe-asset (focus_groups): image AI description Each route now returns 202 + task_id immediately, runs LLM in asyncio background task, delivers result via WebSocket task_completed event. Frontend listeners updated to wait for ws:task_completed instead of HTTP response body. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f4a587c4f7
commit
c7034634e3
7 changed files with 592 additions and 406 deletions
|
|
@ -46,7 +46,7 @@ def _user_key():
|
|||
async def generate_ai_response():
|
||||
"""
|
||||
Generate a response from a persona in a focus group discussion.
|
||||
|
||||
|
||||
Request body:
|
||||
{
|
||||
"focus_group_id": "focus_group_id",
|
||||
|
|
@ -55,110 +55,113 @@ async def generate_ai_response():
|
|||
"temperature": 0.7, # Optional
|
||||
"message_limit": 10 # Optional, number of previous messages to include
|
||||
}
|
||||
|
||||
Returns:
|
||||
A JSON object containing the generated response
|
||||
|
||||
Returns immediately with 202 + task_id; result delivered via WebSocket task_completed event.
|
||||
"""
|
||||
try:
|
||||
data = (await request.get_json()) or {}
|
||||
|
||||
|
||||
# Validate required fields
|
||||
required_fields = ['focus_group_id', 'persona_id', 'current_topic']
|
||||
missing_fields = [field for field in required_fields if field not in data]
|
||||
if missing_fields:
|
||||
return jsonify({
|
||||
"error": "Missing required fields",
|
||||
"error": "Missing required fields",
|
||||
"missing": missing_fields
|
||||
}), 400
|
||||
|
||||
|
||||
focus_group_id = data['focus_group_id']
|
||||
persona_id = data['persona_id']
|
||||
current_topic = data['current_topic']
|
||||
temperature = data.get('temperature', 0.7)
|
||||
|
||||
user_id = get_jwt_identity()
|
||||
|
||||
# Validate focus group exists
|
||||
focus_group = await FocusGroup.find_by_id(focus_group_id)
|
||||
if not focus_group:
|
||||
return jsonify({"error": "Focus group not found"}), 404
|
||||
|
||||
# Get the LLM model and GPT-5 parameters for this focus group
|
||||
llm_model = focus_group.get('llm_model')
|
||||
reasoning_effort = focus_group.get('reasoning_effort', 'low')
|
||||
verbosity = focus_group.get('verbosity', 'medium')
|
||||
|
||||
current_app.logger.info(f"🔍 DEBUG: Focus group data keys: {list(focus_group.keys())}")
|
||||
current_app.logger.info(f"🔍 DEBUG: Raw llm_model value from DB: '{focus_group.get('llm_model')}' (type: {type(focus_group.get('llm_model'))})")
|
||||
current_app.logger.info(f"🤖 Generating AI response using model: {llm_model or 'default (gemini-3-pro-preview)'} for focus group {focus_group_id}")
|
||||
|
||||
# Validate persona exists
|
||||
persona = await Persona.find_by_id(persona_id)
|
||||
if not persona:
|
||||
return jsonify({"error": "Persona not found"}), 404
|
||||
|
||||
|
||||
# Validate persona is part of the focus group
|
||||
if 'participants' not in focus_group or persona_id not in focus_group['participants']:
|
||||
return jsonify({
|
||||
"error": "Persona is not a participant in this focus group"
|
||||
}), 400
|
||||
|
||||
# Skip discussion guide retrieval - not needed for participant responses
|
||||
|
||||
# Get previous messages
|
||||
messages = await FocusGroup.get_messages(focus_group_id)
|
||||
|
||||
# Get all messages, the service will limit to the most recent 50
|
||||
recent_messages = messages
|
||||
|
||||
# Check if this focus group has any active visual context
|
||||
# This is the new approach - use persistent conversation context instead of detection
|
||||
print(f"🎨 Checking for active visual context in focus group {focus_group_id}")
|
||||
from app.services.conversation_context_service import ConversationContextService
|
||||
has_visual_context = await ConversationContextService.has_visual_context(focus_group_id)
|
||||
|
||||
print(f"🎨 Focus group has active visual context: {has_visual_context}")
|
||||
|
||||
# Build multimodal conversation context
|
||||
try:
|
||||
multimodal_context = await ConversationContextService.build_multimodal_context(focus_group_id, recent_messages)
|
||||
print(f"✅ Built multimodal context with {multimodal_context['total_visual_assets']} visual assets")
|
||||
except Exception as e:
|
||||
print(f"❌ Error building multimodal context: {e}")
|
||||
# Fallback to empty context
|
||||
multimodal_context = {
|
||||
"has_visual_context": False,
|
||||
"conversation_context": [],
|
||||
"text_context": "",
|
||||
"visual_timeline": {},
|
||||
"total_messages": len(recent_messages),
|
||||
"total_visual_assets": 0
|
||||
}
|
||||
|
||||
# DEBUG: Log visual context detection
|
||||
print(f"🎨 VISUAL CONTEXT DEBUG:")
|
||||
print(f" - focus_group_id: {focus_group_id}")
|
||||
print(f" - has_visual_context: {has_visual_context}")
|
||||
print(f" - total_visual_assets: {multimodal_context['total_visual_assets']}")
|
||||
print(f" - total_context_items: {len(multimodal_context['conversation_context'])}")
|
||||
current_app.logger.info(f"Visual context detection: has_visual_context={has_visual_context}, total_assets={multimodal_context['total_visual_assets']}")
|
||||
|
||||
# Generate the response using the new contextual approach
|
||||
from app.services.task_manager import get_task_manager
|
||||
from app.websocket_manager_async import get_async_websocket_manager
|
||||
task_manager = get_task_manager()
|
||||
task_id = task_manager.generate_task_id()
|
||||
websocket_manager = get_async_websocket_manager()
|
||||
app = current_app._get_current_object()
|
||||
bg_task = asyncio.create_task(
|
||||
_run_generate_response_bg(app, task_id, user_id, focus_group_id, persona_id, current_topic, temperature)
|
||||
)
|
||||
await task_manager.register_task(
|
||||
bg_task, 'generate_response', user_id,
|
||||
{'focus_group_id': focus_group_id, 'persona_id': persona_id},
|
||||
task_id=task_id
|
||||
)
|
||||
await websocket_manager.emit_to_user(user_id, 'task_started', {
|
||||
'task_id': task_id,
|
||||
'task_type': 'generate_response',
|
||||
'message': f'Started generating response for persona {persona_id}'
|
||||
})
|
||||
return jsonify({'task_id': task_id, 'message': 'Response generation started'}), 202
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Unexpected error starting generate_response: {str(e)}")
|
||||
return jsonify({"error": "Internal server error", "message": str(e)}), 500
|
||||
|
||||
|
||||
async def _run_generate_response_bg(app, task_id, user_id, focus_group_id, persona_id, current_topic, temperature):
|
||||
from app.websocket_manager_async import get_async_websocket_manager
|
||||
websocket_manager = get_async_websocket_manager()
|
||||
async with app.app_context():
|
||||
try:
|
||||
# Get focus group and persona
|
||||
focus_group = await FocusGroup.find_by_id(focus_group_id)
|
||||
persona = await Persona.find_by_id(persona_id)
|
||||
|
||||
llm_model = focus_group.get('llm_model')
|
||||
reasoning_effort = focus_group.get('reasoning_effort', 'low')
|
||||
verbosity = focus_group.get('verbosity', 'medium')
|
||||
|
||||
# Get previous messages
|
||||
messages = await FocusGroup.get_messages(focus_group_id)
|
||||
recent_messages = messages
|
||||
|
||||
# Check for active visual context
|
||||
from app.services.conversation_context_service import ConversationContextService
|
||||
has_visual_context = await ConversationContextService.has_visual_context(focus_group_id)
|
||||
|
||||
# Build multimodal conversation context
|
||||
try:
|
||||
multimodal_context = await ConversationContextService.build_multimodal_context(focus_group_id, recent_messages)
|
||||
except Exception as e:
|
||||
app.logger.warning(f"Error building multimodal context: {e}")
|
||||
multimodal_context = {
|
||||
"has_visual_context": False,
|
||||
"conversation_context": [],
|
||||
"text_context": "",
|
||||
"visual_timeline": {},
|
||||
"total_messages": len(recent_messages),
|
||||
"total_visual_assets": 0
|
||||
}
|
||||
|
||||
# Generate response
|
||||
if has_visual_context:
|
||||
print(f"🎨 Using contextual response generation with visual context")
|
||||
current_app.logger.info(f"Generating contextual response with {multimodal_context['total_visual_assets']} visual assets")
|
||||
|
||||
# Import here to avoid circular imports
|
||||
from app.services.llm_service import LLMService
|
||||
from app.utils.prompt_loader import load_prompt
|
||||
|
||||
# Build persona context for the prompt
|
||||
persona_details = _format_persona_details_for_context(persona)
|
||||
|
||||
# Create the contextual prompt
|
||||
prompt = load_prompt('focus-group-response', {
|
||||
'persona_details': persona_details,
|
||||
'current_topic': current_topic,
|
||||
'previous_messages': multimodal_context['text_context'], # Fallback text context
|
||||
'previous_messages': multimodal_context['text_context'],
|
||||
'length_instructions': _get_response_length_instructions(persona, recent_messages, current_topic),
|
||||
'is_creative_review': True,
|
||||
'creative_instructions': """
|
||||
|
|
@ -168,7 +171,7 @@ You are participating in a focus group discussion where visual materials have be
|
|||
|
||||
Consider:
|
||||
- Your first impression of any visuals shown
|
||||
- How the visual materials relate to the discussion topic
|
||||
- How the visual materials relate to the discussion topic
|
||||
- Any specific elements that catch your attention
|
||||
- How the visuals might appeal to people like you
|
||||
- Any suggestions or concerns you might have
|
||||
|
|
@ -177,8 +180,6 @@ Consider:
|
|||
Be genuine and specific in your feedback, drawing on your personal experiences and preferences.
|
||||
"""
|
||||
})
|
||||
|
||||
# Generate response using contextual conversation method
|
||||
response_text = await LLMService.generate_contextual_response(
|
||||
prompt=prompt,
|
||||
conversation_context=multimodal_context['conversation_context'],
|
||||
|
|
@ -188,9 +189,6 @@ Be genuine and specific in your feedback, drawing on your personal experiences a
|
|||
verbosity=verbosity if llm_model in ('gpt-5', 'gpt-5.2') else None
|
||||
)
|
||||
else:
|
||||
print(f"💬 Using standard response generation (no visual context)")
|
||||
current_app.logger.info(f"Generating standard response")
|
||||
|
||||
response_text = await generate_persona_response(
|
||||
persona=persona,
|
||||
current_topic=current_topic,
|
||||
|
|
@ -201,68 +199,44 @@ Be genuine and specific in your feedback, drawing on your personal experiences a
|
|||
reasoning_effort=reasoning_effort,
|
||||
verbosity=verbosity
|
||||
)
|
||||
|
||||
# Log success with response details
|
||||
response_type = "contextual with visual context" if has_visual_context else "standard"
|
||||
print(f"✅ Generated {response_type} response for persona {persona_id}")
|
||||
print(f"🔍 RESPONSE DEBUG:")
|
||||
print(f" - Response length: {len(response_text) if response_text else 0} characters")
|
||||
print(f" - Response type: {type(response_text)}")
|
||||
print(f" - Response preview: '{response_text[:200] if response_text else 'EMPTY'}...'")
|
||||
print(f" - Response repr: {repr(response_text[:50]) if response_text else 'NONE'}")
|
||||
current_app.logger.info(f"Generated {response_type} response for persona {persona_id} in focus group {focus_group_id}")
|
||||
|
||||
# Save message to DB
|
||||
message_data = {
|
||||
"text": response_text,
|
||||
"type": "response",
|
||||
"senderId": persona_id
|
||||
}
|
||||
message_id = FocusGroup.add_message(focus_group_id, message_data)
|
||||
|
||||
if message_id:
|
||||
await websocket_manager.emit_to_user(user_id, 'task_completed', {
|
||||
'task_id': task_id,
|
||||
'task_type': 'generate_response',
|
||||
'response': response_text,
|
||||
'message_id': message_id,
|
||||
'persona_id': persona_id,
|
||||
'focus_group_id': focus_group_id
|
||||
})
|
||||
else:
|
||||
await websocket_manager.emit_to_user(user_id, 'task_failed', {
|
||||
'task_id': task_id,
|
||||
'task_type': 'generate_response',
|
||||
'message': 'Failed to save message to database'
|
||||
})
|
||||
|
||||
except asyncio.CancelledError:
|
||||
await websocket_manager.emit_to_user(user_id, 'task_cancelled', {
|
||||
'task_id': task_id,
|
||||
'task_type': 'generate_response',
|
||||
'message': 'Response generation was cancelled'
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"❌ Error in response generation: {str(e)}")
|
||||
current_app.logger.error(f"Error generating response: {str(e)}")
|
||||
import traceback
|
||||
print(f"❌ Full traceback: {traceback.format_exc()}")
|
||||
raise
|
||||
|
||||
# Prepare and save the message
|
||||
print(f"💾 Preparing to save message to database...")
|
||||
message_data = {
|
||||
"text": response_text,
|
||||
"type": "response",
|
||||
"senderId": persona_id
|
||||
}
|
||||
print(f"💾 Message data keys: {list(message_data.keys())}")
|
||||
print(f"💾 Message text length: {len(message_data['text']) if message_data['text'] else 0}")
|
||||
print(f"💾 Message text preview: '{message_data['text'][:100] if message_data['text'] else 'EMPTY'}...'")
|
||||
print(f"💾 Message text repr: {repr(message_data['text'][:20]) if message_data['text'] else 'NONE'}")
|
||||
|
||||
print(f"💾 Calling FocusGroup.add_message...")
|
||||
message_id = FocusGroup.add_message(focus_group_id, message_data)
|
||||
print(f"💾 Message saved with ID: {message_id}")
|
||||
|
||||
if not message_id:
|
||||
print(f"❌ Failed to save message to database - no message ID returned")
|
||||
current_app.logger.error("Failed to save message to database")
|
||||
return jsonify({
|
||||
"error": "Failed to save message",
|
||||
"message": "The AI response was generated but could not be saved to the database"
|
||||
}), 500
|
||||
|
||||
|
||||
return jsonify({
|
||||
"message": "Response generated successfully",
|
||||
"response": response_text,
|
||||
"message_id": message_id,
|
||||
"persona_id": persona_id,
|
||||
"focus_group_id": focus_group_id
|
||||
}), 200
|
||||
|
||||
except FocusGroupResponseError as e:
|
||||
current_app.logger.error(f"Focus group response generation error: {str(e)}")
|
||||
return jsonify({
|
||||
"error": "Failed to generate response",
|
||||
"message": str(e)
|
||||
}), 500
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Unexpected error in focus group response: {str(e)}")
|
||||
return jsonify({
|
||||
"error": "Internal server error",
|
||||
"message": "An unexpected error occurred"
|
||||
}), 500
|
||||
app.logger.error(f"Error in _run_generate_response_bg: {str(e)}")
|
||||
await websocket_manager.emit_to_user(user_id, 'task_failed', {
|
||||
'task_id': task_id,
|
||||
'task_type': 'generate_response',
|
||||
'message': str(e)
|
||||
})
|
||||
|
||||
@focus_group_ai_bp.route('/generate-key-themes', methods=['POST'])
|
||||
@jwt_required()
|
||||
|
|
@ -270,132 +244,116 @@ Be genuine and specific in your feedback, drawing on your personal experiences a
|
|||
async def generate_key_themes():
|
||||
"""
|
||||
Generate key themes from a focus group discussion.
|
||||
|
||||
|
||||
Request body:
|
||||
{
|
||||
"focus_group_id": "focus_group_id",
|
||||
"temperature": 0.7 # Optional
|
||||
}
|
||||
|
||||
Returns:
|
||||
A JSON object containing the generated key themes
|
||||
|
||||
Returns immediately with 202 + task_id; result delivered via WebSocket task_completed event.
|
||||
"""
|
||||
try:
|
||||
data = (await request.get_json()) or {}
|
||||
|
||||
|
||||
# Validate required fields
|
||||
if 'focus_group_id' not in data:
|
||||
return jsonify({
|
||||
"error": "Missing required field: focus_group_id"
|
||||
}), 400
|
||||
|
||||
|
||||
focus_group_id = data['focus_group_id']
|
||||
temperature = data.get('temperature', 0.7)
|
||||
|
||||
user_id = get_jwt_identity()
|
||||
|
||||
# Validate focus group exists
|
||||
focus_group = await FocusGroup.find_by_id(focus_group_id)
|
||||
if not focus_group:
|
||||
return jsonify({"error": "Focus group not found"}), 404
|
||||
|
||||
# Get user_id for task tracking (optional for development mode)
|
||||
user_id = None
|
||||
try:
|
||||
user_id = get_jwt_identity()
|
||||
except Exception as jwt_err:
|
||||
current_app.logger.warning(f"Could not retrieve JWT identity for task tracking: {jwt_err}")
|
||||
|
||||
# Register current task for cancellation
|
||||
async with CancellableTask("key_themes_generation", user_id, {"focus_group_id": focus_group_id}) as task_id:
|
||||
|
||||
# Emit task_started event via WebSocket for immediate frontend tracking
|
||||
from app.websocket_manager_async import get_async_websocket_manager
|
||||
websocket_manager = get_async_websocket_manager()
|
||||
if user_id:
|
||||
await websocket_manager.emit_to_user(
|
||||
user_id,
|
||||
'task_started',
|
||||
{
|
||||
'task_id': task_id,
|
||||
'task_type': 'key_themes_generation',
|
||||
'message': f'Started generating key themes for focus group {focus_group_id}'
|
||||
}
|
||||
)
|
||||
|
||||
# Get the LLM model for this focus group
|
||||
llm_model = focus_group.get('llm_model')
|
||||
|
||||
# Generate key themes
|
||||
try:
|
||||
themes = await KeyThemeService.generate_key_themes(
|
||||
focus_group_id=focus_group_id,
|
||||
temperature=temperature,
|
||||
llm_model=llm_model
|
||||
)
|
||||
|
||||
# Log success
|
||||
current_app.logger.info(f"Generated {len(themes)} key themes for focus group {focus_group_id}")
|
||||
|
||||
# Save themes to database
|
||||
theme_ids = await FocusGroup.add_generated_themes(focus_group_id, themes)
|
||||
|
||||
if not theme_ids:
|
||||
current_app.logger.error("Failed to save themes to database")
|
||||
return jsonify({
|
||||
"error": "Failed to save themes",
|
||||
"message": "The themes were generated but could not be saved to the database"
|
||||
}), 500
|
||||
|
||||
# Format the themes for response
|
||||
formatted_themes = []
|
||||
for i, theme in enumerate(themes):
|
||||
if i < len(theme_ids):
|
||||
formatted_themes.append({
|
||||
"id": theme_ids[i],
|
||||
"title": theme["title"],
|
||||
"description": theme["description"],
|
||||
"quotes": theme.get("quotes", []),
|
||||
"source": "generated"
|
||||
})
|
||||
|
||||
# Emit completion event via WebSocket
|
||||
if user_id:
|
||||
await websocket_manager.emit_to_user(
|
||||
user_id,
|
||||
'task_completed',
|
||||
{
|
||||
'task_id': task_id,
|
||||
'task_type': 'key_themes_generation',
|
||||
'message': f'Successfully generated {len(formatted_themes)} key themes',
|
||||
'themes_created': len(formatted_themes)
|
||||
}
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"message": "Key themes generated successfully",
|
||||
"themes": formatted_themes,
|
||||
"focus_group_id": focus_group_id,
|
||||
"task_id": task_id
|
||||
}), 200
|
||||
|
||||
except KeyThemeServiceError as e:
|
||||
current_app.logger.error(f"Error generating key themes: {str(e)}")
|
||||
return jsonify({
|
||||
"error": "Failed to generate key themes",
|
||||
"message": str(e)
|
||||
}), 500
|
||||
|
||||
except asyncio.CancelledError:
|
||||
current_app.logger.info(f"Key themes generation cancelled for focus group {focus_group_id}")
|
||||
return jsonify({
|
||||
"error": "Generation cancelled",
|
||||
"message": "Key themes generation was cancelled by user"
|
||||
}), 499
|
||||
from app.services.task_manager import get_task_manager
|
||||
from app.websocket_manager_async import get_async_websocket_manager
|
||||
task_manager = get_task_manager()
|
||||
task_id = task_manager.generate_task_id()
|
||||
websocket_manager = get_async_websocket_manager()
|
||||
app = current_app._get_current_object()
|
||||
bg_task = asyncio.create_task(
|
||||
_run_key_themes_bg(app, task_id, user_id, focus_group_id, temperature)
|
||||
)
|
||||
await task_manager.register_task(
|
||||
bg_task, 'key_themes_generation', user_id,
|
||||
{'focus_group_id': focus_group_id},
|
||||
task_id=task_id
|
||||
)
|
||||
await websocket_manager.emit_to_user(user_id, 'task_started', {
|
||||
'task_id': task_id,
|
||||
'task_type': 'key_themes_generation',
|
||||
'message': f'Started generating key themes for focus group {focus_group_id}'
|
||||
})
|
||||
return jsonify({'task_id': task_id, 'message': 'Key themes generation started'}), 202
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Unexpected error in key theme generation: {str(e)}")
|
||||
return jsonify({
|
||||
"error": "Internal server error",
|
||||
"message": "An unexpected error occurred"
|
||||
}), 500
|
||||
current_app.logger.error(f"Unexpected error starting generate_key_themes: {str(e)}")
|
||||
return jsonify({"error": "Internal server error", "message": str(e)}), 500
|
||||
|
||||
|
||||
async def _run_key_themes_bg(app, task_id, user_id, focus_group_id, temperature):
|
||||
from app.websocket_manager_async import get_async_websocket_manager
|
||||
websocket_manager = get_async_websocket_manager()
|
||||
async with app.app_context():
|
||||
try:
|
||||
focus_group = await FocusGroup.find_by_id(focus_group_id)
|
||||
llm_model = focus_group.get('llm_model')
|
||||
|
||||
themes = await KeyThemeService.generate_key_themes(
|
||||
focus_group_id=focus_group_id,
|
||||
temperature=temperature,
|
||||
llm_model=llm_model
|
||||
)
|
||||
|
||||
app.logger.info(f"Generated {len(themes)} key themes for focus group {focus_group_id}")
|
||||
|
||||
theme_ids = await FocusGroup.add_generated_themes(focus_group_id, themes)
|
||||
|
||||
if not theme_ids:
|
||||
await websocket_manager.emit_to_user(user_id, 'task_failed', {
|
||||
'task_id': task_id,
|
||||
'task_type': 'key_themes_generation',
|
||||
'message': 'The themes were generated but could not be saved to the database'
|
||||
})
|
||||
return
|
||||
|
||||
formatted_themes = []
|
||||
for i, theme in enumerate(themes):
|
||||
if i < len(theme_ids):
|
||||
formatted_themes.append({
|
||||
"id": theme_ids[i],
|
||||
"title": theme["title"],
|
||||
"description": theme["description"],
|
||||
"quotes": theme.get("quotes", []),
|
||||
"source": "generated"
|
||||
})
|
||||
|
||||
await websocket_manager.emit_to_user(user_id, 'task_completed', {
|
||||
'task_id': task_id,
|
||||
'task_type': 'key_themes_generation',
|
||||
'themes': formatted_themes,
|
||||
'focus_group_id': focus_group_id
|
||||
})
|
||||
|
||||
except asyncio.CancelledError:
|
||||
await websocket_manager.emit_to_user(user_id, 'task_cancelled', {
|
||||
'task_id': task_id,
|
||||
'task_type': 'key_themes_generation',
|
||||
'message': 'Key themes generation was cancelled'
|
||||
})
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error in _run_key_themes_bg: {str(e)}")
|
||||
await websocket_manager.emit_to_user(user_id, 'task_failed', {
|
||||
'task_id': task_id,
|
||||
'task_type': 'key_themes_generation',
|
||||
'message': str(e)
|
||||
})
|
||||
|
||||
@focus_group_ai_bp.route('/key-themes/<focus_group_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
|
|
|
|||
|
|
@ -1592,52 +1592,69 @@ def test_websocket_emission(focus_group_id):
|
|||
"event": "message_update"
|
||||
}), 200
|
||||
|
||||
async def _run_describe_asset_bg(app, task_id, user_id, focus_group_id, asset_filename):
|
||||
from app.websocket_manager_async import get_async_websocket_manager
|
||||
websocket_manager = get_async_websocket_manager()
|
||||
async with app.app_context():
|
||||
try:
|
||||
description = await ImageDescriptionService.generate_description(focus_group_id, asset_filename)
|
||||
await websocket_manager.emit_to_user(user_id, 'task_completed', {
|
||||
'task_id': task_id,
|
||||
'task_type': 'describe_asset',
|
||||
'description': description,
|
||||
'asset_filename': asset_filename
|
||||
})
|
||||
except asyncio.CancelledError:
|
||||
await websocket_manager.emit_to_user(user_id, 'task_cancelled', {
|
||||
'task_id': task_id,
|
||||
'task_type': 'describe_asset'
|
||||
})
|
||||
except Exception as e:
|
||||
await websocket_manager.emit_to_user(user_id, 'task_failed', {
|
||||
'task_id': task_id,
|
||||
'task_type': 'describe_asset',
|
||||
'message': str(e)
|
||||
})
|
||||
|
||||
|
||||
@focus_groups_bp.route('/<focus_group_id>/describe-asset', methods=['POST'])
|
||||
@jwt_required()
|
||||
async def describe_asset(focus_group_id):
|
||||
"""Generate AI description of an asset for enhanced creative review questions."""
|
||||
logger.debug(f"🔍 API ENDPOINT: describe-asset called for focus group {focus_group_id}")
|
||||
logger.debug(f"API ENDPOINT: describe-asset called for focus group {focus_group_id}")
|
||||
try:
|
||||
# Verify focus group exists
|
||||
logger.debug(f"🔍 API: Looking up focus group {focus_group_id}")
|
||||
focus_group = await FocusGroup.find_by_id(focus_group_id)
|
||||
if not focus_group:
|
||||
logger.error(f"API: Focus group {focus_group_id} not found")
|
||||
return jsonify({"error": "Focus group not found"}), 404
|
||||
|
||||
logger.debug(f"API: Focus group {focus_group_id} found")
|
||||
|
||||
|
||||
# Get asset filename from request
|
||||
data = await request.get_json()
|
||||
logger.debug(f"🔍 API: Request data: {data}")
|
||||
if not data or 'asset_filename' not in data:
|
||||
logger.error(f"API: Missing asset_filename in request")
|
||||
return jsonify({"error": "Missing asset_filename in request"}), 400
|
||||
|
||||
|
||||
asset_filename = data['asset_filename']
|
||||
logger.debug(f"🔍 API: Asset filename: {asset_filename}")
|
||||
|
||||
logger.debug(f"API: Generating description for asset {asset_filename} in focus group {focus_group_id}")
|
||||
|
||||
# Generate AI description
|
||||
try:
|
||||
description = await ImageDescriptionService.generate_description(focus_group_id, asset_filename)
|
||||
|
||||
return jsonify({
|
||||
"message": "Asset description generated successfully",
|
||||
"asset_filename": asset_filename,
|
||||
"description": description
|
||||
}), 200
|
||||
|
||||
except ImageDescriptionError as e:
|
||||
error_msg = f"Failed to generate description: {str(e)}"
|
||||
logger.error(f"API: {error_msg}")
|
||||
return jsonify({
|
||||
"error": error_msg,
|
||||
"asset_filename": asset_filename,
|
||||
"fallback": True
|
||||
}), 422 # Unprocessable Entity - client should fallback to original text
|
||||
|
||||
user_id = get_jwt_identity()
|
||||
|
||||
from app.services.task_manager import get_task_manager
|
||||
from app.websocket_manager_async import get_async_websocket_manager
|
||||
task_manager = get_task_manager()
|
||||
task_id = task_manager.generate_task_id()
|
||||
websocket_manager = get_async_websocket_manager()
|
||||
app = current_app._get_current_object()
|
||||
bg_task = asyncio.create_task(
|
||||
_run_describe_asset_bg(app, task_id, user_id, focus_group_id, asset_filename)
|
||||
)
|
||||
await task_manager.register_task(bg_task, 'describe_asset', user_id, {'focus_group_id': focus_group_id, 'asset_filename': asset_filename}, task_id=task_id)
|
||||
await websocket_manager.emit_to_user(user_id, 'task_started', {
|
||||
'task_id': task_id,
|
||||
'task_type': 'describe_asset',
|
||||
'message': f'Started generating description for {asset_filename}'
|
||||
})
|
||||
return jsonify({'task_id': task_id, 'message': 'Asset description generation started'}), 202
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in describe_asset: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import logging
|
||||
from quart import Blueprint, request, jsonify, send_file, Response
|
||||
from quart import Blueprint, request, jsonify, send_file, Response, current_app
|
||||
from app.auth.quart_jwt import jwt_required, get_jwt_identity
|
||||
from app.models.persona import Persona
|
||||
import json
|
||||
|
|
@ -8,7 +8,6 @@ logger = logging.getLogger(__name__)
|
|||
from app.services.persona_export_service import PersonaExportService
|
||||
from app.services.bulk_persona_export_service import BulkPersonaExportService
|
||||
from app.services.persona_modification_service import PersonaModificationService, PersonaModificationError
|
||||
from app.services.task_manager import CancellableTask
|
||||
from bson import ObjectId
|
||||
import datetime
|
||||
import asyncio
|
||||
|
|
@ -154,7 +153,8 @@ async def create_multiple_personas():
|
|||
async def modify_persona_with_ai(persona_id):
|
||||
"""
|
||||
Modify a persona using AI based on natural language instructions.
|
||||
|
||||
Returns 202 immediately; result delivered via WebSocket task_completed event.
|
||||
|
||||
Request body should include:
|
||||
- modification_prompt: Natural language description of desired changes
|
||||
- llm_model: Model to use (defaults to 'gemini-3-pro-preview')
|
||||
|
|
@ -163,49 +163,45 @@ async def modify_persona_with_ai(persona_id):
|
|||
- preview_only: If true, returns modified data without saving to database (defaults to false)
|
||||
"""
|
||||
try:
|
||||
# Get request data
|
||||
request_data = await request.get_json()
|
||||
if not request_data:
|
||||
return jsonify({"error": "No request data provided"}), 400
|
||||
|
||||
|
||||
modification_prompt = request_data.get('modification_prompt')
|
||||
if not modification_prompt:
|
||||
return jsonify({"error": "modification_prompt is required"}), 400
|
||||
|
||||
|
||||
llm_model = request_data.get('llm_model', 'gemini-3-pro-preview')
|
||||
reasoning_effort = request_data.get('reasoning_effort', 'medium')
|
||||
verbosity = request_data.get('verbosity', 'medium')
|
||||
preview_only = request_data.get('preview_only', False)
|
||||
|
||||
mode_text = "previewing" if preview_only else "modifying"
|
||||
logger.debug(f"Backend: {mode_text.title()} persona {persona_id} with {llm_model}")
|
||||
logger.debug(f"Modification prompt: {modification_prompt[:100]}...")
|
||||
|
||||
# Get user_id for task tracking (optional for development mode)
|
||||
user_id = None
|
||||
try:
|
||||
user_id = get_jwt_identity()
|
||||
except Exception as jwt_err:
|
||||
logger.warning(f"Could not retrieve JWT identity for task tracking: {jwt_err}")
|
||||
|
||||
# Register current task for cancellation
|
||||
async with CancellableTask("persona_modification", user_id, {"persona_id": persona_id, "preview_only": preview_only}) as task_id:
|
||||
|
||||
# Emit task_started event via WebSocket for immediate frontend tracking
|
||||
from app.websocket_manager_async import get_async_websocket_manager
|
||||
websocket_manager = get_async_websocket_manager()
|
||||
if user_id:
|
||||
await websocket_manager.emit_to_user(
|
||||
user_id,
|
||||
'task_started',
|
||||
{
|
||||
'task_id': task_id,
|
||||
'task_type': 'persona_modification',
|
||||
'message': f'Started {"previewing" if preview_only else "modifying"} persona {persona_id}'
|
||||
}
|
||||
)
|
||||
|
||||
# Call the modification service
|
||||
user_id = get_jwt_identity()
|
||||
|
||||
from app.services.task_manager import get_task_manager
|
||||
from app.websocket_manager_async import get_async_websocket_manager
|
||||
task_manager = get_task_manager()
|
||||
task_id = task_manager.generate_task_id()
|
||||
websocket_manager = get_async_websocket_manager()
|
||||
app = current_app._get_current_object()
|
||||
|
||||
bg_task = asyncio.create_task(
|
||||
_run_modify_persona_bg(app, task_id, user_id, persona_id, modification_prompt, llm_model, reasoning_effort, verbosity, preview_only)
|
||||
)
|
||||
await task_manager.register_task(bg_task, 'persona_modification', user_id, {'persona_id': persona_id, 'preview_only': preview_only}, task_id=task_id)
|
||||
await websocket_manager.emit_to_user(user_id, 'task_started', {'task_id': task_id, 'task_type': 'persona_modification'})
|
||||
return jsonify({'task_id': task_id, 'message': 'Persona modification started'}), 202
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in persona modification: {e}")
|
||||
return jsonify({"error": f"Unexpected error: {str(e)}"}), 500
|
||||
|
||||
|
||||
async def _run_modify_persona_bg(app, task_id, user_id, persona_id, modification_prompt, llm_model, reasoning_effort, verbosity, preview_only):
|
||||
from app.websocket_manager_async import get_async_websocket_manager
|
||||
websocket_manager = get_async_websocket_manager()
|
||||
async with app.app_context():
|
||||
try:
|
||||
modified_persona_data = await PersonaModificationService.modify_persona(
|
||||
persona_id=persona_id,
|
||||
modification_prompt=modification_prompt,
|
||||
|
|
@ -214,102 +210,102 @@ async def modify_persona_with_ai(persona_id):
|
|||
verbosity=verbosity,
|
||||
preview_only=preview_only
|
||||
)
|
||||
|
||||
# Emit completion event via WebSocket
|
||||
if user_id:
|
||||
await websocket_manager.emit_to_user(
|
||||
user_id,
|
||||
'task_completed',
|
||||
{
|
||||
'task_id': task_id,
|
||||
'task_type': 'persona_modification',
|
||||
'message': f'Successfully {"previewed" if preview_only else "modified"} persona'
|
||||
}
|
||||
)
|
||||
|
||||
success_message = "Persona preview generated successfully" if preview_only else "Persona modified successfully"
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": success_message,
|
||||
"persona": make_serializable(modified_persona_data),
|
||||
"preview_only": preview_only,
|
||||
"task_id": task_id
|
||||
}), 200
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.debug(f"⏹️ Persona modification cancelled for persona {persona_id}")
|
||||
return jsonify({
|
||||
"error": "Generation cancelled",
|
||||
"message": "Persona modification was cancelled by user"
|
||||
}), 499
|
||||
except PersonaModificationError as e:
|
||||
logger.error(f"Persona modification error: {e}")
|
||||
return jsonify({"error": str(e)}), 400
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in persona modification: {e}")
|
||||
return jsonify({"error": f"Unexpected error: {str(e)}"}), 500
|
||||
await websocket_manager.emit_to_user(user_id, 'task_completed', {
|
||||
'task_id': task_id,
|
||||
'task_type': 'persona_modification',
|
||||
'persona': make_serializable(modified_persona_data),
|
||||
'preview_only': preview_only
|
||||
})
|
||||
except asyncio.CancelledError:
|
||||
await websocket_manager.emit_to_user(user_id, 'task_cancelled', {'task_id': task_id})
|
||||
except PersonaModificationError as e:
|
||||
logger.error(f"Persona modification error: {e}")
|
||||
await websocket_manager.emit_to_user(user_id, 'task_failed', {'task_id': task_id, 'message': str(e)})
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in _run_modify_persona_bg: {e}")
|
||||
await websocket_manager.emit_to_user(user_id, 'task_failed', {'task_id': task_id, 'message': str(e)})
|
||||
|
||||
@personas_bp.route('/<persona_id>/export-profile', methods=['POST'])
|
||||
@jwt_required()
|
||||
async def export_persona_profile(persona_id):
|
||||
"""
|
||||
Export a persona profile as beautifully formatted markdown.
|
||||
|
||||
Returns 202 immediately; result delivered via WebSocket task_completed event.
|
||||
|
||||
Request body can optionally include:
|
||||
- llm_model: Model to use (defaults to 'gpt-4.1')
|
||||
- temperature: Temperature for generation (defaults to 0.3)
|
||||
"""
|
||||
try:
|
||||
# Get the persona data
|
||||
persona = await Persona.find_by_id(persona_id)
|
||||
if not persona:
|
||||
return jsonify({"error": "Persona not found"}), 404
|
||||
|
||||
# Get optional parameters from request
|
||||
|
||||
request_data = await request.get_json() or {}
|
||||
llm_model = request_data.get('llm_model', 'gpt-4.1')
|
||||
temperature = request_data.get('temperature', 0.3)
|
||||
|
||||
# Initialize export service
|
||||
export_service = PersonaExportService()
|
||||
|
||||
# Make persona data serializable for JSON processing
|
||||
persona_data = make_serializable(persona)
|
||||
|
||||
logger.debug(f"Backend: Exporting profile for persona {persona_data.get('name', persona_id)} using {llm_model}")
|
||||
|
||||
# Generate the markdown profile
|
||||
result = await export_service.generate_profile_markdown(
|
||||
persona_data=persona_data,
|
||||
llm_model=llm_model,
|
||||
temperature=temperature
|
||||
|
||||
user_id = get_jwt_identity()
|
||||
|
||||
from app.services.task_manager import get_task_manager
|
||||
from app.websocket_manager_async import get_async_websocket_manager
|
||||
task_manager = get_task_manager()
|
||||
task_id = task_manager.generate_task_id()
|
||||
websocket_manager = get_async_websocket_manager()
|
||||
app = current_app._get_current_object()
|
||||
|
||||
bg_task = asyncio.create_task(
|
||||
_run_export_profile_bg(app, task_id, user_id, persona_id, llm_model, temperature)
|
||||
)
|
||||
|
||||
if result.get('success'):
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"markdown_content": result['markdown_content'],
|
||||
"persona_name": result['persona_name'],
|
||||
"model_used": result.get('model_used'),
|
||||
"content_length": result.get('content_length')
|
||||
}), 200
|
||||
else:
|
||||
# If LLM generation failed, try fallback
|
||||
logger.debug(f"⚠️ LLM generation failed, using fallback for {persona_data.get('name', persona_id)}")
|
||||
fallback_markdown = export_service.generate_fallback_markdown(persona_data)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"markdown_content": fallback_markdown,
|
||||
"persona_name": persona_data.get('name', 'Unknown Persona'),
|
||||
"model_used": "fallback",
|
||||
"warning": "Used fallback formatting due to LLM error"
|
||||
}), 200
|
||||
|
||||
await task_manager.register_task(bg_task, 'export_profile', user_id, {'persona_id': persona_id}, task_id=task_id)
|
||||
await websocket_manager.emit_to_user(user_id, 'task_started', {'task_id': task_id, 'task_type': 'export_profile'})
|
||||
return jsonify({'task_id': task_id, 'message': 'Profile export started'}), 202
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in export_persona_profile: {e}")
|
||||
return jsonify({"error": f"Failed to export persona profile: {str(e)}"}), 500
|
||||
|
||||
|
||||
async def _run_export_profile_bg(app, task_id, user_id, persona_id, llm_model, temperature):
|
||||
from app.websocket_manager_async import get_async_websocket_manager
|
||||
websocket_manager = get_async_websocket_manager()
|
||||
async with app.app_context():
|
||||
try:
|
||||
persona = await Persona.find_by_id(persona_id)
|
||||
persona_data = make_serializable(persona)
|
||||
export_service = PersonaExportService()
|
||||
result = await export_service.generate_profile_markdown(
|
||||
persona_data=persona_data,
|
||||
llm_model=llm_model,
|
||||
temperature=temperature
|
||||
)
|
||||
if result.get('success'):
|
||||
await websocket_manager.emit_to_user(user_id, 'task_completed', {
|
||||
'task_id': task_id,
|
||||
'task_type': 'export_profile',
|
||||
'success': True,
|
||||
'markdown_content': result['markdown_content'],
|
||||
'persona_name': result['persona_name'],
|
||||
'model_used': result.get('model_used')
|
||||
})
|
||||
else:
|
||||
logger.debug(f"LLM generation failed, using fallback for persona {persona_id}")
|
||||
fallback_markdown = export_service.generate_fallback_markdown(persona_data)
|
||||
await websocket_manager.emit_to_user(user_id, 'task_completed', {
|
||||
'task_id': task_id,
|
||||
'task_type': 'export_profile',
|
||||
'success': True,
|
||||
'markdown_content': fallback_markdown,
|
||||
'persona_name': persona_data.get('name', 'Unknown'),
|
||||
'model_used': 'fallback',
|
||||
'warning': 'Used fallback formatting due to LLM error'
|
||||
})
|
||||
except asyncio.CancelledError:
|
||||
await websocket_manager.emit_to_user(user_id, 'task_cancelled', {'task_id': task_id})
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in _run_export_profile_bg: {e}")
|
||||
await websocket_manager.emit_to_user(user_id, 'task_failed', {'task_id': task_id, 'message': str(e)})
|
||||
|
||||
@personas_bp.route('/bulk-export', methods=['POST'])
|
||||
@jwt_required()
|
||||
async def bulk_export_personas():
|
||||
|
|
|
|||
|
|
@ -895,6 +895,60 @@ const DiscussionPanel = ({
|
|||
}
|
||||
};
|
||||
|
||||
// Helper: wraps focusGroupAiApi.generateResponse with 202+WebSocket async pattern
|
||||
const generateResponseAsync = async (
|
||||
participantId: string,
|
||||
topicContext: string,
|
||||
temperature?: number
|
||||
): Promise<{ data: { response: string; message_id: string; persona_id: string; focus_group_id: string; timestamp?: string } } | null> => {
|
||||
const response = await focusGroupAiApi.generateResponse(focusGroupId, participantId, topicContext, temperature);
|
||||
|
||||
const taskId = response.data?.task_id;
|
||||
|
||||
if (response.status === 202 && taskId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const handleCompleted = (event: CustomEvent) => {
|
||||
const detail = event.detail;
|
||||
if (detail.task_id !== taskId) return;
|
||||
cleanup();
|
||||
resolve({
|
||||
data: {
|
||||
response: detail.response,
|
||||
message_id: detail.message_id,
|
||||
persona_id: detail.persona_id,
|
||||
focus_group_id: detail.focus_group_id,
|
||||
timestamp: detail.timestamp
|
||||
}
|
||||
});
|
||||
};
|
||||
const handleFailed = (event: CustomEvent) => {
|
||||
if (event.detail.task_id !== taskId) return;
|
||||
cleanup();
|
||||
reject(new Error(event.detail.message || 'Generation failed'));
|
||||
};
|
||||
const handleCancelled = (event: CustomEvent) => {
|
||||
if (event.detail.task_id !== taskId) return;
|
||||
cleanup();
|
||||
resolve(null);
|
||||
};
|
||||
const cleanup = () => {
|
||||
window.removeEventListener('ws:task_completed', handleCompleted as EventListener);
|
||||
window.removeEventListener('ws:task_failed', handleFailed as EventListener);
|
||||
window.removeEventListener('ws:task_cancelled', handleCancelled as EventListener);
|
||||
};
|
||||
window.addEventListener('ws:task_completed', handleCompleted as EventListener);
|
||||
window.addEventListener('ws:task_failed', handleFailed as EventListener);
|
||||
window.addEventListener('ws:task_cancelled', handleCancelled as EventListener);
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback: sync response
|
||||
if (response.data?.response) {
|
||||
return response;
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
// Generate targeted responses for mentioned participants
|
||||
const generateMentionedResponses = async (mentionedParticipantIds: string[], topicContext: string) => {
|
||||
if (!focusGroupId || mentionedParticipantIds.length === 0) return;
|
||||
|
|
@ -921,12 +975,11 @@ const DiscussionPanel = ({
|
|||
|
||||
try {
|
||||
// Generate the response from the mentioned participant
|
||||
const response = await focusGroupAiApi.generateResponse(
|
||||
focusGroupId,
|
||||
const response = await generateResponseAsync(
|
||||
participantId,
|
||||
topicContext || "Continue the conversation based on the latest moderator message."
|
||||
);
|
||||
|
||||
|
||||
if (response?.data?.response) {
|
||||
const aiMessage: Message = {
|
||||
id: response.data.message_id || `msg-${Date.now()}-${participantId}`,
|
||||
|
|
@ -1014,17 +1067,13 @@ const DiscussionPanel = ({
|
|||
});
|
||||
|
||||
// Generate the response from the AI-selected participant
|
||||
const response = await focusGroupAiApi.generateResponse(
|
||||
focusGroupId,
|
||||
participantId,
|
||||
topicContext
|
||||
);
|
||||
|
||||
const response = await generateResponseAsync(participantId, topicContext);
|
||||
|
||||
// Check if we have a valid response before proceeding
|
||||
if (!response || !response.data) {
|
||||
throw new Error("Empty response from API");
|
||||
}
|
||||
|
||||
|
||||
// If the response was successful, the backend has already saved the message
|
||||
if (response?.data?.message_id && response?.data?.response) {
|
||||
// Create a new message object for the UI
|
||||
|
|
@ -1078,12 +1127,8 @@ const DiscussionPanel = ({
|
|||
const currentTopic = getLastModeratorMessage();
|
||||
const personaId = nextPersona._id || nextPersona.id;
|
||||
|
||||
const response = await focusGroupAiApi.generateResponse(
|
||||
focusGroupId,
|
||||
personaId,
|
||||
currentTopic
|
||||
);
|
||||
|
||||
const response = await generateResponseAsync(personaId, currentTopic);
|
||||
|
||||
if (response?.data?.message_id && response?.data?.response) {
|
||||
const newMessage: Message = {
|
||||
id: response.data.message_id,
|
||||
|
|
|
|||
|
|
@ -115,13 +115,51 @@ export default function PersonaModificationModal({
|
|||
modificationControls.setTaskId(response.data.task_id);
|
||||
}
|
||||
|
||||
if (response.status === 202 && response.data?.task_id) {
|
||||
const taskId = response.data.task_id;
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const handleCompleted = (event: CustomEvent) => {
|
||||
if (event.detail.task_id !== taskId) return;
|
||||
cleanup();
|
||||
modificationControls.completeGeneration();
|
||||
toastService.success("Preview generated successfully!", {
|
||||
description: `Ready to review proposed changes to ${persona.name}`
|
||||
});
|
||||
onPersonaPreview(event.detail.persona);
|
||||
handleClose();
|
||||
resolve();
|
||||
};
|
||||
const handleFailed = (event: CustomEvent) => {
|
||||
if (event.detail.task_id !== taskId) return;
|
||||
cleanup();
|
||||
modificationControls.failGeneration(event.detail.message);
|
||||
reject(new Error(event.detail.message));
|
||||
};
|
||||
const handleCancelled = (event: CustomEvent) => {
|
||||
if (event.detail.task_id !== taskId) return;
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
const cleanup = () => {
|
||||
window.removeEventListener('ws:task_completed', handleCompleted as EventListener);
|
||||
window.removeEventListener('ws:task_failed', handleFailed as EventListener);
|
||||
window.removeEventListener('ws:task_cancelled', handleCancelled as EventListener);
|
||||
};
|
||||
window.addEventListener('ws:task_completed', handleCompleted as EventListener);
|
||||
window.addEventListener('ws:task_failed', handleFailed as EventListener);
|
||||
window.addEventListener('ws:task_cancelled', handleCancelled as EventListener);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: sync response
|
||||
if (response.data && response.data.persona) {
|
||||
modificationControls.completeGeneration();
|
||||
|
||||
|
||||
toastService.success("Preview generated successfully!", {
|
||||
description: `Ready to review proposed changes to ${persona.name}`
|
||||
});
|
||||
|
||||
|
||||
onPersonaPreview(response.data.persona);
|
||||
handleClose();
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -110,14 +110,68 @@ export default function PersonaProfile() {
|
|||
temperature: 0.3
|
||||
});
|
||||
|
||||
if (response.status === 202 && response.data?.task_id) {
|
||||
const taskId = response.data.task_id;
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const handleCompleted = (event: CustomEvent) => {
|
||||
if (event.detail.task_id !== taskId) return;
|
||||
cleanup();
|
||||
const { markdown_content, persona_name, model_used, warning } = event.detail;
|
||||
if (markdown_content) {
|
||||
const currentDate = new Date().toISOString().split('T')[0];
|
||||
const safePersonaName = persona_name.replace(/[^a-zA-Z0-9\-\s]/g, '').replace(/\s+/g, '-').toLowerCase();
|
||||
const filename = `${safePersonaName}-profile-${currentDate}.md`;
|
||||
const element = document.createElement('a');
|
||||
const file = new Blob([markdown_content], { type: 'text/markdown' });
|
||||
element.href = URL.createObjectURL(file);
|
||||
element.download = filename;
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
if (warning) {
|
||||
toastService.success("Profile downloaded with fallback formatting", {
|
||||
description: `${persona_name} profile saved as ${filename}`
|
||||
});
|
||||
} else {
|
||||
const modelDisplay = model_used === 'gpt-4.1' ? 'GPT-4.1' : model_used;
|
||||
toastService.success("Profile downloaded successfully", {
|
||||
description: `${persona_name} profile processed with ${modelDisplay} and saved as ${filename}`
|
||||
});
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
const handleFailed = (event: CustomEvent) => {
|
||||
if (event.detail.task_id !== taskId) return;
|
||||
cleanup();
|
||||
reject(new Error(event.detail.message));
|
||||
};
|
||||
const handleCancelled = (event: CustomEvent) => {
|
||||
if (event.detail.task_id !== taskId) return;
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
const cleanup = () => {
|
||||
window.removeEventListener('ws:task_completed', handleCompleted as EventListener);
|
||||
window.removeEventListener('ws:task_failed', handleFailed as EventListener);
|
||||
window.removeEventListener('ws:task_cancelled', handleCancelled as EventListener);
|
||||
};
|
||||
window.addEventListener('ws:task_completed', handleCompleted as EventListener);
|
||||
window.addEventListener('ws:task_failed', handleFailed as EventListener);
|
||||
window.addEventListener('ws:task_cancelled', handleCancelled as EventListener);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: sync response
|
||||
const { markdown_content, persona_name, model_used, warning } = response.data;
|
||||
|
||||
|
||||
if (markdown_content) {
|
||||
// Generate filename with current date
|
||||
const currentDate = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
|
||||
const safePersonaName = persona_name.replace(/[^a-zA-Z0-9\-\s]/g, '').replace(/\s+/g, '-').toLowerCase();
|
||||
const filename = `${safePersonaName}-profile-${currentDate}.md`;
|
||||
|
||||
|
||||
// Create and download the file
|
||||
const element = document.createElement('a');
|
||||
const file = new Blob([markdown_content], { type: 'text/markdown' });
|
||||
|
|
@ -126,7 +180,7 @@ export default function PersonaProfile() {
|
|||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
|
||||
|
||||
// Show success toast
|
||||
if (warning) {
|
||||
toastService.success("Profile downloaded with fallback formatting", {
|
||||
|
|
|
|||
|
|
@ -1568,11 +1568,59 @@ const FocusGroupSession = () => {
|
|||
try {
|
||||
const response = await focusGroupAiApi.generateKeyThemes(id);
|
||||
|
||||
if (response.data && response.data.themes) {
|
||||
// Update themes state immediately
|
||||
setThemes(prevThemes => [...prevThemes, ...response.data.themes]);
|
||||
const taskId = response.data?.task_id;
|
||||
if (taskId) themeGenerationControls.setTaskId(taskId);
|
||||
|
||||
// Allow progress to animate for at least 3 seconds before completing
|
||||
if (response.status === 202 && taskId) {
|
||||
await new Promise<void>((resolve) => {
|
||||
const handleCompleted = (event: CustomEvent) => {
|
||||
const detail = event.detail;
|
||||
if (detail.task_id !== taskId) return;
|
||||
cleanup();
|
||||
if (detail.themes) {
|
||||
setThemes(prev => [...prev, ...detail.themes]);
|
||||
setTimeout(() => {
|
||||
themeGenerationControls.completeGeneration();
|
||||
toastService.success(`Generated ${detail.themes.length} key themes`, {
|
||||
description: "New themes have been added to the analysis."
|
||||
});
|
||||
}, 3000);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
themeGenerationControls.completeGeneration();
|
||||
toastService.warning("No new themes were generated", {
|
||||
description: "Try again when the discussion has more content."
|
||||
});
|
||||
}, 3000);
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
const handleFailed = (event: CustomEvent) => {
|
||||
if (event.detail.task_id !== taskId) return;
|
||||
cleanup();
|
||||
themeGenerationControls.failGeneration(event.detail.message || 'Failed to generate key themes');
|
||||
toastService.error("Failed to generate key themes", {
|
||||
description: "There was an error analyzing the discussion. Please try again."
|
||||
});
|
||||
resolve();
|
||||
};
|
||||
const handleCancelled = (event: CustomEvent) => {
|
||||
if (event.detail.task_id !== taskId) return;
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
const cleanup = () => {
|
||||
window.removeEventListener('ws:task_completed', handleCompleted as EventListener);
|
||||
window.removeEventListener('ws:task_failed', handleFailed as EventListener);
|
||||
window.removeEventListener('ws:task_cancelled', handleCancelled as EventListener);
|
||||
};
|
||||
window.addEventListener('ws:task_completed', handleCompleted as EventListener);
|
||||
window.addEventListener('ws:task_failed', handleFailed as EventListener);
|
||||
window.addEventListener('ws:task_cancelled', handleCancelled as EventListener);
|
||||
});
|
||||
} else if (response.data && response.data.themes) {
|
||||
// Fallback: sync response
|
||||
setThemes(prevThemes => [...prevThemes, ...response.data.themes]);
|
||||
setTimeout(() => {
|
||||
themeGenerationControls.completeGeneration();
|
||||
toastService.success(`Generated ${response.data.themes.length} key themes`, {
|
||||
|
|
@ -1580,7 +1628,6 @@ const FocusGroupSession = () => {
|
|||
});
|
||||
}, 3000);
|
||||
} else {
|
||||
// Allow progress to animate for at least 3 seconds before completing
|
||||
setTimeout(() => {
|
||||
themeGenerationControls.completeGeneration();
|
||||
toastService.warning("No new themes were generated", {
|
||||
|
|
@ -2048,7 +2095,38 @@ const FocusGroupSession = () => {
|
|||
// Generate AI description and enhance the question
|
||||
try {
|
||||
|
||||
const descriptionResponse = await focusGroupsApi.describeAsset(id, assetFilename);
|
||||
const descriptionResponse = await (async () => {
|
||||
const res = await focusGroupsApi.describeAsset(id, assetFilename);
|
||||
if (res.status === 202 && res.data?.task_id) {
|
||||
const taskId = res.data.task_id;
|
||||
return new Promise<typeof res>((resolve, reject) => {
|
||||
const cleanup = () => {
|
||||
window.removeEventListener('ws:task_completed', onCompleted as EventListener);
|
||||
window.removeEventListener('ws:task_failed', onFailed as EventListener);
|
||||
window.removeEventListener('ws:task_cancelled', onCancelled as EventListener);
|
||||
};
|
||||
const onCompleted = (e: CustomEvent) => {
|
||||
if (e.detail.task_id !== taskId) return;
|
||||
cleanup();
|
||||
resolve({ ...res, data: { ...res.data, description: e.detail.description } });
|
||||
};
|
||||
const onFailed = (e: CustomEvent) => {
|
||||
if (e.detail.task_id !== taskId) return;
|
||||
cleanup();
|
||||
reject(new Error(e.detail.message || 'Description failed'));
|
||||
};
|
||||
const onCancelled = (e: CustomEvent) => {
|
||||
if (e.detail.task_id !== taskId) return;
|
||||
cleanup();
|
||||
resolve({ ...res, data: { ...res.data, description: null } });
|
||||
};
|
||||
window.addEventListener('ws:task_completed', onCompleted as EventListener);
|
||||
window.addEventListener('ws:task_failed', onFailed as EventListener);
|
||||
window.addEventListener('ws:task_cancelled', onCancelled as EventListener);
|
||||
});
|
||||
}
|
||||
return res;
|
||||
})();
|
||||
|
||||
if (descriptionResponse.data.description) {
|
||||
// Enhance the question text with the AI description using display reference
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue