diff --git a/CLAUDE.md b/CLAUDE.md
index 7c83d66d..40d1f8ef 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -15,7 +15,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- **Start Backend**: `python run.py` (from backend/ directory)
- **Backend Server**: Runs on port 5137 with Hypercorn ASGI server
- **Database**: MongoDB with PyMongo
-- **Authentication**: JWT tokens via Flask-JWT-Extended
+- **Authentication**: Custom Quart-compatible JWT (not Flask-JWT-Extended)
## Testing
**Python Backend**: After modifying any Python files:
@@ -26,9 +26,30 @@ python -c "from app import create_app; app = create_app()" # Test app creation
```
**Frontend**: Run `npm run build` to verify TypeScript compilation
+## Architecture Overview
+
+### Real-Time Communication
+The application uses Socket.IO for real-time WebSocket communication between frontend and backend:
+- **Backend**: `python-socketio` with `AsyncServer` wrapped in ASGI app
+- **Frontend**: `socket.io-client` managed via `WebSocketContext`
+- WebSocket manager (`websocket_manager_async.py`) handles room-based messaging for focus group sessions
+
+### Autonomous Conversation System
+Focus group sessions can run autonomously with AI-driven conversations:
+- `ai_runner_service.py` - Manages background task execution for autonomous mode
+- `autonomous_conversation_controller.py` - Orchestrates multi-persona conversations
+- `conversation_decision_service.py` - Determines next speaker and conversation flow
+- `conversation_context_service.py` - Maintains conversation state and history
+
+### LLM Integration
+Multi-model support through `llm_service.py`:
+- **Google Gemini** (`gemini-3-pro-preview`) - Default model
+- **OpenAI** (`gpt-4.1`, `gpt-5`) - Alternative models
+- Prompts are stored as markdown templates in `/backend/prompts/`
+
## Code Style Guidelines
- **Imports**: Group imports by source (React, third-party, local)
-- **Types**: Use TypeScript. Project allows nullable types (`strictNullChecks: false`)
+- **Types**: Use TypeScript. Project allows nullable types (`strictNullChecks: false`)
- **Components**: Use functional components with hooks
- **Naming**: Use PascalCase for components, camelCase for variables/functions
- **Formatting**: Follow ESLint recommendations, focus on readability
@@ -41,12 +62,13 @@ python -c "from app import create_app; app = create_app()" # Test app creation
## Project Stack
**Frontend**: Vite, React 18, TypeScript, Tailwind CSS, shadcn-ui
-**Backend**: Flask 2.2.3, Hypercorn, PyMongo, JWT Extended
-**Key Libraries**:
+**Backend**: Quart (async Flask), Hypercorn ASGI, PyMongo, python-socketio
+**Key Libraries**:
- UI: Radix UI components, Lucide React icons
- State: TanStack Query, React Hook Form with Zod validation
- Routing: React Router DOM
-- AI/LLM: OpenAI, Google Generative AI
+- AI/LLM: OpenAI, Google Generative AI (genai)
+- Real-time: Socket.IO (client and server)
- Charts: Recharts
- Drag & Drop: DND Kit
@@ -58,15 +80,15 @@ python -c "from app import create_app; app = create_app()" # Test app creation
## File Organization
- **Backend Services**: `/backend/app/services/` - Business logic and AI integrations
-- **Backend Models**: `/backend/app/models/` - Data models (User, FocusGroup, Persona)
-- **Backend Routes**: `/backend/app/routes/` - API endpoints
-- **AI Prompts**: `/backend/prompts/` - LLM prompt templates
-- **Frontend Components**:
+- **Backend Models**: `/backend/app/models/` - Data models (User, FocusGroup, Persona, Folder)
+- **Backend Routes**: `/backend/app/routes/` - API endpoints (auth, personas, focus-groups, ai-personas, folders, tasks)
+- **AI Prompts**: `/backend/prompts/` - LLM prompt templates (markdown files loaded by `prompt_loader.py`)
+- **Frontend Components**:
- `/src/components/ui/` - Reusable shadcn-ui components
- - `/src/components/focus-group-session/` - Focus group specific components
+ - `/src/components/focus-group-session/` - Focus group session UI (DiscussionPanel, ParticipantPanel, ThemesPanel, etc.)
- `/src/components/persona/` - Persona management components
- **Types**: `/src/types/` - TypeScript type definitions
-- **Contexts**: `/src/contexts/` - React context providers
+- **Contexts**: `/src/contexts/` - React context providers (AuthContext, WebSocketContext, NavigationContext)
## Environment Configuration
diff --git a/backend/app/models/focus_group.py b/backend/app/models/focus_group.py
index bc819754..4ffecdbc 100644
--- a/backend/app/models/focus_group.py
+++ b/backend/app/models/focus_group.py
@@ -82,7 +82,7 @@ class FocusGroup:
# Set default LLM model if not provided
if "llm_model" not in focus_group_data:
- focus_group_data["llm_model"] = "gemini-2.5-pro"
+ focus_group_data["llm_model"] = "gemini-3-pro-preview"
# Set default GPT-5 parameters if not provided
if "reasoning_effort" not in focus_group_data:
diff --git a/backend/app/routes/ai_personas.py b/backend/app/routes/ai_personas.py
index 79d5aba1..3f1af550 100644
--- a/backend/app/routes/ai_personas.py
+++ b/backend/app/routes/ai_personas.py
@@ -67,7 +67,7 @@ async def generate_basic_profiles():
temperature = 1.0
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
+ llm_model = data.get('llm_model', 'gemini-3-pro-preview') # Optional parameter with default
try:
# Register current task for cancellation
@@ -197,7 +197,7 @@ async def complete_and_save_persona():
temperature = 1.0
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
+ llm_model = data.get('llm_model', 'gemini-3-pro-preview') # Optional parameter with default
# Get persona name for logging
persona_name = basic_profile.get('name', 'Unknown')
@@ -816,7 +816,7 @@ async def batch_generate_summaries():
if not (0 <= temperature <= 1.5):
temperature = 1.0
- llm_model = data.get('llm_model', 'gemini-2.5-pro') # Optional parameter with default
+ llm_model = data.get('llm_model', 'gemini-3-pro-preview') # 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}")
@@ -1013,7 +1013,7 @@ async def generate_personas_full():
"count": 5,
"temperature": 0.8,
"customer_data_session_id": "optional_session_id",
- "llm_model": "gemini-2.5-pro",
+ "llm_model": "gemini-3-pro-preview",
"target_folder_id": "optional_folder_id"
}
@@ -1038,7 +1038,7 @@ async def generate_personas_full():
temperature = 1.0
customer_data_session_id = data.get('customer_data_session_id')
- llm_model = data.get('llm_model', 'gemini-2.5-pro')
+ llm_model = data.get('llm_model', 'gemini-3-pro-preview')
target_folder_id = data.get('target_folder_id')
try:
diff --git a/backend/app/routes/focus_group_ai.py b/backend/app/routes/focus_group_ai.py
index 3c2df9df..7788f01a 100644
--- a/backend/app/routes/focus_group_ai.py
+++ b/backend/app/routes/focus_group_ai.py
@@ -84,7 +84,7 @@ async def generate_ai_response():
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"
+ log_msg += f"🤖 [{datetime.datetime.now()}] AI RESPONSE - Using model: {llm_model or 'default (gemini-3-pro-preview)'} for focus group {focus_group_id}\n"
with open('/tmp/focus_group_debug.log', 'a') as f:
f.write(log_msg)
f.flush()
@@ -93,7 +93,7 @@ async def generate_ai_response():
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}")
+ current_app.logger.info(f"🤖 Generating AI response using model: {llm_model or 'default (gemini-3-pro-preview)'} for focus group {focus_group_id}")
# Validate persona exists
persona = await Persona.find_by_id(persona_id)
diff --git a/backend/app/routes/personas.py b/backend/app/routes/personas.py
index c30fbd7e..c5acd216 100644
--- a/backend/app/routes/personas.py
+++ b/backend/app/routes/personas.py
@@ -173,7 +173,7 @@ async def modify_persona_with_ai(persona_id):
Request body should include:
- modification_prompt: Natural language description of desired changes
- - llm_model: Model to use (defaults to 'gemini-2.5-pro')
+ - llm_model: Model to use (defaults to 'gemini-3-pro-preview')
- reasoning_effort: For GPT-5 (minimal, low, medium, high)
- verbosity: For GPT-5 (low, medium, high)
- preview_only: If true, returns modified data without saving to database (defaults to false)
@@ -188,7 +188,7 @@ async def modify_persona_with_ai(persona_id):
if not modification_prompt:
return jsonify({"error": "modification_prompt is required"}), 400
- llm_model = request_data.get('llm_model', 'gemini-2.5-pro')
+ llm_model = request_data.get('llm_model', 'gemini-3-pro-preview')
reasoning_effort = request_data.get('reasoning_effort', 'medium')
verbosity = request_data.get('verbosity', 'medium')
preview_only = request_data.get('preview_only', False)
diff --git a/backend/app/services/ai_persona_service.py b/backend/app/services/ai_persona_service.py
index eee84842..62e0dc43 100644
--- a/backend/app/services/ai_persona_service.py
+++ b/backend/app/services/ai_persona_service.py
@@ -218,7 +218,7 @@ async def _generate_basic_personas_attempt(
# Log the LLM API call with attempt number
attempt_text = f" (attempt {attempt})" if attempt > 1 else ""
- print(f"🤖 Backend: Making LLM API call to {llm_model or 'gemini-2.5-pro'} for basic persona generation{attempt_text}")
+ print(f"🤖 Backend: Making LLM API call to {llm_model or 'gemini-3-pro-preview'} for basic persona generation{attempt_text}")
raw_response = await LLMService.generate_content(
prompt=final_prompt,
@@ -504,7 +504,7 @@ async def generate_persona(
# 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}'")
+ print(f"🤖 Backend: Making LLM API call to {llm_model or 'gemini-3-pro-preview'} for detailed persona generation of '{persona_name}'")
persona_data = await LLMService.generate_structured_response(
prompt=final_prompt,
@@ -589,7 +589,7 @@ async def generate_persona_summary(
# 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}'")
+ print(f"🤖 Backend: Making LLM API call to {llm_model or 'gemini-3-pro-preview'} for summary generation of '{persona_name}'")
raw_response = await LLMService.generate_content(
prompt=final_prompt,
@@ -694,7 +694,7 @@ async def generate_persona_download_summary(
# 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}'")
+ print(f"🤖 Backend: Making LLM API call to {llm_model or 'gemini-3-pro-preview'} for download summary of '{persona_name}'")
# Generate the markdown content directly
markdown_response = await LLMService.generate_content(
diff --git a/backend/app/services/autonomous_conversation_controller.py b/backend/app/services/autonomous_conversation_controller.py
index 5dbbd967..c0547f88 100644
--- a/backend/app/services/autonomous_conversation_controller.py
+++ b/backend/app/services/autonomous_conversation_controller.py
@@ -664,7 +664,7 @@ class AutonomousConversationController:
llm_model = focus_group.get('llm_model')
reasoning_effort = focus_group.get('reasoning_effort', 'medium')
verbosity = focus_group.get('verbosity', 'medium')
- self.logger.info(f"🤖 Autonomous conversation using model: {llm_model or 'default (gemini-2.5-pro)'} for focus group {self.focus_group_id}")
+ self.logger.info(f"🤖 Autonomous conversation using model: {llm_model or 'default (gemini-3-pro-preview)'} for focus group {self.focus_group_id}")
# Get recent messages
messages = await FocusGroup.get_messages(self.focus_group_id)
diff --git a/backend/app/services/focus_group_response_service.py b/backend/app/services/focus_group_response_service.py
index 858f52c7..67da7b3d 100644
--- a/backend/app/services/focus_group_response_service.py
+++ b/backend/app/services/focus_group_response_service.py
@@ -52,7 +52,7 @@ async def generate_persona_response(
if llm_model == 'gpt-5':
print(f" - llm_model: {llm_model} (reasoning_effort: {reasoning_effort or 'medium'}, verbosity: {verbosity or 'medium'}) [using Responses API]")
else:
- print(f" - llm_model: {llm_model or 'default (gemini-2.5-pro)'}")
+ print(f" - llm_model: {llm_model or 'default (gemini-3-pro-preview)'}")
# Import LLMService at the top to avoid scoping issues
from app.services.llm_service import LLMService
diff --git a/backend/app/services/focus_group_service.py b/backend/app/services/focus_group_service.py
index d3ba0f2e..7060d76f 100644
--- a/backend/app/services/focus_group_service.py
+++ b/backend/app/services/focus_group_service.py
@@ -224,7 +224,7 @@ class FocusGroupService:
'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"=== CREATIVE REVIEW VALIDATION RESULTS (Model: {llm_model or 'gemini-3-pro-preview'}) ===")
logger.info(f"Found {creative_review_count} creative_review activities for {len(uploaded_assets)} uploaded assets")
if creative_review_activities:
@@ -236,7 +236,7 @@ class FocusGroupService:
# 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"❌ This suggests {llm_model or 'gemini-2.5-pro'} is not following the creative asset instructions")
+ logger.warning(f"❌ This suggests {llm_model or 'gemini-3-pro-preview'} 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:
diff --git a/backend/app/services/key_theme_service.py b/backend/app/services/key_theme_service.py
index 2ffc3683..9eaa8370 100644
--- a/backend/app/services/key_theme_service.py
+++ b/backend/app/services/key_theme_service.py
@@ -41,7 +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)'}")
+ logger.info(f"Using LLM model: {llm_model or 'default (gemini-3-pro-preview)'}")
try:
# Get the focus group
@@ -105,7 +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)'}")
+ logger.info(f"Theme extraction using LLM model: {llm_model or 'default (gemini-3-pro-preview)'}")
try:
# Load and prepare the prompt for the LLM
@@ -135,7 +135,7 @@ class KeyThemeService:
for attempt in range(max_retries):
attempt_num = attempt + 1
- logger.info(f"Attempt {attempt_num}/{max_retries}: Calling LLM ({llm_model or 'gemini-2.5-pro'}) for theme generation")
+ logger.info(f"Attempt {attempt_num}/{max_retries}: Calling LLM ({llm_model or 'gemini-3-pro-preview'}) for theme generation")
try:
themes = await LLMService.generate_structured_array(
@@ -145,7 +145,7 @@ class KeyThemeService:
model_name=llm_model
)
- logger.info(f"Attempt {attempt_num}/{max_retries}: LLM ({llm_model or 'gemini-2.5-pro'}) call successful, received {len(themes)} themes")
+ logger.info(f"Attempt {attempt_num}/{max_retries}: LLM ({llm_model or 'gemini-3-pro-preview'}) call successful, received {len(themes)} themes")
# Validate the response structure
validated_themes = []
@@ -175,7 +175,7 @@ class KeyThemeService:
validated_themes.append(validated_theme)
- logger.info(f"Theme generation completed successfully with {len(validated_themes)} validated themes using {llm_model or 'gemini-2.5-pro'}")
+ logger.info(f"Theme generation completed successfully with {len(validated_themes)} validated themes using {llm_model or 'gemini-3-pro-preview'}")
return validated_themes
except LLMServiceError as e:
diff --git a/backend/app/services/llm_service.py b/backend/app/services/llm_service.py
index 8acba2cb..3f676899 100644
--- a/backend/app/services/llm_service.py
+++ b/backend/app/services/llm_service.py
@@ -25,12 +25,12 @@ gemini_client = genai.Client(api_key=GEMINI_API_KEY)
OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY', 'REDACTED_OPENAI_KEY')
openai_client = AsyncOpenAI(api_key=OPENAI_API_KEY, timeout=600.0)
-# The default model we're using
-DEFAULT_MODEL = "gemini-2.5-pro"
+# The default model we're using
+DEFAULT_MODEL = "gemini-3-pro-preview"
# Supported models
SUPPORTED_MODELS = {
- 'gemini-2.5-pro': 'gemini',
+ 'gemini-3-pro-preview': 'gemini',
'gpt-4.1': 'openai',
'gpt-5': 'openai'
}
diff --git a/backend/app/services/persona_modification_service.py b/backend/app/services/persona_modification_service.py
index bad46854..487681c7 100644
--- a/backend/app/services/persona_modification_service.py
+++ b/backend/app/services/persona_modification_service.py
@@ -134,7 +134,7 @@ class PersonaModificationService:
async def modify_persona(
persona_id: str,
modification_prompt: str,
- llm_model: str = 'gemini-2.5-pro',
+ llm_model: str = 'gemini-3-pro-preview',
reasoning_effort: str = 'medium',
verbosity: str = 'medium',
max_retries: int = 3,
diff --git a/src/components/AIRecruiter.tsx b/src/components/AIRecruiter.tsx
index 0a8028a7..edf636fa 100644
--- a/src/components/AIRecruiter.tsx
+++ b/src/components/AIRecruiter.tsx
@@ -159,7 +159,7 @@ export default function AIRecruiter({ targetFolderId, targetFolderName }: AIRecr
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 using ${values.llm_model || 'Gemini 2.5 Pro'}. ${response.errors?.length || 0} failed due to timeout or other errors.`,
+ description: `${personas.length} synthetic personas were created using ${values.llm_model || 'Gemini 3 Pro'}. ${response.errors?.length || 0} failed due to timeout or other errors.`,
duration: 8000
});
@@ -175,7 +175,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 using ${values.llm_model || 'Gemini 2.5 Pro'} and saved ${targetFolderId ? `to the "${targetFolderName}" folder` : 'to the database'}.`
+ description: `${personas.length} synthetic personas have been created using ${values.llm_model || 'Gemini 3 Pro'} and saved ${targetFolderId ? `to the "${targetFolderName}" folder` : 'to the database'}.`
});
}
diff --git a/src/components/FocusGroupModerator.tsx b/src/components/FocusGroupModerator.tsx
index 16c85b98..964d8c9f 100644
--- a/src/components/FocusGroupModerator.tsx
+++ b/src/components/FocusGroupModerator.tsx
@@ -527,7 +527,7 @@ export default function FocusGroupModerator({ draftToEdit, onDraftSaved, preSele
focusGroupName: "",
discussionTopics: "",
duration: "60",
- llm_model: "gemini-2.5-pro",
+ llm_model: "gemini-3-pro-preview",
reasoning_effort: "medium",
verbosity: "medium",
},
@@ -554,7 +554,7 @@ export default function FocusGroupModerator({ draftToEdit, onDraftSaved, preSele
objective: values.researchBrief || '',
topic: values.discussionTopics || '',
duration: values.duration ? parseInt(values.duration) : 60,
- llm_model: values.llm_model || 'gemini-2.5-pro',
+ llm_model: values.llm_model || 'gemini-3-pro-preview',
reasoning_effort: values.reasoning_effort || 'medium',
verbosity: values.verbosity || 'medium',
participants: selectedParticipants,
@@ -692,7 +692,7 @@ export default function FocusGroupModerator({ draftToEdit, onDraftSaved, preSele
topic: values.discussionTopics || '',
description: values.researchBrief || '',
objective: values.researchBrief || '',
- llm_model: values.llm_model || 'gemini-2.5-pro',
+ llm_model: values.llm_model || 'gemini-3-pro-preview',
reasoning_effort: values.reasoning_effort || 'medium',
verbosity: values.verbosity || 'medium',
discussionGuide: sourceFocusGroup.discussionGuide
@@ -860,7 +860,7 @@ export default function FocusGroupModerator({ draftToEdit, onDraftSaved, preSele
objective: draftToEdit.description || draftToEdit.objective || '',
topic: draftToEdit.topic || '',
duration: draftToEdit.duration || 60,
- llm_model: draftToEdit.llm_model || 'gemini-2.5-pro',
+ llm_model: draftToEdit.llm_model || 'gemini-3-pro-preview',
reasoning_effort: draftToEdit.reasoning_effort || 'medium',
verbosity: draftToEdit.verbosity || 'medium',
participants: draftToEdit.participants || [],
@@ -1580,7 +1580,7 @@ true;
Gemini 2.5 Pro: Google's advanced model, great for creative and analytical tasks.
+Gemini 3 Pro: Google's advanced model, great for creative and analytical tasks.
GPT-4.1: OpenAI's latest model, excellent for conversational and reasoning tasks.
GPT-5: OpenAI's newest model with advanced reasoning and customizable response styles.