Add LLM-generated one-line summaries for focus groups in list view

- Create focus_group_summary_service.py to generate concise summaries
- Add prompt template for summary generation
- Integrate summary generation after discussion guide creation
- Display summary under focus group title in list view with fallback to description

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
michael 2025-12-04 08:51:28 -06:00
parent f0ac4a14de
commit 96b9bfeedd
4 changed files with 194 additions and 2 deletions

View file

@ -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

View file

@ -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)}")

View file

@ -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"

View file

@ -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"
/>
<div>
<h3 className="font-sf text-lg font-semibold mb-2">{group.name}</h3>
<h3 className="font-sf text-lg font-semibold mb-1">{group.name}</h3>
{/* One-line summary for quick scanning */}
{group.summary ? (
<p className="text-sm text-muted-foreground mb-2 line-clamp-1">
{group.summary}
</p>
) : group.description ? (
<p className="text-sm text-muted-foreground mb-2 line-clamp-1 italic">
{group.description.length > 80 ? `${group.description.substring(0, 80)}...` : group.description}
</p>
) : null}
<div className="flex flex-wrap items-center gap-3 text-sm text-muted-foreground">
<div className="flex items-center">
<Calendar className="h-4 w-4 mr-1" />