import os import gradio as gr from dotenv import load_dotenv from openai import OpenAI from prompts import FOLLOW_UP_INSTRUCTION, MODE_INSTRUCTIONS, QUIZ_INSTRUCTIONS load_dotenv() MODEL_NAME = os.getenv("OPENAI_MODEL", "gpt-4.1-nano") MAX_OUTPUT_TOKENS = int(os.getenv("MAX_OUTPUT_TOKENS", "450")) SYSTEM_MESSAGE = """ You are Python Tutor Bot, a professional and patient Python tutor for beginners. Teach through a clear learning journey: 1. Understand what the learner wants. 2. Explain or review in small steps. 3. Ask one useful follow-up question. 4. Keep answers practical, short, and encouraging. Use simple language, small Python examples, and avoid overwhelming the learner. """ def get_client(): """Create the OpenAI client only when the user submits a request.""" api_key = os.getenv("OPENAI_API_KEY") if not api_key or api_key == "your_openai_api_key_here": return None return OpenAI(api_key=api_key) def chat_with_openai(messages): client = get_client() if client is None: return ( "OPENAI_API_KEY is missing. Add it to your local .env file, " "or add it as a Hugging Face Space secret named OPENAI_API_KEY." ) try: response = client.chat.completions.create( model=MODEL_NAME, messages=messages, temperature=0.3, max_tokens=MAX_OUTPUT_TOKENS, ) return response.choices[0].message.content except Exception as error: return f"Something went wrong while contacting OpenAI: {error}" def start_state(): return { "quiz_active": False, "quiz_topic": "", "quiz_number": 0, "quiz_notes": [], } def build_messages(mode, user_input, message_history): messages = [{"role": "system", "content": SYSTEM_MESSAGE}] for chat_message in message_history[-6:]: messages.append( { "role": chat_message["role"], "content": chat_message["content"], } ) prompt = MODE_INSTRUCTIONS[mode].format(user_input=user_input.strip()) prompt = f"{prompt}\n\n{FOLLOW_UP_INSTRUCTION}" messages.append({"role": "user", "content": prompt}) return messages def build_quiz_messages(user_input, state, message_history): messages = [{"role": "system", "content": SYSTEM_MESSAGE}] for chat_message in message_history[-8:]: messages.append( { "role": chat_message["role"], "content": chat_message["content"], } ) quiz_prompt = QUIZ_INSTRUCTIONS.format( topic=state["quiz_topic"], quiz_number=state["quiz_number"], user_answer=user_input.strip(), quiz_notes="\n".join(state["quiz_notes"]) or "No previous quiz notes yet.", ) messages.append({"role": "user", "content": quiz_prompt}) return messages def tutor_response(mode, user_input, chat_history, message_history, state): chat_history = chat_history or [] message_history = message_history or [] state = state or start_state() if not user_input or not user_input.strip(): warning = "Please type a Python topic, code sample, error message, or quiz answer first." chat_history.append({"role": "assistant", "content": warning}) return chat_history, message_history, state, "" clean_input = user_input.strip() chat_history.append({"role": "user", "content": clean_input}) message_history.append({"role": "user", "content": clean_input}) if mode == "Quiz Me": current_quiz_number = None if not state["quiz_active"]: state = start_state() state["quiz_active"] = True state["quiz_topic"] = clean_input state["quiz_number"] = 1 intro_prompt = ( f"Start a beginner Python quiz journey about: {state['quiz_topic']}.\n" "Ask exactly one multiple-choice question. Do not give the answer yet. " "After the options, ask the learner to reply with A, B, C, or D." ) messages = [{"role": "system", "content": SYSTEM_MESSAGE}, {"role": "user", "content": intro_prompt}] else: current_quiz_number = state["quiz_number"] state["quiz_notes"].append(f"Question {state['quiz_number']} answer: {clean_input}") messages = build_quiz_messages(clean_input, state, message_history) reply = chat_with_openai(messages) if state["quiz_active"] and "Something went wrong" not in reply: if current_quiz_number and current_quiz_number >= 3: state = start_state() elif current_quiz_number: state["quiz_number"] = current_quiz_number + 1 else: state = start_state() messages = build_messages(mode, clean_input, message_history) reply = chat_with_openai(messages) message_history.append({"role": "assistant", "content": reply}) chat_history.append({"role": "assistant", "content": reply}) return chat_history, message_history, state, "" def clear_chat(): return [], [], start_state(), "" with gr.Blocks(title="Python Tutor Bot") as demo: gr.Markdown( """ # Python Tutor Bot Learn Python step by step with explanations, debugging help, quizzes, and code reviews. """ ) app_state = gr.State(start_state()) message_history = gr.State([]) with gr.Row(): mode_dropdown = gr.Dropdown( choices=list(MODE_INSTRUCTIONS.keys()), value="Explain Concept", label="Learning mode", ) chatbot = gr.Chatbot( label="Python Tutor Bot", height=430, ) user_input_box = gr.Textbox( label="Message", placeholder="Example: I want to learn Python lists, or paste code with an error...", lines=5, ) with gr.Row(): submit_button = gr.Button("Send", variant="primary") clear_button = gr.Button("Clear") submit_button.click( fn=tutor_response, inputs=[mode_dropdown, user_input_box, chatbot, message_history, app_state], outputs=[chatbot, message_history, app_state, user_input_box], ) user_input_box.submit( fn=tutor_response, inputs=[mode_dropdown, user_input_box, chatbot, message_history, app_state], outputs=[chatbot, message_history, app_state, user_input_box], ) clear_button.click( fn=clear_chat, inputs=None, outputs=[chatbot, message_history, app_state, user_input_box], ) if __name__ == "__main__": demo.launch()