- Fix missing await on FocusGroup.get_messages() (N-L1) - Replace time.sleep with asyncio.sleep in key_theme_service and focus_group_service (N-P10) - Replace flask import with quart in focus_groups.py (N-S3) - Add logger.error before all 500 returns in focus_groups.py (N-P6) - Add logging to silent except blocks across routes (N-M10, N-M11) - Add @rate_limit to 6 remaining AI endpoints (N-H4) - Add --confirm flag to populate scripts before delete_many (S-H2) - Remove hardcoded Azure ID fallbacks from msal_service.py and msalConfig.ts (A-M2, F-H4) - Centralize make_serializable() in utils.py, remove duplicates from 3 route files (N-P7) - Replace all datetime.utcnow() with datetime.now(timezone.utc) across entire backend (M-L2) - AuthContext.tsx: only mark token validated on 200 success, not on non-401 errors (F-H2) - Rename authType → auth_type in auth.py (N-S4) - Add security_report.md and security_report.pdf with full 92-finding status Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1300 lines
No EOL
54 KiB
Python
Executable file
1300 lines
No EOL
54 KiB
Python
Executable file
"""
|
|
Focus Group AI Routes
|
|
These endpoints handle AI-assisted focus group functionality, including persona responses
|
|
and key theme generation.
|
|
"""
|
|
|
|
from quart import Blueprint, request, jsonify, current_app
|
|
from app.auth.quart_jwt import jwt_required, get_jwt_identity
|
|
from typing import Dict, List, Any
|
|
import time
|
|
import concurrent.futures
|
|
import asyncio
|
|
|
|
from app.services.focus_group_response_service import (
|
|
generate_persona_response,
|
|
generate_creative_review_response,
|
|
extract_asset_filename_from_content,
|
|
FocusGroupResponseError
|
|
)
|
|
from app.services.key_theme_service import (
|
|
KeyThemeService,
|
|
KeyThemeServiceError
|
|
)
|
|
from app.services.task_manager import CancellableTask
|
|
from app.services.ai_moderator_service import AIModeratorService
|
|
from app.services.autonomous_conversation_controller import AutonomousConversationController
|
|
from app.services.conversation_decision_service import ConversationDecisionService, ConversationDecisionError
|
|
from app.services.conversation_state_manager import ConversationStateManager
|
|
from app.services.ai_runner_service import get_ai_runner
|
|
from app.services.image_description_service import ImageDescriptionService, ImageDescriptionError
|
|
from app.models.focus_group import FocusGroup
|
|
from app.models.persona import Persona
|
|
from app.utils.rate_limiter import rate_limit
|
|
|
|
# Create the blueprint
|
|
focus_group_ai_bp = Blueprint('focus_group_ai', __name__)
|
|
|
|
|
|
def _user_key():
|
|
return f"{request.endpoint}:{get_jwt_identity()}"
|
|
|
|
|
|
@focus_group_ai_bp.route('/generate-response', methods=['POST'])
|
|
@jwt_required()
|
|
@rate_limit(max_requests=10, window_seconds=60, key_func=_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",
|
|
"persona_id": "persona_id",
|
|
"current_topic": "What do you think about this product?",
|
|
"temperature": 0.7, # Optional
|
|
"message_limit": 10 # Optional, number of previous messages to include
|
|
}
|
|
|
|
Returns:
|
|
A JSON object containing the generated response
|
|
"""
|
|
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",
|
|
"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)
|
|
|
|
# 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
|
|
try:
|
|
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
|
|
'length_instructions': _get_response_length_instructions(persona, recent_messages, current_topic),
|
|
'is_creative_review': True,
|
|
'creative_instructions': """
|
|
|
|
VISUAL CONTEXT AVAILABLE:
|
|
You are participating in a focus group discussion where visual materials have been shown. The images in your conversation context are part of the ongoing discussion. Please provide your authentic reaction and feedback based on your personality, background, and preferences, taking into account both the conversation history and any visual materials you can see.
|
|
|
|
Consider:
|
|
- Your first impression of any visuals shown
|
|
- 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
|
|
- The ongoing conversation context
|
|
|
|
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'],
|
|
temperature=temperature,
|
|
model_name=llm_model,
|
|
reasoning_effort=reasoning_effort if llm_model in ('gpt-5', 'gpt-5.2') else None,
|
|
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,
|
|
previous_messages=recent_messages,
|
|
temperature=temperature,
|
|
focus_group_id=focus_group_id,
|
|
llm_model=llm_model,
|
|
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}")
|
|
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
|
|
|
|
@focus_group_ai_bp.route('/generate-key-themes', methods=['POST'])
|
|
@jwt_required()
|
|
@rate_limit(max_requests=10, window_seconds=60, key_func=_user_key)
|
|
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
|
|
"""
|
|
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)
|
|
|
|
# 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
|
|
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
|
|
|
|
@focus_group_ai_bp.route('/key-themes/<focus_group_id>', methods=['GET'])
|
|
@jwt_required()
|
|
async def get_key_themes(focus_group_id):
|
|
"""
|
|
Get all generated key themes for a focus group.
|
|
|
|
Returns:
|
|
A JSON object containing the generated key themes
|
|
"""
|
|
try:
|
|
# 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 themes
|
|
themes = await FocusGroup.get_generated_themes(focus_group_id)
|
|
|
|
# Format themes for response
|
|
formatted_themes = []
|
|
for theme in themes:
|
|
formatted_themes.append({
|
|
"id": theme.get("id", ""),
|
|
"title": theme.get("title", ""),
|
|
"description": theme.get("description", ""),
|
|
"quotes": theme.get("quotes", []),
|
|
"source": theme.get("source", "generated")
|
|
})
|
|
|
|
return jsonify({
|
|
"message": "Key themes retrieved successfully",
|
|
"themes": formatted_themes,
|
|
"focus_group_id": focus_group_id
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Error getting key themes: {str(e)}")
|
|
return jsonify({
|
|
"error": "Failed to get key themes",
|
|
"message": str(e)
|
|
}), 500
|
|
|
|
@focus_group_ai_bp.route('/key-themes/<focus_group_id>/<theme_id>', methods=['DELETE'])
|
|
@jwt_required()
|
|
async def delete_key_theme(focus_group_id, theme_id):
|
|
"""
|
|
Delete a key theme from a focus group.
|
|
|
|
Returns:
|
|
A JSON object confirming the deletion
|
|
"""
|
|
try:
|
|
# 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
|
|
|
|
# Delete theme
|
|
success = await FocusGroup.delete_generated_theme(focus_group_id, theme_id)
|
|
|
|
if not success:
|
|
return jsonify({
|
|
"error": "Failed to delete theme",
|
|
"message": "Theme not found or could not be deleted"
|
|
}), 404
|
|
|
|
return jsonify({
|
|
"message": "Theme deleted successfully",
|
|
"theme_id": theme_id,
|
|
"focus_group_id": focus_group_id
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Error deleting key theme: {str(e)}")
|
|
return jsonify({
|
|
"error": "Failed to delete key theme",
|
|
"message": str(e)
|
|
}), 500
|
|
|
|
@focus_group_ai_bp.route('/moderator/status/<focus_group_id>', methods=['GET'])
|
|
@jwt_required()
|
|
async def get_moderator_status(focus_group_id):
|
|
"""
|
|
Get the current moderator status for a focus group.
|
|
|
|
Returns:
|
|
A JSON object containing the current moderator status
|
|
"""
|
|
try:
|
|
status = await AIModeratorService.get_moderator_status(focus_group_id)
|
|
|
|
if "error" in status:
|
|
return jsonify(status), 404 if "not found" in status["error"] else 500
|
|
|
|
return jsonify({
|
|
"message": "Moderator status retrieved successfully",
|
|
"status": status,
|
|
"focus_group_id": focus_group_id
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Error getting moderator status: {str(e)}")
|
|
return jsonify({
|
|
"error": "Failed to get moderator status",
|
|
"message": str(e)
|
|
}), 500
|
|
|
|
@focus_group_ai_bp.route('/moderator/advance/<focus_group_id>', methods=['POST'])
|
|
@jwt_required()
|
|
@rate_limit(max_requests=10, window_seconds=60, key_func=_user_key)
|
|
async def advance_moderator_discussion(focus_group_id):
|
|
"""
|
|
Advance the moderator to the next item in the discussion guide.
|
|
For manual mode, also use AI to decide which participant should respond next.
|
|
|
|
Request body (optional):
|
|
{
|
|
"generate_participant_response": true, # Default: true for manual mode, false for autonomous mode
|
|
"temperature": 0.7
|
|
}
|
|
|
|
Returns:
|
|
A JSON object containing the moderator response, updated position, and optionally participant response
|
|
"""
|
|
try:
|
|
data = (await request.get_json()) or {}
|
|
temperature = data.get('temperature', 0.7)
|
|
|
|
# Check if focus group is in autonomous mode
|
|
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 for this focus group
|
|
llm_model = focus_group.get('llm_model')
|
|
|
|
is_autonomous_mode = focus_group.get('status', '').startswith('autonomous_')
|
|
|
|
# Default: generate participant response for manual mode, not for autonomous mode
|
|
generate_participant_response = data.get('generate_participant_response', not is_autonomous_mode)
|
|
|
|
result = await AIModeratorService.advance_discussion(focus_group_id)
|
|
|
|
if "error" in result:
|
|
return jsonify(result), 404 if "not found" in result["error"] else 500
|
|
|
|
# If we have a moderator response, save it as a message
|
|
if result.get("moderator_response") and not result.get("completed", False):
|
|
# Check if this is a creative review activity that needs visual asset activation
|
|
attached_assets = []
|
|
activates_visual_context = False
|
|
|
|
current_item = result.get("current_item")
|
|
if current_item:
|
|
# Try to get asset info from metadata (new metadata-driven approach)
|
|
asset_filename = None
|
|
display_reference = None
|
|
|
|
metadata = current_item.get('metadata', {})
|
|
visual_asset = metadata.get('visual_asset')
|
|
|
|
if visual_asset:
|
|
# Use metadata (preferred method)
|
|
asset_filename = visual_asset.get('filename')
|
|
display_reference = visual_asset.get('display_reference')
|
|
print(f"🎨 Found asset metadata: {display_reference} -> {asset_filename}")
|
|
else:
|
|
# Fallback to content parsing (legacy support)
|
|
activity_content = current_item.get("content", "")
|
|
asset_filename = extract_asset_filename_from_content(activity_content)
|
|
print(f"🎨 Legacy asset filename extraction: {asset_filename}")
|
|
|
|
if asset_filename:
|
|
print(f"🎨 ADVANCE DISCUSSION: Item with image detected (type: {current_item.get('type')})")
|
|
print(f"🎨 Asset: {display_reference or 'legacy'} -> {asset_filename}")
|
|
|
|
if asset_filename:
|
|
attached_assets = [asset_filename]
|
|
|
|
# Generate AI description and enhance the moderator response
|
|
try:
|
|
print(f"🎨 AI MODE: Generating description for {asset_filename}")
|
|
description = await ImageDescriptionService.generate_description(focus_group_id, asset_filename)
|
|
|
|
# Enhance the moderator response with the description using display reference if available
|
|
if display_reference:
|
|
enhanced_response = ImageDescriptionService.enhance_creative_review_question_with_display_reference(
|
|
result["moderator_response"], display_reference, description
|
|
)
|
|
else:
|
|
# Fallback to old method for legacy content
|
|
enhanced_response = ImageDescriptionService.enhance_creative_review_question(
|
|
result["moderator_response"], asset_filename, description
|
|
)
|
|
|
|
# Update the result with enhanced response
|
|
result["moderator_response"] = enhanced_response
|
|
result["ai_description_generated"] = True
|
|
result["ai_description"] = description
|
|
|
|
print(f"✅ AI MODE: Enhanced moderator response with image description")
|
|
|
|
except ImageDescriptionError as desc_error:
|
|
print(f"⚠️ AI MODE: Failed to generate image description: {desc_error}")
|
|
result["ai_description_error"] = str(desc_error)
|
|
result["ai_description_generated"] = False
|
|
# Continue with original response
|
|
|
|
activates_visual_context = True
|
|
print(f"🎨 ADVANCE DISCUSSION: Will activate visual context for asset: {asset_filename}")
|
|
|
|
# Create visual asset metadata for frontend display
|
|
visual_asset_metadata = None
|
|
if activates_visual_context and attached_assets and len(attached_assets) > 0:
|
|
# Create visual asset metadata that frontend expects
|
|
visual_asset_metadata = {
|
|
"filename": attached_assets[0], # Use first asset
|
|
"displayReference": display_reference or attached_assets[0] # Use display reference or filename as fallback
|
|
}
|
|
|
|
message_data = {
|
|
"text": result["moderator_response"],
|
|
"type": "question",
|
|
"senderId": "moderator",
|
|
"attached_assets": attached_assets,
|
|
"activates_visual_context": activates_visual_context,
|
|
"visual_asset": visual_asset_metadata # Frontend needs this for image display
|
|
}
|
|
|
|
message_id = FocusGroup.add_message(focus_group_id, message_data)
|
|
|
|
# Visual context activation is handled automatically by FocusGroup.add_message()
|
|
# when activates_visual_context=True and attached_assets are present
|
|
if activates_visual_context and attached_assets:
|
|
print(f"✅ ADVANCE DISCUSSION: Visual context activated for {attached_assets}")
|
|
|
|
if message_id:
|
|
result["message_id"] = message_id
|
|
else:
|
|
current_app.logger.warning("Failed to save moderator message to database")
|
|
|
|
# For manual mode, also generate AI participant response
|
|
if generate_participant_response and not result.get("completed", False):
|
|
try:
|
|
# Use AI to decide which participant should respond next
|
|
decision = await ConversationDecisionService.decide_next_action(focus_group_id, temperature, 'ai')
|
|
|
|
if decision.get('action') == 'participant_respond':
|
|
participant_id = decision['details']['participant_id']
|
|
topic_context = decision['details']['topic_context']
|
|
|
|
# Get participant data
|
|
persona = await Persona.find_by_id(participant_id)
|
|
if persona:
|
|
# Get recent messages for context
|
|
messages = await FocusGroup.get_messages(focus_group_id)
|
|
recent_messages = messages[-20:] if len(messages) > 20 else messages
|
|
|
|
# Generate participant response
|
|
response_text = await generate_persona_response(
|
|
persona=persona,
|
|
current_topic=topic_context,
|
|
previous_messages=recent_messages,
|
|
temperature=temperature,
|
|
focus_group_id=focus_group_id,
|
|
llm_model=llm_model
|
|
)
|
|
|
|
# Save participant message
|
|
participant_message_data = {
|
|
"text": response_text,
|
|
"type": "response",
|
|
"senderId": participant_id
|
|
}
|
|
|
|
participant_message_id = FocusGroup.add_message(focus_group_id, participant_message_data)
|
|
|
|
# Add participant response info to result
|
|
result["participant_response"] = {
|
|
"participant_id": participant_id,
|
|
"response": response_text,
|
|
"message_id": participant_message_id,
|
|
"ai_decision": decision
|
|
}
|
|
else:
|
|
current_app.logger.warning(f"Participant {participant_id} not found for response generation")
|
|
else:
|
|
# AI decided on a different action
|
|
result["ai_decision"] = decision
|
|
current_app.logger.info(f"AI suggested action '{decision.get('action')}' instead of participant response")
|
|
|
|
except (ConversationDecisionError, FocusGroupResponseError) as e:
|
|
current_app.logger.warning(f"Could not generate participant response: {str(e)}")
|
|
result["participant_response_error"] = str(e)
|
|
except Exception as e:
|
|
current_app.logger.error(f"Unexpected error generating participant response: {str(e)}")
|
|
result["participant_response_error"] = f"Unexpected error: {str(e)}"
|
|
|
|
return jsonify(result), 200
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Error advancing moderator discussion: {str(e)}")
|
|
return jsonify({
|
|
"error": "Failed to advance discussion",
|
|
"message": str(e)
|
|
}), 500
|
|
|
|
@focus_group_ai_bp.route('/moderator/position/<focus_group_id>', methods=['PUT'])
|
|
@jwt_required()
|
|
async def set_moderator_position(focus_group_id):
|
|
"""
|
|
Set the moderator position to a specific section and item.
|
|
|
|
Request body:
|
|
{
|
|
"section_id": "section_id",
|
|
"item_id": "item_id" # Optional
|
|
}
|
|
|
|
Returns:
|
|
A JSON object confirming the position change
|
|
"""
|
|
try:
|
|
data = (await request.get_json()) or {}
|
|
|
|
# Validate required fields
|
|
if 'section_id' not in data:
|
|
return jsonify({
|
|
"error": "Missing required field: section_id"
|
|
}), 400
|
|
|
|
section_id = data['section_id']
|
|
item_id = data.get('item_id')
|
|
|
|
result = await AIModeratorService.set_moderator_position(focus_group_id, section_id, item_id)
|
|
|
|
if "error" in result:
|
|
return jsonify(result), 404 if "not found" in result["error"] else 400
|
|
|
|
# WebSocket event is already emitted by AIModeratorService.set_moderator_position()
|
|
# No need to emit duplicate event here
|
|
|
|
return jsonify(result), 200
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Error setting moderator position: {str(e)}")
|
|
return jsonify({
|
|
"error": "Failed to set moderator position",
|
|
"message": str(e)
|
|
}), 500
|
|
|
|
@focus_group_ai_bp.route('/autonomous/start/<focus_group_id>', methods=['POST'])
|
|
@jwt_required()
|
|
@rate_limit(max_requests=10, window_seconds=60, key_func=_user_key)
|
|
async def start_autonomous_conversation(focus_group_id):
|
|
"""
|
|
Start autonomous conversation for a focus group.
|
|
|
|
Request body:
|
|
{
|
|
"initial_prompt": "Optional initial prompt to start the conversation"
|
|
}
|
|
|
|
Returns:
|
|
A JSON object containing the start result
|
|
"""
|
|
try:
|
|
current_app.logger.info(f"=== START AUTONOMOUS CONVERSATION API called for focus group {focus_group_id} ===")
|
|
data = (await request.get_json()) or {}
|
|
initial_prompt = data.get('initial_prompt')
|
|
current_app.logger.info(f"Request data: {data}")
|
|
|
|
# Create autonomous conversation controller
|
|
current_app.logger.info("Creating AutonomousConversationController...")
|
|
controller = AutonomousConversationController(focus_group_id, current_app.logger)
|
|
current_app.logger.info("Controller created successfully")
|
|
|
|
current_app.logger.info("Preparing to submit conversation to AI Runner...")
|
|
|
|
# Get the AI Runner service and submit the conversation
|
|
ai_runner = get_ai_runner()
|
|
if not ai_runner.is_running:
|
|
current_app.logger.error("AI Runner service is not running")
|
|
return jsonify({"error": "AI Runner service is not available"}), 503
|
|
|
|
# Submit the conversation to the AI Runner (non-blocking)
|
|
current_app.logger.info("Submitting conversation to AI Runner...")
|
|
try:
|
|
future = ai_runner.submit_conversation(
|
|
focus_group_id,
|
|
controller.start_autonomous_conversation(initial_prompt)
|
|
)
|
|
current_app.logger.info("Conversation submitted to AI Runner successfully")
|
|
except Exception as e:
|
|
current_app.logger.error(f"Failed to submit conversation to AI Runner: {e}")
|
|
return jsonify({"error": f"Failed to start AI conversation: {str(e)}"}), 500
|
|
|
|
# Log the AI mode start event
|
|
try:
|
|
user_id = get_jwt_identity() # Get user ID if available
|
|
mode_event_id = await FocusGroup.add_mode_event(focus_group_id, 'ai_mode_started', user_id)
|
|
current_app.logger.info(f"Logged AI mode start event: {mode_event_id}")
|
|
except Exception as e:
|
|
current_app.logger.warning(f"Failed to log AI mode start event: {e}")
|
|
|
|
# Return immediately with a success response
|
|
result = {
|
|
"message": "Autonomous conversation started",
|
|
"focus_group_id": focus_group_id,
|
|
"state": "starting",
|
|
"background": True,
|
|
"ai_runner_active": True
|
|
}
|
|
current_app.logger.info(f"Controller returned result: {result}")
|
|
|
|
if "error" in result:
|
|
current_app.logger.warning(f"Controller returned error: {result}")
|
|
return jsonify(result), 400
|
|
|
|
current_app.logger.info(f"Returning success response: {result}")
|
|
return jsonify(result), 200
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Exception in start_autonomous_conversation: {str(e)}")
|
|
current_app.logger.exception("Full exception traceback:")
|
|
return jsonify({
|
|
"error": "Failed to start autonomous conversation",
|
|
"message": str(e)
|
|
}), 500
|
|
|
|
|
|
|
|
@focus_group_ai_bp.route('/autonomous/stop/<focus_group_id>', methods=['POST'])
|
|
@jwt_required()
|
|
async def stop_autonomous_conversation(focus_group_id):
|
|
"""
|
|
Stop autonomous conversation for a focus group.
|
|
|
|
Request body:
|
|
{
|
|
"reason": "Optional reason for stopping (default: manual_stop)"
|
|
}
|
|
|
|
Returns:
|
|
A JSON object containing the stop result
|
|
"""
|
|
try:
|
|
data = (await request.get_json()) or {}
|
|
reason = data.get('reason', 'manual_stop')
|
|
|
|
current_app.logger.info(f"=== STOP AUTONOMOUS CONVERSATION API called for focus group {focus_group_id} ===")
|
|
current_app.logger.info(f"Stop reason: {reason}")
|
|
|
|
# Create autonomous conversation controller
|
|
# Use AI Runner to stop the conversation
|
|
current_app.logger.info("Requesting AI Runner to stop conversation...")
|
|
ai_runner = get_ai_runner()
|
|
|
|
if ai_runner.is_running:
|
|
success = ai_runner.stop_conversation(focus_group_id)
|
|
if success:
|
|
current_app.logger.info(f"Successfully requested stop for focus group {focus_group_id}")
|
|
else:
|
|
current_app.logger.warning(f"No active conversation found for focus group {focus_group_id}")
|
|
else:
|
|
current_app.logger.warning("AI Runner is not running, cannot stop conversation")
|
|
|
|
# Update focus group status in database
|
|
from datetime import datetime, timezone
|
|
status = 'completed' if reason in ['completed', 'discussion_guide_completed', 'natural_completion'] else 'active'
|
|
await FocusGroup.update(focus_group_id, {
|
|
'status': status,
|
|
'autonomous_ended_at': datetime.now(timezone.utc),
|
|
'completion_reason': reason
|
|
})
|
|
|
|
current_app.logger.info(f"Signaled autonomous conversation to stop for focus group {focus_group_id}: {reason}")
|
|
|
|
# Add mode event for AI session concluded (regardless of reason)
|
|
user_id = get_jwt_identity() if get_jwt_identity() else None
|
|
mode_event_id = await FocusGroup.add_mode_event(focus_group_id, 'ai_session_concluded', user_id)
|
|
if mode_event_id:
|
|
current_app.logger.info(f"🎯 Added AI session concluded mode event: {mode_event_id}")
|
|
else:
|
|
current_app.logger.warning(f"Failed to add AI session concluded mode event for focus group {focus_group_id}")
|
|
|
|
# Return immediately with a success response like start_autonomous_conversation
|
|
result = {
|
|
"message": "Autonomous conversation stopping",
|
|
"focus_group_id": focus_group_id,
|
|
"state": "stopping",
|
|
"background": True
|
|
}
|
|
current_app.logger.info(f"Returning success response for stop request: {result}")
|
|
|
|
return jsonify(result), 200
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Error stopping autonomous conversation: {str(e)}")
|
|
return jsonify({
|
|
"error": "Failed to stop autonomous conversation",
|
|
"message": str(e)
|
|
}), 500
|
|
|
|
@focus_group_ai_bp.route('/autonomous/status/<focus_group_id>', methods=['GET'])
|
|
@jwt_required()
|
|
async def get_autonomous_conversation_status(focus_group_id):
|
|
"""
|
|
Get the status of autonomous conversation for a focus group.
|
|
|
|
Returns:
|
|
A JSON object containing the conversation status
|
|
"""
|
|
try:
|
|
# Create autonomous conversation controller
|
|
controller = AutonomousConversationController(focus_group_id, current_app.logger)
|
|
|
|
# Get status
|
|
status = await controller.get_conversation_status()
|
|
|
|
return jsonify({
|
|
"message": "Autonomous conversation status retrieved",
|
|
"status": status,
|
|
"focus_group_id": focus_group_id
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Error getting autonomous conversation status: {str(e)}")
|
|
return jsonify({
|
|
"error": "Failed to get autonomous conversation status",
|
|
"message": str(e)
|
|
}), 500
|
|
|
|
@focus_group_ai_bp.route('/conversation/state/<focus_group_id>', methods=['GET'])
|
|
@jwt_required()
|
|
async def get_conversation_state(focus_group_id):
|
|
"""
|
|
Get the current conversation state for a focus group.
|
|
|
|
Returns:
|
|
A JSON object containing the conversation state
|
|
"""
|
|
try:
|
|
# Create conversation state manager
|
|
state_manager = ConversationStateManager(focus_group_id)
|
|
|
|
# Get state
|
|
state = await state_manager.get_conversation_state()
|
|
|
|
if "error" in state:
|
|
return jsonify(state), 404 if "not found" in state["error"] else 500
|
|
|
|
return jsonify({
|
|
"message": "Conversation state retrieved",
|
|
"state": state,
|
|
"focus_group_id": focus_group_id
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Error getting conversation state: {str(e)}")
|
|
return jsonify({
|
|
"error": "Failed to get conversation state",
|
|
"message": str(e)
|
|
}), 500
|
|
|
|
@focus_group_ai_bp.route('/conversation/analytics/<focus_group_id>', methods=['GET'])
|
|
@jwt_required()
|
|
async def get_conversation_analytics(focus_group_id):
|
|
"""
|
|
Get detailed conversation analytics for a focus group.
|
|
|
|
Returns:
|
|
A JSON object containing conversation analytics
|
|
"""
|
|
try:
|
|
# Create conversation state manager
|
|
state_manager = ConversationStateManager(focus_group_id)
|
|
|
|
# Get analytics
|
|
analytics = await state_manager.get_conversation_analytics()
|
|
|
|
if "error" in analytics:
|
|
return jsonify(analytics), 404 if "not found" in analytics["error"] else 500
|
|
|
|
return jsonify({
|
|
"message": "Conversation analytics retrieved",
|
|
"analytics": analytics,
|
|
"focus_group_id": focus_group_id
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Error getting conversation analytics: {str(e)}")
|
|
return jsonify({
|
|
"error": "Failed to get conversation analytics",
|
|
"message": str(e)
|
|
}), 500
|
|
|
|
@focus_group_ai_bp.route('/conversation/decision/<focus_group_id>', methods=['POST'])
|
|
@jwt_required()
|
|
@rate_limit(max_requests=10, window_seconds=60, key_func=_user_key)
|
|
async def make_conversation_decision(focus_group_id):
|
|
"""
|
|
Make a conversation decision using the LLM decision engine.
|
|
|
|
Request body:
|
|
{
|
|
"temperature": 0.7, # Optional, defaults to 0.7
|
|
"mode": "ai" # Optional, "ai" for autonomous mode, "manual" for manual mode, defaults to "ai"
|
|
}
|
|
|
|
Returns:
|
|
A JSON object containing the decision
|
|
"""
|
|
try:
|
|
data = (await request.get_json()) or {}
|
|
temperature = data.get('temperature', 0.7)
|
|
mode = data.get('mode', 'ai') # Default to 'ai' mode for backward compatibility
|
|
|
|
# Make decision
|
|
decision = await ConversationDecisionService.decide_next_action(focus_group_id, temperature, mode)
|
|
|
|
response_data = {
|
|
"message": "Conversation decision made",
|
|
"decision": decision,
|
|
"focus_group_id": focus_group_id
|
|
}
|
|
|
|
return jsonify(response_data), 200
|
|
|
|
except ConversationDecisionError as e:
|
|
current_app.logger.error(f"Error making conversation decision: {str(e)}")
|
|
return jsonify({
|
|
"error": "Failed to make conversation decision",
|
|
"message": str(e)
|
|
}), 500
|
|
except Exception as e:
|
|
current_app.logger.error(f"Unexpected error making conversation decision: {str(e)}")
|
|
return jsonify({
|
|
"error": "Failed to make conversation decision",
|
|
"message": str(e)
|
|
}), 500
|
|
|
|
@focus_group_ai_bp.route('/conversation/insights/<focus_group_id>', methods=['GET'])
|
|
@jwt_required()
|
|
async def get_conversation_insights(focus_group_id):
|
|
"""
|
|
Get LLM-generated insights about the conversation.
|
|
|
|
Returns:
|
|
A JSON object containing conversation insights
|
|
"""
|
|
try:
|
|
# Get insights
|
|
insights = await ConversationDecisionService.get_conversation_insights(focus_group_id)
|
|
|
|
return jsonify({
|
|
"message": "Conversation insights generated",
|
|
"insights": insights,
|
|
"focus_group_id": focus_group_id
|
|
}), 200
|
|
|
|
except ConversationDecisionError as e:
|
|
current_app.logger.error(f"Error getting conversation insights: {str(e)}")
|
|
return jsonify({
|
|
"error": "Failed to get conversation insights",
|
|
"message": str(e)
|
|
}), 500
|
|
except Exception as e:
|
|
current_app.logger.error(f"Unexpected error getting conversation insights: {str(e)}")
|
|
return jsonify({
|
|
"error": "Failed to get conversation insights",
|
|
"message": str(e)
|
|
}), 500
|
|
|
|
@focus_group_ai_bp.route('/conversation/intervene/<focus_group_id>', methods=['POST'])
|
|
@jwt_required()
|
|
@rate_limit(max_requests=10, window_seconds=60, key_func=_user_key)
|
|
async def manual_intervention(focus_group_id):
|
|
"""
|
|
Manually intervene in autonomous conversation.
|
|
|
|
Request body:
|
|
{
|
|
"action": "pause" | "resume" | "redirect" | "call_participant",
|
|
"message": "Optional message to add",
|
|
"participant_id": "Optional participant to call on"
|
|
}
|
|
|
|
Returns:
|
|
A JSON object containing the intervention result
|
|
"""
|
|
try:
|
|
data = (await request.get_json()) or {}
|
|
action = data.get('action', 'pause')
|
|
message = data.get('message')
|
|
participant_id = data.get('participant_id')
|
|
|
|
# Create state manager
|
|
state_manager = ConversationStateManager(focus_group_id)
|
|
|
|
result = None
|
|
|
|
if action == 'pause':
|
|
result = state_manager.pause_autonomous_mode()
|
|
elif action == 'resume':
|
|
result = state_manager.resume_autonomous_mode()
|
|
elif action == 'redirect' and message:
|
|
# Add moderator message
|
|
FocusGroup.add_message(focus_group_id, {
|
|
"text": message,
|
|
"type": "question",
|
|
"senderId": "moderator"
|
|
})
|
|
result = {"message": "Moderator message added"}
|
|
elif action == 'call_participant' and participant_id:
|
|
# Add moderator message calling on specific participant
|
|
persona = await Persona.find_by_id(participant_id)
|
|
if persona:
|
|
call_message = f"{persona.get('name', 'Participant')}, what are your thoughts on this?"
|
|
FocusGroup.add_message(focus_group_id, {
|
|
"text": call_message,
|
|
"type": "question",
|
|
"senderId": "moderator"
|
|
})
|
|
result = {"message": f"Called on {persona.get('name', 'participant')}"}
|
|
else:
|
|
result = {"error": "Participant not found"}
|
|
else:
|
|
result = {"error": "Invalid action or missing parameters"}
|
|
|
|
if result and "error" not in result:
|
|
return jsonify({
|
|
"message": "Manual intervention successful",
|
|
"action": action,
|
|
"result": result,
|
|
"focus_group_id": focus_group_id
|
|
}), 200
|
|
else:
|
|
return jsonify(result), 400
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Error in manual intervention: {str(e)}")
|
|
return jsonify({
|
|
"error": "Failed to perform manual intervention",
|
|
"message": str(e)
|
|
}), 500
|
|
|
|
@focus_group_ai_bp.route('/conversation/reasoning-history/<focus_group_id>', methods=['GET'])
|
|
@jwt_required()
|
|
async def get_reasoning_history(focus_group_id):
|
|
"""
|
|
Get the AI reasoning history for an autonomous conversation.
|
|
|
|
Returns:
|
|
A JSON object containing the reasoning history
|
|
"""
|
|
try:
|
|
# Create autonomous conversation controller to get reasoning history
|
|
controller = AutonomousConversationController(focus_group_id)
|
|
status = await controller.get_conversation_status()
|
|
|
|
reasoning_history = status.get('reasoning_history', [])
|
|
|
|
return jsonify({
|
|
"message": "Reasoning history retrieved successfully",
|
|
"reasoning_history": reasoning_history,
|
|
"focus_group_id": focus_group_id,
|
|
"total_decisions": len(reasoning_history)
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Error getting reasoning history: {str(e)}")
|
|
return jsonify({
|
|
"error": "Failed to get reasoning history",
|
|
"message": str(e)
|
|
}), 500
|
|
|
|
@focus_group_ai_bp.route('/moderator/end-session/<focus_group_id>', methods=['POST'])
|
|
@jwt_required()
|
|
@rate_limit(max_requests=10, window_seconds=60, key_func=_user_key)
|
|
async def end_focus_group_session(focus_group_id):
|
|
"""
|
|
End a focus group session with a concluding moderator statement.
|
|
|
|
Request body (optional):
|
|
{
|
|
"reason": "manual_stop" | "auto_complete" | "timeout" | "session_ended"
|
|
}
|
|
|
|
Returns:
|
|
A JSON object containing the concluding statement and session end confirmation
|
|
"""
|
|
try:
|
|
current_app.logger.info(f"=== END FOCUS GROUP SESSION API called for focus group {focus_group_id} ===")
|
|
|
|
data = (await request.get_json()) or {}
|
|
reason = data.get('reason', 'session_ended')
|
|
|
|
current_app.logger.info(f"Session ending reason: {reason}")
|
|
|
|
# Validate focus group exists
|
|
focus_group = await FocusGroup.find_by_id(focus_group_id)
|
|
if not focus_group:
|
|
current_app.logger.warning(f"Focus group not found: {focus_group_id}")
|
|
return jsonify({"error": "Focus group not found"}), 404
|
|
|
|
current_app.logger.info(f"Focus group found: {focus_group.get('name', 'Unnamed')}")
|
|
|
|
# End the session with concluding statement
|
|
result = await AIModeratorService.end_session_with_concluding_statement(focus_group_id, reason)
|
|
|
|
if "error" in result:
|
|
current_app.logger.error(f"Error ending session: {result['error']}")
|
|
return jsonify(result), 400 if "not found" in result["error"] else 500
|
|
|
|
current_app.logger.info(f"Session ended successfully with concluding statement")
|
|
current_app.logger.info(f"Concluding statement: {result.get('concluding_statement', '')[:100]}...")
|
|
|
|
return jsonify(result), 200
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Unexpected error ending focus group session: {str(e)}")
|
|
return jsonify({
|
|
"error": "Failed to end session",
|
|
"message": str(e)
|
|
}), 500
|
|
|
|
# Helper functions for the new contextual response system
|
|
|
|
def _format_persona_details_for_context(persona: Dict[str, Any]) -> str:
|
|
"""Format persona details for the prompt context."""
|
|
details = []
|
|
|
|
# Basic demographics
|
|
details.append(f"Name: {persona.get('name', 'Unknown')}")
|
|
details.append(f"Age: {persona.get('age', 'Unknown')}")
|
|
details.append(f"Gender: {persona.get('gender', 'Unknown')}")
|
|
details.append(f"Occupation: {persona.get('occupation', 'Unknown')}")
|
|
details.append(f"Education: {persona.get('education', 'Unknown')}")
|
|
details.append(f"Location: {persona.get('location', 'Unknown')}")
|
|
|
|
# Personality characteristics
|
|
details.append(f"Personality: {persona.get('personality', 'Not specified')}")
|
|
|
|
# OCEAN traits if available
|
|
ocean = persona.get('oceanTraits', {})
|
|
if ocean:
|
|
traits = []
|
|
if 'openness' in ocean:
|
|
traits.append(f"Openness: {ocean['openness']}/100")
|
|
if 'conscientiousness' in ocean:
|
|
traits.append(f"Conscientiousness: {ocean['conscientiousness']}/100")
|
|
if 'extraversion' in ocean:
|
|
traits.append(f"Extraversion: {ocean['extraversion']}/100")
|
|
if 'agreeableness' in ocean:
|
|
traits.append(f"Agreeableness: {ocean['agreeableness']}/100")
|
|
if 'neuroticism' in ocean:
|
|
traits.append(f"Neuroticism: {ocean['neuroticism']}/100")
|
|
|
|
if traits:
|
|
details.append("OCEAN Traits:")
|
|
details.extend([f"- {trait}" for trait in traits])
|
|
|
|
# Goals, frustrations, motivations
|
|
if 'goals' in persona and persona['goals']:
|
|
details.append("Goals:")
|
|
details.extend([f"- {goal}" for goal in persona['goals']])
|
|
|
|
if 'frustrations' in persona and persona['frustrations']:
|
|
details.append("Frustrations:")
|
|
details.extend([f"- {frustration}" for frustration in persona['frustrations']])
|
|
|
|
if 'motivations' in persona and persona['motivations']:
|
|
details.append("Motivations:")
|
|
details.extend([f"- {motivation}" for motivation in persona['motivations']])
|
|
|
|
# Think, feel, do
|
|
tfd = persona.get('thinkFeelDo', {})
|
|
if tfd:
|
|
if 'thinks' in tfd and tfd['thinks']:
|
|
details.append("Thinks:")
|
|
details.extend([f"- {thought}" for thought in tfd['thinks']])
|
|
|
|
if 'feels' in tfd and tfd['feels']:
|
|
details.append("Feels:")
|
|
details.extend([f"- {feeling}" for feeling in tfd['feels']])
|
|
|
|
if 'does' in tfd and tfd['does']:
|
|
details.append("Does:")
|
|
details.extend([f"- {action}" for action in tfd['does']])
|
|
|
|
# Join all details with line breaks
|
|
return "\n".join(details)
|
|
|
|
def _get_response_length_instructions(persona: Dict[str, Any], previous_messages: List[Dict[str, Any]], current_topic: str) -> str:
|
|
"""Get response length instructions based on persona and context."""
|
|
# Import the helper from focus_group_response_service
|
|
from app.services.focus_group_response_service import _determine_response_length_preference, _get_length_specific_instructions
|
|
|
|
length_preference = _determine_response_length_preference(persona, previous_messages, current_topic)
|
|
return _get_length_specific_instructions(length_preference) |