dalinstone commited on
Commit
a0118d2
Β·
1 Parent(s): e100fec

this is the final product that probably won't ever be used, but that's alright

Browse files
Files changed (1) hide show
  1. app.py +262 -265
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
- ### NURSING ESSAY GRADING RUBRIC
15
-
16
- 1. **Content & Analysis (40 points):**
17
- * **Thesis/Argument:** Clear, focused, and relevant thesis statement. (10 points)
18
- * **Evidence & Support:** Strong use of credible, evidence-based sources to support claims. (15 points)
19
- * **Critical Thinking:** Demonstrates deep analysis, synthesis of ideas, and understanding of complex health science concepts. (15 points)
20
-
21
- 2. **Organization & Structure (20 points):**
22
- * **Introduction:** Engaging introduction with a clear purpose and thesis. (5 points)
23
- * **Body Paragraphs:** Logical flow, clear topic sentences, and well-developed paragraphs. (10 points)
24
- * **Conclusion:** Effectively summarizes the argument and provides a sense of closure. (5 points)
25
-
26
- 3. **APA Formatting & Citations (25 points):**
27
- * **Reference List:** Correctly formatted according to APA 7th edition. (10 points)
28
- * **In-Text Citations:** Accurate and correctly placed in-text citations. (10 points)
29
- * **General Formatting:** Correct title page, running head, font, and margins. (5 points)
30
-
31
- 4. **Clarity & Mechanics (15 points):**
32
- * **Grammar & Spelling:** Free of significant errors in grammar, punctuation, and spelling. (5 points)
33
- * **Sentence Structure:** Clear, varied, and concise sentence structure. (5 points)
34
- * **Professional Tone:** Maintains a scholarly and professional tone appropriate for nursing. (5 points)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  """
36
 
37
- GEMINI_PROMPT = f"""
38
- You are an expert-level college university grader for a nursing department. Your task is to evaluate a short health science essay based on a strict rubric and provide your feedback in a structured JSON format.
 
39
 
40
- **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.
41
 
42
- **Use the following rubric to grade the essay:**
 
 
 
 
 
 
43
  {GRADING_RUBRIC}
44
 
45
- **Instructions:**
46
- 1. Read the entire essay provided below.
47
- 2. Assess the essay against each category in the rubric.
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
- **Example of the required JSON output format:**
58
  {{
59
- "finalGrade": 88,
60
- "pointDeductions": {{
61
- "Content & Analysis": 2,
62
- "Organization & Structure": 0,
63
- "APA Formatting & Citations": 8,
64
- "Clarity & Mechanics": 2
65
- }},
66
- "feedback": {{
67
- "Content & Analysis": "The thesis was slightly unfocused, but the evidence used was strong.",
68
- "Organization & Structure": "No points deducted.",
69
- "APA Formatting & Citations": "Multiple errors in the reference list formatting and three missing in-text citations.",
70
- "Clarity & Mechanics": "Minor grammatical errors and occasional awkward phrasing."
71
- }},
72
- "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."
 
 
 
 
 
 
 
 
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
- grade: Optional[int] = None
87
- deductions: Dict[str, int] = field(default_factory=dict)
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
- """Initializes the Gemini model."""
 
 
 
 
 
 
 
 
 
 
114
  try:
115
- genai.configure(api_key=api_key)
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
- # Validate the structure of the returned JSON
152
- required_keys = ["finalGrade",
153
- "pointDeductions", "feedback", "summary"]
154
- if not all(key in data for key in required_keys):
155
- raise KeyError(
156
- "The model's response was missing one or more required keys.")
157
-
158
  return GradingResult(
159
- file_name=file_name,
160
  success=True,
161
- grade=data["finalGrade"],
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
- # --- Gradio Application ---
 
 
 
 
 
 
 
 
 
 
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
- if not api_key:
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
- except ValueError as e:
199
- raise gr.Error(str(e))
200
-
201
- file_paths = [file.name for file in files]
202
- total_files = len(file_paths)
203
-
204
- # Use a ThreadPoolExecutor to run synchronous tasks concurrently
205
- with ThreadPoolExecutor(max_workers=10) as executor:
206
- # Create a future for each file processing task
207
- loop = asyncio.get_event_loop()
208
- tasks = [
209
- loop.run_in_executor(
210
- executor,
211
- process_single_file,
212
- file_path,
213
- grader
214
- )
215
- for file_path in file_paths
216
- ]
217
-
218
- results = []
219
- # Process results as they are completed
220
- for i, future in enumerate(asyncio.as_completed(tasks)):
221
- progress(i + 1, desc=f"Grading paper {i+1}/{total_files}...")
222
- result = await future
223
- results.append(result)
224
-
225
- # --- Format the final output ---
226
- successful_grades = [res for res in results if res.success]
227
- failed_grades = [res for res in results if not res.success]
228
-
229
- output_markdown = ""
230
- for result in successful_grades:
231
- output_markdown += f"### βœ… Grade for: **{result.file_name}**\n"
232
- output_markdown += f"**Final Grade:** {result.grade}/100\n\n"
233
-
234
- # Format point deductions
235
- deductions_str = ""
236
- for category, points in result.deductions.items():
237
- if points > 0:
238
- deductions_str += f"- **{category}:** Lost {points} points. *Reason: {result.feedback.get(category, 'N/A')}*\n"
239
- if not deductions_str:
240
- deductions_str = "Excellent work! No points were deducted.\n"
241
-
242
- output_markdown += "**Point Deductions Breakdown:**\n" + deductions_str + "\n"
243
- output_markdown += f"**Summary:** {result.summary}\n"
244
- output_markdown += "---\n"
245
-
246
- if failed_grades:
247
- output_markdown += "### ❌ Failed Papers\n"
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 in Word format (`.docx`) to have them graded by AI.
290
- 1. Enter your Google API Key (enabling the Gemini API in your Google Cloud project is required).
291
- 2. Upload the `.docx` files.
292
- 3. Click "Grade All Papers". The results will appear below.
 
293
  """
294
  )
295
-
296
  with gr.Row():
297
- api_key_input = gr.Textbox(
298
- label="Google API Key",
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
- status_output = gr.Textbox(
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)