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:
parent
f0ac4a14de
commit
96b9bfeedd
4 changed files with 194 additions and 2 deletions
|
|
@ -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
|
||||
|
|
|
|||
125
backend/app/services/focus_group_summary_service.py
Normal file
125
backend/app/services/focus_group_summary_service.py
Normal 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)}")
|
||||
27
backend/prompts/focus-group-summary-generation.md
Normal file
27
backend/prompts/focus-group-summary-generation.md
Normal 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"
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue