diff --git a/backend/app/routes/focus_groups.py b/backend/app/routes/focus_groups.py index d581fdd2..8522932d 100644 --- a/backend/app/routes/focus_groups.py +++ b/backend/app/routes/focus_groups.py @@ -871,7 +871,34 @@ async def generate_discussion_guide(focus_group_id=None): focus_group_id=focus_group_id, llm_model=llm_model ) - + + # Generate one-line summary for list view display + summary = None + try: + from app.services.focus_group_summary_service import generate_focus_group_summary + + summary_data = { + 'name': focus_group_name, + 'topic': formatted_topic, + 'duration': duration, + 'description': data.get('description', ''), + 'discussionGuide': discussion_guide + } + + summary = await generate_focus_group_summary( + summary_data, + llm_model=llm_model + ) + + # Save summary to focus group if we have an ID + if focus_group_id and summary: + await FocusGroup.update(focus_group_id, {'summary': summary}) + logger.info(f"Saved summary for focus group {focus_group_id}: {summary}") + + except Exception as summary_error: + logger.warning(f"Failed to generate summary (non-critical): {summary_error}") + # Don't fail the request - summary is optional + # Emit completion event via WebSocket if user_id: await websocket_manager.emit_to_user( @@ -888,6 +915,7 @@ async def generate_discussion_guide(focus_group_id=None): return jsonify({ "message": "Discussion guide generated successfully", "discussionGuide": discussion_guide, + "summary": summary, # Include the generated summary "success": True, "task_id": task_id }), 200 diff --git a/backend/app/services/focus_group_summary_service.py b/backend/app/services/focus_group_summary_service.py new file mode 100644 index 00000000..49ccb8b6 --- /dev/null +++ b/backend/app/services/focus_group_summary_service.py @@ -0,0 +1,125 @@ +""" +Focus Group Summary Service + +This service generates LLM-powered one-line summaries for focus groups, +designed for display in the focus group list view to help users quickly +understand what each session is about. +""" + +from typing import Dict, Any, Optional +import logging + +from app.services.llm_service import LLMService, LLMServiceError +from app.utils.prompt_loader import load_prompt, PromptLoaderError + +# Set up logger for this module +logger = logging.getLogger(__name__) + + +class FocusGroupSummaryError(Exception): + """Custom exception for focus group summary generation errors.""" + pass + + +async def generate_focus_group_summary( + focus_group_data: Dict[str, Any], + temperature: float = 0.7, + llm_model: Optional[str] = None +) -> Optional[str]: + """ + Generate a one-line summary of a focus group for display in list views. + + Args: + focus_group_data: Dictionary containing: + - name: Focus group name + - topic: Discussion topic + - duration: Session duration in minutes + - description: Research brief/objective + - discussionGuide: (optional) The generated discussion guide + temperature: LLM temperature setting (default 0.7 for balanced creativity) + llm_model: Optional LLM model override + + Returns: + A concise one-line summary string (max ~100 characters), or None on failure + """ + try: + # Extract data from focus group + name = focus_group_data.get('name', 'Unnamed Focus Group') + topic = focus_group_data.get('topic', 'General Research') + duration = focus_group_data.get('duration', 60) + description = focus_group_data.get('description') or focus_group_data.get('objective', '') + discussion_guide = focus_group_data.get('discussionGuide', '') + + # Build discussion guide section for prompt + discussion_guide_section = "" + if discussion_guide: + # Handle both string and dict formats for discussion guide + if isinstance(discussion_guide, dict): + guide_title = discussion_guide.get('title', '') + guide_sections = discussion_guide.get('sections', []) + guide_text = f"Discussion Guide Title: {guide_title}\n" + for section in guide_sections[:3]: # Limit to first 3 sections to avoid too much context + section_title = section.get('title', '') + section_content = section.get('content', '')[:200] # Truncate content + guide_text += f"- {section_title}: {section_content}...\n" + discussion_guide_section = f"Discussion Guide Overview:\n{guide_text}" + elif isinstance(discussion_guide, str) and len(discussion_guide) > 0: + # Truncate long guides + truncated_guide = discussion_guide[:500] + "..." if len(discussion_guide) > 500 else discussion_guide + discussion_guide_section = f"Discussion Guide Overview:\n{truncated_guide}" + + # Load and format the prompt + try: + final_prompt = load_prompt('focus-group-summary-generation', { + 'name': name, + 'topic': topic, + 'duration': str(duration), + 'description': description or 'Not specified', + 'discussion_guide_section': discussion_guide_section + }) + except PromptLoaderError as e: + logger.error(f"Error loading focus group summary prompt: {e}") + raise FocusGroupSummaryError(f"Error loading summary prompt: {str(e)}") + + # Log the LLM API call + logger.info(f"Generating summary for focus group: {name}") + print(f"🤖 Backend: Generating one-line summary for focus group '{name}' using {llm_model or 'default model'}") + + try: + raw_response = await LLMService.generate_content( + prompt=final_prompt, + temperature=temperature, + model_name=llm_model + ) + + # Clean up the response + summary = raw_response.strip() + + # Remove any quotes that might wrap the response + if summary.startswith('"') and summary.endswith('"'): + summary = summary[1:-1] + if summary.startswith("'") and summary.endswith("'"): + summary = summary[1:-1] + + # Remove any markdown formatting + if summary.startswith("```"): + summary = summary.strip("```").strip() + + # Truncate if too long (enforce max 150 characters for safety) + if len(summary) > 150: + summary = summary[:147] + "..." + + logger.info(f"Generated summary for '{name}': {summary}") + print(f"✅ Generated summary: {summary}") + + return summary + + except LLMServiceError as e: + logger.error(f"LLM service error generating summary: {e}") + raise FocusGroupSummaryError(f"Error from LLM service: {str(e)}") + + except FocusGroupSummaryError: + raise + except Exception as e: + logger.error(f"Unexpected error generating focus group summary: {e}") + raise FocusGroupSummaryError(f"Error generating focus group summary: {str(e)}") diff --git a/backend/prompts/focus-group-summary-generation.md b/backend/prompts/focus-group-summary-generation.md new file mode 100644 index 00000000..f0d673ba --- /dev/null +++ b/backend/prompts/focus-group-summary-generation.md @@ -0,0 +1,27 @@ +You are an expert at summarizing market research focus groups in a single, concise sentence. + +Given the focus group details below, generate a one-line summary (maximum 100 characters) that captures the essence of what this session will explore. + +## Guidelines: +- Keep it brief but informative (aim for 60-100 characters) +- Focus on the key research question or topic being explored +- Use action-oriented language when possible (e.g., "Exploring...", "Understanding...", "Evaluating...") +- Do not repeat the focus group name +- Make it scannable at a glance +- Focus on what makes this research valuable and interesting + +## Focus Group Details: +Name: {name} +Topic: {topic} +Duration: {duration} minutes +Research Brief: {description} + +{discussion_guide_section} + +## Required Output: +Return ONLY a single sentence summary. No quotes, no JSON, no explanation - just the summary text itself. + +Example outputs: +- "Exploring user frustrations with mobile checkout experiences" +- "Understanding brand perception among Gen Z consumers" +- "Evaluating new packaging designs for premium product line" diff --git a/src/pages/FocusGroups.tsx b/src/pages/FocusGroups.tsx index b227854a..980271d5 100644 --- a/src/pages/FocusGroups.tsx +++ b/src/pages/FocusGroups.tsx @@ -83,6 +83,8 @@ interface FocusGroup { duration: number; topic: string; discussionGuide?: string; + description?: string; + summary?: string; // LLM-generated one-line summary for list display created_at?: string; created_by?: string; updated_at?: string; @@ -391,7 +393,17 @@ const FocusGroups = () => { className="mt-1" />
+ {group.summary} +
+ ) : group.description ? ( ++ {group.description.length > 80 ? `${group.description.substring(0, 80)}...` : group.description} +
+ ) : null}