ppalit commited on
Commit
f8f01d6
·
verified ·
1 Parent(s): d750382

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +340 -0
app.py ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ # Gradio web app for Hugging Face Spaces: Bloom's Taxonomy Classifier, Generator, and Rewriter
3
+ # No external model downloads; purely rule-based and reproducible.
4
+
5
+ import re
6
+ import random
7
+ from typing import Dict, List, Tuple
8
+
9
+ import gradio as gr
10
+
11
+ BLOOMS_LEVELS = [
12
+ "Remember",
13
+ "Understand",
14
+ "Apply",
15
+ "Analyze",
16
+ "Evaluate",
17
+ "Create",
18
+ ]
19
+
20
+ # Seed for reproducibility across runs
21
+ random.seed(42)
22
+
23
+ # ---- Heuristic resources -----------------------------------------------------
24
+
25
+ VERBS = {
26
+ "Remember": [
27
+ "define", "list", "name", "recall", "state", "identify", "label",
28
+ "match", "enumerate", "select", "recognize", "who", "what", "when",
29
+ ],
30
+ "Understand": [
31
+ "explain", "describe", "summarize", "paraphrase", "interpret", "classify",
32
+ "outline", "illustrate", "give an example", "why", "how does", "compare (basic)",
33
+ ],
34
+ "Apply": [
35
+ "apply", "use", "compute", "calculate", "solve", "demonstrate", "implement",
36
+ "show how", "execute", "perform",
37
+ ],
38
+ "Analyze": [
39
+ "analyze", "compare", "contrast", "differentiate", "examine", "categorize",
40
+ "diagram", "break down", "distinguish", "investigate", "what is the relationship",
41
+ "cause", "effect", "why (multi-part)",
42
+ ],
43
+ "Evaluate": [
44
+ "evaluate", "argue", "assess", "justify", "critique", "defend", "appraise",
45
+ "prioritize", "recommend", "debate", "which is better", "to what extent",
46
+ ],
47
+ "Create": [
48
+ "create", "design", "develop", "compose", "formulate", "invent", "propose",
49
+ "construct", "devise", "generate", "plan", "produce", "draft",
50
+ ],
51
+ }
52
+
53
+ # Templates to generate or rewrite questions by Bloom level.
54
+ TEMPLATES = {
55
+ "Remember": [
56
+ "Define {topic}.",
57
+ "List three key facts about {topic}.",
58
+ "Identify the primary purpose of {topic}.",
59
+ "State the meaning of {topic} in one sentence.",
60
+ "Name two examples related to {topic}.",
61
+ ],
62
+ "Understand": [
63
+ "Explain {topic} in your own words.",
64
+ "Describe how {topic} works to a beginner.",
65
+ "Summarize the main idea behind {topic}.",
66
+ "Classify {topic} into an appropriate category and explain why.",
67
+ "Illustrate {topic} with a simple, concrete example.",
68
+ ],
69
+ "Apply": [
70
+ "Apply {topic} to solve a real-world problem; show the steps.",
71
+ "Use {topic} to compute the result for this scenario: [add numbers/conditions].",
72
+ "Demonstrate {topic} on a new example you devise.",
73
+ "Solve a practical case where {topic} is required; justify each step.",
74
+ "Implement a basic procedure using {topic} and report the outcome.",
75
+ ],
76
+ "Analyze": [
77
+ "Compare and contrast {topic} with a closely related concept; highlight key differences.",
78
+ "Analyze the components of {topic} and explain how they interact.",
79
+ "Examine the causes and effects involved in {topic}.",
80
+ "Categorize elements of {topic} into meaningful groups and justify your grouping.",
81
+ "Break down {topic} into sub-problems and explain each part.",
82
+ ],
83
+ "Evaluate": [
84
+ "Evaluate the strengths and weaknesses of {topic}; provide criteria and evidence.",
85
+ "Argue for or against the use of {topic} in a specific context.",
86
+ "Assess the effectiveness of {topic} using clear metrics.",
87
+ "Justify a recommendation regarding {topic}, noting trade-offs.",
88
+ "Critique a common approach to {topic} and suggest improvements.",
89
+ ],
90
+ "Create": [
91
+ "Design a novel solution that uses {topic}; outline the key steps.",
92
+ "Develop a project plan that incorporates {topic} with milestones and deliverables.",
93
+ "Propose an original framework that extends {topic}.",
94
+ "Formulate a research question involving {topic} and sketch a method to investigate it.",
95
+ "Compose a scenario or case study that requires {topic} to resolve.",
96
+ ],
97
+ }
98
+
99
+ # Light penalties/bonuses to help disambiguate borderline levels
100
+ STRUCTURE_HINTS = {
101
+ "Remember": ["define", "list", "identify", "name", "state"],
102
+ "Understand": ["explain", "describe", "summarize", "illustrate", "classify"],
103
+ "Apply": ["solve", "compute", "calculate", "apply", "use", "demonstrate"],
104
+ "Analyze": ["compare", "contrast", "analyze", "why", "cause", "effect", "break down"],
105
+ "Evaluate": ["evaluate", "assess", "justify", "which is better", "argue", "critique"],
106
+ "Create": ["design", "develop", "propose", "create", "formulate", "invent", "plan"],
107
+ }
108
+
109
+ def normalize(text: str) -> str:
110
+ return re.sub(r"\s+", " ", text.strip().lower())
111
+
112
+ def score_levels(question: str) -> Dict[str, float]:
113
+ """Keyword scoring with mild normalization and overlap handling."""
114
+ q = normalize(question)
115
+ scores = {lvl: 0.0 for lvl in BLOOMS_LEVELS}
116
+ # Count verb hits (phrase -> weight)
117
+ for lvl, words in VERBS.items():
118
+ for w in words:
119
+ # phrase-aware search; word boundary for single words
120
+ if " " in w:
121
+ if w in q:
122
+ scores[lvl] += 1.5
123
+ else:
124
+ # word-boundary with stemming-ish match for simple endings
125
+ if re.search(rf"\b{re.escape(w)}(e|ed|ing|s)?\b", q):
126
+ scores[lvl] += 1.0
127
+
128
+ # Structural hints get a small bonus
129
+ for lvl, hints in STRUCTURE_HINTS.items():
130
+ for h in hints:
131
+ if h in q:
132
+ scores[lvl] += 0.25
133
+
134
+ # Heuristic nudges
135
+ # Longer, multi-part questions tend to be higher-order
136
+ tokens = len(q.split())
137
+ if tokens >= 25:
138
+ scores["Analyze"] += 0.3
139
+ scores["Evaluate"] += 0.3
140
+ scores["Create"] += 0.3
141
+
142
+ # Presence of "design", "propose", etc. strongly suggests Create
143
+ if any(k in q for k in ["design", "develop", "propose", "invent", "formulate", "plan"]):
144
+ scores["Create"] += 1.2
145
+
146
+ # Presence of justification language boosts Evaluate
147
+ if any(k in q for k in ["justify", "assess", "evaluate", "argue", "critique", "recommend"]):
148
+ scores["Evaluate"] += 1.2
149
+
150
+ # "compare/contrast" and "why" boost Analyze
151
+ if any(k in q for k in ["compare", "contrast", "analyze", "why", "cause", "effect"]):
152
+ scores["Analyze"] += 0.8
153
+
154
+ # Basic who/what/when/where—if no other signal—leans Remember/Understand
155
+ if re.search(r"\b(who|what|when|where)\b", q):
156
+ scores["Remember"] += 0.4
157
+ scores["Understand"] += 0.2
158
+
159
+ return scores
160
+
161
+ def classify_question(question: str) -> Tuple[str, Dict[str, float], str]:
162
+ if not question or not question.strip():
163
+ return "", {}, "Please enter a question to classify."
164
+ scores = score_levels(question)
165
+ # pick max; tie-break by deeper levels if scores equal (to avoid under-classification)
166
+ best_level = max(BLOOMS_LEVELS, key=lambda lvl: (scores[lvl], BLOOMS_LEVELS.index(lvl)))
167
+ # Rationale: top 2 contributors
168
+ top = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:2]
169
+ rationale = "Top signals → " + ", ".join(f"{k}: {v:.2f}" for k, v in top)
170
+ return best_level, scores, rationale
171
+
172
+ def extract_topic_from_question(q: str) -> str:
173
+ """Very lightweight topic guesser for rewrite; falls back to original content."""
174
+ text = normalize(q).rstrip("?.")
175
+
176
+ # Prefer phrases after 'about/on/of'
177
+ m = re.search(r"\b(?:about|on|of)\s+(.+)", text)
178
+ if m:
179
+ topic = m.group(1)
180
+ # trim trailing question cues
181
+ topic = re.sub(r"\b(why|how|what|when|where|who)\b.*$", "", topic).strip()
182
+ if topic:
183
+ return topic
184
+
185
+ # Remove leading command verbs
186
+ command_verbs = sum(VERBS.values(), [])
187
+ pattern = r"^(" + "|".join(re.escape(v) for v in sorted(set(command_verbs), key=len, reverse=True)) + r")\b"
188
+ text2 = re.sub(pattern, "", text).strip(",.:- ").strip()
189
+ if text2:
190
+ return text2
191
+
192
+ # Fallback: keep original question minus punctuation
193
+ return re.sub(r"[?.!]+$", "", q).strip()
194
+
195
+ def generate_questions(topic: str, level: str, n: int) -> str:
196
+ topic = topic.strip()
197
+ if not topic:
198
+ return "Please provide a topic."
199
+ if level not in BLOOMS_LEVELS:
200
+ return "Please choose a valid Bloom’s level."
201
+ n = max(1, min(int(n), 20)) # keep it reasonable
202
+ templates = TEMPLATES[level][:]
203
+ out: List[str] = []
204
+ used: set = set()
205
+
206
+ # Slight variations to avoid duplicates: add extra cues or contexts
207
+ extras = [
208
+ "",
209
+ " Include one concrete example.",
210
+ " Keep your response concise.",
211
+ " Explain your reasoning.",
212
+ " Use no more than 3 sentences.",
213
+ " Include assumptions you make.",
214
+ " Relate this to a real-world case.",
215
+ " Provide step-by-step reasoning.",
216
+ ]
217
+
218
+ attempts = 0
219
+ while len(out) < n and attempts < n * 10:
220
+ attempts += 1
221
+ template = random.choice(templates)
222
+ extra = random.choice(extras)
223
+ q = (template.format(topic=topic) + extra).strip()
224
+ if q not in used:
225
+ used.add(q)
226
+ out.append(q)
227
+
228
+ # Ensure numbering and distinctness
229
+ lines = [f"{i+1}. {q}" for i, q in enumerate(out)]
230
+ return "\n".join(lines)
231
+
232
+ def rewrite_question_to_level(question: str, new_level: str) -> str:
233
+ question = question.strip()
234
+ if not question:
235
+ return "Please paste a question to rewrite."
236
+ if new_level not in BLOOMS_LEVELS:
237
+ return "Please choose a valid Bloom’s level."
238
+
239
+ topic_guess = extract_topic_from_question(question)
240
+ # Select a template that best preserves meaning while switching the cognitive demand
241
+ # Prefer more general templates for rewrite
242
+ candidates = TEMPLATES[new_level]
243
+ # avoid extremely specific ones when the source already contains constraints
244
+ generic_first = sorted(candidates, key=lambda s: len(s))
245
+ template = generic_first[0] if generic_first else "{topic}"
246
+
247
+ rewritten = template.format(topic=topic_guess)
248
+ # Add helpful adaptation notes for higher levels
249
+ if new_level in ["Analyze", "Evaluate", "Create"]:
250
+ # Encourage depth without changing grading rubric here
251
+ rewritten += " Be explicit about your reasoning."
252
+
253
+ return rewritten
254
+
255
+ # ---- Gradio UI ---------------------------------------------------------------
256
+
257
+ EXAMPLES = [
258
+ ["What is photosynthesis and when does it occur?",],
259
+ ["Design a simple experiment to test the effect of sunlight on plant growth.",],
260
+ ["Compare and contrast arrays and linked lists.",],
261
+ ["Justify whether encryption should be mandatory on consumer devices.",],
262
+ ]
263
+
264
+ with gr.Blocks(title="Bloom's Taxonomy Helper", theme=gr.themes.Soft()) as demo:
265
+ gr.Markdown(
266
+ """
267
+ # Bloom’s Taxonomy Helper
268
+ A compact tool to **classify**, **generate**, and **rewrite** exam questions by Bloom’s level:
269
+ **Remember · Understand · Apply · Analyze · Evaluate · Create**
270
+ """
271
+ )
272
+
273
+ with gr.Tabs():
274
+ # --- Tab 1: Classify ---------------------------------------------------
275
+ with gr.TabItem("Classify"):
276
+ gr.Markdown("### Paste or type a question. I’ll guess its Bloom’s level.")
277
+ q_in = gr.Textbox(label="Question", placeholder="e.g., Compare and contrast stack and queue operations.", lines=3)
278
+ classify_btn = gr.Button("Classify")
279
+ level_out = gr.Label(label="Predicted Bloom’s level")
280
+ scores_out = gr.JSON(label="Scores by level (higher = stronger signal)")
281
+ rationale_out = gr.Markdown()
282
+
283
+ def on_classify(q):
284
+ lvl, scores, rationale = classify_question(q)
285
+ # Format Label expects mapping; we provide {'label': score}
286
+ if lvl:
287
+ return {lvl: 1.0}, scores, f"**Rationale:** {rationale}"
288
+ else:
289
+ return {}, {}, rationale
290
+
291
+ classify_btn.click(on_classify, inputs=[q_in], outputs=[level_out, scores_out, rationale_out])
292
+
293
+ gr.Examples(
294
+ examples=EXAMPLES,
295
+ inputs=[q_in],
296
+ label="Try examples",
297
+ )
298
+
299
+ # --- Tab 2: Generate ---------------------------------------------------
300
+ with gr.TabItem("Generate"):
301
+ gr.Markdown("### Enter a topic, choose a Bloom’s level, and how many questions to create.")
302
+ topic_in = gr.Textbox(label="Topic", placeholder="e.g., Dynamic programming, photosynthesis, supply and demand", lines=2)
303
+ level_in = gr.Dropdown(choices=BLOOMS_LEVELS, value="Understand", label="Bloom’s level")
304
+ num_in = gr.Slider(1, 20, value=5, step=1, label="Number of questions")
305
+ gen_btn = gr.Button("Generate")
306
+ gen_out = gr.Textbox(label="Generated Questions", lines=12)
307
+
308
+ gen_btn.click(
309
+ fn=lambda topic, level, n: generate_questions(topic, level, int(n)),
310
+ inputs=[topic_in, level_in, num_in],
311
+ outputs=[gen_out],
312
+ )
313
+
314
+ # --- Tab 3: Rewrite ----------------------------------------------------
315
+ with gr.TabItem("Rewrite"):
316
+ gr.Markdown("### Paste a question and choose a new Bloom’s level. I’ll rewrite it for that cognitive level.")
317
+ rewrite_in = gr.Textbox(label="Original Question", placeholder="e.g., List the steps of cellular respiration.", lines=3)
318
+ rewrite_level = gr.Dropdown(choices=BLOOMS_LEVELS, value="Analyze", label="Target Bloom’s level")
319
+ rewrite_btn = gr.Button("Rewrite")
320
+ rewrite_out = gr.Textbox(label="Rewritten Question", lines=5)
321
+
322
+ rewrite_btn.click(
323
+ fn=rewrite_question_to_level,
324
+ inputs=[rewrite_in, rewrite_level],
325
+ outputs=[rewrite_out],
326
+ )
327
+
328
+ gr.Markdown(
329
+ """
330
+ ---
331
+ **Notes**
332
+ - This app uses lightweight heuristics (keyword signals and structure cues) rather than a trained model.
333
+ - Generation ensures questions are distinct and clearly numbered.
334
+ - Rewriting keeps the topic while shifting cognitive demand to the selected Bloom’s level.
335
+ """
336
+ )
337
+
338
+ if __name__ == "__main__":
339
+ # For local testing; on Spaces you don't need to set server params explicitly.
340
+ demo.launch()