Spaces:
Sleeping
Sleeping
Create app.py
Browse files
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()
|