| import gradio as gr | |
| import requests | |
| import os | |
| from datetime import datetime | |
| from collections import defaultdict | |
| HF_TOKEN = os.getenv("HUGGINGFACE_TOKEN") | |
| MODEL = "meta-llama/Llama-3.3-70B-Instruct" | |
| class LearningTracker: | |
| def __init__(self): | |
| self.vocabulary = set() | |
| self.grammar_points = set() | |
| self.error_patterns = defaultdict(int) | |
| self.review_items = [] | |
| self.session_start = datetime.now() | |
| def add_vocabulary(self, words): | |
| self.vocabulary.update(words) | |
| def add_grammar(self, point): | |
| self.grammar_points.add(point) | |
| def log_error(self, error_type): | |
| self.error_patterns[error_type] += 1 | |
| def get_stats(self): | |
| session_duration = (datetime.now() - self.session_start).seconds // 60 | |
| return { | |
| 'vocabulary_count': len(self.vocabulary), | |
| 'grammar_points': len(self.grammar_points), | |
| 'errors': dict(self.error_patterns), | |
| 'review_queue': len(self.review_items), | |
| 'session_minutes': session_duration | |
| } | |
| tracker = LearningTracker() | |
| def build_system_prompt(stats): | |
| error_focus = "" | |
| if stats['errors']: | |
| top_errors = sorted(stats['errors'].items(), key=lambda x: x[1], reverse=True)[:3] | |
| error_list = ", ".join([f"{e[0]} ({e[1]}x)" for e in top_errors]) | |
| error_focus = f"\n\nCOMMON ERRORS TO ADDRESS: {error_list}\nGently reinforce these areas in conversation." | |
| memory_context = "" | |
| if stats['vocabulary_count'] > 0: | |
| memory_context = f"\n\nLEARNING PROGRESS:\n- Vocabulary introduced: {stats['vocabulary_count']} words\n- Grammar points covered: {stats['grammar_points']}\n- Items ready for review: {stats['review_queue']}\nReference previous topics naturally in conversation." | |
| return f"""You're tutoring a linguistics PhD student learning Spanish (A2-B1 level). They're fluent in English, French, and Arabic. | |
| Use B1-level Spanish for all conversation. Adapt to whatever they need - conversation practice, grammar questions, vocabulary building, or scenario practice. | |
| Balance accessible comparisons with metalinguistic insight: | |
| - Simple: "Spanish 'he comido' = French 'j'ai mangé' = English 'I have eaten'" | |
| - Analytical: "Notice the pro-drop here - Spanish allows null subjects unlike French" | |
| - Pattern recognition: "The subjunctive after 'querer que' works like French 'vouloir que' + subjonctif" | |
| For grammar questions, give clear side-by-side comparisons with linguistic depth: | |
| - "Spanish: 'Yo como' / French: 'Je mange' / English: 'I eat' / Arabic: 'آكل' (subject optional in Spanish/Arabic)" | |
| - Discuss: "Both Spanish and Arabic are pro-drop languages, unlike French and English" | |
| - Show conjugation patterns across languages, note morphological strategies | |
| For vocabulary, start with practical comparisons, layer in analysis: | |
| - Spanish: "almohada" / Arabic: "المخدة" (al-mikhadda) → discuss Arabic substrate | |
| - Spanish: "importante" / French: "important" / English: "important" → Latin cognates | |
| - Note each new word you introduce by marking it: [VOCAB: word] | |
| For errors, gently recast and mark the pattern: [ERROR: ser/estar] or [ERROR: subjunctive] or [ERROR: preterite/imperfect] | |
| When you see repeated error patterns, address them directly but kindly.{error_focus}{memory_context} | |
| SPACED REPETITION: Every 5-7 messages, naturally weave in a review question about vocabulary or grammar from earlier in the conversation.""" | |
| def extract_learning_data(text): | |
| vocab = [] | |
| grammar = [] | |
| errors = [] | |
| if '[VOCAB:' in text: | |
| parts = text.split('[VOCAB:') | |
| for part in parts[1:]: | |
| word = part.split(']')[0].strip() | |
| vocab.append(word) | |
| if '[GRAMMAR:' in text: | |
| parts = text.split('[GRAMMAR:') | |
| for part in parts[1:]: | |
| point = part.split(']')[0].strip() | |
| grammar.append(point) | |
| if '[ERROR:' in text: | |
| parts = text.split('[ERROR:') | |
| for part in parts[1:]: | |
| error_type = part.split(']')[0].strip() | |
| errors.append(error_type) | |
| return vocab, grammar, errors | |
| def get_progress_display(): | |
| stats = tracker.get_stats() | |
| progress_text = f"""### 📊 Your Progress | |
| **Session Stats:** | |
| - ⏱️ Time: {stats['session_minutes']} minutes | |
| - 📚 Vocabulary: {stats['vocabulary_count']} words | |
| - 📖 Grammar points: {stats['grammar_points']} | |
| - 🔄 Review queue: {stats['review_queue']} items | |
| """ | |
| if stats['errors']: | |
| progress_text += "\n**Error Patterns (focus areas):**\n" | |
| for error, count in sorted(stats['errors'].items(), key=lambda x: x[1], reverse=True): | |
| progress_text += f"- {error}: {count}x\n" | |
| return progress_text | |
| def query_model(messages, stream=True): | |
| API_URL = "https://router.huggingface.co/v1/chat/completions" | |
| headers = { | |
| "Authorization": f"Bearer {HF_TOKEN}", | |
| "Content-Type": "application/json" | |
| } | |
| payload = { | |
| "model": MODEL, | |
| "messages": messages, | |
| "max_tokens": 1000, | |
| "temperature": 0.7, | |
| "stream": stream | |
| } | |
| response = requests.post(API_URL, headers=headers, json=payload, timeout=120, stream=stream) | |
| return response | |
| def request_review(history): | |
| if history is None: | |
| history = [] | |
| stats = tracker.get_stats() | |
| if stats['review_queue'] == 0 and stats['vocabulary_count'] == 0: | |
| review_msg = "No hay nada que revisar todavía. (Nothing to review yet. Keep conversing!)" | |
| else: | |
| review_msg = "Dame un repaso de lo que hemos aprendido. (Give me a review of what we've learned.)" | |
| return history + [[review_msg, None]] | |
| with gr.Blocks() as demo: | |
| gr.Markdown("# 🇪🇸 Spanish Tutor - Advanced Learning System") | |
| gr.Markdown("*Powered by Llama 3.3 70B with memory, spaced repetition, and progress tracking*") | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| chatbot = gr.Chatbot(height=500) | |
| msg = gr.Textbox(label="Message", placeholder="Type in Spanish, English, French, or Arabic...") | |
| with gr.Row(): | |
| send = gr.Button("Send", variant="primary") | |
| review = gr.Button("📝 Request Review", variant="secondary") | |
| clear = gr.Button("Clear") | |
| with gr.Column(scale=1): | |
| progress_display = gr.Markdown(get_progress_display()) | |
| gr.Markdown(""" | |
| ### 💡 Tips | |
| **The tutor tracks:** | |
| - New vocabulary you learn | |
| - Grammar points covered | |
| - Your common error patterns | |
| - Items for spaced repetition | |
| **Error patterns help focus practice on:** | |
| - ser/estar confusion | |
| - subjunctive usage | |
| - preterite/imperfect | |
| - gender agreement | |
| Click **Request Review** for spaced repetition practice. | |
| """) | |
| def user_submit(user_message, history): | |
| return "", history + [[user_message, None]] | |
| def bot_respond(history): | |
| if history is None or len(history) == 0: | |
| return history, get_progress_display() | |
| user_message = history[-1][0] | |
| stats = tracker.get_stats() | |
| messages = [ | |
| {"role": "system", "content": build_system_prompt(stats)} | |
| ] | |
| for user_msg, assistant_msg in history[:-1]: | |
| if user_msg: | |
| messages.append({"role": "user", "content": user_msg}) | |
| if assistant_msg: | |
| messages.append({"role": "assistant", "content": assistant_msg}) | |
| messages.append({"role": "user", "content": user_message}) | |
| try: | |
| response_obj = query_model(messages, stream=True) | |
| if response_obj.status_code == 200: | |
| full_response = "" | |
| for line in response_obj.iter_lines(): | |
| if line: | |
| line = line.decode('utf-8') | |
| if line.startswith('data: '): | |
| line = line[6:] | |
| if line.strip() == '[DONE]': | |
| break | |
| try: | |
| import json | |
| chunk = json.loads(line) | |
| if 'choices' in chunk and len(chunk['choices']) > 0: | |
| delta = chunk['choices'][0].get('delta', {}) | |
| content = delta.get('content', '') | |
| if content: | |
| full_response += content | |
| history[-1][1] = full_response | |
| yield history, get_progress_display() | |
| except: | |
| continue | |
| vocab, grammar, errors = extract_learning_data(full_response) | |
| tracker.add_vocabulary(vocab) | |
| for g in grammar: | |
| tracker.add_grammar(g) | |
| for e in errors: | |
| tracker.log_error(e) | |
| else: | |
| history[-1][1] = f"Error {response_obj.status_code}: {response_obj.text}" | |
| yield history, get_progress_display() | |
| except Exception as e: | |
| history[-1][1] = f"Error: {str(e)}" | |
| yield history, get_progress_display() | |
| msg.submit(user_submit, [msg, chatbot], [msg, chatbot], queue=False).then( | |
| bot_respond, chatbot, [chatbot, progress_display] | |
| ) | |
| send.click(user_submit, [msg, chatbot], [msg, chatbot], queue=False).then( | |
| bot_respond, chatbot, [chatbot, progress_display] | |
| ) | |
| review.click(request_review, chatbot, chatbot, queue=False).then( | |
| bot_respond, chatbot, [chatbot, progress_display] | |
| ) | |
| clear.click(lambda: [], None, chatbot, queue=False) | |
| if __name__ == "__main__": | |
| demo.launch() |