mgbam commited on
Commit
085c524
·
verified ·
1 Parent(s): 267db18

Update services/pdf_report.py

Browse files
Files changed (1) hide show
  1. services/pdf_report.py +180 -1
services/pdf_report.py CHANGED
@@ -5,7 +5,7 @@ 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
 
@@ -13,6 +13,185 @@ 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
 
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
9
  from pathlib import Path
10
  from datetime import datetime
11
 
 
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 data class to hold message attributes for PDF generation.
19
+ Ensures consistent attribute access and helps with type handling.
20
+ """
21
+ def __init__(self, role: str, content: str, timestamp: Optional[datetime], tool_name: Optional[str] = None, **kwargs):
22
+ self.role: str = str(role if role is not None else "unknown")
23
+ self.content: str = str(content if content is not None else "")
24
+ self.timestamp: Optional[datetime] = timestamp # Keep as datetime, format when used
25
+ self.tool_name: Optional[str] = str(tool_name) if tool_name is not None else None
26
+
27
+ # Store any other potential attributes (e.g., source_references, confidence)
28
+ for key, value in kwargs.items():
29
+ setattr(self, key, value)
30
+
31
+ def get_formatted_timestamp(self, default_val: str = "Time N/A") -> str:
32
+ return self.timestamp.strftime('%Y-%m-%d %H:%M:%S UTC') if self.timestamp else default_val
33
+
34
+ def get_formatted_content_for_pdf(self) -> str:
35
+ # Ensure content is a string, handle newlines for PDF, and escape HTML-sensitive characters
36
+ text = self.content.replace('\n', '<br/>\n')
37
+ return text.replace("<", "<").replace(">", ">").replace("<br/>", "<br/>")
38
+
39
+
40
+ def generate_pdf_report(report_data: Dict[str, Any]) -> BytesIO:
41
+ buffer = BytesIO()
42
+ doc = SimpleDocTemplate(buffer, pagesize=letter,
43
+ leftMargin=0.75*inch, rightMargin=0.75*inch,
44
+ topMargin=0.75*inch, bottomMargin=0.75*inch)
45
+ styles = getSampleStyleSheet()
46
+ story = []
47
+
48
+ # --- Custom Styles Definition ---
49
+ styles.add(ParagraphStyle(name='Justify', alignment=4, parent=styles['Normal']))
50
+ styles.add(ParagraphStyle(name='Disclaimer', parent=styles['Italic'], fontSize=8, leading=10, spaceBefore=6, spaceAfter=6, textColor=colors.dimgrey))
51
+ styles.add(ParagraphStyle(name='SectionHeader', parent=styles['h2'], spaceBefore=12, spaceAfter=6, keepWithNext=1, textColor=colors.darkblue))
52
+ styles.add(ParagraphStyle(name='SubHeader', parent=styles['h3'], spaceBefore=6, spaceAfter=3, keepWithNext=1, textColor=colors.cadetblue))
53
+ styles.add(ParagraphStyle(name='ListItem', parent=styles['Normal'], leftIndent=0.25*inch, bulletIndent=0.1*inch, spaceBefore=3))
54
+
55
+ base_message_style = ParagraphStyle(name='BaseMessage', parent=styles['Normal'], spaceBefore=2, spaceAfter=2, leftIndent=0.1*inch, rightIndent=0.1*inch, leading=12, borderWidth=0.5, padding=(4, 4, 4, 4)) # top, right, bottom, left
56
+ user_msg_style = ParagraphStyle(name='UserMessage', parent=base_message_style, backColor=colors.whitesmoke, borderColor=colors.lightgrey)
57
+ ai_msg_style = ParagraphStyle(name='AIMessage', parent=base_message_style, backColor=colors.lightcyan, borderColor=colors.lightgrey)
58
+ tool_msg_style = ParagraphStyle(name='ToolMessage', parent=base_message_style, backColor=colors.lightgoldenrodyellow, textColor=colors.darkslategrey, borderColor=colors.darkgrey, fontName='Courier')
59
+ system_msg_style = ParagraphStyle(name='SystemMessage', parent=base_message_style, backColor=colors.beige, fontSize=9, textColor=colors.dimgrey, borderColor=colors.darkgrey, fontName='Helvetica-Oblique')
60
+
61
+ # --- Extract data safely from report_data dictionary ---
62
+ clinician_username = str(report_data.get("patient_name", "N/A Clinician")) # Actually the clinician's username
63
+ session_id_str = str(report_data.get("session_id", "N/A"))
64
+ session_title_str = str(report_data.get("session_title", "Untitled Consultation"))
65
+ session_start_time_obj = report_data.get("session_start_time") # This is a datetime object or None
66
+ patient_context_summary_str = str(report_data.get("patient_context_summary", "No specific patient context was provided."))
67
+
68
+ # Messages are expected to be List[MockChatMessage] from the caller
69
+ messages: List[MockChatMessage] = report_data.get("messages", [])
70
+
71
+
72
+ # 1. Logo and Document Header
73
+ app_logger.debug("PDF Generation: Adding logo and header.")
74
+ logo_path_str = get_logo_path()
75
+ if logo_path_str and Path(logo_path_str).exists():
76
+ try:
77
+ img = Image(logo_path_str, width=0.75*inch, height=0.75*inch, preserveAspectRatio=True)
78
+ img.hAlign = 'LEFT'
79
+ story.append(img)
80
+ story.append(Spacer(1, 0.05*inch))
81
+ except Exception as e: app_logger.warning(f"PDF Report: Could not add logo image: {e}")
82
+
83
+ story.append(Paragraph(str(settings.APP_TITLE), styles['h1']))
84
+ story.append(Paragraph("AI-Assisted Consultation Summary", styles['h2']))
85
+ story.append(Spacer(1, 0.2*inch))
86
+
87
+ # 2. Report Metadata Table
88
+ app_logger.debug("PDF Generation: Adding metadata table.")
89
+ report_date_str = datetime.now().strftime('%Y-%m-%d %H:%M UTC')
90
+ session_start_time_str_display = session_start_time_obj.strftime('%Y-%m-%d %H:%M UTC') if session_start_time_obj else "N/A"
91
+
92
+ meta_data_content = [
93
+ [Paragraph("<b>Report Generated:</b>", styles['Normal']), Paragraph(report_date_str, styles['Normal'])],
94
+ [Paragraph("<b>Clinician:</b>", styles['Normal']), Paragraph(clinician_username, styles['Normal'])],
95
+ [Paragraph("<b>Consultation Session ID:</b>", styles['Normal']), Paragraph(session_id_str, styles['Normal'])],
96
+ [Paragraph("<b>Session Title:</b>", styles['Normal']), Paragraph(session_title_str, styles['Normal'])],
97
+ [Paragraph("<b>Session Start Time:</b>", styles['Normal']), Paragraph(session_start_time_str_display, styles['Normal'])],
98
+ ]
99
+ meta_table = Table(meta_data_content, colWidths=[2.0*inch, None])
100
+ meta_table.setStyle(TableStyle([
101
+ ('GRID', (0,0), (-1,-1), 0.5, colors.darkgrey), ('VALIGN', (0,0), (-1,-1), 'TOP'),
102
+ ('BACKGROUND', (0,0), (0,-1), colors.lightgrey), ('LEFTPADDING', (0,0), (-1,-1), 5),
103
+ ('RIGHTPADDING', (0,0), (-1,-1), 5), ('TOPPADDING', (0,0), (-1,-1), 3), ('BOTTOMPADDING', (0,0), (-1,-1), 3)
104
+ ]))
105
+ story.append(meta_table)
106
+ story.append(Spacer(1, 0.3*inch))
107
+
108
+ # 3. Disclaimer
109
+ app_logger.debug("PDF Generation: Adding disclaimers.")
110
+ story.append(Paragraph("<b>Important Disclaimer:</b>", styles['SubHeader']))
111
+ story.append(Paragraph(str(settings.MAIN_DISCLAIMER_LONG), styles['Disclaimer']))
112
+ if settings.SIMULATION_DISCLAIMER:
113
+ story.append(Paragraph(str(settings.SIMULATION_DISCLAIMER), styles['Disclaimer']))
114
+ story.append(Spacer(1, 0.3*inch))
115
+
116
+ # 4. Patient Context Provided (if any)
117
+ app_logger.debug("PDF Generation: Adding patient context summary.")
118
+ if patient_context_summary_str and patient_context_summary_str.lower() not in ["no specific patient context was provided for this session.", "not provided."]:
119
+ story.append(Paragraph("Patient Context Provided by Clinician (Simulated Data):", styles['SectionHeader']))
120
+ # Split summary string into list items for better readability
121
+ context_items_raw = patient_context_summary_str.replace("Patient Context: ", "").replace("Initial Patient Context Set: ","").split(';')
122
+ for item_raw in context_items_raw:
123
+ item_clean = item_raw.strip()
124
+ if item_clean: story.append(Paragraph(f"• {item_clean}", styles['ListItem']))
125
+ story.append(Spacer(1, 0.2*inch))
126
+
127
+ # 5. Consultation Transcript
128
+ app_logger.debug(f"PDF Generation: Adding transcript with {len(messages)} messages.")
129
+ story.append(Paragraph("Consultation Transcript:", styles['SectionHeader']))
130
+ story.append(Spacer(1, 0.1*inch))
131
+
132
+ for msg_obj in messages: # msg_obj is now an instance of MockChatMessage
133
+ # Skip system messages that are just context logging, unless you want them
134
+ if msg_obj.role == 'system' and "Initial Patient Context Set:" in msg_obj.content:
135
+ app_logger.debug(f"Skipping system context message in PDF transcript: {msg_obj.content[:50]}...")
136
+ continue
137
+
138
+ formatted_timestamp = msg_obj.get_formatted_timestamp()
139
+ prefix_display = ""
140
+ active_message_style = styles['Normal']
141
+
142
+ if msg_obj.role == 'assistant':
143
+ prefix_display = f"AI Assistant ({formatted_timestamp}):"
144
+ active_message_style = ai_msg_style
145
+ elif msg_obj.role == 'user':
146
+ prefix_display = f"Clinician ({formatted_timestamp}):"
147
+ active_message_style = user_msg_style
148
+ elif msg_obj.role == 'tool':
149
+ tool_name_str = msg_obj.tool_name or "Tool"
150
+ prefix_display = f"{tool_name_str.capitalize()} Output ({formatted_timestamp}):"
151
+ active_message_style = tool_msg_style
152
+ elif msg_obj.role == 'system':
153
+ prefix_display = f"System Note ({formatted_timestamp}):"
154
+ active_message_style = system_msg_style
155
+ else: # Fallback for any other roles
156
+ prefix_display = f"{msg_obj.role.capitalize()} ({formatted_timestamp}):"
157
+
158
+ formatted_content = msg_obj.get_formatted_content_for_pdf()
159
+
160
+ story.append(Paragraph(f"<b>{prefix_display}</b>", styles['Normal']))
161
+ story.append(Paragraph(formatted_content, active_message_style))
162
+ # story.append(Spacer(1, 0.05*inch)) # Can make it too sparse
163
+
164
+ story.append(Spacer(1, 0.5*inch))
165
+ story.append(Paragraph("--- End of Report ---", ParagraphStyle(name='EndOfReport', parent=styles['Italic'], alignment=1, spaceBefore=12)))
166
+
167
+ # --- Build PDF ---
168
+ app_logger.debug("PDF Generation: Building document.")
169
+ try:
170
+ doc.build(story)
171
+ buffer.seek(0)
172
+ app_logger.info(f"PDF report generated successfully for session ID: {session_id_str}")
173
+ except Exception as e_build:
174
+ app_logger.error(f"Failed to build PDF document for session ID {session_id_str}: {e_build}", exc_info=True)
175
+ # Return an error PDF or an empty buffer
176
+ buffer = BytesIO()
177
+ error_styles_local = getSampleStyleSheet()
178
+ error_doc_local = SimpleDocTemplate(buffer, pagesize=letter)
179
+ error_story_local = [
180
+ Paragraph("Error: Could not generate PDF report.", error_styles_local['h1']),
181
+ Paragraph(f"An error occurred during PDF construction: {str(e_build)[:500]}", error_styles_local['Normal']),
182
+ Paragraph("Please check application logs for more details.", error_styles_local['Normal'])
183
+ ]
184
+ try:
185
+ error_doc_local.build(error_story_local)
186
+ except Exception as e_err_pdf:
187
+ app_logger.error(f"Failed to build even the error PDF: {e_err_pdf}", exc_info=True)
188
+ # If error PDF fails, buffer will be empty from the reset
189
+ buffer.seek(0)
190
+
191
+ return buffer
192
+ from assets.logo import get_logo_path # Assuming this function exists and works
193
+ from services.logger import app_logger
194
+
195
  class MockChatMessage:
196
  """
197
  A simple class to hold message data for PDF generation if generate_pdf_report