Spaces:
Sleeping
Sleeping
Commit ·
31d72a3
0
Parent(s):
Initial commit for DVD application
Browse files- .gitignore +7 -0
- .space +9 -0
- Dockerfile +10 -0
- License +21 -0
- app.py +679 -0
- dvd_evaluator.py +359 -0
- note_criteria.json +63 -0
- readme.md +141 -0
- requirements.txt +11 -0
- templates/index.html +356 -0
.gitignore
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.pyc
|
| 2 |
+
__pycache__/
|
| 3 |
+
.env
|
| 4 |
+
*.csv
|
| 5 |
+
env/
|
| 6 |
+
venv/
|
| 7 |
+
.venv/
|
.space
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Create .space file
|
| 2 |
+
echo '
|
| 3 |
+
title: Document vs Document Evaluator
|
| 4 |
+
emoji: 📄
|
| 5 |
+
colorFrom: blue
|
| 6 |
+
colorTo: green
|
| 7 |
+
sdk: docker
|
| 8 |
+
app_port: 7860
|
| 9 |
+
' > .space
|
Dockerfile
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install -r requirements.txt
|
| 7 |
+
|
| 8 |
+
COPY . .
|
| 9 |
+
|
| 10 |
+
CMD ["python", "app.py"]
|
License
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2024 [Your Name]
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
app.py
ADDED
|
@@ -0,0 +1,679 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, render_template, request, jsonify
|
| 2 |
+
import os
|
| 3 |
+
import tempfile
|
| 4 |
+
import pandas as pd
|
| 5 |
+
from werkzeug.utils import secure_filename
|
| 6 |
+
import csv
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
from typing import List, Dict, Any, Optional, Union
|
| 9 |
+
from pydantic import BaseModel, Field
|
| 10 |
+
from langchain_openai import ChatOpenAI
|
| 11 |
+
from langchain_core.messages import HumanMessage, SystemMessage
|
| 12 |
+
import tiktoken
|
| 13 |
+
import json
|
| 14 |
+
from dotenv import load_dotenv
|
| 15 |
+
from dvd_evaluator import (
|
| 16 |
+
generate_mcqs_for_note,
|
| 17 |
+
present_mcqs_to_content,
|
| 18 |
+
MCQ,
|
| 19 |
+
Document
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
# Load environment variables
|
| 23 |
+
load_dotenv()
|
| 24 |
+
|
| 25 |
+
# Define data models
|
| 26 |
+
class MCQ(BaseModel):
|
| 27 |
+
question: str
|
| 28 |
+
options: List[str]
|
| 29 |
+
correct_answer: str
|
| 30 |
+
source_name: str = Field(default="Unknown")
|
| 31 |
+
|
| 32 |
+
class Document(BaseModel):
|
| 33 |
+
name: str = ''
|
| 34 |
+
content: str
|
| 35 |
+
mcqs: List[MCQ] = Field(default_factory=list)
|
| 36 |
+
|
| 37 |
+
app = Flask(__name__)
|
| 38 |
+
app.config['UPLOAD_FOLDER'] = tempfile.mkdtemp()
|
| 39 |
+
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
|
| 40 |
+
|
| 41 |
+
ALLOWED_EXTENSIONS = {'txt'}
|
| 42 |
+
MODELS = ['gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo'] # Update with supported models
|
| 43 |
+
|
| 44 |
+
with open('note_criteria.json', 'r') as f:
|
| 45 |
+
NOTE_CRITERIA = json.load(f)['note_types'] # Note the ['note_types'] key
|
| 46 |
+
|
| 47 |
+
def allowed_file(filename):
|
| 48 |
+
"""Check if the uploaded file has an allowed extension."""
|
| 49 |
+
return '.' in filename and \
|
| 50 |
+
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
| 51 |
+
|
| 52 |
+
def num_tokens_from_messages(messages, model="gpt-4o"):
|
| 53 |
+
"""
|
| 54 |
+
Estimate token usage for messages using tiktoken.
|
| 55 |
+
"""
|
| 56 |
+
encoding = tiktoken.encoding_for_model(model)
|
| 57 |
+
num_tokens = 0
|
| 58 |
+
for message in messages:
|
| 59 |
+
num_tokens += 4 # every message follows <im_start>{role/name}\n{content}<im_end>\n
|
| 60 |
+
for key, value in message.items():
|
| 61 |
+
num_tokens += len(encoding.encode(value))
|
| 62 |
+
num_tokens += 2 # every reply is primed with <im_start>assistant
|
| 63 |
+
return num_tokens
|
| 64 |
+
|
| 65 |
+
def generate_mcqs_for_note(note_content: str, total_tokens: List[int], source_name: str = '', document_type: str = 'discharge_note') -> List[MCQ]:
|
| 66 |
+
"""
|
| 67 |
+
Generate Multiple Choice Questions (MCQs) from medical notes.
|
| 68 |
+
"""
|
| 69 |
+
# Get relevancy criteria for selected document type
|
| 70 |
+
criteria = NOTE_CRITERIA[document_type]['relevancy_criteria']
|
| 71 |
+
criteria_list = "\n".join(f"{i+1}. {criterion}" for i, criterion in enumerate(criteria))
|
| 72 |
+
|
| 73 |
+
system_prompt = f"""
|
| 74 |
+
You are an expert in creating MCQs based on medical notes. Generate 20 MCQs that ONLY focus on these key areas:
|
| 75 |
+
{criteria_list}
|
| 76 |
+
|
| 77 |
+
Rules and Format:
|
| 78 |
+
1. Each question must relate to specific content from these areas
|
| 79 |
+
2. Skip areas not mentioned in the note
|
| 80 |
+
3. Each question must have exactly 5 options (A-D plus E="I don't know")
|
| 81 |
+
4. Provide only questions and answers, no explanations
|
| 82 |
+
5. Use this exact format:
|
| 83 |
+
|
| 84 |
+
Question: [text]
|
| 85 |
+
A. [option]
|
| 86 |
+
B. [option]
|
| 87 |
+
C. [option]
|
| 88 |
+
D. [option]
|
| 89 |
+
E. I don't know
|
| 90 |
+
Correct Answer: [letter]
|
| 91 |
+
"""
|
| 92 |
+
|
| 93 |
+
def parse_mcq(mcq_text: str) -> Optional[MCQ]:
|
| 94 |
+
"""Parse a single MCQ from text format into an MCQ object."""
|
| 95 |
+
try:
|
| 96 |
+
lines = [line.strip() for line in mcq_text.split('\n') if line.strip()]
|
| 97 |
+
if len(lines) < 7: # Question + 5 options + correct answer
|
| 98 |
+
return None
|
| 99 |
+
|
| 100 |
+
# Extract question
|
| 101 |
+
if not lines[0].startswith('Question:'):
|
| 102 |
+
return None
|
| 103 |
+
question = lines[0].replace('Question:', '', 1).strip()
|
| 104 |
+
|
| 105 |
+
# Extract options
|
| 106 |
+
options = []
|
| 107 |
+
for i, line in enumerate(lines[1:6], 1):
|
| 108 |
+
if not line.startswith(chr(ord('A') + i - 1) + '.'):
|
| 109 |
+
return None
|
| 110 |
+
option = line.split('.', 1)[1].strip()
|
| 111 |
+
options.append(option)
|
| 112 |
+
|
| 113 |
+
# Extract correct answer
|
| 114 |
+
correct_line = lines[6]
|
| 115 |
+
if not correct_line.lower().startswith('correct answer:'):
|
| 116 |
+
return None
|
| 117 |
+
|
| 118 |
+
correct_letter = correct_line.split(':', 1)[1].strip().upper()
|
| 119 |
+
if correct_letter not in 'ABCDE':
|
| 120 |
+
return None
|
| 121 |
+
|
| 122 |
+
correct_index = ord(correct_letter) - ord('A')
|
| 123 |
+
correct_answer = options[correct_index] if correct_index < len(options) else options[-1]
|
| 124 |
+
|
| 125 |
+
return MCQ(
|
| 126 |
+
question=question,
|
| 127 |
+
options=options,
|
| 128 |
+
correct_answer=correct_answer,
|
| 129 |
+
source_name=source_name
|
| 130 |
+
)
|
| 131 |
+
except Exception as e:
|
| 132 |
+
print(f"Error parsing MCQ: {str(e)}")
|
| 133 |
+
return None
|
| 134 |
+
|
| 135 |
+
# Generate MCQs using LLM
|
| 136 |
+
try:
|
| 137 |
+
messages = [
|
| 138 |
+
SystemMessage(content=system_prompt),
|
| 139 |
+
HumanMessage(content=f"Create MCQs from this note:\n\n{note_content}")
|
| 140 |
+
]
|
| 141 |
+
|
| 142 |
+
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
|
| 143 |
+
response = llm(messages)
|
| 144 |
+
|
| 145 |
+
# Update token count
|
| 146 |
+
tokens_used = num_tokens_from_messages([
|
| 147 |
+
{"role": "system", "content": system_prompt},
|
| 148 |
+
{"role": "user", "content": note_content},
|
| 149 |
+
{"role": "assistant", "content": response.content}
|
| 150 |
+
], model="gpt-4")
|
| 151 |
+
total_tokens[0] += tokens_used
|
| 152 |
+
|
| 153 |
+
# Parse MCQs from response
|
| 154 |
+
mcqs = []
|
| 155 |
+
for mcq_text in response.content.strip().split('\n\n'):
|
| 156 |
+
if mcq := parse_mcq(mcq_text):
|
| 157 |
+
mcqs.append(mcq)
|
| 158 |
+
|
| 159 |
+
return mcqs
|
| 160 |
+
|
| 161 |
+
except Exception as e:
|
| 162 |
+
print(f"Error in MCQ generation: {str(e)}")
|
| 163 |
+
return []
|
| 164 |
+
|
| 165 |
+
def present_mcqs_to_content(mcqs: List[MCQ], content: str, total_tokens: List[int]) -> List[Dict]:
|
| 166 |
+
"""
|
| 167 |
+
Present MCQs to content and collect responses.
|
| 168 |
+
"""
|
| 169 |
+
user_responses = []
|
| 170 |
+
batch_size = 20
|
| 171 |
+
llm = ChatOpenAI(model="gpt-4", temperature=0)
|
| 172 |
+
|
| 173 |
+
for i in range(0, len(mcqs), batch_size):
|
| 174 |
+
batch_mcqs = mcqs[i:i + batch_size]
|
| 175 |
+
questions_text = "\n\n".join([
|
| 176 |
+
f"Question {j+1}: {mcq.question}\n"
|
| 177 |
+
f"A. {mcq.options[0]}\n"
|
| 178 |
+
f"B. {mcq.options[1]}\n"
|
| 179 |
+
f"C. {mcq.options[2]}\n"
|
| 180 |
+
f"D. {mcq.options[3]}\n"
|
| 181 |
+
f"E. I don't know"
|
| 182 |
+
for j, mcq in enumerate(batch_mcqs)
|
| 183 |
+
])
|
| 184 |
+
|
| 185 |
+
batch_prompt = f"""
|
| 186 |
+
You are an expert medical knowledge evaluator. Given a medical note and multiple questions:
|
| 187 |
+
1. For each question, verify if it can be answered from the given content
|
| 188 |
+
2. If a question cannot be answered from the content, choose 'E' (I don't know)
|
| 189 |
+
3. If a question can be answered, choose the most accurate option based ONLY on the given content
|
| 190 |
+
|
| 191 |
+
Document Content: {content}
|
| 192 |
+
|
| 193 |
+
{questions_text}
|
| 194 |
+
|
| 195 |
+
Respond with ONLY the question numbers and corresponding letters, one per line, like this:
|
| 196 |
+
1: A
|
| 197 |
+
2: B
|
| 198 |
+
etc.
|
| 199 |
+
"""
|
| 200 |
+
|
| 201 |
+
messages = [HumanMessage(content=batch_prompt)]
|
| 202 |
+
response = llm(messages)
|
| 203 |
+
|
| 204 |
+
tokens_used = num_tokens_from_messages([
|
| 205 |
+
{"role": "user", "content": batch_prompt},
|
| 206 |
+
{"role": "assistant", "content": response.content}
|
| 207 |
+
], model="gpt-4o-mini")
|
| 208 |
+
total_tokens[0] += tokens_used
|
| 209 |
+
|
| 210 |
+
try:
|
| 211 |
+
response_lines = response.content.strip().split('\n')
|
| 212 |
+
for j, line in enumerate(response_lines):
|
| 213 |
+
if j >= len(batch_mcqs):
|
| 214 |
+
break
|
| 215 |
+
|
| 216 |
+
mcq = batch_mcqs[j]
|
| 217 |
+
try:
|
| 218 |
+
# Get the letter answer (A, B, C, D, or E)
|
| 219 |
+
answer_letter = line.split(':')[1].strip().upper()
|
| 220 |
+
if answer_letter not in ['A', 'B', 'C', 'D', 'E']:
|
| 221 |
+
answer_letter = 'E'
|
| 222 |
+
|
| 223 |
+
# Convert letter to corresponding option text
|
| 224 |
+
if answer_letter == 'E':
|
| 225 |
+
user_answer_text = "I don't know"
|
| 226 |
+
else:
|
| 227 |
+
# Get the index (0-3) from the letter (A-D)
|
| 228 |
+
option_index = ord(answer_letter) - ord('A')
|
| 229 |
+
user_answer_text = mcq.options[option_index]
|
| 230 |
+
|
| 231 |
+
except (IndexError, ValueError):
|
| 232 |
+
user_answer_text = "I don't know"
|
| 233 |
+
|
| 234 |
+
user_responses.append({
|
| 235 |
+
"question": mcq.question,
|
| 236 |
+
"user_answer": user_answer_text,
|
| 237 |
+
"correct_answer": mcq.correct_answer
|
| 238 |
+
})
|
| 239 |
+
|
| 240 |
+
except Exception as e:
|
| 241 |
+
print(f"Error processing batch responses: {str(e)}")
|
| 242 |
+
# If something fails, default the remainder to "I don't know"
|
| 243 |
+
for mcq in batch_mcqs[len(user_responses):]:
|
| 244 |
+
user_responses.append({
|
| 245 |
+
"question": mcq.question,
|
| 246 |
+
"user_answer": "I don't know",
|
| 247 |
+
"correct_answer": mcq.correct_answer
|
| 248 |
+
})
|
| 249 |
+
|
| 250 |
+
return user_responses
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def run_evaluation(ai_content: str, ai_mcqs: List[MCQ], note_content: str, note_mcqs: List[MCQ],
|
| 254 |
+
note_name: str, original_note_number: int, total_tokens: List[int]) -> List[Dict]:
|
| 255 |
+
|
| 256 |
+
# For Doc1: use questions from Doc2 (note_mcqs)
|
| 257 |
+
# For Doc2: use questions from Doc1 (ai_mcqs)
|
| 258 |
+
mcqs_to_use = ai_mcqs if note_name == 'Doc2' else note_mcqs
|
| 259 |
+
content_to_evaluate = note_content
|
| 260 |
+
|
| 261 |
+
responses = present_mcqs_to_content(mcqs_to_use, content_to_evaluate, total_tokens)
|
| 262 |
+
|
| 263 |
+
results = []
|
| 264 |
+
for i, mcq in enumerate(mcqs_to_use):
|
| 265 |
+
results.append({
|
| 266 |
+
"original_note_number": original_note_number,
|
| 267 |
+
"new_note_name": note_name,
|
| 268 |
+
"question": mcq.question,
|
| 269 |
+
"options": mcq.options,
|
| 270 |
+
"source_document": 'Doc2' if note_name == 'Doc1' else 'Doc1',
|
| 271 |
+
"ideal_answer": mcq.correct_answer,
|
| 272 |
+
"model_answer": responses[i]["user_answer"],
|
| 273 |
+
"is_correct": responses[i]["user_answer"] == mcq.correct_answer
|
| 274 |
+
})
|
| 275 |
+
|
| 276 |
+
return results
|
| 277 |
+
import concurrent.futures
|
| 278 |
+
|
| 279 |
+
import concurrent.futures
|
| 280 |
+
import csv
|
| 281 |
+
import os
|
| 282 |
+
from flask import jsonify, request
|
| 283 |
+
|
| 284 |
+
@app.route('/compare', methods=['POST'])
|
| 285 |
+
def compare_documents():
|
| 286 |
+
"""
|
| 287 |
+
Compare two documents by generating and answering MCQs for each document.
|
| 288 |
+
Returns analysis of how well each document contains information from the other.
|
| 289 |
+
"""
|
| 290 |
+
print("\n=== Starting document comparison ===")
|
| 291 |
+
|
| 292 |
+
try:
|
| 293 |
+
# Validate API key
|
| 294 |
+
api_key = request.form.get('api_key')
|
| 295 |
+
if not api_key:
|
| 296 |
+
return jsonify({"error": "OpenAI API key is required"}), 400
|
| 297 |
+
os.environ['OPENAI_API_KEY'] = api_key
|
| 298 |
+
|
| 299 |
+
# Get model and document type selection
|
| 300 |
+
model = request.form.get('model', 'gpt-4o-mini')
|
| 301 |
+
document_type = request.form.get('document_type', 'discharge_note')
|
| 302 |
+
|
| 303 |
+
# Initialize OpenAI client with selected model
|
| 304 |
+
llm = ChatOpenAI(model=model, temperature=0)
|
| 305 |
+
|
| 306 |
+
# Validate file uploads
|
| 307 |
+
if 'doc1' not in request.files or 'doc2' not in request.files:
|
| 308 |
+
print("Error: Missing files in request")
|
| 309 |
+
return jsonify({"error": "Both doc1 and doc2 are required"}), 400
|
| 310 |
+
|
| 311 |
+
doc1_file = request.files['doc1']
|
| 312 |
+
doc2_file = request.files['doc2']
|
| 313 |
+
|
| 314 |
+
print(f"Received files: {doc1_file.filename} and {doc2_file.filename}")
|
| 315 |
+
|
| 316 |
+
# Validate filenames
|
| 317 |
+
if not all([doc1_file.filename, doc2_file.filename]):
|
| 318 |
+
print("Error: Empty filename(s)")
|
| 319 |
+
return jsonify({"error": "Both documents need valid filenames"}), 400
|
| 320 |
+
|
| 321 |
+
# Validate file types
|
| 322 |
+
if not all(allowed_file(f.filename) for f in [doc1_file, doc2_file]):
|
| 323 |
+
print("Error: Invalid file type(s)")
|
| 324 |
+
return jsonify({"error": "Only .txt files are allowed"}), 400
|
| 325 |
+
|
| 326 |
+
# Read document contents
|
| 327 |
+
try:
|
| 328 |
+
doc1_text = doc1_file.read().decode('utf-8')
|
| 329 |
+
doc2_text = doc2_file.read().decode('utf-8')
|
| 330 |
+
print(f"Doc1 length: {len(doc1_text)} chars")
|
| 331 |
+
print(f"Doc2 length: {len(doc2_text)} chars")
|
| 332 |
+
except UnicodeDecodeError as e:
|
| 333 |
+
print(f"Decode error: {str(e)}")
|
| 334 |
+
return jsonify({"error": "Error decoding one of the documents"}), 400
|
| 335 |
+
|
| 336 |
+
# Initialize token counter
|
| 337 |
+
total_tokens = [0]
|
| 338 |
+
|
| 339 |
+
# Generate MCQs for both documents
|
| 340 |
+
print("\nGenerating MCQs for Doc1...")
|
| 341 |
+
doc1_mcqs = generate_mcqs_for_note(
|
| 342 |
+
note_content=doc1_text,
|
| 343 |
+
total_tokens=total_tokens,
|
| 344 |
+
source_name='Doc1',
|
| 345 |
+
document_type=document_type
|
| 346 |
+
)
|
| 347 |
+
print(f"Generated {len(doc1_mcqs)} MCQs for Doc1")
|
| 348 |
+
|
| 349 |
+
print("\nGenerating MCQs for Doc2...")
|
| 350 |
+
doc2_mcqs = generate_mcqs_for_note(
|
| 351 |
+
note_content=doc2_text,
|
| 352 |
+
total_tokens=total_tokens,
|
| 353 |
+
source_name='Doc2',
|
| 354 |
+
document_type=document_type
|
| 355 |
+
)
|
| 356 |
+
print(f"Generated {len(doc2_mcqs)} MCQs for Doc2")
|
| 357 |
+
|
| 358 |
+
# Present each doc's MCQs to the other doc
|
| 359 |
+
print("\nGetting answers for Doc1...")
|
| 360 |
+
doc1_responses = present_mcqs_to_content(doc2_mcqs, doc1_text, total_tokens)
|
| 361 |
+
print(f"Received {len(doc1_responses)} answers for Doc1")
|
| 362 |
+
|
| 363 |
+
print("\nGetting answers for Doc2...")
|
| 364 |
+
doc2_responses = present_mcqs_to_content(doc1_mcqs, doc2_text, total_tokens)
|
| 365 |
+
print(f"Received {len(doc2_responses)} answers for Doc2")
|
| 366 |
+
|
| 367 |
+
def process_mcq_results(responses, mcqs):
|
| 368 |
+
"""Process MCQ responses and organize into categories."""
|
| 369 |
+
attempted = []
|
| 370 |
+
unknown = []
|
| 371 |
+
correct_count = 0
|
| 372 |
+
total_count = len(responses)
|
| 373 |
+
|
| 374 |
+
for i, response in enumerate(responses):
|
| 375 |
+
if i >= len(mcqs): # Safety check
|
| 376 |
+
continue
|
| 377 |
+
|
| 378 |
+
mcq = mcqs[i]
|
| 379 |
+
answer = response.get("user_answer", "I don't know")
|
| 380 |
+
|
| 381 |
+
result = {
|
| 382 |
+
"question": mcq.question,
|
| 383 |
+
"options": mcq.options,
|
| 384 |
+
"ideal_answer": mcq.correct_answer,
|
| 385 |
+
"model_answer": answer,
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
if answer == "I don't know":
|
| 389 |
+
unknown.append(result)
|
| 390 |
+
else:
|
| 391 |
+
is_correct = answer == mcq.correct_answer
|
| 392 |
+
if is_correct:
|
| 393 |
+
correct_count += 1
|
| 394 |
+
result["is_correct"] = is_correct
|
| 395 |
+
attempted.append(result)
|
| 396 |
+
|
| 397 |
+
return {
|
| 398 |
+
"score": f"{correct_count}/{total_count}",
|
| 399 |
+
"attempted_answers": attempted,
|
| 400 |
+
"unknown_answers": unknown
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
# Process results for both documents
|
| 404 |
+
doc1_analysis = process_mcq_results(doc1_responses, doc2_mcqs)
|
| 405 |
+
doc2_analysis = process_mcq_results(doc2_responses, doc1_mcqs)
|
| 406 |
+
|
| 407 |
+
# Prepare response
|
| 408 |
+
response = {
|
| 409 |
+
"doc1_analysis": doc1_analysis,
|
| 410 |
+
"doc2_analysis": doc2_analysis,
|
| 411 |
+
"total_tokens": total_tokens[0],
|
| 412 |
+
"doc1_content": doc1_text,
|
| 413 |
+
"doc2_content": doc2_text
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
print("\nSending response...")
|
| 417 |
+
print(f"Total tokens used: {total_tokens[0]}")
|
| 418 |
+
return jsonify(response), 200
|
| 419 |
+
|
| 420 |
+
except Exception as e:
|
| 421 |
+
import traceback
|
| 422 |
+
print(f"\nERROR in compare_documents:")
|
| 423 |
+
print(traceback.format_exc())
|
| 424 |
+
return jsonify({"error": str(e)}), 500
|
| 425 |
+
|
| 426 |
+
finally:
|
| 427 |
+
print("=== Comparison complete ===\n")
|
| 428 |
+
|
| 429 |
+
def process_responses(responses, mcqs, doc_name):
|
| 430 |
+
"""Process responses and organize them into categories."""
|
| 431 |
+
attempted = []
|
| 432 |
+
unknown = []
|
| 433 |
+
correct_count = 0
|
| 434 |
+
|
| 435 |
+
for i, response in enumerate(responses):
|
| 436 |
+
mcq = mcqs[i]
|
| 437 |
+
answer_text = response['user_answer']
|
| 438 |
+
|
| 439 |
+
if answer_text == "I don't know": # Changed from 'E' to "I don't know"
|
| 440 |
+
unknown.append({
|
| 441 |
+
'question': mcq.question,
|
| 442 |
+
'options': mcq.options,
|
| 443 |
+
'ideal_answer': mcq.correct_answer
|
| 444 |
+
})
|
| 445 |
+
else:
|
| 446 |
+
is_correct = response['user_answer'] == response['correct_answer']
|
| 447 |
+
if is_correct:
|
| 448 |
+
correct_count += 1
|
| 449 |
+
|
| 450 |
+
attempted.append({
|
| 451 |
+
'question': mcq.question,
|
| 452 |
+
'options': mcq.options,
|
| 453 |
+
'ideal_answer': mcq.correct_answer,
|
| 454 |
+
'model_answer': answer_text, # Use the answer text directly
|
| 455 |
+
'is_correct': is_correct
|
| 456 |
+
})
|
| 457 |
+
|
| 458 |
+
return {
|
| 459 |
+
'total_score': f"{correct_count}/{len(responses)}",
|
| 460 |
+
'attempted_answers': attempted,
|
| 461 |
+
'unknown_answers': unknown
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
@app.route('/')
|
| 465 |
+
def index():
|
| 466 |
+
"""Serve the main page."""
|
| 467 |
+
return render_template('index.html', models=MODELS)
|
| 468 |
+
|
| 469 |
+
if __name__ == '__main__':
|
| 470 |
+
# Ensure templates directory exists
|
| 471 |
+
if not os.path.exists('templates'):
|
| 472 |
+
os.makedirs('templates')
|
| 473 |
+
|
| 474 |
+
# Create index.html in templates directory if it doesn't exist
|
| 475 |
+
template_path = os.path.join('templates', 'index.html')
|
| 476 |
+
if not os.path.exists(template_path):
|
| 477 |
+
with open(template_path, 'w', encoding='utf-8') as f:
|
| 478 |
+
f.write("""<!DOCTYPE html>
|
| 479 |
+
<html lang="en">
|
| 480 |
+
<head>
|
| 481 |
+
<meta charset="UTF-8">
|
| 482 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 483 |
+
<title>Document Comparison Tool</title>
|
| 484 |
+
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
| 485 |
+
</head>
|
| 486 |
+
<body class="bg-gray-100 p-8">
|
| 487 |
+
<div class="max-w-4xl mx-auto">
|
| 488 |
+
<h1 class="text-3xl font-bold mb-8">Document Comparison Tool</h1>
|
| 489 |
+
|
| 490 |
+
<!-- API Key Input -->
|
| 491 |
+
<div class="bg-white p-6 rounded-lg shadow-md mb-8">
|
| 492 |
+
<div class="mb-4">
|
| 493 |
+
<label class="block text-sm font-medium mb-2">OpenAI API Key</label>
|
| 494 |
+
<input type="password" id="apiKey"
|
| 495 |
+
class="w-full border rounded p-2"
|
| 496 |
+
placeholder="Enter your OpenAI API key">
|
| 497 |
+
</div>
|
| 498 |
+
</div>
|
| 499 |
+
|
| 500 |
+
<!-- Upload Form -->
|
| 501 |
+
<form id="uploadForm" class="bg-white p-6 rounded-lg shadow-md mb-8">
|
| 502 |
+
<div class="grid grid-cols-2 gap-6 mb-6">
|
| 503 |
+
<div>
|
| 504 |
+
<label class="block text-sm font-medium mb-2">Document 1</label>
|
| 505 |
+
<input type="file" name="doc1" accept=".txt" required
|
| 506 |
+
class="w-full border rounded p-2">
|
| 507 |
+
</div>
|
| 508 |
+
<div>
|
| 509 |
+
<label class="block text-sm font-medium mb-2">Document 2</label>
|
| 510 |
+
<input type="file" name="doc2" accept=".txt" required
|
| 511 |
+
class="w-full border rounded p-2">
|
| 512 |
+
</div>
|
| 513 |
+
</div>
|
| 514 |
+
|
| 515 |
+
<div class="mb-6">
|
| 516 |
+
<label class="block text-sm font-medium mb-2">Model</label>
|
| 517 |
+
<select name="model" class="w-full border rounded p-2">
|
| 518 |
+
{% for model in models %}
|
| 519 |
+
<option value="{{ model }}">{{ model }}</option>
|
| 520 |
+
{% endfor %}
|
| 521 |
+
</select>
|
| 522 |
+
</div>
|
| 523 |
+
|
| 524 |
+
<div class="mb-6">
|
| 525 |
+
<label class="block text-sm font-medium mb-2">Document Type</label>
|
| 526 |
+
<select name="document_type" class="w-full border rounded p-2">
|
| 527 |
+
{% for type_id, type_info in document_types.items() %}
|
| 528 |
+
<option value="{{ type_id }}">{{ type_info.name }}</option>
|
| 529 |
+
{% endfor %}
|
| 530 |
+
</select>
|
| 531 |
+
</div>
|
| 532 |
+
|
| 533 |
+
<button type="submit"
|
| 534 |
+
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
|
| 535 |
+
Compare Documents
|
| 536 |
+
</button>
|
| 537 |
+
</form>
|
| 538 |
+
|
| 539 |
+
<!-- Loading indicator -->
|
| 540 |
+
<div id="loading" class="hidden">
|
| 541 |
+
<div class="text-center py-4">
|
| 542 |
+
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto"></div>
|
| 543 |
+
<p class="mt-2">Processing documents... May take few minutes.</p>
|
| 544 |
+
</div>
|
| 545 |
+
</div>
|
| 546 |
+
|
| 547 |
+
<!-- Results Section -->
|
| 548 |
+
<div id="results" class="hidden">
|
| 549 |
+
<div class="grid grid-cols-2 gap-6">
|
| 550 |
+
<!-- Document 1 Results -->
|
| 551 |
+
<div class="bg-white p-6 rounded-lg shadow-md">
|
| 552 |
+
<h2 class="text-xl font-bold mb-4">Document 1 Results</h2>
|
| 553 |
+
<div id="doc1Results"></div>
|
| 554 |
+
</div>
|
| 555 |
+
|
| 556 |
+
<!-- Document 2 Results -->
|
| 557 |
+
<div class="bg-white p-6 rounded-lg shadow-md">
|
| 558 |
+
<h2 class="text-xl font-bold mb-4">Document 2 Results</h2>
|
| 559 |
+
<div id="doc2Results"></div>
|
| 560 |
+
</div>
|
| 561 |
+
</div>
|
| 562 |
+
</div>
|
| 563 |
+
</div>
|
| 564 |
+
|
| 565 |
+
<script>
|
| 566 |
+
document.getElementById('uploadForm').addEventListener('submit', async (e) => {
|
| 567 |
+
e.preventDefault();
|
| 568 |
+
|
| 569 |
+
const apiKey = document.getElementById('apiKey').value;
|
| 570 |
+
if (!apiKey) {
|
| 571 |
+
alert('Please enter your OpenAI API key');
|
| 572 |
+
return;
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
const loading = document.getElementById('loading');
|
| 576 |
+
const results = document.getElementById('results');
|
| 577 |
+
|
| 578 |
+
loading.classList.remove('hidden');
|
| 579 |
+
results.classList.add('hidden');
|
| 580 |
+
|
| 581 |
+
const formData = new FormData(e.target);
|
| 582 |
+
formData.append('api_key', apiKey); // Add API key to form data
|
| 583 |
+
|
| 584 |
+
try {
|
| 585 |
+
const response = await fetch('/compare', {
|
| 586 |
+
method: 'POST',
|
| 587 |
+
body: formData
|
| 588 |
+
});
|
| 589 |
+
|
| 590 |
+
const data = await response.json();
|
| 591 |
+
|
| 592 |
+
if (response.ok) {
|
| 593 |
+
displayResults('doc1Results', data.doc1_analysis);
|
| 594 |
+
displayResults('doc2Results', data.doc2_analysis);
|
| 595 |
+
results.classList.remove('hidden');
|
| 596 |
+
} else {
|
| 597 |
+
alert(data.error || 'An error occurred');
|
| 598 |
+
}
|
| 599 |
+
} catch (error) {
|
| 600 |
+
alert('An error occurred while processing the documents');
|
| 601 |
+
} finally {
|
| 602 |
+
loading.classList.add('hidden');
|
| 603 |
+
}
|
| 604 |
+
});
|
| 605 |
+
|
| 606 |
+
function displayResults(elementId, analysis) {
|
| 607 |
+
const container = document.getElementById(elementId);
|
| 608 |
+
|
| 609 |
+
container.innerHTML = `
|
| 610 |
+
<div class="mb-4">
|
| 611 |
+
<h3 class="font-bold">Total Score:</h3>
|
| 612 |
+
<p>${analysis.total_score}</p>
|
| 613 |
+
</div>
|
| 614 |
+
|
| 615 |
+
<div class="mb-4">
|
| 616 |
+
<h3 class="font-bold">Self Questions Mistakes:</h3>
|
| 617 |
+
${renderQuestionList(analysis.self_mistakes)}
|
| 618 |
+
</div>
|
| 619 |
+
|
| 620 |
+
<div class="mb-4">
|
| 621 |
+
<h3 class="font-bold">Other Document Mistakes:</h3>
|
| 622 |
+
${renderQuestionList(analysis.other_mistakes)}
|
| 623 |
+
</div>
|
| 624 |
+
|
| 625 |
+
<div class="mb-4">
|
| 626 |
+
<h3 class="font-bold">Unknown Answers:</h3>
|
| 627 |
+
${renderQuestionList(analysis.unknown_answers, true)}
|
| 628 |
+
</div>
|
| 629 |
+
`;
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
function renderQuestionList(questions, isUnknown = false) {
|
| 633 |
+
if (!questions.length) {
|
| 634 |
+
return '<p class="text-gray-500">None</p>';
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
return questions.map((q, idx) => {
|
| 638 |
+
// We'll store the snippet in a hidden div and toggle it on click
|
| 639 |
+
const questionId = `question-${Math.random().toString(36).slice(2)}`;
|
| 640 |
+
|
| 641 |
+
return `
|
| 642 |
+
<div class="mb-2 p-2 bg-gray-50 rounded" id="${questionId}">
|
| 643 |
+
<button
|
| 644 |
+
class="font-medium text-left w-full"
|
| 645 |
+
onclick="toggleSnippet('${questionId}')"
|
| 646 |
+
>
|
| 647 |
+
${q.question}
|
| 648 |
+
</button>
|
| 649 |
+
<p class="text-sm">Ideal Answer: ${q.ideal_answer}</p>
|
| 650 |
+
${!isUnknown ? `<p class="text-sm">Model Answer: ${q.model_answer}</p>` : ''}
|
| 651 |
+
|
| 652 |
+
<!-- Hidden snippet container -->
|
| 653 |
+
<div class="hidden mt-2 p-2 border-l-4 border-blue-300" id="${questionId}-snippet">
|
| 654 |
+
<h4 class="font-bold mb-1">Relevant Snippet (Doc1):</h4>
|
| 655 |
+
<p class="text-sm mb-2">${q.snippet_doc1 || 'No snippet found'}</p>
|
| 656 |
+
|
| 657 |
+
<h4 class="font-bold mb-1">Relevant Snippet (Doc2):</h4>
|
| 658 |
+
<p class="text-sm">${q.snippet_doc2 || 'No snippet found'}</p>
|
| 659 |
+
</div>
|
| 660 |
+
</div>
|
| 661 |
+
`;
|
| 662 |
+
}).join('');
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
// JavaScript function to toggle snippet visibility
|
| 666 |
+
function toggleSnippet(questionId) {
|
| 667 |
+
const snippetDiv = document.getElementById(`${questionId}-snippet`);
|
| 668 |
+
if (snippetDiv.classList.contains('hidden')) {
|
| 669 |
+
snippetDiv.classList.remove('hidden');
|
| 670 |
+
} else {
|
| 671 |
+
snippetDiv.classList.add('hidden');
|
| 672 |
+
}
|
| 673 |
+
}
|
| 674 |
+
</script>
|
| 675 |
+
</body>
|
| 676 |
+
</html>
|
| 677 |
+
""")
|
| 678 |
+
|
| 679 |
+
app.run(debug=True)
|
dvd_evaluator.py
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import csv
|
| 3 |
+
import argparse
|
| 4 |
+
import pandas as pd
|
| 5 |
+
from typing import List, Dict, Any
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from pydantic import BaseModel, Field
|
| 8 |
+
from tqdm import tqdm
|
| 9 |
+
import tiktoken
|
| 10 |
+
from typing import List, Dict, Any, Optional
|
| 11 |
+
import json
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
from langchain_openai import ChatOpenAI
|
| 15 |
+
from langchain_core.messages import HumanMessage, SystemMessage
|
| 16 |
+
|
| 17 |
+
from dotenv import load_dotenv
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
load_dotenv()
|
| 21 |
+
|
| 22 |
+
# Define data models
|
| 23 |
+
class MCQ(BaseModel):
|
| 24 |
+
question: str
|
| 25 |
+
options: List[str]
|
| 26 |
+
correct_answer: str
|
| 27 |
+
source_name: str = Field(default="Unknown") # Add source_name field with default value
|
| 28 |
+
|
| 29 |
+
class Document(BaseModel):
|
| 30 |
+
name: str = ''
|
| 31 |
+
content: str
|
| 32 |
+
mcqs: List[MCQ] = Field(default_factory=list)
|
| 33 |
+
|
| 34 |
+
# Load note criteria at module level
|
| 35 |
+
with open('note_criteria.json', 'r') as f:
|
| 36 |
+
NOTE_CRITERIA = json.load(f)['note_types']
|
| 37 |
+
|
| 38 |
+
def num_tokens_from_messages(messages, model="gpt-4"):
|
| 39 |
+
"""
|
| 40 |
+
Estimate token usage for messages using tiktoken.
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
messages: List of message dictionaries
|
| 44 |
+
model (str): Model name for token counting. Defaults to 'gpt-4'
|
| 45 |
+
"""
|
| 46 |
+
try:
|
| 47 |
+
encoding = tiktoken.encoding_for_model(model)
|
| 48 |
+
num_tokens = 0
|
| 49 |
+
for message in messages:
|
| 50 |
+
num_tokens += 4
|
| 51 |
+
for key, value in message.items():
|
| 52 |
+
num_tokens += len(encoding.encode(value))
|
| 53 |
+
num_tokens += 2
|
| 54 |
+
return num_tokens
|
| 55 |
+
except Exception as e:
|
| 56 |
+
print(f"Warning: Error counting tokens: {str(e)}")
|
| 57 |
+
return 0
|
| 58 |
+
|
| 59 |
+
def generate_mcqs_for_note(note_content: str, total_tokens: List[int], source_name: str = '', document_type: str = 'discharge_note') -> List[MCQ]:
|
| 60 |
+
"""
|
| 61 |
+
Generate Multiple Choice Questions (MCQs) from medical notes.
|
| 62 |
+
"""
|
| 63 |
+
# Get criteria based on document type
|
| 64 |
+
criteria = NOTE_CRITERIA.get(document_type, NOTE_CRITERIA['discharge_note'])
|
| 65 |
+
criteria_points = criteria['relevancy_criteria']
|
| 66 |
+
|
| 67 |
+
# Create dynamic system prompt based on document type
|
| 68 |
+
system_prompt = f"""
|
| 69 |
+
You are an expert in creating MCQs based on {criteria['name']}s. Generate 20 MCQs that ONLY focus on these key areas:
|
| 70 |
+
{chr(10).join(f"{i+1}. {point}" for i, point in enumerate(criteria_points))}
|
| 71 |
+
|
| 72 |
+
Rules and Format:
|
| 73 |
+
1. Each question must relate to specific content from these areas
|
| 74 |
+
2. Skip areas not mentioned in the note
|
| 75 |
+
3. Each question must have exactly 5 options (A-D plus E="I don't know")
|
| 76 |
+
4. Provide only questions and answers, no explanations
|
| 77 |
+
5. Use this exact format:
|
| 78 |
+
|
| 79 |
+
Question: [text]
|
| 80 |
+
A. [option]
|
| 81 |
+
B. [option]
|
| 82 |
+
C. [option]
|
| 83 |
+
D. [option]
|
| 84 |
+
E. I don't know
|
| 85 |
+
Correct Answer: [letter]
|
| 86 |
+
"""
|
| 87 |
+
|
| 88 |
+
try:
|
| 89 |
+
messages = [
|
| 90 |
+
SystemMessage(content=system_prompt),
|
| 91 |
+
HumanMessage(content=f"Create MCQs from this {criteria['name'].lower()}:\n\n{note_content}")
|
| 92 |
+
]
|
| 93 |
+
|
| 94 |
+
llm = ChatOpenAI(temperature=0)
|
| 95 |
+
response = llm(messages)
|
| 96 |
+
|
| 97 |
+
# Update token count with default model
|
| 98 |
+
tokens_used = num_tokens_from_messages([
|
| 99 |
+
{"role": "system", "content": system_prompt},
|
| 100 |
+
{"role": "user", "content": note_content},
|
| 101 |
+
{"role": "assistant", "content": response.content}
|
| 102 |
+
])
|
| 103 |
+
total_tokens[0] += tokens_used
|
| 104 |
+
|
| 105 |
+
# Parse MCQs from response
|
| 106 |
+
mcqs = []
|
| 107 |
+
for mcq_text in response.content.strip().split('\n\n'):
|
| 108 |
+
if mcq := parse_mcq(mcq_text):
|
| 109 |
+
mcq.source_name = source_name
|
| 110 |
+
mcqs.append(mcq)
|
| 111 |
+
|
| 112 |
+
return mcqs
|
| 113 |
+
|
| 114 |
+
except Exception as e:
|
| 115 |
+
print(f"Error in MCQ generation: {str(e)}")
|
| 116 |
+
return []
|
| 117 |
+
|
| 118 |
+
def present_mcqs_to_content(mcqs: List[MCQ], content: str, total_tokens: List[int], document_type: str = 'discharge_note') -> List[Dict]:
|
| 119 |
+
"""
|
| 120 |
+
Present MCQs to content and collect responses.
|
| 121 |
+
"""
|
| 122 |
+
# Get criteria based on document type
|
| 123 |
+
criteria = NOTE_CRITERIA.get(document_type, NOTE_CRITERIA['discharge_note'])
|
| 124 |
+
|
| 125 |
+
batch_size = 20
|
| 126 |
+
llm = ChatOpenAI(temperature=0) # Remove model parameter
|
| 127 |
+
user_responses = []
|
| 128 |
+
|
| 129 |
+
for i in range(0, len(mcqs), batch_size):
|
| 130 |
+
batch_mcqs = mcqs[i:i + batch_size]
|
| 131 |
+
questions_text = "\n\n".join([
|
| 132 |
+
f"Question {j+1}: {mcq.question}\n"
|
| 133 |
+
f"A. {mcq.options[0]}\n"
|
| 134 |
+
f"B. {mcq.options[1]}\n"
|
| 135 |
+
f"C. {mcq.options[2]}\n"
|
| 136 |
+
f"D. {mcq.options[3]}\n"
|
| 137 |
+
f"E. I don't know"
|
| 138 |
+
for j, mcq in enumerate(batch_mcqs)
|
| 139 |
+
])
|
| 140 |
+
|
| 141 |
+
batch_prompt = f"""
|
| 142 |
+
You are an expert {criteria['name'].lower()} evaluator. Given a medical note and multiple questions:
|
| 143 |
+
1. For each question, verify if it can be answered from the given content
|
| 144 |
+
2. If a question cannot be answered from the content, choose 'E' (I don't know)
|
| 145 |
+
3. If a question can be answered, choose the most accurate option based ONLY on the given content
|
| 146 |
+
|
| 147 |
+
Document Content: {content}
|
| 148 |
+
|
| 149 |
+
{questions_text}
|
| 150 |
+
|
| 151 |
+
Respond with ONLY the question numbers and corresponding letters, one per line, like this:
|
| 152 |
+
1: A
|
| 153 |
+
2: B
|
| 154 |
+
etc.
|
| 155 |
+
"""
|
| 156 |
+
|
| 157 |
+
messages = [HumanMessage(content=batch_prompt)]
|
| 158 |
+
response = llm(messages)
|
| 159 |
+
|
| 160 |
+
tokens_used = num_tokens_from_messages([
|
| 161 |
+
{"role": "user", "content": batch_prompt},
|
| 162 |
+
{"role": "assistant", "content": response.content}
|
| 163 |
+
]) # Remove model parameter
|
| 164 |
+
|
| 165 |
+
total_tokens[0] += tokens_used
|
| 166 |
+
|
| 167 |
+
try:
|
| 168 |
+
response_lines = response.content.strip().split('\n')
|
| 169 |
+
for j, line in enumerate(response_lines):
|
| 170 |
+
if j >= len(batch_mcqs):
|
| 171 |
+
break
|
| 172 |
+
|
| 173 |
+
try:
|
| 174 |
+
answer = line.split(':')[1].strip().upper()
|
| 175 |
+
if answer not in ['A', 'B', 'C', 'D', 'E']:
|
| 176 |
+
answer = 'E'
|
| 177 |
+
|
| 178 |
+
mcq = batch_mcqs[j]
|
| 179 |
+
user_responses.append({
|
| 180 |
+
"question": mcq.question,
|
| 181 |
+
"user_answer": answer,
|
| 182 |
+
"correct_answer": chr(ord('A') + mcq.options.index(mcq.correct_answer))
|
| 183 |
+
})
|
| 184 |
+
except (IndexError, ValueError):
|
| 185 |
+
mcq = batch_mcqs[j]
|
| 186 |
+
user_responses.append({
|
| 187 |
+
"question": mcq.question,
|
| 188 |
+
"user_answer": "E",
|
| 189 |
+
"correct_answer": chr(ord('A') + mcq.options.index(mcq.correct_answer))
|
| 190 |
+
})
|
| 191 |
+
|
| 192 |
+
except Exception as e:
|
| 193 |
+
print(f"Error processing batch responses: {str(e)}")
|
| 194 |
+
for mcq in batch_mcqs[len(user_responses):]:
|
| 195 |
+
user_responses.append({
|
| 196 |
+
"question": mcq.question,
|
| 197 |
+
"user_answer": "E",
|
| 198 |
+
"correct_answer": chr(ord('A') + mcq.options.index(mcq.correct_answer))
|
| 199 |
+
})
|
| 200 |
+
|
| 201 |
+
return user_responses
|
| 202 |
+
|
| 203 |
+
def evaluate_responses(user_responses) -> int:
|
| 204 |
+
"""
|
| 205 |
+
Evaluate responses and return score.
|
| 206 |
+
"""
|
| 207 |
+
correct = 0
|
| 208 |
+
for response in user_responses:
|
| 209 |
+
if response["user_answer"] == "E": # "I don't know" is now "E"
|
| 210 |
+
continue
|
| 211 |
+
elif response["user_answer"] == response["correct_answer"]:
|
| 212 |
+
correct += 1
|
| 213 |
+
|
| 214 |
+
return correct
|
| 215 |
+
|
| 216 |
+
def run_evaluation(ai_content: str, ai_mcqs: List[MCQ], note_content: str, note_mcqs: List[MCQ],
|
| 217 |
+
note_name: str, original_note_number: int, total_tokens: List[int],
|
| 218 |
+
document_type: str = 'discharge_note') -> List[Dict]:
|
| 219 |
+
"""
|
| 220 |
+
Run evaluation with specified document type.
|
| 221 |
+
"""
|
| 222 |
+
# For Doc1: use questions from Doc2 (note_mcqs)
|
| 223 |
+
# For Doc2: use questions from Doc1 (ai_mcqs)
|
| 224 |
+
mcqs_to_use = ai_mcqs if note_name == 'Doc2' else note_mcqs
|
| 225 |
+
content_to_evaluate = note_content
|
| 226 |
+
|
| 227 |
+
responses = present_mcqs_to_content(mcqs_to_use, content_to_evaluate, total_tokens, document_type=document_type)
|
| 228 |
+
|
| 229 |
+
results = []
|
| 230 |
+
for i, mcq in enumerate(mcqs_to_use):
|
| 231 |
+
result = {
|
| 232 |
+
"original_note_number": original_note_number,
|
| 233 |
+
"new_note_name": note_name,
|
| 234 |
+
"question": mcq.question,
|
| 235 |
+
"source_document": mcq.source_name,
|
| 236 |
+
"options": mcq.options,
|
| 237 |
+
"ideal_answer": mcq.options[ord(responses[i]["correct_answer"]) - ord('A')],
|
| 238 |
+
"correct_answer": responses[i]["correct_answer"],
|
| 239 |
+
"ai_answer": responses[i]["user_answer"],
|
| 240 |
+
"note_answer": responses[i]["user_answer"],
|
| 241 |
+
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 242 |
+
}
|
| 243 |
+
results.append(result)
|
| 244 |
+
|
| 245 |
+
return results
|
| 246 |
+
|
| 247 |
+
def main():
|
| 248 |
+
parser = argparse.ArgumentParser(description="Process CSV containing AI and modified notes.")
|
| 249 |
+
parser.add_argument("--modified_csv", required=True, help="Path to CSV with AI & modified notes")
|
| 250 |
+
parser.add_argument("--result_csv", default="results.csv", help="Output CSV file")
|
| 251 |
+
parser.add_argument("--start", type=int, default=0, help="Start original_note_number (inclusive)")
|
| 252 |
+
parser.add_argument("--end", type=int, default=10, help="End original_note_number (exclusive)")
|
| 253 |
+
parser.add_argument("--model", default="gpt-4o-mini", help="OpenAI model to use")
|
| 254 |
+
args = parser.parse_args()
|
| 255 |
+
|
| 256 |
+
print(f"\n=== MCQ EVALUATOR ===")
|
| 257 |
+
print(f"Reading from: {args.modified_csv}")
|
| 258 |
+
print(f"Writing results to: {args.result_csv}")
|
| 259 |
+
print(f"Processing original_note_number in [{args.start}, {args.end})")
|
| 260 |
+
print(f"Using model: {args.model}\n")
|
| 261 |
+
|
| 262 |
+
global llm
|
| 263 |
+
llm = ChatOpenAI(model=args.model, temperature=0)
|
| 264 |
+
|
| 265 |
+
if not os.path.exists(args.modified_csv):
|
| 266 |
+
print(f"ERROR: {args.modified_csv} not found.")
|
| 267 |
+
return
|
| 268 |
+
|
| 269 |
+
try:
|
| 270 |
+
print("Loading CSV file...")
|
| 271 |
+
df = pd.read_csv(args.modified_csv)
|
| 272 |
+
print(f"Loaded {len(df)} rows")
|
| 273 |
+
except Exception as e:
|
| 274 |
+
print(f"ERROR reading {args.modified_csv}: {e}")
|
| 275 |
+
return
|
| 276 |
+
|
| 277 |
+
needed_cols = {"original_note_number", "new_note_name", "modified_text"}
|
| 278 |
+
if not needed_cols.issubset(df.columns):
|
| 279 |
+
print(f"ERROR: Missing columns in {args.modified_csv}. We need {needed_cols}.")
|
| 280 |
+
return
|
| 281 |
+
|
| 282 |
+
df_in_range = df[(df["original_note_number"] >= args.start) &
|
| 283 |
+
(df["original_note_number"] < args.end)]
|
| 284 |
+
if df_in_range.empty:
|
| 285 |
+
print("No rows found in the specified range.")
|
| 286 |
+
return
|
| 287 |
+
|
| 288 |
+
print(f"Found {len(df_in_range)} rows in specified range")
|
| 289 |
+
|
| 290 |
+
results = []
|
| 291 |
+
total_tokens = [0]
|
| 292 |
+
grouped = df_in_range.groupby("original_note_number")
|
| 293 |
+
|
| 294 |
+
for onum, group in tqdm(grouped, desc="Processing notes"):
|
| 295 |
+
print(f"\n\nProcessing original_note_number {onum}")
|
| 296 |
+
|
| 297 |
+
# Get AI note and generate MCQs once per group
|
| 298 |
+
ai_row = group[group["new_note_name"] == "AI"]
|
| 299 |
+
if ai_row.empty:
|
| 300 |
+
print(f"Warning: No AI note found for original_note_number={onum}, skipping.")
|
| 301 |
+
continue
|
| 302 |
+
|
| 303 |
+
ai_text = ai_row.iloc[0]["modified_text"]
|
| 304 |
+
print("Generating MCQs for AI note...")
|
| 305 |
+
mcqs_ai = generate_mcqs_for_note(
|
| 306 |
+
note_content=ai_text,
|
| 307 |
+
total_tokens=total_tokens,
|
| 308 |
+
source_name='AI',
|
| 309 |
+
document_type='discharge_note'
|
| 310 |
+
)
|
| 311 |
+
print(f"Generated {len(mcqs_ai)} MCQs from AI note")
|
| 312 |
+
|
| 313 |
+
# Process ALL other notes (including original)
|
| 314 |
+
print("\nProcessing comparisons...")
|
| 315 |
+
other_rows = group[group["new_note_name"] != "AI"]
|
| 316 |
+
|
| 317 |
+
for idx, row in other_rows.iterrows():
|
| 318 |
+
note_name = row["new_note_name"]
|
| 319 |
+
print(f"\nProcessing comparison with {note_name}")
|
| 320 |
+
note_text = row["modified_text"]
|
| 321 |
+
|
| 322 |
+
result = run_evaluation(
|
| 323 |
+
ai_content=ai_text,
|
| 324 |
+
ai_mcqs=mcqs_ai,
|
| 325 |
+
note_content=note_text,
|
| 326 |
+
note_mcqs=mcqs_ai,
|
| 327 |
+
note_name=note_name,
|
| 328 |
+
original_note_number=onum,
|
| 329 |
+
total_tokens=total_tokens,
|
| 330 |
+
document_type='discharge_note'
|
| 331 |
+
)
|
| 332 |
+
results.extend(result)
|
| 333 |
+
|
| 334 |
+
file_exists = os.path.exists(args.result_csv)
|
| 335 |
+
mode = 'a' if file_exists else 'w'
|
| 336 |
+
|
| 337 |
+
fieldnames = ["original_note_number", "new_note_name", "question", "source_document",
|
| 338 |
+
"options", "ideal_answer", "correct_answer", "ai_answer", "note_answer",
|
| 339 |
+
"timestamp", "total_tokens"]
|
| 340 |
+
|
| 341 |
+
with open(args.result_csv, mode, newline='', encoding='utf-8') as csvfile:
|
| 342 |
+
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
| 343 |
+
if not file_exists:
|
| 344 |
+
writer.writeheader()
|
| 345 |
+
|
| 346 |
+
# Fix: Modify how we handle the results
|
| 347 |
+
for result in results: # results is already a list of dictionaries
|
| 348 |
+
result_dict = dict(result) # Create a copy of the result dictionary
|
| 349 |
+
result_dict["total_tokens"] = total_tokens[0] # Add token count
|
| 350 |
+
writer.writerow(result_dict)
|
| 351 |
+
|
| 352 |
+
print(f"\nResults written to {args.result_csv}")
|
| 353 |
+
print(f"Total tokens used: {total_tokens[0]}")
|
| 354 |
+
print("=== Done ===")
|
| 355 |
+
|
| 356 |
+
if __name__ == "__main__":
|
| 357 |
+
main()
|
| 358 |
+
|
| 359 |
+
#python dvd_evaluator.py --modified_csv "modified_notes/modified_notes_4o-mini_0_to_10.csv" --result_csv "results_4o_mini_0to10.csv" --start 0 --end 10 --model "gpt-4o-mini"
|
note_criteria.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"note_types": {
|
| 3 |
+
"discharge_note": {
|
| 4 |
+
"name": "Discharge Note",
|
| 5 |
+
"relevancy_criteria": [
|
| 6 |
+
"Hospital Admission and Discharge Details",
|
| 7 |
+
"Reason for Hospitalization",
|
| 8 |
+
"Hospital Course Summary",
|
| 9 |
+
"Discharge Diagnosis",
|
| 10 |
+
"Procedures Performed",
|
| 11 |
+
"Imaging studies",
|
| 12 |
+
"Medications at Discharge",
|
| 13 |
+
"Discharge Instructions",
|
| 14 |
+
"Follow-Up Care",
|
| 15 |
+
"Patient's Condition at Discharge",
|
| 16 |
+
"Patient Education and Counseling",
|
| 17 |
+
"Pending Results",
|
| 18 |
+
"Advance Directives and Legal Considerations",
|
| 19 |
+
"Important Abnormal (not normal)lab results, e.g. bacterial cultures, urine cultures, electrolyte disturbances, etc.",
|
| 20 |
+
"Important abnormal vital signs, e.g. fever, tachycardia, hypotension, etc.",
|
| 21 |
+
"Admission to ICU",
|
| 22 |
+
"comorbidities, e.g. diabetes, hypertension, etc.",
|
| 23 |
+
"Equipment needed at discharge, e.g. wheelchair, crutches, etc.",
|
| 24 |
+
"Prosthetics and tubes, e.g. Foley catheter, etc.",
|
| 25 |
+
"Allergies",
|
| 26 |
+
"Consultations (e.g., specialty or ancillary services)",
|
| 27 |
+
"Functional Capacity (ADLs and mobility status)",
|
| 28 |
+
"Lifestyle Modifications (diet, exercise, smoking cessation, etc.)",
|
| 29 |
+
"Wound Care or Other Specific Care Instructions"
|
| 30 |
+
]
|
| 31 |
+
},
|
| 32 |
+
"admission_note": {
|
| 33 |
+
"name": "Admission Note",
|
| 34 |
+
"relevancy_criteria": [
|
| 35 |
+
"Patient Demographics and Identification",
|
| 36 |
+
"Chief Complaint",
|
| 37 |
+
"History of Present Illness",
|
| 38 |
+
"Past Medical History",
|
| 39 |
+
"Past Surgical History",
|
| 40 |
+
"Current Medications",
|
| 41 |
+
"Allergies",
|
| 42 |
+
"Social History (including smoking, alcohol, drugs)",
|
| 43 |
+
"Family History",
|
| 44 |
+
"Review of Systems",
|
| 45 |
+
"Physical Examination Findings",
|
| 46 |
+
"Vital Signs on Admission",
|
| 47 |
+
"Initial Laboratory Results",
|
| 48 |
+
"Initial Imaging Results",
|
| 49 |
+
"Initial Assessment/Impression",
|
| 50 |
+
"Differential Diagnosis",
|
| 51 |
+
"Initial Treatment Plan",
|
| 52 |
+
"Admission Orders",
|
| 53 |
+
"Code Status and Advance Directives",
|
| 54 |
+
"Consultations Requested",
|
| 55 |
+
"Anticipated Course of Stay",
|
| 56 |
+
"Functional Status on Admission",
|
| 57 |
+
"Mental Status Assessment",
|
| 58 |
+
"Pain Assessment",
|
| 59 |
+
"Admission Precautions (isolation, fall risk, etc.)"
|
| 60 |
+
]
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
}
|
readme.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Document vs Document (DVD) Evaluator
|
| 2 |
+
|
| 3 |
+
This tool evaluates and compares the information content between two medical documents (e.g., admission notes, discharge summaries) using Multiple Choice Questions (MCQs) generated by GPT-4o-mini or GPT-4o. It helps to compare the content of two documents and generate a score for each document. Ideally, it is used to compare the content of an AI generated document vs a human generated document. The DVD score is simply the percentage of correct answers of each docuemnt when answering the MCQs generated by the other document, hence the name Document vs Document (DVD).
|
| 4 |
+
|
| 5 |
+
The tool is not designed to check for hallucinations but it can hint to parts of the document that be checked for hallucinations. Hallucinations may manifest as wrong answers by document 1 (which means there is new information in document 2, that has to be validated by checking the source of the note; e.g. admission note when writing a discharge summary). The other manifestation of hallucinations could be wrong answers by document 2 (not answering I don't know). Currently, careful human evaluation is still needed to check for hallucinations.
|
| 6 |
+
|
| 7 |
+
## 🚀 Features
|
| 8 |
+
|
| 9 |
+
- Generate MCQs based on medical document content
|
| 10 |
+
- Compare information preservation between documents
|
| 11 |
+
- Support for different note types (discharge notes, admission notes)
|
| 12 |
+
- Parallel processing for improved performance
|
| 13 |
+
- Detailed analysis with categorized results (correct, unknown, hallucinations)
|
| 14 |
+
- Interactive web interface for document comparison
|
| 15 |
+
- Configurable document type criteria via JSON
|
| 16 |
+
|
| 17 |
+
## 📋 Requirements
|
| 18 |
+
|
| 19 |
+
- Python 3.8+
|
| 20 |
+
- OpenAI API key
|
| 21 |
+
- Required Python packages (see requirements.txt)
|
| 22 |
+
|
| 23 |
+
## 🛠️ Installation
|
| 24 |
+
|
| 25 |
+
1. Clone the repository:
|
| 26 |
+
```bash
|
| 27 |
+
git clone https://huggingface.co/spaces/[your-username]/dvd-evaluator
|
| 28 |
+
cd dvd-evaluator
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
2. Install dependencies:
|
| 32 |
+
```bash
|
| 33 |
+
pip install -r requirements.txt
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
3. Set up your OpenAI API key:
|
| 37 |
+
```bash
|
| 38 |
+
export OPENAI_API_KEY='your-api-key-here'
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
## 📊 Usage
|
| 42 |
+
|
| 43 |
+
### Command Line Interface
|
| 44 |
+
|
| 45 |
+
1. Basic usage:
|
| 46 |
+
```bash
|
| 47 |
+
python dvd_evaluator.py \
|
| 48 |
+
--modified_csv "your_data.csv" \
|
| 49 |
+
--result_csv "results.csv" \
|
| 50 |
+
--start 0 \
|
| 51 |
+
--end 10 \
|
| 52 |
+
--model "gpt-4" \
|
| 53 |
+
--document_type "discharge_note"
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
2. Arguments:
|
| 57 |
+
- `--modified_csv`: Input CSV file containing the documents to compare
|
| 58 |
+
- `--result_csv`: Output file for results
|
| 59 |
+
- `--start`: Starting index for processing
|
| 60 |
+
- `--end`: Ending index for processing
|
| 61 |
+
- `--model`: OpenAI model to use
|
| 62 |
+
- `--document_type`: Type of medical note (discharge_note or admission_note)
|
| 63 |
+
- `--batch_size`: Number of documents to process in parallel
|
| 64 |
+
|
| 65 |
+
### Web Interface
|
| 66 |
+
|
| 67 |
+
1. Start the Flask server:
|
| 68 |
+
```bash
|
| 69 |
+
python app.py
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
2. Open your browser and navigate to `http://localhost:5000`
|
| 73 |
+
|
| 74 |
+
3. Upload two documents and click "Compare Documents"
|
| 75 |
+
|
| 76 |
+
## 📁 Repository Structure
|
| 77 |
+
|
| 78 |
+
```
|
| 79 |
+
dvd-evaluator/
|
| 80 |
+
├── app.py # Flask web application
|
| 81 |
+
├── dvd_evaluator.py # Main evaluation script
|
| 82 |
+
├── note_criteria.json # Document type criteria
|
| 83 |
+
├── requirements.txt # Python dependencies
|
| 84 |
+
├── templates/
|
| 85 |
+
│ └── index.html # Web interface template
|
| 86 |
+
└── README.md # This file
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
## 📝 Input Format
|
| 90 |
+
|
| 91 |
+
The input CSV should have the following columns:
|
| 92 |
+
- `original_note_number`: Unique identifier for the note pair
|
| 93 |
+
- `new_note_name`: Name/identifier for each document
|
| 94 |
+
- `modified_text`: The document text content
|
| 95 |
+
|
| 96 |
+
## 🔍 Output Format
|
| 97 |
+
|
| 98 |
+
The tool generates a CSV file with:
|
| 99 |
+
- Document scores
|
| 100 |
+
- Question-answer pairs
|
| 101 |
+
- Correct/incorrect responses
|
| 102 |
+
- Potential hallucinations
|
| 103 |
+
- Token usage statistics
|
| 104 |
+
|
| 105 |
+
## ⚙️ Customization
|
| 106 |
+
|
| 107 |
+
You can customize document type criteria by modifying `note_criteria.json`:
|
| 108 |
+
|
| 109 |
+
```json
|
| 110 |
+
{
|
| 111 |
+
"note_types": {
|
| 112 |
+
"discharge_note": {
|
| 113 |
+
"name": "Discharge Note",
|
| 114 |
+
"relevancy_criteria": [
|
| 115 |
+
"Hospital Admission and Discharge Details",
|
| 116 |
+
"Reason for Hospitalization",
|
| 117 |
+
...
|
| 118 |
+
]
|
| 119 |
+
},
|
| 120 |
+
...
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
## 🤝 Contributing
|
| 126 |
+
|
| 127 |
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
| 128 |
+
|
| 129 |
+
## 📜 License
|
| 130 |
+
|
| 131 |
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
| 132 |
+
|
| 133 |
+
## 🙏 Acknowledgments
|
| 134 |
+
|
| 135 |
+
- OpenAI for GPT-4 API
|
| 136 |
+
- Anthropic for development support
|
| 137 |
+
- Medical professionals for domain expertise
|
| 138 |
+
|
| 139 |
+
## 📧 Contact
|
| 140 |
+
|
| 141 |
+
For questions or feedback, please open an issue on the repository.
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask==3.1.0
|
| 2 |
+
pandas==2.2.3
|
| 3 |
+
werkzeug==3.1.3
|
| 4 |
+
pydantic==2.10.4
|
| 5 |
+
langchain-openai==0.2.14
|
| 6 |
+
langchain-core==0.3.28
|
| 7 |
+
tiktoken==0.8.0
|
| 8 |
+
python-dotenv==1.0.1
|
| 9 |
+
tqdm==4.67.1
|
| 10 |
+
openai==1.58.1
|
| 11 |
+
werkzeug>=2.0.0
|
templates/index.html
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Document vs. Document Evaluator</title>
|
| 7 |
+
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
| 8 |
+
</head>
|
| 9 |
+
<body class="bg-gray-100 min-h-screen">
|
| 10 |
+
<div class="container mx-auto px-4 py-8 max-w-7xl">
|
| 11 |
+
<!-- Header -->
|
| 12 |
+
<header class="mb-8">
|
| 13 |
+
<h1 class="text-4xl font-bold text-gray-800">Document vs. Document Evaluator</h1>
|
| 14 |
+
<p class="mt-2 text-gray-600">Compare and analyze two documents for content similarity</p>
|
| 15 |
+
</header>
|
| 16 |
+
|
| 17 |
+
<!-- Main Content -->
|
| 18 |
+
<main>
|
| 19 |
+
<!-- Upload Form -->
|
| 20 |
+
<section class="bg-white rounded-lg shadow-md p-6 mb-8">
|
| 21 |
+
<!-- API Key Input -->
|
| 22 |
+
<div class="mb-6">
|
| 23 |
+
<label class="block text-sm font-medium text-gray-700 mb-2">
|
| 24 |
+
OpenAI API Key <span class="text-red-500">*</span>
|
| 25 |
+
</label>
|
| 26 |
+
<input type="password"
|
| 27 |
+
id="apiKey"
|
| 28 |
+
class="w-full border rounded-md px-3 py-2"
|
| 29 |
+
placeholder="Enter your OpenAI API key"
|
| 30 |
+
required>
|
| 31 |
+
</div>
|
| 32 |
+
|
| 33 |
+
<form id="uploadForm" class="space-y-6">
|
| 34 |
+
<div class="grid md:grid-cols-2 gap-6">
|
| 35 |
+
<!-- Document 1 Upload -->
|
| 36 |
+
<div>
|
| 37 |
+
<label class="block text-sm font-medium text-gray-700 mb-2">
|
| 38 |
+
Document 1 <span class="text-red-500">*</span>
|
| 39 |
+
</label>
|
| 40 |
+
<input type="file"
|
| 41 |
+
name="doc1"
|
| 42 |
+
id="doc1"
|
| 43 |
+
accept=".txt"
|
| 44 |
+
required
|
| 45 |
+
class="w-full border rounded-md px-3 py-2">
|
| 46 |
+
<p id="doc1Name" class="mt-2 text-sm text-gray-500"></p>
|
| 47 |
+
</div>
|
| 48 |
+
|
| 49 |
+
<!-- Document 2 Upload -->
|
| 50 |
+
<div>
|
| 51 |
+
<label class="block text-sm font-medium text-gray-700 mb-2">
|
| 52 |
+
Document 2 <span class="text-red-500">*</span>
|
| 53 |
+
</label>
|
| 54 |
+
<input type="file"
|
| 55 |
+
name="doc2"
|
| 56 |
+
id="doc2"
|
| 57 |
+
accept=".txt"
|
| 58 |
+
required
|
| 59 |
+
class="w-full border rounded-md px-3 py-2">
|
| 60 |
+
<p id="doc2Name" class="mt-2 text-sm text-gray-500"></p>
|
| 61 |
+
</div>
|
| 62 |
+
</div>
|
| 63 |
+
|
| 64 |
+
<!-- Model Selection -->
|
| 65 |
+
<div>
|
| 66 |
+
<label class="block text-sm font-medium text-gray-700 mb-2">Model</label>
|
| 67 |
+
<select name="model" id="model" class="w-full border rounded-md px-3 py-2">
|
| 68 |
+
<option value="gpt-4o-mini">GPT-4o-mini</option>
|
| 69 |
+
<option value="gpt-4o">GPT-4o</option>
|
| 70 |
+
</select>
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
+
<!-- Document Type Selection -->
|
| 74 |
+
<div>
|
| 75 |
+
<label class="block text-sm font-medium text-gray-700 mb-2">Document Type</label>
|
| 76 |
+
<select name="document_type" id="documentType" class="w-full border rounded-md px-3 py-2">
|
| 77 |
+
<option value="discharge_note">Discharge Note</option>
|
| 78 |
+
<option value="admission_note">Admission Note</option>
|
| 79 |
+
</select>
|
| 80 |
+
</div>
|
| 81 |
+
|
| 82 |
+
<!-- Submit Button -->
|
| 83 |
+
<button type="submit"
|
| 84 |
+
class="w-full bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors">
|
| 85 |
+
Compare Documents
|
| 86 |
+
</button>
|
| 87 |
+
</form>
|
| 88 |
+
</section>
|
| 89 |
+
|
| 90 |
+
<!-- Loading Overlay -->
|
| 91 |
+
<div id="loading" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
| 92 |
+
<div class="bg-white p-8 rounded-lg shadow-xl text-center max-w-md mx-4">
|
| 93 |
+
<div class="animate-spin rounded-full h-16 w-16 border-b-4 border-blue-600 mx-auto"></div>
|
| 94 |
+
<p class="mt-4 text-lg">Processing documents...<br>This may take a few minutes</p>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
|
| 98 |
+
<!-- Results Section -->
|
| 99 |
+
<div id="results" class="hidden space-y-8">
|
| 100 |
+
<!-- Summary Stats -->
|
| 101 |
+
<section class="bg-white rounded-lg shadow-md p-6">
|
| 102 |
+
<h2 class="text-2xl font-bold mb-4">Summary Statistics</h2>
|
| 103 |
+
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
|
| 104 |
+
<div class="p-4 bg-gray-50 rounded-md">
|
| 105 |
+
<div class="text-sm text-gray-500">Total Tokens Used</div>
|
| 106 |
+
<div id="totalTokens" class="text-xl font-semibold">-</div>
|
| 107 |
+
</div>
|
| 108 |
+
<div class="p-4 bg-gray-50 rounded-md">
|
| 109 |
+
<div class="text-sm text-gray-500">DVD Ratio</div>
|
| 110 |
+
<div id="dvdRatio" class="text-xl font-semibold">-</div>
|
| 111 |
+
</div>
|
| 112 |
+
</div>
|
| 113 |
+
</section>
|
| 114 |
+
|
| 115 |
+
<!-- Document Results -->
|
| 116 |
+
<div class="grid md:grid-cols-2 gap-8">
|
| 117 |
+
<!-- Document 1 Results -->
|
| 118 |
+
<section class="bg-white rounded-lg shadow-md p-6">
|
| 119 |
+
<h2 class="text-2xl font-bold mb-4">Document 1 Analysis</h2>
|
| 120 |
+
<div id="doc1Results">
|
| 121 |
+
<div class="mb-4">
|
| 122 |
+
<h3 class="font-semibold">Score:</h3>
|
| 123 |
+
<p id="doc1Score" class="text-lg">-</p>
|
| 124 |
+
</div>
|
| 125 |
+
<div id="doc1Questions" class="space-y-4"></div>
|
| 126 |
+
</div>
|
| 127 |
+
</section>
|
| 128 |
+
|
| 129 |
+
<!-- Document 2 Results -->
|
| 130 |
+
<section class="bg-white rounded-lg shadow-md p-6">
|
| 131 |
+
<h2 class="text-2xl font-bold mb-4">Document 2 Analysis</h2>
|
| 132 |
+
<div id="doc2Results">
|
| 133 |
+
<div class="mb-4">
|
| 134 |
+
<h3 class="font-semibold">Score:</h3>
|
| 135 |
+
<p id="doc2Score" class="text-lg">-</p>
|
| 136 |
+
</div>
|
| 137 |
+
<div id="doc2Questions" class="space-y-4"></div>
|
| 138 |
+
</div>
|
| 139 |
+
</section>
|
| 140 |
+
</div>
|
| 141 |
+
|
| 142 |
+
<!-- Original Documents -->
|
| 143 |
+
<section class="grid md:grid-cols-2 gap-8">
|
| 144 |
+
<div class="bg-white rounded-lg shadow-md p-6">
|
| 145 |
+
<h2 class="text-2xl font-bold mb-4">Document 1 Text</h2>
|
| 146 |
+
<pre id="doc1Text" class="whitespace-pre-wrap text-sm bg-gray-50 p-4 rounded-md overflow-auto max-h-96"></pre>
|
| 147 |
+
</div>
|
| 148 |
+
<div class="bg-white rounded-lg shadow-md p-6">
|
| 149 |
+
<h2 class="text-2xl font-bold mb-4">Document 2 Text</h2>
|
| 150 |
+
<pre id="doc2Text" class="whitespace-pre-wrap text-sm bg-gray-50 p-4 rounded-md overflow-auto max-h-96"></pre>
|
| 151 |
+
</div>
|
| 152 |
+
</section>
|
| 153 |
+
</div>
|
| 154 |
+
</main>
|
| 155 |
+
</div>
|
| 156 |
+
|
| 157 |
+
<script>
|
| 158 |
+
// Utility functions
|
| 159 |
+
const utils = {
|
| 160 |
+
safeGetElement: (id) => document.getElementById(id),
|
| 161 |
+
|
| 162 |
+
safeUpdateElement: (id, value) => {
|
| 163 |
+
const element = document.getElementById(id);
|
| 164 |
+
if (element) element.textContent = value;
|
| 165 |
+
},
|
| 166 |
+
|
| 167 |
+
calculateScore: (analysis) => {
|
| 168 |
+
if (!analysis?.score) return { score: 0, percentage: 0 };
|
| 169 |
+
const [correct, total] = analysis.score.split('/').map(Number);
|
| 170 |
+
return {
|
| 171 |
+
score: analysis.score,
|
| 172 |
+
percentage: total > 0 ? (correct / total) * 100 : 0
|
| 173 |
+
};
|
| 174 |
+
},
|
| 175 |
+
|
| 176 |
+
renderQuestion: (question, container) => {
|
| 177 |
+
const questionDiv = document.createElement('div');
|
| 178 |
+
questionDiv.className = 'p-4 bg-gray-50 rounded-lg';
|
| 179 |
+
|
| 180 |
+
// Question text and status
|
| 181 |
+
const questionText = document.createElement('div');
|
| 182 |
+
questionText.className = 'mb-3';
|
| 183 |
+
const isCorrect = question.model_answer === question.ideal_answer;
|
| 184 |
+
questionText.innerHTML = `
|
| 185 |
+
<span class="font-medium">${question.question}</span>
|
| 186 |
+
<span class="ml-2 ${isCorrect ? 'text-green-600' : 'text-red-600'}">
|
| 187 |
+
${isCorrect ? '✅' : '❌'}
|
| 188 |
+
</span>
|
| 189 |
+
`;
|
| 190 |
+
questionDiv.appendChild(questionText);
|
| 191 |
+
|
| 192 |
+
// Options
|
| 193 |
+
const optionsDiv = document.createElement('div');
|
| 194 |
+
optionsDiv.className = 'space-y-2 ml-4';
|
| 195 |
+
|
| 196 |
+
question.options.forEach((option, idx) => {
|
| 197 |
+
const isCorrectAnswer = option === question.ideal_answer;
|
| 198 |
+
const isSelectedAnswer = option === question.model_answer;
|
| 199 |
+
const optionElement = document.createElement('div');
|
| 200 |
+
optionElement.className = [
|
| 201 |
+
isCorrectAnswer ? 'font-bold text-green-700' : '',
|
| 202 |
+
isSelectedAnswer && !isCorrectAnswer ? 'text-red-600' : ''
|
| 203 |
+
].join(' ').trim();
|
| 204 |
+
|
| 205 |
+
const letter = String.fromCharCode(65 + idx); // A, B, C, D, E
|
| 206 |
+
optionElement.textContent = `${letter}. ${option}`;
|
| 207 |
+
|
| 208 |
+
if (isCorrectAnswer) {
|
| 209 |
+
const correctLabel = document.createElement('span');
|
| 210 |
+
correctLabel.className = 'ml-2 text-sm';
|
| 211 |
+
correctLabel.textContent = '(Correct Answer)';
|
| 212 |
+
optionElement.appendChild(correctLabel);
|
| 213 |
+
}
|
| 214 |
+
if (isSelectedAnswer && !isCorrectAnswer) {
|
| 215 |
+
const selectedLabel = document.createElement('span');
|
| 216 |
+
selectedLabel.className = 'ml-2 text-sm';
|
| 217 |
+
selectedLabel.textContent = '(Selected Answer)';
|
| 218 |
+
optionElement.appendChild(selectedLabel);
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
optionsDiv.appendChild(optionElement);
|
| 222 |
+
});
|
| 223 |
+
|
| 224 |
+
questionDiv.appendChild(optionsDiv);
|
| 225 |
+
container.appendChild(questionDiv);
|
| 226 |
+
},
|
| 227 |
+
|
| 228 |
+
displayResults: (docId, analysis) => {
|
| 229 |
+
// Update score
|
| 230 |
+
const score = utils.calculateScore(analysis);
|
| 231 |
+
utils.safeUpdateElement(`${docId}Score`,
|
| 232 |
+
`${score.score} (${score.percentage.toFixed(1)}%)`);
|
| 233 |
+
|
| 234 |
+
// Clear and update questions
|
| 235 |
+
const questionsContainer = utils.safeGetElement(`${docId}Questions`);
|
| 236 |
+
if (questionsContainer) {
|
| 237 |
+
questionsContainer.innerHTML = '';
|
| 238 |
+
|
| 239 |
+
// Combine all questions
|
| 240 |
+
const allQuestions = [
|
| 241 |
+
...(analysis.attempted_answers || []),
|
| 242 |
+
...(analysis.unknown_answers || []).map(q => ({
|
| 243 |
+
...q,
|
| 244 |
+
model_answer: "I don't know"
|
| 245 |
+
}))
|
| 246 |
+
];
|
| 247 |
+
|
| 248 |
+
// Render all questions
|
| 249 |
+
allQuestions.forEach(question => {
|
| 250 |
+
utils.renderQuestion(question, questionsContainer);
|
| 251 |
+
});
|
| 252 |
+
}
|
| 253 |
+
}
|
| 254 |
+
};
|
| 255 |
+
|
| 256 |
+
// Form manager
|
| 257 |
+
const formManager = {
|
| 258 |
+
initializeFileInputs: () => {
|
| 259 |
+
['doc1', 'doc2'].forEach(id => {
|
| 260 |
+
const input = utils.safeGetElement(id);
|
| 261 |
+
const nameDisplay = utils.safeGetElement(`${id}Name`);
|
| 262 |
+
|
| 263 |
+
if (input && nameDisplay) {
|
| 264 |
+
input.addEventListener('change', (e) => {
|
| 265 |
+
const fileName = e.target.files[0]?.name || 'No file selected';
|
| 266 |
+
nameDisplay.textContent = `Selected: ${fileName}`;
|
| 267 |
+
});
|
| 268 |
+
}
|
| 269 |
+
});
|
| 270 |
+
},
|
| 271 |
+
|
| 272 |
+
validateForm: () => {
|
| 273 |
+
const apiKey = utils.safeGetElement('apiKey')?.value;
|
| 274 |
+
if (!apiKey) {
|
| 275 |
+
throw new Error('Please enter your OpenAI API key');
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
const doc1 = utils.safeGetElement('doc1')?.files[0];
|
| 279 |
+
const doc2 = utils.safeGetElement('doc2')?.files[0];
|
| 280 |
+
if (!doc1 || !doc2) {
|
| 281 |
+
throw new Error('Please select both documents');
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
if (!doc1.name.toLowerCase().endsWith('.txt') || !doc2.name.toLowerCase().endsWith('.txt')) {
|
| 285 |
+
throw new Error('Only .txt files are allowed');
|
| 286 |
+
}
|
| 287 |
+
},
|
| 288 |
+
|
| 289 |
+
handleSubmit: async (e) => {
|
| 290 |
+
e.preventDefault();
|
| 291 |
+
|
| 292 |
+
try {
|
| 293 |
+
formManager.validateForm();
|
| 294 |
+
|
| 295 |
+
const loading = utils.safeGetElement('loading');
|
| 296 |
+
const results = utils.safeGetElement('results');
|
| 297 |
+
|
| 298 |
+
loading.classList.remove('hidden');
|
| 299 |
+
results.classList.add('hidden');
|
| 300 |
+
|
| 301 |
+
const formData = new FormData();
|
| 302 |
+
formData.append('api_key', utils.safeGetElement('apiKey').value);
|
| 303 |
+
formData.append('doc1', utils.safeGetElement('doc1').files[0]);
|
| 304 |
+
formData.append('doc2', utils.safeGetElement('doc2').files[0]);
|
| 305 |
+
formData.append('model', utils.safeGetElement('model').value);
|
| 306 |
+
formData.append('document_type', utils.safeGetElement('documentType').value);
|
| 307 |
+
|
| 308 |
+
const response = await fetch('/compare', {
|
| 309 |
+
method: 'POST',
|
| 310 |
+
body: formData
|
| 311 |
+
});
|
| 312 |
+
|
| 313 |
+
const data = await response.json();
|
| 314 |
+
if (!response.ok) {
|
| 315 |
+
throw new Error(data.error || 'An error occurred');
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
// Update summary statistics
|
| 319 |
+
utils.safeUpdateElement('totalTokens', data.total_tokens);
|
| 320 |
+
|
| 321 |
+
const doc1Score = utils.calculateScore(data.doc1_analysis);
|
| 322 |
+
const doc2Score = utils.calculateScore(data.doc2_analysis);
|
| 323 |
+
const dvdRatio = doc1Score.percentage > 0 ?
|
| 324 |
+
(doc2Score.percentage / doc1Score.percentage).toFixed(2) : 'N/A';
|
| 325 |
+
utils.safeUpdateElement('dvdRatio', dvdRatio);
|
| 326 |
+
|
| 327 |
+
// Update document texts
|
| 328 |
+
utils.safeUpdateElement('doc1Text', data.doc1_content || '');
|
| 329 |
+
utils.safeUpdateElement('doc2Text', data.doc2_content || '');
|
| 330 |
+
|
| 331 |
+
// Display results for both documents
|
| 332 |
+
utils.displayResults('doc1', data.doc1_analysis);
|
| 333 |
+
utils.displayResults('doc2', data.doc2_analysis);
|
| 334 |
+
|
| 335 |
+
results.classList.remove('hidden');
|
| 336 |
+
} catch (error) {
|
| 337 |
+
console.error('Error:', error);
|
| 338 |
+
alert(error.message || 'An error occurred while processing the documents');
|
| 339 |
+
} finally {
|
| 340 |
+
loading.classList.add('hidden');
|
| 341 |
+
}
|
| 342 |
+
}
|
| 343 |
+
};
|
| 344 |
+
|
| 345 |
+
// Initialize application
|
| 346 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 347 |
+
formManager.initializeFileInputs();
|
| 348 |
+
|
| 349 |
+
const form = utils.safeGetElement('uploadForm');
|
| 350 |
+
if (form) {
|
| 351 |
+
form.addEventListener('submit', formManager.handleSubmit);
|
| 352 |
+
}
|
| 353 |
+
});
|
| 354 |
+
</script>
|
| 355 |
+
</body>
|
| 356 |
+
</html>
|