add verifier
Browse files- app.py +3 -4
- requirements.txt +1 -0
- templates/oneclick.html +10 -3
- utils/oneclick.py +28 -18
- utils/verifier.py +70 -0
app.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
# app.py
|
| 2 |
-
|
| 3 |
from flask import Flask, render_template, request, send_file, redirect, url_for
|
| 4 |
import os
|
| 5 |
import logging
|
|
@@ -7,7 +6,6 @@ from utils.meldrx import MeldRxAPI
|
|
| 7 |
from utils.oneclick import generate_discharge_paper_one_click
|
| 8 |
from huggingface_hub import InferenceClient
|
| 9 |
|
| 10 |
-
|
| 11 |
logging.basicConfig(level=logging.DEBUG)
|
| 12 |
logger = logging.getLogger(__name__)
|
| 13 |
|
|
@@ -30,7 +28,6 @@ HF_TOKEN = os.getenv("HF_TOKEN")
|
|
| 30 |
if not HF_TOKEN:
|
| 31 |
raise ValueError("HF_TOKEN environment variable not set.")
|
| 32 |
client = InferenceClient(api_key=HF_TOKEN)
|
| 33 |
-
MODEL_NAME = "meta-llama/Llama-3.3-70B-Instruct"
|
| 34 |
|
| 35 |
@app.route('/')
|
| 36 |
def index():
|
|
@@ -77,7 +74,7 @@ def one_click():
|
|
| 77 |
|
| 78 |
logger.info(f"One-click request - ID: {patient_id}, First: {first_name}, Last: {last_name}, Action: {action}")
|
| 79 |
|
| 80 |
-
pdf_path, status, basic_summary, ai_summary = generate_discharge_paper_one_click(
|
| 81 |
meldrx_api, client, patient_id, first_name, last_name
|
| 82 |
)
|
| 83 |
|
|
@@ -86,6 +83,7 @@ def one_click():
|
|
| 86 |
status=status,
|
| 87 |
basic_summary=basic_summary.replace('\n', '<br>') if basic_summary else None,
|
| 88 |
ai_summary=ai_summary.replace('\n', '<br>') if ai_summary else None,
|
|
|
|
| 89 |
patient_id=patient_id,
|
| 90 |
first_name=first_name,
|
| 91 |
last_name=last_name)
|
|
@@ -97,6 +95,7 @@ def one_click():
|
|
| 97 |
status=status,
|
| 98 |
basic_summary=basic_summary.replace('\n', '<br>') if basic_summary else None,
|
| 99 |
ai_summary=ai_summary.replace('\n', '<br>') if ai_summary else None,
|
|
|
|
| 100 |
patient_id=patient_id,
|
| 101 |
first_name=first_name,
|
| 102 |
last_name=last_name)
|
|
|
|
| 1 |
# app.py
|
|
|
|
| 2 |
from flask import Flask, render_template, request, send_file, redirect, url_for
|
| 3 |
import os
|
| 4 |
import logging
|
|
|
|
| 6 |
from utils.oneclick import generate_discharge_paper_one_click
|
| 7 |
from huggingface_hub import InferenceClient
|
| 8 |
|
|
|
|
| 9 |
logging.basicConfig(level=logging.DEBUG)
|
| 10 |
logger = logging.getLogger(__name__)
|
| 11 |
|
|
|
|
| 28 |
if not HF_TOKEN:
|
| 29 |
raise ValueError("HF_TOKEN environment variable not set.")
|
| 30 |
client = InferenceClient(api_key=HF_TOKEN)
|
|
|
|
| 31 |
|
| 32 |
@app.route('/')
|
| 33 |
def index():
|
|
|
|
| 74 |
|
| 75 |
logger.info(f"One-click request - ID: {patient_id}, First: {first_name}, Last: {last_name}, Action: {action}")
|
| 76 |
|
| 77 |
+
pdf_path, status, basic_summary, ai_summary, verified_summary = generate_discharge_paper_one_click(
|
| 78 |
meldrx_api, client, patient_id, first_name, last_name
|
| 79 |
)
|
| 80 |
|
|
|
|
| 83 |
status=status,
|
| 84 |
basic_summary=basic_summary.replace('\n', '<br>') if basic_summary else None,
|
| 85 |
ai_summary=ai_summary.replace('\n', '<br>') if ai_summary else None,
|
| 86 |
+
verified_summary=verified_summary if verified_summary else None,
|
| 87 |
patient_id=patient_id,
|
| 88 |
first_name=first_name,
|
| 89 |
last_name=last_name)
|
|
|
|
| 95 |
status=status,
|
| 96 |
basic_summary=basic_summary.replace('\n', '<br>') if basic_summary else None,
|
| 97 |
ai_summary=ai_summary.replace('\n', '<br>') if ai_summary else None,
|
| 98 |
+
verified_summary=verified_summary if verified_summary else None,
|
| 99 |
patient_id=patient_id,
|
| 100 |
first_name=first_name,
|
| 101 |
last_name=last_name)
|
requirements.txt
CHANGED
|
@@ -13,3 +13,4 @@ gradio
|
|
| 13 |
huggingface_hub
|
| 14 |
lxml
|
| 15 |
reportlab
|
|
|
|
|
|
| 13 |
huggingface_hub
|
| 14 |
lxml
|
| 15 |
reportlab
|
| 16 |
+
lettucedetect
|
templates/oneclick.html
CHANGED
|
@@ -2,9 +2,9 @@
|
|
| 2 |
{% block content %}
|
| 3 |
<h2>One-Click Discharge Summary</h2>
|
| 4 |
<form method="POST">
|
| 5 |
-
<input type="text" name="patient_id" placeholder="Patient ID (Optional)">
|
| 6 |
-
<input type="text" name="first_name" placeholder="First Name (Optional)">
|
| 7 |
-
<input type="text" name="last_name" placeholder="Last Name (Optional)"><br><br>
|
| 8 |
<input type="submit" name="action" value="Display Summary" class="cyberpunk-button">
|
| 9 |
<input type="submit" name="action" value="Generate PDF" class="cyberpunk-button">
|
| 10 |
</form>
|
|
@@ -27,6 +27,13 @@
|
|
| 27 |
</div>
|
| 28 |
{% endif %}
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
<style>
|
| 31 |
.status-message {
|
| 32 |
margin: 20px 0;
|
|
|
|
| 2 |
{% block content %}
|
| 3 |
<h2>One-Click Discharge Summary</h2>
|
| 4 |
<form method="POST">
|
| 5 |
+
<input type="text" name="patient_id" placeholder="Patient ID (Optional)" value="{{ patient_id or '' }}">
|
| 6 |
+
<input type="text" name="first_name" placeholder="First Name (Optional)" value="{{ first_name or '' }}">
|
| 7 |
+
<input type="text" name="last_name" placeholder="Last Name (Optional)" value="{{ last_name or '' }}"><br><br>
|
| 8 |
<input type="submit" name="action" value="Display Summary" class="cyberpunk-button">
|
| 9 |
<input type="submit" name="action" value="Generate PDF" class="cyberpunk-button">
|
| 10 |
</form>
|
|
|
|
| 27 |
</div>
|
| 28 |
{% endif %}
|
| 29 |
|
| 30 |
+
{% if verified_summary %}
|
| 31 |
+
<div class="summary-container">
|
| 32 |
+
<h3>Verified AI Discharge Summary (Hallucinations Highlighted)</h3>
|
| 33 |
+
<div class="summary-content">{{ verified_summary | safe }}</div>
|
| 34 |
+
</div>
|
| 35 |
+
{% endif %}
|
| 36 |
+
|
| 37 |
<style>
|
| 38 |
.status-message {
|
| 39 |
margin: 20px 0;
|
utils/oneclick.py
CHANGED
|
@@ -3,10 +3,11 @@ from typing import Tuple, Optional, Dict
|
|
| 3 |
from .meldrx import MeldRxAPI
|
| 4 |
from .responseparser import PatientDataExtractor
|
| 5 |
from .pdfutils import PDFGenerator
|
|
|
|
| 6 |
import logging
|
| 7 |
import json
|
| 8 |
from huggingface_hub import InferenceClient
|
| 9 |
-
import os
|
| 10 |
|
| 11 |
logger = logging.getLogger(__name__)
|
| 12 |
|
|
@@ -15,9 +16,10 @@ if not HF_TOKEN:
|
|
| 15 |
raise ValueError("HF_TOKEN environment variable not set.")
|
| 16 |
client = InferenceClient(api_key=HF_TOKEN)
|
| 17 |
MODEL_NAME = "meta-llama/Llama-3.3-70B-Instruct"
|
|
|
|
| 18 |
|
| 19 |
-
def generate_ai_discharge_summary(patient_dict: Dict[str, str], client) -> Optional[str]:
|
| 20 |
-
"""Generate a discharge summary using AI
|
| 21 |
try:
|
| 22 |
formatted_summary = format_discharge_summary(patient_dict)
|
| 23 |
|
|
@@ -49,12 +51,22 @@ def generate_ai_discharge_summary(patient_dict: Dict[str, str], client) -> Optio
|
|
| 49 |
if content:
|
| 50 |
discharge_summary += content
|
| 51 |
|
|
|
|
| 52 |
logger.info("AI discharge summary generated successfully")
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
except Exception as e:
|
| 56 |
logger.error(f"Error generating AI discharge summary: {str(e)}", exc_info=True)
|
| 57 |
-
return None
|
| 58 |
|
| 59 |
def generate_discharge_paper_one_click(
|
| 60 |
api: MeldRxAPI,
|
|
@@ -62,12 +74,12 @@ def generate_discharge_paper_one_click(
|
|
| 62 |
patient_id: str = "",
|
| 63 |
first_name: str = "",
|
| 64 |
last_name: str = ""
|
| 65 |
-
) -> Tuple[Optional[str], str, Optional[str], Optional[str]]:
|
| 66 |
try:
|
| 67 |
patients_data = api.get_patients()
|
| 68 |
if not patients_data or "entry" not in patients_data:
|
| 69 |
logger.error("No patient data received from MeldRx API")
|
| 70 |
-
return None, "Failed to fetch patient data from MeldRx API", None, None
|
| 71 |
|
| 72 |
logger.debug(f"Raw patient data from API: {patients_data}")
|
| 73 |
|
|
@@ -75,7 +87,7 @@ def generate_discharge_paper_one_click(
|
|
| 75 |
|
| 76 |
if not extractor.patients:
|
| 77 |
logger.error("No patients found in the parsed data")
|
| 78 |
-
return None, "No patients found in the data", None, None
|
| 79 |
|
| 80 |
logger.info(f"Found {len(extractor.patients)} patients in the data")
|
| 81 |
|
|
@@ -102,10 +114,8 @@ def generate_discharge_paper_one_click(
|
|
| 102 |
logger.debug(f"Comparing - Input: ID={patient_id_input}, First={first_name_input}, Last={last_name_input}")
|
| 103 |
|
| 104 |
matches = True
|
| 105 |
-
# Only enforce ID match if both input and data have non-empty IDs
|
| 106 |
if patient_id_input and patient_id_from_data and patient_id_input != patient_id_from_data:
|
| 107 |
matches = False
|
| 108 |
-
# Use exact match for names if provided, ignoring case
|
| 109 |
if first_name_input and first_name_input != first_name_from_data:
|
| 110 |
matches = False
|
| 111 |
if last_name_input and last_name_input != last_name_from_data:
|
|
@@ -123,28 +133,28 @@ def generate_discharge_paper_one_click(
|
|
| 123 |
logger.info(f"Available patient names: {all_patient_names}")
|
| 124 |
return None, (f"No patients found matching criteria: {search_criteria}\n"
|
| 125 |
f"Available IDs: {', '.join(all_patient_ids)}\n"
|
| 126 |
-
f"Available Names: {', '.join(all_patient_names)}"), None, None
|
| 127 |
-
|
| 128 |
patient_data = matching_patients[0]
|
| 129 |
logger.info(f"Selected patient data: {patient_data}")
|
| 130 |
|
| 131 |
basic_summary = format_discharge_summary(patient_data)
|
| 132 |
-
ai_summary = generate_ai_discharge_summary(patient_data, client)
|
| 133 |
|
| 134 |
-
if not ai_summary:
|
| 135 |
-
return None, "Failed to generate AI summary", basic_summary, None
|
| 136 |
|
| 137 |
pdf_gen = PDFGenerator()
|
| 138 |
filename = f"discharge_{patient_data.get('id', 'unknown')}_{patient_data.get('last_name', 'patient')}.pdf"
|
| 139 |
pdf_path = pdf_gen.generate_pdf_from_text(ai_summary, filename)
|
| 140 |
|
| 141 |
if pdf_path:
|
| 142 |
-
return pdf_path, "Discharge summary generated successfully", basic_summary, ai_summary
|
| 143 |
-
return None, "Failed to generate PDF file", basic_summary, ai_summary
|
| 144 |
|
| 145 |
except Exception as e:
|
| 146 |
logger.error(f"Error in one-click discharge generation: {str(e)}", exc_info=True)
|
| 147 |
-
return None, f"Error generating discharge summary: {str(e)}", None, None
|
| 148 |
|
| 149 |
def format_discharge_summary(patient_data: dict) -> str:
|
| 150 |
"""Format patient data into a discharge summary text."""
|
|
|
|
| 3 |
from .meldrx import MeldRxAPI
|
| 4 |
from .responseparser import PatientDataExtractor
|
| 5 |
from .pdfutils import PDFGenerator
|
| 6 |
+
from .verifier import DischargeVerifier # Import the verifier
|
| 7 |
import logging
|
| 8 |
import json
|
| 9 |
from huggingface_hub import InferenceClient
|
| 10 |
+
import os
|
| 11 |
|
| 12 |
logger = logging.getLogger(__name__)
|
| 13 |
|
|
|
|
| 16 |
raise ValueError("HF_TOKEN environment variable not set.")
|
| 17 |
client = InferenceClient(api_key=HF_TOKEN)
|
| 18 |
MODEL_NAME = "meta-llama/Llama-3.3-70B-Instruct"
|
| 19 |
+
verifier = DischargeVerifier() # Initialize the verifier
|
| 20 |
|
| 21 |
+
def generate_ai_discharge_summary(patient_dict: Dict[str, str], client) -> Tuple[Optional[str], Optional[str]]:
|
| 22 |
+
"""Generate a discharge summary using AI and verify it for hallucinations."""
|
| 23 |
try:
|
| 24 |
formatted_summary = format_discharge_summary(patient_dict)
|
| 25 |
|
|
|
|
| 51 |
if content:
|
| 52 |
discharge_summary += content
|
| 53 |
|
| 54 |
+
discharge_summary = discharge_summary.strip()
|
| 55 |
logger.info("AI discharge summary generated successfully")
|
| 56 |
+
|
| 57 |
+
# Verify the summary for hallucinations
|
| 58 |
+
question = "Provide a complete discharge summary based on the patient information."
|
| 59 |
+
verified_summary = verifier.verify_discharge_summary(
|
| 60 |
+
context=formatted_summary,
|
| 61 |
+
question=question,
|
| 62 |
+
answer=discharge_summary
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
return discharge_summary, verified_summary
|
| 66 |
|
| 67 |
except Exception as e:
|
| 68 |
logger.error(f"Error generating AI discharge summary: {str(e)}", exc_info=True)
|
| 69 |
+
return None, None
|
| 70 |
|
| 71 |
def generate_discharge_paper_one_click(
|
| 72 |
api: MeldRxAPI,
|
|
|
|
| 74 |
patient_id: str = "",
|
| 75 |
first_name: str = "",
|
| 76 |
last_name: str = ""
|
| 77 |
+
) -> Tuple[Optional[str], str, Optional[str], Optional[str], Optional[str]]:
|
| 78 |
try:
|
| 79 |
patients_data = api.get_patients()
|
| 80 |
if not patients_data or "entry" not in patients_data:
|
| 81 |
logger.error("No patient data received from MeldRx API")
|
| 82 |
+
return None, "Failed to fetch patient data from MeldRx API", None, None, None
|
| 83 |
|
| 84 |
logger.debug(f"Raw patient data from API: {patients_data}")
|
| 85 |
|
|
|
|
| 87 |
|
| 88 |
if not extractor.patients:
|
| 89 |
logger.error("No patients found in the parsed data")
|
| 90 |
+
return None, "No patients found in the data", None, None, None
|
| 91 |
|
| 92 |
logger.info(f"Found {len(extractor.patients)} patients in the data")
|
| 93 |
|
|
|
|
| 114 |
logger.debug(f"Comparing - Input: ID={patient_id_input}, First={first_name_input}, Last={last_name_input}")
|
| 115 |
|
| 116 |
matches = True
|
|
|
|
| 117 |
if patient_id_input and patient_id_from_data and patient_id_input != patient_id_from_data:
|
| 118 |
matches = False
|
|
|
|
| 119 |
if first_name_input and first_name_input != first_name_from_data:
|
| 120 |
matches = False
|
| 121 |
if last_name_input and last_name_input != last_name_from_data:
|
|
|
|
| 133 |
logger.info(f"Available patient names: {all_patient_names}")
|
| 134 |
return None, (f"No patients found matching criteria: {search_criteria}\n"
|
| 135 |
f"Available IDs: {', '.join(all_patient_ids)}\n"
|
| 136 |
+
f"Available Names: {', '.join(all_patient_names)}"), None, None, None
|
| 137 |
+
|
| 138 |
patient_data = matching_patients[0]
|
| 139 |
logger.info(f"Selected patient data: {patient_data}")
|
| 140 |
|
| 141 |
basic_summary = format_discharge_summary(patient_data)
|
| 142 |
+
ai_summary, verified_summary = generate_ai_discharge_summary(patient_data, client)
|
| 143 |
|
| 144 |
+
if not ai_summary or not verified_summary:
|
| 145 |
+
return None, "Failed to generate or verify AI summary", basic_summary, None, None
|
| 146 |
|
| 147 |
pdf_gen = PDFGenerator()
|
| 148 |
filename = f"discharge_{patient_data.get('id', 'unknown')}_{patient_data.get('last_name', 'patient')}.pdf"
|
| 149 |
pdf_path = pdf_gen.generate_pdf_from_text(ai_summary, filename)
|
| 150 |
|
| 151 |
if pdf_path:
|
| 152 |
+
return pdf_path, "Discharge summary generated and verified successfully", basic_summary, ai_summary, verified_summary
|
| 153 |
+
return None, "Failed to generate PDF file", basic_summary, ai_summary, verified_summary
|
| 154 |
|
| 155 |
except Exception as e:
|
| 156 |
logger.error(f"Error in one-click discharge generation: {str(e)}", exc_info=True)
|
| 157 |
+
return None, f"Error generating discharge summary: {str(e)}", None, None, None
|
| 158 |
|
| 159 |
def format_discharge_summary(patient_data: dict) -> str:
|
| 160 |
"""Format patient data into a discharge summary text."""
|
utils/verifier.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# utils/verifier.py
|
| 2 |
+
from lettucedetect.models.inference import HallucinationDetector
|
| 3 |
+
import logging
|
| 4 |
+
from typing import List, Dict, Optional
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
class DischargeVerifier:
|
| 9 |
+
def __init__(self):
|
| 10 |
+
"""Initialize the hallucination detector."""
|
| 11 |
+
try:
|
| 12 |
+
self.detector = HallucinationDetector(
|
| 13 |
+
method="transformer",
|
| 14 |
+
model_path="KRLabsOrg/lettucedect-base-modernbert-en-v1",
|
| 15 |
+
)
|
| 16 |
+
logger.info("Hallucination detector initialized successfully")
|
| 17 |
+
except Exception as e:
|
| 18 |
+
logger.error(f"Failed to initialize hallucination detector: {str(e)}")
|
| 19 |
+
raise
|
| 20 |
+
|
| 21 |
+
def create_interactive_text(self, text: str, spans: List[Dict[str, int | float]]) -> str:
|
| 22 |
+
"""Create interactive HTML with highlighting and hover effects."""
|
| 23 |
+
html_text = text
|
| 24 |
+
|
| 25 |
+
for span in sorted(spans, key=lambda x: x["start"], reverse=True):
|
| 26 |
+
span_text = text[span["start"]:span["end"]]
|
| 27 |
+
highlighted_span = (
|
| 28 |
+
f'<span class="hallucination" title="Confidence: {span["confidence"]:.3f}">{span_text}</span>'
|
| 29 |
+
)
|
| 30 |
+
html_text = (
|
| 31 |
+
html_text[:span["start"]] + highlighted_span + html_text[span["end"]:]
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
return f"""
|
| 35 |
+
<style>
|
| 36 |
+
.container {{
|
| 37 |
+
font-family: Arial, sans-serif;
|
| 38 |
+
font-size: 16px;
|
| 39 |
+
line-height: 1.6;
|
| 40 |
+
padding: 20px;
|
| 41 |
+
}}
|
| 42 |
+
.hallucination {{
|
| 43 |
+
background-color: rgba(255, 99, 71, 0.3);
|
| 44 |
+
padding: 2px;
|
| 45 |
+
border-radius: 3px;
|
| 46 |
+
cursor: help;
|
| 47 |
+
}}
|
| 48 |
+
.hallucination:hover {{
|
| 49 |
+
background-color: rgba(255, 99, 71, 0.5);
|
| 50 |
+
}}
|
| 51 |
+
</style>
|
| 52 |
+
<div class="container">{html_text}</div>
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
def verify_discharge_summary(
|
| 56 |
+
self, context: str, question: str, answer: str
|
| 57 |
+
) -> Optional[str]:
|
| 58 |
+
"""Verify the discharge summary for hallucinations and return highlighted HTML."""
|
| 59 |
+
try:
|
| 60 |
+
predictions = self.detector.predict(
|
| 61 |
+
context=[context],
|
| 62 |
+
question=question,
|
| 63 |
+
answer=answer,
|
| 64 |
+
output_format="spans"
|
| 65 |
+
)
|
| 66 |
+
logger.debug(f"Hallucination predictions: {predictions}")
|
| 67 |
+
return self.create_interactive_text(answer, predictions)
|
| 68 |
+
except Exception as e:
|
| 69 |
+
logger.error(f"Error verifying discharge summary: {str(e)}")
|
| 70 |
+
return None
|