report_generator.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. """Report generation service (P4-M4).
  2. Generates Word simulation reports from task results.
  3. Includes cover, parameters, results summary, metrics, AI analysis.
  4. """
  5. import json
  6. import os
  7. from datetime import datetime
  8. from typing import Any, Dict, List, Optional
  9. try:
  10. from docx import Document
  11. from docx.shared import Inches, Pt, RGBColor
  12. from docx.enum.text import WD_ALIGN_PARAGRAPH
  13. HAS_DOCX = True
  14. except ImportError:
  15. HAS_DOCX = False
  16. class ReportGenerator:
  17. """Generate simulation reports from task results."""
  18. def __init__(self, output_dir: Optional[str] = None):
  19. self.output_dir = output_dir or os.path.join(
  20. os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
  21. "output", "reports"
  22. )
  23. os.makedirs(self.output_dir, exist_ok=True)
  24. def generate_report(self, task_data: Dict[str, Any],
  25. ai_analysis: Optional[Dict[str, Any]] = None,
  26. report_title: Optional[str] = None) -> str:
  27. """Generate a Word report from task data.
  28. Args:
  29. task_data: Task data including parameters, results, metrics
  30. ai_analysis: Optional AI analysis results
  31. report_title: Custom report title
  32. Returns:
  33. Path to generated report file
  34. """
  35. if not HAS_DOCX:
  36. return self._generate_json_report(task_data, ai_analysis, report_title)
  37. doc = Document()
  38. # Title
  39. title = report_title or f"Simulation Report - {task_data.get('task_name', 'Task')}"
  40. heading = doc.add_heading(title, level=0)
  41. heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
  42. # Metadata
  43. doc.add_paragraph(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
  44. doc.add_paragraph(f"Task ID: {task_data.get('task_id', 'N/A')}")
  45. doc.add_paragraph(f"Status: {task_data.get('status', 'N/A')}")
  46. # Parameters section
  47. doc.add_heading("Simulation Parameters", level=1)
  48. params = task_data.get("plan_data", {})
  49. if params:
  50. table = doc.add_table(rows=1, cols=2)
  51. table.style = "Table Grid"
  52. hdr = table.rows[0].cells
  53. hdr[0].text = "Parameter"
  54. hdr[1].text = "Value"
  55. for key, value in params.items():
  56. row = table.add_row().cells
  57. row[0].text = str(key)
  58. row[1].text = str(value)
  59. # Results summary
  60. doc.add_heading("Results Summary", level=1)
  61. results = task_data.get("result_metrics", {})
  62. if results:
  63. table = doc.add_table(rows=1, cols=2)
  64. table.style = "Table Grid"
  65. hdr = table.rows[0].cells
  66. hdr[0].text = "Metric"
  67. hdr[1].text = "Value"
  68. for key, value in results.items():
  69. row = table.add_row().cells
  70. row[0].text = str(key)
  71. row[1].text = str(value)
  72. # Per-point results
  73. doc.add_heading("Per-Point Results", level=1)
  74. points = task_data.get("points", [])
  75. if points:
  76. table = doc.add_table(rows=1, cols=4)
  77. table.style = "Table Grid"
  78. hdr = table.rows[0].cells
  79. hdr[0].text = "Point"
  80. hdr[1].text = "Parameters"
  81. hdr[2].text = "Status"
  82. hdr[3].text = "Duration (s)"
  83. for i, point in enumerate(points):
  84. row = table.add_row().cells
  85. row[0].text = str(i + 1)
  86. row[1].text = json.dumps(point.get("params", {}), ensure_ascii=False)
  87. row[2].text = point.get("status", "N/A")
  88. row[3].text = str(point.get("duration_s", "N/A"))
  89. # AI Analysis section
  90. if ai_analysis:
  91. doc.add_heading("AI Analysis", level=1)
  92. if "summary" in ai_analysis:
  93. doc.add_paragraph(ai_analysis["summary"])
  94. if "convergence" in ai_analysis:
  95. doc.add_heading("Convergence Assessment", level=2)
  96. conv = ai_analysis["convergence"]
  97. doc.add_paragraph(f"Converged: {conv.get('converged', 'N/A')}")
  98. doc.add_paragraph(f"Confidence: {conv.get('confidence', 'N/A')}")
  99. if "recommendations" in ai_analysis:
  100. doc.add_heading("Recommendations", level=2)
  101. for rec in ai_analysis["recommendations"]:
  102. doc.add_paragraph(rec, style="List Bullet")
  103. # Save
  104. filename = f"report_{task_data.get('task_id', 'task')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.docx"
  105. filepath = os.path.join(self.output_dir, filename)
  106. doc.save(filepath)
  107. return filepath
  108. def _generate_json_report(self, task_data: Dict[str, Any],
  109. ai_analysis: Optional[Dict[str, Any]],
  110. report_title: Optional[str]) -> str:
  111. """Fallback: generate JSON report when python-docx is unavailable."""
  112. report = {
  113. "title": report_title or f"Simulation Report - {task_data.get('task_name', 'Task')}",
  114. "generated_at": datetime.now().isoformat(),
  115. "task_data": task_data,
  116. "ai_analysis": ai_analysis,
  117. }
  118. filename = f"report_{task_data.get('task_id', 'task')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
  119. filepath = os.path.join(self.output_dir, filename)
  120. with open(filepath, "w", encoding="utf-8") as f:
  121. json.dump(report, f, ensure_ascii=False, indent=2)
  122. return filepath
  123. def list_reports(self) -> List[Dict[str, Any]]:
  124. """List all generated reports."""
  125. reports = []
  126. if os.path.exists(self.output_dir):
  127. for f in os.listdir(self.output_dir):
  128. if f.endswith((".docx", ".json")):
  129. filepath = os.path.join(self.output_dir, f)
  130. reports.append({
  131. "filename": f,
  132. "path": filepath,
  133. "size": os.path.getsize(filepath),
  134. "created_at": datetime.fromtimestamp(os.path.getctime(filepath)).isoformat(),
  135. })
  136. return sorted(reports, key=lambda x: x["created_at"], reverse=True)
  137. _generator: Optional[ReportGenerator] = None
  138. def get_report_generator() -> ReportGenerator:
  139. global _generator
  140. if _generator is None:
  141. _generator = ReportGenerator()
  142. return _generator