iyadsultan commited on
Commit
d7e8f11
·
1 Parent(s): 95722da

Enhance project configuration: Updated .gitignore to include more file types, modified Dockerfile for improved structure and environment variable handling, and revised README.md to provide comprehensive project details and usage instructions. The application now consistently listens on port 7860 across all components.

Browse files
Files changed (8) hide show
  1. .gitignore +43 -5
  2. .gradio/certificate.pem +31 -0
  3. Dockerfile +14 -5
  4. README.md +54 -12
  5. app.py +0 -1
  6. combine.py +71 -0
  7. combined.py +1411 -0
  8. dvd_evaluator.py +2 -1
.gitignore CHANGED
@@ -1,8 +1,46 @@
1
- *.pyc
2
  __pycache__/
3
- .env
4
- env/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  venv/
6
- .venv/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  .DS_Store
8
- *.log
 
1
+ # Python
2
  __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual Environment
24
  venv/
25
+ ENV/
26
+ env/
27
+
28
+ # IDE
29
+ .idea/
30
+ .vscode/
31
+ *.swp
32
+ *.swo
33
+
34
+ # Environment variables
35
+ .env
36
+
37
+ # Logs
38
+ *.log
39
+
40
+ # Temporary files
41
+ tmp/
42
+ temp/
43
+
44
+ # System Files
45
  .DS_Store
46
+ Thumbs.db
.gradio/certificate.pem ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
3
+ TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
4
+ cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
5
+ WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
6
+ ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
7
+ MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
8
+ h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
9
+ 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
10
+ A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
11
+ T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
12
+ B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
13
+ B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
14
+ KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
15
+ OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
16
+ jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
17
+ qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
18
+ rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
19
+ HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
20
+ hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
21
+ ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
22
+ 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
23
+ NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
24
+ ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
25
+ TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
26
+ jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
27
+ oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
28
+ 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
29
+ mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
30
+ emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
31
+ -----END CERTIFICATE-----
Dockerfile CHANGED
@@ -1,13 +1,22 @@
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
  EXPOSE 7860
11
 
12
- ENTRYPOINT ["python"]
13
- CMD ["app.py"]
 
1
  FROM python:3.10-slim
2
 
3
+ WORKDIR /code
4
 
5
+ # Install dependencies
6
  COPY requirements.txt .
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
 
9
+ # Copy necessary files
10
+ COPY app.py .
11
+ COPY dvd_evaluator.py .
12
+ COPY note_criteria.json .
13
+ COPY templates/ templates/
14
 
15
+ # Set environment variables
16
+ ENV PORT=7860
17
+
18
+ # Expose the port
19
  EXPOSE 7860
20
 
21
+ # Run the application
22
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,12 +1,54 @@
1
- ---
2
- title: Document vs Document Evaluator
3
- emoji: 📄
4
- colorFrom: blue
5
- colorTo: indigo
6
- sdk: static
7
- sdk_version: "3.1.0"
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Document vs Document Evaluator
2
+
3
+ A web application for comparing and analyzing medical documents using OpenAI's GPT models.
4
+
5
+ ## Features
6
+
7
+ - Upload and compare two text documents
8
+ - Generate Multiple Choice Questions (MCQs) for document analysis
9
+ - Compare document content and generate similarity scores
10
+ - Interactive web interface with real-time results
11
+ - Support for different document types (discharge notes, admission notes)
12
+
13
+ ## Requirements
14
+
15
+ - Python 3.10+
16
+ - OpenAI API key (user-provided)
17
+ - Dependencies listed in requirements.txt
18
+
19
+ ## Environment Variables
20
+
21
+ - `PORT`: Set automatically by Hugging Face Spaces (default: 7860)
22
+ - `OPENAI_API_KEY`: Provided by users through the interface
23
+
24
+ ## Setup
25
+
26
+ 1. Install dependencies:
27
+ ```bash
28
+ pip install -r requirements.txt
29
+ ```
30
+
31
+ 2. Run the application:
32
+ ```bash
33
+ python app.py
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ 1. Open the web interface
39
+ 2. Enter your OpenAI API key
40
+ 3. Upload two text documents for comparison
41
+ 4. Select the document type and model
42
+ 5. Click "Compare Documents" to start the analysis
43
+
44
+ ## File Structure
45
+
46
+ - `app.py`: Main Flask application
47
+ - `dvd_evaluator.py`: Core evaluation logic
48
+ - `note_criteria.json`: Evaluation criteria configuration
49
+ - `templates/`: HTML templates
50
+ - `requirements.txt`: Python dependencies
51
+
52
+ ## License
53
+
54
+ MIT
app.py CHANGED
@@ -467,6 +467,5 @@ def index():
467
  return render_template('index.html', models=MODELS)
468
 
469
  if __name__ == "__main__":
470
- # Get port from environment variable for Hugging Face Spaces
471
  port = int(os.environ.get("PORT", 7860))
472
  app.run(host="0.0.0.0", port=port)
 
467
  return render_template('index.html', models=MODELS)
468
 
469
  if __name__ == "__main__":
 
470
  port = int(os.environ.get("PORT", 7860))
471
  app.run(host="0.0.0.0", port=port)
combine.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ def get_files_recursively(directory, extensions):
4
+ """
5
+ Recursively get all files with specified extensions from directory and subdirectories,
6
+ excluding combined.py and venv folder.
7
+ """
8
+ file_list = []
9
+ for root, dirs, files in os.walk(directory):
10
+ # Exclude the 'venv' directory from the search
11
+ if 'venv' in root:
12
+ continue
13
+ for file in files:
14
+ # Exclude the 'combined.py' file from the results
15
+ if file == 'combined.py':
16
+ continue
17
+ if any(file.endswith(ext) for ext in extensions):
18
+ file_list.append(os.path.join(root, file))
19
+ return file_list
20
+
21
+ def combine_files(output_file, file_list):
22
+ """
23
+ Combine contents of all files in file_list into output_file
24
+ """
25
+ with open(output_file, 'a', encoding='utf-8') as outfile:
26
+ for fname in file_list:
27
+ # Add a header comment to show which file's contents follow
28
+ outfile.write(f"\n\n# Contents from: {fname}\n")
29
+ try:
30
+ with open(fname, 'r', encoding='utf-8') as infile:
31
+ for line in infile:
32
+ outfile.write(line)
33
+ except Exception as e:
34
+ outfile.write(f"# Error reading file {fname}: {str(e)}\n")
35
+
36
+ def main():
37
+ # Define the base directory (current directory in this case)
38
+ base_directory = "."
39
+ output_file = 'combined.py'
40
+ extensions = ('.py','html', 'md', 'json') # Ensure this is a tuple
41
+
42
+ # Remove output file if it exists
43
+ if os.path.exists(output_file):
44
+ try:
45
+ os.remove(output_file)
46
+ except Exception as e:
47
+ print(f"Error removing existing {output_file}: {str(e)}")
48
+ return
49
+
50
+ # Get all files recursively
51
+ all_files = get_files_recursively(base_directory, extensions)
52
+
53
+ # Sort files by extension and then by name
54
+ all_files.sort(key=lambda x: (os.path.splitext(x)[1], x))
55
+
56
+ # Add a header to the output file
57
+ with open(output_file, 'w', encoding='utf-8') as outfile:
58
+ outfile.write("# Combined Python files\n")
59
+ outfile.write(f"# Generated from directory: {os.path.abspath(base_directory)}\n")
60
+ outfile.write(f"# Total files found: {len(all_files)}\n\n")
61
+
62
+ # Combine all files
63
+ combine_files(output_file, all_files)
64
+
65
+ print(f"Successfully combined {len(all_files)} files into {output_file}")
66
+ print("Files processed:")
67
+ for file in all_files:
68
+ print(f" - {file}")
69
+
70
+ if __name__ == "__main__":
71
+ main()
combined.py ADDED
@@ -0,0 +1,1411 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Combined Python files
2
+ # Generated from directory: C:\Users\USER\Documents\DVD_hf
3
+ # Total files found: 7
4
+
5
+
6
+
7
+ # Contents from: .\templates\index.html
8
+ <!DOCTYPE html>
9
+ <html lang="en">
10
+ <head>
11
+ <meta charset="UTF-8">
12
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
13
+ <title>Document vs. Document Evaluator</title>
14
+ <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
15
+ </head>
16
+ <body class="bg-gray-100 min-h-screen">
17
+ <div class="container mx-auto px-4 py-8 max-w-7xl">
18
+ <!-- Header -->
19
+ <header class="mb-8">
20
+ <h1 class="text-4xl font-bold text-gray-800">Document vs. Document Evaluator</h1>
21
+ <p class="mt-2 text-gray-600">Compare and analyze two documents for content similarity</p>
22
+ </header>
23
+
24
+ <!-- Main Content -->
25
+ <main>
26
+ <!-- Upload Form -->
27
+ <section class="bg-white rounded-lg shadow-md p-6 mb-8">
28
+ <!-- API Key Input -->
29
+ <div class="mb-6">
30
+ <label class="block text-sm font-medium text-gray-700 mb-2">
31
+ OpenAI API Key <span class="text-red-500">*</span>
32
+ </label>
33
+ <input type="password"
34
+ id="apiKey"
35
+ class="w-full border rounded-md px-3 py-2"
36
+ placeholder="Enter your OpenAI API key"
37
+ required>
38
+ </div>
39
+
40
+ <form id="uploadForm" class="space-y-6">
41
+ <div class="grid md:grid-cols-2 gap-6">
42
+ <!-- Document 1 Upload -->
43
+ <div>
44
+ <label class="block text-sm font-medium text-gray-700 mb-2">
45
+ Document 1 <span class="text-red-500">*</span>
46
+ </label>
47
+ <input type="file"
48
+ name="doc1"
49
+ id="doc1"
50
+ accept=".txt"
51
+ required
52
+ class="w-full border rounded-md px-3 py-2">
53
+ <p id="doc1Name" class="mt-2 text-sm text-gray-500"></p>
54
+ </div>
55
+
56
+ <!-- Document 2 Upload -->
57
+ <div>
58
+ <label class="block text-sm font-medium text-gray-700 mb-2">
59
+ Document 2 <span class="text-red-500">*</span>
60
+ </label>
61
+ <input type="file"
62
+ name="doc2"
63
+ id="doc2"
64
+ accept=".txt"
65
+ required
66
+ class="w-full border rounded-md px-3 py-2">
67
+ <p id="doc2Name" class="mt-2 text-sm text-gray-500"></p>
68
+ </div>
69
+ </div>
70
+
71
+ <!-- Model Selection -->
72
+ <div>
73
+ <label class="block text-sm font-medium text-gray-700 mb-2">Model</label>
74
+ <select name="model" id="model" class="w-full border rounded-md px-3 py-2">
75
+ <option value="gpt-4o-mini">GPT-4o-mini</option>
76
+ <option value="gpt-4o">GPT-4o</option>
77
+ </select>
78
+ </div>
79
+
80
+ <!-- Document Type Selection -->
81
+ <div>
82
+ <label class="block text-sm font-medium text-gray-700 mb-2">Document Type</label>
83
+ <select name="document_type" id="documentType" class="w-full border rounded-md px-3 py-2">
84
+ <option value="discharge_note">Discharge Note</option>
85
+ <option value="admission_note">Admission Note</option>
86
+ </select>
87
+ </div>
88
+
89
+ <!-- Submit Button -->
90
+ <button type="submit"
91
+ class="w-full bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors">
92
+ Compare Documents
93
+ </button>
94
+ </form>
95
+ </section>
96
+
97
+ <!-- Loading Overlay -->
98
+ <div id="loading" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
99
+ <div class="bg-white p-8 rounded-lg shadow-xl text-center max-w-md mx-4">
100
+ <div class="animate-spin rounded-full h-16 w-16 border-b-4 border-blue-600 mx-auto"></div>
101
+ <p class="mt-4 text-lg">Processing documents...<br>This may take a few minutes</p>
102
+ </div>
103
+ </div>
104
+
105
+ <!-- Results Section -->
106
+ <div id="results" class="hidden space-y-8">
107
+ <!-- Summary Stats -->
108
+ <section class="bg-white rounded-lg shadow-md p-6">
109
+ <h2 class="text-2xl font-bold mb-4">Summary Statistics</h2>
110
+ <div class="grid grid-cols-2 md:grid-cols-3 gap-4">
111
+ <div class="p-4 bg-gray-50 rounded-md">
112
+ <div class="text-sm text-gray-500">Total Tokens Used</div>
113
+ <div id="totalTokens" class="text-xl font-semibold">-</div>
114
+ </div>
115
+ <div class="p-4 bg-gray-50 rounded-md">
116
+ <div class="text-sm text-gray-500">DVD Ratio</div>
117
+ <div id="dvdRatio" class="text-xl font-semibold">-</div>
118
+ </div>
119
+ </div>
120
+ </section>
121
+
122
+ <!-- Document Results -->
123
+ <div class="grid md:grid-cols-2 gap-8">
124
+ <!-- Document 1 Results -->
125
+ <section class="bg-white rounded-lg shadow-md p-6">
126
+ <h2 class="text-2xl font-bold mb-4">Document 1 Analysis</h2>
127
+ <div id="doc1Results">
128
+ <div class="mb-4">
129
+ <h3 class="font-semibold">Score:</h3>
130
+ <p id="doc1Score" class="text-lg">-</p>
131
+ </div>
132
+ <div id="doc1Questions" class="space-y-4"></div>
133
+ </div>
134
+ </section>
135
+
136
+ <!-- Document 2 Results -->
137
+ <section class="bg-white rounded-lg shadow-md p-6">
138
+ <h2 class="text-2xl font-bold mb-4">Document 2 Analysis</h2>
139
+ <div id="doc2Results">
140
+ <div class="mb-4">
141
+ <h3 class="font-semibold">Score:</h3>
142
+ <p id="doc2Score" class="text-lg">-</p>
143
+ </div>
144
+ <div id="doc2Questions" class="space-y-4"></div>
145
+ </div>
146
+ </section>
147
+ </div>
148
+
149
+ <!-- Original Documents -->
150
+ <section class="grid md:grid-cols-2 gap-8">
151
+ <div class="bg-white rounded-lg shadow-md p-6">
152
+ <h2 class="text-2xl font-bold mb-4">Document 1 Text</h2>
153
+ <pre id="doc1Text" class="whitespace-pre-wrap text-sm bg-gray-50 p-4 rounded-md overflow-auto max-h-96"></pre>
154
+ </div>
155
+ <div class="bg-white rounded-lg shadow-md p-6">
156
+ <h2 class="text-2xl font-bold mb-4">Document 2 Text</h2>
157
+ <pre id="doc2Text" class="whitespace-pre-wrap text-sm bg-gray-50 p-4 rounded-md overflow-auto max-h-96"></pre>
158
+ </div>
159
+ </section>
160
+ </div>
161
+ </main>
162
+ </div>
163
+
164
+ <script>
165
+ // Utility functions
166
+ const utils = {
167
+ safeGetElement: (id) => document.getElementById(id),
168
+
169
+ safeUpdateElement: (id, value) => {
170
+ const element = document.getElementById(id);
171
+ if (element) element.textContent = value;
172
+ },
173
+
174
+ calculateScore: (analysis) => {
175
+ if (!analysis?.score) return { score: 0, percentage: 0 };
176
+ const [correct, total] = analysis.score.split('/').map(Number);
177
+ return {
178
+ score: analysis.score,
179
+ percentage: total > 0 ? (correct / total) * 100 : 0
180
+ };
181
+ },
182
+
183
+ renderQuestion: (question, container) => {
184
+ const questionDiv = document.createElement('div');
185
+ questionDiv.className = 'p-4 bg-gray-50 rounded-lg';
186
+
187
+ // Question text and status
188
+ const questionText = document.createElement('div');
189
+ questionText.className = 'mb-3';
190
+ const isCorrect = question.model_answer === question.ideal_answer;
191
+ questionText.innerHTML = `
192
+ <span class="font-medium">${question.question}</span>
193
+ <span class="ml-2 ${isCorrect ? 'text-green-600' : 'text-red-600'}">
194
+ ${isCorrect ? '✅' : '❌'}
195
+ </span>
196
+ `;
197
+ questionDiv.appendChild(questionText);
198
+
199
+ // Options
200
+ const optionsDiv = document.createElement('div');
201
+ optionsDiv.className = 'space-y-2 ml-4';
202
+
203
+ question.options.forEach((option, idx) => {
204
+ const isCorrectAnswer = option === question.ideal_answer;
205
+ const isSelectedAnswer = option === question.model_answer;
206
+ const optionElement = document.createElement('div');
207
+ optionElement.className = [
208
+ isCorrectAnswer ? 'font-bold text-green-700' : '',
209
+ isSelectedAnswer && !isCorrectAnswer ? 'text-red-600' : ''
210
+ ].join(' ').trim();
211
+
212
+ const letter = String.fromCharCode(65 + idx); // A, B, C, D, E
213
+ optionElement.textContent = `${letter}. ${option}`;
214
+
215
+ if (isCorrectAnswer) {
216
+ const correctLabel = document.createElement('span');
217
+ correctLabel.className = 'ml-2 text-sm';
218
+ correctLabel.textContent = '(Correct Answer)';
219
+ optionElement.appendChild(correctLabel);
220
+ }
221
+ if (isSelectedAnswer && !isCorrectAnswer) {
222
+ const selectedLabel = document.createElement('span');
223
+ selectedLabel.className = 'ml-2 text-sm';
224
+ selectedLabel.textContent = '(Selected Answer)';
225
+ optionElement.appendChild(selectedLabel);
226
+ }
227
+
228
+ optionsDiv.appendChild(optionElement);
229
+ });
230
+
231
+ questionDiv.appendChild(optionsDiv);
232
+ container.appendChild(questionDiv);
233
+ },
234
+
235
+ displayResults: (docId, analysis) => {
236
+ // Update score
237
+ const score = utils.calculateScore(analysis);
238
+ utils.safeUpdateElement(`${docId}Score`,
239
+ `${score.score} (${score.percentage.toFixed(1)}%)`);
240
+
241
+ // Clear and update questions
242
+ const questionsContainer = utils.safeGetElement(`${docId}Questions`);
243
+ if (questionsContainer) {
244
+ questionsContainer.innerHTML = '';
245
+
246
+ // Combine all questions
247
+ const allQuestions = [
248
+ ...(analysis.attempted_answers || []),
249
+ ...(analysis.unknown_answers || []).map(q => ({
250
+ ...q,
251
+ model_answer: "I don't know"
252
+ }))
253
+ ];
254
+
255
+ // Render all questions
256
+ allQuestions.forEach(question => {
257
+ utils.renderQuestion(question, questionsContainer);
258
+ });
259
+ }
260
+ }
261
+ };
262
+
263
+ // Form manager
264
+ const formManager = {
265
+ initializeFileInputs: () => {
266
+ ['doc1', 'doc2'].forEach(id => {
267
+ const input = utils.safeGetElement(id);
268
+ const nameDisplay = utils.safeGetElement(`${id}Name`);
269
+
270
+ if (input && nameDisplay) {
271
+ input.addEventListener('change', (e) => {
272
+ const fileName = e.target.files[0]?.name || 'No file selected';
273
+ nameDisplay.textContent = `Selected: ${fileName}`;
274
+ });
275
+ }
276
+ });
277
+ },
278
+
279
+ validateForm: () => {
280
+ const apiKey = utils.safeGetElement('apiKey')?.value;
281
+ if (!apiKey) {
282
+ throw new Error('Please enter your OpenAI API key');
283
+ }
284
+
285
+ const doc1 = utils.safeGetElement('doc1')?.files[0];
286
+ const doc2 = utils.safeGetElement('doc2')?.files[0];
287
+ if (!doc1 || !doc2) {
288
+ throw new Error('Please select both documents');
289
+ }
290
+
291
+ if (!doc1.name.toLowerCase().endsWith('.txt') || !doc2.name.toLowerCase().endsWith('.txt')) {
292
+ throw new Error('Only .txt files are allowed');
293
+ }
294
+ },
295
+
296
+ handleSubmit: async (e) => {
297
+ e.preventDefault();
298
+
299
+ try {
300
+ formManager.validateForm();
301
+
302
+ const loading = utils.safeGetElement('loading');
303
+ const results = utils.safeGetElement('results');
304
+
305
+ loading.classList.remove('hidden');
306
+ results.classList.add('hidden');
307
+
308
+ const formData = new FormData();
309
+ formData.append('api_key', utils.safeGetElement('apiKey').value);
310
+ formData.append('doc1', utils.safeGetElement('doc1').files[0]);
311
+ formData.append('doc2', utils.safeGetElement('doc2').files[0]);
312
+ formData.append('model', utils.safeGetElement('model').value);
313
+ formData.append('document_type', utils.safeGetElement('documentType').value);
314
+
315
+ const response = await fetch('/compare', {
316
+ method: 'POST',
317
+ body: formData
318
+ });
319
+
320
+ const data = await response.json();
321
+ if (!response.ok) {
322
+ throw new Error(data.error || 'An error occurred');
323
+ }
324
+
325
+ // Update summary statistics
326
+ utils.safeUpdateElement('totalTokens', data.total_tokens);
327
+
328
+ const doc1Score = utils.calculateScore(data.doc1_analysis);
329
+ const doc2Score = utils.calculateScore(data.doc2_analysis);
330
+ const dvdRatio = doc1Score.percentage > 0 ?
331
+ (doc2Score.percentage / doc1Score.percentage).toFixed(2) : 'N/A';
332
+ utils.safeUpdateElement('dvdRatio', dvdRatio);
333
+
334
+ // Update document texts
335
+ utils.safeUpdateElement('doc1Text', data.doc1_content || '');
336
+ utils.safeUpdateElement('doc2Text', data.doc2_content || '');
337
+
338
+ // Display results for both documents
339
+ utils.displayResults('doc1', data.doc1_analysis);
340
+ utils.displayResults('doc2', data.doc2_analysis);
341
+
342
+ results.classList.remove('hidden');
343
+ } catch (error) {
344
+ console.error('Error:', error);
345
+ alert(error.message || 'An error occurred while processing the documents');
346
+ } finally {
347
+ loading.classList.add('hidden');
348
+ }
349
+ }
350
+ };
351
+
352
+ // Initialize application
353
+ document.addEventListener('DOMContentLoaded', () => {
354
+ formManager.initializeFileInputs();
355
+
356
+ const form = utils.safeGetElement('uploadForm');
357
+ if (form) {
358
+ form.addEventListener('submit', formManager.handleSubmit);
359
+ }
360
+ });
361
+ </script>
362
+ </body>
363
+ </html>
364
+
365
+ # Contents from: .\note_criteria.json
366
+ {
367
+ "note_types": {
368
+ "discharge_note": {
369
+ "name": "Discharge Note",
370
+ "relevancy_criteria": [
371
+ "Hospital Admission and Discharge Details",
372
+ "Reason for Hospitalization",
373
+ "Hospital Course Summary",
374
+ "Discharge Diagnosis",
375
+ "Procedures Performed",
376
+ "Imaging studies",
377
+ "Medications at Discharge",
378
+ "Discharge Instructions",
379
+ "Follow-Up Care",
380
+ "Patient's Condition at Discharge",
381
+ "Patient Education and Counseling",
382
+ "Pending Results",
383
+ "Advance Directives and Legal Considerations",
384
+ "Important Abnormal (not normal)lab results, e.g. bacterial cultures, urine cultures, electrolyte disturbances, etc.",
385
+ "Important abnormal vital signs, e.g. fever, tachycardia, hypotension, etc.",
386
+ "Admission to ICU",
387
+ "comorbidities, e.g. diabetes, hypertension, etc.",
388
+ "Equipment needed at discharge, e.g. wheelchair, crutches, etc.",
389
+ "Prosthetics and tubes, e.g. Foley catheter, etc.",
390
+ "Allergies",
391
+ "Consultations (e.g., specialty or ancillary services)",
392
+ "Functional Capacity (ADLs and mobility status)",
393
+ "Lifestyle Modifications (diet, exercise, smoking cessation, etc.)",
394
+ "Wound Care or Other Specific Care Instructions"
395
+ ]
396
+ },
397
+ "admission_note": {
398
+ "name": "Admission Note",
399
+ "relevancy_criteria": [
400
+ "Patient Demographics and Identification",
401
+ "Chief Complaint",
402
+ "History of Present Illness",
403
+ "Past Medical History",
404
+ "Past Surgical History",
405
+ "Current Medications",
406
+ "Allergies",
407
+ "Social History (including smoking, alcohol, drugs)",
408
+ "Family History",
409
+ "Review of Systems",
410
+ "Physical Examination Findings",
411
+ "Vital Signs on Admission",
412
+ "Initial Laboratory Results",
413
+ "Initial Imaging Results",
414
+ "Initial Assessment/Impression",
415
+ "Differential Diagnosis",
416
+ "Initial Treatment Plan",
417
+ "Admission Orders",
418
+ "Code Status and Advance Directives",
419
+ "Consultations Requested",
420
+ "Anticipated Course of Stay",
421
+ "Functional Status on Admission",
422
+ "Mental Status Assessment",
423
+ "Pain Assessment",
424
+ "Admission Precautions (isolation, fall risk, etc.)"
425
+ ]
426
+ }
427
+ }
428
+ }
429
+
430
+
431
+ # Contents from: .\DOCKER_README.md
432
+ # Document vs Document Evaluator
433
+
434
+ ## Deployment Instructions
435
+
436
+ 1. The application requires the following environment variables:
437
+ - PORT: Set by Hugging Face Spaces automatically
438
+ - OPENAI_API_KEY: Provided by users through the interface
439
+
440
+ 2. Required files:
441
+ - app.py: Main application file
442
+ - dvd_evaluator.py: Core evaluation logic
443
+ - note_criteria.json: Evaluation criteria
444
+ - requirements.txt: Dependencies
445
+ - templates/index.html: Web interface
446
+
447
+ 3. The application uses Flask for the web interface and requires the following ports:
448
+ - Default port: 7860 (Hugging Face Spaces default)
449
+
450
+ # Contents from: .\README.md
451
+ ---
452
+ title: Document vs Document Evaluator
453
+ emoji: 📄
454
+ colorFrom: blue
455
+ colorTo: indigo
456
+ sdk: static
457
+ sdk_version: "3.1.0"
458
+ app_file: app.py
459
+ pinned: false
460
+ ---
461
+
462
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
463
+
464
+ # Contents from: .\app.py
465
+ from flask import Flask, render_template, request, jsonify
466
+ import os
467
+ import tempfile
468
+ import pandas as pd
469
+ from werkzeug.utils import secure_filename
470
+ import csv
471
+ from datetime import datetime
472
+ from typing import List, Dict, Any, Optional, Union
473
+ from pydantic import BaseModel, Field
474
+ from langchain_openai import ChatOpenAI
475
+ from langchain_core.messages import HumanMessage, SystemMessage
476
+ import tiktoken
477
+ import json
478
+ from dotenv import load_dotenv
479
+ from dvd_evaluator import (
480
+ generate_mcqs_for_note,
481
+ present_mcqs_to_content,
482
+ MCQ,
483
+ Document
484
+ )
485
+
486
+ # Load environment variables
487
+ load_dotenv()
488
+
489
+ # Define data models
490
+ class MCQ(BaseModel):
491
+ question: str
492
+ options: List[str]
493
+ correct_answer: str
494
+ source_name: str = Field(default="Unknown")
495
+
496
+ class Document(BaseModel):
497
+ name: str = ''
498
+ content: str
499
+ mcqs: List[MCQ] = Field(default_factory=list)
500
+
501
+ app = Flask(__name__)
502
+ app.config['UPLOAD_FOLDER'] = tempfile.mkdtemp()
503
+ app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
504
+
505
+ ALLOWED_EXTENSIONS = {'txt'}
506
+ MODELS = ['gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo'] # Update with supported models
507
+
508
+ with open('note_criteria.json', 'r') as f:
509
+ NOTE_CRITERIA = json.load(f)['note_types'] # Note the ['note_types'] key
510
+
511
+ def allowed_file(filename):
512
+ """Check if the uploaded file has an allowed extension."""
513
+ return '.' in filename and \
514
+ filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
515
+
516
+ def num_tokens_from_messages(messages, model="gpt-4o"):
517
+ """
518
+ Estimate token usage for messages using tiktoken.
519
+ """
520
+ encoding = tiktoken.encoding_for_model(model)
521
+ num_tokens = 0
522
+ for message in messages:
523
+ num_tokens += 4 # every message follows <im_start>{role/name}\n{content}<im_end>\n
524
+ for key, value in message.items():
525
+ num_tokens += len(encoding.encode(value))
526
+ num_tokens += 2 # every reply is primed with <im_start>assistant
527
+ return num_tokens
528
+
529
+ def generate_mcqs_for_note(note_content: str, total_tokens: List[int], source_name: str = '', document_type: str = 'discharge_note') -> List[MCQ]:
530
+ """
531
+ Generate Multiple Choice Questions (MCQs) from medical notes.
532
+ """
533
+ # Get relevancy criteria for selected document type
534
+ criteria = NOTE_CRITERIA[document_type]['relevancy_criteria']
535
+ criteria_list = "\n".join(f"{i+1}. {criterion}" for i, criterion in enumerate(criteria))
536
+
537
+ system_prompt = f"""
538
+ You are an expert in creating MCQs based on medical notes. Generate 20 MCQs that ONLY focus on these key areas:
539
+ {criteria_list}
540
+
541
+ Rules and Format:
542
+ 1. Each question must relate to specific content from these areas
543
+ 2. Skip areas not mentioned in the note
544
+ 3. Each question must have exactly 5 options (A-D plus E="I don't know")
545
+ 4. Provide only questions and answers, no explanations
546
+ 5. Use this exact format:
547
+
548
+ Question: [text]
549
+ A. [option]
550
+ B. [option]
551
+ C. [option]
552
+ D. [option]
553
+ E. I don't know
554
+ Correct Answer: [letter]
555
+ """
556
+
557
+ def parse_mcq(mcq_text: str) -> Optional[MCQ]:
558
+ """Parse a single MCQ from text format into an MCQ object."""
559
+ try:
560
+ lines = [line.strip() for line in mcq_text.split('\n') if line.strip()]
561
+ if len(lines) < 7: # Question + 5 options + correct answer
562
+ return None
563
+
564
+ # Extract question
565
+ if not lines[0].startswith('Question:'):
566
+ return None
567
+ question = lines[0].replace('Question:', '', 1).strip()
568
+
569
+ # Extract options
570
+ options = []
571
+ for i, line in enumerate(lines[1:6], 1):
572
+ if not line.startswith(chr(ord('A') + i - 1) + '.'):
573
+ return None
574
+ option = line.split('.', 1)[1].strip()
575
+ options.append(option)
576
+
577
+ # Extract correct answer
578
+ correct_line = lines[6]
579
+ if not correct_line.lower().startswith('correct answer:'):
580
+ return None
581
+
582
+ correct_letter = correct_line.split(':', 1)[1].strip().upper()
583
+ if correct_letter not in 'ABCDE':
584
+ return None
585
+
586
+ correct_index = ord(correct_letter) - ord('A')
587
+ correct_answer = options[correct_index] if correct_index < len(options) else options[-1]
588
+
589
+ return MCQ(
590
+ question=question,
591
+ options=options,
592
+ correct_answer=correct_answer,
593
+ source_name=source_name
594
+ )
595
+ except Exception as e:
596
+ print(f"Error parsing MCQ: {str(e)}")
597
+ return None
598
+
599
+ # Generate MCQs using LLM
600
+ try:
601
+ messages = [
602
+ SystemMessage(content=system_prompt),
603
+ HumanMessage(content=f"Create MCQs from this note:\n\n{note_content}")
604
+ ]
605
+
606
+ llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
607
+ response = llm(messages)
608
+
609
+ # Update token count
610
+ tokens_used = num_tokens_from_messages([
611
+ {"role": "system", "content": system_prompt},
612
+ {"role": "user", "content": note_content},
613
+ {"role": "assistant", "content": response.content}
614
+ ], model="gpt-4")
615
+ total_tokens[0] += tokens_used
616
+
617
+ # Parse MCQs from response
618
+ mcqs = []
619
+ for mcq_text in response.content.strip().split('\n\n'):
620
+ if mcq := parse_mcq(mcq_text):
621
+ mcqs.append(mcq)
622
+
623
+ return mcqs
624
+
625
+ except Exception as e:
626
+ print(f"Error in MCQ generation: {str(e)}")
627
+ return []
628
+
629
+ def present_mcqs_to_content(mcqs: List[MCQ], content: str, total_tokens: List[int]) -> List[Dict]:
630
+ """
631
+ Present MCQs to content and collect responses.
632
+ """
633
+ user_responses = []
634
+ batch_size = 20
635
+ llm = ChatOpenAI(model="gpt-4", temperature=0)
636
+
637
+ for i in range(0, len(mcqs), batch_size):
638
+ batch_mcqs = mcqs[i:i + batch_size]
639
+ questions_text = "\n\n".join([
640
+ f"Question {j+1}: {mcq.question}\n"
641
+ f"A. {mcq.options[0]}\n"
642
+ f"B. {mcq.options[1]}\n"
643
+ f"C. {mcq.options[2]}\n"
644
+ f"D. {mcq.options[3]}\n"
645
+ f"E. I don't know"
646
+ for j, mcq in enumerate(batch_mcqs)
647
+ ])
648
+
649
+ batch_prompt = f"""
650
+ You are an expert medical knowledge evaluator. Given a medical note and multiple questions:
651
+ 1. For each question, verify if it can be answered from the given content
652
+ 2. If a question cannot be answered from the content, choose 'E' (I don't know)
653
+ 3. If a question can be answered, choose the most accurate option based ONLY on the given content
654
+
655
+ Document Content: {content}
656
+
657
+ {questions_text}
658
+
659
+ Respond with ONLY the question numbers and corresponding letters, one per line, like this:
660
+ 1: A
661
+ 2: B
662
+ etc.
663
+ """
664
+
665
+ messages = [HumanMessage(content=batch_prompt)]
666
+ response = llm(messages)
667
+
668
+ tokens_used = num_tokens_from_messages([
669
+ {"role": "user", "content": batch_prompt},
670
+ {"role": "assistant", "content": response.content}
671
+ ], model="gpt-4o-mini")
672
+ total_tokens[0] += tokens_used
673
+
674
+ try:
675
+ response_lines = response.content.strip().split('\n')
676
+ for j, line in enumerate(response_lines):
677
+ if j >= len(batch_mcqs):
678
+ break
679
+
680
+ mcq = batch_mcqs[j]
681
+ try:
682
+ # Get the letter answer (A, B, C, D, or E)
683
+ answer_letter = line.split(':')[1].strip().upper()
684
+ if answer_letter not in ['A', 'B', 'C', 'D', 'E']:
685
+ answer_letter = 'E'
686
+
687
+ # Convert letter to corresponding option text
688
+ if answer_letter == 'E':
689
+ user_answer_text = "I don't know"
690
+ else:
691
+ # Get the index (0-3) from the letter (A-D)
692
+ option_index = ord(answer_letter) - ord('A')
693
+ user_answer_text = mcq.options[option_index]
694
+
695
+ except (IndexError, ValueError):
696
+ user_answer_text = "I don't know"
697
+
698
+ user_responses.append({
699
+ "question": mcq.question,
700
+ "user_answer": user_answer_text,
701
+ "correct_answer": mcq.correct_answer
702
+ })
703
+
704
+ except Exception as e:
705
+ print(f"Error processing batch responses: {str(e)}")
706
+ # If something fails, default the remainder to "I don't know"
707
+ for mcq in batch_mcqs[len(user_responses):]:
708
+ user_responses.append({
709
+ "question": mcq.question,
710
+ "user_answer": "I don't know",
711
+ "correct_answer": mcq.correct_answer
712
+ })
713
+
714
+ return user_responses
715
+
716
+
717
+ def run_evaluation(ai_content: str, ai_mcqs: List[MCQ], note_content: str, note_mcqs: List[MCQ],
718
+ note_name: str, original_note_number: int, total_tokens: List[int]) -> List[Dict]:
719
+
720
+ # For Doc1: use questions from Doc2 (note_mcqs)
721
+ # For Doc2: use questions from Doc1 (ai_mcqs)
722
+ mcqs_to_use = ai_mcqs if note_name == 'Doc2' else note_mcqs
723
+ content_to_evaluate = note_content
724
+
725
+ responses = present_mcqs_to_content(mcqs_to_use, content_to_evaluate, total_tokens)
726
+
727
+ results = []
728
+ for i, mcq in enumerate(mcqs_to_use):
729
+ results.append({
730
+ "original_note_number": original_note_number,
731
+ "new_note_name": note_name,
732
+ "question": mcq.question,
733
+ "options": mcq.options,
734
+ "source_document": 'Doc2' if note_name == 'Doc1' else 'Doc1',
735
+ "ideal_answer": mcq.correct_answer,
736
+ "model_answer": responses[i]["user_answer"],
737
+ "is_correct": responses[i]["user_answer"] == mcq.correct_answer
738
+ })
739
+
740
+ return results
741
+ import concurrent.futures
742
+
743
+ import concurrent.futures
744
+ import csv
745
+ import os
746
+ from flask import jsonify, request
747
+
748
+ @app.route('/compare', methods=['POST'])
749
+ def compare_documents():
750
+ """
751
+ Compare two documents by generating and answering MCQs for each document.
752
+ Returns analysis of how well each document contains information from the other.
753
+ """
754
+ print("\n=== Starting document comparison ===")
755
+
756
+ try:
757
+ # Validate API key
758
+ api_key = request.form.get('api_key')
759
+ if not api_key:
760
+ return jsonify({"error": "OpenAI API key is required"}), 400
761
+ os.environ['OPENAI_API_KEY'] = api_key
762
+
763
+ # Get model and document type selection
764
+ model = request.form.get('model', 'gpt-4o-mini')
765
+ document_type = request.form.get('document_type', 'discharge_note')
766
+
767
+ # Initialize OpenAI client with selected model
768
+ llm = ChatOpenAI(model=model, temperature=0)
769
+
770
+ # Validate file uploads
771
+ if 'doc1' not in request.files or 'doc2' not in request.files:
772
+ print("Error: Missing files in request")
773
+ return jsonify({"error": "Both doc1 and doc2 are required"}), 400
774
+
775
+ doc1_file = request.files['doc1']
776
+ doc2_file = request.files['doc2']
777
+
778
+ print(f"Received files: {doc1_file.filename} and {doc2_file.filename}")
779
+
780
+ # Validate filenames
781
+ if not all([doc1_file.filename, doc2_file.filename]):
782
+ print("Error: Empty filename(s)")
783
+ return jsonify({"error": "Both documents need valid filenames"}), 400
784
+
785
+ # Validate file types
786
+ if not all(allowed_file(f.filename) for f in [doc1_file, doc2_file]):
787
+ print("Error: Invalid file type(s)")
788
+ return jsonify({"error": "Only .txt files are allowed"}), 400
789
+
790
+ # Read document contents
791
+ try:
792
+ doc1_text = doc1_file.read().decode('utf-8')
793
+ doc2_text = doc2_file.read().decode('utf-8')
794
+ print(f"Doc1 length: {len(doc1_text)} chars")
795
+ print(f"Doc2 length: {len(doc2_text)} chars")
796
+ except UnicodeDecodeError as e:
797
+ print(f"Decode error: {str(e)}")
798
+ return jsonify({"error": "Error decoding one of the documents"}), 400
799
+
800
+ # Initialize token counter
801
+ total_tokens = [0]
802
+
803
+ # Generate MCQs for both documents
804
+ print("\nGenerating MCQs for Doc1...")
805
+ doc1_mcqs = generate_mcqs_for_note(
806
+ note_content=doc1_text,
807
+ total_tokens=total_tokens,
808
+ source_name='Doc1',
809
+ document_type=document_type
810
+ )
811
+ print(f"Generated {len(doc1_mcqs)} MCQs for Doc1")
812
+
813
+ print("\nGenerating MCQs for Doc2...")
814
+ doc2_mcqs = generate_mcqs_for_note(
815
+ note_content=doc2_text,
816
+ total_tokens=total_tokens,
817
+ source_name='Doc2',
818
+ document_type=document_type
819
+ )
820
+ print(f"Generated {len(doc2_mcqs)} MCQs for Doc2")
821
+
822
+ # Present each doc's MCQs to the other doc
823
+ print("\nGetting answers for Doc1...")
824
+ doc1_responses = present_mcqs_to_content(doc2_mcqs, doc1_text, total_tokens)
825
+ print(f"Received {len(doc1_responses)} answers for Doc1")
826
+
827
+ print("\nGetting answers for Doc2...")
828
+ doc2_responses = present_mcqs_to_content(doc1_mcqs, doc2_text, total_tokens)
829
+ print(f"Received {len(doc2_responses)} answers for Doc2")
830
+
831
+ def process_mcq_results(responses, mcqs):
832
+ """Process MCQ responses and organize into categories."""
833
+ attempted = []
834
+ unknown = []
835
+ correct_count = 0
836
+ total_count = len(responses)
837
+
838
+ for i, response in enumerate(responses):
839
+ if i >= len(mcqs): # Safety check
840
+ continue
841
+
842
+ mcq = mcqs[i]
843
+ answer = response.get("user_answer", "I don't know")
844
+
845
+ result = {
846
+ "question": mcq.question,
847
+ "options": mcq.options,
848
+ "ideal_answer": mcq.correct_answer,
849
+ "model_answer": answer,
850
+ }
851
+
852
+ if answer == "I don't know":
853
+ unknown.append(result)
854
+ else:
855
+ is_correct = answer == mcq.correct_answer
856
+ if is_correct:
857
+ correct_count += 1
858
+ result["is_correct"] = is_correct
859
+ attempted.append(result)
860
+
861
+ return {
862
+ "score": f"{correct_count}/{total_count}",
863
+ "attempted_answers": attempted,
864
+ "unknown_answers": unknown
865
+ }
866
+
867
+ # Process results for both documents
868
+ doc1_analysis = process_mcq_results(doc1_responses, doc2_mcqs)
869
+ doc2_analysis = process_mcq_results(doc2_responses, doc1_mcqs)
870
+
871
+ # Prepare response
872
+ response = {
873
+ "doc1_analysis": doc1_analysis,
874
+ "doc2_analysis": doc2_analysis,
875
+ "total_tokens": total_tokens[0],
876
+ "doc1_content": doc1_text,
877
+ "doc2_content": doc2_text
878
+ }
879
+
880
+ print("\nSending response...")
881
+ print(f"Total tokens used: {total_tokens[0]}")
882
+ return jsonify(response), 200
883
+
884
+ except Exception as e:
885
+ import traceback
886
+ print(f"\nERROR in compare_documents:")
887
+ print(traceback.format_exc())
888
+ return jsonify({"error": str(e)}), 500
889
+
890
+ finally:
891
+ print("=== Comparison complete ===\n")
892
+
893
+ def process_responses(responses, mcqs, doc_name):
894
+ """Process responses and organize them into categories."""
895
+ attempted = []
896
+ unknown = []
897
+ correct_count = 0
898
+
899
+ for i, response in enumerate(responses):
900
+ mcq = mcqs[i]
901
+ answer_text = response['user_answer']
902
+
903
+ if answer_text == "I don't know": # Changed from 'E' to "I don't know"
904
+ unknown.append({
905
+ 'question': mcq.question,
906
+ 'options': mcq.options,
907
+ 'ideal_answer': mcq.correct_answer
908
+ })
909
+ else:
910
+ is_correct = response['user_answer'] == response['correct_answer']
911
+ if is_correct:
912
+ correct_count += 1
913
+
914
+ attempted.append({
915
+ 'question': mcq.question,
916
+ 'options': mcq.options,
917
+ 'ideal_answer': mcq.correct_answer,
918
+ 'model_answer': answer_text, # Use the answer text directly
919
+ 'is_correct': is_correct
920
+ })
921
+
922
+ return {
923
+ 'total_score': f"{correct_count}/{len(responses)}",
924
+ 'attempted_answers': attempted,
925
+ 'unknown_answers': unknown
926
+ }
927
+
928
+ @app.route('/')
929
+ def index():
930
+ """Serve the main page."""
931
+ return render_template('index.html', models=MODELS)
932
+
933
+ if __name__ == "__main__":
934
+ # Get port from environment variable for Hugging Face Spaces
935
+ port = int(os.environ.get("PORT", 7860))
936
+ app.run(host="0.0.0.0", port=port)
937
+
938
+ # Contents from: .\combine.py
939
+ import os
940
+
941
+ def get_files_recursively(directory, extensions):
942
+ """
943
+ Recursively get all files with specified extensions from directory and subdirectories,
944
+ excluding combined.py and venv folder.
945
+ """
946
+ file_list = []
947
+ for root, dirs, files in os.walk(directory):
948
+ # Exclude the 'venv' directory from the search
949
+ if 'venv' in root:
950
+ continue
951
+ for file in files:
952
+ # Exclude the 'combined.py' file from the results
953
+ if file == 'combined.py':
954
+ continue
955
+ if any(file.endswith(ext) for ext in extensions):
956
+ file_list.append(os.path.join(root, file))
957
+ return file_list
958
+
959
+ def combine_files(output_file, file_list):
960
+ """
961
+ Combine contents of all files in file_list into output_file
962
+ """
963
+ with open(output_file, 'a', encoding='utf-8') as outfile:
964
+ for fname in file_list:
965
+ # Add a header comment to show which file's contents follow
966
+ outfile.write(f"\n\n# Contents from: {fname}\n")
967
+ try:
968
+ with open(fname, 'r', encoding='utf-8') as infile:
969
+ for line in infile:
970
+ outfile.write(line)
971
+ except Exception as e:
972
+ outfile.write(f"# Error reading file {fname}: {str(e)}\n")
973
+
974
+ def main():
975
+ # Define the base directory (current directory in this case)
976
+ base_directory = "."
977
+ output_file = 'combined.py'
978
+ extensions = ('.py','html', 'md', 'json') # Ensure this is a tuple
979
+
980
+ # Remove output file if it exists
981
+ if os.path.exists(output_file):
982
+ try:
983
+ os.remove(output_file)
984
+ except Exception as e:
985
+ print(f"Error removing existing {output_file}: {str(e)}")
986
+ return
987
+
988
+ # Get all files recursively
989
+ all_files = get_files_recursively(base_directory, extensions)
990
+
991
+ # Sort files by extension and then by name
992
+ all_files.sort(key=lambda x: (os.path.splitext(x)[1], x))
993
+
994
+ # Add a header to the output file
995
+ with open(output_file, 'w', encoding='utf-8') as outfile:
996
+ outfile.write("# Combined Python files\n")
997
+ outfile.write(f"# Generated from directory: {os.path.abspath(base_directory)}\n")
998
+ outfile.write(f"# Total files found: {len(all_files)}\n\n")
999
+
1000
+ # Combine all files
1001
+ combine_files(output_file, all_files)
1002
+
1003
+ print(f"Successfully combined {len(all_files)} files into {output_file}")
1004
+ print("Files processed:")
1005
+ for file in all_files:
1006
+ print(f" - {file}")
1007
+
1008
+ if __name__ == "__main__":
1009
+ main()
1010
+
1011
+ # Contents from: .\dvd_evaluator.py
1012
+ import os
1013
+ import csv
1014
+ import argparse
1015
+ import pandas as pd
1016
+ from typing import List, Dict, Any
1017
+ from datetime import datetime
1018
+ from pydantic import BaseModel, Field
1019
+ from tqdm import tqdm
1020
+ import tiktoken
1021
+ from typing import List, Dict, Any, Optional
1022
+ import json
1023
+
1024
+
1025
+ from langchain_openai import ChatOpenAI
1026
+ from langchain_core.messages import HumanMessage, SystemMessage
1027
+
1028
+ from dotenv import load_dotenv
1029
+
1030
+
1031
+ load_dotenv()
1032
+
1033
+ # Define data models
1034
+ class MCQ(BaseModel):
1035
+ question: str
1036
+ options: List[str]
1037
+ correct_answer: str
1038
+ source_name: str = Field(default="Unknown") # Add source_name field with default value
1039
+
1040
+ class Document(BaseModel):
1041
+ name: str = ''
1042
+ content: str
1043
+ mcqs: List[MCQ] = Field(default_factory=list)
1044
+
1045
+ # Load note criteria at module level
1046
+ with open('note_criteria.json', 'r') as f:
1047
+ NOTE_CRITERIA = json.load(f)['note_types']
1048
+
1049
+ def num_tokens_from_messages(messages, model="gpt-4"):
1050
+ """
1051
+ Estimate token usage for messages using tiktoken.
1052
+
1053
+ Args:
1054
+ messages: List of message dictionaries
1055
+ model (str): Model name for token counting. Defaults to 'gpt-4'
1056
+ """
1057
+ try:
1058
+ encoding = tiktoken.encoding_for_model(model)
1059
+ num_tokens = 0
1060
+ for message in messages:
1061
+ num_tokens += 4
1062
+ for key, value in message.items():
1063
+ num_tokens += len(encoding.encode(value))
1064
+ num_tokens += 2
1065
+ return num_tokens
1066
+ except Exception as e:
1067
+ print(f"Warning: Error counting tokens: {str(e)}")
1068
+ return 0
1069
+
1070
+ def generate_mcqs_for_note(note_content: str, total_tokens: List[int], source_name: str = '', document_type: str = 'discharge_note') -> List[MCQ]:
1071
+ """
1072
+ Generate Multiple Choice Questions (MCQs) from medical notes.
1073
+ """
1074
+ # Get criteria based on document type
1075
+ criteria = NOTE_CRITERIA.get(document_type, NOTE_CRITERIA['discharge_note'])
1076
+ criteria_points = criteria['relevancy_criteria']
1077
+
1078
+ # Create dynamic system prompt based on document type
1079
+ system_prompt = f"""
1080
+ You are an expert in creating MCQs based on {criteria['name']}s. Generate 20 MCQs that ONLY focus on these key areas:
1081
+ {chr(10).join(f"{i+1}. {point}" for i, point in enumerate(criteria_points))}
1082
+
1083
+ Rules and Format:
1084
+ 1. Each question must relate to specific content from these areas
1085
+ 2. Skip areas not mentioned in the note
1086
+ 3. Each question must have exactly 5 options (A-D plus E="I don't know")
1087
+ 4. Provide only questions and answers, no explanations
1088
+ 5. Use this exact format:
1089
+
1090
+ Question: [text]
1091
+ A. [option]
1092
+ B. [option]
1093
+ C. [option]
1094
+ D. [option]
1095
+ E. I don't know
1096
+ Correct Answer: [letter]
1097
+ """
1098
+ def parse_mcq(mcq_text: str) -> Optional[MCQ]:
1099
+ """Parse a single MCQ from text format into an MCQ object."""
1100
+ try:
1101
+ lines = [line.strip() for line in mcq_text.split('\n') if line.strip()]
1102
+ if len(lines) < 7: # Question + 5 options + correct answer
1103
+ return None
1104
+
1105
+ # Extract question
1106
+ if not lines[0].startswith('Question:'):
1107
+ return None
1108
+ question = lines[0].replace('Question:', '', 1).strip()
1109
+
1110
+ # Extract options
1111
+ options = []
1112
+ for i, line in enumerate(lines[1:6], 1):
1113
+ if not line.startswith(chr(ord('A') + i - 1) + '.'):
1114
+ return None
1115
+ option = line.split('.', 1)[1].strip()
1116
+ options.append(option)
1117
+
1118
+ # Extract correct answer
1119
+ correct_line = lines[6]
1120
+ if not correct_line.lower().startswith('correct answer:'):
1121
+ return None
1122
+
1123
+ correct_letter = correct_line.split(':', 1)[1].strip().upper()
1124
+ if correct_letter not in 'ABCDE':
1125
+ return None
1126
+
1127
+ correct_index = ord(correct_letter) - ord('A')
1128
+ correct_answer = options[correct_index] if correct_index < len(options) else options[-1]
1129
+
1130
+ return MCQ(
1131
+ question=question,
1132
+ options=options,
1133
+ correct_answer=correct_answer,
1134
+ source_name=source_name
1135
+ )
1136
+ except Exception as e:
1137
+ print(f"Error parsing MCQ: {str(e)}")
1138
+ return None
1139
+
1140
+ try:
1141
+ messages = [
1142
+ SystemMessage(content=system_prompt),
1143
+ HumanMessage(content=f"Create MCQs from this {criteria['name'].lower()}:\n\n{note_content}")
1144
+ ]
1145
+
1146
+ llm = ChatOpenAI(temperature=0)
1147
+ response = llm(messages)
1148
+
1149
+ # Update token count with default model
1150
+ tokens_used = num_tokens_from_messages([
1151
+ {"role": "system", "content": system_prompt},
1152
+ {"role": "user", "content": note_content},
1153
+ {"role": "assistant", "content": response.content}
1154
+ ])
1155
+ total_tokens[0] += tokens_used
1156
+
1157
+ # Parse MCQs from response
1158
+ mcqs = []
1159
+ for mcq_text in response.content.strip().split('\n\n'):
1160
+ if mcq := parse_mcq(mcq_text):
1161
+ mcq.source_name = source_name
1162
+ mcqs.append(mcq)
1163
+
1164
+ return mcqs
1165
+
1166
+ except Exception as e:
1167
+ print(f"Error in MCQ generation: {str(e)}")
1168
+ return []
1169
+
1170
+ def present_mcqs_to_content(mcqs: List[MCQ], content: str, total_tokens: List[int], document_type: str = 'discharge_note') -> List[Dict]:
1171
+ """
1172
+ Present MCQs to content and collect responses.
1173
+ """
1174
+ # Get criteria based on document type
1175
+ criteria = NOTE_CRITERIA.get(document_type, NOTE_CRITERIA['discharge_note'])
1176
+
1177
+ batch_size = 20
1178
+ llm = ChatOpenAI(temperature=0) # Remove model parameter
1179
+ user_responses = []
1180
+
1181
+ for i in range(0, len(mcqs), batch_size):
1182
+ batch_mcqs = mcqs[i:i + batch_size]
1183
+ questions_text = "\n\n".join([
1184
+ f"Question {j+1}: {mcq.question}\n"
1185
+ f"A. {mcq.options[0]}\n"
1186
+ f"B. {mcq.options[1]}\n"
1187
+ f"C. {mcq.options[2]}\n"
1188
+ f"D. {mcq.options[3]}\n"
1189
+ f"E. I don't know"
1190
+ for j, mcq in enumerate(batch_mcqs)
1191
+ ])
1192
+
1193
+ batch_prompt = f"""
1194
+ You are an expert {criteria['name'].lower()} evaluator. Given a medical note and multiple questions:
1195
+ 1. For each question, verify if it can be answered from the given content
1196
+ 2. If a question cannot be answered from the content, choose 'E' (I don't know)
1197
+ 3. If a question can be answered, choose the most accurate option based ONLY on the given content
1198
+
1199
+ Document Content: {content}
1200
+
1201
+ {questions_text}
1202
+
1203
+ Respond with ONLY the question numbers and corresponding letters, one per line, like this:
1204
+ 1: A
1205
+ 2: B
1206
+ etc.
1207
+ """
1208
+
1209
+ messages = [HumanMessage(content=batch_prompt)]
1210
+ response = llm(messages)
1211
+
1212
+ tokens_used = num_tokens_from_messages([
1213
+ {"role": "user", "content": batch_prompt},
1214
+ {"role": "assistant", "content": response.content}
1215
+ ]) # Remove model parameter
1216
+
1217
+ total_tokens[0] += tokens_used
1218
+
1219
+ try:
1220
+ response_lines = response.content.strip().split('\n')
1221
+ for j, line in enumerate(response_lines):
1222
+ if j >= len(batch_mcqs):
1223
+ break
1224
+
1225
+ try:
1226
+ answer = line.split(':')[1].strip().upper()
1227
+ if answer not in ['A', 'B', 'C', 'D', 'E']:
1228
+ answer = 'E'
1229
+
1230
+ mcq = batch_mcqs[j]
1231
+ user_responses.append({
1232
+ "question": mcq.question,
1233
+ "user_answer": answer,
1234
+ "correct_answer": chr(ord('A') + mcq.options.index(mcq.correct_answer))
1235
+ })
1236
+ except (IndexError, ValueError):
1237
+ mcq = batch_mcqs[j]
1238
+ user_responses.append({
1239
+ "question": mcq.question,
1240
+ "user_answer": "E",
1241
+ "correct_answer": chr(ord('A') + mcq.options.index(mcq.correct_answer))
1242
+ })
1243
+
1244
+ except Exception as e:
1245
+ print(f"Error processing batch responses: {str(e)}")
1246
+ for mcq in batch_mcqs[len(user_responses):]:
1247
+ user_responses.append({
1248
+ "question": mcq.question,
1249
+ "user_answer": "E",
1250
+ "correct_answer": chr(ord('A') + mcq.options.index(mcq.correct_answer))
1251
+ })
1252
+
1253
+ return user_responses
1254
+
1255
+ def evaluate_responses(user_responses) -> int:
1256
+ """
1257
+ Evaluate responses and return score.
1258
+ """
1259
+ correct = 0
1260
+ for response in user_responses:
1261
+ if response["user_answer"] == "E": # "I don't know" is now "E"
1262
+ continue
1263
+ elif response["user_answer"] == response["correct_answer"]:
1264
+ correct += 1
1265
+
1266
+ return correct
1267
+
1268
+ def run_evaluation(ai_content: str, ai_mcqs: List[MCQ], note_content: str, note_mcqs: List[MCQ],
1269
+ note_name: str, original_note_number: int, total_tokens: List[int],
1270
+ document_type: str = 'discharge_note') -> List[Dict]:
1271
+ """
1272
+ Run evaluation with specified document type.
1273
+ """
1274
+ # For Doc1: use questions from Doc2 (note_mcqs)
1275
+ # For Doc2: use questions from Doc1 (ai_mcqs)
1276
+ mcqs_to_use = ai_mcqs if note_name == 'Doc2' else note_mcqs
1277
+ content_to_evaluate = note_content
1278
+
1279
+ responses = present_mcqs_to_content(mcqs_to_use, content_to_evaluate, total_tokens, document_type=document_type)
1280
+
1281
+ results = []
1282
+ for i, mcq in enumerate(mcqs_to_use):
1283
+ result = {
1284
+ "original_note_number": original_note_number,
1285
+ "new_note_name": note_name,
1286
+ "question": mcq.question,
1287
+ "source_document": mcq.source_name,
1288
+ "options": mcq.options,
1289
+ "ideal_answer": mcq.options[ord(responses[i]["correct_answer"]) - ord('A')],
1290
+ "correct_answer": responses[i]["correct_answer"],
1291
+ "ai_answer": responses[i]["user_answer"],
1292
+ "note_answer": responses[i]["user_answer"],
1293
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
1294
+ }
1295
+ results.append(result)
1296
+
1297
+ return results
1298
+
1299
+ def main():
1300
+ parser = argparse.ArgumentParser(description="Process CSV containing AI and modified notes.")
1301
+ parser.add_argument("--modified_csv", required=True, help="Path to CSV with AI & modified notes")
1302
+ parser.add_argument("--result_csv", default="results.csv", help="Output CSV file")
1303
+ parser.add_argument("--start", type=int, default=0, help="Start original_note_number (inclusive)")
1304
+ parser.add_argument("--end", type=int, default=10, help="End original_note_number (exclusive)")
1305
+ parser.add_argument("--model", default="gpt-4o-mini", help="OpenAI model to use")
1306
+ args = parser.parse_args()
1307
+
1308
+ print(f"\n=== MCQ EVALUATOR ===")
1309
+ print(f"Reading from: {args.modified_csv}")
1310
+ print(f"Writing results to: {args.result_csv}")
1311
+ print(f"Processing original_note_number in [{args.start}, {args.end})")
1312
+ print(f"Using model: {args.model}\n")
1313
+
1314
+ global llm
1315
+ llm = ChatOpenAI(model=args.model, temperature=0)
1316
+
1317
+ if not os.path.exists(args.modified_csv):
1318
+ print(f"ERROR: {args.modified_csv} not found.")
1319
+ return
1320
+
1321
+ try:
1322
+ print("Loading CSV file...")
1323
+ df = pd.read_csv(args.modified_csv)
1324
+ print(f"Loaded {len(df)} rows")
1325
+ except Exception as e:
1326
+ print(f"ERROR reading {args.modified_csv}: {e}")
1327
+ return
1328
+
1329
+ needed_cols = {"original_note_number", "new_note_name", "modified_text"}
1330
+ if not needed_cols.issubset(df.columns):
1331
+ print(f"ERROR: Missing columns in {args.modified_csv}. We need {needed_cols}.")
1332
+ return
1333
+
1334
+ df_in_range = df[(df["original_note_number"] >= args.start) &
1335
+ (df["original_note_number"] < args.end)]
1336
+ if df_in_range.empty:
1337
+ print("No rows found in the specified range.")
1338
+ return
1339
+
1340
+ print(f"Found {len(df_in_range)} rows in specified range")
1341
+
1342
+ results = []
1343
+ total_tokens = [0]
1344
+ grouped = df_in_range.groupby("original_note_number")
1345
+
1346
+ for onum, group in tqdm(grouped, desc="Processing notes"):
1347
+ print(f"\n\nProcessing original_note_number {onum}")
1348
+
1349
+ # Get AI note and generate MCQs once per group
1350
+ ai_row = group[group["new_note_name"] == "AI"]
1351
+ if ai_row.empty:
1352
+ print(f"Warning: No AI note found for original_note_number={onum}, skipping.")
1353
+ continue
1354
+
1355
+ ai_text = ai_row.iloc[0]["modified_text"]
1356
+ print("Generating MCQs for AI note...")
1357
+ mcqs_ai = generate_mcqs_for_note(
1358
+ note_content=ai_text,
1359
+ total_tokens=total_tokens,
1360
+ source_name='AI',
1361
+ document_type='discharge_note'
1362
+ )
1363
+ print(f"Generated {len(mcqs_ai)} MCQs from AI note")
1364
+
1365
+ # Process ALL other notes (including original)
1366
+ print("\nProcessing comparisons...")
1367
+ other_rows = group[group["new_note_name"] != "AI"]
1368
+
1369
+ for idx, row in other_rows.iterrows():
1370
+ note_name = row["new_note_name"]
1371
+ print(f"\nProcessing comparison with {note_name}")
1372
+ note_text = row["modified_text"]
1373
+
1374
+ result = run_evaluation(
1375
+ ai_content=ai_text,
1376
+ ai_mcqs=mcqs_ai,
1377
+ note_content=note_text,
1378
+ note_mcqs=mcqs_ai,
1379
+ note_name=note_name,
1380
+ original_note_number=onum,
1381
+ total_tokens=total_tokens,
1382
+ document_type='discharge_note'
1383
+ )
1384
+ results.extend(result)
1385
+
1386
+ file_exists = os.path.exists(args.result_csv)
1387
+ mode = 'a' if file_exists else 'w'
1388
+
1389
+ fieldnames = ["original_note_number", "new_note_name", "question", "source_document",
1390
+ "options", "ideal_answer", "correct_answer", "ai_answer", "note_answer",
1391
+ "timestamp", "total_tokens"]
1392
+
1393
+ with open(args.result_csv, mode, newline='', encoding='utf-8') as csvfile:
1394
+ writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
1395
+ if not file_exists:
1396
+ writer.writeheader()
1397
+
1398
+ # Fix: Modify how we handle the results
1399
+ for result in results: # results is already a list of dictionaries
1400
+ result_dict = dict(result) # Create a copy of the result dictionary
1401
+ result_dict["total_tokens"] = total_tokens[0] # Add token count
1402
+ writer.writerow(result_dict)
1403
+
1404
+ print(f"\nResults written to {args.result_csv}")
1405
+ print(f"Total tokens used: {total_tokens[0]}")
1406
+ print("=== Done ===")
1407
+
1408
+ if __name__ == "__main__":
1409
+ app.run(host='0.0.0.0', port=5000, debug=True)
1410
+
1411
+ #python dvd_evaluator.py --modified_csv "examples/example.csv" --result_csv "results.csv" --start 1 --end 2 --model "gpt-4o-mini"
dvd_evaluator.py CHANGED
@@ -395,6 +395,7 @@ def main():
395
  print("=== Done ===")
396
 
397
  if __name__ == "__main__":
398
- app.run(host='0.0.0.0', port=5000, debug=True)
 
399
 
400
  #python dvd_evaluator.py --modified_csv "examples/example.csv" --result_csv "results.csv" --start 1 --end 2 --model "gpt-4o-mini"
 
395
  print("=== Done ===")
396
 
397
  if __name__ == "__main__":
398
+ port = int(os.environ.get("PORT", 7860))
399
+ app.run(host="0.0.0.0", port=port)
400
 
401
  #python dvd_evaluator.py --modified_csv "examples/example.csv" --result_csv "results.csv" --start 1 --end 2 --model "gpt-4o-mini"