Update app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import os
|
| 2 |
import sys
|
|
|
|
| 3 |
import gradio as gr
|
| 4 |
from langchain_openai import ChatOpenAI
|
| 5 |
from langgraph.prebuilt import create_react_agent
|
|
@@ -9,6 +10,7 @@ from langchain_mcp_adapters.tools import load_mcp_tools
|
|
| 9 |
from langchain_core.messages import HumanMessage, SystemMessage
|
| 10 |
|
| 11 |
# --- Configuration ---
|
|
|
|
| 12 |
NEBIUS_API_KEY = os.getenv("NEBIUS_API_KEY")
|
| 13 |
NEBIUS_BASE_URL = "https://api.studio.nebius.ai/v1/"
|
| 14 |
|
|
@@ -21,29 +23,41 @@ AVAILABLE_MODELS = [
|
|
| 21 |
# --- System Prompt ---
|
| 22 |
SYSTEM_PROMPT = """You are a 'Vibe Coding' Python Tutor.
|
| 23 |
Your goal is to teach by DOING and then providing resources.
|
|
|
|
| 24 |
BEHAVIOR GUIDELINES:
|
| 25 |
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.
|
| 26 |
- DO NOT generate the lesson structure, files, or resources for simple greetings.
|
|
|
|
| 27 |
2. **Teaching Mode**: ONLY when the user asks a coding question or requests a topic (e.g., "dictionaries", "how do loops work"):
|
| 28 |
- **The Lesson**: Explain the concept clearly.
|
| 29 |
- **The Code**: ALWAYS create a Python file, run it, and show the output using tools ('write_file', 'run_python_script').
|
| 30 |
- **The Context**: Use 'list_directory' to see the student's workspace.
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
---SECTION: VIDEOS---
|
| 33 |
(List 2-3 YouTube search queries or URLs relevant to the topic)
|
|
|
|
| 34 |
---SECTION: ARTICLES---
|
| 35 |
(List 2-3 documentation links or course names, e.g., RealPython, FreeCodeCamp)
|
|
|
|
| 36 |
---SECTION: QUIZ---
|
| 37 |
(Create 2 short multiple-choice question. Use HTML <details> and <summary> tags to hide the answer. Example:
|
| 38 |
**Question**: ...
|
| 39 |
- A) ...
|
| 40 |
- B) ...
|
| 41 |
-
|
| 42 |
<details><summary>π Reveal Answers</summary>Correct is A because...</details>)
|
| 43 |
"""
|
| 44 |
|
| 45 |
def parse_agent_response(full_text):
|
| 46 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 47 |
chat_content = full_text
|
| 48 |
|
| 49 |
# Default values for "waiting" state
|
|
@@ -51,36 +65,57 @@ def parse_agent_response(full_text):
|
|
| 51 |
articles = "### π Articles & Courses\n*Ask a coding question to get resources!*"
|
| 52 |
quiz = "### π§ Quick Quiz\n*Ask a coding question to take a quiz!*"
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
try:
|
| 55 |
-
#
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
-
if
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
-
if
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
| 75 |
except Exception as e:
|
| 76 |
print(f"Parsing error: {e}")
|
|
|
|
|
|
|
| 77 |
|
| 78 |
return chat_content, videos, articles, quiz
|
| 79 |
|
| 80 |
async def run_tutor_dashboard(user_message, model_id):
|
| 81 |
"""
|
| 82 |
Main function to run the agent loop.
|
| 83 |
-
|
| 84 |
"""
|
| 85 |
server_params = StdioServerParameters(
|
| 86 |
command=sys.executable,
|
|
@@ -103,7 +138,6 @@ async def run_tutor_dashboard(user_message, model_id):
|
|
| 103 |
|
| 104 |
agent_executor = create_react_agent(llm, tools)
|
| 105 |
|
| 106 |
-
# Prepend the SystemMessage to ensure the agent follows instructions
|
| 107 |
inputs = {
|
| 108 |
"messages": [
|
| 109 |
SystemMessage(content=SYSTEM_PROMPT),
|
|
@@ -116,7 +150,6 @@ async def run_tutor_dashboard(user_message, model_id):
|
|
| 116 |
return parse_agent_response(final_text)
|
| 117 |
|
| 118 |
# --- Gradio Dashboard UI ---
|
| 119 |
-
# Professional "Slate" theme
|
| 120 |
theme = gr.themes.Soft(
|
| 121 |
primary_hue="slate",
|
| 122 |
secondary_hue="indigo",
|
|
@@ -133,11 +166,9 @@ theme = gr.themes.Soft(
|
|
| 133 |
with gr.Blocks(title="AI Python Tutor", theme=theme, fill_height=True) as demo:
|
| 134 |
# --- Header with Model Selector ---
|
| 135 |
with gr.Row(variant="compact", elem_classes="header-row"):
|
| 136 |
-
# scale=1: Consumes all available space, pushing the next column to the right
|
| 137 |
with gr.Column(scale=1):
|
| 138 |
gr.Markdown("## π AI Python Tutor")
|
| 139 |
|
| 140 |
-
# scale=0: Takes only minimum required width, sitting on the far right
|
| 141 |
with gr.Column(scale=0, min_width=250):
|
| 142 |
model_selector = gr.Dropdown(
|
| 143 |
choices=AVAILABLE_MODELS,
|
|
@@ -151,7 +182,6 @@ with gr.Blocks(title="AI Python Tutor", theme=theme, fill_height=True) as demo:
|
|
| 151 |
with gr.Row(equal_height=True):
|
| 152 |
# Left Column: Chat & Input
|
| 153 |
with gr.Column(scale=3, variant="panel"):
|
| 154 |
-
# Custom Header with Focus Mode Button
|
| 155 |
with gr.Row():
|
| 156 |
gr.Markdown("### π¬ Interactive Session")
|
| 157 |
fullscreen_btn = gr.Button("βΆ Focus Mode", size="sm", variant="secondary", scale=0, min_width=120)
|
|
@@ -191,7 +221,6 @@ with gr.Blocks(title="AI Python Tutor", theme=theme, fill_height=True) as demo:
|
|
| 191 |
with gr.Column(scale=2) as right_col:
|
| 192 |
gr.Markdown("### π Learning Dashboard")
|
| 193 |
|
| 194 |
-
# Tabbed interface for cleaner look
|
| 195 |
with gr.Tabs():
|
| 196 |
with gr.TabItem("πΊ Videos"):
|
| 197 |
video_box_side = gr.Markdown(value="### Recommended Videos\n*Ask a topic to see video suggestions!*")
|
|
@@ -265,14 +294,12 @@ with gr.Blocks(title="AI Python Tutor", theme=theme, fill_height=True) as demo:
|
|
| 265 |
video_box_bottom, article_box_bottom, quiz_box_bottom
|
| 266 |
]
|
| 267 |
|
| 268 |
-
# Added model_selector to inputs
|
| 269 |
submit_btn.click(
|
| 270 |
respond,
|
| 271 |
[msg, chatbot, model_selector],
|
| 272 |
outputs_list
|
| 273 |
)
|
| 274 |
|
| 275 |
-
# Added model_selector to inputs
|
| 276 |
msg.submit(
|
| 277 |
respond,
|
| 278 |
[msg, chatbot, model_selector],
|
|
|
|
| 1 |
import os
|
| 2 |
import sys
|
| 3 |
+
import re
|
| 4 |
import gradio as gr
|
| 5 |
from langchain_openai import ChatOpenAI
|
| 6 |
from langgraph.prebuilt import create_react_agent
|
|
|
|
| 10 |
from langchain_core.messages import HumanMessage, SystemMessage
|
| 11 |
|
| 12 |
# --- Configuration ---
|
| 13 |
+
# Ensure your API key is set in your environment variables
|
| 14 |
NEBIUS_API_KEY = os.getenv("NEBIUS_API_KEY")
|
| 15 |
NEBIUS_BASE_URL = "https://api.studio.nebius.ai/v1/"
|
| 16 |
|
|
|
|
| 23 |
# --- System Prompt ---
|
| 24 |
SYSTEM_PROMPT = """You are a 'Vibe Coding' Python Tutor.
|
| 25 |
Your goal is to teach by DOING and then providing resources.
|
| 26 |
+
|
| 27 |
BEHAVIOR GUIDELINES:
|
| 28 |
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.
|
| 29 |
- DO NOT generate the lesson structure, files, or resources for simple greetings.
|
| 30 |
+
|
| 31 |
2. **Teaching Mode**: ONLY when the user asks a coding question or requests a topic (e.g., "dictionaries", "how do loops work"):
|
| 32 |
- **The Lesson**: Explain the concept clearly.
|
| 33 |
- **The Code**: ALWAYS create a Python file, run it, and show the output using tools ('write_file', 'run_python_script').
|
| 34 |
- **The Context**: Use 'list_directory' to see the student's workspace.
|
| 35 |
+
|
| 36 |
+
CRITICAL FORMATTING INSTRUCTIONS:
|
| 37 |
+
When in "Teaching Mode", you MUST end your response by strictly following this format.
|
| 38 |
+
Do not add extra text between the sections.
|
| 39 |
+
|
| 40 |
+
(End of your main lesson text)
|
| 41 |
+
|
| 42 |
---SECTION: VIDEOS---
|
| 43 |
(List 2-3 YouTube search queries or URLs relevant to the topic)
|
| 44 |
+
|
| 45 |
---SECTION: ARTICLES---
|
| 46 |
(List 2-3 documentation links or course names, e.g., RealPython, FreeCodeCamp)
|
| 47 |
+
|
| 48 |
---SECTION: QUIZ---
|
| 49 |
(Create 2 short multiple-choice question. Use HTML <details> and <summary> tags to hide the answer. Example:
|
| 50 |
**Question**: ...
|
| 51 |
- A) ...
|
| 52 |
- B) ...
|
|
|
|
| 53 |
<details><summary>π Reveal Answers</summary>Correct is A because...</details>)
|
| 54 |
"""
|
| 55 |
|
| 56 |
def parse_agent_response(full_text):
|
| 57 |
+
"""
|
| 58 |
+
Robust parsing using Regex to handle LLM formatting inconsistencies.
|
| 59 |
+
Splits the single LLM response into 4 UI components: Chat, Videos, Articles, Quiz.
|
| 60 |
+
"""
|
| 61 |
chat_content = full_text
|
| 62 |
|
| 63 |
# Default values for "waiting" state
|
|
|
|
| 65 |
articles = "### π Articles & Courses\n*Ask a coding question to get resources!*"
|
| 66 |
quiz = "### π§ Quick Quiz\n*Ask a coding question to take a quiz!*"
|
| 67 |
|
| 68 |
+
# Regex patterns that allow for slight variations in spacing or casing
|
| 69 |
+
# Examples caught: "---SECTION: VIDEOS---", "---SECTION:VIDEOS ---", "--- section: videos ---"
|
| 70 |
+
video_pattern = r"---SECTION:\s*VIDEOS\s*---"
|
| 71 |
+
article_pattern = r"---SECTION:\s*ARTICLES\s*---"
|
| 72 |
+
quiz_pattern = r"---SECTION:\s*QUIZ\s*---"
|
| 73 |
+
|
| 74 |
try:
|
| 75 |
+
# 1. Check if we are in Teaching Mode (look for Video section)
|
| 76 |
+
# re.split returns a list. If found, index 0 is before the split, index 1 is after.
|
| 77 |
+
split_video = re.split(video_pattern, full_text, flags=re.IGNORECASE, maxsplit=1)
|
| 78 |
+
|
| 79 |
+
if len(split_video) > 1:
|
| 80 |
+
chat_content = split_video[0].strip()
|
| 81 |
+
remaining_resources = split_video[1]
|
| 82 |
+
|
| 83 |
+
# 2. Extract Videos vs Articles
|
| 84 |
+
split_article = re.split(article_pattern, remaining_resources, flags=re.IGNORECASE, maxsplit=1)
|
| 85 |
|
| 86 |
+
if len(split_article) > 0:
|
| 87 |
+
# Everything before ARTICLE tag is Video content
|
| 88 |
+
video_content = split_article[0].strip()
|
| 89 |
+
if video_content:
|
| 90 |
+
videos = f"### πΊ Recommended Videos\n{video_content}"
|
| 91 |
+
|
| 92 |
+
if len(split_article) > 1:
|
| 93 |
+
remaining_quiz = split_article[1]
|
| 94 |
+
|
| 95 |
+
# 3. Extract Articles vs Quiz
|
| 96 |
+
split_quiz = re.split(quiz_pattern, remaining_quiz, flags=re.IGNORECASE, maxsplit=1)
|
| 97 |
|
| 98 |
+
if len(split_quiz) > 0:
|
| 99 |
+
article_content = split_quiz[0].strip()
|
| 100 |
+
if article_content:
|
| 101 |
+
articles = f"### π Articles & Courses\n{article_content}"
|
| 102 |
+
|
| 103 |
+
if len(split_quiz) > 1:
|
| 104 |
+
quiz_content = split_quiz[1].strip()
|
| 105 |
+
if quiz_content:
|
| 106 |
+
quiz = f"### π§ Quick Quiz\n{quiz_content}"
|
| 107 |
+
|
| 108 |
except Exception as e:
|
| 109 |
print(f"Parsing error: {e}")
|
| 110 |
+
# Fallback: Just show everything in chat if parsing fails completely, so no data is lost
|
| 111 |
+
chat_content = full_text
|
| 112 |
|
| 113 |
return chat_content, videos, articles, quiz
|
| 114 |
|
| 115 |
async def run_tutor_dashboard(user_message, model_id):
|
| 116 |
"""
|
| 117 |
Main function to run the agent loop.
|
| 118 |
+
Accepts 'model_id' to select the LLM dynamically.
|
| 119 |
"""
|
| 120 |
server_params = StdioServerParameters(
|
| 121 |
command=sys.executable,
|
|
|
|
| 138 |
|
| 139 |
agent_executor = create_react_agent(llm, tools)
|
| 140 |
|
|
|
|
| 141 |
inputs = {
|
| 142 |
"messages": [
|
| 143 |
SystemMessage(content=SYSTEM_PROMPT),
|
|
|
|
| 150 |
return parse_agent_response(final_text)
|
| 151 |
|
| 152 |
# --- Gradio Dashboard UI ---
|
|
|
|
| 153 |
theme = gr.themes.Soft(
|
| 154 |
primary_hue="slate",
|
| 155 |
secondary_hue="indigo",
|
|
|
|
| 166 |
with gr.Blocks(title="AI Python Tutor", theme=theme, fill_height=True) as demo:
|
| 167 |
# --- Header with Model Selector ---
|
| 168 |
with gr.Row(variant="compact", elem_classes="header-row"):
|
|
|
|
| 169 |
with gr.Column(scale=1):
|
| 170 |
gr.Markdown("## π AI Python Tutor")
|
| 171 |
|
|
|
|
| 172 |
with gr.Column(scale=0, min_width=250):
|
| 173 |
model_selector = gr.Dropdown(
|
| 174 |
choices=AVAILABLE_MODELS,
|
|
|
|
| 182 |
with gr.Row(equal_height=True):
|
| 183 |
# Left Column: Chat & Input
|
| 184 |
with gr.Column(scale=3, variant="panel"):
|
|
|
|
| 185 |
with gr.Row():
|
| 186 |
gr.Markdown("### π¬ Interactive Session")
|
| 187 |
fullscreen_btn = gr.Button("βΆ Focus Mode", size="sm", variant="secondary", scale=0, min_width=120)
|
|
|
|
| 221 |
with gr.Column(scale=2) as right_col:
|
| 222 |
gr.Markdown("### π Learning Dashboard")
|
| 223 |
|
|
|
|
| 224 |
with gr.Tabs():
|
| 225 |
with gr.TabItem("πΊ Videos"):
|
| 226 |
video_box_side = gr.Markdown(value="### Recommended Videos\n*Ask a topic to see video suggestions!*")
|
|
|
|
| 294 |
video_box_bottom, article_box_bottom, quiz_box_bottom
|
| 295 |
]
|
| 296 |
|
|
|
|
| 297 |
submit_btn.click(
|
| 298 |
respond,
|
| 299 |
[msg, chatbot, model_selector],
|
| 300 |
outputs_list
|
| 301 |
)
|
| 302 |
|
|
|
|
| 303 |
msg.submit(
|
| 304 |
respond,
|
| 305 |
[msg, chatbot, model_selector],
|