Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
-
import docx
|
| 3 |
import asyncio
|
| 4 |
from concurrent.futures import ThreadPoolExecutor
|
| 5 |
from google import generativeai as genai
|
|
@@ -9,7 +8,7 @@ from typing import List, Dict, Optional
|
|
| 9 |
import os
|
| 10 |
from pathlib import Path
|
| 11 |
import time
|
| 12 |
-
|
| 13 |
|
| 14 |
GRADING_RUBRIC = """
|
| 15 |
GRADING RUBRIC (Total 100 points)
|
|
@@ -82,49 +81,26 @@ Professor's Summary:
|
|
| 82 |
|
| 83 |
"""
|
| 84 |
|
|
|
|
| 85 |
BASE_PROMPT_TEMPLATE = """
|
| 86 |
You are about to assume a role. Carefully review the persona, context, task, and output requirements before proceeding.
|
| 87 |
|
| 88 |
1. PERSONA
|
| 89 |
-
|
| 90 |
You are Dr. Stone, a meticulous and experienced Associate Professor of Nursing at a major university. You have been teaching for over 20 years, with a specialization in medical-surgical nursing. You are known for your high standards, particularly regarding academic integrity and the strict application of the American Psychological Association (APA) 7th Edition formatting guidelines. Your feedback is always direct, precise, and aimed at preparing students for the rigorous documentation standards required in the healthcare profession. Your tone is professional, authoritative, and educational. You do not offer praise for meeting baseline expectations; you simply state that the requirements have been met. Your criticism is specific, referencing the exact rule or rubric criterion that was violated.
|
| 91 |
|
| 92 |
2. CONTEXT
|
| 93 |
-
|
| 94 |
-
You are grading an essay for your undergraduate course, NURS 310: Fundamentals of Nursing. The assignment is an essay on the topic of "Skin Lesions." Students were provided with a detailed grading rubric, which you must follow perfectly and without exception. The total possible score for this assignment is 100 points.
|
| 95 |
|
| 96 |
3. PRIMARY TASK
|
| 97 |
-
|
| 98 |
-
Your task is to receive a student's essay submission and grade it with absolute precision according to the provided rubric. You must function as a perfect and unflinchingly accurate grader. There is no room for subjective interpretation or leniency. You will identify every error in content, formatting, spelling, grammar, and citation, and assign points strictly based on the rubric's quantitative thresholds.
|
| 99 |
|
| 100 |
Your output should be a single JSON format structure response as indicated by the example below.
|
| 101 |
|
| 102 |
**DO NOT** provide any introductory text, conversational pleasantries, or explanations outside of the requested JSON structure. Your entire response must be a single, valid JSON object.
|
| 103 |
|
| 104 |
-
**Use the following rubric to grade the
|
| 105 |
{GRADING_RUBRIC}
|
| 106 |
|
| 107 |
-
**For your reference, here is an example of a perfectly formatted paper. Use it as a guide for what correct formatting looks like:**
|
| 108 |
-
---
|
| 109 |
-
**EXAMPLE PAPER START**
|
| 110 |
-
{EXAMPLE_PAPER}
|
| 111 |
-
**EXAMPLE PAPER END**
|
| 112 |
-
---
|
| 113 |
-
|
| 114 |
-
**Instructions:**
|
| 115 |
-
1. Read the entire essay provided below.
|
| 116 |
-
2. Assess the essay against each category in the rubric.
|
| 117 |
-
3. Calculate the total points lost and the final grade out of 100.
|
| 118 |
-
4. Provide brief, specific comments explaining why points were deducted in each category.
|
| 119 |
-
5. Write a 2-3 sentence summary of the overall grade.
|
| 120 |
-
6. Format your entire output as a single JSON object with the following keys and value types:
|
| 121 |
-
- `finalGrade`: (Integer) The final score from 0-100.
|
| 122 |
-
- `pointDeductions`: (Object) An object where keys are the main rubric categories ("Content & Analysis", "Organization & Structure", "APA Formatting & Citations", "Clarity & Mechanics") and values are the integer number of points lost for that category.
|
| 123 |
-
- `feedback`: (Object) An object with the same keys as `pointDeductions`, where values are brief string comments explaining the point deductions for that category. If no points are lost, the comment should be "No points deducted."
|
| 124 |
-
- `summary`: (String) A 2-3 sentence summary of the paper's performance and the rationale for the grade.
|
| 125 |
-
|
| 126 |
-
Do not deviate from this format.
|
| 127 |
-
|
| 128 |
**Example of the required JSON output format:**
|
| 129 |
{{
|
| 130 |
"finalGrade": 88,
|
|
@@ -142,13 +118,8 @@ Do not deviate from this format.
|
|
| 142 |
}},
|
| 143 |
"summary": "This is a strong paper with excellent critical analysis. The final grade was primarily impacted by significant APA formatting errors, which should be the main focus for improvement."
|
| 144 |
}}
|
| 145 |
-
|
| 146 |
-
---
|
| 147 |
-
**ESSAY TO GRADE:**
|
| 148 |
-
|
| 149 |
"""
|
| 150 |
|
| 151 |
-
|
| 152 |
@dataclass
|
| 153 |
class GradingResult:
|
| 154 |
"""Holds the structured result of a single graded essay."""
|
|
@@ -160,94 +131,43 @@ class GradingResult:
|
|
| 160 |
summary: Optional[str] = None
|
| 161 |
error_message: Optional[str] = None
|
| 162 |
|
| 163 |
-
|
| 164 |
# --- Core Logic Classes ---
|
| 165 |
-
|
| 166 |
-
class EssayParser:
|
| 167 |
-
"""Parses text content from a .docx file."""
|
| 168 |
-
@staticmethod
|
| 169 |
-
def parse_docx(file_path: str) -> str:
|
| 170 |
-
"""Extracts all text from a Word document."""
|
| 171 |
-
try:
|
| 172 |
-
doc = docx.Document(file_path)
|
| 173 |
-
return "\n".join([para.text for para in doc.paragraphs if para.text])
|
| 174 |
-
except Exception as e:
|
| 175 |
-
# Handles cases where the file is corrupted or not a valid docx
|
| 176 |
-
raise IOError(
|
| 177 |
-
f"Could not read file: {os.path.basename(file_path)}. Error: {e}")
|
| 178 |
-
|
| 179 |
-
@staticmethod
|
| 180 |
-
def parse_pdf(file_path: str) -> str:
|
| 181 |
-
"""Extracts all text from a PDF document."""
|
| 182 |
-
try:
|
| 183 |
-
doc = fitz.open(file_path)
|
| 184 |
-
text = ""
|
| 185 |
-
for page in doc:
|
| 186 |
-
text += page.get_text()
|
| 187 |
-
return text
|
| 188 |
-
except Exception as e:
|
| 189 |
-
raise IOError(
|
| 190 |
-
f"Could not read PDF file: {os.path.basename(file_path)}. Error: {e}")
|
| 191 |
-
|
| 192 |
-
|
| 193 |
|
| 194 |
class GeminiGrader:
|
| 195 |
"""Manages interaction with the Google Gemini API for grading."""
|
| 196 |
-
|
| 197 |
def __init__(self, api_key: str):
|
| 198 |
"""Initializes the Gemini model."""
|
| 199 |
try:
|
| 200 |
genai.configure(api_key=api_key)
|
| 201 |
-
|
| 202 |
-
generation_config = {
|
| 203 |
-
"temperature": 0.1,
|
| 204 |
-
"top_p": 0.95,
|
| 205 |
-
"top_k": 40,
|
| 206 |
-
}
|
| 207 |
-
# Safety settings to prevent the model from refusing to grade
|
| 208 |
-
safety_settings = [
|
| 209 |
-
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
|
| 210 |
-
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
|
| 211 |
-
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
| 212 |
-
"threshold": "BLOCK_NONE"},
|
| 213 |
-
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
| 214 |
-
"threshold": "BLOCK_NONE"},
|
| 215 |
-
]
|
| 216 |
-
self.model = genai.GenerativeModel(
|
| 217 |
-
model_name="gemini-1.5-pro-latest",
|
| 218 |
-
generation_config=generation_config,
|
| 219 |
-
safety_settings=safety_settings
|
| 220 |
-
)
|
| 221 |
except Exception as e:
|
| 222 |
raise ValueError(f"Failed to configure Gemini API: {e}")
|
| 223 |
|
| 224 |
-
def grade_essay(self,
|
| 225 |
"""
|
| 226 |
-
Sends
|
| 227 |
-
This
|
| 228 |
"""
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
|
|
|
|
|
|
| 234 |
|
| 235 |
-
prompt_with_essay = f"{final_prompt}\n{essay_text}"
|
| 236 |
try:
|
| 237 |
-
response = self.model.generate_content(
|
| 238 |
-
# Clean the response to ensure it's valid JSON
|
| 239 |
cleaned_response = response.text.strip().replace("```json", "").replace("```", "")
|
| 240 |
data = json.loads(cleaned_response)
|
| 241 |
|
| 242 |
-
|
| 243 |
-
required_keys = ["finalGrade",
|
| 244 |
-
"pointDeductions", "feedback", "summary"]
|
| 245 |
if not all(key in data for key in required_keys):
|
| 246 |
-
raise KeyError(
|
| 247 |
-
"The model's response was missing one or more required keys.")
|
| 248 |
|
| 249 |
return GradingResult(
|
| 250 |
-
file_name=
|
| 251 |
success=True,
|
| 252 |
grade=data["finalGrade"],
|
| 253 |
deductions=data["pointDeductions"],
|
|
@@ -256,163 +176,139 @@ class GeminiGrader:
|
|
| 256 |
)
|
| 257 |
except json.JSONDecodeError:
|
| 258 |
return GradingResult(
|
| 259 |
-
file_name=
|
| 260 |
success=False,
|
| 261 |
-
error_message="Failed to parse the model's response.
|
| 262 |
)
|
| 263 |
except Exception as e:
|
| 264 |
return GradingResult(
|
| 265 |
-
file_name=
|
| 266 |
success=False,
|
| 267 |
error_message=f"An API or model error occurred: {str(e)}"
|
| 268 |
)
|
| 269 |
|
| 270 |
-
|
| 271 |
# --- Gradio Application ---
|
| 272 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
async def grade_papers_concurrently(
|
| 274 |
files: List[gr.File], example_paper_file: gr.File, api_key: str, progress=gr.Progress(track_tqdm=True)
|
| 275 |
) -> (str, str):
|
| 276 |
"""
|
| 277 |
The main asynchronous function that orchestrates the grading process.
|
| 278 |
-
It's triggered by the Gradio button click.
|
| 279 |
"""
|
| 280 |
start_time = time.time()
|
| 281 |
-
|
| 282 |
if not api_key:
|
| 283 |
raise gr.Error("Google API Key is required.")
|
| 284 |
if not files:
|
| 285 |
-
raise gr.Error("Please upload at least one
|
| 286 |
-
|
| 287 |
-
# Process the optional example paper
|
| 288 |
-
example_paper_text = "No example paper provided."
|
| 289 |
-
if example_paper_file:
|
| 290 |
-
try:
|
| 291 |
-
progress(0, desc="Parsing example paper...")
|
| 292 |
-
example_paper_text = EssayParser.parse_pdf(example_paper_file.name)
|
| 293 |
-
except IOError as e:
|
| 294 |
-
raise gr.Error(f"Failed to process example PDF: {e}")
|
| 295 |
|
|
|
|
| 296 |
try:
|
| 297 |
grader = GeminiGrader(api_key)
|
| 298 |
-
except ValueError as e:
|
| 299 |
-
raise gr.Error(str(e))
|
| 300 |
-
|
| 301 |
-
file_paths = [file.name for file in files]
|
| 302 |
-
total_files = len(file_paths)
|
| 303 |
-
|
| 304 |
-
# Use a ThreadPoolExecutor to run synchronous tasks concurrently
|
| 305 |
-
with ThreadPoolExecutor(max_workers=1) as executor:
|
| 306 |
-
# Create a future for each file processing task
|
| 307 |
-
loop = asyncio.get_event_loop()
|
| 308 |
-
tasks = [
|
| 309 |
-
loop.run_in_executor(
|
| 310 |
-
executor,
|
| 311 |
-
process_single_file,
|
| 312 |
-
file_path,
|
| 313 |
-
grader,
|
| 314 |
-
example_paper_text
|
| 315 |
-
)
|
| 316 |
-
for file_path in file_paths
|
| 317 |
-
]
|
| 318 |
-
|
| 319 |
-
results = []
|
| 320 |
-
# Process results as they are completed
|
| 321 |
-
for i, future in enumerate(asyncio.as_completed(tasks)):
|
| 322 |
-
progress(i + 1, desc=f"Grading paper {i+1}/{total_files}...")
|
| 323 |
-
result = await future
|
| 324 |
-
results.append(result)
|
| 325 |
-
|
| 326 |
-
# --- Format the final output ---
|
| 327 |
-
successful_grades = [res for res in results if res.success]
|
| 328 |
-
failed_grades = [res for res in results if not res.success]
|
| 329 |
-
|
| 330 |
-
output_markdown = ""
|
| 331 |
-
for result in successful_grades:
|
| 332 |
-
output_markdown += f"### ✅ Grade for: **{result.file_name}**\n"
|
| 333 |
-
output_markdown += f"**Final Grade:** {result.grade}/100\n\n"
|
| 334 |
-
|
| 335 |
-
# Format point deductions
|
| 336 |
-
deductions_str = ""
|
| 337 |
-
for category, points in result.deductions.items():
|
| 338 |
-
if points > 0:
|
| 339 |
-
deductions_str += f"- **{category}:** Lost {points} points. *Reason: {result.feedback.get(category, 'N/A')}*\n"
|
| 340 |
-
if not deductions_str:
|
| 341 |
-
deductions_str = "Excellent work! No points were deducted.\n"
|
| 342 |
-
|
| 343 |
-
output_markdown += "**Point Deductions Breakdown:**\n" + deductions_str + "\n"
|
| 344 |
-
output_markdown += f"**Summary:** {result.summary}\n"
|
| 345 |
-
output_markdown += "---\n"
|
| 346 |
-
|
| 347 |
-
if failed_grades:
|
| 348 |
-
output_markdown += "### ❌ Failed Papers\n"
|
| 349 |
-
for result in failed_grades:
|
| 350 |
-
output_markdown += f"- **File:** {result.file_name}\n"
|
| 351 |
-
output_markdown += f" - **Error:** {result.error_message}\n"
|
| 352 |
-
output_markdown += "---\n"
|
| 353 |
-
|
| 354 |
-
end_time = time.time()
|
| 355 |
-
runtime = f"Total runtime: {end_time - start_time:.2f} seconds."
|
| 356 |
-
|
| 357 |
-
status = (
|
| 358 |
-
f"Grading complete. {len(successful_grades)} papers graded successfully, "
|
| 359 |
-
f"{len(failed_grades)} failed."
|
| 360 |
-
)
|
| 361 |
-
|
| 362 |
-
return output_markdown, f"{status}\n{runtime}"
|
| 363 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
if not
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
|
| 383 |
|
| 384 |
# --- Build the Gradio Interface ---
|
| 385 |
-
|
| 386 |
with gr.Blocks(theme=gr.themes.Soft(), title="Nursing Essay Grader") as demo:
|
| 387 |
gr.Markdown(
|
| 388 |
"""
|
| 389 |
# 📝 Gemini-Powered Nursing Essay Grader
|
| 390 |
-
Upload one or more student essays
|
| 391 |
-
1. Enter your Google API Key
|
| 392 |
-
2. Upload the `.docx` files you want to grade.
|
| 393 |
-
3. Optionally, upload a single `.pdf` file as a "perfect" example
|
| 394 |
-
4. Click "Grade All Papers".
|
| 395 |
"""
|
| 396 |
)
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
label="Google API Key",
|
| 400 |
-
placeholder="Enter your Google API Key here",
|
| 401 |
-
type="password",
|
| 402 |
-
scale=1
|
| 403 |
-
)
|
| 404 |
with gr.Row():
|
| 405 |
file_uploads = gr.File(
|
| 406 |
-
label="Upload
|
| 407 |
file_count="multiple",
|
| 408 |
-
file_types=[
|
| 409 |
type="filepath",
|
| 410 |
scale=2
|
| 411 |
)
|
| 412 |
example_paper_upload = gr.File(
|
| 413 |
-
label="Upload Example
|
| 414 |
file_count="single",
|
| 415 |
-
file_types=[
|
| 416 |
type="filepath",
|
| 417 |
scale=1
|
| 418 |
)
|
|
@@ -421,11 +317,7 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Nursing Essay Grader") as demo:
|
|
| 421 |
gr.Markdown("---")
|
| 422 |
gr.Markdown("## 📊 Grading Results")
|
| 423 |
results_output = gr.Markdown(label="Formatted Grades")
|
| 424 |
-
status_output = gr.Textbox(
|
| 425 |
-
label="Runtime Status",
|
| 426 |
-
lines=2,
|
| 427 |
-
interactive=False
|
| 428 |
-
)
|
| 429 |
|
| 430 |
grade_button.click(
|
| 431 |
fn=grade_papers_concurrently,
|
|
|
|
| 1 |
import gradio as gr
|
|
|
|
| 2 |
import asyncio
|
| 3 |
from concurrent.futures import ThreadPoolExecutor
|
| 4 |
from google import generativeai as genai
|
|
|
|
| 8 |
import os
|
| 9 |
from pathlib import Path
|
| 10 |
import time
|
| 11 |
+
# The docx and fitz imports are no longer needed for the core logic
|
| 12 |
|
| 13 |
GRADING_RUBRIC = """
|
| 14 |
GRADING RUBRIC (Total 100 points)
|
|
|
|
| 81 |
|
| 82 |
"""
|
| 83 |
|
| 84 |
+
# The prompt is now simpler, as the files are passed as arguments to the model, not as text.
|
| 85 |
BASE_PROMPT_TEMPLATE = """
|
| 86 |
You are about to assume a role. Carefully review the persona, context, task, and output requirements before proceeding.
|
| 87 |
|
| 88 |
1. PERSONA
|
|
|
|
| 89 |
You are Dr. Stone, a meticulous and experienced Associate Professor of Nursing at a major university. You have been teaching for over 20 years, with a specialization in medical-surgical nursing. You are known for your high standards, particularly regarding academic integrity and the strict application of the American Psychological Association (APA) 7th Edition formatting guidelines. Your feedback is always direct, precise, and aimed at preparing students for the rigorous documentation standards required in the healthcare profession. Your tone is professional, authoritative, and educational. You do not offer praise for meeting baseline expectations; you simply state that the requirements have been met. Your criticism is specific, referencing the exact rule or rubric criterion that was violated.
|
| 90 |
|
| 91 |
2. CONTEXT
|
| 92 |
+
You are grading an essay for your undergraduate course, NURS 310: Fundamentals of Nursing. The assignment is an essay on the topic of "Skin Lesions." You will be provided with the student's paper as an attached file, and optionally an example paper file for reference.
|
|
|
|
| 93 |
|
| 94 |
3. PRIMARY TASK
|
| 95 |
+
Your task is to analyze the attached student paper file and grade it with absolute precision according to the provided rubric. You must function as a perfect and unflinchingly accurate grader. There is no room for subjective interpretation or leniency. You will identify every error in content, formatting, spelling, grammar, and citation, and assign points strictly based on the rubric's quantitative thresholds.
|
|
|
|
| 96 |
|
| 97 |
Your output should be a single JSON format structure response as indicated by the example below.
|
| 98 |
|
| 99 |
**DO NOT** provide any introductory text, conversational pleasantries, or explanations outside of the requested JSON structure. Your entire response must be a single, valid JSON object.
|
| 100 |
|
| 101 |
+
**Use the following rubric to grade the attached student paper:**
|
| 102 |
{GRADING_RUBRIC}
|
| 103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
**Example of the required JSON output format:**
|
| 105 |
{{
|
| 106 |
"finalGrade": 88,
|
|
|
|
| 118 |
}},
|
| 119 |
"summary": "This is a strong paper with excellent critical analysis. The final grade was primarily impacted by significant APA formatting errors, which should be the main focus for improvement."
|
| 120 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
"""
|
| 122 |
|
|
|
|
| 123 |
@dataclass
|
| 124 |
class GradingResult:
|
| 125 |
"""Holds the structured result of a single graded essay."""
|
|
|
|
| 131 |
summary: Optional[str] = None
|
| 132 |
error_message: Optional[str] = None
|
| 133 |
|
|
|
|
| 134 |
# --- Core Logic Classes ---
|
| 135 |
+
# EssayParser class is no longer needed and has been removed.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
class GeminiGrader:
|
| 138 |
"""Manages interaction with the Google Gemini API for grading."""
|
|
|
|
| 139 |
def __init__(self, api_key: str):
|
| 140 |
"""Initializes the Gemini model."""
|
| 141 |
try:
|
| 142 |
genai.configure(api_key=api_key)
|
| 143 |
+
self.model = genai.GenerativeModel(model_name="gemini-1.5-pro-latest")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
except Exception as e:
|
| 145 |
raise ValueError(f"Failed to configure Gemini API: {e}")
|
| 146 |
|
| 147 |
+
def grade_essay(self, student_paper_file: object, example_paper_file: Optional[object]) -> GradingResult:
|
| 148 |
"""
|
| 149 |
+
Sends files to Gemini for grading and parses the JSON response.
|
| 150 |
+
This method now takes API file objects directly.
|
| 151 |
"""
|
| 152 |
+
prompt_text = BASE_PROMPT_TEMPLATE.format(GRADING_RUBRIC=GRADING_RUBRIC)
|
| 153 |
+
|
| 154 |
+
# Build the content list for the API call
|
| 155 |
+
# It includes the prompt text and the file objects
|
| 156 |
+
content_list = [prompt_text, student_paper_file]
|
| 157 |
+
if example_paper_file:
|
| 158 |
+
content_list.append(example_paper_file)
|
| 159 |
|
|
|
|
| 160 |
try:
|
| 161 |
+
response = self.model.generate_content(content_list)
|
|
|
|
| 162 |
cleaned_response = response.text.strip().replace("```json", "").replace("```", "")
|
| 163 |
data = json.loads(cleaned_response)
|
| 164 |
|
| 165 |
+
required_keys = ["finalGrade", "pointDeductions", "feedback", "summary"]
|
|
|
|
|
|
|
| 166 |
if not all(key in data for key in required_keys):
|
| 167 |
+
raise KeyError("Model response missing required keys.")
|
|
|
|
| 168 |
|
| 169 |
return GradingResult(
|
| 170 |
+
file_name=student_paper_file.display_name,
|
| 171 |
success=True,
|
| 172 |
grade=data["finalGrade"],
|
| 173 |
deductions=data["pointDeductions"],
|
|
|
|
| 176 |
)
|
| 177 |
except json.JSONDecodeError:
|
| 178 |
return GradingResult(
|
| 179 |
+
file_name=student_paper_file.display_name,
|
| 180 |
success=False,
|
| 181 |
+
error_message="Failed to parse the model's JSON response."
|
| 182 |
)
|
| 183 |
except Exception as e:
|
| 184 |
return GradingResult(
|
| 185 |
+
file_name=student_paper_file.display_name,
|
| 186 |
success=False,
|
| 187 |
error_message=f"An API or model error occurred: {str(e)}"
|
| 188 |
)
|
| 189 |
|
|
|
|
| 190 |
# --- Gradio Application ---
|
| 191 |
|
| 192 |
+
def process_single_file(file_path: str, grader: GeminiGrader, example_paper_file_obj: Optional[object]) -> GradingResult:
|
| 193 |
+
"""
|
| 194 |
+
Uploads a single student paper and calls the grader.
|
| 195 |
+
This function is what runs in each thread of the ThreadPoolExecutor.
|
| 196 |
+
"""
|
| 197 |
+
student_paper_file_obj = None
|
| 198 |
+
try:
|
| 199 |
+
# Upload the student paper file for this specific job
|
| 200 |
+
student_paper_file_obj = genai.upload_file(path=file_path, display_name=os.path.basename(file_path))
|
| 201 |
+
return grader.grade_essay(student_paper_file_obj, example_paper_file_obj)
|
| 202 |
+
except Exception as e:
|
| 203 |
+
return GradingResult(file_name=os.path.basename(file_path), success=False, error_message=str(e))
|
| 204 |
+
finally:
|
| 205 |
+
# Clean up the uploaded student paper file after processing
|
| 206 |
+
if student_paper_file_obj:
|
| 207 |
+
genai.delete_file(student_paper_file_obj.name)
|
| 208 |
+
|
| 209 |
async def grade_papers_concurrently(
|
| 210 |
files: List[gr.File], example_paper_file: gr.File, api_key: str, progress=gr.Progress(track_tqdm=True)
|
| 211 |
) -> (str, str):
|
| 212 |
"""
|
| 213 |
The main asynchronous function that orchestrates the grading process.
|
|
|
|
| 214 |
"""
|
| 215 |
start_time = time.time()
|
|
|
|
| 216 |
if not api_key:
|
| 217 |
raise gr.Error("Google API Key is required.")
|
| 218 |
if not files:
|
| 219 |
+
raise gr.Error("Please upload at least one paper to grade.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
|
| 221 |
+
example_paper_file_obj = None
|
| 222 |
try:
|
| 223 |
grader = GeminiGrader(api_key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
|
| 225 |
+
# Upload the example paper ONCE if it exists
|
| 226 |
+
if example_paper_file:
|
| 227 |
+
progress(0, desc="Uploading example paper...")
|
| 228 |
+
example_paper_file_obj = genai.upload_file(path=example_paper_file.name, display_name=os.path.basename(example_paper_file.name))
|
| 229 |
+
|
| 230 |
+
file_paths = [file.name for file in files]
|
| 231 |
+
total_files = len(file_paths)
|
| 232 |
+
|
| 233 |
+
with ThreadPoolExecutor(max_workers=1) as executor:
|
| 234 |
+
loop = asyncio.get_event_loop()
|
| 235 |
+
tasks = [
|
| 236 |
+
loop.run_in_executor(
|
| 237 |
+
executor,
|
| 238 |
+
process_single_file,
|
| 239 |
+
file_path,
|
| 240 |
+
grader,
|
| 241 |
+
example_paper_file_obj
|
| 242 |
+
)
|
| 243 |
+
for file_path in file_paths
|
| 244 |
+
]
|
| 245 |
|
| 246 |
+
results = []
|
| 247 |
+
for i, future in enumerate(asyncio.as_completed(tasks)):
|
| 248 |
+
progress(i + 1, desc=f"Grading paper {i+1}/{total_files}...")
|
| 249 |
+
result = await future
|
| 250 |
+
results.append(result)
|
| 251 |
+
|
| 252 |
+
# --- Format the final output ---
|
| 253 |
+
successful_grades = [res for res in results if res.success]
|
| 254 |
+
failed_grades = [res for res in results if not res.success]
|
| 255 |
+
output_markdown = ""
|
| 256 |
+
for result in successful_grades:
|
| 257 |
+
output_markdown += f"### ✅ Grade for: **{result.file_name}**\n"
|
| 258 |
+
output_markdown += f"**Final Grade:** {result.grade}/100\n\n"
|
| 259 |
+
deductions_str = ""
|
| 260 |
+
for category, points in result.deductions.items():
|
| 261 |
+
if points > 0:
|
| 262 |
+
deductions_str += f"- **{category}:** Lost {points} points. *Reason: {result.feedback.get(category, 'N/A')}*\n"
|
| 263 |
+
if not deductions_str:
|
| 264 |
+
deductions_str = "Excellent work! No points were deducted.\n"
|
| 265 |
+
output_markdown += "**Point Deductions Breakdown:**\n" + deductions_str + "\n"
|
| 266 |
+
output_markdown += f"**Summary:** {result.summary}\n"
|
| 267 |
+
output_markdown += "---\n"
|
| 268 |
+
if failed_grades:
|
| 269 |
+
output_markdown += "### ❌ Failed Papers\n"
|
| 270 |
+
for result in failed_grades:
|
| 271 |
+
output_markdown += f"- **File:** {result.file_name}\n"
|
| 272 |
+
output_markdown += f" - **Error:** {result.error_message}\n"
|
| 273 |
+
output_markdown += "---\n"
|
| 274 |
+
|
| 275 |
+
end_time = time.time()
|
| 276 |
+
runtime = f"Total runtime: {end_time - start_time:.2f} seconds."
|
| 277 |
+
status = f"Grading complete. {len(successful_grades)} papers graded successfully, {len(failed_grades)} failed."
|
| 278 |
+
return output_markdown, f"{status}\n{runtime}"
|
| 279 |
+
|
| 280 |
+
finally:
|
| 281 |
+
# Clean up the uploaded example paper file at the very end
|
| 282 |
+
if example_paper_file_obj:
|
| 283 |
+
genai.delete_file(example_paper_file_obj.name)
|
| 284 |
|
| 285 |
|
| 286 |
# --- Build the Gradio Interface ---
|
|
|
|
| 287 |
with gr.Blocks(theme=gr.themes.Soft(), title="Nursing Essay Grader") as demo:
|
| 288 |
gr.Markdown(
|
| 289 |
"""
|
| 290 |
# 📝 Gemini-Powered Nursing Essay Grader
|
| 291 |
+
Upload one or more student essays to have them graded by AI.
|
| 292 |
+
1. Enter your Google API Key.
|
| 293 |
+
2. Upload the `.docx` or `.pdf` files you want to grade.
|
| 294 |
+
3. Optionally, upload a single `.docx` or `.pdf` file as a "perfect" example.
|
| 295 |
+
4. Click "Grade All Papers".
|
| 296 |
"""
|
| 297 |
)
|
| 298 |
+
api_key_input = gr.Textbox(label="Google API Key", placeholder="Enter your Google API Key here", type="password")
|
| 299 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
with gr.Row():
|
| 301 |
file_uploads = gr.File(
|
| 302 |
+
label="Upload Essays to Grade",
|
| 303 |
file_count="multiple",
|
| 304 |
+
file_types=['.pdf', '.docx'], # Allow both types
|
| 305 |
type="filepath",
|
| 306 |
scale=2
|
| 307 |
)
|
| 308 |
example_paper_upload = gr.File(
|
| 309 |
+
label="Upload Example Paper (Optional)",
|
| 310 |
file_count="single",
|
| 311 |
+
file_types=['.pdf', '.docx'], # Allow both types
|
| 312 |
type="filepath",
|
| 313 |
scale=1
|
| 314 |
)
|
|
|
|
| 317 |
gr.Markdown("---")
|
| 318 |
gr.Markdown("## 📊 Grading Results")
|
| 319 |
results_output = gr.Markdown(label="Formatted Grades")
|
| 320 |
+
status_output = gr.Textbox(label="Runtime Status", lines=2, interactive=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
|
| 322 |
grade_button.click(
|
| 323 |
fn=grade_papers_concurrently,
|