newpm-ai / modules /report_generator.py
Parimal Kalpande
deploy
2fd1b76
# modules/report_generator.py
import datetime
import os
import numpy as np
import matplotlib.pyplot as plt
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak, Image, HRFlowable
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_JUSTIFY, TA_CENTER
from reportlab.lib.units import inch
from reportlab.lib import colors
from modules.llm_handler import generate_coaching_feedback
import config
import tempfile
def define_skill_areas(coaching_type):
"""Define key skill areas based on product management coaching type."""
skill_mapping = {
'Product Strategy & Vision': ['Strategic Thinking', 'Vision Articulation', 'Business Alignment'],
'Market Research & Analysis': ['Research Skills', 'Data Analysis', 'Market Understanding'],
'User Experience & Design Thinking': ['User Empathy', 'Design Process', 'Problem Solving'],
'Product Roadmap Planning': ['Prioritization', 'Planning', 'Communication'],
'Metrics & Analytics': ['Data Literacy', 'Analytical Thinking', 'Decision Making'],
'Stakeholder Management': ['Communication', 'Negotiation', 'Relationship Building'],
'Product Launch Strategy': ['Execution', 'Planning', 'Cross-functional Leadership'],
'Competitive Analysis': ['Market Analysis', 'Strategic Thinking', 'Opportunity Recognition'],
'Feature Prioritization': ['Decision Making', 'Framework Application', 'Trade-off Analysis'],
'Customer Development': ['Customer Empathy', 'Research Skills', 'Insight Generation'],
'Resume & Application Strategy': ['Application Skills', 'Personal Branding', 'Interview Preparation']
}
return skill_mapping.get(coaching_type, ['Strategic Thinking', 'Problem Solving', 'Communication'])
def create_coaching_progress_chart(labels, file_path):
"""Create a visual representation of coaching areas covered with error handling."""
try:
plt.clf() # Clear any existing plots
fig, ax = plt.subplots(figsize=(8, 6))
y_pos = np.arange(len(labels))
# Create a progress-style chart
progress_values = [100] * len(labels) # All areas were covered
colors_list = plt.cm.Blues(np.linspace(0.4, 0.8, len(labels)))
bars = ax.barh(y_pos, progress_values, color=colors_list, alpha=0.7)
ax.set_yticks(y_pos)
ax.set_yticklabels(labels)
ax.set_xlabel('Coaching Coverage (%)')
ax.set_title('Product Management Skills Coaching Session', fontsize=14, fontweight='bold')
ax.set_xlim(0, 100)
# Add checkmarks to indicate completion
for i, bar in enumerate(bars):
ax.text(bar.get_width() - 10, bar.get_y() + bar.get_height()/2,
'βœ“', ha='center', va='center', fontsize=16, color='white', fontweight='bold')
plt.tight_layout()
plt.savefig(file_path, dpi=300, bbox_inches='tight')
plt.close(fig) # Explicitly close the figure
print(f"βœ… Chart saved to: {file_path}")
return True
except Exception as e:
print(f"❌ Error creating chart: {e}")
return False
plt.tight_layout()
plt.savefig(file_path, dpi=300, bbox_inches='tight')
plt.close(fig)
print(f"πŸ“ˆ Coaching progress chart saved to {file_path}")
def clean_text_for_pdf(text):
"""Remove markdown formatting and clean text for professional PDF appearance."""
import re
# Remove markdown bold/italic formatting - handle nested patterns
text = re.sub(r'\*\*\*(.*?)\*\*\*', r'\1', text) # Remove ***bold italic***
text = re.sub(r'\*\*(.*?)\*\*', r'\1', text) # Remove **bold**
text = re.sub(r'\*(.*?)\*', r'\1', text) # Remove *italic*
text = re.sub(r'__(.*?)__', r'\1', text) # Remove __bold__
text = re.sub(r'_(.*?)_', r'\1', text) # Remove _italic_
# Clean up section headers (remove markdown)
text = re.sub(r'#{1,6}\s*', '', text) # Remove # headers
text = re.sub(r'\*\*([A-Z\s]+[A-Za-z]):\*\*', r'\1:', text) # Clean section headers with colons
text = re.sub(r'\*\*([A-Z][A-Za-z\s]*)\*\*', r'\1', text) # Clean other bold headers
# Remove markdown links
text = re.sub(r'\[([^\]]+)\]\([^\)]+\)', r'\1', text)
# Clean up bullet points and list markers - preserve structure
text = re.sub(r'^\s*[-\*\+β€’]\s+', 'β€’ ', text, flags=re.MULTILINE)
text = re.sub(r'^\s*\d+\.\s+', '', text, flags=re.MULTILINE) # Remove numbered list markers
# Remove extra asterisks and underscores that might be left over
text = re.sub(r'\*+', '', text)
text = re.sub(r'_+', '', text)
# Clean up multiple spaces and normalize line breaks while preserving structure
text = re.sub(r'\n\s*\n\s*\n+', '\n\n', text) # Collapse multiple line breaks
text = re.sub(r'[ \t]+', ' ', text) # Normalize spaces but preserve line breaks
# Clean up common formatting artifacts
text = re.sub(r':\s*-', ':', text) # Remove dashes after colons
text = re.sub(r'\s+:', ':', text) # Remove spaces before colons
return text.strip()
def format_text_into_paragraphs(text, max_length=500):
"""Split long text into readable paragraphs for better PDF formatting."""
if not text or len(text) < max_length:
return [text] if text else []
# Split on double line breaks first (natural paragraph breaks)
paragraphs = text.split('\n\n')
formatted_paragraphs = []
for paragraph in paragraphs:
paragraph = paragraph.strip()
if not paragraph:
continue
# If paragraph is still too long, split on sentences
if len(paragraph) > max_length:
sentences = paragraph.split('. ')
current_para = ""
for sentence in sentences:
sentence = sentence.strip()
if not sentence:
continue
# Ensure sentence ends with period if it doesn't already
if not sentence.endswith('.') and not sentence.endswith('!') and not sentence.endswith('?'):
sentence += '.'
# Check if adding this sentence would exceed max length
if current_para and len(current_para + ' ' + sentence) > max_length:
formatted_paragraphs.append(current_para.strip())
current_para = sentence
else:
current_para = current_para + ' ' + sentence if current_para else sentence
# Add remaining text
if current_para:
formatted_paragraphs.append(current_para.strip())
else:
formatted_paragraphs.append(paragraph)
return [p for p in formatted_paragraphs if p.strip()]
def format_feedback_sections(feedback_text):
"""Break feedback into structured sections for better visual presentation."""
# First, clean the text of markdown formatting
cleaned_text = clean_text_for_pdf(feedback_text)
sections = []
current_section = ""
current_title = ""
lines = cleaned_text.split('\n')
for line in lines:
line = line.strip()
if not line:
continue
# Check if line is a section header (contains keywords followed by colon)
section_patterns = [
'EVALUATION SCORES', 'STRENGTHS', 'AREAS FOR GROWTH', 'FRAMEWORK RECOMMENDATIONS',
'KEY METRICS', 'ACTIONABLE NEXT STEPS', 'EXPERT INSIGHT', 'FEEDBACK',
'KEY STRENGTHS', 'RECOMMENDATIONS', 'IMPROVEMENT AREAS', 'NEXT STEPS'
]
is_header = False
for pattern in section_patterns:
if pattern in line.upper() and (':' in line or line.upper().endswith(pattern)):
is_header = True
break
if is_header:
# Save previous section if it exists
if current_title and current_section.strip():
# Format the section content into readable paragraphs
formatted_content = format_section_content(current_section.strip())
sections.append({
'title': current_title,
'content': formatted_content
})
# Start new section
current_title = line.replace(':', '').strip()
current_section = ""
else:
# Add to current section
current_section += line + "\n"
# Add final section
if current_title and current_section.strip():
formatted_content = format_section_content(current_section.strip())
sections.append({
'title': current_title,
'content': formatted_content
})
return sections
def format_section_content(content):
"""Format section content for better readability in PDF."""
if not content:
return ""
# Check if content contains bullet points or list items
lines = content.split('\n')
# If content has bullet points, format as list
if any(line.strip().startswith('β€’') for line in lines):
formatted_items = []
for line in lines:
line = line.strip()
if line and not line.startswith('β€’'):
# Add bullet if missing
line = f"β€’ {line}"
if line:
formatted_items.append(line)
return '\n'.join(formatted_items)
# If content contains scores/ratings, format specially
if any(char in content for char in ['/', '10', 'Score', 'Rating']):
# Keep score formatting intact
return content
# For regular text, format into readable paragraphs
paragraphs = format_text_into_paragraphs(content)
return '\n\n'.join(paragraphs)
def generate_pdf_report(coaching_data, file_path):
"""Generate a professional product management coaching report without markdown formatting."""
try:
doc = SimpleDocTemplate(file_path, pagesize=(8.5 * inch, 11 * inch))
styles = getSampleStyleSheet()
# Professional styles for coaching report - only add if they don't exist
def safe_add_style(styles, name, **kwargs):
"""Safely add a style only if it doesn't already exist"""
if name not in styles:
styles.add(ParagraphStyle(name=name, **kwargs))
safe_add_style(styles, 'ReportTitle',
parent=styles['Heading1'],
fontSize=24,
alignment=TA_CENTER,
spaceAfter=20,
textColor=colors.darkblue,
fontName='Helvetica-Bold'
)
safe_add_style(styles, 'SectionHeader',
parent=styles['Heading2'],
fontSize=16,
spaceBefore=15,
spaceAfter=10,
textColor=colors.darkblue,
fontName='Helvetica-Bold'
)
safe_add_style(styles, 'SubHeader',
parent=styles['Heading3'],
fontSize=14,
spaceBefore=12,
spaceAfter=8,
textColor=colors.darkgreen,
fontName='Helvetica-Bold'
)
safe_add_style(styles, 'BodyText',
parent=styles['Normal'],
fontSize=11,
spaceAfter=12,
spaceBefore=6,
alignment=TA_JUSTIFY,
fontName='Helvetica'
)
safe_add_style(styles, 'BulletText',
parent=styles['Normal'],
fontSize=11,
spaceAfter=8,
spaceBefore=4,
leftIndent=20,
fontName='Helvetica'
)
safe_add_style(styles, 'BulletPoint',
parent=styles['Normal'],
fontSize=11,
spaceAfter=6,
spaceBefore=3,
leftIndent=20,
fontName='Helvetica'
)
safe_add_style(styles, 'ScoreText',
parent=styles['Normal'],
fontSize=12,
spaceAfter=8,
spaceBefore=4,
textColor=colors.blue,
fontName='Helvetica-Bold'
)
safe_add_style(styles, 'HighlightBox',
parent=styles['Normal'],
fontSize=11,
spaceAfter=12,
spaceBefore=12,
leftIndent=15,
rightIndent=15,
borderWidth=1,
borderColor=colors.lightgrey,
backColor=colors.lightgrey,
fontName='Helvetica'
)
print("βœ… Stylesheet created successfully")
story = []
# Title Page
story.append(Paragraph("Personal AI Product Coach", styles['ReportTitle']))
story.append(Paragraph("Product Management Coaching Report", styles['SectionHeader']))
story.append(Spacer(1, 0.5 * inch))
# Executive Summary Table
story.append(Paragraph("Executive Summary", styles['SectionHeader']))
story.append(Paragraph(f"Participant: {coaching_data.get('name', 'Product Manager')}", styles['BodyText']))
story.append(Paragraph(f"Coaching Focus: {coaching_data.get('type', 'General PM Coaching')}", styles['BodyText']))
story.append(Paragraph(f"Session Date: {datetime.datetime.now().strftime('%B %d, %Y')}", styles['BodyText']))
story.append(Paragraph(f"Scenarios Completed: {len(coaching_data.get('q_and_a', []))}", styles['BodyText']))
# Calculate overall session performance
session_scores = []
for scenario in coaching_data.get('q_and_a', []):
if 'overall_score' in scenario and scenario['overall_score'] > 0:
session_scores.append(scenario['overall_score'])
if session_scores:
avg_score = sum(session_scores) / len(session_scores)
story.append(Paragraph(f"Session Average Score: {avg_score:.1f}/10", styles['ScoreText']))
story.append(PageBreak())
# Overall Performance Analysis
story.append(Paragraph("Overall Performance Analysis", styles['SectionHeader']))
# Generate comprehensive feedback with error handling
try:
overall_feedback = generate_coaching_feedback(coaching_data.get('q_and_a', []),
coaching_data.get('type', 'General'),
coaching_data.get('name', 'Product Manager'))
# Format the feedback into structured sections
feedback_sections = format_feedback_sections(overall_feedback)
if feedback_sections:
for section in feedback_sections:
if section['title'] and section['content']:
# Add section title with proper styling
story.append(Paragraph(section['title'], styles['SubHeader']))
story.append(Spacer(1, 0.05 * inch))
# Process content based on type
content = section['content']
if 'β€’' in content:
# Handle bullet points
bullet_lines = content.split('\n')
for line in bullet_lines:
line = line.strip()
if line and line.startswith('β€’'):
story.append(Paragraph(line, styles['BulletPoint']))
elif line:
story.append(Paragraph(f"β€’ {line}", styles['BulletPoint']))
elif any(char in content for char in ['/', '10', 'Score', 'Rating']):
# Handle scores
story.append(Paragraph(content, styles['ScoreText']))
else:
# Handle regular paragraphs with proper formatting
paragraphs = format_text_into_paragraphs(content, 500)
for para in paragraphs:
if para.strip():
story.append(Paragraph(para, styles['BodyText']))
story.append(Spacer(1, 0.05 * inch))
story.append(Spacer(1, 0.15 * inch))
else:
# Fallback - format as structured paragraphs
paragraphs = format_text_into_paragraphs(overall_feedback, 500)
for para in paragraphs:
if para.strip():
story.append(Paragraph(para, styles['BodyText']))
story.append(Spacer(1, 0.1 * inch))
except Exception as feedback_error:
print(f"⚠️ Error generating overall feedback: {feedback_error}")
story.append(Paragraph("Great work in this coaching session! You demonstrated solid product management thinking and approach to the scenarios presented.", styles['BodyText']))
story.append(Spacer(1, 0.3 * inch))
# Add progress chart with error handling
try:
skill_labels = define_skill_areas(coaching_data.get('type', 'General'))
chart_path = os.path.join(config.REPORT_FOLDER, "coaching_progress.png")
if os.path.exists(chart_path):
os.remove(chart_path)
if create_coaching_progress_chart(skill_labels, chart_path):
story.append(Image(chart_path, width=6*inch, height=4*inch, hAlign='CENTER'))
else:
story.append(Paragraph("Skills covered in this coaching session:", styles['SubHeader']))
for skill in skill_labels:
story.append(Paragraph(f"β€’ {skill}", styles['BodyText']))
except Exception as chart_error:
print(f"⚠️ Error creating chart: {chart_error}")
story.append(Paragraph("Skills Development Summary", styles['SubHeader']))
story.append(Paragraph("This coaching session covered key product management competencies.", styles['BodyText']))
story.append(PageBreak())
# Detailed Scenario Analysis
story.append(Paragraph("Detailed Scenario Analysis", styles['SectionHeader']))
story.append(Spacer(1, 0.2 * inch))
for i, scenario in enumerate(coaching_data.get('q_and_a', [])):
try:
# Scenario Header with visual separation
story.append(Paragraph(f"Scenario {i+1}", styles['SubHeader']))
story.append(Spacer(1, 0.1 * inch))
# Challenge Description in highlighted box
story.append(Paragraph("Challenge:", styles['SubHeader']))
question_text = scenario.get('question', 'Product management scenario')
clean_question = clean_text_for_pdf(question_text)
# Format question into readable format
question_paragraphs = format_text_into_paragraphs(clean_question, 350)
for para in question_paragraphs:
if para.strip():
story.append(Paragraph(para, styles['HighlightBox']))
story.append(Spacer(1, 0.15 * inch))
# Participant's Response - format into readable paragraphs
story.append(Paragraph("Your Approach:", styles['SubHeader']))
response_text = scenario.get('response', 'Response provided')
clean_response = clean_text_for_pdf(response_text)
# Format response into readable paragraphs
response_paragraphs = format_text_into_paragraphs(clean_response, 400)
for para in response_paragraphs:
if para.strip():
story.append(Paragraph(para, styles['BodyText']))
story.append(Spacer(1, 0.05 * inch))
story.append(Spacer(1, 0.1 * inch))
# Score Display with visual emphasis
if scenario.get('overall_score', 0) > 0:
score_text = f"Overall Score: {scenario['overall_score']}/10"
story.append(Paragraph(score_text, styles['ScoreText']))
story.append(Spacer(1, 0.1 * inch))
# Coaching Feedback - break into sections for better formatting
story.append(Paragraph("Coaching Analysis:", styles['SubHeader']))
feedback_text = scenario.get('feedback', 'Great work on this scenario!')
# Clean and format the feedback
cleaned_feedback = clean_text_for_pdf(feedback_text)
feedback_sections = format_feedback_sections(cleaned_feedback)
if feedback_sections:
for section in feedback_sections:
if section['title'] and section['content']:
# Add section title
story.append(Paragraph(section['title'], styles['SubHeader']))
story.append(Spacer(1, 0.05 * inch))
# Process content based on type
content = section['content']
if 'β€’' in content:
# Handle bullet points
bullet_lines = content.split('\n')
for line in bullet_lines:
line = line.strip()
if line and line.startswith('β€’'):
story.append(Paragraph(line, styles['BulletPoint']))
elif line:
story.append(Paragraph(f"β€’ {line}", styles['BulletPoint']))
elif any(char in content for char in ['/', '10', 'Score', 'Rating']):
# Handle scores - format as highlighted text
story.append(Paragraph(content, styles['ScoreText']))
else:
# Handle regular paragraphs
paragraphs = format_text_into_paragraphs(content, 400)
for para in paragraphs:
if para.strip():
story.append(Paragraph(para, styles['BodyText']))
story.append(Spacer(1, 0.1 * inch))
else:
# Fallback - format as structured paragraphs
paragraphs = format_text_into_paragraphs(cleaned_feedback, 400)
for para in paragraphs:
if para.strip():
story.append(Paragraph(para, styles['BodyText']))
story.append(Spacer(1, 0.05 * inch))
# Add separator between scenarios
if i < len(coaching_data.get('q_and_a', [])) - 1:
story.append(Spacer(1, 0.3 * inch))
# Add a subtle line separator
story.append(HRFlowable(width="100%", thickness=1, lineCap='round', color=colors.lightgrey))
story.append(Spacer(1, 0.2 * inch))
except Exception as scenario_error:
print(f"⚠️ Error processing scenario {i+1}: {scenario_error}")
story.append(Paragraph(f"Scenario {i+1}: Completed successfully", styles['BodyText']))
story.append(Spacer(1, 0.2 * inch))
# Development Plan Section
story.append(PageBreak())
story.append(Paragraph("Your Product Management Development Plan", styles['SectionHeader']))
story.append(Spacer(1, 0.2 * inch))
# Focus Areas
story.append(Paragraph("Recommended Focus Areas", styles['SubHeader']))
focus_areas = [
f"Continue practicing {coaching_data.get('type', 'product management').lower()} scenarios with real-world applications",
"Explore and master relevant PM frameworks and methodologies",
"Seek feedback from peers and mentors on your product management approach",
"Apply these concepts in your current role or personal projects"
]
for area in focus_areas:
story.append(Paragraph(f"β€’ {area}", styles['BulletText']))
story.append(Spacer(1, 0.2 * inch))
# Learning Resources
story.append(Paragraph("Suggested Learning Resources", styles['SubHeader']))
resources = [
'"Inspired" by Marty Cagan - Product management fundamentals',
'"The Lean Startup" by Eric Ries - Validation and iteration principles',
'"Hooked" by Nir Eyal - User engagement and product psychology',
'Product management communities and industry blogs for current best practices'
]
for resource in resources:
story.append(Paragraph(f"β€’ {resource}", styles['BulletText']))
story.append(Spacer(1, 0.2 * inch))
# Next Steps
story.append(Paragraph("Next Steps for Growth", styles['SubHeader']))
next_steps_text = """Product management is a continuous learning journey. Use this coaching session as a foundation to build upon.
Regular practice with realistic scenarios, combined with framework application and stakeholder feedback,
will accelerate your development as a product manager.
Remember to stay curious about user needs, data-driven in your decisions, and collaborative in your approach.
The best product managers never stop learning and adapting."""
story.append(Paragraph(next_steps_text, styles['BodyText']))
# Footer
story.append(Spacer(1, 0.5 * inch))
story.append(Paragraph("Personal AI Product Coach - Developing Product Management Excellence",
styles['BodyText']))
# Build the PDF
doc.build(story)
print(f"βœ… Professional coaching report generated: {file_path}")
return True
except Exception as e:
print(f"❌ Error generating PDF report: {e}")
print(f"❌ Error details: {str(e)}")
return False
# Legacy function for backward compatibility
def generate_holistic_feedback(interview_log):
"""Legacy function - redirects to generate_coaching_feedback."""
return "This coaching session focused on developing key product management skills through practical scenarios."