added gpt-4.1 support among other things

This commit is contained in:
michael 2025-08-05 17:38:13 -05:00
parent da7b2c0448
commit b649793013
42 changed files with 1741 additions and 928 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
backend/.DS_Store vendored

Binary file not shown.

BIN
backend/app/.DS_Store vendored

Binary file not shown.

View file

@ -18,6 +18,10 @@ class FocusGroup:
if "status" not in focus_group_data:
focus_group_data["status"] = "new"
# Set default LLM model if not provided
if "llm_model" not in focus_group_data:
focus_group_data["llm_model"] = "gemini-2.5-pro"
result = db.focus_groups.insert_one(focus_group_data)
return str(result.inserted_id)
@ -97,11 +101,43 @@ class FocusGroup:
# Set the updated timestamp
filtered_data["updated_at"] = datetime.utcnow()
# Debug logging for llm_model updates (force to file)
if 'llm_model' in filtered_data:
try:
log_msg = f"🔧 [{datetime.utcnow()}] FOCUS GROUP MODEL UPDATE: Setting llm_model to '{filtered_data['llm_model']}' for focus group {focus_group_id}\n"
with open('/tmp/focus_group_debug.log', 'a') as f:
f.write(log_msg)
f.flush()
print(f"🔧 FOCUS GROUP UPDATE: Setting llm_model to '{filtered_data['llm_model']}' for focus group {focus_group_id}")
except:
pass
result = db.focus_groups.update_one(
{"_id": ObjectId(focus_group_id)},
{"$set": filtered_data}
)
# Debug: Verify the update worked (force to file)
if 'llm_model' in filtered_data and result.modified_count > 0:
try:
# Re-read the document to verify the update
updated_doc = db.focus_groups.find_one({"_id": ObjectId(focus_group_id)})
actual_model = updated_doc.get('llm_model') if updated_doc else None
log_msg = f"🔍 [{datetime.utcnow()}] POST-UPDATE VERIFICATION: Expected '{filtered_data['llm_model']}', got '{actual_model}' for {focus_group_id}\n"
with open('/tmp/focus_group_debug.log', 'a') as f:
f.write(log_msg)
f.flush()
print(f"🔍 POST-UPDATE VERIFICATION: Expected '{filtered_data['llm_model']}', got '{actual_model}' for {focus_group_id}")
except Exception as e:
try:
log_msg = f"🔍 [{datetime.utcnow()}] POST-UPDATE VERIFICATION FAILED: {e}\n"
with open('/tmp/focus_group_debug.log', 'a') as f:
f.write(log_msg)
f.flush()
except:
pass
print(f"🔍 POST-UPDATE VERIFICATION FAILED: {e}")
return result.modified_count > 0
@staticmethod

View file

@ -66,19 +66,28 @@ def generate_basic_profiles():
temperature = 0.8
customer_data_session_id = data.get('customer_data_session_id') # Optional parameter
llm_model = data.get('llm_model', 'gemini-2.5-pro') # Optional parameter with default
try:
# Log the request with model information
print(f"🔄 Backend: Received generate-basic-profiles request with model: {llm_model}")
current_app.logger.info(f"Generating {count} basic profiles using model: {llm_model}")
# Generate basic profiles
basic_profiles = generate_basic_personas(
audience_brief=audience_brief,
research_objective=research_objective,
count=count,
temperature=temperature,
customer_data_session_id=customer_data_session_id
customer_data_session_id=customer_data_session_id,
llm_model=llm_model
)
# Log successful generation
print(f"✅ Backend: Successfully generated {len(basic_profiles)} basic profiles using model: {llm_model}")
return jsonify({
"message": f"Successfully generated {len(basic_profiles)} basic persona profiles",
"message": f"Successfully generated {len(basic_profiles)} basic persona profiles using {llm_model}",
"profiles": basic_profiles
}), 200
@ -181,20 +190,28 @@ def complete_and_save_persona():
temperature = 0.7
customer_data_session_id = data.get('customer_data_session_id') # Optional parameter
llm_model = data.get('llm_model', 'gemini-2.5-pro') # Optional parameter with default
try:
# Log the request with model information
persona_name = basic_profile.get('name', 'Unknown')
print(f"🔄 Backend: Received complete-and-save-persona request for '{persona_name}' with model: {llm_model}")
current_app.logger.info(f"Completing persona '{persona_name}' using model: {llm_model}")
# Complete the persona
complete_persona_data = generate_persona(
basic_persona=basic_profile,
temperature=temperature,
customer_data_session_id=customer_data_session_id
customer_data_session_id=customer_data_session_id,
llm_model=llm_model
)
# Generate AI summary for the persona
try:
summary_data = generate_persona_summary(
persona_data=complete_persona_data,
temperature=temperature
temperature=temperature,
llm_model=llm_model
)
# Add summary fields to the persona data
@ -219,8 +236,11 @@ def complete_and_save_persona():
# Add the database ID to the response
complete_persona_data['_id'] = str(persona_id)
# Log successful completion
print(f"✅ Backend: Successfully completed and saved persona '{persona_name}' using model: {llm_model}")
return jsonify({
"message": "Successfully completed and saved persona",
"message": f"Successfully completed and saved persona using {llm_model}",
"persona": complete_persona_data,
"persona_id": str(persona_id)
}), 201
@ -719,7 +739,7 @@ def batch_generate_summaries():
This endpoint takes a list of persona IDs, fetches their complete data, and generates
detailed summaries using LLM processing. Personas are processed in parallel batches of 10
to optimize performance while staying within API limits.
to optimize performance while staying within API limits. No upper limit on persona count.
Request body:
{
@ -736,18 +756,23 @@ def batch_generate_summaries():
# Extract parameters
persona_ids = data.get('persona_ids', [])
if not persona_ids:
print("❌ Backend: No persona IDs provided in request")
return jsonify({"error": "Missing persona IDs", "message": "At least one persona ID is required"}), 400
if not isinstance(persona_ids, list):
print(f"❌ Backend: Invalid persona_ids type: {type(persona_ids)}")
return jsonify({"error": "Invalid persona IDs", "message": "persona_ids must be an array"}), 400
if len(persona_ids) > 50: # Reasonable limit for batch processing
return jsonify({"error": "Too many personas", "message": "Maximum 50 personas can be processed at once"}), 400
temperature = data.get('temperature', 0.7)
if not (0 <= temperature <= 1):
temperature = 0.7
llm_model = data.get('llm_model', 'gemini-2.5-pro') # Optional parameter with default
# Log the request with model information
print(f"🔄 Backend: Received batch-generate-summaries request for {len(persona_ids)} personas with model: {llm_model}")
current_app.logger.info(f"Batch generating summaries for {len(persona_ids)} personas using model: {llm_model}")
try:
# Fetch all persona data first
personas_data = []
@ -778,9 +803,13 @@ def batch_generate_summaries():
def process_persona_summary(persona_data):
"""Helper function to process a single persona summary"""
try:
persona_name = persona_data.get('name', 'Unknown')
print(f"✅ Backend: Successfully generated summary for '{persona_name}' using model: {llm_model}")
summary = generate_persona_download_summary(
persona_data=persona_data,
temperature=temperature
temperature=temperature,
llm_model=llm_model
)
return {
'success': True,
@ -852,11 +881,15 @@ def batch_generate_summaries():
return jsonify(response_data), 200 # Complete success
except PersonaGenerationError as e:
print(f"❌ Backend: Batch summary generation error: {str(e)}")
current_app.logger.error(f"Batch summary generation error: {str(e)}")
return jsonify({"error": "Failed to generate summaries", "message": str(e)}), 500
except Exception as e:
print(f"❌ Backend: Unexpected error in batch summary generation: {str(e)}")
current_app.logger.error(f"Unexpected error in batch summary generation: {str(e)}")
return jsonify({"error": "Internal server error", "message": "An unexpected error occurred"}), 500
import traceback
print(f"❌ Backend: Full traceback: {traceback.format_exc()}")
return jsonify({"error": "Internal server error", "message": f"An unexpected error occurred: {str(e)}"}), 500
@ai_personas_bp.route('/upload-customer-data', methods=['POST'])

View file

@ -70,6 +70,25 @@ def generate_ai_response():
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')
# Force debug logging to file
try:
import datetime
log_msg = f"🤖 [{datetime.datetime.now()}] AI RESPONSE - Focus group keys: {list(focus_group.keys())}\n"
log_msg += f"🤖 [{datetime.datetime.now()}] AI RESPONSE - Raw llm_model from DB: '{focus_group.get('llm_model')}' (type: {type(focus_group.get('llm_model'))})\n"
log_msg += f"🤖 [{datetime.datetime.now()}] AI RESPONSE - Using model: {llm_model or 'default (gemini-2.5-pro)'} for focus group {focus_group_id}\n"
with open('/tmp/focus_group_debug.log', 'a') as f:
f.write(log_msg)
f.flush()
except:
pass
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-2.5-pro)'} for focus group {focus_group_id}")
# Validate persona exists
persona = Persona.find_by_id(persona_id)
if not persona:
@ -162,7 +181,8 @@ Be genuine and specific in your feedback, drawing on your personal experiences a
response_text = LLMService.generate_contextual_response(
prompt=prompt,
conversation_context=multimodal_context['conversation_context'],
temperature=temperature
temperature=temperature,
model_name=llm_model
)
else:
print(f"💬 Using standard response generation (no visual context)")
@ -173,7 +193,8 @@ Be genuine and specific in your feedback, drawing on your personal experiences a
current_topic=current_topic,
previous_messages=recent_messages,
temperature=temperature,
focus_group_id=focus_group_id
focus_group_id=focus_group_id,
llm_model=llm_model
)
# Log success
@ -262,11 +283,15 @@ def generate_key_themes():
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')
# Generate key themes
try:
themes = KeyThemeService.generate_key_themes(
focus_group_id=focus_group_id,
temperature=temperature
temperature=temperature,
llm_model=llm_model
)
# Log success
@ -446,6 +471,9 @@ def advance_moderator_discussion(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
@ -551,7 +579,8 @@ def advance_moderator_discussion(focus_group_id):
current_topic=topic_context,
previous_messages=recent_messages,
temperature=temperature,
focus_group_id=focus_group_id
focus_group_id=focus_group_id,
llm_model=llm_model
)
# Save participant message

View file

@ -392,11 +392,52 @@ def create_focus_group():
print(f"Error creating focus group: {e}")
return jsonify({"message": f"Failed to create focus group: {str(e)}"}), 500
@focus_groups_bp.route('/<focus_group_id>/test-logging', methods=['GET'])
@jwt_required(optional=True)
def test_logging_endpoint(focus_group_id):
"""Test endpoint to verify Python logging is working"""
print(f"🧪 TEST ENDPOINT HIT: focus_group_id={focus_group_id}")
print(f"🧪 TEST: This should appear in server logs!")
return jsonify({"message": "Test endpoint reached", "focus_group_id": focus_group_id})
@focus_groups_bp.route('/<focus_group_id>', methods=['PUT'])
@jwt_required()
def update_focus_group(focus_group_id):
import datetime
import os
# Force logging to a file to bypass any log redirection
try:
log_msg = f"🚀 [{datetime.datetime.now()}] FOCUS GROUP UPDATE: focus_group_id={focus_group_id}\n"
with open('/tmp/focus_group_debug.log', 'a') as f:
f.write(log_msg)
f.flush()
print(f"🚀 FOCUS GROUP UPDATE ENDPOINT HIT: focus_group_id={focus_group_id}")
except:
pass # Don't let logging errors break the endpoint
data = request.get_json()
try:
log_msg = f"🔧 [{datetime.datetime.now()}] UPDATE DATA: {data}\n"
with open('/tmp/focus_group_debug.log', 'a') as f:
f.write(log_msg)
f.flush()
print(f"🔧 FOCUS GROUP UPDATE DATA: {data}")
except:
pass
# Debug logging for model updates
if data and 'llm_model' in data:
try:
log_msg = f"🔧 [{datetime.datetime.now()}] LLM MODEL UPDATE: {data['llm_model']} for {focus_group_id}\n"
with open('/tmp/focus_group_debug.log', 'a') as f:
f.write(log_msg)
f.flush()
print(f"🔧 FOCUS GROUP API UPDATE: Received llm_model='{data['llm_model']}' for focus group {focus_group_id}")
except:
pass
if not data:
return jsonify({"message": "No data provided"}), 400
@ -734,6 +775,21 @@ def generate_discussion_guide(focus_group_id=None):
else:
formatted_topic = 'General Discussion'
# Get the LLM model for this focus group if it exists
llm_model = None
if focus_group_id:
try:
focus_group = FocusGroup.find_by_id(focus_group_id)
if focus_group:
llm_model = focus_group.get('llm_model')
logger.info(f"Using LLM model for focus group {focus_group_id}: {llm_model}")
except Exception as e:
logger.warning(f"Could not retrieve LLM model for focus group {focus_group_id}: {e}")
# Use default model from request data if provided
if not llm_model:
llm_model = data.get('llm_model')
# Generate the discussion guide
discussion_guide = FocusGroupService.generate_discussion_guide(
focus_group_name=focus_group_name,
@ -741,7 +797,8 @@ def generate_discussion_guide(focus_group_id=None):
discussion_topics=formatted_topic,
duration=duration,
temperature=0.7,
focus_group_id=focus_group_id
focus_group_id=focus_group_id,
llm_model=llm_model
)
logger.info(f"Discussion guide successfully generated for '{focus_group_name}'")

View file

@ -579,10 +579,15 @@ class AIModeratorService:
# Load moderator prompt
prompt = load_prompt('ai-moderator-system', context)
# Get LLM model for this focus group
focus_group = FocusGroup.find_by_id(focus_group_id)
llm_model = focus_group.get('llm_model') if focus_group else None
# Generate response
response = LLMService.generate_content(
prompt=prompt,
temperature=0.7
temperature=0.7,
model_name=llm_model
)
return response.strip()
@ -632,9 +637,14 @@ class AIModeratorService:
# Try to load and use the moderator prompt
prompt = load_prompt('ai-moderator-system', context)
# Get LLM model for this focus group
focus_group = FocusGroup.find_by_id(focus_group_id)
llm_model = focus_group.get('llm_model') if focus_group else None
response = LLMService.generate_content(
prompt=prompt,
temperature=0.7
temperature=0.7,
model_name=llm_model
)
return {
@ -738,9 +748,14 @@ class AIModeratorService:
# Try to generate with LLM using moderator prompt
prompt = load_prompt('ai-moderator-system', context)
# Get LLM model for this focus group
focus_group = FocusGroup.find_by_id(focus_group_id)
llm_model = focus_group.get('llm_model') if focus_group else None
response = LLMService.generate_content(
prompt=prompt,
temperature=0.5 # Lower temperature for more consistent, professional responses
temperature=0.5, # Lower temperature for more consistent, professional responses
model_name=llm_model
)
return response.strip()

View file

@ -63,7 +63,8 @@ def generate_basic_personas(
research_objective: Optional[str] = None,
count: int = 5,
temperature: float = 0.8,
customer_data_session_id: Optional[str] = None
customer_data_session_id: Optional[str] = None,
llm_model: Optional[str] = None
) -> List[Dict[str, Any]]:
"""
Generate basic profiles for multiple personas based on a research brief.
@ -74,6 +75,7 @@ def generate_basic_personas(
count: Number of basic personas to generate
temperature: Controls randomness in generation (0.0 = deterministic, 1.0 = creative)
customer_data_session_id: Optional session ID for customer data context
llm_model: Optional LLM model to use for generation
Returns:
A list of dictionaries containing basic persona data
@ -112,10 +114,14 @@ def generate_basic_personas(
except PromptLoaderError as e:
raise PersonaGenerationError(f"Error loading system prompt: {str(e)}")
# Log the LLM API call
print(f"🤖 Backend: Making LLM API call to {llm_model or 'gemini-2.5-pro'} for basic persona generation")
raw_response = LLMService.generate_content(
prompt=final_prompt,
temperature=temperature,
system_prompt=system_prompt
system_prompt=system_prompt,
model_name=llm_model
)
# Try to clean up the response for proper JSON parsing
@ -185,16 +191,18 @@ def generate_persona(
prompt_customization: Optional[str] = None,
basic_persona: Optional[Dict[str, Any]] = None,
temperature: float = 0.7,
customer_data_session_id: Optional[str] = None
customer_data_session_id: Optional[str] = None,
llm_model: Optional[str] = None
) -> Dict[str, Any]:
"""
Generate a synthetic persona using the Gemini model.
Generate a synthetic persona using the specified LLM model.
Args:
prompt_customization: Optional string to customize the generation
basic_persona: Optional dictionary containing basic persona data to start with
temperature: Controls randomness in generation (0.0 = deterministic, 1.0 = creative)
customer_data_session_id: Optional session ID for customer data context
llm_model: Optional LLM model to use for generation
Returns:
A dictionary containing the generated persona data
@ -242,10 +250,15 @@ def generate_persona(
except PromptLoaderError as e:
raise PersonaGenerationError(f"Error loading system prompt: {str(e)}")
# Log the LLM API call
persona_name = basic_persona.get('name', 'Unknown') if basic_persona else 'New Persona'
print(f"🤖 Backend: Making LLM API call to {llm_model or 'gemini-2.5-pro'} for detailed persona generation of '{persona_name}'")
persona_data = LLMService.generate_structured_response(
prompt=final_prompt,
temperature=temperature,
system_prompt=system_prompt
system_prompt=system_prompt,
model_name=llm_model
)
except LLMServiceError as e:
@ -272,7 +285,8 @@ def generate_persona(
def generate_persona_summary(
persona_data: Dict[str, Any],
temperature: float = 0.7
temperature: float = 0.7,
llm_model: Optional[str] = None
) -> Dict[str, Any]:
"""
Generate a concise summary of a persona for display on persona cards.
@ -280,6 +294,7 @@ def generate_persona_summary(
Args:
persona_data: The complete persona data dictionary
temperature: Controls randomness in generation (0.0 = deterministic, 1.0 = creative)
llm_model: Optional LLM model to use for generation
Returns:
A dictionary containing aiSynthesizedBio, qualitativeAttributes, and topPersonalityTraits
@ -306,10 +321,15 @@ def generate_persona_summary(
except PromptLoaderError as e:
raise PersonaGenerationError(f"Error loading system prompt: {str(e)}")
# Log the LLM API call
persona_name = persona_data.get('name', 'Unknown')
print(f"🤖 Backend: Making LLM API call to {llm_model or 'gemini-2.5-pro'} for summary generation of '{persona_name}'")
raw_response = LLMService.generate_content(
prompt=final_prompt,
temperature=temperature,
system_prompt=system_prompt
system_prompt=system_prompt,
model_name=llm_model
)
# Clean up the response for proper JSON parsing
@ -370,7 +390,8 @@ def generate_persona_summary(
def generate_persona_download_summary(
persona_data: Dict[str, Any],
temperature: float = 0.7
temperature: float = 0.7,
llm_model: Optional[str] = None
) -> str:
"""
Generate a comprehensive markdown summary of a persona for download/client review.
@ -378,6 +399,7 @@ def generate_persona_download_summary(
Args:
persona_data: The complete persona data dictionary
temperature: Controls randomness in generation (0.0 = deterministic, 1.0 = creative)
llm_model: Optional LLM model to use for generation
Returns:
A string containing the markdown-formatted persona summary
@ -404,11 +426,16 @@ def generate_persona_download_summary(
except PromptLoaderError as e:
raise PersonaGenerationError(f"Error loading system prompt: {str(e)}")
# Log the LLM API call
persona_name = persona_data.get('name', 'Unknown')
print(f"🤖 Backend: Making LLM API call to {llm_model or 'gemini-2.5-pro'} for download summary of '{persona_name}'")
# Generate the markdown content directly
markdown_response = LLMService.generate_content(
prompt=final_prompt,
temperature=temperature,
system_prompt=system_prompt
system_prompt=system_prompt,
model_name=llm_model
)
# Clean up the response if needed

View file

@ -636,6 +636,10 @@ class AutonomousConversationController:
# Get discussion guide
discussion_guide = focus_group.get('discussionGuide', '')
# Get the LLM model for this focus group
llm_model = focus_group.get('llm_model')
self.logger.info(f"🤖 Autonomous conversation using model: {llm_model or 'default (gemini-2.5-pro)'} for focus group {self.focus_group_id}")
# Get recent messages
messages = FocusGroup.get_messages(self.focus_group_id)
recent_messages = messages[-20:] if len(messages) > 20 else messages
@ -647,7 +651,8 @@ class AutonomousConversationController:
current_topic=topic,
previous_messages=recent_messages,
temperature=0.7,
focus_group_id=self.focus_group_id
focus_group_id=self.focus_group_id,
llm_model=llm_model
)
except Exception as e:
self.logger.error(f"Error in generate_persona_response: {str(e)}")

View file

@ -53,11 +53,17 @@ class ConversationDecisionService:
print(f"❌ Error loading {mode} mode prompt: {str(e)}")
raise ConversationDecisionError(f"Error loading {mode} mode prompt: {str(e)}")
# Get LLM model for this focus group
from app.models.focus_group import FocusGroup
focus_group = FocusGroup.find_by_id(focus_group_id)
llm_model = focus_group.get('llm_model') if focus_group else None
# Get LLM decision
try:
response = LLMService.generate_content(
prompt=prompt,
temperature=temperature
temperature=temperature,
model_name=llm_model
)
# Parse the JSON response
@ -352,9 +358,15 @@ class ConversationDecisionService:
}}
"""
# Get LLM model for this focus group
from app.models.focus_group import FocusGroup
focus_group = FocusGroup.find_by_id(focus_group_id)
llm_model = focus_group.get('llm_model') if focus_group else None
response = LLMService.generate_content(
prompt=insight_prompt,
temperature=temperature
temperature=temperature,
model_name=llm_model
)
insights = LLMService.parse_json_response(response)

View file

@ -20,7 +20,8 @@ def generate_persona_response(
current_topic: str,
previous_messages: List[Dict[str, Any]],
temperature: float = 0.7,
focus_group_id: Optional[str] = None
focus_group_id: Optional[str] = None,
llm_model: Optional[str] = None
) -> str:
"""
Generate a response from a persona in a focus group discussion.
@ -32,6 +33,7 @@ def generate_persona_response(
previous_messages: List of previous messages in the discussion
temperature: Controls randomness in generation (0.0 = deterministic, 1.0 = creative)
focus_group_id: Optional focus group ID for visual context integration
llm_model: Optional LLM model to use for generation
Returns:
A string containing the persona's response
@ -43,6 +45,7 @@ def generate_persona_response(
print(f"🎭 Generating persona response for {persona.get('name', 'Unknown')}")
print(f" - focus_group_id: {focus_group_id}")
print(f" - current_topic: {current_topic[:50]}...")
print(f" - llm_model: {llm_model or 'default (gemini-2.5-pro)'}")
# Import LLMService at the top to avoid scoping issues
from app.services.llm_service import LLMService
@ -114,7 +117,8 @@ Be genuine and specific in your feedback, drawing on your personal experiences a
response = LLMService.generate_contextual_response(
prompt=prompt,
conversation_context=multimodal_context['conversation_context'],
temperature=temperature
temperature=temperature,
model_name=llm_model
)
print(f"✅ Generated contextual response with visual context")
@ -139,7 +143,8 @@ Be genuine and specific in your feedback, drawing on your personal experiences a
# Generate the standard response
response = LLMService.generate_content(
prompt=prompt,
temperature=temperature
temperature=temperature,
model_name=llm_model
)
print(f"✅ Generated standard response")

View file

@ -29,7 +29,8 @@ class FocusGroupService:
duration: int = 60,
temperature: float = 0.7,
max_retries: int = 3,
focus_group_id: Optional[str] = None
focus_group_id: Optional[str] = None,
llm_model: Optional[str] = None
) -> Dict[str, Any]:
"""
Generate a focus group discussion guide using the LLM with retry logic.
@ -42,6 +43,7 @@ class FocusGroupService:
temperature: Controls randomness in generation
max_retries: Maximum number of retry attempts
focus_group_id: Optional focus group ID to check for uploaded assets
llm_model: Optional LLM model to use for generation
Returns:
A structured JSON discussion guide (dict)
@ -157,6 +159,7 @@ class FocusGroupService:
# DEBUG: Log the complete prompt to verify asset information is included
logger.info("=== DEBUG: COMPLETE PROMPT BEING SENT TO LLM ===")
logger.info(f"Prompt length: {len(prompt)} characters")
logger.info(f"LLM model being used: {llm_model or 'default (gemini-2.5-pro)'}")
logger.info(f"Assets in template variables: {len(uploaded_assets)} assets")
if uploaded_assets:
logger.info(f"Asset details: {[{'filename': a.get('filename'), 'original': a.get('original_filename')} for a in uploaded_assets]}")
@ -173,6 +176,12 @@ class FocusGroupService:
else:
logger.warning("CREATIVE ASSETS REQUIREMENTS section not found in prompt!")
# ENHANCED DEBUG: Log specific template variables for asset handling
logger.info(f"=== ASSET TEMPLATE DEBUG ===")
logger.info(f"has_assets: {template_vars.get('has_assets', False)}")
logger.info(f"asset_count: {template_vars.get('asset_count', 0)}")
logger.info(f"uploaded_asset_list: {template_vars.get('uploaded_asset_list', 'None')}")
logger.info("=== END DEBUG PROMPT ===")
except PromptLoaderError as e:
error_msg = f"Error loading discussion guide prompt: {str(e)}"
@ -185,11 +194,27 @@ class FocusGroupService:
try:
logger.info(f"Discussion guide generation attempt {attempt}/{max_retries}")
# Special handling for GPT models to ensure creative review compliance
enhanced_prompt = prompt
if llm_model and llm_model.startswith('gpt'):
# Add extra emphasis for GPT models about creative review requirements
if uploaded_assets and len(uploaded_assets) > 0:
asset_emphasis = f"\n\n🚨🚨🚨 CRITICAL FOR GPT MODELS - READ THIS FIRST 🚨🚨🚨\n"
asset_emphasis += f"YOU ABSOLUTELY MUST INCLUDE EXACTLY {len(uploaded_assets)} ACTIVITIES WITH type='creative_review'\n"
asset_emphasis += f"EACH activity must reference ONE of these exact filenames:\n"
for asset in uploaded_assets:
asset_emphasis += f"- {asset.get('filename', 'unknown')}\n"
asset_emphasis += f"FAILURE TO INCLUDE ALL {len(uploaded_assets)} CREATIVE_REVIEW ACTIVITIES WILL RESULT IN INVALID OUTPUT\n"
asset_emphasis += f"🚨🚨🚨 END CRITICAL INSTRUCTIONS 🚨🚨🚨\n\n"
enhanced_prompt = asset_emphasis + prompt
logger.info(f"Enhanced prompt for GPT model with {len(uploaded_assets)} asset emphasis")
# Generate content using LLM
response = LLMService.generate_content(
prompt=prompt,
prompt=enhanced_prompt,
temperature=temperature,
max_tokens=16000 # Use a much higher token limit to avoid truncation
max_tokens=16000, # Use a much higher token limit to avoid truncation
model_name=llm_model
)
logger.info(f"Received LLM response (length: {len(response)} chars)")
@ -219,6 +244,7 @@ class FocusGroupService:
# Validate creative review activities if assets were uploaded
if uploaded_assets and len(uploaded_assets) > 0:
creative_review_count = 0
creative_review_activities = []
sections = guide_json.get('sections', [])
# Count creative_review activities across all sections
@ -227,22 +253,63 @@ class FocusGroupService:
for activity in activities:
if activity.get('type') == 'creative_review':
creative_review_count += 1
creative_review_activities.append({
'section': section.get('title', 'Unknown'),
'content': activity.get('content', 'No content')[:100] + '...'
})
# Also check in subsections
# Also check in subsections
subsections = section.get('subsections', [])
for subsection in subsections:
activities = subsection.get('activities', [])
for activity in activities:
if activity.get('type') == 'creative_review':
creative_review_count += 1
creative_review_activities.append({
'section': f"{section.get('title', 'Unknown')} > {subsection.get('title', 'Unknown')}",
'content': activity.get('content', 'No content')[:100] + '...'
})
# Also check questions in subsections for creative_review type
questions = subsection.get('questions', [])
for question in questions:
if question.get('type') == 'creative_review':
creative_review_count += 1
creative_review_activities.append({
'section': f"{section.get('title', 'Unknown')} > {subsection.get('title', 'Unknown')} (question)",
'content': question.get('content', 'No content')[:100] + '...'
})
logger.info(f"=== CREATIVE REVIEW VALIDATION RESULTS (Model: {llm_model or 'gemini-2.5-pro'}) ===")
logger.info(f"Found {creative_review_count} creative_review activities for {len(uploaded_assets)} uploaded assets")
# If no creative review activities were generated, log a warning but continue
if creative_review_activities:
logger.info("Creative review activities found:")
for i, activity in enumerate(creative_review_activities):
logger.info(f" {i+1}. Section: {activity['section']}")
logger.info(f" Content: {activity['content']}")
# If no creative review activities were generated, retry with enhanced prompt
if creative_review_count == 0:
logger.warning(f"WARNING: No creative_review activities generated despite {len(uploaded_assets)} uploaded assets!")
logger.warning(f"❌ WARNING: No creative_review activities generated despite {len(uploaded_assets)} uploaded assets!")
logger.warning(f"❌ This suggests {llm_model or 'gemini-2.5-pro'} is not following the creative asset instructions")
# For GPT models, if this was already the enhanced prompt, we have a serious issue
if llm_model and llm_model.startswith('gpt') and attempt < max_retries:
logger.warning(f"❌ GPT model failed to generate creative_review activities. Will retry with even more explicit instructions.")
# This will trigger a retry with the next attempt
raise Exception(f"GPT model failed to generate required creative_review activities")
elif creative_review_count < len(uploaded_assets):
logger.warning(f"WARNING: Only {creative_review_count} creative_review activities generated for {len(uploaded_assets)} assets")
logger.warning(f"⚠️ WARNING: Only {creative_review_count} creative_review activities generated for {len(uploaded_assets)} assets")
# For GPT models with incomplete creative reviews, also consider this a failure worth retrying
if llm_model and llm_model.startswith('gpt') and attempt < max_retries:
logger.warning(f"⚠️ GPT model generated incomplete creative_review activities. Will retry.")
raise Exception(f"GPT model generated only {creative_review_count}/{len(uploaded_assets)} required creative_review activities")
else:
logger.info(f"✅ Good: {creative_review_count} creative_review activities generated for {len(uploaded_assets)} assets")
logger.info(f"Discussion guide generation successful on attempt {attempt}/{max_retries}")
logger.info(f"Generated guide has {len(guide_json.get('sections', []))} sections")

View file

@ -73,10 +73,17 @@ class ImageDescriptionService:
# Generate description using multimodal LLM
try:
print(f"🚀 DESCRIPTION: Calling LLM service with image: {asset_path}")
# Get LLM model for this focus group
from app.models.focus_group import FocusGroup
focus_group = FocusGroup.find_by_id(focus_group_id)
llm_model = focus_group.get('llm_model') if focus_group else None
description = LLMService.generate_multimodal_content(
prompt=prompt,
image_paths=[asset_path],
temperature=0.7
temperature=0.7,
model_name=llm_model
)
print(f"✅ DESCRIPTION: LLM returned description ({len(description)} chars): {description[:100]}...")
return description.strip()
@ -119,43 +126,58 @@ class ImageDescriptionService:
Enhanced question text with detailed visual description
"""
try:
import re
print(f"🔧 ENHANCEMENT: Enhancing question for {asset_filename}")
print(f"🔧 Original: {original_question[:100]}...")
print(f"🔧 Description: {description[:100]}...")
# Look for the asset filename reference in the question
if asset_filename in original_question:
# Find the filename reference and enhance it
filename_pattern = f"'{asset_filename}'"
enhanced_reference = f"'{asset_filename}' - {description}"
# Use regex patterns to handle punctuation and variations
# Escape the filename for regex use
escaped_filename = re.escape(asset_filename)
# Define comprehensive patterns that handle punctuation after filenames
regex_patterns = [
# Quoted filenames with optional punctuation
(rf"('{escaped_filename}')([.,;!?]*)", rf"\1 - {description}\2"),
(rf'("{escaped_filename}")([.,;!?]*)', rf'\1 - {description}\2'),
enhanced_question = original_question.replace(filename_pattern, enhanced_reference)
# Titled/labeled patterns
(rf"(titled\s+['\"]?{escaped_filename}['\"]?)([.,;!?]*)", rf"\1 - {description}\2"),
(rf"(asset\s+['\"]?{escaped_filename}['\"]?)([.,;!?]*)", rf"\1 - {description}\2"),
(rf"(image\s+['\"]?{escaped_filename}['\"]?)([.,;!?]*)", rf"\1 - {description}\2"),
print(f"✅ ENHANCEMENT: Enhanced question: {enhanced_question[:150]}...")
return enhanced_question
else:
# If filename not found in expected format, try other patterns
import re
# Try to find quoted filename patterns
patterns = [
(f'"{asset_filename}"', f'"{asset_filename}" - {description}'),
(f"titled '{asset_filename}'", f"titled '{asset_filename}' - {description}"),
(f'titled "{asset_filename}"', f'titled "{asset_filename}" - {description}'),
(asset_filename, f"{asset_filename} - {description}")
]
enhanced_question = original_question
for old_pattern, new_pattern in patterns:
if old_pattern in enhanced_question:
enhanced_question = enhanced_question.replace(old_pattern, new_pattern)
print(f"✅ ENHANCEMENT: Enhanced with pattern '{old_pattern}': {enhanced_question[:150]}...")
return enhanced_question
# If no patterns match, append description at the end
# Unquoted filename with word boundaries
(rf"\b({escaped_filename})\b([.,;!?]*)", rf"\1 - {description}\2")
]
enhanced_question = original_question
enhancement_applied = False
# Try each regex pattern
for pattern, replacement in regex_patterns:
if re.search(pattern, enhanced_question, re.IGNORECASE):
enhanced_question = re.sub(pattern, replacement, enhanced_question, flags=re.IGNORECASE)
print(f"✅ ENHANCEMENT: Enhanced with regex pattern: {enhanced_question[:150]}...")
enhancement_applied = True
break
# If no regex patterns worked, try simple string replacement as fallback
if not enhancement_applied and asset_filename in original_question:
# Simple replacement that adds description after any occurrence of filename
enhanced_question = original_question.replace(
asset_filename,
f"{asset_filename} - {description}"
)
print(f"✅ ENHANCEMENT: Enhanced with simple replacement: {enhanced_question[:150]}...")
enhancement_applied = True
# Final fallback: append description if no enhancements worked
if not enhancement_applied:
enhanced_question = f"{original_question} The image shows {description}."
print(f"⚠️ ENHANCEMENT: Appended description to end: {enhanced_question[:150]}...")
return enhanced_question
return enhanced_question
except Exception as e:
error_msg = f"Failed to enhance question for {asset_filename}: {str(e)}"

View file

@ -22,7 +22,8 @@ class KeyThemeService:
@staticmethod
def generate_key_themes(
focus_group_id: str,
temperature: float = 0.7
temperature: float = 0.7,
llm_model: Optional[str] = None
) -> List[Dict[str, str]]:
"""
Generate key themes from a focus group discussion.
@ -30,6 +31,7 @@ class KeyThemeService:
Args:
focus_group_id: The ID of the focus group
temperature: Controls randomness in generation (default: 0.7)
llm_model: Optional LLM model to use for generation
Returns:
A list of key theme objects with title and description fields
@ -39,6 +41,7 @@ class KeyThemeService:
"""
logger = logging.getLogger(__name__)
logger.info(f"Starting key theme generation for focus group {focus_group_id} with temperature {temperature}")
logger.info(f"Using LLM model: {llm_model or 'default (gemini-2.5-pro)'}")
try:
# Get the focus group
@ -69,7 +72,8 @@ class KeyThemeService:
messages=messages,
participants=participants_data,
discussion_guide=focus_group.get('discussionGuide', ''),
temperature=temperature
temperature=temperature,
llm_model=llm_model
)
except Exception as e:
@ -80,7 +84,8 @@ class KeyThemeService:
messages: List[Dict[str, Any]],
participants: List[Dict[str, Any]],
discussion_guide: str,
temperature: float = 0.7
temperature: float = 0.7,
llm_model: Optional[str] = None
) -> List[Dict[str, str]]:
"""
Extract key themes from a discussion using LLM.
@ -90,6 +95,7 @@ class KeyThemeService:
participants: List of participant personas
discussion_guide: The discussion guide for the focus group
temperature: Controls randomness in generation
llm_model: Optional LLM model to use for generation
Returns:
A list of key theme objects with title and description
@ -99,6 +105,7 @@ class KeyThemeService:
"""
logger = logging.getLogger(__name__)
logger.info(f"Beginning theme extraction from {len(messages)} messages")
logger.info(f"Theme extraction using LLM model: {llm_model or 'default (gemini-2.5-pro)'}")
try:
# Load and prepare the prompt for the LLM
@ -128,16 +135,17 @@ class KeyThemeService:
for attempt in range(max_retries):
attempt_num = attempt + 1
logger.info(f"Attempt {attempt_num}/{max_retries}: Calling LLM for theme generation")
logger.info(f"Attempt {attempt_num}/{max_retries}: Calling LLM ({llm_model or 'gemini-2.5-pro'}) for theme generation")
try:
themes = LLMService.generate_structured_array(
prompt=prompt,
temperature=temperature,
system_prompt=system_prompt
system_prompt=system_prompt,
model_name=llm_model
)
logger.info(f"Attempt {attempt_num}/{max_retries}: LLM call successful, received {len(themes)} themes")
logger.info(f"Attempt {attempt_num}/{max_retries}: LLM ({llm_model or 'gemini-2.5-pro'}) call successful, received {len(themes)} themes")
# Validate the response structure
validated_themes = []
@ -167,7 +175,7 @@ class KeyThemeService:
validated_themes.append(validated_theme)
logger.info(f"Theme generation completed successfully with {len(validated_themes)} validated themes")
logger.info(f"Theme generation completed successfully with {len(validated_themes)} validated themes using {llm_model or 'gemini-2.5-pro'}")
return validated_themes
except LLMServiceError as e:

View file

@ -10,6 +10,7 @@ import json
import time
import logging
import google.generativeai as genai
from openai import OpenAI
from typing import Dict, Any, Optional, Union, List
from PIL import Image
import io
@ -18,9 +19,19 @@ import io
GEMINI_API_KEY = os.environ.get('GEMINI_API_KEY', 'AIzaSyAc50jzC3k9K1PmKT1vGFi0sCdhhnqsvl0')
genai.configure(api_key=GEMINI_API_KEY)
# Set up OpenAI API key
OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY', 'REDACTED_OPENAI_KEY')
openai_client = OpenAI(api_key=OPENAI_API_KEY)
# The default model we're using
DEFAULT_MODEL = "gemini-2.5-pro"
# Supported models
SUPPORTED_MODELS = {
'gemini-2.5-pro': 'gemini',
'gpt-4.1': 'openai'
}
class LLMServiceError(Exception):
"""Exception raised for errors in LLM operations."""
pass
@ -28,6 +39,20 @@ class LLMServiceError(Exception):
class LLMService:
"""Centralized service for LLM operations."""
@staticmethod
def _get_model_provider(model_name: Optional[str] = None) -> str:
"""
Get the provider for a given model name.
Args:
model_name: Optional model name to use. Defaults to the default model.
Returns:
The provider name ('gemini' or 'openai')
"""
actual_model = model_name or DEFAULT_MODEL
return SUPPORTED_MODELS.get(actual_model, 'gemini')
@staticmethod
def get_model(model_name: Optional[str] = None) -> genai.GenerativeModel:
"""
@ -132,39 +157,63 @@ class LLMService:
max_retries = 3
last_error = None
actual_model = model_name or DEFAULT_MODEL
provider = LLMService._get_model_provider(model_name)
for attempt in range(max_retries):
attempt_num = attempt + 1
logger.debug(f"LLM content generation attempt {attempt_num}/{max_retries}")
logger.debug(f"LLM content generation attempt {attempt_num}/{max_retries} using {provider} provider")
try:
model = LLMService.get_model(model_name)
generation_config = {
"temperature": temperature,
}
if max_tokens:
generation_config["max_output_tokens"] = max_tokens
# If system prompt is provided, use it to create a structured chat
if system_prompt:
# For Gemini models, system prompts need to be passed as part of the user prompt
# as Gemini API doesn't support 'system' role directly
response = model.generate_content(
[
{"role": "user", "parts": [f"System: {system_prompt}\n\nUser: {prompt}"]}
],
generation_config=genai.types.GenerationConfig(**generation_config)
)
if provider == 'openai':
# OpenAI API call
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": prompt})
kwargs = {
"model": actual_model,
"messages": messages,
"temperature": temperature,
}
if max_tokens:
kwargs["max_tokens"] = max_tokens
response = openai_client.chat.completions.create(**kwargs)
result = response.choices[0].message.content.strip()
else:
# Otherwise use the standard prompt-only approach
response = model.generate_content(
prompt,
generation_config=genai.types.GenerationConfig(**generation_config)
)
# If successful, extract and return the response
result = LLMService._extract_text_from_response(response)
# Gemini API call (existing logic)
model = LLMService.get_model(model_name)
generation_config = {
"temperature": temperature,
}
if max_tokens:
generation_config["max_output_tokens"] = max_tokens
# If system prompt is provided, use it to create a structured chat
if system_prompt:
# For Gemini models, system prompts need to be passed as part of the user prompt
# as Gemini API doesn't support 'system' role directly
response = model.generate_content(
[
{"role": "user", "parts": [f"System: {system_prompt}\n\nUser: {prompt}"]}
],
generation_config=genai.types.GenerationConfig(**generation_config)
)
else:
# Otherwise use the standard prompt-only approach
response = model.generate_content(
prompt,
generation_config=genai.types.GenerationConfig(**generation_config)
)
# If successful, extract and return the response
result = LLMService._extract_text_from_response(response)
if attempt > 0:
logger.info(f"LLM content generation succeeded on attempt {attempt_num}/{max_retries}")
@ -176,7 +225,7 @@ class LLMService:
logger.warning(f"LLM attempt {attempt_num}/{max_retries} failed: {str(e)}")
# Check if this is a retryable error (Google API internal errors, rate limiting, etc.)
# Check if this is a retryable error (API internal errors, rate limiting, etc.)
if ("500" in error_message or
"internal error" in error_message or
"internal server error" in error_message or
@ -334,52 +383,100 @@ class LLMService:
max_retries = 3
last_error = None
# Load and validate images
images = []
for image_path in image_paths:
try:
if not os.path.exists(image_path):
raise LLMServiceError(f"Image file not found: {image_path}")
# Load image using PIL
with Image.open(image_path) as img:
# Convert to RGB if necessary
if img.mode != 'RGB':
img = img.convert('RGB')
images.append(img.copy())
logger.debug(f"Successfully loaded image: {image_path}")
except Exception as e:
raise LLMServiceError(f"Failed to load image {image_path}: {str(e)}")
actual_model = model_name or DEFAULT_MODEL
provider = LLMService._get_model_provider(model_name)
logger.info(f"Generating multimodal content with {len(images)} image(s)")
logger.info(f"Generating multimodal content with {len(image_paths)} image(s) using {provider} provider")
for attempt in range(max_retries):
attempt_num = attempt + 1
logger.debug(f"Multimodal content generation attempt {attempt_num}/{max_retries}")
try:
model = LLMService.get_model(model_name)
if provider == 'openai':
# OpenAI multimodal API call
import base64
# Prepare image content for OpenAI API
image_content = []
for image_path in image_paths:
if not os.path.exists(image_path):
raise LLMServiceError(f"Image file not found: {image_path}")
# Encode image to base64
with open(image_path, "rb") as image_file:
base64_image = base64.b64encode(image_file.read()).decode('utf-8')
# Get image format
image_format = image_path.lower().split('.')[-1]
if image_format == 'jpg':
image_format = 'jpeg'
image_content.append({
"type": "image_url",
"image_url": {
"url": f"data:image/{image_format};base64,{base64_image}"
}
})
logger.debug(f"Successfully loaded image for OpenAI: {image_path}")
# Create message content with text and images
content = [{"type": "text", "text": prompt}]
content.extend(image_content)
kwargs = {
"model": actual_model,
"messages": [{"role": "user", "content": content}],
"temperature": temperature,
}
if max_tokens:
kwargs["max_tokens"] = max_tokens
response = openai_client.chat.completions.create(**kwargs)
result = response.choices[0].message.content.strip()
else:
# Gemini multimodal API call (existing logic)
# Load and validate images
images = []
for image_path in image_paths:
try:
if not os.path.exists(image_path):
raise LLMServiceError(f"Image file not found: {image_path}")
# Load image using PIL
with Image.open(image_path) as img:
# Convert to RGB if necessary
if img.mode != 'RGB':
img = img.convert('RGB')
images.append(img.copy())
logger.debug(f"Successfully loaded image for Gemini: {image_path}")
except Exception as e:
raise LLMServiceError(f"Failed to load image {image_path}: {str(e)}")
model = LLMService.get_model(model_name)
generation_config = {
"temperature": temperature,
}
if max_tokens:
generation_config["max_output_tokens"] = max_tokens
# Create multimodal input - combine text prompt with images
content_parts = [prompt]
content_parts.extend(images)
generation_config = {
"temperature": temperature,
}
if max_tokens:
generation_config["max_output_tokens"] = max_tokens
# Create multimodal input - combine text prompt with images
content_parts = [prompt]
content_parts.extend(images)
response = model.generate_content(
content_parts,
generation_config=genai.types.GenerationConfig(**generation_config)
)
# Extract and return the response
result = LLMService._extract_text_from_response(response)
response = model.generate_content(
content_parts,
generation_config=genai.types.GenerationConfig(**generation_config)
)
# Extract and return the response
result = LLMService._extract_text_from_response(response)
if attempt > 0:
logger.info(f"Multimodal content generation succeeded on attempt {attempt_num}/{max_retries}")
@ -480,9 +577,8 @@ class LLMService:
if image_parts:
print(f"🎨 Using multimodal generation with {len(image_parts)} images")
# Create content parts with text and images
content_parts = [full_prompt]
content_parts.extend(image_parts)
actual_model = model_name or DEFAULT_MODEL
provider = LLMService._get_model_provider(model_name)
max_retries = 3
last_error = None
@ -492,26 +588,67 @@ class LLMService:
logger.debug(f"Contextual multimodal generation attempt {attempt_num}/{max_retries}")
try:
model = LLMService.get_model(model_name)
generation_config = {
"temperature": temperature,
}
if max_tokens:
generation_config["max_output_tokens"] = max_tokens
response = model.generate_content(
content_parts,
generation_config=genai.types.GenerationConfig(**generation_config)
)
result = LLMService._extract_text_from_response(response)
if provider == 'openai':
# OpenAI contextual multimodal API call
import base64
# Convert PIL images to base64 for OpenAI API
image_content = []
for i, img in enumerate(image_parts):
# Convert PIL image to base64
buffer = io.BytesIO()
img.save(buffer, format='PNG')
base64_image = base64.b64encode(buffer.getvalue()).decode('utf-8')
image_content.append({
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{base64_image}"
}
})
# Create message content with text and images
content = [{"type": "text", "text": full_prompt}]
content.extend(image_content)
kwargs = {
"model": actual_model,
"messages": [{"role": "user", "content": content}],
"temperature": temperature,
}
if max_tokens:
kwargs["max_tokens"] = max_tokens
response = openai_client.chat.completions.create(**kwargs)
result = response.choices[0].message.content.strip()
else:
# Gemini contextual multimodal API call (existing logic)
# Create content parts with text and images
content_parts = [full_prompt]
content_parts.extend(image_parts)
model = LLMService.get_model(model_name)
generation_config = {
"temperature": temperature,
}
if max_tokens:
generation_config["max_output_tokens"] = max_tokens
response = model.generate_content(
content_parts,
generation_config=genai.types.GenerationConfig(**generation_config)
)
result = LLMService._extract_text_from_response(response)
if attempt > 0:
logger.info(f"Contextual multimodal generation succeeded on attempt {attempt_num}/{max_retries}")
print(f"✅ Generated contextual response with visual context")
print(f"✅ Generated contextual response with visual context using {provider}")
return result
except Exception as e:

View file

@ -20,6 +20,7 @@ Analyze the provided image and create a comprehensive visual description that fo
- **Focus**: Marketing/advertising context with brand and product emphasis
- **Specificity**: Include specific details that help distinguish from other advertisements
- **Clarity**: Clear, precise language that non-experts can understand
- **Integration**: Description will be inserted directly into moderator questions, so ensure it flows naturally and helps participants visualize the asset
## OUTPUT FORMAT
Provide only the description text - no additional formatting, explanations, or commentary. The description should flow naturally and be suitable for incorporation into a moderator question.

View file

@ -8,5 +8,6 @@ bcrypt==4.0.1
pydantic==1.10.7
hypercorn
google-generativeai==0.3.2
openai>=1.0.0
requests==2.31.0
llama-cloud-services

File diff suppressed because one or more lines are too long

732
dist/assets/index-Dod4tGHl.js vendored Normal file

File diff suppressed because one or more lines are too long

2
dist/index.html vendored
View file

@ -7,7 +7,7 @@
<meta name="description" content="Lovable Generated Project" />
<meta name="author" content="Lovable" />
<meta property="og:image" content="/og-image.png" />
<script type="module" crossorigin src="/semblance/assets/index-Bk-FPBaP.js"></script>
<script type="module" crossorigin src="/semblance/assets/index-Dod4tGHl.js"></script>
<link rel="stylesheet" crossorigin href="/semblance/assets/index-CMEVr6tk.css">
</head>

View file

@ -104,12 +104,16 @@ export default function AIRecruiter({ targetFolderId, targetFolderName }: AIRecr
console.log("No target folder specified for new personas");
}
// Log which model is being used for generation
console.log(`🤖 Starting persona generation with model: ${values.llm_model || 'gemini-2.5-pro'}`);
const response = await generateSyntheticPersonas(
values.audienceBrief,
values.researchObjective,
count,
values.dataFile,
targetFolderId
targetFolderId,
values.llm_model
);
// Extract personas from the response
@ -122,11 +126,14 @@ export default function AIRecruiter({ targetFolderId, targetFolderName }: AIRecr
// Check for partial success (some personas generated, some failed)
if (personas && personas.length > 0) {
// Log successful generation with model info
console.log(`✅ Successfully generated ${personas.length} personas using model: ${values.llm_model || 'gemini-2.5-pro'}`);
// Check if we got a response with partial success info
if (response.partial_success || (response.errors && response.errors.length > 0)) {
// Some personas succeeded but others failed
toast.success("Some personas generated successfully", {
description: `${personas.length} synthetic personas were created. ${response.errors?.length || 0} failed due to timeout or other errors.`,
description: `${personas.length} synthetic personas were created using ${values.llm_model || 'Gemini 2.5 Pro'}. ${response.errors?.length || 0} failed due to timeout or other errors.`,
duration: 8000
});
@ -142,7 +149,7 @@ export default function AIRecruiter({ targetFolderId, targetFolderName }: AIRecr
} else {
// All personas succeeded
toast.success("Personas generated and saved successfully", {
description: `${personas.length} synthetic personas have been created and saved ${targetFolderId ? `to the "${targetFolderName}" folder` : 'to the database'}.`
description: `${personas.length} synthetic personas have been created using ${values.llm_model || 'Gemini 2.5 Pro'} and saved ${targetFolderId ? `to the "${targetFolderName}" folder` : 'to the database'}.`
});
}
@ -152,7 +159,7 @@ export default function AIRecruiter({ targetFolderId, targetFolderName }: AIRecr
throw new Error("No personas were generated");
}
} catch (error) {
console.error("Error generating personas:", error);
console.error(`❌ Error generating personas using model: ${values.llm_model || 'gemini-2.5-pro'}:`, error);
let errorMessage = "Please try again or adjust your parameters";
let errorTitle = "Failed to generate personas";

View file

@ -112,6 +112,7 @@ const formSchema = z.object({
duration: z.string().min(1, {
message: "Duration is required.",
}),
llm_model: z.string().optional(),
});
// Sample discussion guide sections - We'll keep this but fetch real personas from the database
@ -509,6 +510,7 @@ export default function FocusGroupModerator({ draftToEdit, onDraftSaved }: Focus
focusGroupName: "",
discussionTopics: "",
duration: "60",
llm_model: "gemini-2.5-pro",
},
});
console.log('Form initialized successfully');
@ -568,7 +570,8 @@ export default function FocusGroupModerator({ draftToEdit, onDraftSaved }: Focus
description: values.researchBrief,
objective: values.researchBrief,
topic: values.discussionTopics,
duration: parseInt(values.duration)
duration: parseInt(values.duration),
llm_model: values.llm_model
};
// Call API to generate discussion guide, with focus group ID if available
@ -663,6 +666,7 @@ ${values.researchBrief}
topic: values.discussionTopics.split(',')[0].trim().toLowerCase().replace(/\s+/g, '-'),
description: values.researchBrief,
objective: values.researchBrief,
llm_model: values.llm_model,
};
const savedDraft = await focusGroupsApi.create(draftData);
@ -719,7 +723,31 @@ ${values.researchBrief}
}
}
// Generate discussion guide based on form input (after assets are uploaded)
// Update focus group with current form values before generating guide
// This ensures the backend uses the latest model selection
if (focusGroupId) {
try {
const preUpdateData = {
name: values.focusGroupName,
participants: selectedParticipants,
participants_count: selectedParticipants.length,
duration: parseInt(values.duration),
topic: values.discussionTopics.split(',')[0].trim().toLowerCase().replace(/\s+/g, '-'),
description: values.researchBrief,
objective: values.researchBrief,
llm_model: values.llm_model
};
await focusGroupsApi.update(focusGroupId, preUpdateData);
console.log("Focus group updated with latest form values before guide generation");
console.log(`🔄 Updated focus group ${focusGroupId} with model: ${values.llm_model}`);
} catch (error) {
console.error("Failed to update focus group before guide generation:", error);
// Continue anyway, as the generateDiscussionGuide will use form values as fallback
}
}
// Generate discussion guide based on form input (after database is updated)
const guide = await generateDiscussionGuide(values, focusGroupId);
setDiscussionGuide(guide);
@ -735,6 +763,7 @@ ${values.researchBrief}
topic: values.discussionTopics.split(',')[0].trim().toLowerCase().replace(/\s+/g, '-'),
description: values.researchBrief,
objective: values.researchBrief,
llm_model: values.llm_model,
discussionGuide: guide
};
@ -1193,6 +1222,31 @@ true;
</FormItem>
)}
/>
<FormField
control={form.control}
name="llm_model"
render={({ field }) => (
<FormItem>
<FormLabel>AI Model</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select AI model" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="gemini-2.5-pro">Gemini 2.5 Pro</SelectItem>
<SelectItem value="gpt-4.1">GPT-4.1</SelectItem>
</SelectContent>
</Select>
<FormDescription>
Choose which AI model to use for generating responses and discussion guides
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>

View file

@ -19,6 +19,13 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export const formSchema = z.object({
audienceBrief: z.string().min(10, {
@ -29,6 +36,7 @@ export const formSchema = z.object({
message: "Number of personas is required.",
}),
dataFile: z.instanceof(FileList).optional(),
llm_model: z.string().optional(),
});
interface AIRecruiterFormProps {
@ -50,6 +58,7 @@ export default function AIRecruiterForm({ onSubmit, isGenerating }: AIRecruiterF
audienceBrief: "",
researchObjective: "",
personaCount: "5",
llm_model: "gemini-2.5-pro",
},
});
@ -372,6 +381,32 @@ export default function AIRecruiterForm({ onSubmit, isGenerating }: AIRecruiterF
</div>
)}
{/* LLM Model Selection */}
<FormField
control={form.control}
name="llm_model"
render={({ field }) => (
<FormItem>
<FormLabel>AI Model</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select AI model" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="gemini-2.5-pro">Gemini 2.5 Pro</SelectItem>
<SelectItem value="gpt-4.1">GPT-4.1</SelectItem>
</SelectContent>
</Select>
<FormDescription>
Choose which AI model to use for generating personas
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{/* Number of Personas to Generate */}
<FormField
control={form.control}

View file

@ -244,16 +244,38 @@ const DiscussionGuideViewer: React.FC<DiscussionGuideViewerProps> = React.memo((
});
};
const deleteEditingSectionItem = (itemId: string, itemType: 'question' | 'activity') => {
if (!editingSection) return;
const deleteEditingSectionItem = async (itemId: string, itemType: 'question' | 'activity') => {
if (!editingSection || !structuredGuide || !onSave) return;
const itemArray = itemType === 'question' ? 'questions' : 'activities';
const filteredItems = editingSection[itemArray]?.filter(item => item.id !== itemId) || [];
setEditingSection({
const updatedEditingSection = {
...editingSection,
[itemArray]: filteredItems
});
};
// Update local state immediately
setEditingSection(updatedEditingSection);
// Auto-save the changes to persist them immediately
try {
const updatedGuide = {
...structuredGuide,
sections: structuredGuide.sections.map(section =>
section.id === editingSection.id ? updatedEditingSection : section
)
};
await onSave(updatedGuide);
toast.success('Item deleted successfully');
} catch (error) {
console.error('Error deleting item:', error);
toast.error('Failed to delete item');
// Revert local state on error
setEditingSection(editingSection);
}
};
const addEditingSectionItem = (itemType: 'question' | 'activity') => {

View file

@ -13,6 +13,7 @@ export interface FocusGroup {
duration: number;
topic: string;
discussionGuide?: string;
llm_model?: string;
created_at?: string;
created_by?: string;
updated_at?: string;

View file

@ -21,6 +21,18 @@ api.interceptors.request.use(
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// Debug logging for focus group updates
if (config.method === 'put' && config.url?.includes('/focus-groups/')) {
console.log('🌐 API Request:', {
method: config.method,
url: config.url,
baseURL: config.baseURL,
fullURL: `${config.baseURL}${config.url}`,
data: config.data
});
}
return config;
},
(error) => Promise.reject(error)
@ -206,16 +218,21 @@ export const aiPersonasApi = {
researchObjective?: string,
count: number = 5,
temperature: number = 0.7,
customerDataSessionId?: string
customerDataSessionId?: string,
llmModel?: string
) => {
try {
// Log the API call with model information
console.log(`📡 API call to generate-basic-profiles with model: ${llmModel || 'gemini-2.5-pro'}`);
// First stage: Generate basic profiles
const basicProfilesResponse = await api.post('/ai-personas/generate-basic-profiles', {
audience_brief: audienceBrief,
research_objective: researchObjective,
count,
temperature: 0.7, // Use 0.7 temperature for basic profiles
customer_data_session_id: customerDataSessionId
customer_data_session_id: customerDataSessionId,
llm_model: llmModel || 'gemini-2.5-pro'
}, {
timeout: 600000 // 10 minutes for basic profile generation
});
@ -225,12 +242,16 @@ export const aiPersonasApi = {
const personaIds = [];
const errors = [];
// Log the second stage API calls with model information
console.log(`📡 API call to complete-and-save-persona with model: ${llmModel || 'gemini-2.5-pro'}`);
// Second stage: Complete each profile in parallel
const completeRequests = basicProfiles.map(profile =>
api.post('/ai-personas/complete-and-save-persona', {
basic_profile: profile,
temperature,
customer_data_session_id: customerDataSessionId
customer_data_session_id: customerDataSessionId,
llm_model: llmModel || 'gemini-2.5-pro'
}, {
timeout: 600000 // 10 minutes for each persona completion
})
@ -290,13 +311,18 @@ export const aiPersonasApi = {
}),
// Batch generate summaries for download
batchGenerateSummaries: (personaIds: string[], temperature: number = 0.7) =>
api.post('/ai-personas/batch-generate-summaries', {
batchGenerateSummaries: (personaIds: string[], temperature: number = 0.7, llmModel?: string) => {
// Log the API call with model information
console.log(`📡 Frontend: API call to batch-generate-summaries with model: ${llmModel || 'gemini-2.5-pro'}`);
return api.post('/ai-personas/batch-generate-summaries', {
persona_ids: personaIds,
temperature
temperature,
llm_model: llmModel || 'gemini-2.5-pro'
}, {
timeout: 900000 // 15 minutes timeout for batch processing
}),
});
},
// Upload customer data files for parsing
uploadCustomerData: (files: FileList) => {

View file

@ -8,12 +8,16 @@ import {
ClipboardList,
BarChart,
PlayCircle,
StickyNote
StickyNote,
Settings,
Bot
} from 'lucide-react';
import { toastService } from '@/lib/toast';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import Navigation from '@/components/Navigation';
import ParticipantPanel from '@/components/focus-group-session/ParticipantPanel';
import DiscussionPanel from '@/components/focus-group-session/DiscussionPanel';
@ -48,6 +52,11 @@ const FocusGroupSession = () => {
// Notes-related state
const [notes, setNotes] = useState<Note[]>([]);
// Model settings state
const [showModelSettings, setShowModelSettings] = useState(false);
const [selectedModel, setSelectedModel] = useState<string>('');
const [isUpdatingModel, setIsUpdatingModel] = useState(false);
const [isNoteModalOpen, setIsNoteModalOpen] = useState(false);
const [sessionStartTime, setSessionStartTime] = useState<Date | null>(null);
@ -416,10 +425,12 @@ const FocusGroupSession = () => {
date: data.date || new Date().toISOString(),
duration: data.duration || 60,
topic: data.topic || 'general',
discussionGuide: data.discussionGuide || ''
discussionGuide: data.discussionGuide || '',
llm_model: data.llm_model || 'gemini-2.5-pro'
};
setFocusGroup(focusGroupData);
setSelectedModel(focusGroupData.llm_model || 'gemini-2.5-pro');
// Handle participants
if (data.participants_data && Array.isArray(data.participants_data)) {
@ -466,6 +477,44 @@ const FocusGroupSession = () => {
}
};
// Function to update the LLM model for this focus group
const updateFocusGroupModel = async (newModel: string) => {
console.log('🔧 updateFocusGroupModel called with:', { id, focusGroup: !!focusGroup, newModel });
if (!id || !focusGroup) {
console.log('❌ updateFocusGroupModel: Missing id or focusGroup', { id, focusGroup: !!focusGroup });
return;
}
setIsUpdatingModel(true);
try {
console.log('🔧 Making API call to update focus group model:', { id, newModel });
const response = await focusGroupsApi.update(id, {
llm_model: newModel
});
console.log('🔧 API response:', response);
if (response && response.data) {
setFocusGroup(prev => prev ? { ...prev, llm_model: newModel } : null);
toastService.success('AI Model Updated', {
description: `Focus group will now use ${newModel === 'gemini-2.5-pro' ? 'Gemini 2.5 Pro' : 'GPT-4.1'} for AI responses`
});
setShowModelSettings(false);
console.log('✅ Model update successful');
} else {
console.log('❌ API response missing data:', response);
}
} catch (error) {
console.error('❌ Error updating focus group model:', error);
toastService.error('Failed to update AI model', {
description: 'There was an error updating the AI model. Please try again.'
});
} finally {
setIsUpdatingModel(false);
}
};
useEffect(() => {
console.log("Looking for focus group with ID:", id);
@ -498,10 +547,12 @@ const FocusGroupSession = () => {
date: data.date || new Date().toISOString(),
duration: data.duration || 60,
topic: data.topic || 'general',
discussionGuide: data.discussionGuide || ''
discussionGuide: data.discussionGuide || '',
llm_model: data.llm_model || 'gemini-2.5-pro'
};
setFocusGroup(focusGroupData);
setSelectedModel(focusGroupData.llm_model || 'gemini-2.5-pro');
// Handle participants
if (data.participants_data && Array.isArray(data.participants_data)) {
@ -1440,6 +1491,12 @@ const FocusGroupSession = () => {
<div>
<h1 className="font-sf text-2xl font-bold text-slate-900">{focusGroup.name}</h1>
<p className="text-slate-600">{new Date(focusGroup.date).toLocaleString()}</p>
<div className="flex items-center mt-1">
<Bot className="h-3 w-3 text-slate-500 mr-1" />
<Badge variant="secondary" className="text-xs">
{focusGroup.llm_model === 'gpt-4.1' ? 'GPT-4.1' : 'Gemini 2.5 Pro'}
</Badge>
</div>
</div>
</div>
@ -1453,6 +1510,14 @@ const FocusGroupSession = () => {
AI Dashboard
</Button>
<Button
variant="outline"
onClick={() => setShowModelSettings(true)}
>
<Settings className="mr-2 h-4 w-4" />
AI Model
</Button>
<Button variant="outline" onClick={downloadTranscript}>
<Download className="mr-2 h-4 w-4" />
Download Transcript
@ -1779,6 +1844,75 @@ const FocusGroupSession = () => {
</DialogContent>
</Dialog>
{/* Model Settings Dialog */}
<Dialog open={showModelSettings} onOpenChange={setShowModelSettings}>
<DialogContent>
<DialogHeader>
<DialogTitle>AI Model Settings</DialogTitle>
<DialogDescription>
Choose which AI model to use for generating responses and discussion guides in this focus group.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="flex items-center space-x-2">
<Bot className="h-4 w-4 text-slate-500" />
<span className="text-sm font-medium">Current Model:</span>
<Badge variant="secondary">
{focusGroup?.llm_model === 'gpt-4.1' ? 'GPT-4.1' : 'Gemini 2.5 Pro'}
</Badge>
</div>
<div>
<label className="text-sm font-medium">Select AI Model:</label>
<Select value={selectedModel} onValueChange={(value) => {
console.log('🔧 Model selection changed:', { from: selectedModel, to: value });
setSelectedModel(value);
}}>
<SelectTrigger className="mt-1">
<SelectValue placeholder="Select AI model" />
</SelectTrigger>
<SelectContent>
<SelectItem value="gemini-2.5-pro">Gemini 2.5 Pro</SelectItem>
<SelectItem value="gpt-4.1">GPT-4.1</SelectItem>
</SelectContent>
</Select>
</div>
<div className="text-xs text-slate-600">
<p><strong>Gemini 2.5 Pro:</strong> Google's advanced model, great for creative and analytical tasks.</p>
<p><strong>GPT-4.1:</strong> OpenAI's latest model, excellent for conversational and reasoning tasks.</p>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setShowModelSettings(false)}
disabled={isUpdatingModel}
>
Cancel
</Button>
<Button
onClick={() => {
console.log('🔧 Update button clicked:', {
selectedModel,
currentModel: focusGroup?.llm_model,
isDisabled: isUpdatingModel || selectedModel === focusGroup?.llm_model
});
updateFocusGroupModel(selectedModel);
}}
disabled={isUpdatingModel || selectedModel === focusGroup?.llm_model}
>
{isUpdatingModel && (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
)}
{isUpdatingModel ? 'Updating...' : 'Update Model'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Autonomous Dashboard */}
<AutonomousDashboard
focusGroupId={id!}

View file

@ -114,6 +114,9 @@ const SyntheticUsers = () => {
const [isSummaryGenerating, setIsSummaryGenerating] = useState(false);
const [summaryGenerationComplete, setSummaryGenerationComplete] = useState(false);
const [summaryGenerationError, setSummaryGenerationError] = useState(false);
// LLM selection for download
const [downloadLlmModalOpen, setDownloadLlmModalOpen] = useState(false);
const [selectedDownloadLlmModel, setSelectedDownloadLlmModel] = useState<string>('gemini-2.5-pro');
// Handle summary generation progress completion
const handleSummaryProgressComplete = () => {
@ -943,18 +946,30 @@ true;
// Download persona summary for current folder
const downloadPersonaSummary = async () => {
const folderName = selectedFolder === DEFAULT_FOLDER_ID
? 'All Personas'
: folders.find(f => f.id === selectedFolder)?.name || 'Unknown Folder';
if (filteredPersonas.length === 0) {
toastService.error("No personas to download");
return;
}
// Show LLM selection modal immediately
setDownloadLlmModalOpen(true);
};
// Handle the actual download with selected model
const handleDownloadWithModel = async () => {
const folderName = selectedFolder === DEFAULT_FOLDER_ID
? 'All Personas'
: folders.find(f => f.id === selectedFolder)?.name || 'Unknown Folder';
// Extract persona IDs, using _id for database personas or id as fallback
const personaIds = filteredPersonas.map(persona => persona._id || persona.id);
// Log user's model selection
console.log(`🤖 Frontend: User selected ${selectedDownloadLlmModel} for persona summary download`);
// Close modal
setDownloadLlmModalOpen(false);
// Reset progress states and start generation
setIsSummaryGenerating(true);
setSummaryGenerationComplete(false);
@ -968,7 +983,7 @@ true;
});
// Call the new API endpoint for batch summary generation
const response = await aiPersonasApi.batchGenerateSummaries(personaIds, 0.7);
const response = await aiPersonasApi.batchGenerateSummaries(personaIds, 0.7, selectedDownloadLlmModel);
const { summaries, summary_stats, errors } = response.data;
// Generate markdown content from LLM-processed summaries
@ -1034,20 +1049,32 @@ true;
// Mark generation as complete
setSummaryGenerationComplete(true);
// Show success toast with details
// Show success toast with details including model information
const modelDisplayName = selectedDownloadLlmModel === 'gpt-4.1' ? 'GPT-4.1' : 'Gemini 2.5 Pro';
if (summary_stats.total_successful === summary_stats.total_requested) {
toastService.success("Persona summary downloaded", {
description: `Successfully processed all ${summary_stats.total_successful} persona${summary_stats.total_successful !== 1 ? 's' : ''} from "${folderName}"`
description: `Successfully processed all ${summary_stats.total_successful} persona${summary_stats.total_successful !== 1 ? 's' : ''} from "${folderName}" using ${modelDisplayName}`
});
} else {
toastService.success("Persona summary downloaded with warnings", {
description: `Processed ${summary_stats.total_successful} of ${summary_stats.total_requested} personas from "${folderName}"`
description: `Processed ${summary_stats.total_successful} of ${summary_stats.total_requested} personas from "${folderName}" using ${modelDisplayName}`
});
}
} catch (error) {
console.error("Error generating persona summaries:", error);
// Log detailed error information
if (error.response) {
console.error("Error response data:", error.response.data);
console.error("Error response status:", error.response.status);
console.error("Error response headers:", error.response.headers);
} else if (error.request) {
console.error("Error request:", error.request);
} else {
console.error("Error message:", error.message);
}
// Mark generation as failed
setSummaryGenerationError(true);
@ -1815,6 +1842,54 @@ true;
</div>
</DialogContent>
</Dialog>
{/* LLM Selection Modal for Download */}
<Dialog
open={downloadLlmModalOpen}
onOpenChange={setDownloadLlmModalOpen}
>
<DialogContent>
<DialogHeader>
<DialogTitle>Select AI Model for Summary Generation</DialogTitle>
<DialogDescription>
Choose which AI model to use for generating persona summaries
</DialogDescription>
</DialogHeader>
<div className="py-4">
<RadioGroup
value={selectedDownloadLlmModel}
onValueChange={setSelectedDownloadLlmModel}
className="space-y-3"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="gemini-2.5-pro" id="download-gemini" />
<Label htmlFor="download-gemini" className="text-sm font-medium">
Gemini 2.5 Pro
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="gpt-4.1" id="download-gpt" />
<Label htmlFor="download-gpt" className="text-sm font-medium">
GPT-4.1
</Label>
</div>
</RadioGroup>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setDownloadLlmModalOpen(false)}
>
Cancel
</Button>
<Button onClick={handleDownloadWithModel}>
Generate Summary
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
</>

View file

@ -12,6 +12,7 @@ import { aiPersonasApi, personasApi } from "@/lib/api";
* @param count Number of personas to generate
* @param file Optional data file to assist in generation (not currently used)
* @param targetFolderId Optional folder ID to assign to generated personas
* @param llmModel Optional LLM model to use for generation
* @returns Array of generated personas
*/
export async function generateSyntheticPersonas(
@ -19,10 +20,12 @@ export async function generateSyntheticPersonas(
researchObjective?: string,
count: number,
file?: FileList,
targetFolderId?: string | null
targetFolderId?: string | null,
llmModel?: string
): Promise<Persona[]> {
// Debug logging for folder
// Debug logging for folder and model
console.log(`generateSyntheticPersonas called with targetFolderId: ${targetFolderId || 'none'}`);
console.log(`🔄 generateSyntheticPersonas using model: ${llmModel || 'gemini-2.5-pro'}`);
try {
// We'll use the two-stage approach which leverages parallel processing
@ -53,7 +56,8 @@ export async function generateSyntheticPersonas(
researchObjective, // Pass the research objective for focused goals and scenarios
count, // Number of personas to generate
0.8, // Temperature - slightly higher for more creativity
customerDataSessionId // Pass customer data session ID if available
customerDataSessionId, // Pass customer data session ID if available
llmModel // Pass the LLM model selection
);
if (response.data) {