SmartHeal-Agentic-AI / src /patient_history.py
SmartHeal's picture
Upload 33 files
185c377 verified
raw
history blame
22.4 kB
import logging
import json
from datetime import datetime
from typing import List, Dict, Optional
class PatientHistoryManager:
"""Complete patient history and wound tracking system"""
def __init__(self, database_manager):
self.db = database_manager
def get_patient_complete_history(self, user_id: int, patient_name: str = None) -> List[Dict]:
"""Get complete patient history with all wound analyses and images"""
try:
if patient_name:
query = """
SELECT
qr.id as response_id,
p.name as patient_name,
p.age as patient_age,
p.gender as patient_gender,
qr.response_data,
qr.submitted_at as visit_date,
w.position as wound_location,
w.moisture,
w.infection,
w.notes,
wi.image as image_url,
a.analysis_data,
a.summary,
a.recommendations,
a.risk_score,
a.risk_level
FROM questionnaire_responses qr
JOIN patients p ON qr.patient_id = p.id
LEFT JOIN wounds w ON w.patient_id = CAST(p.id AS CHAR)
LEFT JOIN wound_images wi ON wi.patient_id = CAST(p.id AS CHAR)
LEFT JOIN ai_analyses a ON a.questionnaire_id = qr.id
WHERE qr.practitioner_id = %s AND p.name = %s
ORDER BY qr.submitted_at DESC
"""
params = (user_id, patient_name)
else:
query = """
SELECT
qr.id as response_id,
p.name as patient_name,
p.age as patient_age,
p.gender as patient_gender,
qr.response_data,
qr.submitted_at as visit_date,
w.position as wound_location,
w.moisture,
w.infection,
w.notes,
wi.image as image_url,
a.analysis_data,
a.summary,
a.recommendations,
a.risk_score,
a.risk_level
FROM questionnaire_responses qr
JOIN patients p ON qr.patient_id = p.id
LEFT JOIN wounds w ON w.patient_id = CAST(p.id AS CHAR)
LEFT JOIN wound_images wi ON wi.patient_id = CAST(p.id AS CHAR)
LEFT JOIN ai_analyses a ON a.questionnaire_id = qr.id
WHERE qr.practitioner_id = %s
ORDER BY qr.submitted_at DESC
"""
params = (user_id,)
result = self.db.execute_query(query, params, fetch=True)
return result or []
except Exception as e:
logging.error(f"Error fetching patient history: {e}")
return []
def get_patient_list(self, user_id: int) -> List[Dict]:
"""Get list of all patients for this practitioner"""
try:
query = """
SELECT DISTINCT
p.name as patient_name,
p.age as patient_age,
p.gender as patient_gender,
COUNT(qr.id) as total_visits,
MAX(qr.submitted_at) as last_visit,
MIN(qr.submitted_at) as first_visit
FROM questionnaire_responses qr
JOIN patients p ON qr.patient_id = p.id
WHERE qr.practitioner_id = %s
GROUP BY p.name, p.age, p.gender
ORDER BY last_visit DESC
"""
result = self.db.execute_query(query, (user_id,), fetch=True)
return result or []
except Exception as e:
logging.error(f"Error fetching patient list: {e}")
return []
def get_wound_progression(self, user_id: int, patient_name: str) -> List[Dict]:
"""Get wound progression data for timeline visualization"""
try:
query = """
SELECT
qr.submitted_at as visit_date,
qr.response_data,
w.position as wound_location,
w.moisture,
w.infection,
a.risk_score,
a.risk_level,
a.summary,
wi.image as image_url
FROM questionnaire_responses qr
JOIN patients p ON qr.patient_id = p.id
LEFT JOIN wounds w ON w.patient_id = CAST(p.id AS CHAR)
LEFT JOIN wound_images wi ON wi.patient_id = CAST(p.id AS CHAR)
LEFT JOIN ai_analyses a ON a.questionnaire_id = qr.id
WHERE qr.practitioner_id = %s AND p.name = %s
ORDER BY qr.submitted_at ASC
"""
result = self.db.execute_query(query, (user_id, patient_name), fetch=True)
return result or []
except Exception as e:
logging.error(f"Error fetching wound progression: {e}")
return []
def save_patient_note(self, user_id: int, patient_name: str, note: str) -> bool:
"""Save a clinical note for a patient"""
try:
# Check if notes table exists, if not use questionnaires additional_notes
query = """
UPDATE questionnaires
SET additional_notes = CONCAT(IFNULL(additional_notes, ''), '\n--- Clinical Note (', NOW(), ') ---\n', %s)
WHERE user_id = %s AND patient_name = %s
ORDER BY created_at DESC
LIMIT 1
"""
result = self.db.execute_query(query, (note, user_id, patient_name))
return bool(result)
except Exception as e:
logging.error(f"Error saving patient note: {e}")
return False
class ReportGenerator:
"""Professional HTML report generator for wound analysis"""
def __init__(self):
pass
def generate_analysis_report(self, patient_data: Dict, analysis_data: Dict, image_url: str = None) -> str:
"""Generate comprehensive HTML report for wound analysis"""
report_html = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SmartHeal AI - Wound Analysis Report</title>
<style>
body {{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
background-color: #f8f9fa;
}}
.report-container {{
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
overflow: hidden;
}}
.header {{
background: linear-gradient(135deg, #3182ce 0%, #2c5aa0 100%);
color: white;
padding: 30px;
text-align: center;
}}
.header h1 {{
margin: 0;
font-size: 28px;
font-weight: 600;
}}
.header p {{
margin: 10px 0 0 0;
opacity: 0.9;
font-size: 16px;
}}
.content {{
padding: 30px;
}}
.section {{
margin-bottom: 30px;
border-left: 4px solid #3182ce;
padding-left: 20px;
}}
.section h2 {{
color: #2c5aa0;
margin-top: 0;
font-size: 20px;
font-weight: 600;
}}
.patient-info {{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}}
.info-card {{
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
border: 1px solid #e9ecef;
}}
.info-card h3 {{
margin: 0 0 10px 0;
color: #495057;
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
}}
.info-card p {{
margin: 0;
font-weight: 500;
color: #212529;
}}
.risk-indicator {{
display: inline-block;
padding: 8px 16px;
border-radius: 20px;
font-weight: 600;
text-transform: uppercase;
font-size: 12px;
letter-spacing: 0.5px;
}}
.risk-low {{ background: #d4edda; color: #155724; }}
.risk-moderate {{ background: #fff3cd; color: #856404; }}
.risk-high {{ background: #f8d7da; color: #721c24; }}
.recommendations {{
background: #e7f3ff;
border: 1px solid #b3d9ff;
border-radius: 8px;
padding: 20px;
margin-top: 15px;
}}
.recommendations ul {{
margin: 10px 0;
padding-left: 20px;
}}
.recommendations li {{
margin-bottom: 8px;
color: #0056b3;
}}
.image-section {{
text-align: center;
margin: 20px 0;
}}
.wound-image {{
max-width: 100%;
height: auto;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}}
.footer {{
background: #f8f9fa;
padding: 20px 30px;
text-align: center;
color: #6c757d;
border-top: 1px solid #e9ecef;
}}
.connect-clinician {{
background: #28a745;
color: white;
padding: 12px 30px;
border: none;
border-radius: 25px;
font-weight: 600;
cursor: pointer;
margin: 20px 0;
font-size: 16px;
}}
.connect-clinician:hover {{
background: #218838;
}}
@media print {{
body {{ background: white; }}
.report-container {{ box-shadow: none; }}
.connect-clinician {{ display: none; }}
}}
</style>
</head>
<body>
<div class="report-container">
<div class="header">
<h1>🩺 SmartHeal AI Wound Analysis Report</h1>
<p>Advanced AI-Powered Clinical Assessment</p>
</div>
<div class="content">
<div class="section">
<h2>Patient Information</h2>
<div class="patient-info">
<div class="info-card">
<h3>Patient Name</h3>
<p>{patient_data.get('patient_name', 'N/A')}</p>
</div>
<div class="info-card">
<h3>Age</h3>
<p>{patient_data.get('patient_age', 'N/A')} years</p>
</div>
<div class="info-card">
<h3>Gender</h3>
<p>{patient_data.get('patient_gender', 'N/A')}</p>
</div>
<div class="info-card">
<h3>Assessment Date</h3>
<p>{datetime.now().strftime('%B %d, %Y at %I:%M %p')}</p>
</div>
</div>
</div>
<div class="section">
<h2>Wound Assessment</h2>
<div class="patient-info">
<div class="info-card">
<h3>Location</h3>
<p>{patient_data.get('wound_location', 'N/A')}</p>
</div>
<div class="info-card">
<h3>Duration</h3>
<p>{patient_data.get('wound_duration', 'N/A')}</p>
</div>
<div class="info-card">
<h3>Pain Level</h3>
<p>{patient_data.get('pain_level', 'N/A')}/10</p>
</div>
<div class="info-card">
<h3>Risk Assessment</h3>
<p>
<span class="risk-indicator risk-{analysis_data.get('risk_level', 'unknown').lower()}">
{analysis_data.get('risk_level', 'Unknown')} Risk
</span>
</p>
</div>
</div>
</div>
{f'''
<div class="section">
<h2>Wound Image</h2>
<div class="image-section">
<img src="{image_url}" alt="Wound Image" class="wound-image">
</div>
</div>
''' if image_url else ''}
<div class="section">
<h2>AI Analysis Summary</h2>
<p>{analysis_data.get('summary', 'No analysis summary available.')}</p>
<div class="recommendations">
<h3>🎯 Clinical Recommendations</h3>
{self._format_recommendations(analysis_data.get('recommendations', ''))}
</div>
</div>
<div class="section">
<h2>Medical History</h2>
<div class="patient-info">
<div class="info-card">
<h3>Medical History</h3>
<p>{patient_data.get('medical_history', 'None reported')}</p>
</div>
<div class="info-card">
<h3>Current Medications</h3>
<p>{patient_data.get('medications', 'None reported')}</p>
</div>
<div class="info-card">
<h3>Known Allergies</h3>
<p>{patient_data.get('allergies', 'None reported')}</p>
</div>
<div class="info-card">
<h3>Additional Notes</h3>
<p>{patient_data.get('additional_notes', 'None')}</p>
</div>
</div>
</div>
<div style="text-align: center;">
<button class="connect-clinician" onclick="connectToClinician()">
📞 Connect to Clinician
</button>
</div>
</div>
<div class="footer">
<p><strong>SmartHeal AI</strong> - Advanced Wound Care Analysis & Clinical Support System</p>
<p>Generated on {datetime.now().strftime('%B %d, %Y at %I:%M %p')} | For professional medical use only</p>
<p>⚠️ This AI analysis is for clinical decision support only. Always consult with healthcare professionals for diagnosis and treatment.</p>
</div>
</div>
<script>
function connectToClinician() {{
alert('🩺 Connecting to clinician...\n\nThis feature would connect you to a healthcare professional for consultation.\n\nIn a production environment, this would:\n• Initiate video call\n• Send analysis to specialist\n• Schedule consultation\n• Provide emergency contact');
}}
</script>
</body>
</html>
"""
return report_html
def _format_recommendations(self, recommendations: str) -> str:
"""Format recommendations as HTML list"""
if not recommendations:
return "<p>No specific recommendations available.</p>"
# Split recommendations by common delimiters
items = []
for delimiter in ['\n', '. ', '; ']:
if delimiter in recommendations:
items = [item.strip() for item in recommendations.split(delimiter) if item.strip()]
break
if not items:
items = [recommendations]
html = "<ul>"
for item in items:
if item and len(item) > 3: # Avoid very short fragments
html += f"<li>{item}</li>"
html += "</ul>"
return html
def generate_patient_history_report(self, patient_history: List[Dict]) -> str:
"""Generate comprehensive patient history report"""
if not patient_history:
return "<p>No patient history available.</p>"
patient_name = patient_history[0].get('patient_name', 'Unknown Patient')
html = f"""
<div style="max-width: 800px; margin: 0 auto; font-family: 'Segoe UI', sans-serif;">
<div style="background: linear-gradient(135deg, #3182ce 0%, #2c5aa0 100%); color: white; padding: 20px; border-radius: 10px 10px 0 0;">
<h2 style="margin: 0;">📋 Patient History: {patient_name}</h2>
<p style="margin: 10px 0 0 0; opacity: 0.9;">Complete Treatment Timeline</p>
</div>
<div style="background: white; border: 1px solid #e9ecef; border-top: none; border-radius: 0 0 10px 10px;">
"""
for i, visit in enumerate(patient_history):
visit_date = visit.get('visit_date', '')
if isinstance(visit_date, str):
try:
visit_date = datetime.fromisoformat(visit_date.replace('Z', '+00:00'))
except:
pass
risk_class = f"risk-{visit.get('risk_level', 'unknown').lower()}"
html += f"""
<div style="padding: 20px; border-bottom: 1px solid #f0f0f0;">
<div style="display: flex; justify-content: between; align-items: center; margin-bottom: 15px;">
<h3 style="color: #2c5aa0; margin: 0;">Visit #{len(patient_history) - i}</h3>
<span style="color: #6c757d; font-size: 14px;">{visit_date.strftime('%B %d, %Y') if hasattr(visit_date, 'strftime') else str(visit_date)}</span>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; margin-bottom: 15px;">
<div style="background: #f8f9fa; padding: 10px; border-radius: 5px;">
<strong>Location:</strong> {visit.get('wound_location', 'N/A')}
</div>
<div style="background: #f8f9fa; padding: 10px; border-radius: 5px;">
<strong>Pain Level:</strong> {visit.get('pain_level', 'N/A')}/10
</div>
<div style="background: #f8f9fa; padding: 10px; border-radius: 5px;">
<strong>Risk:</strong>
<span class="risk-indicator {risk_class}" style="background: {'#d4edda' if 'low' in risk_class else '#fff3cd' if 'moderate' in risk_class else '#f8d7da'}; color: {'#155724' if 'low' in risk_class else '#856404' if 'moderate' in risk_class else '#721c24'}; padding: 2px 8px; border-radius: 10px; font-size: 12px;">
{visit.get('risk_level', 'Unknown')}
</span>
</div>
</div>
{f'<p><strong>Summary:</strong> {visit.get("summary", "No summary available")}</p>' if visit.get("summary") else ""}
{f'<p><strong>Recommendations:</strong> {visit.get("recommendations", "No recommendations available")}</p>' if visit.get("recommendations") else ""}
</div>
"""
html += """
</div>
</div>
"""
return html