diff --git a/cli/main.py b/cli/main.py
index 64616ee1..2f15cfb9 100644
--- a/cli/main.py
+++ b/cli/main.py
@@ -22,6 +22,7 @@ from rich.rule import Rule
from tradingagents.graph.trading_graph import TradingAgentsGraph
from tradingagents.default_config import DEFAULT_CONFIG
+from tradingagents.reports import TradingReportPDFGenerator
from cli.models import AnalystType
from cli.utils import *
@@ -1093,6 +1094,38 @@ def run_analysis():
# Display the complete final report
display_complete_report(final_state)
+ # Generate PDF report if enabled
+ if config.get("pdf_generation", {}).get("auto_generate", True):
+ try:
+ pdf_generator = TradingReportPDFGenerator(config.get("pdf_generation", {}))
+ if pdf_generator.is_available():
+ # Prepare analysis data for PDF generation
+ analysis_data = {
+ "final_report": final_state.get("final_trade_decision", ""),
+ "report_sections": {
+ section: content for section, content in message_buffer.report_sections.items()
+ if content is not None
+ },
+ "final_state": final_state # Pass complete state for structured formatting
+ }
+
+ pdf_path = pdf_generator.generate_pdf(
+ analysis_data,
+ selections["ticker"],
+ selections["analysis_date"]
+ )
+
+ if pdf_path:
+ console.print(f"\nš [bold green]PDF report generated:[/bold green] {pdf_path}")
+ console.print(f"š [bold green]HTML backup saved:[/bold green] {pdf_path.replace('.pdf', '.html')}")
+ else:
+ console.print("\nā ļø [yellow]PDF generation failed, but analysis completed successfully[/yellow]")
+ else:
+ console.print("\nā ļø [yellow]PDF generation not available (dependencies not installed or disabled)[/yellow]")
+ except Exception as e:
+ console.print(f"\nā ļø [yellow]PDF generation failed: {str(e)}[/yellow]")
+ console.print("Analysis completed successfully, but PDF could not be generated.")
+
update_display(layout)
diff --git a/requirements.txt b/requirements.txt
index a6154cd2..6ffcac24 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -24,3 +24,4 @@ rich
questionary
langchain_anthropic
langchain-google-genai
+playwright>=1.40.0
diff --git a/tradingagents/reports/README.md b/tradingagents/reports/README.md
new file mode 100644
index 00000000..228aa6ce
--- /dev/null
+++ b/tradingagents/reports/README.md
@@ -0,0 +1,87 @@
+# TradingAgents Reports Module
+
+This module provides comprehensive report generation capabilities for TradingAgents analysis results.
+
+## Structure
+
+```
+tradingagents/reports/
+āāā __init__.py # Main module interface
+āāā README.md # This documentation
+āāā generators/ # Report generators
+ā āāā __init__.py
+ā āāā pdf_generator.py # PDF generation with Playwright/WeasyPrint
+āāā formatters/ # Content formatters
+ā āāā __init__.py
+ā āāā report_formatter.py # Structures analysis data for reports
+āāā converters/ # Content converters
+ āāā __init__.py
+ āāā html_converter.py # Rich console to HTML conversion
+```
+
+## Features
+
+### šÆ **Automatic PDF Generation**
+- Generates PDF reports automatically after each analysis
+- Uses Playwright as primary engine (cross-platform, no system dependencies)
+- WeasyPrint fallback for compatibility
+- Professional styling with proper sections and formatting
+
+### š **Structured Reports**
+Reports follow the exact terminal structure:
+1. **I. Analyst Team Reports** - Market/Social/News/Fundamentals analysts
+2. **II. Research Team Decision** - Bull/Bear/Research Manager analysis
+3. **III. Trading Team Plan** - Trader's strategic recommendations
+4. **IV. Risk Management Team Decision** - Risk analysts' perspectives
+5. **V. Portfolio Manager Decision** - Final investment decision
+
+### šØ **Rich Formatting**
+- Color-coded sections matching terminal output
+- Professional panels and styling
+- Markdown content conversion
+- Responsive layout for different content types
+
+## Usage
+
+The reports module is automatically integrated into the main CLI workflow. No manual intervention required.
+
+```python
+from tradingagents.reports import TradingReportPDFGenerator
+
+# Used automatically in cli/main.py
+generator = TradingReportPDFGenerator()
+pdf_path = generator.generate_pdf(analysis_data, ticker, date)
+```
+
+## Configuration
+
+PDF generation can be configured in `tradingagents/default_config.py`:
+
+```python
+"pdf_generation": {
+ "enabled": True,
+ "output_dir": "results",
+ "page_format": "A4",
+ "margin": "2cm",
+ "font_family": "Arial, sans-serif",
+ "font_size": "12pt",
+ "auto_generate": True,
+}
+```
+
+## Dependencies
+
+- **playwright**: Primary PDF generation engine
+- **weasyprint**: Fallback PDF generation (optional)
+- **rich**: Terminal styling and content formatting
+- **jinja2**: HTML templating
+
+## Output
+
+Generated files are saved to:
+- `results/{ticker}/{date}/analysis_report.pdf` - Main PDF report
+- `results/{ticker}/{date}/analysis_report.html` - HTML backup
+
+Example file sizes:
+- PDF: ~900KB (comprehensive analysis with all sections)
+- HTML: ~45KB (structured content backup)
\ No newline at end of file
diff --git a/tradingagents/reports/__init__.py b/tradingagents/reports/__init__.py
new file mode 100644
index 00000000..341cee3b
--- /dev/null
+++ b/tradingagents/reports/__init__.py
@@ -0,0 +1,21 @@
+"""
+TradingAgents Reports Module
+
+This module provides comprehensive report generation capabilities for TradingAgents analysis,
+including PDF generation, HTML conversion, and structured formatting.
+
+Structure:
+- generators/: PDF and other report generators
+- formatters/: Report structure and formatting logic
+- converters/: Content conversion utilities
+"""
+
+try:
+ from .generators import TradingReportPDFGenerator
+ from .formatters import ReportFormatter
+ from .converters import RichToHTMLConverter
+ __all__ = ['TradingReportPDFGenerator', 'ReportFormatter', 'RichToHTMLConverter']
+except ImportError as e:
+ # Handle missing dependencies gracefully
+ print(f"Warning: Could not import report components: {e}")
+ __all__ = []
\ No newline at end of file
diff --git a/tradingagents/reports/converters/__init__.py b/tradingagents/reports/converters/__init__.py
new file mode 100644
index 00000000..57f53c1f
--- /dev/null
+++ b/tradingagents/reports/converters/__init__.py
@@ -0,0 +1,9 @@
+"""
+Content Converters
+
+This module contains converters for transforming content between different formats.
+"""
+
+from .html_converter import RichToHTMLConverter
+
+__all__ = ['RichToHTMLConverter']
\ No newline at end of file
diff --git a/tradingagents/reports/converters/html_converter.py b/tradingagents/reports/converters/html_converter.py
new file mode 100644
index 00000000..422c22ae
--- /dev/null
+++ b/tradingagents/reports/converters/html_converter.py
@@ -0,0 +1,129 @@
+"""
+HTML Converter for Rich Console Output
+
+This module converts Rich console output to clean HTML suitable for PDF generation.
+"""
+
+from rich.console import Console
+from rich.text import Text
+import re
+from typing import Dict, Any, Optional
+
+
+class RichToHTMLConverter:
+ """Converts Rich console output to clean HTML for PDF generation."""
+
+ def __init__(self):
+ self.console = Console(record=True, width=120)
+
+ def rich_to_html(self, console_output: str) -> str:
+ """
+ Convert Rich console output to clean HTML.
+
+ Args:
+ console_output: Raw console output with Rich formatting
+
+ Returns:
+ Clean HTML string suitable for PDF generation
+ """
+ # Create a console and export to HTML
+ html_content = self.console.export_html(
+ inline_styles=True,
+ code_format=None
+ )
+
+ # Clean and optimize for PDF
+ return self.clean_html_for_pdf(html_content)
+
+ def clean_html_for_pdf(self, html_content: str) -> str:
+ """
+ Clean HTML content for better PDF rendering.
+
+ Args:
+ html_content: Raw HTML content from Rich export
+
+ Returns:
+ Cleaned HTML content
+ """
+ # Remove background colors that don't work well in PDF
+ html_content = re.sub(r'background-color:\s*#[0-9a-fA-F]{6};?', '', html_content)
+
+ # Ensure good contrast for text
+ html_content = re.sub(r'color:\s*#[0-9a-fA-F]{6};?', 'color: #333333;', html_content)
+
+ # Remove excessive margins and padding
+ html_content = re.sub(r'margin:\s*\d+px;?', 'margin: 5px;', html_content)
+ html_content = re.sub(r'padding:\s*\d+px;?', 'padding: 3px;', html_content)
+
+ return html_content
+
+ def apply_pdf_styles(self, html_content: str) -> str:
+ """
+ Apply PDF-specific styles to HTML content.
+
+ Args:
+ html_content: HTML content to style
+
+ Returns:
+ HTML content with PDF-optimized styles
+ """
+ pdf_styles = """
+
+ """
+
+ # Insert styles into HTML head
+ if '
' in html_content:
+ html_content = html_content.replace('', f'{pdf_styles}')
+ else:
+ html_content = f'{pdf_styles}{html_content}'
+
+ return html_content
\ No newline at end of file
diff --git a/tradingagents/reports/formatters/__init__.py b/tradingagents/reports/formatters/__init__.py
new file mode 100644
index 00000000..98c53347
--- /dev/null
+++ b/tradingagents/reports/formatters/__init__.py
@@ -0,0 +1,9 @@
+"""
+Report Formatters
+
+This module contains formatters for structuring analysis data into various output formats.
+"""
+
+from .report_formatter import ReportFormatter
+
+__all__ = ['ReportFormatter']
\ No newline at end of file
diff --git a/tradingagents/reports/formatters/report_formatter.py b/tradingagents/reports/formatters/report_formatter.py
new file mode 100644
index 00000000..ea20ffd0
--- /dev/null
+++ b/tradingagents/reports/formatters/report_formatter.py
@@ -0,0 +1,281 @@
+"""
+Report Formatter for TradingAgents Analysis
+
+This module formats and structures analysis results for PDF generation,
+matching the exact terminal output structure.
+"""
+
+from typing import Dict, Any, List, Optional
+from datetime import datetime
+import json
+
+
+class ReportFormatter:
+ """Formats trading analysis results into structured sections for PDF generation."""
+
+ def __init__(self):
+ self.sections = {}
+
+ def format_complete_report(self, analysis_data: Dict[str, Any], ticker: str, date: str) -> str:
+ """
+ Format complete analysis report into structured HTML matching the terminal output.
+
+ Args:
+ analysis_data: Complete analysis results including final_state
+ ticker: Stock ticker symbol
+ date: Analysis date
+
+ Returns:
+ Formatted HTML content matching terminal structure
+ """
+ html_sections = []
+
+ # Cover page
+ html_sections.append(self._create_cover_page(ticker, date))
+
+ # Get final_state for structured data
+ final_state = analysis_data.get('final_state', {})
+
+ # Complete Analysis Report Header
+ html_sections.append('Complete Analysis Report
')
+
+ # I. Analyst Team Reports
+ analyst_section = self._format_analyst_team_reports(final_state)
+ if analyst_section:
+ html_sections.append(analyst_section)
+
+ # II. Research Team Decision
+ research_section = self._format_research_team_decision(final_state)
+ if research_section:
+ html_sections.append(research_section)
+
+ # III. Trading Team Plan
+ trading_section = self._format_trading_team_plan(final_state)
+ if trading_section:
+ html_sections.append(trading_section)
+
+ # IV. Risk Management Team Decision
+ risk_section = self._format_risk_management_team_decision(final_state)
+ if risk_section:
+ html_sections.append(risk_section)
+
+ # V. Portfolio Manager Decision
+ portfolio_section = self._format_portfolio_manager_decision(final_state)
+ if portfolio_section:
+ html_sections.append(portfolio_section)
+
+ return '\n'.join(html_sections)
+
+ def _create_cover_page(self, ticker: str, date: str) -> str:
+ """Create report cover page."""
+ return f"""
+
+
TradingAgents Analysis Report
+
{ticker}
+
Analysis Date: {date}
+
Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
+
+
Multi-Agents LLM Financial Trading Framework
+
+ Workflow: Analyst Team ā Research Team ā Trader ā Risk Management ā Portfolio Management
+
+
+
+ """
+
+ def _format_analyst_team_reports(self, final_state: Dict[str, Any]) -> str:
+ """Format I. Analyst Team Reports section."""
+ analysts = []
+
+ # Market Analyst Report
+ if final_state.get("market_report"):
+ analysts.append(self._create_analyst_panel("Market Analyst", final_state["market_report"]))
+
+ # Social Analyst Report
+ if final_state.get("sentiment_report"):
+ analysts.append(self._create_analyst_panel("Social Analyst", final_state["sentiment_report"]))
+
+ # News Analyst Report
+ if final_state.get("news_report"):
+ analysts.append(self._create_analyst_panel("News Analyst", final_state["news_report"]))
+
+ # Fundamentals Analyst Report
+ if final_state.get("fundamentals_report"):
+ analysts.append(self._create_analyst_panel("Fundamentals Analyst", final_state["fundamentals_report"]))
+
+ if not analysts:
+ return ""
+
+ return f"""
+
+
+ I. Analyst Team Reports
+
+
+ {''.join(analysts)}
+
+
+ """
+
+ def _format_research_team_decision(self, final_state: Dict[str, Any]) -> str:
+ """Format II. Research Team Decision section."""
+ if not final_state.get("investment_debate_state"):
+ return ""
+
+ debate_state = final_state["investment_debate_state"]
+ researchers = []
+
+ # Bull Researcher Analysis
+ if debate_state.get("bull_history"):
+ researchers.append(self._create_analyst_panel("Bull Researcher", debate_state["bull_history"]))
+
+ # Bear Researcher Analysis
+ if debate_state.get("bear_history"):
+ researchers.append(self._create_analyst_panel("Bear Researcher", debate_state["bear_history"]))
+
+ # Research Manager Decision
+ if debate_state.get("judge_decision"):
+ researchers.append(self._create_analyst_panel("Research Manager", debate_state["judge_decision"]))
+
+ if not researchers:
+ return ""
+
+ return f"""
+
+
+ II. Research Team Decision
+
+
+ {''.join(researchers)}
+
+
+ """
+
+ def _format_trading_team_plan(self, final_state: Dict[str, Any]) -> str:
+ """Format III. Trading Team Plan section."""
+ if not final_state.get("trader_investment_plan"):
+ return ""
+
+ return f"""
+
+
+ III. Trading Team Plan
+
+
+ {self._create_analyst_panel("Trader", final_state["trader_investment_plan"])}
+
+
+ """
+
+ def _format_risk_management_team_decision(self, final_state: Dict[str, Any]) -> str:
+ """Format IV. Risk Management Team Decision section."""
+ if not final_state.get("risk_debate_state"):
+ return ""
+
+ risk_state = final_state["risk_debate_state"]
+ risk_analysts = []
+
+ # Aggressive (Risky) Analyst Analysis
+ if risk_state.get("risky_history"):
+ risk_analysts.append(self._create_analyst_panel("Aggressive Analyst", risk_state["risky_history"]))
+
+ # Conservative (Safe) Analyst Analysis
+ if risk_state.get("safe_history"):
+ risk_analysts.append(self._create_analyst_panel("Conservative Analyst", risk_state["safe_history"]))
+
+ # Neutral Analyst Analysis
+ if risk_state.get("neutral_history"):
+ risk_analysts.append(self._create_analyst_panel("Neutral Analyst", risk_state["neutral_history"]))
+
+ if not risk_analysts:
+ return ""
+
+ return f"""
+
+
+ IV. Risk Management Team Decision
+
+
+ {''.join(risk_analysts)}
+
+
+ """
+
+ def _format_portfolio_manager_decision(self, final_state: Dict[str, Any]) -> str:
+ """Format V. Portfolio Manager Decision section."""
+ if not final_state.get("risk_debate_state") or not final_state["risk_debate_state"].get("judge_decision"):
+ return ""
+
+ decision = final_state["risk_debate_state"]["judge_decision"]
+
+ return f"""
+
+
+ V. Portfolio Manager Decision
+
+
+ {self._create_analyst_panel("Portfolio Manager", decision)}
+
+
+ """
+
+ def _create_analyst_panel(self, title: str, content: str) -> str:
+ """Create a styled panel for an analyst's report."""
+ # Convert markdown-style content to HTML if needed
+ formatted_content = self._format_markdown_content(content)
+
+ return f"""
+
+
+ {title}
+
+
+ {formatted_content}
+
+
+ """
+
+ def _format_markdown_content(self, content: str) -> str:
+ """Convert basic markdown formatting to HTML."""
+ if not content:
+ return ""
+
+ # Replace markdown headers
+ content = content.replace('### ', '').replace('\n### ', '
\n')
+ content = content.replace('## ', '').replace('\n## ', '
\n')
+ content = content.replace('# ', '').replace('\n# ', '
\n')
+
+ # Replace bold text
+ import re
+ content = re.sub(r'\*\*(.*?)\*\*', r'\1', content)
+
+ # Replace bullet points
+ content = re.sub(r'\n\s*ā¢\s+', '\n
', content)
+ content = re.sub(r'\n\s*\*\s+', '\n', content)
+ content = re.sub(r'\n\s*-\s+', '\n', content)
+
+ # Wrap consecutive list items in tags
+ content = re.sub(r'(- .*?)(?=\n(?!
- ))', r'', content, flags=re.DOTALL)
+
+ # Replace line breaks with
for better formatting
+ content = content.replace('\n\n', '
')
+ content = content.replace('\n', '
')
+
+ # Clean up any unclosed headers
+ if '', '', '')):
+ content += ''
+
+ return content
+
+ def _combine_sections(self, sections: List[str]) -> str:
+ """Combine all sections into final HTML."""
+ return '\n'.join(sections)
\ No newline at end of file
diff --git a/tradingagents/reports/generators/__init__.py b/tradingagents/reports/generators/__init__.py
new file mode 100644
index 00000000..e91f7ce2
--- /dev/null
+++ b/tradingagents/reports/generators/__init__.py
@@ -0,0 +1,13 @@
+"""
+PDF and Report Generators
+
+This module contains various generators for creating reports in different formats.
+"""
+
+try:
+ from .pdf_generator import TradingReportPDFGenerator
+ __all__ = ['TradingReportPDFGenerator']
+except ImportError as e:
+ # Handle missing dependencies gracefully
+ print(f"Warning: Could not import PDF generator: {e}")
+ __all__ = []
\ No newline at end of file
diff --git a/tradingagents/reports/generators/pdf_generator.py b/tradingagents/reports/generators/pdf_generator.py
new file mode 100644
index 00000000..42d82764
--- /dev/null
+++ b/tradingagents/reports/generators/pdf_generator.py
@@ -0,0 +1,412 @@
+"""
+PDF Generator for TradingAgents Analysis Reports
+
+This module generates PDF reports from analysis results using WeasyPrint.
+"""
+
+import os
+import logging
+from pathlib import Path
+from typing import Dict, Any, Optional
+from datetime import datetime
+
+try:
+ from playwright.sync_api import sync_playwright
+ PLAYWRIGHT_AVAILABLE = True
+except (ImportError, OSError) as e:
+ PLAYWRIGHT_AVAILABLE = False
+ logging.warning(f"Playwright not available: {str(e)}. Trying WeasyPrint fallback.")
+
+try:
+ from weasyprint import HTML, CSS
+ from weasyprint.text.fonts import FontConfiguration
+ WEASYPRINT_AVAILABLE = True
+except (ImportError, OSError) as e:
+ WEASYPRINT_AVAILABLE = False
+ HTML = None
+ CSS = None
+ FontConfiguration = None
+ if not PLAYWRIGHT_AVAILABLE:
+ logging.warning(f"WeasyPrint also not available: {str(e)}. PDF generation will be disabled.")
+
+from ..converters.html_converter import RichToHTMLConverter
+from ..formatters.report_formatter import ReportFormatter
+
+
+class TradingReportPDFGenerator:
+ """Generates PDF reports from TradingAgents analysis results."""
+
+ def __init__(self, config: Optional[Dict[str, Any]] = None):
+ """
+ Initialize PDF generator.
+
+ Args:
+ config: Configuration dictionary for PDF generation
+ """
+ self.config = config or self._default_config()
+ self.html_converter = RichToHTMLConverter()
+ self.report_formatter = ReportFormatter()
+ self.logger = logging.getLogger(__name__)
+
+ if not WEASYPRINT_AVAILABLE:
+ self.logger.warning("WeasyPrint not available. PDF generation disabled.")
+
+ def _default_config(self) -> Dict[str, Any]:
+ """Get default PDF generation configuration."""
+ return {
+ "enabled": True,
+ "output_dir": "results",
+ "page_format": "A4",
+ "margin": "2cm",
+ "font_family": "Arial, sans-serif",
+ "font_size": "12pt"
+ }
+
+ def generate_pdf(self, analysis_results: Dict[str, Any], ticker: str,
+ date: str, output_path: Optional[str] = None) -> Optional[str]:
+ """
+ Generate PDF report from analysis results.
+
+ Args:
+ analysis_results: Complete analysis results dictionary
+ ticker: Stock ticker symbol
+ date: Analysis date
+ output_path: Optional custom output path
+
+ Returns:
+ Path to generated PDF file, or None if generation failed
+ """
+ if not self.is_available():
+ self.logger.error("No PDF generation method available.")
+ return None
+
+ if not self.config.get("enabled", True):
+ self.logger.info("PDF generation disabled in configuration.")
+ return None
+
+ try:
+ # Generate output path if not provided
+ if not output_path:
+ output_path = self._generate_output_path(ticker, date)
+
+ # Ensure output directory exists
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
+
+ # Format the report content
+ html_content = self._create_html_report(analysis_results, ticker, date)
+
+ # Generate PDF using available method
+ success = self._html_to_pdf(html_content, output_path)
+
+ if success:
+ # Also save HTML backup
+ html_path = output_path.replace('.pdf', '.html')
+ with open(html_path, 'w', encoding='utf-8') as f:
+ f.write(html_content)
+
+ self.logger.info(f"PDF report generated: {output_path}")
+ return output_path
+ else:
+ return None
+
+ except Exception as e:
+ self.logger.error(f"Failed to generate PDF report: {str(e)}")
+ return None
+
+ def _generate_output_path(self, ticker: str, date: str) -> str:
+ """Generate output path for PDF file."""
+ output_dir = Path(self.config["output_dir"]) / ticker / date
+ return str(output_dir / "analysis_report.pdf")
+
+ def _create_html_report(self, analysis_results: Dict[str, Any],
+ ticker: str, date: str) -> str:
+ """
+ Create complete HTML report from analysis results.
+
+ Args:
+ analysis_results: Analysis results dictionary
+ ticker: Stock ticker symbol
+ date: Analysis date
+
+ Returns:
+ Complete HTML content for PDF generation
+ """
+ # Format the main report content
+ report_content = self.report_formatter.format_complete_report(
+ analysis_results, ticker, date
+ )
+
+ # Wrap in complete HTML document with styles
+ html_template = f"""
+
+
+
+
+
+ TradingAgents Analysis: {ticker}
+ {self._get_pdf_styles()}
+
+
+ {report_content}
+
+
+
+ """
+
+ return html_template
+
+ def _get_pdf_styles(self) -> str:
+ """Get CSS styles for PDF generation."""
+ return """
+
+ """
+
+ def _html_to_pdf(self, html_content: str, output_path: str) -> bool:
+ """
+ Convert HTML content to PDF using available method.
+
+ Args:
+ html_content: HTML content to convert
+ output_path: Output PDF file path
+
+ Returns:
+ True if successful, False otherwise
+ """
+ # Try Playwright first (preferred method)
+ if PLAYWRIGHT_AVAILABLE:
+ try:
+ return self._html_to_pdf_playwright(html_content, output_path)
+ except Exception as e:
+ self.logger.warning(f"Playwright PDF generation failed: {str(e)}. Trying WeasyPrint fallback.")
+
+ # Fallback to WeasyPrint
+ if WEASYPRINT_AVAILABLE:
+ try:
+ return self._html_to_pdf_weasyprint(html_content, output_path)
+ except Exception as e:
+ self.logger.error(f"WeasyPrint PDF generation also failed: {str(e)}")
+
+ return False
+
+ def _html_to_pdf_playwright(self, html_content: str, output_path: str) -> bool:
+ """
+ Convert HTML content to PDF using Playwright.
+
+ Args:
+ html_content: HTML content to convert
+ output_path: Output PDF file path
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ with sync_playwright() as p:
+ browser = p.chromium.launch(headless=True)
+ page = browser.new_page()
+
+ # Set content and wait for it to load
+ page.set_content(html_content, wait_until='networkidle')
+
+ # Generate PDF with options
+ page.pdf(
+ path=output_path,
+ format='A4',
+ margin={
+ 'top': '2cm',
+ 'right': '2cm',
+ 'bottom': '2cm',
+ 'left': '2cm'
+ },
+ print_background=True,
+ display_header_footer=True,
+ header_template='TradingAgents Analysis Report
',
+ footer_template=' /
'
+ )
+
+ browser.close()
+ self.logger.info("PDF generated successfully using Playwright")
+ return True
+
+ except Exception as e:
+ self.logger.error(f"Playwright PDF generation failed: {str(e)}")
+ return False
+
+ def _html_to_pdf_weasyprint(self, html_content: str, output_path: str) -> bool:
+ """
+ Convert HTML content to PDF using WeasyPrint.
+
+ Args:
+ html_content: HTML content to convert
+ output_path: Output PDF file path
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ # Create font configuration
+ font_config = FontConfiguration()
+
+ # Generate PDF
+ html_doc = HTML(string=html_content)
+ html_doc.write_pdf(
+ output_path,
+ font_config=font_config,
+ optimize_images=True
+ )
+
+ self.logger.info("PDF generated successfully using WeasyPrint")
+ return True
+
+ except Exception as e:
+ self.logger.error(f"WeasyPrint conversion failed: {str(e)}")
+ # Try without font configuration as fallback
+ try:
+ html_doc = HTML(string=html_content)
+ html_doc.write_pdf(output_path)
+ self.logger.info("PDF generated successfully using WeasyPrint (fallback mode)")
+ return True
+ except Exception as fallback_error:
+ self.logger.error(f"WeasyPrint fallback also failed: {str(fallback_error)}")
+ return False
+
+ def is_available(self) -> bool:
+ """Check if PDF generation is available."""
+ return (PLAYWRIGHT_AVAILABLE or WEASYPRINT_AVAILABLE) and self.config.get("enabled", True)
\ No newline at end of file