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()