mgbam commited on
Commit
07918a8
·
verified ·
1 Parent(s): 6ba91d3

Update services/pdf_report.py

Browse files
Files changed (1) hide show
  1. services/pdf_report.py +130 -80
services/pdf_report.py CHANGED
@@ -1,118 +1,168 @@
1
  # /home/user/app/services/pdf_report.py
2
  from reportlab.lib.pagesizes import letter
3
- from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image
4
- from reportlab.lib.styles import getSampleStyleSheet
5
  from reportlab.lib.units import inch
 
6
  from io import BytesIO
7
- from typing import List, Optional # Optional for tool_name
8
  from pathlib import Path
 
9
 
10
- # Assuming ChatMessage is defined in models.py or models/chat.py
11
- # Make sure ChatMessage has 'role', 'content', and optionally 'tool_name' attributes.
12
- # Example ChatMessage structure (Pydantic or dataclass):
13
- # class ChatMessage(BaseModel):
14
- # role: str # "user", "assistant", "tool"
15
- # content: str
16
- # tool_name: Optional[str] = None
17
- from models import ChatMessage # Or from models.chat import ChatMessage
18
- from config.settings import settings # For APP_TITLE
19
- from assets.logo import get_logo_path # Corrected import path
20
- from services.logger import app_logger # For logging issues
21
-
22
- def generate_pdf_report(chat_messages: List[ChatMessage], patient_name: str = "Patient") -> BytesIO:
 
 
 
 
23
  buffer = BytesIO()
24
- # Reduce margins for more content space
25
  doc = SimpleDocTemplate(buffer, pagesize=letter,
26
  leftMargin=0.75*inch, rightMargin=0.75*inch,
27
  topMargin=0.75*inch, bottomMargin=0.75*inch)
28
  styles = getSampleStyleSheet()
29
  story = []
30
 
31
- # Style adjustments
32
- styles['h1'].alignment = 1 # Center align H1
33
- styles['Normal'].spaceBefore = 6
34
- styles['Normal'].spaceAfter = 6
35
- code_style = styles['Code'] # For AI and Tool messages
36
- code_style.spaceBefore = 6
37
- code_style.spaceAfter = 6
38
- code_style.leftIndent = 10 # Indent AI/Tool messages slightly
 
 
 
 
39
 
40
- # 1. Logo (optional)
 
 
 
 
 
 
 
 
 
 
 
 
41
  logo_path_str = get_logo_path()
42
- if logo_path_str:
43
- logo_file = Path(logo_path_str) # Convert to Path object
44
- if logo_file.exists():
45
- try:
46
- # Adjust width/height as needed, preserve aspect ratio if possible
47
- img = Image(logo_file, width=1.0*inch, height=1.0*inch, preserveAspectRatio=True)
48
- img.hAlign = 'LEFT' # Align logo to the left
49
- story.append(img)
50
- story.append(Spacer(1, 0.2*inch))
51
- except Exception as e:
52
- app_logger.warning(f"Could not add logo to PDF from path '{logo_path_str}': {e}")
53
- else:
54
- app_logger.warning(f"Logo path '{logo_path_str}' from get_logo_path() does not exist.")
55
-
56
- # 2. Title
57
- title_text = f"{settings.APP_TITLE} - Consultation Report"
58
- title = Paragraph(title_text, styles['h1'])
59
- story.append(title)
60
  story.append(Spacer(1, 0.2*inch))
61
 
62
- # 3. Patient Info
63
- patient_info_text = f"<b>Patient:</b> {patient_name}" # Make "Patient:" bold
64
- patient_info = Paragraph(patient_info_text, styles['h2'])
65
- story.append(patient_info)
66
- story.append(Spacer(1, 0.3*inch)) # More space after patient info
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
- # 4. Chat Transcript
69
- story.append(Paragraph("<u>Consultation Transcript:</u>", styles['h3'])) # Underline transcript title
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  story.append(Spacer(1, 0.1*inch))
71
 
72
- for msg in chat_messages:
 
73
  prefix = ""
74
  current_style = styles['Normal']
75
 
76
  if msg.role == 'assistant':
77
- prefix = "<b>AI Assistant:</b> "
78
- current_style = code_style
79
  elif msg.role == 'user':
80
- prefix = "<b>You:</b> "
81
- current_style = styles['Normal']
82
  elif msg.role == 'tool':
83
- tool_name = getattr(msg, 'tool_name', 'UnknownTool') # Handle if tool_name is missing
84
- prefix = f"<b>Tool ({tool_name}):</b> "
85
- current_style = code_style
86
- else:
87
- prefix = f"<b>{msg.role.capitalize()}:</b> " # Fallback for other roles
88
-
89
- # Sanitize content for ReportLab Paragraph (handles HTML-like tags)
90
- # Replace newlines with <br/> for PDF line breaks
91
- content = msg.content.replace('\n', '<br/>\n')
92
- # Escape < and > that are not part of <br/>
93
- content = content.replace("<", "<").replace(">", ">").replace("<br/>", "<br/>")
 
 
94
 
95
- try:
96
- story.append(Paragraph(prefix + content, current_style))
97
- story.append(Spacer(1, 0.05*inch)) # Smaller spacer between messages
98
- except Exception as e:
99
- app_logger.error(f"Error adding message to PDF: {prefix}{content}. Error: {e}")
100
- story.append(Paragraph(f"<i>Error rendering message: {e}</i>", styles['Italic']))
101
 
 
 
 
 
102
 
103
  try:
104
  doc.build(story)
105
  buffer.seek(0)
106
- app_logger.info(f"PDF report generated successfully for patient: {patient_name}")
107
  except Exception as e:
108
- app_logger.error(f"Failed to build PDF document: {e}", exc_info=True)
109
- # Return an empty or error buffer if build fails
110
- buffer = BytesIO() # Reset buffer
 
111
  error_doc = SimpleDocTemplate(buffer, pagesize=letter)
112
- error_story = [Paragraph("Error generating PDF report. Please check logs.", styles['h1'])]
 
113
  try:
114
  error_doc.build(error_story)
115
- except: pass # If even error doc fails, just return empty buffer
116
  buffer.seek(0)
117
-
118
  return buffer
 
1
  # /home/user/app/services/pdf_report.py
2
  from reportlab.lib.pagesizes import letter
3
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image, Table, TableStyle, PageBreak
4
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
5
  from reportlab.lib.units import inch
6
+ from reportlab.lib import colors
7
  from io import BytesIO
8
+ from typing import List, Dict, Any # For the input data structure
9
  from pathlib import Path
10
+ from datetime import datetime
11
 
12
+ # from models import ChatMessage # Not strictly needed if we use dicts/mock objects
13
+ from config.settings import settings
14
+ from assets.logo import get_logo_path
15
+ from services.logger import app_logger
16
+
17
+ class MockChatMessage: # Helper class if generate_pdf_report expects objects with attributes
18
+ def __init__(self, role: str, content: str, timestamp: datetime, tool_name: Optional[str] = None, **kwargs):
19
+ self.role = role
20
+ self.content = content
21
+ self.timestamp = timestamp
22
+ self.tool_name = tool_name
23
+ # Allow other attributes like source_references, confidence_score if added
24
+ for key, value in kwargs.items():
25
+ setattr(self, key, value)
26
+
27
+
28
+ def generate_pdf_report(report_data: Dict[str, Any]) -> BytesIO:
29
  buffer = BytesIO()
 
30
  doc = SimpleDocTemplate(buffer, pagesize=letter,
31
  leftMargin=0.75*inch, rightMargin=0.75*inch,
32
  topMargin=0.75*inch, bottomMargin=0.75*inch)
33
  styles = getSampleStyleSheet()
34
  story = []
35
 
36
+ # Custom Styles
37
+ styles.add(ParagraphStyle(name='Justify', alignment=4, parent=styles['Normal']))
38
+ styles.add(ParagraphStyle(name='Disclaimer', parent=styles['Italic'], fontSize=8, leading=10))
39
+ styles.add(ParagraphStyle(name='SectionHeader', parent=styles['h2'], spaceBefore=12, spaceAfter=6, keepWithNext=1))
40
+ styles.add(ParagraphStyle(name='SubHeader', parent=styles['h3'], spaceBefore=6, spaceAfter=3, keepWithNext=1))
41
+ styles.add(ParagraphStyle(name='ListItem', parent=styles['Normal'], leftIndent=0.25*inch, bulletIndent=0.1*inch))
42
+
43
+ user_msg_style = ParagraphStyle(name='UserMessage', parent=styles['Normal'], spaceBefore=3, spaceAfter=3, leftIndent=0.1*inch, backColor=colors.whitesmoke)
44
+ ai_msg_style = ParagraphStyle(name='AIMessage', parent=styles['Normal'], spaceBefore=3, spaceAfter=3, leftIndent=0.1*inch, backColor=colors.lightcyan)
45
+ tool_msg_style = ParagraphStyle(name='ToolMessage', parent=styles['Code'], spaceBefore=3, spaceAfter=3, leftIndent=0.1*inch, backColor=colors.lightgrey, textColor=colors.darkblue)
46
+ system_msg_style = ParagraphStyle(name='SystemMessage', parent=styles['Italic'], spaceBefore=3, spaceAfter=3, leftIndent=0.1*inch, backColor=colors.beige, fontSize=9)
47
+
48
 
49
+ # --- Extract data from report_data dictionary ---
50
+ patient_name = report_data.get("patient_name", "N/A")
51
+ session_id = report_data.get("session_id", "N/A")
52
+ session_title = report_data.get("session_title", "Untitled Consultation")
53
+ session_start_time_obj = report_data.get("session_start_time")
54
+ patient_context_summary = report_data.get("patient_context_summary", "No specific patient context was provided for this session.")
55
+
56
+ # Convert message dictionaries to MockChatMessage objects if needed, or use dicts directly
57
+ raw_messages = report_data.get("messages", [])
58
+ messages = [MockChatMessage(**msg_data) for msg_data in raw_messages] # If generate_pdf_report expects objects
59
+
60
+
61
+ # 1. Logo and Document Header
62
  logo_path_str = get_logo_path()
63
+ if logo_path_str and Path(logo_path_str).exists():
64
+ try:
65
+ img = Image(logo_path_str, width=0.8*inch, height=0.8*inch, preserveAspectRatio=True)
66
+ img.hAlign = 'LEFT'
67
+ story.append(img)
68
+ story.append(Spacer(1, 0.1*inch))
69
+ except Exception as e:
70
+ app_logger.warning(f"PDF Report: Could not add logo: {e}")
71
+
72
+ story.append(Paragraph(f"{settings.APP_TITLE}", styles['h1']))
73
+ story.append(Paragraph("AI-Assisted Consultation Summary", styles['h2']))
 
 
 
 
 
 
 
74
  story.append(Spacer(1, 0.2*inch))
75
 
76
+ # 2. Report Metadata Table
77
+ report_date_str = datetime.now().strftime('%Y-%m-%d %H:%M UTC')
78
+ session_start_time_str = session_start_time_obj.strftime('%Y-%m-%d %H:%M UTC') if session_start_time_obj else "N/A"
79
+
80
+ meta_data = [
81
+ [Paragraph("<b>Report Generated:</b>", styles['Normal']), Paragraph(report_date_str, styles['Normal'])],
82
+ [Paragraph("<b>Clinician:</b>", styles['Normal']), Paragraph(patient_name, styles['Normal'])], # "patient_name" is the clinician's username
83
+ [Paragraph("<b>Consultation Session ID:</b>", styles['Normal']), Paragraph(str(session_id), styles['Normal'])],
84
+ [Paragraph("<b>Session Title:</b>", styles['Normal']), Paragraph(session_title, styles['Normal'])],
85
+ [Paragraph("<b>Session Start Time:</b>", styles['Normal']), Paragraph(session_start_time_str, styles['Normal'])],
86
+ ]
87
+ meta_table = Table(meta_data, colWidths=[1.8*inch, None])
88
+ meta_table.setStyle(TableStyle([
89
+ ('GRID', (0,0), (-1,-1), 0.5, colors.grey),
90
+ ('VALIGN', (0,0), (-1,-1), 'TOP'),
91
+ ('BACKGROUND', (0,0), (0,-1), colors.whitesmoke)
92
+ ]))
93
+ story.append(meta_table)
94
+ story.append(Spacer(1, 0.3*inch))
95
 
96
+ # 3. Disclaimer
97
+ story.append(Paragraph("<b>Important Disclaimer:</b>", styles['SubHeader']))
98
+ story.append(Paragraph(settings.MAIN_DISCLAIMER_LONG, styles['Disclaimer']))
99
+ story.append(Paragraph(settings.SIMULATION_DISCLAIMER, styles['Disclaimer']))
100
+ story.append(Spacer(1, 0.3*inch))
101
+
102
+ # 4. Patient Context Provided (if any)
103
+ if patient_context_summary and patient_context_summary != "No specific patient context provided.":
104
+ story.append(Paragraph("Patient Context Provided (Simulated Data):", styles['SectionHeader']))
105
+ # Assuming patient_context_summary is a string. If it's structured, format it nicely.
106
+ # Example: if it's "Key1: Value1; Key2: Value2", split and list.
107
+ context_parts = patient_context_summary.replace("Patient Context: ", "").split(';')
108
+ for part in context_parts:
109
+ if part.strip():
110
+ story.append(Paragraph(f"• {part.strip()}", styles['ListItem']))
111
+ story.append(Spacer(1, 0.2*inch))
112
+
113
+ # 5. Consultation Transcript
114
+ story.append(Paragraph("Consultation Transcript:", styles['SectionHeader']))
115
  story.append(Spacer(1, 0.1*inch))
116
 
117
+ for msg in messages:
118
+ timestamp_str = msg.timestamp.strftime('%Y-%m-%d %H:%M:%S UTC') if msg.timestamp else "N/A"
119
  prefix = ""
120
  current_style = styles['Normal']
121
 
122
  if msg.role == 'assistant':
123
+ prefix = f"AI Assistant ({timestamp_str}):"
124
+ current_style = ai_msg_style
125
  elif msg.role == 'user':
126
+ prefix = f"Clinician ({timestamp_str}):"
127
+ current_style = user_msg_style
128
  elif msg.role == 'tool':
129
+ tool_name = getattr(msg, 'tool_name', 'Tool')
130
+ prefix = f"{tool_name.capitalize()} ({timestamp_str}):"
131
+ current_style = tool_msg_style
132
+ elif msg.role == 'system': # Typically system context, might not always be included
133
+ prefix = f"System Context ({timestamp_str}):"
134
+ current_style = system_msg_style
135
+ else: # Fallback for any other roles
136
+ prefix = f"{msg.role.capitalize()} ({timestamp_str}):"
137
+ current_style = styles['Normal']
138
+
139
+ # Sanitize content for ReportLab Paragraph (handles HTML-like tags for <br/>)
140
+ content_for_pdf = msg.content.replace('\n', '<br/>\n')
141
+ content_for_pdf = content_for_pdf.replace("<", "<").replace(">", ">").replace("<br/>", "<br/>")
142
 
143
+ story.append(Paragraph(f"<b>{prefix}</b><br/>{content_for_pdf}", current_style))
144
+ # story.append(Spacer(1, 0.05*inch)) # Removed for tighter message packing
 
 
 
 
145
 
146
+ # Footer on each page (if needed, more complex - requires onLaterPages in SimpleDocTemplate)
147
+ # For simplicity, a final "End of Report"
148
+ story.append(Spacer(1, 0.5*inch))
149
+ story.append(Paragraph("--- End of Report ---", styles['Italic']))
150
 
151
  try:
152
  doc.build(story)
153
  buffer.seek(0)
154
+ app_logger.info(f"PDF report generated for session ID: {session_id}")
155
  except Exception as e:
156
+ app_logger.error(f"Failed to build PDF document for session ID {session_id}: {e}", exc_info=True)
157
+ buffer = BytesIO() # Return empty buffer on error
158
+ # Optionally, create a simple error PDF
159
+ error_styles = getSampleStyleSheet()
160
  error_doc = SimpleDocTemplate(buffer, pagesize=letter)
161
+ error_story = [Paragraph("Error: Could not generate PDF report.", error_styles['h1']),
162
+ Paragraph(f"Details: {str(e)[:500]}", error_styles['Normal'])]
163
  try:
164
  error_doc.build(error_story)
165
+ except: pass # If even error PDF fails, return empty
166
  buffer.seek(0)
167
+
168
  return buffer