|
|
import os |
|
|
import sys |
|
|
import gradio as gr |
|
|
from langchain_openai import ChatOpenAI |
|
|
from langgraph.prebuilt import create_react_agent |
|
|
from mcp import ClientSession, StdioServerParameters |
|
|
from mcp.client.stdio import stdio_client |
|
|
from langchain_mcp_adapters.tools import load_mcp_tools |
|
|
from langchain_core.messages import HumanMessage, SystemMessage |
|
|
|
|
|
|
|
|
NEBIUS_API_KEY = os.getenv("NEBIUS_API_KEY") |
|
|
NEBIUS_BASE_URL = "https://api.studio.nebius.ai/v1/" |
|
|
MODEL_NAME = "openai/gpt-oss-20b" |
|
|
|
|
|
|
|
|
SYSTEM_PROMPT = """You are a 'Vibe Coding' Python Tutor. |
|
|
Your goal is to teach by DOING and then providing resources. |
|
|
BEHAVIOR GUIDELINES: |
|
|
1. **Greetings & Small Talk**: If the user says "hello", "hi", or asks non-coding questions, respond conversationally and politely. Ask them what they want to learn today. |
|
|
- DO NOT generate the lesson structure, files, or resources for simple greetings. |
|
|
2. **Teaching Mode**: ONLY when the user asks a coding question or requests a topic (e.g., "dictionaries", "how do loops work"): |
|
|
- **The Lesson**: Explain the concept clearly. |
|
|
- **The Code**: ALWAYS create a Python file, run it, and show the output using tools ('write_file', 'run_python_script'). |
|
|
- **The Context**: Use 'list_directory' to see the student's workspace. |
|
|
CRITICAL: When in "Teaching Mode", you must end your response with these exact separators: |
|
|
---SECTION: VIDEOS--- |
|
|
(List 2-3 YouTube search queries or URLs relevant to the topic) |
|
|
---SECTION: ARTICLES--- |
|
|
(List 2-3 documentation links or course names, e.g., RealPython, FreeCodeCamp) |
|
|
---SECTION: QUIZ--- |
|
|
(Create 2 short multiple-choice question. Use HTML <details> and <summary> tags to hide the answer. Example: |
|
|
**Question**: ... |
|
|
- A) ... |
|
|
- B) ... |
|
|
|
|
|
<details><summary>π Reveal Answers</summary>Correct is A because...</details>) |
|
|
""" |
|
|
|
|
|
def parse_agent_response(full_text): |
|
|
"""Splits the single LLM response into 4 UI components.""" |
|
|
chat_content = full_text |
|
|
|
|
|
|
|
|
videos = "### πΊ Recommended Videos\n*Ask a coding question to get recommendations!*" |
|
|
articles = "### π Articles & Courses\n*Ask a coding question to get resources!*" |
|
|
quiz = "### π§ Quick Quiz\n*Ask a coding question to take a quiz!*" |
|
|
|
|
|
try: |
|
|
|
|
|
if "---SECTION: VIDEOS---" in full_text: |
|
|
parts = full_text.split("---SECTION: VIDEOS---") |
|
|
chat_content = parts[0].strip() |
|
|
remainder = parts[1] |
|
|
|
|
|
if "---SECTION: ARTICLES---" in remainder: |
|
|
v_parts = remainder.split("---SECTION: ARTICLES---") |
|
|
|
|
|
videos = f"### πΊ Recommended Videos\n{v_parts[0].strip()}" |
|
|
remainder = v_parts[1] |
|
|
|
|
|
if "---SECTION: QUIZ---" in remainder: |
|
|
a_parts = remainder.split("---SECTION: QUIZ---") |
|
|
articles = f"### π Articles & Courses\n{a_parts[0].strip()}" |
|
|
quiz = f"### π§ Quick Quiz\n{a_parts[1].strip()}" |
|
|
else: |
|
|
articles = f"### π Articles & Courses\n{remainder.strip()}" |
|
|
else: |
|
|
videos = f"### πΊ Recommended Videos\n{remainder.strip()}" |
|
|
except Exception as e: |
|
|
print(f"Parsing error: {e}") |
|
|
|
|
|
return chat_content, videos, articles, quiz |
|
|
|
|
|
async def run_tutor_dashboard(user_message): |
|
|
""" |
|
|
Main function to run the agent loop. |
|
|
""" |
|
|
server_params = StdioServerParameters( |
|
|
command=sys.executable, |
|
|
args=["server.py"], |
|
|
env=os.environ.copy() |
|
|
) |
|
|
|
|
|
async with stdio_client(server_params) as (read, write): |
|
|
async with ClientSession(read, write) as session: |
|
|
await session.initialize() |
|
|
tools = await load_mcp_tools(session) |
|
|
|
|
|
llm = ChatOpenAI( |
|
|
api_key=NEBIUS_API_KEY, |
|
|
base_url=NEBIUS_BASE_URL, |
|
|
model=MODEL_NAME, |
|
|
temperature=0.7 |
|
|
) |
|
|
|
|
|
agent_executor = create_react_agent(llm, tools) |
|
|
|
|
|
|
|
|
inputs = { |
|
|
"messages": [ |
|
|
SystemMessage(content=SYSTEM_PROMPT), |
|
|
HumanMessage(content=user_message) |
|
|
] |
|
|
} |
|
|
response = await agent_executor.ainvoke(inputs) |
|
|
final_text = response["messages"][-1].content |
|
|
|
|
|
return parse_agent_response(final_text) |
|
|
|
|
|
|
|
|
|
|
|
theme = gr.themes.Soft( |
|
|
primary_hue="slate", |
|
|
secondary_hue="indigo", |
|
|
text_size="lg", |
|
|
spacing_size="md", |
|
|
font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui"], |
|
|
).set( |
|
|
body_background_fill="*neutral_50", |
|
|
block_background_fill="white", |
|
|
block_border_width="1px", |
|
|
block_title_text_weight="600" |
|
|
) |
|
|
|
|
|
with gr.Blocks(title="AI Python Tutor", theme=theme, fill_height=True) as demo: |
|
|
|
|
|
with gr.Row(variant="compact"): |
|
|
with gr.Column(scale=1): |
|
|
gr.Markdown("## π Vibe Coding Academy\n### Your AI-Powered Python Tutor") |
|
|
|
|
|
with gr.Row(equal_height=True): |
|
|
|
|
|
with gr.Column(scale=3, variant="panel"): |
|
|
|
|
|
with gr.Row(): |
|
|
gr.Markdown("### π¬ Interactive Session") |
|
|
fullscreen_btn = gr.Button("βΆ Focus Mode", size="sm", variant="secondary", scale=0, min_width=120) |
|
|
|
|
|
chatbot = gr.Chatbot( |
|
|
height=600, |
|
|
show_label=False, |
|
|
type="messages", |
|
|
bubble_full_width=False, |
|
|
show_copy_button=True, |
|
|
avatar_images=(None, "https://api.dicebear.com/9.x/bottts-neutral/svg?seed=vibe") |
|
|
) |
|
|
|
|
|
with gr.Row(equal_height=True): |
|
|
msg = gr.Textbox( |
|
|
label="What's your goal?", |
|
|
placeholder="Type 'Hello' to start, or ask: 'How do lists work?'", |
|
|
lines=1, |
|
|
scale=5, |
|
|
container=False, |
|
|
autofocus=True |
|
|
) |
|
|
submit_btn = gr.Button("π Start", variant="primary", scale=1) |
|
|
|
|
|
|
|
|
gr.Examples( |
|
|
examples=[ |
|
|
"Hello! I'm new to Python.", |
|
|
"How do for-loops work?", |
|
|
"Explain dictionaries with an example.", |
|
|
"Write a script to calculate Fibonacci numbers." |
|
|
], |
|
|
inputs=msg |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Column(scale=2) as right_col: |
|
|
gr.Markdown("### π Learning Dashboard") |
|
|
|
|
|
|
|
|
with gr.Tabs(): |
|
|
with gr.TabItem("πΊ Videos"): |
|
|
video_box_side = gr.Markdown(value="### Recommended Videos\n*Ask a topic to see video suggestions!*") |
|
|
|
|
|
with gr.TabItem("π Reading"): |
|
|
article_box_side = gr.Markdown(value="### Articles & Docs\n*Ask a topic to see reading materials!*") |
|
|
|
|
|
with gr.TabItem("π§ Quiz"): |
|
|
quiz_box_side = gr.Markdown(value="### Knowledge Check\n*Ask a topic to unlock the quiz!*") |
|
|
|
|
|
|
|
|
with gr.Row(visible=False) as bottom_dashboard: |
|
|
with gr.Column(): |
|
|
gr.Markdown("### π Learning Dashboard") |
|
|
with gr.Tabs(): |
|
|
with gr.TabItem("πΊ Videos"): |
|
|
video_box_bottom = gr.Markdown(value="### Recommended Videos\n*Ask a topic to see video suggestions!*") |
|
|
|
|
|
with gr.TabItem("π Reading"): |
|
|
article_box_bottom = gr.Markdown(value="### Articles & Docs\n*Ask a topic to see reading materials!*") |
|
|
|
|
|
with gr.TabItem("π§ Quiz"): |
|
|
quiz_box_bottom = gr.Markdown(value="### Knowledge Check\n*Ask a topic to unlock the quiz!*") |
|
|
|
|
|
|
|
|
async def respond(user_message, history): |
|
|
if history is None: history = [] |
|
|
|
|
|
|
|
|
history.append({"role": "user", "content": user_message}) |
|
|
|
|
|
history.append({"role": "assistant", "content": "Thinking..."}) |
|
|
|
|
|
|
|
|
|
|
|
yield history, "", "", "", "", "", "", "" |
|
|
|
|
|
|
|
|
chat_text, video_text, article_text, quiz_text = await run_tutor_dashboard(user_message) |
|
|
|
|
|
|
|
|
history[-1]["content"] = chat_text |
|
|
|
|
|
|
|
|
yield history, "", video_text, article_text, quiz_text, video_text, article_text, quiz_text |
|
|
|
|
|
|
|
|
is_fullscreen = gr.State(False) |
|
|
|
|
|
def toggle_fullscreen(current_state): |
|
|
new_state = not current_state |
|
|
|
|
|
|
|
|
|
|
|
side_visible = not new_state |
|
|
bottom_visible = new_state |
|
|
|
|
|
btn_text = "β© Exit Focus" if new_state else "βΆ Focus Mode" |
|
|
|
|
|
return new_state, gr.Column(visible=side_visible), gr.Row(visible=bottom_visible), btn_text |
|
|
|
|
|
fullscreen_btn.click( |
|
|
toggle_fullscreen, |
|
|
inputs=[is_fullscreen], |
|
|
outputs=[is_fullscreen, right_col, bottom_dashboard, fullscreen_btn] |
|
|
) |
|
|
|
|
|
|
|
|
outputs_list = [ |
|
|
chatbot, msg, |
|
|
video_box_side, article_box_side, quiz_box_side, |
|
|
video_box_bottom, article_box_bottom, quiz_box_bottom |
|
|
] |
|
|
|
|
|
submit_btn.click( |
|
|
respond, |
|
|
[msg, chatbot], |
|
|
outputs_list |
|
|
) |
|
|
|
|
|
msg.submit( |
|
|
respond, |
|
|
[msg, chatbot], |
|
|
outputs_list |
|
|
) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.queue().launch() |