fabrizziomcl commited on
Commit
82d5c6f
·
verified ·
1 Parent(s): 3b3c461

EduCrate — Spanish Socratic tutor (Qwen3-0.6B SFT+GRPO), runs on CPU

Browse files
Files changed (4) hide show
  1. README.md +47 -7
  2. __pycache__/app.cpython-314.pyc +0 -0
  3. app.py +211 -0
  4. requirements.txt +5 -0
README.md CHANGED
@@ -1,13 +1,53 @@
1
  ---
2
- title: Educrate
3
- emoji: 📈
4
- colorFrom: red
5
- colorTo: gray
6
  sdk: gradio
7
- sdk_version: 6.18.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
 
 
 
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: EduCrate — Socratic Tutor
3
+ emoji: 🦉
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: gradio
 
 
7
  app_file: app.py
8
  pinned: false
9
+ license: apache-2.0
10
+ short_description: Spanish Socratic tutor on CPU; never gives the answer.
11
+ tags:
12
+ - build-small-hackathon
13
+ - backyard-ai
14
+ - tiny-titan
15
+ models:
16
+ - fabrizziomcl/nanoballena-qwen3-sft
17
+ - fabrizziomcl/nanoballena-qwen3-socratic
18
  ---
19
 
20
+ # 🦉 EduCrate A Socratic Tutor for Peruvian Public-School Students
21
+
22
+ **EduCrate never gives the final answer.** It guides students with one question at a
23
+ time, detects their mistake, and offers progressive hints (the *maieutic* method) so they
24
+ discover the answer themselves. Spanish-language tutoring focused on **mathematical
25
+ reasoning** and **reading comprehension**, small enough to **run on CPU**.
26
+
27
+ ## The problem
28
+ Peru's public secondary schools face a learning crisis. In **PISA 2022 (OECD)**, only
29
+ **34%** of Peruvian 15-year-olds reached basic proficiency in **math** (66% below) and
30
+ **50%** in **reading**. Peru's national assessment (**ECE / MINEDU, grade 8, 2022**) found
31
+ only **~12.7%** *Satisfactory* in math, with public (state) schools far behind private
32
+ ones. Most chatbots just hand over the answer — which does not build reasoning.
33
+
34
+ ## The model
35
+ - Base: **Qwen/Qwen3-0.6B** (596M), fine-tuned with **SFT + GRPO** on **~4,900 Spanish
36
+ Socratic dialogues** generated for this project. Runs on **CPU** → no GPU, no paid API.
37
+ - Models: `fabrizziomcl/nanoballena-qwen3-socratic` (SFT+GRPO) · `…-qwen3-sft` (SFT).
38
+
39
+ ## Measured behavior (held-out mGSM-es)
40
+ | Model | Answer-withholding | Asks a question | Avg words |
41
+ |---|---|---|---|
42
+ | Qwen3-0.6B base | 84% | 100% | 66.5 |
43
+ | **EduCrate (fine-tuned)** | **100%** | 100% | ~11 |
44
+
45
+ Evidence of real behavioral change on unseen problems — not memorization.
46
+
47
+ ## How to use
48
+ Click an **example card**, or: (optional) paste a reading passage → choose what you need
49
+ (*understand my mistake* / *need a fact* / *just chat*) → ask your question. The tutor
50
+ replies **in Spanish** with a guiding question, never the answer.
51
+
52
+ > Built for the **Build Small Hackathon** — track *Backyard AI*, **Tiny Titan** badge (≤4B).
53
+ > Made with generative AI; validate any pedagogical use with a teacher.
__pycache__/app.cpython-314.pyc ADDED
Binary file (11 kB). View file
 
app.py ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ EduCrate — Socratic Tutor (Gradio app)
4
+ A Spanish-language Socratic tutor for Peruvian public secondary-school students.
5
+
6
+ UI in English (international judges); the tutoring itself happens in Spanish.
7
+ Model: Qwen3-0.6B fine-tuned (SFT + GRPO). Runs on CPU.
8
+ Gradio 6 (Chatbot uses the messages format).
9
+ """
10
+ import os
11
+ import gradio as gr
12
+ import torch
13
+
14
+ MODEL_ID = os.environ.get("MODEL_ID", "fabrizziomcl/nanoballena-qwen3-socratic")
15
+ MAX_TURNS = int(os.environ.get("MAX_TURNS", "8"))
16
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
17
+ DTYPE = torch.bfloat16 if DEVICE == "cuda" else torch.float32
18
+
19
+ # Anchors the Socratic behavior (mirrors the training system prompt).
20
+ SYSTEM_PROMPT = (
21
+ "Eres EduCrate, un tutor socrático en español para estudiantes de secundaria "
22
+ "de colegios públicos del Perú (comprensión lectora y razonamiento matemático). "
23
+ "REGLA ABSOLUTA: NUNCA des la respuesta final ni el resultado directamente. "
24
+ "Guía con UNA pregunta a la vez, detecta el error del estudiante y da pistas "
25
+ "progresivas (primero suave, luego más concreta) hasta que él mismo descubra la "
26
+ "respuesta. Sé breve, amable y claro."
27
+ )
28
+
29
+ # maieutic-inspired modes: reference (give a fact) vs reasoning (counter-question).
30
+ MODES = {
31
+ "💬 Just chat": "",
32
+ "🤔 Understand my mistake": (
33
+ "\n\nEl estudiante quiere entender su razonamiento: NO le des datos ni la "
34
+ "respuesta; respóndele con una contrapregunta que lo haga revisar su propio paso."
35
+ ),
36
+ "💡 I need a fact/formula": (
37
+ "\n\nEl estudiante pide un DATO o fórmula puntual: puedes dárselo de forma breve, "
38
+ "pero NUNCA lo apliques hasta la respuesta final por él; devuélvele la pregunta."
39
+ ),
40
+ }
41
+
42
+ # Example cards (Spanish prompts = real student use). Each: (title, desc, reading, message).
43
+ EXAMPLES = [
44
+ ("➗ Linear equation",
45
+ "Solve 3x + 6 = 15 — guided, no answer",
46
+ "", "Ayúdame a resolver 3x + 6 = 15, pero no me des la respuesta."),
47
+ ("🍕 Adding fractions",
48
+ "Understand 1/2 + 1/3 step by step",
49
+ "", "No entiendo cómo sumar 1/2 + 1/3. ¿Me ayudas a pensarlo?"),
50
+ ("% Percentages",
51
+ "What is 20% of 50? — guide me",
52
+ "", "¿Cómo calculo el 20% de 50? No me lo resuelvas, guíame paso a paso."),
53
+ ("📖 Reading comprehension",
54
+ "Main idea of a short passage",
55
+ "El reciclaje ayuda a reducir la basura en las ciudades. Si separamos los "
56
+ "plásticos y el papel, menos residuos llegan a los ríos.",
57
+ "¿Cuál es la idea principal de este texto? Guíame, no me des la respuesta."),
58
+ ]
59
+
60
+ _load_error = None
61
+ model = tokenizer = None
62
+ try:
63
+ from transformers import AutoModelForCausalLM, AutoTokenizer
64
+ print(f"Loading {MODEL_ID} on {DEVICE} ...")
65
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
66
+ model = AutoModelForCausalLM.from_pretrained(MODEL_ID, dtype=DTYPE).to(DEVICE)
67
+ model.eval()
68
+ if DEVICE == "cpu":
69
+ torch.set_num_threads(int(os.environ.get("OMP_NUM_THREADS", "4")))
70
+ if tokenizer.pad_token is None:
71
+ tokenizer.pad_token = tokenizer.eos_token
72
+ print("Model loaded.")
73
+ except Exception as e: # noqa: BLE001
74
+ _load_error = str(e)
75
+ print(f"[WARN] Could not load model: {e}")
76
+
77
+
78
+ def _render(messages):
79
+ try:
80
+ return tokenizer.apply_chat_template(
81
+ messages, tokenize=False, add_generation_prompt=True, enable_thinking=False)
82
+ except TypeError:
83
+ return tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
84
+
85
+
86
+ def _generate(messages, max_new_tokens):
87
+ inputs = tokenizer(_render(messages), return_tensors="pt").to(DEVICE)
88
+ with torch.no_grad():
89
+ out = model.generate(
90
+ **inputs, max_new_tokens=max_new_tokens, do_sample=True,
91
+ temperature=0.7, top_p=0.9, repetition_penalty=1.1,
92
+ pad_token_id=tokenizer.pad_token_id or tokenizer.eos_token_id)
93
+ text = tokenizer.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
94
+ for junk in ("<think>", "</think>"):
95
+ text = text.replace(junk, "")
96
+ return text.strip()
97
+
98
+
99
+ def respond(user_msg, history, reading_text, mode, hint_mode):
100
+ history = history or []
101
+ if not user_msg or not user_msg.strip():
102
+ return history, ""
103
+ if model is None:
104
+ return history + [
105
+ {"role": "user", "content": user_msg},
106
+ {"role": "assistant", "content": f"⚠️ Model failed to load ({_load_error})."},
107
+ ], ""
108
+ system = SYSTEM_PROMPT + MODES.get(mode, "")
109
+ if reading_text and reading_text.strip():
110
+ system += f"\n\nTexto de lectura del estudiante:\n{reading_text.strip()}"
111
+ if hint_mode:
112
+ system += ("\n\nEl estudiante pidió una PISTA: da UNA sola pista corta que lo "
113
+ "acerque, sin revelar la respuesta.")
114
+ messages = [{"role": "system", "content": system}]
115
+ messages += history[-2 * MAX_TURNS:]
116
+ messages.append({"role": "user", "content": user_msg})
117
+ reply = _generate(messages, max_new_tokens=180 if hint_mode else 256)
118
+ return history + [
119
+ {"role": "user", "content": user_msg},
120
+ {"role": "assistant", "content": reply},
121
+ ], ""
122
+
123
+
124
+ CSS = """
125
+ .educrate-hero {text-align:center; padding: 6px 0 2px 0;}
126
+ .ex-card {border:1px solid #e5e7eb; border-radius:12px; padding:6px 10px;}
127
+ footer {visibility:hidden}
128
+ """
129
+
130
+ with gr.Blocks(title="EduCrate — Socratic Tutor", theme=gr.themes.Soft(), css=CSS) as demo:
131
+ gr.Markdown(
132
+ """
133
+ <div class="educrate-hero">
134
+
135
+ # 🦉 EduCrate — A Socratic Tutor that **never gives the answer**
136
+ **For Peruvian public secondary-school students** · Math reasoning & reading comprehension
137
+ · Runs **on CPU** (0.6B) · `Build Small` · *Backyard AI* · **Tiny Titan**
138
+
139
+ </div>
140
+ """
141
+ )
142
+ if _load_error:
143
+ gr.Markdown(f"> ⚠️ *Model `{MODEL_ID}` failed to load: {_load_error}*")
144
+
145
+ gr.Markdown("### ✨ Try an example — click a card to start a guided dialogue")
146
+ ex_buttons = []
147
+ with gr.Row():
148
+ for title, desc, reading, message in EXAMPLES:
149
+ with gr.Column(scale=1, min_width=150):
150
+ b = gr.Button(f"{title}\n{desc}", elem_classes="ex-card", size="md")
151
+ ex_buttons.append((b, reading, message))
152
+
153
+ with gr.Row():
154
+ with gr.Column(scale=1):
155
+ reading_text = gr.Textbox(
156
+ label="📖 Reading passage (optional) · pega una lectura",
157
+ placeholder="Paste a short text to practice reading comprehension...",
158
+ lines=7,
159
+ )
160
+ mode = gr.Radio(choices=list(MODES.keys()), value="💬 Just chat",
161
+ label="What do you need? (cambia el estilo del tutor)")
162
+ hint_mode = gr.Checkbox(label="💡 Give me a single short hint")
163
+ with gr.Column(scale=2):
164
+ chatbot = gr.Chatbot(label="Chat with EduCrate (responde en español)", height=440)
165
+ user_input = gr.Textbox(
166
+ label="💬 Your question · tu pregunta", lines=2,
167
+ placeholder="Escribe aquí y presiona Enviar / Type here and press Send...",
168
+ )
169
+ with gr.Row():
170
+ submit_btn = gr.Button("Send / Enviar", variant="primary")
171
+ clear_btn = gr.Button("🗑️ New topic")
172
+
173
+ inp = [user_input, chatbot, reading_text, mode, hint_mode]
174
+ submit_btn.click(respond, inp, [chatbot, user_input])
175
+ user_input.submit(respond, inp, [chatbot, user_input])
176
+ clear_btn.click(lambda: ([], "", ""), outputs=[chatbot, user_input, reading_text])
177
+
178
+ # Example cards: fill inputs (clearing chat), then auto-run one Socratic turn.
179
+ for b, reading, message in ex_buttons:
180
+ b.click(lambda r=reading, m=message: (r, m, []),
181
+ outputs=[reading_text, user_input, chatbot]).then(
182
+ respond, inp, [chatbot, user_input])
183
+
184
+ with gr.Accordion("ℹ️ About this project — why it matters", open=False):
185
+ gr.Markdown(
186
+ """
187
+ **The problem.** Peru's public secondary schools face a deep learning crisis. In
188
+ **PISA 2022 (OECD)**, only **34%** of Peruvian 15-year-olds reached basic proficiency in
189
+ **mathematics** (66% below) and **50%** in **reading**. Peru's national assessment
190
+ (**ECE / MINEDU, grade 8, 2022**) found only **~12.7%** *Satisfactory* in math — with
191
+ public (state) schools far behind private ones and a large socioeconomic gap.
192
+
193
+ **Why Socratic.** Most chatbots hand over the answer, which doesn't build reasoning.
194
+ **EduCrate never gives the final answer** — it asks one guiding question at a time, detects
195
+ the student's mistake, and offers progressive hints (the *maieutic* method) so the student
196
+ discovers the answer themselves.
197
+
198
+ **Why small.** A **Qwen3-0.6B** model fine-tuned (SFT + GRPO) on ~4,900 Spanish Socratic
199
+ dialogues. It **runs on CPU**, so it works on the low-resource laptops common in public
200
+ schools — no GPU, no paid API.
201
+
202
+ **Measured behavior (held-out mGSM-es).** The fine-tune raised the *answer-withholding rate*
203
+ from **84% (base) → 100%**, turning verbose solutions into concise guiding questions.
204
+
205
+ *Built for the Build Small Hackathon (Backyard AI track). Made with generative AI; validate
206
+ any pedagogical use with a teacher.*
207
+ """
208
+ )
209
+
210
+ if __name__ == "__main__":
211
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio>=4.44
2
+ torch
3
+ transformers>=4.51
4
+ tokenizers
5
+ accelerate