File size: 10,623 Bytes
7140e26
e3731b4
7140e26
e3731b4
c8f1de1
e3731b4
 
 
95b74e1
e3731b4
 
c8f1de1
e3731b4
95b74e1
 
ee3275c
e3731b4
697651e
95b74e1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3731b4
95b74e1
e3731b4
 
697651e
 
 
95b74e1
 
 
 
 
697651e
 
95b74e1
697651e
 
 
 
 
 
 
95b74e1
 
697651e
 
 
 
95b74e1
 
697651e
95b74e1
697651e
95b74e1
697651e
 
 
 
 
ee3275c
7140e26
40b28ea
7140e26
e3731b4
 
 
 
 
7140e26
e3731b4
 
 
 
7140e26
e3731b4
 
 
 
 
 
7140e26
95b74e1
697651e
95b74e1
 
 
 
 
 
 
c8f1de1
697651e
e3731b4
697651e
7140e26
95b74e1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3731b4
95b74e1
697651e
95b74e1
 
 
 
 
 
 
 
 
 
 
 
 
 
697651e
95b74e1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
697651e
 
95b74e1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
697651e
95b74e1
ee3275c
 
 
95b74e1
 
 
 
dc90612
95b74e1
 
 
 
 
ee3275c
dc90612
95b74e1
 
697651e
95b74e1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7140e26
697651e
ee3275c
 
95b74e1
697651e
 
 
ee3275c
 
95b74e1
7140e26
 
e3731b4
7140e26
e3731b4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
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

# --- Configuration ---
NEBIUS_API_KEY = os.getenv("NEBIUS_API_KEY") 
NEBIUS_BASE_URL = "https://api.studio.nebius.ai/v1/"
MODEL_NAME = "openai/gpt-oss-20b"
# --- openai/gpt-oss-120b ---
# --- System Prompt ---
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
    
    # Default values for "waiting" state
    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:
        # Only try to split if the separators exist (i.e., we are in Teaching Mode)
        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---")
                # Add headers back for the UI
                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)
            
            # Prepend the SystemMessage to ensure the agent follows instructions
            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)

# --- Gradio Dashboard UI ---
# Professional "Slate" theme
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:
    # Header
    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):
        # Left Column: Chat & Input
        with gr.Column(scale=3, variant="panel"):
            # Custom Header with Focus Mode Button
            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, # Removed built-in label to match custom header
                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)

            # Quick Examples
            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
            )

        # Right Column: Resources Dashboard (Side View - Visible by default)
        with gr.Column(scale=2) as right_col:
            gr.Markdown("### πŸŽ’ Learning Dashboard")
            
            # Tabbed interface for cleaner look
            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!*")

    # Bottom Row: Resources Dashboard (Bottom View - Hidden by default)
    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!*")

    # --- Interaction Logic ---
    async def respond(user_message, history):
        if history is None: history = []
        
        # Immediate user update
        history.append({"role": "user", "content": user_message})
        # Placeholder for AI
        history.append({"role": "assistant", "content": "Thinking..."}) 
        
        # Yield placeholders to ALL output boxes (Side AND Bottom)
        # Structure: history, msg, 3x Side Boxes, 3x Bottom Boxes
        yield history, "", "", "", "", "", "", ""
        
        # Run Agent
        chat_text, video_text, article_text, quiz_text = await run_tutor_dashboard(user_message)
        
        # Update AI response
        history[-1]["content"] = chat_text
        
        # Yield final content to ALL output boxes
        yield history, "", video_text, article_text, quiz_text, video_text, article_text, quiz_text

    # --- Focus Mode Logic ---
    is_fullscreen = gr.State(False)

    def toggle_fullscreen(current_state):
        new_state = not current_state
        # If new_state is True (Fullscreen): Hide side col, Show bottom row
        # If False (Normal): Show side col, Hide bottom row
        
        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]
    )

    # Actions - We must map outputs to BOTH side and bottom components
    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
    )

# --- Launch ---
if __name__ == "__main__":
    demo.queue().launch()