Education / app.py
ciaochris's picture
Update app.py
e67d493 verified
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())