mgbam commited on
Commit
6019d24
·
verified ·
1 Parent(s): 8a16650

Update services/pdf_report.py

Browse files
Files changed (1) hide show
  1. services/pdf_report.py +67 -72
services/pdf_report.py CHANGED
@@ -1,30 +1,32 @@
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,
@@ -35,28 +37,26 @@ def generate_pdf_report(report_data: Dict[str, Any]) -> BytesIO:
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()
@@ -65,11 +65,10 @@ def generate_pdf_report(report_data: Dict[str, Any]) -> BytesIO:
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
 
@@ -77,18 +76,18 @@ def generate_pdf_report(report_data: Dict[str, Any]) -> BytesIO:
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))
@@ -96,73 +95,69 @@ def generate_pdf_report(report_data: Dict[str, Any]) -> BytesIO:
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
 
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
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, Optional # IMPORTED Optional
9
  from pathlib import Path
10
  from datetime import datetime
11
 
 
12
  from config.settings import settings
13
+ from assets.logo import get_logo_path # Assuming this function exists and works
14
  from services.logger import app_logger
15
 
16
+ class MockChatMessage:
17
+ """
18
+ A simple class to hold message data for PDF generation if generate_pdf_report
19
+ prefers object attribute access over dictionary key access for messages.
20
+ """
21
  def __init__(self, role: str, content: str, timestamp: datetime, tool_name: Optional[str] = None, **kwargs):
22
  self.role = role
23
  self.content = content
24
  self.timestamp = timestamp
25
  self.tool_name = tool_name
26
+ # Capture any other attributes that might be passed (e.g., source_references)
27
  for key, value in kwargs.items():
28
  setattr(self, key, value)
29
 
 
30
  def generate_pdf_report(report_data: Dict[str, Any]) -> BytesIO:
31
  buffer = BytesIO()
32
  doc = SimpleDocTemplate(buffer, pagesize=letter,
 
37
 
38
  # Custom Styles
39
  styles.add(ParagraphStyle(name='Justify', alignment=4, parent=styles['Normal']))
40
+ styles.add(ParagraphStyle(name='Disclaimer', parent=styles['Italic'], fontSize=8, leading=10, spaceBefore=6, spaceAfter=6))
41
  styles.add(ParagraphStyle(name='SectionHeader', parent=styles['h2'], spaceBefore=12, spaceAfter=6, keepWithNext=1))
42
  styles.add(ParagraphStyle(name='SubHeader', parent=styles['h3'], spaceBefore=6, spaceAfter=3, keepWithNext=1))
43
+ styles.add(ParagraphStyle(name='ListItem', parent=styles['Normal'], leftIndent=0.25*inch, bulletIndent=0.1*inch, spaceBefore=3))
44
 
45
+ user_msg_style = ParagraphStyle(name='UserMessage', parent=styles['Normal'], spaceBefore=3, spaceAfter=3, leftIndent=0.1*inch, backColor=colors.whitesmoke, borderColor=colors.lightgrey, borderWidth=0.5, padding=3)
46
+ ai_msg_style = ParagraphStyle(name='AIMessage', parent=styles['Normal'], spaceBefore=3, spaceAfter=3, leftIndent=0.1*inch, backColor=colors.lightcyan, borderColor=colors.lightgrey, borderWidth=0.5, padding=3)
47
+ tool_msg_style = ParagraphStyle(name='ToolMessage', parent=styles['Code'], spaceBefore=3, spaceAfter=3, leftIndent=0.1*inch, backColor=colors.lightgrey, textColor=colors.darkblue, borderColor=colors.grey, borderWidth=0.5, padding=3)
48
+ system_msg_style = ParagraphStyle(name='SystemMessage', parent=styles['Italic'], spaceBefore=3, spaceAfter=3, leftIndent=0.1*inch, backColor=colors.beige, fontSize=9, borderColor=colors.darkgrey, borderWidth=0.5, padding=3)
 
49
 
50
+ # --- Extract data ---
51
+ patient_name = report_data.get("patient_name", "N/A Clinician") # Clinician's username
52
  session_id = report_data.get("session_id", "N/A")
53
  session_title = report_data.get("session_title", "Untitled Consultation")
54
  session_start_time_obj = report_data.get("session_start_time")
55
  patient_context_summary = report_data.get("patient_context_summary", "No specific patient context was provided for this session.")
56
 
57
+ raw_messages_data = report_data.get("messages", []) # Expects a list of dicts
58
+ # Convert message dictionaries to MockChatMessage objects for consistent attribute access
59
+ messages = [MockChatMessage(**msg_data) for msg_data in raw_messages_data]
 
60
 
61
  # 1. Logo and Document Header
62
  logo_path_str = get_logo_path()
 
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.05*inch)) # Reduced spacer
69
+ except Exception as e: app_logger.warning(f"PDF Report: Could not add logo: {e}")
 
70
 
71
+ story.append(Paragraph(settings.APP_TITLE, styles['h1']))
72
  story.append(Paragraph("AI-Assisted Consultation Summary", styles['h2']))
73
  story.append(Spacer(1, 0.2*inch))
74
 
 
76
  report_date_str = datetime.now().strftime('%Y-%m-%d %H:%M UTC')
77
  session_start_time_str = session_start_time_obj.strftime('%Y-%m-%d %H:%M UTC') if session_start_time_obj else "N/A"
78
 
79
+ meta_data_content = [
80
  [Paragraph("<b>Report Generated:</b>", styles['Normal']), Paragraph(report_date_str, styles['Normal'])],
81
+ [Paragraph("<b>Clinician:</b>", styles['Normal']), Paragraph(patient_name, styles['Normal'])],
82
  [Paragraph("<b>Consultation Session ID:</b>", styles['Normal']), Paragraph(str(session_id), styles['Normal'])],
83
  [Paragraph("<b>Session Title:</b>", styles['Normal']), Paragraph(session_title, styles['Normal'])],
84
  [Paragraph("<b>Session Start Time:</b>", styles['Normal']), Paragraph(session_start_time_str, styles['Normal'])],
85
  ]
86
+ meta_table = Table(meta_data_content, colWidths=[2.0*inch, None]) # Adjusted colWidths
87
  meta_table.setStyle(TableStyle([
88
+ ('GRID', (0,0), (-1,-1), 0.5, colors.grey), ('VALIGN', (0,0), (-1,-1), 'TOP'),
89
+ ('BACKGROUND', (0,0), (0,-1), colors.lightgrey), ('LEFTPADDING', (0,0), (-1,-1), 3),
90
+ ('RIGHTPADDING', (0,0), (-1,-1), 3), ('TOPPADDING', (0,0), (-1,-1), 3), ('BOTTOMPADDING', (0,0), (-1,-1), 3)
91
  ]))
92
  story.append(meta_table)
93
  story.append(Spacer(1, 0.3*inch))
 
95
  # 3. Disclaimer
96
  story.append(Paragraph("<b>Important Disclaimer:</b>", styles['SubHeader']))
97
  story.append(Paragraph(settings.MAIN_DISCLAIMER_LONG, styles['Disclaimer']))
98
+ if settings.SIMULATION_DISCLAIMER: # Only add if defined
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 not in ["No specific patient context was provided for this session.", "Not provided."]:
104
+ story.append(Paragraph("Patient Context Provided by Clinician (Simulated Data):", styles['SectionHeader']))
105
+ context_items = patient_context_summary.replace("Patient Context: ", "").split(';')
106
+ for item in context_items:
107
+ if item.strip(): story.append(Paragraph(f" {item.strip()}", styles['ListItem']))
 
 
 
108
  story.append(Spacer(1, 0.2*inch))
109
 
110
  # 5. Consultation Transcript
111
  story.append(Paragraph("Consultation Transcript:", styles['SectionHeader']))
112
  story.append(Spacer(1, 0.1*inch))
113
 
114
+ for msg in messages: # Iterate over MockChatMessage objects
115
  timestamp_str = msg.timestamp.strftime('%Y-%m-%d %H:%M:%S UTC') if msg.timestamp else "N/A"
116
+ prefix_text = ""
117
+ current_message_style = styles['Normal']
118
 
119
  if msg.role == 'assistant':
120
+ prefix_text = f"AI Assistant ({timestamp_str}):"
121
+ current_message_style = ai_msg_style
122
  elif msg.role == 'user':
123
+ prefix_text = f"Clinician ({timestamp_str}):"
124
+ current_message_style = user_msg_style
125
  elif msg.role == 'tool':
126
+ tool_name_display = getattr(msg, 'tool_name', 'Tool') or "Tool" # Handle None or empty tool_name
127
+ prefix_text = f"{tool_name_display.capitalize()} Output ({timestamp_str}):"
128
+ current_message_style = tool_msg_style
129
+ elif msg.role == 'system' and msg.content.startswith("Initial Patient Context Set:"): # Don't show this verbose system message
130
+ continue
131
+ elif msg.role == 'system':
132
+ prefix_text = f"System Note ({timestamp_str}):"
133
+ current_message_style = system_msg_style
134
+ else:
135
+ prefix_text = f"{msg.role.capitalize()} ({timestamp_str}):"
136
 
 
137
  content_for_pdf = msg.content.replace('\n', '<br/>\n')
138
  content_for_pdf = content_for_pdf.replace("<", "<").replace(">", ">").replace("<br/>", "<br/>")
139
+
140
+ story.append(Paragraph(f"<b>{prefix_text}</b>", styles['Normal'])) # Prefix bolded on its own line
141
+ story.append(Paragraph(content_for_pdf, current_message_style)) # Content in styled box
142
+ story.append(Spacer(1, 0.05*inch))
143
 
 
 
 
 
 
144
  story.append(Spacer(1, 0.5*inch))
145
+ story.append(Paragraph("--- End of Report ---", ParagraphStyle(name='EndOfReport', parent=styles['Italic'], alignment=1)))
146
 
147
  try:
148
  doc.build(story)
149
  buffer.seek(0)
150
+ app_logger.info(f"PDF report generated successfully for session ID: {session_id}")
151
  except Exception as e:
152
  app_logger.error(f"Failed to build PDF document for session ID {session_id}: {e}", exc_info=True)
153
+ buffer = BytesIO() # Reset to empty buffer on error
154
+ # Create a simple error PDF
155
+ error_styles_local = getSampleStyleSheet() # Get fresh styles
156
+ error_doc_local = SimpleDocTemplate(buffer, pagesize=letter)
157
+ error_story_local = [Paragraph("Error: Could not generate PDF report.", error_styles_local['h1']),
158
+ Paragraph(f"Details: {str(e)[:500]}", error_styles_local['Normal'])]
159
+ try: error_doc_local.build(error_story_local)
160
+ except: pass # If even error PDF fails, return empty buffer
 
161
  buffer.seek(0)
162
 
163
  return buffer