import gradio as gr from groq import Groq import os import json import logging import re import sys import traceback from typing import Dict, Any, Tuple from dotenv import load_dotenv import markdown2 # Set up detailed logging logging.basicConfig( level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s', handlers=[ logging.StreamHandler(sys.stdout), logging.FileHandler("tutor_app.log") ] ) # Load environment variables load_dotenv() logging.debug("Environment variables loaded") # Print environment check logging.debug(f"GROQ_API_KEY present: {bool(os.getenv('GROQ_API_KEY'))}") # Initialize Groq client with error handling try: api_key = os.getenv("GROQ_API_KEY") if not api_key: logging.critical("GROQ_API_KEY is missing or empty") raise EnvironmentError("GROQ_API_KEY environment variable is required but not set") client = Groq(api_key=api_key) logging.debug("Groq client initialized successfully") # Test the client with a simple API call try: models = client.models.list() logging.debug(f"API connection test successful. Available models: {[m.id for m in models.data][:3]}...") except Exception as e: logging.warning(f"API connection test failed: {e}") except Exception as e: logging.critical(f"Failed to initialize Groq client: {str(e)}") logging.critical(traceback.format_exc()) raise EnvironmentError(f"Error initializing Groq API client: {str(e)}") def transcribe_audio(audio): """Transcribe audio file to text using Groq's API.""" try: if audio is None: logging.debug("No audio input provided") return "" logging.debug(f"Audio input received: {type(audio)} - {audio}") # Fixed: Properly handle audio path from Gradio if isinstance(audio, dict) and 'path' in audio: audio_path = audio['path'] else: audio_path = audio logging.debug(f"Audio path: {audio_path}") if audio_path and os.path.exists(audio_path): with open(audio_path, "rb") as audio_file: audio_data = audio_file.read() logging.debug(f"Audio file opened, size: {len(audio_data)} bytes") try: logging.debug("Sending audio to Groq API for transcription") transcription = client.audio.transcriptions.create( file=("audio.wav", audio_data), model="distil-whisper-large-v3-en", ) logging.debug(f"Transcription response received: {transcription}") if hasattr(transcription, 'text'): result = transcription.text logging.debug(f"Transcription text: {result}") return result else: result = transcription.get('text', "Transcription succeeded but returned no text") logging.debug(f"Transcription text (alt format): {result}") return result except Exception as e: logging.error(f"Audio transcription API error: {str(e)}") return f"Audio transcription failed: {str(e)}" else: logging.warning(f"Audio file not found at path: {audio_path}") return "Audio file not found. Please try recording again." except Exception as e: logging.error(f"Error in transcription: {str(e)}") return f"Error in transcription: {str(e)}" def generate_tutor_output(subject: str, difficulty: str, student_input: str) -> Dict[str, str]: """Generate educational content based on student input.""" try: logging.debug(f"Generating tutor output for subject: {subject}, difficulty: {difficulty}") logging.debug(f"Student input: {student_input}") # Define enhanced topics for all subjects topics = { "math": { "quadratic equation": "solving quadratic equations, including methods like factoring, using the quadratic formula, and completing the square", "pythagorean theorem": "the Pythagorean theorem, its proof, and applications in geometry and trigonometry", "calculus": "fundamental concepts of calculus, including limits, derivatives, and integrals", "linear algebra": "basics of linear algebra, including vectors, matrices, and linear transformations", "statistics": "key concepts in statistics, such as probability distributions, hypothesis testing, and regression analysis" }, "science": { "photosynthesis": "the process of photosynthesis in plants, including light-dependent and light-independent reactions", "newton's laws": "Newton's laws of motion and their applications in classical mechanics", "periodic table": "the structure and organization of the periodic table of elements", "dna replication": "the process of DNA replication and its importance in cell division", "climate change": "the causes and effects of climate change, including global warming and its impact on ecosystems" }, } subject_lower = subject.lower() if subject else "general" enhanced_topic = None for subj, subject_topics in topics.items(): for topic, description in subject_topics.items(): if topic.lower() in student_input.lower(): enhanced_topic = description logging.debug(f"Enhanced topic detected: {topic}") break if enhanced_topic: break # Strengthened prompt with strict quiz formatting if enhanced_topic: logging.debug(f"Using enhanced topic: {enhanced_topic}") prompt = f""" You are an expert tutor specializing in {enhanced_topic} at the {difficulty} level. The student has asked: "{student_input}" Generate a detailed response as a valid JSON object with exactly these keys and content: - "lesson": A comprehensive lesson (3-4 paragraphs, at least 200 words) based on national educational standards, including historical context. - "example": A detailed step-by-step example problem with a full solution, formatted as: "Example Problem: [question]\nStep 1: [step]\nStep 2: [step]\nAnswer: [solution]". - "real_world_problem": A challenging real-world application of this concept (at least 100 words). - "quiz": A single string containing exactly 3 multiple-choice questions, each formatted as "1. [Question]\n a) [option]\n b) [option]\n c) [option]\n Correct answer: [letter]", separated by newlines. Ensure all sections are fully populated with relevant content. Return only the JSON object, enclosed in ```json``` markers, with no additional text outside the markers. """ else: logging.debug(f"Using general subject: {subject_lower}") prompt = f""" You are an expert tutor in {subject_lower} at the {difficulty} level. The student has asked: "{student_input}" Generate a detailed response as a valid JSON object with exactly these keys and content: - "lesson": A descriptive, engaging lesson (3-4 paragraphs, at least 200 words) on the topic. - "example": An example problem with a full solution, formatted as: "Example Problem: [question]\nStep 1: [step]\nStep 2: [step]\nAnswer: [solution]". - "real_world_problem": A real-world problem solvable using the lesson concepts (at least 100 words). - "quiz": A single string containing exactly 3 multiple-choice questions, each formatted as "1. [Question]\n a) [option]\n b) [option]\n c) [option]\n Correct answer: [letter]", separated by newlines. Ensure all sections are fully populated with relevant content. Return only the JSON object, enclosed in ```json``` markers, with no additional text outside the markers. """ # Model selection with fallback try: models = client.models.list() available_models = [m.id for m in models.data] logging.debug(f"Available models: {available_models}") target_model = "llama-3.3-70b-versatile" if "llama-3.3-70b-versatile" in available_models else available_models[0] except Exception: logging.warning("Could not fetch model list, using default model") target_model = "llama-3.3-70b-versatile" logging.debug(f"Sending prompt to model: {target_model}") completion = client.chat.completions.create( messages=[ { "role": "system", "content": f"You are the world's best AI tutor, renowned for your ability to explain complex concepts clearly and engagingly. Your expertise in {subject_lower} is unparalleled, and you tailor your teaching to {difficulty} level students. Always return a complete response with all requested sections as a valid JSON object enclosed in ```json``` markers.", }, { "role": "user", "content": prompt, } ], model=target_model, max_tokens=4000, ) response_content = completion.choices[0].message.content logging.debug(f"Raw API response: {response_content}") # Extract JSON from response json_match = re.search(r'```json\s*([\s\S]*?)\s*```', response_content) if json_match: json_str = json_match.group(1) try: result = json.loads(json_str) logging.debug("Successfully parsed JSON from response") except json.JSONDecodeError as e: logging.warning(f"JSON parsing failed: {e}") result = None else: logging.warning("No JSON markers found in response") result = None # Improved fallback parsing with better section detection if not result: logging.debug("Falling back to text parsing") sections = { "lesson": "", "example": "", "real_world_problem": "", "quiz": "" } # Fix: Improved section detection current_section = None section_lines = { "lesson": [], "example": [], "real_world_problem": [], "quiz": [] } # First, identify section headers lines = response_content.split('\n') for i, line in enumerate(lines): line = line.strip() if not line or line.startswith('```'): continue lower_line = line.lower() if "lesson" in lower_line or "introduction" in lower_line: current_section = "lesson" continue elif "example" in lower_line or "problem" in lower_line and "real" not in lower_line: current_section = "example" continue elif any(kw in lower_line for kw in ["real-world", "real world", "application"]): current_section = "real_world_problem" continue elif "quiz" in lower_line or "questions" in lower_line: current_section = "quiz" continue if current_section: section_lines[current_section].append(line) # Join lines for each section for section, lines in section_lines.items(): sections[section] = "\n".join(lines) logging.debug(f"Parsed sections: {sections}") result = sections # Ensure all keys are present and non-empty for key in ["lesson", "example", "real_world_problem", "quiz"]: if key not in result or not result[key].strip(): result[key] = f"No {key.replace('_', ' ')} provided - generation incomplete" return result except Exception as e: logging.error(f"Error in generate_tutor_output: {str(e)}") logging.error(traceback.format_exc()) return { "lesson": f"Error: {str(e)}", "example": "", "real_world_problem": "", "quiz": "" } def process_output(output: Dict[str, Any]) -> Tuple[str, str, str, str]: """Process the output from generate_tutor_output into HTML format.""" try: logging.debug(f"Processing output: {str(output)}") lesson = markdown2.markdown(output.get("lesson", "No lesson available")) example = markdown2.markdown(output.get("example", "No example available")) real_world = markdown2.markdown(output.get("real_world_problem", "No real-world application available")) quiz = markdown2.markdown(output.get("quiz", "No quiz available")) logging.debug("Output processed successfully") return lesson, example, real_world, quiz except Exception as e: logging.error(f"Error processing output: {str(e)}") logging.error(traceback.format_exc()) return f"Error processing output: {str(e)}", "", "", "" def create_interface() -> gr.Blocks: """Create the Gradio interface.""" logging.debug("Creating Gradio interface") with gr.Blocks(theme=gr.themes.Soft()) as demo: gr.Markdown("# ๐ŸŽ“ Vers3Dynamics Tutor: Your Personal Learning Companion") state = gr.State({"is_submitting": False}) with gr.Row(): with gr.Column(scale=2): subject = gr.Dropdown( ["Art History", "Computer Science", "Literature", "Math", "Music", "Science", "Social Science"], label="Subject", info="Choose the subject of your lesson", value="Math" ) difficulty = gr.Radio( ["Primary", "Secondary", "Higher Education"], label="Difficulty Level", info="Select your proficiency level", value="Secondary" ) student_input = gr.Textbox( placeholder="Type your topic or question here...", label="Type Your Question", info="Enter the topic you want to explore" ) audio_input = gr.Audio( type="filepath", label="Speak Your Question", sources=["microphone"], format="wav" ) with gr.Row(): submit_button = gr.Button("๐Ÿ“š Teach Me", variant="primary") clear_button = gr.Button("๐Ÿงน Clear", variant="secondary") status_indicator = gr.Textbox( label="Status", value="Ready", interactive=False ) with gr.Column(scale=3): transcription_output = gr.Textbox(label="Transcribed Audio (if provided)") lesson_output = gr.HTML(label="Lesson") example_output = gr.HTML(label="Example") real_world_output = gr.HTML(label="Real-World Application") quiz_output = gr.HTML(label="Quiz") gr.Markdown(""" ### How to Use 1. Select a subject from the dropdown. 2. Choose your difficulty level. 3. Enter the topic or question you'd like to explore, or use the microphone to speak your question. 4. Click 'Teach Me' to receive a personalized lesson, example, real-world application, and quiz. 5. Review the AI-generated content to enhance your learning. 6. Use the 'Clear' button to reset all fields and start a new query. 7. Feel free to ask follow-up questions or explore new topics! Remember: This is an AI-powered educational tool. Always verify important information with authoritative sources. ### How to Record Your Voice 1. Look for the microphone icon in the "Or speak your question" section. 2. Click on the microphone icon to start recording. 3. Speak clearly and at a normal pace into your device's microphone. 4. Click the stop button (square icon) when you're done speaking. 5. You can play back your recording using the play button to check if it's clear. 6. If you're satisfied with the recording, click 'Teach Me' to process your spoken question. 7. If you're not happy with the recording, you can click the microphone icon again to start over. Note: Make sure your browser has permission to access your microphone. If you encounter any issues, try using a different browser or check your device's audio settings. """) def process_input(subject, difficulty, text_input, audio_input, state): """Process input from text or audio.""" try: logging.info(f"Received inputs - subject: {subject}, difficulty: {difficulty}") logging.info(f"Text input: '{text_input}', Audio input: {audio_input}") subject = subject or "Math" difficulty = difficulty or "Secondary" if not text_input.strip() and not audio_input: return ( {"is_submitting": False}, "Ready", "No input provided", "Please provide a question to begin", "", "", "" ) if text_input.strip(): student_input = text_input.strip() transcribed_text = "Using text input" elif audio_input: transcribed_text = transcribe_audio(audio_input) student_input = transcribed_text if "error" in transcribed_text.lower(): return ( {"is_submitting": False}, "Ready", transcribed_text, "Transcription error. Please try typing your question.", "", "", "" ) else: return ( {"is_submitting": False}, "Ready", "No valid input", "Please provide a question", "", "", "" ) tutor_output = generate_tutor_output(subject, difficulty, student_input) lesson, example, real_world, quiz = process_output(tutor_output) return ( {"is_submitting": False}, "Ready", transcribed_text, lesson, example, real_world, quiz ) except Exception as e: logging.error(f"Error in process_input: {str(e)}") logging.error(traceback.format_exc()) return ( {"is_submitting": False}, "Error", f"Error: {str(e)}", f"Error processing request: {str(e)}", "", "", "" ) def clear_outputs(): """Clear all inputs and outputs.""" logging.debug("Clearing all outputs") return {"is_submitting": False}, "Ready", "", "", "", "", "", "" submit_button.click( fn=process_input, inputs=[subject, difficulty, student_input, audio_input, state], outputs=[state, status_indicator, transcription_output, lesson_output, example_output, real_world_output, quiz_output] ) clear_button.click( fn=clear_outputs, inputs=[], outputs=[state, status_indicator, student_input, transcription_output, lesson_output, example_output, real_world_output, quiz_output] ) return demo def check_health(): """Perform a health check of required components.""" problems = [] if not os.getenv("GROQ_API_KEY"): problems.append("GROQ_API_KEY environment variable is not set") try: import markdown2 except ImportError: problems.append("markdown2 package is not installed") try: client = Groq(api_key=os.getenv("GROQ_API_KEY") or "dummy_key_for_test") except Exception as e: problems.append(f"Groq client initialization failed: {str(e)}") return problems if __name__ == "__main__": logging.info("Starting Vers3Dynamics Tutor application") health_issues = check_health() if health_issues: for issue in health_issues: logging.critical(f"Health check failed: {issue}") logging.critical("Application may not function properly due to the above issues") try: demo = create_interface() logging.info("Gradio interface created successfully") try: logging.info("Launching Gradio server") demo.queue() demo.launch( server_name="0.0.0.0", server_port=7860, debug=True ) except Exception as e: logging.critical(f"Failed to launch Gradio server: {str(e)}") logging.critical(traceback.format_exc()) except Exception as e: logging.critical(f"Failed to create Gradio interface: {str(e)}") logging.critical(traceback.format_exc())