Spaces:
Sleeping
Sleeping
| """Data models for phase-organized analysis reports.""" | |
| from dataclasses import dataclass, field | |
| from datetime import datetime | |
| from typing import Any, Dict, List, Optional | |
| from config.models import AnalysisPhase, InvestmentStyle, PhaseConfiguration | |
| class AgentOutput: | |
| """Output from a single agent within a phase.""" | |
| agent_name: str | |
| """Name of the agent (e.g., 'IndicatorAgent', 'PatternAgent').""" | |
| report: str | |
| """Markdown-formatted agent analysis report.""" | |
| charts: List[str] = field(default_factory=list) | |
| """List of base64-encoded chart images (PNG format, no data URI prefix).""" | |
| educational_notes: Optional[str] = None | |
| """Plain-English explanation of agent findings (if educational_mode enabled).""" | |
| execution_time_seconds: float = 0.0 | |
| """Time taken to execute this agent.""" | |
| metadata: Dict[str, Any] = field(default_factory=dict) | |
| """Additional agent-specific metadata (e.g., indicator values, pattern names).""" | |
| class PhaseOutput: | |
| """Output from a complete analysis phase.""" | |
| phase: AnalysisPhase | |
| """Which phase this output represents.""" | |
| agents: List[AgentOutput] = field(default_factory=list) | |
| """Outputs from all agents in this phase.""" | |
| phase_summary: str = "" | |
| """High-level summary of this phase's findings.""" | |
| execution_time_seconds: float = 0.0 | |
| """Total time to execute all agents in this phase.""" | |
| score: Optional[float] = None | |
| """Numeric score (0-10) representing phase analysis outcome. | |
| 0=extremely bearish, 5=neutral, 10=extremely bullish.""" | |
| class AnalysisReport: | |
| """Complete analysis report with phase-organized results.""" | |
| ticker: str | |
| """Ticker symbol analyzed.""" | |
| timeframe: str | |
| """Timeframe used for analysis (1d, 1w, 1mo, etc.).""" | |
| investment_style: InvestmentStyle | |
| """Investment style profile used.""" | |
| timestamp: datetime = field(default_factory=datetime.now) | |
| """When analysis was generated.""" | |
| phases: List[PhaseOutput] = field(default_factory=list) | |
| """Phase-by-phase analysis results.""" | |
| executive_summary: str = "" | |
| """High-level summary of entire analysis (generated by final agent).""" | |
| final_decision: Optional[str] = None | |
| """Final trading decision (BUY/SELL/HOLD) if DECISION phase was enabled.""" | |
| total_execution_time_seconds: float = 0.0 | |
| """End-to-end analysis execution time.""" | |
| config: Optional[PhaseConfiguration] = None | |
| """Configuration used for this analysis.""" | |
| cost_summary: Optional[Dict[str, Any]] = None | |
| """Cost tracking information from analysis (if available). | |
| Includes total_cost, tokens, provider breakdown, and free tier usage.""" | |
| def get_phase_output(self, phase: AnalysisPhase) -> Optional[PhaseOutput]: | |
| """Retrieve output for a specific phase. | |
| Args: | |
| phase: The phase to retrieve | |
| Returns: | |
| PhaseOutput if found, None otherwise | |
| """ | |
| for p in self.phases: | |
| if p.phase == phase: | |
| return p | |
| return None | |
| def to_markdown(self) -> str: | |
| """Convert report to markdown format for display. | |
| Returns: | |
| Markdown-formatted report string | |
| """ | |
| # Header section | |
| style_name = self.investment_style.value.replace("_", " ").title() | |
| md = f"""# Analysis Report: {self.ticker} | |
| **Timeframe**: {self.timeframe} | |
| **Investment Style**: {style_name} | |
| **Generated**: {self.timestamp.strftime("%Y-%m-%d %H:%M:%S")} | |
| **Execution Time**: {self.total_execution_time_seconds:.1f}s | |
| --- | |
| ## Executive Summary | |
| {self.executive_summary} | |
| """ | |
| # Final decision (if available) | |
| if self.final_decision: | |
| md += f"\n**Final Decision**: {self.final_decision}\n" | |
| # Cost information (if available) | |
| if self.cost_summary: | |
| md += "\n## Analysis Cost Summary\n\n" | |
| md += f"**Total Cost**: ${self.cost_summary['total_cost']:.4f}\n" | |
| md += f"**Total Tokens**: {self.cost_summary['total_tokens']:,} " | |
| md += f"({self.cost_summary['total_input_tokens']:,} in + {self.cost_summary['total_output_tokens']:,} out)\n" | |
| # Provider breakdown | |
| if self.cost_summary.get("provider_costs"): | |
| md += "\n**Cost by Provider:**\n\n" | |
| md += "| Provider | Cost | Tokens | Free Tier Calls |\n" | |
| md += "|----------|------|--------|----------------|\n" | |
| for provider, cost in sorted( | |
| self.cost_summary["provider_costs"].items(), | |
| key=lambda x: x[1], | |
| reverse=True, | |
| ): | |
| tokens = self.cost_summary.get("provider_tokens", {}).get( | |
| provider, 0 | |
| ) | |
| free_calls = self.cost_summary.get("free_tier_calls", {}).get( | |
| provider, 0 | |
| ) | |
| if free_calls > 0: | |
| cost_str = ( | |
| f"${cost:.4f} (free)" if cost == 0.0 else f"${cost:.4f}" | |
| ) | |
| md += ( | |
| f"| {provider} | {cost_str} | {tokens:,} | {free_calls} |\n" | |
| ) | |
| else: | |
| md += f"| {provider} | ${cost:.4f} | {tokens:,} | 0 |\n" | |
| md += "\n*Costs are estimates based on current pricing. Free tier usage is tracked automatically.*\n" | |
| md += "\n---\n\n" | |
| # Phase-by-phase breakdown | |
| for phase_output in self.phases: | |
| phase_name = phase_output.phase.value.replace("_", " ").title() | |
| md += f"## {phase_name} Phase\n\n" | |
| if phase_output.phase_summary: | |
| md += f"*{phase_output.phase_summary}*\n\n" | |
| md += f"*Execution time: {phase_output.execution_time_seconds:.1f}s*\n\n" | |
| # Agent outputs within this phase | |
| for agent_output in phase_output.agents: | |
| md += f"### {agent_output.agent_name}\n\n" | |
| md += f"{agent_output.report}\n\n" | |
| # Educational notes | |
| if agent_output.educational_notes: | |
| md += f"💡 **Plain English**: {agent_output.educational_notes}\n\n" | |
| # Embedded charts | |
| for chart_b64 in agent_output.charts: | |
| md += f'<img src="data:image/png;base64,{chart_b64}" width="800"/>\n\n' | |
| md += "\n---\n\n" | |
| return md | |
| def to_dict(self) -> Dict[str, Any]: | |
| """Convert report to dictionary for JSON serialization. | |
| Returns: | |
| Dictionary representation of the report | |
| """ | |
| return { | |
| "ticker": self.ticker, | |
| "timeframe": self.timeframe, | |
| "investment_style": self.investment_style.value, | |
| "timestamp": self.timestamp.isoformat(), | |
| "executive_summary": self.executive_summary, | |
| "final_decision": self.final_decision, | |
| "total_execution_time_seconds": self.total_execution_time_seconds, | |
| "cost_summary": self.cost_summary if self.cost_summary else None, | |
| "config": { | |
| "investment_style": self.config.investment_style.value | |
| if self.config | |
| else None, | |
| "enabled_phases": [p.value for p in self.config.enabled_phases] | |
| if self.config | |
| else [], | |
| "timeframe": self.config.timeframe if self.config else None, | |
| "chart_period_days": self.config.chart_period_days | |
| if self.config | |
| else None, | |
| "educational_mode": self.config.educational_mode | |
| if self.config | |
| else None, | |
| } | |
| if self.config | |
| else None, | |
| "phases": [ | |
| { | |
| "phase": phase.phase.value, | |
| "phase_summary": phase.phase_summary, | |
| "execution_time_seconds": phase.execution_time_seconds, | |
| "agents": [ | |
| { | |
| "agent_name": agent.agent_name, | |
| "report": agent.report, | |
| "charts": agent.charts, | |
| "educational_notes": agent.educational_notes, | |
| "execution_time_seconds": agent.execution_time_seconds, | |
| "metadata": agent.metadata, | |
| } | |
| for agent in phase.agents | |
| ], | |
| } | |
| for phase in self.phases | |
| ], | |
| } | |
| def to_html(self) -> str: | |
| """Convert report to HTML format with enhanced styling. | |
| Returns: | |
| HTML-formatted report string with CSS styling | |
| """ | |
| style_name = self.investment_style.value.replace("_", " ").title() | |
| # CSS styling | |
| html = """ | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Analysis Report: {ticker}</title> | |
| <style> | |
| body {{ | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | |
| line-height: 1.6; | |
| color: #333; | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| background-color: #f5f5f5; | |
| }} | |
| .container {{ | |
| background-color: white; | |
| padding: 40px; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| }} | |
| h1 {{ | |
| color: #1a1a1a; | |
| border-bottom: 3px solid #4CAF50; | |
| padding-bottom: 10px; | |
| margin-bottom: 20px; | |
| }} | |
| h2 {{ | |
| color: #2c3e50; | |
| margin-top: 40px; | |
| margin-bottom: 20px; | |
| border-bottom: 2px solid #e0e0e0; | |
| padding-bottom: 10px; | |
| }} | |
| h3 {{ | |
| color: #34495e; | |
| margin-top: 30px; | |
| margin-bottom: 15px; | |
| }} | |
| .metadata {{ | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 10px; | |
| background-color: #f8f9fa; | |
| padding: 20px; | |
| border-radius: 6px; | |
| margin-bottom: 30px; | |
| }} | |
| .metadata-item {{ | |
| display: flex; | |
| flex-direction: column; | |
| }} | |
| .metadata-label {{ | |
| font-weight: bold; | |
| color: #666; | |
| font-size: 0.9em; | |
| margin-bottom: 5px; | |
| }} | |
| .metadata-value {{ | |
| color: #1a1a1a; | |
| font-size: 1.1em; | |
| }} | |
| .executive-summary {{ | |
| background-color: #e8f5e9; | |
| border-left: 4px solid #4CAF50; | |
| padding: 20px; | |
| margin: 20px 0; | |
| border-radius: 4px; | |
| }} | |
| .final-decision {{ | |
| background-color: #fff3e0; | |
| border-left: 4px solid #FF9800; | |
| padding: 20px; | |
| margin: 20px 0; | |
| border-radius: 4px; | |
| font-size: 1.2em; | |
| font-weight: bold; | |
| }} | |
| .phase-section {{ | |
| margin-bottom: 40px; | |
| border: 1px solid #e0e0e0; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| }} | |
| .phase-header {{ | |
| background-color: #3498db; | |
| color: white; | |
| padding: 15px 20px; | |
| font-size: 1.3em; | |
| font-weight: bold; | |
| }} | |
| .phase-content {{ | |
| padding: 20px; | |
| }} | |
| .phase-summary {{ | |
| font-style: italic; | |
| color: #555; | |
| margin-bottom: 15px; | |
| padding: 10px; | |
| background-color: #f8f9fa; | |
| border-radius: 4px; | |
| }} | |
| .agent-output {{ | |
| margin-bottom: 30px; | |
| padding: 15px; | |
| background-color: #fafafa; | |
| border-radius: 6px; | |
| }} | |
| .agent-name {{ | |
| font-weight: bold; | |
| color: #2c3e50; | |
| font-size: 1.1em; | |
| margin-bottom: 10px; | |
| }} | |
| .educational-note {{ | |
| background-color: #e3f2fd; | |
| border-left: 4px solid #2196F3; | |
| padding: 15px; | |
| margin: 15px 0; | |
| border-radius: 4px; | |
| }} | |
| .educational-note::before {{ | |
| content: "💡 "; | |
| font-size: 1.2em; | |
| }} | |
| .chart {{ | |
| margin: 20px 0; | |
| text-align: center; | |
| }} | |
| .chart img {{ | |
| max-width: 100%; | |
| height: auto; | |
| border-radius: 4px; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| }} | |
| .execution-time {{ | |
| color: #666; | |
| font-size: 0.9em; | |
| font-style: italic; | |
| }} | |
| hr {{ | |
| border: none; | |
| border-top: 1px solid #e0e0e0; | |
| margin: 30px 0; | |
| }} | |
| @media print {{ | |
| body {{ | |
| background-color: white; | |
| }} | |
| .container {{ | |
| box-shadow: none; | |
| padding: 0; | |
| }} | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>Analysis Report: {ticker}</h1> | |
| <div class="metadata"> | |
| <div class="metadata-item"> | |
| <span class="metadata-label">Timeframe</span> | |
| <span class="metadata-value">{timeframe}</span> | |
| </div> | |
| <div class="metadata-item"> | |
| <span class="metadata-label">Investment Style</span> | |
| <span class="metadata-value">{style_name}</span> | |
| </div> | |
| <div class="metadata-item"> | |
| <span class="metadata-label">Generated</span> | |
| <span class="metadata-value">{timestamp}</span> | |
| </div> | |
| <div class="metadata-item"> | |
| <span class="metadata-label">Execution Time</span> | |
| <span class="metadata-value">{execution_time:.1f}s</span> | |
| </div> | |
| </div> | |
| <h2>Executive Summary</h2> | |
| <div class="executive-summary"> | |
| {executive_summary} | |
| </div> | |
| """.format( | |
| ticker=self.ticker, | |
| timeframe=self.timeframe, | |
| style_name=style_name, | |
| timestamp=self.timestamp.strftime("%Y-%m-%d %H:%M:%S"), | |
| execution_time=self.total_execution_time_seconds, | |
| executive_summary=self._markdown_to_html(self.executive_summary), | |
| ) | |
| # Final decision | |
| if self.final_decision: | |
| html += f""" | |
| <div class="final-decision"> | |
| Final Decision: {self.final_decision} | |
| </div> | |
| """ | |
| # Cost summary | |
| if self.cost_summary: | |
| html += """ | |
| <h2>Analysis Cost Summary</h2> | |
| <div class="metadata"> | |
| <div class="metadata-item"> | |
| <span class="metadata-label">Total Cost</span> | |
| <span class="metadata-value">${total_cost:.4f}</span> | |
| </div> | |
| <div class="metadata-item"> | |
| <span class="metadata-label">Total Tokens</span> | |
| <span class="metadata-value">{total_tokens:,}</span> | |
| </div> | |
| <div class="metadata-item"> | |
| <span class="metadata-label">Input Tokens</span> | |
| <span class="metadata-value">{input_tokens:,}</span> | |
| </div> | |
| <div class="metadata-item"> | |
| <span class="metadata-label">Output Tokens</span> | |
| <span class="metadata-value">{output_tokens:,}</span> | |
| </div> | |
| </div> | |
| """.format( | |
| total_cost=self.cost_summary["total_cost"], | |
| total_tokens=self.cost_summary["total_tokens"], | |
| input_tokens=self.cost_summary["total_input_tokens"], | |
| output_tokens=self.cost_summary["total_output_tokens"], | |
| ) | |
| # Provider breakdown table | |
| if self.cost_summary.get("provider_costs"): | |
| html += """ | |
| <h3>Cost by Provider</h3> | |
| <table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;"> | |
| <thead> | |
| <tr style="background-color: #f0f0f0; border-bottom: 2px solid #ddd;"> | |
| <th style="padding: 12px; text-align: left;">Provider</th> | |
| <th style="padding: 12px; text-align: right;">Cost</th> | |
| <th style="padding: 12px; text-align: right;">Tokens</th> | |
| <th style="padding: 12px; text-align: right;">Free Tier Calls</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| """ | |
| for provider, cost in sorted( | |
| self.cost_summary["provider_costs"].items(), | |
| key=lambda x: x[1], | |
| reverse=True, | |
| ): | |
| tokens = self.cost_summary.get("provider_tokens", {}).get( | |
| provider, 0 | |
| ) | |
| free_calls = self.cost_summary.get("free_tier_calls", {}).get( | |
| provider, 0 | |
| ) | |
| if free_calls > 0: | |
| cost_str = ( | |
| f"${cost:.4f} (free)" if cost == 0.0 else f"${cost:.4f}" | |
| ) | |
| style = "background-color: #e8f5e9;" if cost == 0.0 else "" | |
| else: | |
| cost_str = f"${cost:.4f}" | |
| style = "" | |
| html += f""" | |
| <tr style="border-bottom: 1px solid #ddd; {style}"> | |
| <td style="padding: 10px;">{provider}</td> | |
| <td style="padding: 10px; text-align: right;">{cost_str}</td> | |
| <td style="padding: 10px; text-align: right;">{tokens:,}</td> | |
| <td style="padding: 10px; text-align: right;">{free_calls}</td> | |
| </tr> | |
| """ | |
| html += """ | |
| </tbody> | |
| </table> | |
| <p style="font-size: 0.9em; color: #666; font-style: italic;"> | |
| Costs are estimates based on current pricing. Free tier usage is tracked automatically. | |
| </p> | |
| """ | |
| html += "\n <hr>\n" | |
| # Phase-by-phase breakdown | |
| for phase_output in self.phases: | |
| phase_name = phase_output.phase.value.replace("_", " ").title() | |
| html += f""" | |
| <div class="phase-section"> | |
| <div class="phase-header">{phase_name} Phase</div> | |
| <div class="phase-content"> | |
| """ | |
| if phase_output.phase_summary: | |
| html += f""" | |
| <div class="phase-summary">{self._markdown_to_html(phase_output.phase_summary)}</div> | |
| """ | |
| html += f""" | |
| <p class="execution-time">Execution time: {phase_output.execution_time_seconds:.1f}s</p> | |
| """ | |
| # Agent outputs within this phase | |
| for agent_output in phase_output.agents: | |
| html += f""" | |
| <div class="agent-output"> | |
| <div class="agent-name">{agent_output.agent_name}</div> | |
| <div class="agent-report"> | |
| {self._markdown_to_html(agent_output.report)} | |
| </div> | |
| """ | |
| # Educational notes | |
| if agent_output.educational_notes: | |
| html += f""" | |
| <div class="educational-note"> | |
| <strong>Plain English:</strong> {self._markdown_to_html(agent_output.educational_notes)} | |
| </div> | |
| """ | |
| # Embedded charts | |
| if agent_output.charts: | |
| for chart_b64 in agent_output.charts: | |
| html += f""" | |
| <div class="chart"> | |
| <img src="data:image/png;base64,{chart_b64}" alt="Chart"> | |
| </div> | |
| """ | |
| html += """ | |
| </div> | |
| """ | |
| html += """ | |
| </div> | |
| </div> | |
| """ | |
| html += """ | |
| </div> | |
| </body> | |
| </html> | |
| """ | |
| return html | |
| def _markdown_to_html(self, markdown_text: str) -> str: | |
| """Convert basic markdown to HTML (simplified converter). | |
| Args: | |
| markdown_text: Markdown-formatted text | |
| Returns: | |
| HTML-formatted text with basic markdown converted | |
| """ | |
| if not markdown_text: | |
| return "" | |
| import re | |
| html = markdown_text | |
| # Bold text | |
| html = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", html) | |
| # Italic text | |
| html = re.sub(r"\*(.+?)\*", r"<em>\1</em>", html) | |
| # Links | |
| html = re.sub(r"\[(.+?)\]\((.+?)\)", r'<a href="\2">\1</a>', html) | |
| # Code blocks | |
| html = re.sub(r"`(.+?)`", r"<code>\1</code>", html) | |
| # Line breaks | |
| html = html.replace("\n\n", "</p><p>") | |
| html = html.replace("\n", "<br>") | |
| # Wrap in paragraph if not already | |
| if not html.startswith("<"): | |
| html = f"<p>{html}</p>" | |
| return html | |