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()