Spaces:
Sleeping
Sleeping
Commit
Β·
a0118d2
1
Parent(s):
e100fec
this is the final product that probably won't ever be used, but that's alright
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
|
|
@@ -10,322 +9,320 @@ import os
|
|
| 10 |
from pathlib import Path
|
| 11 |
import time
|
| 12 |
|
|
|
|
| 13 |
GRADING_RUBRIC = """
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
3
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
"""
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
|
|
|
| 39 |
|
| 40 |
-
|
| 41 |
|
| 42 |
-
**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
{GRADING_RUBRIC}
|
| 44 |
|
| 45 |
-
**
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
3. Calculate the total points lost and the final grade out of 100.
|
| 49 |
-
4. Provide brief, specific comments explaining why points were deducted in each category.
|
| 50 |
-
5. Write a 2-3 sentence summary of the overall grade.
|
| 51 |
-
6. Format your entire output as a single JSON object with the following keys and value types:
|
| 52 |
-
- `finalGrade`: (Integer) The final score from 0-100.
|
| 53 |
-
- `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.
|
| 54 |
-
- `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."
|
| 55 |
-
- `summary`: (String) A 2-3 sentence summary of the paper's performance and the rationale for the grade.
|
| 56 |
|
| 57 |
-
**
|
| 58 |
{{
|
| 59 |
-
"
|
| 60 |
-
"
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
}}
|
| 74 |
-
|
| 75 |
-
---
|
| 76 |
-
**ESSAY TO GRADE:**
|
| 77 |
-
|
| 78 |
"""
|
| 79 |
|
| 80 |
-
|
| 81 |
@dataclass
|
| 82 |
class GradingResult:
|
| 83 |
"""Holds the structured result of a single graded essay."""
|
| 84 |
file_name: str
|
| 85 |
success: bool
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
feedback: Dict[str, str] = field(default_factory=dict)
|
| 89 |
-
summary: Optional[str] = None
|
| 90 |
error_message: Optional[str] = None
|
| 91 |
|
| 92 |
-
|
| 93 |
-
# --- Core Logic Classes ---
|
| 94 |
-
|
| 95 |
-
class EssayParser:
|
| 96 |
-
"""Parses text content from a .docx file."""
|
| 97 |
-
@staticmethod
|
| 98 |
-
def parse_docx(file_path: str) -> str:
|
| 99 |
-
"""Extracts all text from a Word document."""
|
| 100 |
-
try:
|
| 101 |
-
doc = docx.Document(file_path)
|
| 102 |
-
return "\n".join([para.text for para in doc.paragraphs if para.text])
|
| 103 |
-
except Exception as e:
|
| 104 |
-
# Handles cases where the file is corrupted or not a valid docx
|
| 105 |
-
raise IOError(
|
| 106 |
-
f"Could not read file: {os.path.basename(file_path)}. Error: {e}")
|
| 107 |
-
|
| 108 |
-
|
| 109 |
class GeminiGrader:
|
| 110 |
"""Manages interaction with the Google Gemini API for grading."""
|
| 111 |
-
|
| 112 |
def __init__(self, api_key: str):
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
try:
|
| 115 |
-
|
| 116 |
-
# Configuration for safer, more deterministic output
|
| 117 |
-
generation_config = {
|
| 118 |
-
"temperature": 0.1,
|
| 119 |
-
"top_p": 0.95,
|
| 120 |
-
"top_k": 40,
|
| 121 |
-
}
|
| 122 |
-
# Safety settings to prevent the model from refusing to grade
|
| 123 |
-
safety_settings = [
|
| 124 |
-
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
|
| 125 |
-
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
|
| 126 |
-
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
| 127 |
-
"threshold": "BLOCK_NONE"},
|
| 128 |
-
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
| 129 |
-
"threshold": "BLOCK_NONE"},
|
| 130 |
-
]
|
| 131 |
-
self.model = genai.GenerativeModel(
|
| 132 |
-
model_name="gemini-1.5-pro-latest",
|
| 133 |
-
generation_config=generation_config,
|
| 134 |
-
safety_settings=safety_settings
|
| 135 |
-
)
|
| 136 |
-
except Exception as e:
|
| 137 |
-
raise ValueError(f"Failed to configure Gemini API: {e}")
|
| 138 |
-
|
| 139 |
-
def grade_essay(self, essay_text: str, file_name: str) -> GradingResult:
|
| 140 |
-
"""
|
| 141 |
-
Sends the essay to Gemini for grading and parses the JSON response.
|
| 142 |
-
This is a synchronous method designed to be run in a thread pool.
|
| 143 |
-
"""
|
| 144 |
-
prompt_with_essay = f"{GEMINI_PROMPT}\n{essay_text}"
|
| 145 |
-
try:
|
| 146 |
-
response = self.model.generate_content(prompt_with_essay)
|
| 147 |
-
# Clean the response to ensure it's valid JSON
|
| 148 |
cleaned_response = response.text.strip().replace("```json", "").replace("```", "")
|
| 149 |
data = json.loads(cleaned_response)
|
| 150 |
|
| 151 |
-
#
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
raise KeyError(
|
| 156 |
-
"The model's response was missing one or more required keys.")
|
| 157 |
-
|
| 158 |
return GradingResult(
|
| 159 |
-
file_name=
|
| 160 |
success=True,
|
| 161 |
-
|
| 162 |
-
deductions=data["pointDeductions"],
|
| 163 |
-
feedback=data["feedback"],
|
| 164 |
-
summary=data["summary"]
|
| 165 |
-
)
|
| 166 |
-
except json.JSONDecodeError:
|
| 167 |
-
return GradingResult(
|
| 168 |
-
file_name=file_name,
|
| 169 |
-
success=False,
|
| 170 |
-
error_message="Failed to parse the model's response. The output was not valid JSON."
|
| 171 |
)
|
|
|
|
|
|
|
|
|
|
| 172 |
except Exception as e:
|
| 173 |
-
return GradingResult(
|
| 174 |
-
file_name=file_name,
|
| 175 |
-
success=False,
|
| 176 |
-
error_message=f"An API or model error occurred: {str(e)}"
|
| 177 |
-
)
|
| 178 |
-
|
| 179 |
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
|
| 182 |
async def grade_papers_concurrently(
|
| 183 |
-
files: List[gr.File], api_key: str, progress=gr.Progress(track_tqdm=True)
|
| 184 |
) -> (str, str):
|
| 185 |
-
"""
|
| 186 |
-
The main asynchronous function that orchestrates the grading process.
|
| 187 |
-
It's triggered by the Gradio button click.
|
| 188 |
-
"""
|
| 189 |
start_time = time.time()
|
|
|
|
|
|
|
| 190 |
|
| 191 |
-
|
| 192 |
-
raise gr.Error("Google API Key is required.")
|
| 193 |
-
if not files:
|
| 194 |
-
raise gr.Error("Please upload at least one Word document.")
|
| 195 |
-
|
| 196 |
try:
|
| 197 |
grader = GeminiGrader(api_key)
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
output_markdown
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
for result in failed_grades:
|
| 249 |
-
output_markdown += f"- **File:** {result.file_name}\n"
|
| 250 |
-
output_markdown += f" - **Error:** {result.error_message}\n"
|
| 251 |
-
output_markdown += "---\n"
|
| 252 |
-
|
| 253 |
-
end_time = time.time()
|
| 254 |
-
runtime = f"Total runtime: {end_time - start_time:.2f} seconds."
|
| 255 |
-
|
| 256 |
-
status = (
|
| 257 |
-
f"Grading complete. {len(successful_grades)} papers graded successfully, "
|
| 258 |
-
f"{len(failed_grades)} failed."
|
| 259 |
-
)
|
| 260 |
-
|
| 261 |
-
return output_markdown, f"{status}\n{runtime}"
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
def process_single_file(file_path: str, grader: GeminiGrader) -> GradingResult:
|
| 265 |
-
"""
|
| 266 |
-
Synchronous wrapper function to parse and grade one file.
|
| 267 |
-
This function is what runs in each thread of the ThreadPoolExecutor.
|
| 268 |
-
"""
|
| 269 |
-
file_name = os.path.basename(file_path)
|
| 270 |
-
try:
|
| 271 |
-
essay_text = EssayParser.parse_docx(file_path)
|
| 272 |
-
if not essay_text.strip():
|
| 273 |
-
return GradingResult(
|
| 274 |
-
file_name=file_name,
|
| 275 |
-
success=False,
|
| 276 |
-
error_message="The document is empty or contains no readable text."
|
| 277 |
-
)
|
| 278 |
-
return grader.grade_essay(essay_text, file_name)
|
| 279 |
-
except Exception as e:
|
| 280 |
-
return GradingResult(file_name=file_name, success=False, error_message=str(e))
|
| 281 |
-
|
| 282 |
|
| 283 |
# --- Build the Gradio Interface ---
|
| 284 |
-
|
| 285 |
with gr.Blocks(theme=gr.themes.Soft(), title="Nursing Essay Grader") as demo:
|
| 286 |
gr.Markdown(
|
| 287 |
"""
|
| 288 |
# π Gemini-Powered Nursing Essay Grader
|
| 289 |
-
Upload one or more student essays
|
| 290 |
-
1. Enter your Google API Key
|
| 291 |
-
2. Upload the `.docx` files.
|
| 292 |
-
3.
|
|
|
|
| 293 |
"""
|
| 294 |
)
|
| 295 |
-
|
| 296 |
with gr.Row():
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
placeholder="Enter your Google API Key here",
|
| 300 |
-
type="password",
|
| 301 |
-
scale=1
|
| 302 |
-
)
|
| 303 |
-
|
| 304 |
-
file_uploads = gr.File(
|
| 305 |
-
label="Upload Word Document Essays",
|
| 306 |
-
file_count="multiple",
|
| 307 |
-
file_types=[".docx"],
|
| 308 |
-
type="filepath" # Use filepath for easier handling
|
| 309 |
-
)
|
| 310 |
-
|
| 311 |
grade_button = gr.Button("π Grade All Papers", variant="primary")
|
| 312 |
-
|
| 313 |
gr.Markdown("---")
|
| 314 |
gr.Markdown("## π Grading Results")
|
| 315 |
-
|
| 316 |
results_output = gr.Markdown(label="Formatted Grades")
|
|
|
|
| 317 |
|
| 318 |
-
|
| 319 |
-
label="Runtime Status",
|
| 320 |
-
lines=2,
|
| 321 |
-
interactive=False
|
| 322 |
-
)
|
| 323 |
-
|
| 324 |
-
grade_button.click(
|
| 325 |
-
fn=grade_papers_concurrently,
|
| 326 |
-
inputs=[file_uploads, api_key_input],
|
| 327 |
-
outputs=[results_output, status_output]
|
| 328 |
-
)
|
| 329 |
|
| 330 |
if __name__ == "__main__":
|
| 331 |
demo.launch(debug=True)
|
|
|
|
| 1 |
import gradio as gr
|
|
|
|
| 2 |
import asyncio
|
| 3 |
from concurrent.futures import ThreadPoolExecutor
|
| 4 |
from google import generativeai as genai
|
|
|
|
| 9 |
from pathlib import Path
|
| 10 |
import time
|
| 11 |
|
| 12 |
+
# The original, verbose rubric has been restored to provide maximum detail to the model.
|
| 13 |
GRADING_RUBRIC = """
|
| 14 |
+
DETAILED GRADING RUBRIC & INSTRUCTIONS
|
| 15 |
+
You will assess the paper against the following 16 criteria. For each criterion, you will determine the student's score based on a forensic analysis of their work.
|
| 16 |
+
I. APA Formatting (Total 60 points)
|
| 17 |
+
1. APA Title Page (5 pts):
|
| 18 |
+
Check for the 9 required components of a student title page. For this task, the 9 components are defined as: (1) Page number in the header (Page 1), (2) Paper title (bolded), (3) Author's name, (4) Department name, (5) University name, (6) Course number and name, (7) Instructor's name, (8) Assignment due date, (9) All elements are centered and correctly spaced in the upper half of the page.
|
| 19 |
+
Scoring:
|
| 20 |
+
5 pts: All 9 components are present and correctly formatted.
|
| 21 |
+
0 pts: 1 or more components are missing or incorrectly formatted.
|
| 22 |
+
2. APA General Guidelines - Main Body (5 pts):
|
| 23 |
+
Check for adherence to all of the following:
|
| 24 |
+
(1) Paper is typed.
|
| 25 |
+
(2) 1-inch margins on all sides.
|
| 26 |
+
(3) Font is either 11 pt Calibri or 12 pt Times New Roman (consistently).
|
| 27 |
+
The 3 additional main body text requirements are: (4) All text is double-spaced, (5) Text is aligned to the left margin (not justified), (6) The first line of every paragraph is indented 0.5 inches.
|
| 28 |
+
Scoring:
|
| 29 |
+
5 pts: All 6 guidelines are met perfectly.
|
| 30 |
+
2 pts: 1 guideline is not met.
|
| 31 |
+
0 pts: 2 or more guidelines are not met.
|
| 32 |
+
3. APA Text - Main Body Misc. Errors (5 pts):
|
| 33 |
+
Scan the entire document for the following: (1) Extra spacing between paragraphs, (2) Misspellings, (3) Typographical errors.
|
| 34 |
+
Scoring:
|
| 35 |
+
5 pts: 0 errors found.
|
| 36 |
+
4 pts: Exactly 1 error found.
|
| 37 |
+
0 pts: 2 or more errors found.
|
| 38 |
+
4. APA In-text Citations - Presence (5 pts):
|
| 39 |
+
Analyze the text: Identify every statement of fact, statistic, or opinion that is not common knowledge and requires a citation. Count how many of these required citations are missing.
|
| 40 |
+
Scoring:
|
| 41 |
+
5 pts: 0 missing in-text citations.
|
| 42 |
+
3.5 pts: 1 to 5 missing in-text citations.
|
| 43 |
+
0 pts: 6 or more missing in-text citations.
|
| 44 |
+
5. APA In-text Citation - Formatting (5 pts):
|
| 45 |
+
Analyze every provided in-text citation. Check for correct APA 7th Ed. format (e.g., (Author, Year) for parenthetical; Author (Year) for narrative; et al. usage; multiple authors). Count every single formatting error.
|
| 46 |
+
Scoring:
|
| 47 |
+
5 pts: 0 formatting errors.
|
| 48 |
+
2 pts: 1 to 2 formatting errors.
|
| 49 |
+
0 pts: 3 or more formatting errors.
|
| 50 |
+
6. APA Reference Page - General Formatting (5 pts):
|
| 51 |
+
Check for the following: (1) The title "References" is on a new page, centered, and bolded. (2) All entries are alphabetized by the first author's last name. (3) A 0.5-inch hanging indent is applied to all entries.
|
| 52 |
+
Scoring:
|
| 53 |
+
5 pts: The entire page meets all 3 general formatting expectations.
|
| 54 |
+
0 pts: 1 or more errors are present.
|
| 55 |
+
7. APA Reference Page - Line Spacing (5 pts):
|
| 56 |
+
Check for the following: The entire reference page, including between entries, is uniformly double-spaced.
|
| 57 |
+
Scoring:
|
| 58 |
+
5 pts: The entire page adheres to APA double-spacing rules.
|
| 59 |
+
0 pts: 1 or more line spacing errors are present (e.g., single spacing, extra space between entries).
|
| 60 |
+
8. APA References - Scholarly & Timely (10 pts):
|
| 61 |
+
Analyze the reference list. A "scholarly" reference is a peer-reviewed journal article, an academic book/chapter, or a publication from a professional organization (e.g., CDC, WHO). A non-scholarly source would be a general website, blog, or news article. A timely reference is one published within the last 7 years (i.e., between July 20, 2018, and July 19, 2025).
|
| 62 |
+
Check for two conditions: (1) Are there at least 3 scholarly references? AND (2) Are all of those references dated within the last 7 years?
|
| 63 |
+
- For the Justification, you MUST first list the publication years of all references found. Then, state that the current grading year is 2025. Finally, provide your judgment based on these facts.
|
| 64 |
+
Scoring:
|
| 65 |
+
10 pts: At least 3 scholarly references are cited, AND all references are dated within the last 7 years.
|
| 66 |
+
0 pts: Fewer than 3 scholarly references are cited, OR one or more references are older than 7 years.
|
| 67 |
+
9. APA References - Author Names (5 pts):
|
| 68 |
+
Analyze all author names on the reference page. Check for correct format: Lastname, F. M.. Also check for correct handling of sources with no author (the title or organization name moves to the author position).
|
| 69 |
+
Scoring:
|
| 70 |
+
5 pts: All author names are formatted perfectly.
|
| 71 |
+
2 pts: There is 1 error where an author's name should have been a title/publisher, or vice versa.
|
| 72 |
+
0 pts: There are general, repeated formatting errors in author names.
|
| 73 |
+
10. APA References - Dates (5 pts):
|
| 74 |
+
Analyze the date for each reference. Check for correct APA 7th Ed. format (e.g., (Year). for journals/books; (Year, Month Day). for web sources). Count every error.
|
| 75 |
+
Scoring:
|
| 76 |
+
5 pts: 0 errors.
|
| 77 |
+
3 pts: Exactly 1 error.
|
| 78 |
+
0 pts: 2 or more errors.
|
| 79 |
+
11. APA References - Capitalization (5 pts):
|
| 80 |
+
Analyze the titles in each reference. Check for correct APA 7th Ed. capitalization rules: Sentence case for article and book titles. Title case for periodical (journal) titles. Count every error.
|
| 81 |
+
Scoring:
|
| 82 |
+
5 pts: 0 errors.
|
| 83 |
+
3 pts: Exactly 1 error.
|
| 84 |
+
0 pts: 2 or more errors.
|
| 85 |
+
12. APA References - Italics (5 pts):
|
| 86 |
+
Analyze each reference for correct italicization. Check for APA 7th Ed. rules: Italicize journal titles and volume numbers. Italicize book titles. Count every error.
|
| 87 |
+
Scoring:
|
| 88 |
+
5 pts: 0 or 1 error in italicization.
|
| 89 |
+
0 pts: 2 or more errors in italicization.
|
| 90 |
+
13. APA References - Hyperlinks (5 pts):
|
| 91 |
+
Analyze all DOIs and URLs. The 4 guidelines are: (1) All DOIs are presented as a full, active hyperlink (e.g., https://doi.org/...). (2) The phrase "Retrieved from" is NOT used before a URL or DOI. (3) There is no period after the DOI or URL. (4) URLs that are not DOIs are included.
|
| 92 |
+
Scoring:
|
| 93 |
+
5 pts: All 4 guidelines are followed for all relevant references.
|
| 94 |
+
2 pts: 1 of the 4 guidelines is not followed.
|
| 95 |
+
0 pts: 2 or more of the 4 guidelines are not followed.
|
| 96 |
+
II. Content (Total 40 points)
|
| 97 |
+
14. Skin Lesion: Introduction (10 pts):
|
| 98 |
+
Analyze the introduction. Check that it: (1) Is one or two paragraphs long. (2) Clearly introduces the topic of skin lesions and states the issue to be examined. (3) Is not more than one page long. (4) Is free of spelling/typographical errors.
|
| 99 |
+
Scoring:
|
| 100 |
+
10 pts: Meets all requirements with 0 spelling/typo errors.
|
| 101 |
+
5 pts: Meets length/content requirements but has 1-2 spelling/typo errors.
|
| 102 |
+
0 pts: Does not include an introduction, OR the introduction is more than 1 page long, OR it has 3 or more spelling/typographical errors.
|
| 103 |
+
15. Skin Lesion: Common Causes (10 pts):
|
| 104 |
+
Analyze the body of the essay. Check if the paper discusses at least one common cause of skin lesions (e.g., infection, trauma, allergic reactions, systemic disease).
|
| 105 |
+
Scoring:
|
| 106 |
+
10 pts: Discusses at least 1 common cause.
|
| 107 |
+
0 pts: Does not discuss any common causes.
|
| 108 |
+
16. Skin Lesion: Nursing Considerations (10 pts):
|
| 109 |
+
Analyze the body of the essay. Check if the paper discusses at least one specific nursing consideration or intervention for patients with skin lesions (e.g., wound care, patient education, assessment techniques like ABCDE for melanoma, comfort measures).
|
| 110 |
+
Scoring:
|
| 111 |
+
10 pts: Discusses at least 1 nursing intervention/consideration.
|
| 112 |
+
0 pts: Does not discuss any nursing interventions/considerations.
|
| 113 |
+
REQUIRED OUTPUT FORMAT
|
| 114 |
+
You must present your final evaluation in the following structured format. Do not deviate from this format.
|
| 115 |
+
Grading Evaluation: NURS 305 Essay - Skin Lesions
|
| 116 |
+
Student Submission Analysis
|
| 117 |
+
Final Score: [Total Points] / 100
|
| 118 |
+
Part I: APA Formatting (Score: [Points] / 60)
|
| 119 |
+
1. APA Title Page: [Score]/5. Justification: [State precisely why the score was given. E.g., "Met all 9 requirements." or "0 points awarded. The title page was missing the course number and instructor's name."]
|
| 120 |
+
2. APA General Guidelines: [Score]/5. Justification: [E.g., "5 points awarded. The document used 12 pt Times New Roman, 1-inch margins, double-spacing, left alignment, and paragraph indents." or "2 points awarded. The left and right margins were set to 1.25 inches instead of the required 1 inch."]
|
| 121 |
+
3. APA Text - Misc. Errors: [Score]/5. Justification: [E.g., "4 points awarded. One typographical error ('hte' instead of 'the') was noted in paragraph 3."]
|
| 122 |
+
4. APA In-text Citations - Presence: [Score]/5. Justification: [E.g., "3.5 points awarded. Analysis identified 4 statements requiring a citation that were not cited."]
|
| 123 |
+
5. APA In-text Citation - Formatting: [Score]/5. Justification: [E.g., "2 points awarded. Two citations used an ampersand in the narrative format (e.g., 'Smith & Jones (2022) found...') which is incorrect."]
|
| 124 |
+
6. APA Reference Page - General Formatting: [Score]/5. Justification: [E.g., "0 points awarded. The title 'References' was not bolded, and a hanging indent was not used."]
|
| 125 |
+
7. APA Reference Page - Line Spacing: [Score]/5. Justification: [E.g., "0 points awarded. An extra space was added between each reference entry."]
|
| 126 |
+
8. APA References - Scholarly & Timely: [Score]/10. Justification: [E.g., "10 points awarded. The paper cited 4 peer-reviewed journal articles, all published between 2020 and 2024." or "0 points awarded. Only two scholarly sources were used, and one reference was from 2016."]
|
| 127 |
+
9. APA References - Author Names: [Score]/5. Justification: [E.g., "5 points awarded. All author names were formatted correctly."]
|
| 128 |
+
10. APA References - Dates: [Score]/5. Justification: [E.g., "3 points awarded. One reference was missing the period after the year: (2021) instead of (2021)."]
|
| 129 |
+
11. APA References - Capitalization: [Score]/5. Justification: [E.g., "0 points awarded. The titles of two journal articles were written in title case instead of sentence case."]
|
| 130 |
+
12. APA References - Italics: [Score]/5. Justification: [E.g., "5 points awarded. One error noted where a journal volume number was not italicized. This falls within the 0-1 error threshold for full points."]
|
| 131 |
+
13. APA References - Hyperlinks: [Score]/5. Justification: [E.g., "2 points awarded. The phrase 'Retrieved from' was incorrectly used before a URL."]
|
| 132 |
+
Part II: Content (Score: [Points] / 40)
|
| 133 |
+
14. Introduction: [Score]/10. Justification: [E.g., "10 points awarded. The introduction was a single, concise paragraph that clearly stated the paper's focus. No errors noted."]
|
| 134 |
+
15. Common Causes: [Score]/10. Justification: [E.g., "10 points awarded. The paper successfully discussed infectious agents as a common cause of skin lesions."]
|
| 135 |
+
16. Nursing Considerations: [Score]/10. Justification: [E.g., "0 points awarded. The paper failed to discuss any nursing interventions or specific considerations for patients with skin lesions."]
|
| 136 |
+
Professor's Summary:
|
| 137 |
+
[Provide a 2-3 sentence summary in the persona of Dr. Vance. E.g., "While the content discussing the cause of lesions was adequate, the submission demonstrated significant and widespread deficiencies in adhering to APA 7th Edition standards. These formatting and citation skills are non-negotiable in academic and professional nursing. Careful review of the APA manual is required before the next submission."]
|
| 138 |
"""
|
| 139 |
|
| 140 |
+
# The prompt has been updated to request the new, more verbose JSON structure.
|
| 141 |
+
BASE_PROMPT_TEMPLATE = """
|
| 142 |
+
You are Dr. Stone, a meticulous Associate Professor of Nursing. Your task is to analyze the attached student paper file and grade it with absolute precision against the provided rubric. You may also be provided with an example of a perfectly formatted paper for your reference.
|
| 143 |
|
| 144 |
+
Your entire response must be a single, valid JSON object and nothing else.
|
| 145 |
|
| 146 |
+
**GRADING INSTRUCTIONS:**
|
| 147 |
+
1. Carefully analyze the student's paper file, paying close attention to all formatting details (margins, fonts, spacing, etc.) and content.
|
| 148 |
+
2. For each of the 16 criteria in the rubric, provide a score and a brief, specific justification for that score.
|
| 149 |
+
3. Calculate the final score by summing the scores from all 16 criteria.
|
| 150 |
+
4. Provide a 2-3 sentence professional summary of the paper's performance.
|
| 151 |
+
|
| 152 |
+
**RUBRIC:**
|
| 153 |
{GRADING_RUBRIC}
|
| 154 |
|
| 155 |
+
**REQUIRED JSON OUTPUT FORMAT:**
|
| 156 |
+
Your output must be a JSON object with three top-level keys: `finalScore`, `gradingBreakdown`, and `summary`.
|
| 157 |
+
The `gradingBreakdown` key must contain a list of 16 objects, one for each criterion in the rubric.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
+
**EXAMPLE OF THE REQUIRED JSON OUTPUT FORMAT:**
|
| 160 |
{{
|
| 161 |
+
"finalScore": 89,
|
| 162 |
+
"gradingBreakdown": [
|
| 163 |
+
{{
|
| 164 |
+
"criterion": "1. APA Title Page",
|
| 165 |
+
"score": 5,
|
| 166 |
+
"maxScore": 5,
|
| 167 |
+
"justification": "All 9 required components are present and correctly formatted."
|
| 168 |
+
}},
|
| 169 |
+
{{
|
| 170 |
+
"criterion": "2. APA General Guidelines",
|
| 171 |
+
"score": 2,
|
| 172 |
+
"maxScore": 5,
|
| 173 |
+
"justification": "The document uses 1.25-inch margins instead of the required 1-inch, which is one guideline violation."
|
| 174 |
+
}},
|
| 175 |
+
{{
|
| 176 |
+
"criterion": "3. APA Text - Misc. Errors",
|
| 177 |
+
"score": 4,
|
| 178 |
+
"maxScore": 5,
|
| 179 |
+
"justification": "One typographical error ('hte' instead of 'the') was noted in paragraph 3."
|
| 180 |
+
}}
|
| 181 |
+
],
|
| 182 |
+
"summary": "This is a strong paper with excellent critical analysis. The final grade was primarily impacted by minor but frequent APA formatting errors, which should be the main focus for improvement."
|
| 183 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
"""
|
| 185 |
|
|
|
|
| 186 |
@dataclass
|
| 187 |
class GradingResult:
|
| 188 |
"""Holds the structured result of a single graded essay."""
|
| 189 |
file_name: str
|
| 190 |
success: bool
|
| 191 |
+
# This now stores the entire JSON response for detailed output
|
| 192 |
+
raw_response: Optional[dict] = None
|
|
|
|
|
|
|
| 193 |
error_message: Optional[str] = None
|
| 194 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
class GeminiGrader:
|
| 196 |
"""Manages interaction with the Google Gemini API for grading."""
|
|
|
|
| 197 |
def __init__(self, api_key: str):
|
| 198 |
+
genai.configure(api_key=api_key)
|
| 199 |
+
self.model = genai.GenerativeModel(model_name="gemini-1.5-pro-latest")
|
| 200 |
+
|
| 201 |
+
def grade_essay(self, student_paper_file: object, example_paper_file: Optional[object]) -> GradingResult:
|
| 202 |
+
"""Sends files to Gemini for grading and parses the JSON response."""
|
| 203 |
+
prompt_text = BASE_PROMPT_TEMPLATE.format(GRADING_RUBRIC=GRADING_RUBRIC)
|
| 204 |
+
content_list = ["Please grade the attached student paper.", student_paper_file, prompt_text]
|
| 205 |
+
if example_paper_file:
|
| 206 |
+
content_list.insert(2, "Use this second file as a reference example of a perfectly formatted paper.")
|
| 207 |
+
content_list.insert(3, example_paper_file)
|
| 208 |
+
|
| 209 |
try:
|
| 210 |
+
response = self.model.generate_content(content_list, request_options={'timeout': 600})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
cleaned_response = response.text.strip().replace("```json", "").replace("```", "")
|
| 212 |
data = json.loads(cleaned_response)
|
| 213 |
|
| 214 |
+
# Basic validation for the new structure
|
| 215 |
+
if not all(key in data for key in ["finalScore", "gradingBreakdown", "summary"]):
|
| 216 |
+
raise KeyError("Model response missing one or more required top-level keys.")
|
| 217 |
+
|
|
|
|
|
|
|
|
|
|
| 218 |
return GradingResult(
|
| 219 |
+
file_name=student_paper_file.display_name,
|
| 220 |
success=True,
|
| 221 |
+
raw_response=data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
)
|
| 223 |
+
except json.JSONDecodeError as e:
|
| 224 |
+
error_details = f"Model returned malformed JSON. Error: {e}. Raw Response: {cleaned_response}"
|
| 225 |
+
return GradingResult(file_name=student_paper_file.display_name, success=False, error_message=error_details)
|
| 226 |
except Exception as e:
|
| 227 |
+
return GradingResult(file_name=student_paper_file.display_name, success=False, error_message=f"An API or model error occurred: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
|
| 229 |
+
def process_single_file(file_path: str, grader: GeminiGrader, example_paper_file_obj: Optional[object]) -> GradingResult:
|
| 230 |
+
"""Uploads a single student paper and calls the grader."""
|
| 231 |
+
student_paper_file_obj = None
|
| 232 |
+
try:
|
| 233 |
+
student_paper_file_obj = genai.upload_file(path=file_path, display_name=os.path.basename(file_path))
|
| 234 |
+
return grader.grade_essay(student_paper_file_obj, example_paper_file_obj)
|
| 235 |
+
except Exception as e:
|
| 236 |
+
return GradingResult(file_name=os.path.basename(file_path), success=False, error_message=str(e))
|
| 237 |
+
finally:
|
| 238 |
+
if student_paper_file_obj:
|
| 239 |
+
genai.delete_file(student_paper_file_obj.name)
|
| 240 |
|
| 241 |
async def grade_papers_concurrently(
|
| 242 |
+
files: List[gr.File], example_paper_file: gr.File, api_key: str, progress=gr.Progress(track_tqdm=True)
|
| 243 |
) -> (str, str):
|
| 244 |
+
"""The main asynchronous function that orchestrates the grading process."""
|
|
|
|
|
|
|
|
|
|
| 245 |
start_time = time.time()
|
| 246 |
+
if not api_key: raise gr.Error("Google API Key is required.")
|
| 247 |
+
if not files: raise gr.Error("Please upload at least one paper to grade.")
|
| 248 |
|
| 249 |
+
example_paper_file_obj = None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
try:
|
| 251 |
grader = GeminiGrader(api_key)
|
| 252 |
+
if example_paper_file:
|
| 253 |
+
progress(0, desc="Uploading example paper...")
|
| 254 |
+
example_paper_file_obj = genai.upload_file(path=example_paper_file.name, display_name=os.path.basename(example_paper_file.name))
|
| 255 |
+
|
| 256 |
+
file_paths = [file.name for file in files]
|
| 257 |
+
|
| 258 |
+
with ThreadPoolExecutor(max_workers=1) as executor:
|
| 259 |
+
loop = asyncio.get_event_loop()
|
| 260 |
+
tasks = [loop.run_in_executor(executor, process_single_file, fp, grader, example_paper_file_obj) for fp in file_paths]
|
| 261 |
+
# Use tqdm for progress tracking in the console/logs
|
| 262 |
+
results = [await f for f in asyncio.as_completed(tasks)]
|
| 263 |
+
progress(1) # Mark progress as complete
|
| 264 |
+
|
| 265 |
+
# --- THIS SECTION IS UPDATED TO CALCULATE THE SCORE ---
|
| 266 |
+
output_markdown = ""
|
| 267 |
+
successful_grades = [res for res in results if res.success]
|
| 268 |
+
failed_grades = [res for res in results if not res.success]
|
| 269 |
+
|
| 270 |
+
for result in successful_grades:
|
| 271 |
+
response_data = result.raw_response
|
| 272 |
+
output_markdown += f"### β
Grade for: **{result.file_name}**\n"
|
| 273 |
+
|
| 274 |
+
# Calculate the score from the breakdown instead of trusting the AI's sum
|
| 275 |
+
breakdown = response_data.get('gradingBreakdown', [])
|
| 276 |
+
calculated_score = sum(item.get('score', 0) for item in breakdown)
|
| 277 |
+
|
| 278 |
+
output_markdown += f"**Final Score:** {calculated_score}/100\n\n"
|
| 279 |
+
output_markdown += "**Detailed Grading Breakdown:**\n"
|
| 280 |
+
for item in breakdown:
|
| 281 |
+
output_markdown += f"- **{item.get('criterion', 'N/A')}**: {item.get('score', 'N/A')} / {item.get('maxScore', 'N/A')}\n"
|
| 282 |
+
output_markdown += f" - *Justification: {item.get('justification', 'No justification provided.')}*\n"
|
| 283 |
+
output_markdown += f"\n**Summary:** {response_data.get('summary', 'No summary provided.')}\n"
|
| 284 |
+
output_markdown += "---\n"
|
| 285 |
+
# --- END OF UPDATED SECTION ---
|
| 286 |
+
|
| 287 |
+
if failed_grades:
|
| 288 |
+
output_markdown += "### β Failed Papers\n"
|
| 289 |
+
for result in failed_grades:
|
| 290 |
+
output_markdown += f"- **File:** {result.file_name}\n"
|
| 291 |
+
output_markdown += f" - **Error:** {result.error_message}\n"
|
| 292 |
+
output_markdown += "---\n"
|
| 293 |
+
|
| 294 |
+
end_time = time.time()
|
| 295 |
+
runtime = f"Total runtime: {end_time - start_time:.2f} seconds."
|
| 296 |
+
status = f"Grading complete. {len(successful_grades)} papers graded successfully, {len(failed_grades)} failed."
|
| 297 |
+
return output_markdown, f"{status}\n{runtime}"
|
| 298 |
+
|
| 299 |
+
finally:
|
| 300 |
+
if example_paper_file_obj:
|
| 301 |
+
genai.delete_file(example_paper_file_obj.name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
|
| 303 |
# --- Build the Gradio Interface ---
|
|
|
|
| 304 |
with gr.Blocks(theme=gr.themes.Soft(), title="Nursing Essay Grader") as demo:
|
| 305 |
gr.Markdown(
|
| 306 |
"""
|
| 307 |
# π Gemini-Powered Nursing Essay Grader
|
| 308 |
+
Upload one or more student essays to have them graded by AI.
|
| 309 |
+
1. Enter your Google API Key.
|
| 310 |
+
2. Upload the `.docx` or `.pdf` files you want to grade.
|
| 311 |
+
3. Optionally, upload a single `.docx` or `.pdf` file as a "perfect" example.
|
| 312 |
+
4. Click "Grade All Papers".
|
| 313 |
"""
|
| 314 |
)
|
| 315 |
+
api_key_input = gr.Textbox(label="Google API Key", placeholder="Enter your Google API Key here", type="password")
|
| 316 |
with gr.Row():
|
| 317 |
+
file_uploads = gr.File(label="Upload Essays to Grade", file_count="multiple", file_types=['.pdf', '.docx'], type="filepath", scale=2)
|
| 318 |
+
example_paper_upload = gr.File(label="Upload Example Paper (Optional)", file_count="single", file_types=['.pdf', '.docx'], type="filepath", scale=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
grade_button = gr.Button("π Grade All Papers", variant="primary")
|
|
|
|
| 320 |
gr.Markdown("---")
|
| 321 |
gr.Markdown("## π Grading Results")
|
|
|
|
| 322 |
results_output = gr.Markdown(label="Formatted Grades")
|
| 323 |
+
status_output = gr.Textbox(label="Runtime Status", lines=2, interactive=False)
|
| 324 |
|
| 325 |
+
grade_button.click(fn=grade_papers_concurrently, inputs=[file_uploads, example_paper_upload, api_key_input], outputs=[results_output, status_output])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
|
| 327 |
if __name__ == "__main__":
|
| 328 |
demo.launch(debug=True)
|