added gpt-4.1 support among other things
This commit is contained in:
parent
da7b2c0448
commit
b649793013
42 changed files with 1741 additions and 928 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
BIN
backend/.DS_Store
vendored
BIN
backend/.DS_Store
vendored
Binary file not shown.
BIN
backend/app/.DS_Store
vendored
BIN
backend/app/.DS_Store
vendored
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -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'])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}'")
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)}")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)}"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
732
dist/assets/index-Bk-FPBaP.js
vendored
732
dist/assets/index-Bk-FPBaP.js
vendored
File diff suppressed because one or more lines are too long
732
dist/assets/index-Dod4tGHl.js
vendored
Normal file
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
2
dist/index.html
vendored
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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') => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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!}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue