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