SXQ-Report / app.py
MathiasAESandnes's picture
Update app.py
c4718ea verified
from pathlib import Path
import gradio as gr
from anthropic import Anthropic
from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.chart.data import CategoryChartData
from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION
import os
import json
from typing import Dict, List, Any, Optional
from datetime import datetime
class ReportGenerator:
def __init__(self):
if 'ANTHROPIC_API_KEY' not in os.environ:
raise EnvironmentError("ANTHROPIC_API_KEY not found in environment variables")
self.client = Anthropic(api_key=os.environ['ANTHROPIC_API_KEY'])
self.colors = {
'primary': RGBColor(0, 76, 153),
'secondary': RGBColor(64, 224, 208),
'success': RGBColor(39, 174, 96),
'warning': RGBColor(241, 196, 15),
'danger': RGBColor(231, 76, 60),
'text': RGBColor(44, 62, 80)
}
def validate_metrics(self, metrics: List[Dict]) -> None:
if not metrics:
raise ValueError("No metrics data provided")
required_fields = ['metric', 'target', 'actual', 'achievement']
for metric in metrics:
missing = [field for field in required_fields if field not in metric]
if missing:
raise ValueError(f"Missing required metric fields: {', '.join(missing)}")
def validate_file(self, file_path: str) -> None:
if not os.path.exists(file_path):
raise FileNotFoundError(f"File not found: {file_path}")
if os.path.getsize(file_path) > 5 * 1024 * 1024:
raise ValueError("File size exceeds 5MB limit")
try:
with open(file_path, 'r', encoding='utf-8') as f:
f.read()
except UnicodeDecodeError:
raise ValueError("File must be a valid text file with UTF-8 encoding")
def validate_insights(self, insights: Dict) -> None:
required_keys = [
'executive_summary', 'objectives', 'metrics_analysis',
'insights', 'recommendations', 'next_month'
]
missing_keys = [key for key in required_keys if key not in insights]
if missing_keys:
raise ValueError(f"Missing required sections: {', '.join(missing_keys)}")
def get_insights(self, file_path: str) -> Dict[str, Any]:
self.validate_file(file_path)
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()
print(f"\n=== PROCESSING FILE: {file_path} ===")
prompt = f"""
You are a sales performance analyst. Analyze this sales tracking data and provide your analysis in JSON format with the following exact structure:
{{
"executive_summary": {{
"overview": "string", // 2-3 sentence overview of month's performance
"overall_performance_score": "number" // 0-100 based on overall metrics achievement
}},
"objectives": {{
"total_objectives": "number",
"objectives_achieved": "number",
"objectives_list": [
{{
"objective": "string",
"achievement": "number",
"status": "string" // "Achieved", "Partial", or "Not Achieved"
}}
]
}},
"metrics_analysis": {{
"key_metrics": [
{{
"metric": "string",
"target": "number",
"actual": "number",
"achievement": "number"
}}
],
"conversion_rates": {{
"connection_rate": {{
"target": "number",
"actual": "number"
}},
"conversion_rate": {{
"target": "number",
"actual": "number"
}}
}},
"pipeline_value": {{
"target": "number",
"actual": "number",
"gap": "number"
}}
}},
"insights": {{
"key_achievements": ["string"],
"areas_of_concern": ["string"],
"learnings": ["string"]
}},
"recommendations": {{
"immediate_actions": ["string"],
"long_term_suggestions": ["string"]
}},
"next_month": {{
"suggested_objectives": [
{{
"objective": "string",
"target": "string",
"rationale": "string"
}}
],
"focus_areas": ["string"]
}}
}}
Extract exact numbers from the data where available. For missing data, use the context to make reasonable estimates. Ensure all numeric values match the data exactly where present.
Data to analyze:
{content}
"""
try:
message = self.client.messages.create(
model="claude-3-sonnet-20240229",
max_tokens=1500,
temperature=0,
messages=[{"role": "user", "content": prompt}]
)
insights = json.loads(message.content[0].text)
self.validate_insights(insights)
return insights
except json.JSONDecodeError as e:
print(f"Error parsing Claude's response: {e}")
raise ValueError("Failed to parse Claude's response as JSON")
except Exception as e:
print(f"Error in Claude API call: {e}")
raise
def create_presentation(self, insights: Dict[str, Any], output_path: Optional[str] = None) -> str:
if output_path is None:
output_path = f"sales_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pptx"
prs = Presentation()
prs.slide_width = Inches(13.333)
prs.slide_height = Inches(7.5)
self._add_title_slide(prs, insights)
self._add_objectives_slide(prs, insights)
self._add_analysis_slide(prs, insights)
self._add_forward_slide(prs, insights)
prs.save(output_path)
return output_path
def _add_title_slide(self, prs: Presentation, insights: Dict[str, Any]) -> None:
"""Create executive overview slide with enhanced formatting"""
slide = prs.slides.add_slide(prs.slide_layouts[0])
# Title with better positioning and formatting
title = slide.shapes.title
title.top = Inches(0.3)
title.left = Inches(0.5)
title.width = Inches(12)
title.text = "Monthly Sales Performance Review"
title.text_frame.paragraphs[0].font.size = Pt(44)
title.text_frame.paragraphs[0].font.color.rgb = self.colors['primary']
title.text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER
# Date subtitle
subtitle_left = Inches(0.5)
subtitle_top = Inches(1.2)
subtitle_width = Inches(12)
subtitle_height = Inches(0.5)
subtitle = slide.shapes.add_textbox(subtitle_left, subtitle_top, subtitle_width, subtitle_height)
tf = subtitle.text_frame
p = tf.add_paragraph()
p.text = datetime.now().strftime("%B %Y")
p.font.size = Pt(16)
p.alignment = PP_ALIGN.CENTER
p.font.color.rgb = self.colors['secondary']
# Performance Score in large circle
score = insights['executive_summary']['overall_performance_score']
score_left = Inches(0.8)
score_top = Inches(2.0)
score_width = Inches(2.5)
score_height = Inches(2.5)
score_box = slide.shapes.add_shape(
MSO_SHAPE.OVAL,
score_left, score_top,
score_width, score_height
)
score_box.fill.solid()
score_box.fill.fore_color.rgb = self.colors['primary']
score_text = score_box.text_frame
score_text.vertical_anchor = MSO_ANCHOR.MIDDLE
p = score_text.add_paragraph()
p.text = f"{score}%"
p.font.size = Pt(36)
p.font.bold = True
p.font.color.rgb = RGBColor(255, 255, 255)
p.alignment = PP_ALIGN.CENTER
p = score_text.add_paragraph()
p.text = "Performance"
p.font.size = Pt(14)
p.font.color.rgb = RGBColor(255, 255, 255)
p.alignment = PP_ALIGN.CENTER
# Executive Summary Box
summary_left = Inches(3.8)
summary_top = Inches(2.0)
summary_width = Inches(8.7)
summary_height = Inches(1.0)
summary_box = slide.shapes.add_textbox(summary_left, summary_top, summary_width, summary_height)
tf = summary_box.text_frame
p = tf.add_paragraph()
p.text = "Executive Summary"
p.font.bold = True
p.font.size = Pt(18)
p.font.color.rgb = self.colors['primary']
p = tf.add_paragraph()
p.text = insights['executive_summary']['overview']
p.font.size = Pt(12)
p.space_before = Pt(6)
# KPI Dashboard Table
self._add_kpi_dashboard(slide, insights['metrics_analysis']['key_metrics'])
def _add_kpi_dashboard(self, slide: Any, metrics: List[Dict]) -> None:
"""Add enhanced KPI dashboard table"""
left = Inches(0.5)
top = Inches(4.0)
width = Inches(12)
height = Inches(3)
# Create table with fixed column widths
table = slide.shapes.add_table(
rows=len(metrics) + 1, # +1 for header
cols=4,
left=left,
top=top,
width=width,
height=height
).table
# Set column widths
table.columns[0].width = Inches(4) # Metric name
table.columns[1].width = Inches(2.5) # Target
table.columns[2].width = Inches(2.5) # Actual
table.columns[3].width = Inches(3) # Achievement
# Headers
headers = ['Key Metrics', 'Target', 'Actual', 'Achievement']
for i, header in enumerate(headers):
cell = table.cell(0, i)
cell.fill.solid()
cell.fill.fore_color.rgb = self.colors['primary']
paragraph = cell.text_frame.paragraphs[0]
paragraph.text = header
paragraph.font.size = Pt(12)
paragraph.font.bold = True
paragraph.font.color.rgb = RGBColor(255, 255, 255)
paragraph.alignment = PP_ALIGN.CENTER
# Data rows with conditional formatting
for i, metric in enumerate(metrics, start=1):
# Metric name
cell = table.cell(i, 0)
cell.text = metric['metric']
cell.text_frame.paragraphs[0].font.size = Pt(11)
# Target value (right-aligned)
cell = table.cell(i, 1)
cell.text = str(metric['target'])
p = cell.text_frame.paragraphs[0]
p.alignment = PP_ALIGN.CENTER
p.font.size = Pt(11)
# Actual value (right-aligned)
cell = table.cell(i, 2)
cell.text = str(metric['actual'])
p = cell.text_frame.paragraphs[0]
p.alignment = PP_ALIGN.CENTER
p.font.size = Pt(11)
# Achievement with conditional color
achievement = metric['achievement']
cell = table.cell(i, 3)
cell.text = f"{achievement}%"
p = cell.text_frame.paragraphs[0]
p.alignment = PP_ALIGN.CENTER
p.font.size = Pt(11)
p.font.bold = True
# Color coding based on achievement
if achievement >= 90:
p.font.color.rgb = self.colors['success']
elif achievement >= 75:
p.font.color.rgb = self.colors['warning']
else:
p.font.color.rgb = self.colors['danger']
# Add subtle fill to alternate rows
if i % 2 == 0:
for j in range(4):
cell = table.cell(i, j)
cell.fill.solid()
cell.fill.fore_color.rgb = RGBColor(245, 245, 245)
def _add_objectives_slide(self, prs: Presentation, insights: Dict[str, Any]) -> None:
"""Create objectives and metrics slide with enhanced layout"""
slide = prs.slides.add_slide(prs.slide_layouts[1])
# Title
title = slide.shapes.title
title.text = "Objectives & Key Metrics"
title.top = Inches(0.3)
title.left = Inches(0.5)
title.width = Inches(12)
title.text_frame.paragraphs[0].font.size = Pt(40)
title.text_frame.paragraphs[0].font.color.rgb = self.colors['primary']
title.text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER
# Left side: Objectives
objectives_left = Inches(0.5)
objectives_top = Inches(1.3)
objectives_width = Inches(5.8)
objectives_height = Inches(5.7)
self._add_objectives_section(
slide,
insights['objectives'],
objectives_left,
objectives_top,
objectives_width,
objectives_height
)
# Right side: Metrics Chart
chart_left = Inches(6.8)
chart_top = Inches(1.3)
chart_width = Inches(6)
chart_height = Inches(5.7)
self._add_metrics_chart(
slide,
insights['metrics_analysis'],
chart_left,
chart_top,
chart_width,
chart_height
)
def _add_objectives_section(self, slide: Any, objectives: Dict, left: Inches, top: Inches, width: Inches,
height: Inches) -> None:
"""Add enhanced objectives section with status indicators"""
# Create main title shape instead of textbox
title_shape = slide.shapes.add_textbox(left, top, width, Inches(0.5))
title_frame = title_shape.text_frame
title_frame.word_wrap = True
# Section header with completion ratio
p = title_frame.paragraphs[0] # Use existing paragraph
p.text = f"Monthly Objectives Progress ({objectives['objectives_achieved']}/{objectives['total_objectives']})"
p.font.size = Pt(18)
p.font.bold = True
p.font.color.rgb = self.colors['primary']
current_top = top + Inches(0.7) # Adjusted spacing after title
# Add each objective with enhanced status display
for obj in objectives['objectives_list']:
# Create container shape for each objective
obj_container = slide.shapes.add_shape(
MSO_SHAPE.RECTANGLE,
left,
current_top,
width,
Inches(0.8)
)
obj_container.fill.solid()
obj_container.fill.fore_color.rgb = RGBColor(248, 249, 250)
obj_container.line.color.rgb = RGBColor(230, 230, 230)
# Add status indicator box
status_color = (
self.colors['success'] if obj['status'] == "Achieved"
else self.colors['warning'] if obj['status'] == "Partial"
else self.colors['danger']
)
status_box = slide.shapes.add_shape(
MSO_SHAPE.RECTANGLE,
left + Inches(0.2),
current_top + Inches(0.2),
Inches(0.15),
Inches(0.15)
)
status_box.fill.solid()
status_box.fill.fore_color.rgb = status_color
# Add text frame for objective content
obj_text = slide.shapes.add_textbox(
left + Inches(0.5), # Moved right to accommodate status box
current_top + Inches(0.1),
width - Inches(0.6),
Inches(0.6)
)
tf = obj_text.text_frame
tf.word_wrap = True
# Objective title
p = tf.paragraphs[0]
p.text = obj['objective']
p.font.size = Pt(14)
p.font.bold = True
# Status text
p = tf.add_paragraph()
p.text = f"Status: {obj['status']} ({obj['achievement']}%)"
p.font.size = Pt(12)
p.font.color.rgb = status_color
# Increment current_top for next objective
current_top += Inches(1) # Reduced spacing between objectives
def _add_metrics_chart(self, slide: Any, metrics_analysis: Dict, left: Inches, top: Inches, width: Inches,
height: Inches) -> None:
"""Add enhanced metrics comparison chart"""
metrics = metrics_analysis['key_metrics']
chart_data = CategoryChartData()
chart_data.categories = [m['metric'] for m in metrics]
chart_data.add_series('Target', [m['target'] for m in metrics])
chart_data.add_series('Actual', [m['actual'] for m in metrics])
chart = slide.shapes.add_chart(
XL_CHART_TYPE.COLUMN_CLUSTERED,
left, top, width, height,
chart_data
).chart
# Enhanced chart formatting
chart.has_legend = True
chart.has_title = True
chart.chart_title.text_frame.text = "Target vs Actual Performance"
chart.chart_title.text_frame.paragraphs[0].font.size = Pt(16)
chart.chart_title.text_frame.paragraphs[0].font.bold = True
# Format plot area
plot = chart.plots[0]
plot.gap_width = 50
plot.overlap = -25
# Format axes
category_axis = chart.category_axis
category_axis.tick_labels.font.size = Pt(9)
category_axis.tick_labels.font.color.rgb = self.colors['text']
value_axis = chart.value_axis
value_axis.tick_labels.font.size = Pt(9)
value_axis.tick_labels.font.color.rgb = self.colors['text']
# Format legend
chart.legend.position = XL_LEGEND_POSITION.BOTTOM
chart.legend.include_in_layout = False
chart.legend.font.size = Pt(10)
def _add_analysis_slide(self, prs: Presentation, insights: Dict[str, Any]) -> None:
"""Create analysis and insights slide with enhanced formatting"""
slide = prs.slides.add_slide(prs.slide_layouts[1])
# Title
title = slide.shapes.title
title.text = "Analysis & Insights"
title.top = Inches(0.3)
title.left = Inches(0.5)
title.width = Inches(12)
title.text_frame.paragraphs[0].font.size = Pt(40)
title.text_frame.paragraphs[0].font.color.rgb = self.colors['primary']
title.text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER
# Two columns for achievements and concerns
achievements_box = slide.shapes.add_shape(
MSO_SHAPE.RECTANGLE,
Inches(0.5),
Inches(1.3),
Inches(5.8),
Inches(2.5)
)
achievements_box.fill.solid()
achievements_box.fill.fore_color.rgb = RGBColor(248, 249, 250)
achievements_box.line.color.rgb = self.colors['success']
# Add achievements text
achievements_text = slide.shapes.add_textbox(
Inches(0.7), # Slightly indented from box
Inches(1.4),
Inches(5.4),
Inches(2.3)
)
tf = achievements_text.text_frame
tf.word_wrap = True
# Add header
p = tf.paragraphs[0]
p.text = "Key Achievements"
p.font.size = Pt(18)
p.font.bold = True
p.font.color.rgb = self.colors['success']
p.space_after = Pt(12)
# Add achievements
for achievement in insights['insights']['key_achievements']:
p = tf.add_paragraph()
p.text = f"• {achievement}"
p.font.size = Pt(14)
p.space_before = Pt(6)
p.space_after = Pt(6)
# Concerns box
concerns_box = slide.shapes.add_shape(
MSO_SHAPE.RECTANGLE,
Inches(6.8),
Inches(1.3),
Inches(5.8),
Inches(2.5)
)
concerns_box.fill.solid()
concerns_box.fill.fore_color.rgb = RGBColor(248, 249, 250)
concerns_box.line.color.rgb = self.colors['danger']
# Add concerns text
concerns_text = slide.shapes.add_textbox(
Inches(7.0), # Slightly indented from box
Inches(1.4),
Inches(5.4),
Inches(2.3)
)
tf = concerns_text.text_frame
tf.word_wrap = True
# Add header
p = tf.paragraphs[0]
p.text = "Areas of Concern"
p.font.size = Pt(18)
p.font.bold = True
p.font.color.rgb = self.colors['danger']
p.space_after = Pt(12)
# Add concerns
for concern in insights['insights']['areas_of_concern']:
p = tf.add_paragraph()
p.text = f"• {concern}"
p.font.size = Pt(14)
p.space_before = Pt(6)
p.space_after = Pt(6)
# Key learnings section
learnings_box = slide.shapes.add_shape(
MSO_SHAPE.RECTANGLE,
Inches(0.5),
Inches(4.1),
Inches(12.1),
Inches(3)
)
learnings_box.fill.solid()
learnings_box.fill.fore_color.rgb = RGBColor(248, 249, 250)
learnings_box.line.color.rgb = self.colors['secondary']
def _add_insight_section(self, slide: Any, title: str, items: List[str], left: Inches, top: Inches, width: Inches,
color: RGBColor) -> None:
"""Add formatted insight section"""
box = slide.shapes.add_textbox(left, top, width, Inches(0.5))
tf = box.text_frame
# Title
p = tf.add_paragraph()
p.text = title
p.font.bold = True
p.font.size = Pt(16)
p.font.color.rgb = color
p.space_after = Pt(10)
# Content items
content_box = slide.shapes.add_textbox(left, top + Inches(0.6), width, Inches(2))
tf = content_box.text_frame
for item in items:
p = tf.add_paragraph()
p.text = f"• {item}"
p.font.size = Pt(12)
p.space_after = Pt(6)
def _add_forward_slide(self, prs: Presentation, insights: Dict[str, Any]) -> None:
"""Create forward-looking slide with enhanced formatting"""
slide = prs.slides.add_slide(prs.slide_layouts[1])
# Title
title = slide.shapes.title
title.text = "Action Plan & Next Steps"
title.top = Inches(0.3)
title.left = Inches(0.5)
title.width = Inches(12)
title.text_frame.paragraphs[0].font.size = Pt(40)
title.text_frame.paragraphs[0].font.color.rgb = self.colors['primary']
title.text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER
# Actions sections
self._add_actions_section(
slide,
"Immediate Actions",
insights['recommendations']['immediate_actions'],
Inches(0.5),
Inches(1.3),
Inches(5.8),
self.colors['warning']
)
self._add_actions_section(
slide,
"Strategic Initiatives",
insights['recommendations']['long_term_suggestions'],
Inches(6.8),
Inches(1.3),
Inches(5.8),
self.colors['primary']
)
# Next month objectives
self._add_next_month_section(
slide,
insights['next_month'],
Inches(0.5),
Inches(4.1),
Inches(12.1)
)
def _add_actions_section(self, slide: Any, title: str, items: List[str], left: Inches, top: Inches, width: Inches,
color: RGBColor) -> None:
"""Add formatted actions section"""
# Background box
box = slide.shapes.add_shape(
MSO_SHAPE.RECTANGLE, # Changed from add_rectangle
left, top, width, Inches(2.5)
)
box.fill.solid()
box.fill.fore_color.rgb = RGBColor(248, 249, 250)
box.line.color.rgb = color
# Content
text_box = slide.shapes.add_textbox(left + Inches(0.2), top + Inches(0.1), width - Inches(0.4), Inches(2.3))
tf = text_box.text_frame
p = tf.add_paragraph()
p.text = title
p.font.bold = True
p.font.size = Pt(16)
p.font.color.rgb = color
p.space_after = Pt(10)
for item in items:
p = tf.add_paragraph()
p.text = f"• {item}"
p.font.size = Pt(12)
p.space_after = Pt(6)
def _add_next_month_section(self, slide: Any, next_month: Dict, left: Inches, top: Inches, width: Inches) -> None:
"""Add enhanced next month objectives section"""
# Background box
box = slide.shapes.add_shape(
MSO_SHAPE.RECTANGLE,
left, top, width, Inches(3)
)
box.fill.solid()
box.fill.fore_color.rgb = RGBColor(248, 249, 250)
box.line.color.rgb = self.colors['secondary']
# Main content box
text_box = slide.shapes.add_textbox(
left + Inches(0.2),
top + Inches(0.1),
width - Inches(0.4),
Inches(2.8)
)
tf = text_box.text_frame
tf.word_wrap = True
# Title
p = tf.add_paragraph()
p.text = "Objectives for Next Month"
p.font.bold = True
p.font.size = Pt(16)
p.font.color.rgb = self.colors['secondary']
p.space_after = Pt(12)
# Add objectives with proper spacing
for obj in next_month['suggested_objectives']:
# Objective title
p = tf.add_paragraph()
p.text = f"• {obj['objective']}"
p.font.bold = True
p.font.size = Pt(14)
p.space_after = Pt(6)
# Target
p = tf.add_paragraph()
p.text = f" Target: {obj['target']}" # Indented with spaces
p.font.size = Pt(12)
p.space_before = Pt(3)
p.space_after = Pt(3)
p.level = 1 # Indentation level
# Rationale
p = tf.add_paragraph()
p.text = f" Rationale: {obj['rationale']}" # Indented with spaces
p.font.size = Pt(12)
p.space_before = Pt(3)
p.space_after = Pt(12)
p.level = 1 # Indentation level
# Add focus areas if present
if 'focus_areas' in next_month and next_month['focus_areas']:
p = tf.add_paragraph()
p.text = "Key Focus Areas:"
p.font.bold = True
p.font.size = Pt(14)
p.space_before = Pt(12)
p.space_after = Pt(6)
for area in next_month['focus_areas']:
p = tf.add_paragraph()
p.text = f"• {area}"
p.font.size = Pt(12)
p.space_after = Pt(4)
def process_file(file: gr.File) -> str:
if not file.name.endswith('.txt'):
raise gr.Error("Please upload a text file (.txt)")
try:
generator = ReportGenerator()
insights = generator.get_insights(file.name)
return generator.create_presentation(insights)
except Exception as e:
raise gr.Error(f"Error processing file: {str(e)}")
# Gradio interface
interface = gr.Interface(
fn=process_file,
inputs=gr.File(label="Upload Sales Tracking Data (Text File)", file_types=[".txt"]),
outputs=gr.File(label="Download Enhanced Report"),
title="Sales Performance Report Generator",
description="Upload your sales tracking data to generate a detailed performance analysis report."
)
if __name__ == "__main__":
interface.launch()