Spaces:
Sleeping
Sleeping
| # -------------------------------------------------------------- | |
| # IGCSE/GCSE Language Platform β Multi-AI System (Z.ai + Gemini + Cohere + MiniMax) | |
| # Models: Z.ai (Primary) β Gemini β Cohere β MiniMax (Fallbacks) | |
| # -------------------------------------------------------------- | |
| import os | |
| import json | |
| from datetime import datetime | |
| import gradio as gr | |
| import PyPDF2 | |
| import time | |
| # ---------- 1. Configure ALL AI Systems ---------- | |
| # Z.ai (Primary) - Using Z.ai SDK | |
| try: | |
| import zai | |
| zai_client = zai.Client(api_key=os.getenv("ZAI_API_KEY")) | |
| print("β Z.ai SDK initialized successfully (PRIMARY)") | |
| except Exception as e: | |
| print(f"β Error initializing Z.ai SDK: {e}") | |
| zai_client = None | |
| # Gemini (Secondary) | |
| try: | |
| import google.generativeai as genai | |
| genai.configure(api_key=os.getenv("GEMINI_API_KEY")) | |
| gemini_model = genai.GenerativeModel('gemini-2.5-pro') | |
| print("β Gemini AI initialized successfully (SECONDARY)") | |
| except Exception as e: | |
| print(f"β Error initializing Gemini: {e}") | |
| gemini_model = None | |
| # Cohere (Tertiary) | |
| try: | |
| import cohere | |
| cohere_client = cohere.Client(os.getenv("COHERE_API_KEY")) | |
| print("β Cohere initialized successfully (TERTIARY)") | |
| except Exception as e: | |
| print(f"β Error initializing Cohere: {e}") | |
| cohere_client = None | |
| # MiniMax (Final Fallback) | |
| try: | |
| from huggingface_hub import InferenceClient | |
| minimax_client = InferenceClient( | |
| provider="novita", | |
| api_key=os.environ.get("HF_TOKEN"), | |
| ) | |
| print("β MiniMax AI initialized successfully (FINAL FALLBACK)") | |
| except Exception as e: | |
| print(f"β Error initializing MiniMax: {e}") | |
| minimax_client = None | |
| # ---------- 2. Unified AI Function with Smart Fallback ---------- | |
| def ask_ai(prompt, temperature=0.7, max_retries=2): | |
| """ | |
| Try models in order: Z.ai β Gemini β Cohere β MiniMax | |
| Returns: (response_text, source_name) | |
| """ | |
| last_error = None | |
| # Try Z.ai first (Primary) - Using Z.ai SDK | |
| if zai_client: | |
| for attempt in range(max_retries): | |
| try: | |
| response = zai_client.chat.completions.create( | |
| model="glm-4.6", # Replace with actual model name | |
| messages=[{"role": "user", "content": prompt}], | |
| temperature=temperature | |
| ) | |
| return response.choices[0].message.content, "zai" | |
| except Exception as e: | |
| last_error = e | |
| print(f"β Z.ai attempt {attempt+1} failed: {str(e)}") | |
| if attempt < max_retries - 1: | |
| time.sleep(1) | |
| # Try Gemini (Secondary) | |
| if gemini_model: | |
| for attempt in range(max_retries): | |
| try: | |
| response = gemini_model.generate_content( | |
| prompt, | |
| generation_config=genai.types.GenerationConfig( | |
| temperature=temperature, | |
| ) | |
| ) | |
| return response.text, "gemini" | |
| except Exception as e: | |
| last_error = e | |
| print(f"β Gemini attempt {attempt+1} failed: {str(e)}") | |
| if attempt < max_retries - 1: | |
| time.sleep(1) | |
| # Try Cohere (Tertiary) | |
| if cohere_client: | |
| for attempt in range(max_retries): | |
| try: | |
| response = cohere_client.chat( | |
| model="command-r-plus-08-2024", | |
| message=prompt, | |
| temperature=temperature | |
| ) | |
| return response.text, "cohere" | |
| except Exception as e: | |
| last_error = e | |
| print(f"β Cohere attempt {attempt+1} failed: {str(e)}") | |
| if attempt < max_retries - 1: | |
| time.sleep(1) | |
| # Try MiniMax (Final Fallback) | |
| if minimax_client: | |
| try: | |
| completion = minimax_client.chat.completions.create( | |
| model="MiniMaxAI/MiniMax-M2", | |
| messages=[{"role": "user", "content": prompt}], | |
| temperature=temperature | |
| ) | |
| return completion.choices[0].message.content, "minimax" | |
| except Exception as e: | |
| last_error = e | |
| print(f"β MiniMax fallback failed: {str(e)}") | |
| # All failed | |
| error_msg = f"β Error: All AI services failed. Last error: {str(last_error)}" | |
| return error_msg, "error" | |
| # ---------- 3. Global storage ---------- | |
| papers_storage = [] | |
| pdf_content_storage = {} | |
| ADMIN_PASSWORD = "@mikaelJ46" | |
| # ---------- 4. Topic lists ---------- | |
| french_topics = [ | |
| "Greetings & Introductions", "Family & Relationships", "Daily Routines", | |
| "Food & Restaurants", "Shopping & Money", "Travel & Transport", | |
| "School & Education", "Hobbies & Free Time", "Weather & Seasons", | |
| "House & Home", "Health & Body", "Work & Future Plans", | |
| "Technology & Media", "Environment", "Grammar: Present Tense", | |
| "Grammar: Past Tenses", "Grammar: Future Tense", "Grammar: Pronouns", | |
| "Grammar: Adjectives" | |
| ] | |
| efl_topics = [ | |
| "Reading Comprehension", "Writing: Narrative", "Writing: Descriptive", | |
| "Writing: Argumentative", "Writing: Formal Letters", "Writing: Informal Letters", | |
| "Grammar: Tenses", "Grammar: Conditionals", "Grammar: Passive Voice", | |
| "Grammar: Reported Speech", "Vocabulary: Idioms", "Vocabulary: Phrasal Verbs", | |
| "Literature Analysis", "Poetry Analysis", "Speaking & Pronunciation", | |
| "Listening Comprehension" | |
| ] | |
| # ---------- 5. PDF Processing ---------- | |
| def extract_text_from_pdf(pdf_file): | |
| """Extract text from uploaded PDF file""" | |
| if pdf_file is None: | |
| return "" | |
| try: | |
| pdf_reader = PyPDF2.PdfReader(pdf_file) | |
| text = "" | |
| for page in pdf_reader.pages: | |
| text += page.extract_text() + "\n" | |
| return text | |
| except Exception as e: | |
| return f"Error extracting PDF: {e}" | |
| # ---------- 6. AI Tutor with Multi-Model Support ---------- | |
| def ai_tutor_chat(message, history, subject, topic, level): | |
| if not message.strip(): | |
| return history | |
| system = f"""You are an expert {'French' if subject == 'French' else 'EFL'} {level} tutor. | |
| Focus on {topic or 'any topic'}. Be encouraging, clear, and pedagogical. | |
| Adjust difficulty and explanations appropriately for {level} level students. | |
| Provide detailed explanations with examples when needed. | |
| Use a friendly, supportive tone to help students learn effectively.""" | |
| # Build conversation context | |
| conversation = "" | |
| for user_msg, bot_msg in history[-5:]: # Last 5 exchanges | |
| if user_msg: | |
| conversation += f"Student: {user_msg}\n" | |
| if bot_msg: | |
| # Remove emoji indicators that match what we're actually adding | |
| clean_msg = bot_msg.replace("π΅ ", "").replace("π’ ", "").replace("π£ ", "").replace("π ", "") | |
| conversation += f"Tutor: {clean_msg}\n" | |
| conversation += f"Student: {message}\nTutor:" | |
| full_prompt = f"{system}\n\nConversation:\n{conversation}" | |
| bot_response, source = ask_ai(full_prompt, temperature=0.7) | |
| # Add source indicator if not from Z.ai | |
| if source == "gemini": | |
| bot_response = f"π΅ {bot_response}" | |
| elif source == "cohere": | |
| bot_response = f"π {bot_response}" | |
| elif source == "minimax": | |
| bot_response = f"π£ {bot_response}" | |
| elif source == "error": | |
| pass # Error already formatted | |
| history.append((message, bot_response)) | |
| return history | |
| def clear_chat(): | |
| return [] | |
| # ---------- 7. Translator ---------- | |
| def translate_text(text, direction): | |
| if not text.strip(): | |
| return "Enter text first." | |
| src = "English" if direction == "English β French" else "French" | |
| tgt = "French" if direction == "English β French" else "English" | |
| prompt = f"""You are a professional translator. | |
| Translate the following text from {src} to {tgt}. | |
| Provide only the translation without explanations: | |
| {text}""" | |
| response, source = ask_ai(prompt, temperature=0.3) | |
| # Add subtle source indicator if not primary | |
| if source in ["gemini", "cohere", "minimax"]: | |
| response = f"{response}\n\n_[Translated using {source.title()}]_" | |
| return response | |
| # ---------- 8. Dictionary ---------- | |
| def dictionary_lookup(word): | |
| if not word.strip(): | |
| return "Enter a French word." | |
| prompt = f"""Provide a detailed French dictionary entry for "{word}": | |
| - Part of speech (noun, verb, adjective, etc.) | |
| - Gender (if noun: masculine/feminine) | |
| - English meaning(s) and translations | |
| - 3 example sentences in French with English translations | |
| - Common phrases and idioms using this word | |
| - Any important usage notes or context | |
| - Related words or derivatives""" | |
| response, source = ask_ai(prompt, temperature=0.3) | |
| if source in ["gemini", "cohere", "minimax"]: | |
| response = f"{response}\n\n_[Dictionary powered by {source.title()}]_" | |
| return response | |
| # ---------- 9. Search Past Papers for Real Questions ---------- | |
| def search_past_papers(subject, topic, level): | |
| """Search uploaded past papers for questions matching the topic""" | |
| if not topic: | |
| return "β Select a topic first!" | |
| # Find matching papers | |
| matching_content = [] | |
| for paper_id, content in pdf_content_storage.items(): | |
| paper = next((p for p in papers_storage if p['id'] == paper_id), None) | |
| if paper and paper['subject'].lower() == subject.lower() and paper['level'] == level: | |
| matching_content.append({ | |
| 'title': paper['title'], | |
| 'content': content, | |
| 'uploaded': paper['uploaded_at'] | |
| }) | |
| if not matching_content: | |
| return f"π No past papers found for {subject} {level}.\n\nTip: Upload past papers in the Admin Panel to enable this feature." | |
| # Use AI to extract relevant questions from the papers | |
| combined_content = "\n\n".join([f"=== {p['title']} ===\n{p['content'][:5000]}" for p in matching_content]) | |
| prompt = f"""You are analyzing real {level} {subject} past papers to find questions about "{topic}". | |
| PAST PAPER CONTENT: | |
| {combined_content} | |
| TASK: Extract and return ALL questions from these papers that relate to the topic "{topic}". | |
| For each question found, provide: | |
| 1. The complete question text (exactly as written) | |
| 2. The paper it came from | |
| 3. Any mark allocations mentioned | |
| 4. Any accompanying resources/images mentioned | |
| Format your response clearly with question numbers and paper sources. | |
| If no questions directly match this topic, return questions from related topics and explain the connection. | |
| If no relevant questions exist at all, clearly state this.""" | |
| response, source = ask_ai(prompt, temperature=0.3) | |
| if source in ["gemini", "cohere", "minimax"]: | |
| response = f"{response}\n\n_[Search powered by {source.title()}]_" | |
| return response | |
| # ---------- 10. Practice Questions (Enhanced with PDF context) ---------- | |
| def generate_question(subject, topic, level): | |
| if not topic: | |
| return "Select a topic!", "", "" | |
| # Get relevant PDF content if available | |
| pdf_context = "" | |
| for paper_id, content in pdf_content_storage.items(): | |
| paper = next((p for p in papers_storage if p['id'] == paper_id), None) | |
| if paper and paper['subject'].lower() == subject.lower() and paper['level'] == level: | |
| pdf_context += f"\n\nReference material from {paper['title']}:\n{content[:3000]}" | |
| prompt = f"""Create ONE high-quality {level} {subject} exam question on the topic: "{topic}". | |
| {"Base the question style, difficulty level, and format on this reference material:" + pdf_context if pdf_context else "Create an authentic exam-style question."} | |
| The question should: | |
| - Be appropriate for {level} level students | |
| - Test understanding and application | |
| - Include clear instructions | |
| - Be answerable in 5-10 minutes | |
| Return ONLY valid JSON (no markdown): | |
| {{"question": "complete question text", "expectedAnswer": "what a good answer should include", "markScheme": "marking criteria"}}""" | |
| response, source = ask_ai(prompt, temperature=0.4) | |
| try: | |
| clean_txt = response.replace("```json", "").replace("```", "").strip() | |
| data = json.loads(clean_txt) | |
| return data["question"], data.get("expectedAnswer", ""), data.get("markScheme", "") | |
| except Exception as e: | |
| return response, "", f"Error: {e}" | |
| def check_answer(question, expected, user_answer, subject, level): | |
| if not user_answer.strip(): | |
| return "Write your answer first!" | |
| prompt = f"""Evaluate this student's answer: | |
| Question: {question} | |
| Expected: {expected} | |
| Student's answer: | |
| {user_answer} | |
| Return JSON (no markdown): | |
| {{"isCorrect": true/false, "score": 0-100, "feedback": "detailed feedback", "improvements": "suggestions", "strengths": "what was done well"}}""" | |
| response, source = ask_ai(prompt, temperature=0.3) | |
| try: | |
| clean_txt = response.replace("```json", "").replace("```", "").strip() | |
| fb = json.loads(clean_txt) | |
| result = f"""π Score: {fb['score']}% | |
| π Detailed Feedback: | |
| {fb['feedback']} | |
| β Your Strengths: | |
| {fb.get('strengths', 'Good effort!')} | |
| π How to Improve: | |
| {fb['improvements']}""" | |
| if source in ["gemini", "cohere", "minimax"]: | |
| result += f"\n\n_[Graded by {source.title()}]_" | |
| return result | |
| except Exception: | |
| return response | |
| # ---------- 11. Admin β Past Papers ---------- | |
| def verify_admin_password(password): | |
| if password == ADMIN_PASSWORD: | |
| return gr.update(visible=True), gr.update(visible=False), "β Access granted!" | |
| return gr.update(visible=False), gr.update(visible=True), "β Incorrect password!" | |
| def upload_paper(title, subject, level, content, pdf_file): | |
| if not all([title, subject, level, content]): | |
| return "β Please fill all required fields!", get_papers_list() | |
| paper_id = len(papers_storage) + 1 | |
| pdf_text = "" | |
| if pdf_file is not None: | |
| pdf_text = extract_text_from_pdf(pdf_file) | |
| if pdf_text and not pdf_text.startswith("Error"): | |
| pdf_content_storage[paper_id] = pdf_text | |
| content += f"\n\n[π PDF extracted: {len(pdf_text)} characters]" | |
| papers_storage.append({ | |
| "id": paper_id, | |
| "title": title, | |
| "subject": subject.lower(), | |
| "level": level, | |
| "content": content, | |
| "has_pdf": bool(pdf_text and not pdf_text.startswith("Error")), | |
| "uploaded_at": datetime.now().strftime("%Y-%m-%d %H:%M") | |
| }) | |
| return "β Paper uploaded!", get_papers_list() | |
| def get_papers_list(): | |
| if not papers_storage: | |
| return "No papers yet." | |
| return "\n".join( | |
| f"**{p['title']}** ({p['subject'].upper()} - {p['level']}) {'π PDF' if p.get('has_pdf') else ''}\nβ° {p['uploaded_at']}\n{p['content'][:120]}...\n{'β'*60}" | |
| for p in papers_storage | |
| ) | |
| def view_papers_student(subject, level): | |
| filtered = [p for p in papers_storage | |
| if p["subject"] == subject.lower() and p["level"] == level] | |
| if not filtered: | |
| return f"π No {subject} {level} papers available." | |
| return "\n".join( | |
| f"**{p['title']}** {'π PDF' if p.get('has_pdf') else ''}\nβ° {p['uploaded_at']}\n\n{p['content']}\n\n{'β'*60}" | |
| for p in filtered | |
| ) | |
| # ---------- 12. Gradio UI ---------- | |
| with gr.Blocks(theme=gr.themes.Soft(), title="IGCSE/GCSE Platform") as app: | |
| gr.Markdown(""" | |
| # π IGCSE/GCSE Language Learning Platform | |
| π€ AI Tutor | π Translator | π Dictionary | π Past Papers | |
| _Powered by Z.ai with intelligent multi-model fallback system_ | |
| """) | |
| with gr.Tabs(): | |
| # βββββ STUDENT PORTAL βββββ | |
| with gr.Tab("π¨βπ Student Portal"): | |
| with gr.Tabs(): | |
| # AI TUTOR | |
| with gr.Tab("π€ AI Tutor"): | |
| gr.Markdown("### Chat with Your AI Tutor\n*Powered by Z.ai with automatic fallback*") | |
| with gr.Row(): | |
| subj = gr.Radio(["French", "EFL"], label="Subject", value="French") | |
| lvl = gr.Radio(["IGCSE", "GCSE"], label="Level", value="IGCSE") | |
| topc = gr.Dropdown(french_topics, label="Topic (optional)", allow_custom_value=True) | |
| def upd_topics(s): | |
| return gr.Dropdown(choices=french_topics if s == "French" else efl_topics, value=None) | |
| subj.change(upd_topics, subj, topc) | |
| chat = gr.Chatbot(height=450, show_label=False) | |
| txt = gr.Textbox(placeholder="Ask anything... e.g., 'Explain the passΓ© composΓ©'", label="Message") | |
| with gr.Row(): | |
| send = gr.Button("Send π€", variant="primary") | |
| clr = gr.Button("Clear π") | |
| send.click(ai_tutor_chat, [txt, chat, subj, topc, lvl], chat) | |
| txt.submit(ai_tutor_chat, [txt, chat, subj, topc, lvl], chat) | |
| clr.click(clear_chat, outputs=chat) | |
| # TRANSLATOR | |
| with gr.Tab("π Translator"): | |
| gr.Markdown("### English β· French Translation") | |
| dir_ = gr.Radio(["English β French", "French β English"], label="Direction", value="English β French") | |
| inp = gr.Textbox(lines=6, label="Input Text", placeholder="Enter text...") | |
| out = gr.Textbox(lines=6, label="Translation") | |
| gr.Button("Translate π", variant="primary").click(translate_text, [inp, dir_], out) | |
| # DICTIONARY | |
| with gr.Tab("π Dictionary"): | |
| gr.Markdown("### French Dictionary") | |
| w = gr.Textbox(placeholder="Enter French word...", label="Word") | |
| o = gr.Textbox(lines=16, label="Definition") | |
| gr.Button("Look Up π", variant="primary").click(dictionary_lookup, w, o) | |
| # PRACTICE QUESTIONS | |
| with gr.Tab("β Practice"): | |
| gr.Markdown("### Generate & Practice Exam Questions") | |
| with gr.Row(): | |
| ps = gr.Radio(["French", "EFL"], label="Subject", value="French") | |
| pl = gr.Radio(["IGCSE", "GCSE"], label="Level", value="IGCSE") | |
| pt = gr.Dropdown(french_topics, label="Topic") | |
| ps.change(upd_topics, ps, pt) | |
| q = gr.Textbox(label="π Question", lines=5, interactive=False) | |
| exp = gr.Textbox(label="Expected", lines=2, visible=False) | |
| mark = gr.Textbox(label="π Mark Scheme", lines=3, interactive=False) | |
| ans = gr.Textbox(lines=8, label="β Your Answer", placeholder="Type your answer...") | |
| fb = gr.Textbox(lines=12, label="π Feedback", interactive=False) | |
| with gr.Row(): | |
| gr.Button("π² Generate", variant="primary").click(generate_question, [ps, pt, pl], [q, exp, mark]) | |
| gr.Button("β Check", variant="secondary").click(check_answer, [q, exp, ans, ps, pl], fb) | |
| # PAST PAPERS | |
| with gr.Tab("π Past Papers"): | |
| gr.Markdown("### Browse Past Papers") | |
| with gr.Row(): | |
| psb = gr.Radio(["French", "EFL"], label="Subject", value="French") | |
| plb = gr.Radio(["IGCSE", "GCSE"], label="Level", value="IGCSE") | |
| pd = gr.Textbox(lines=22, label="Papers", interactive=False) | |
| gr.Button("π Show", variant="primary").click(view_papers_student, [psb, plb], pd) | |
| # βββββ ADMIN PANEL βββββ | |
| with gr.Tab("π Admin Panel"): | |
| with gr.Column() as login_section: | |
| gr.Markdown("### π Admin Login") | |
| pwd = gr.Textbox(label="Password", type="password", placeholder="Enter password") | |
| login_btn = gr.Button("π Login", variant="primary") | |
| login_status = gr.Textbox(label="Status", interactive=False) | |
| with gr.Column(visible=False) as admin_section: | |
| gr.Markdown("### π€ Upload Past Papers") | |
| with gr.Row(): | |
| with gr.Column(): | |
| t = gr.Textbox(label="Title", placeholder="e.g., Paper 1 - June 2023") | |
| with gr.Row(): | |
| s = gr.Radio(["French", "EFL"], label="Subject", value="French") | |
| lv = gr.Radio(["IGCSE", "GCSE"], label="Level", value="IGCSE") | |
| c = gr.Textbox(lines=6, label="Description") | |
| pdf = gr.File(label="π PDF (optional)", file_types=[".pdf"]) | |
| up = gr.Button("β¬ Upload", variant="primary") | |
| st = gr.Textbox(label="Status") | |
| with gr.Column(): | |
| lst = gr.Textbox(lines=24, label="All Papers", value=get_papers_list(), interactive=False) | |
| up.click(upload_paper, [t, s, lv, c, pdf], [st, lst]) | |
| login_btn.click(verify_admin_password, [pwd], [admin_section, login_section, login_status]) | |
| gr.Markdown(""" | |
| --- | |
| **System Status:** π’ Z.ai (Primary) | π΅ Gemini (Secondary) | π Cohere (Tertiary) | π£ MiniMax (Fallback) | |
| """) | |
| app.launch() |