Kiy-K commited on
Commit
58ed551
·
verified ·
1 Parent(s): 595439c

Update demo.py

Browse files
Files changed (1) hide show
  1. demo.py +270 -503
demo.py CHANGED
@@ -18,13 +18,12 @@ try:
18
  except ImportError:
19
  E2B_AVAILABLE = False
20
 
21
- # Import for Web Search (DuckDuckGo is much more stable than raw Google scraping)
22
  try:
23
  from ddgs import DDGS
24
  SEARCH_AVAILABLE = True
25
  except ImportError:
26
  SEARCH_AVAILABLE = False
27
- print("⚠️ 'duckduckgo-search' not found. Install it via: pip install ddgs")
28
 
29
  # Setup logging
30
  logging.basicConfig(level=logging.INFO)
@@ -35,27 +34,66 @@ logger = logging.getLogger("FyodorIDE")
35
  MINIMAX_API_KEY = os.getenv("MINIMAX_API_KEY")
36
  E2B_API_KEY = os.getenv("E2B_API_KEY")
37
 
38
- # Client Configuration
39
  API_CONFIG = {
40
  "api_key": MINIMAX_API_KEY if MINIMAX_API_KEY else "dummy-key",
41
  "base_url": "https://api.minimax.io/v1",
42
  }
43
  MODEL_ID = "MiniMax-M2"
 
44
 
45
- # Session Duration (in seconds)
46
- SESSION_TIMEOUT_SEC = 15 * 60 # 15 Minutes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
- # ==================== TOOL: E2B SANDBOX ====================
49
 
50
- def execute_code_e2b(code_block):
 
 
 
51
  if not E2B_AVAILABLE or not E2B_API_KEY:
52
  return "❌ Error: E2B API Key missing. Cannot execute code."
53
 
54
- logger.info("⚡ Executing in E2B Sandbox (ISOLATED - No Internet Access)...")
 
 
 
 
55
  try:
56
- # Create a fully isolated sandbox with NO internet access
57
  with Sandbox.create(allow_internet_access=False) as sandbox:
58
- execution = sandbox.run_code(code_block)
 
 
 
 
 
 
59
 
60
  output_parts = []
61
  if execution.logs.stdout:
@@ -73,258 +111,76 @@ def execute_code_e2b(code_block):
73
  except Exception as e:
74
  return f"❌ Sandbox Error: {str(e)}"
75
 
76
- # ==================== HELPER FUNCTIONS ====================
77
-
78
- def export_script(code):
79
- """Saves the current code to a temporary .py file for download."""
80
- if not code.strip():
81
- return None
82
-
83
- try:
84
- with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.py', prefix='fyodor_script_') as f:
85
- f.write(code)
86
- return f.name
87
- except Exception as e:
88
- logger.error(f"Export failed: {e}")
89
- return None
90
 
91
- def execute_modified_code(code, chatbot, session_data):
92
- """Executes manually modified code and updates chat history."""
93
- if session_data is None:
94
- session_data = init_session()
95
-
96
- elapsed = time.time() - session_data["start_time"]
97
- if elapsed >= SESSION_TIMEOUT_SEC:
98
- logger.info("🔄 Session expired during execution - Auto-renewing...")
99
- session_data = init_session()
100
-
101
- history = session_data["history"]
102
-
103
- if not code.strip():
104
- # Return current history (list of dicts) if no code
105
- return history
106
-
107
- # 1. Append User Action
108
- history.append({"role": "user", "content": "🔄 **User Modified Code Execution**"})
109
-
110
- # 2. Execute Code
111
- result = execute_code_e2b(code)
112
-
113
- # 3. Append Assistant Result
114
- history.append({"role": "assistant", "content": f"**Execution Result:**\n\n{result}"})
115
-
116
- # 4. Return the updated history list (Gradio Messages format)
117
- # DO NOT use legacy list of lists format: chatbot + [[msg1, msg2]]
118
- return history
119
-
120
- # ==================== ROBUST WEB SEARCH (DuckDuckGo) ====================
121
-
122
- def search_web(query, max_results=6):
123
  """
124
- Searches the web using DuckDuckGo.
125
- Returns strict JSON output to ensure clean data for agents.
126
  """
127
- if not SEARCH_AVAILABLE:
128
- return json.dumps({"error": "`duckduckgo-search` library not installed"})
129
-
130
- logger.info(f"🔍 Searching Web for: {query}")
131
-
132
- try:
133
- results = []
134
- # DDGS() is the context manager for DuckDuckGo Search
135
- with DDGS() as ddgs:
136
- # .text() performs a standard text search
137
- ddg_results = list(ddgs.text(query, max_results=max_results))
138
-
139
- if not ddg_results:
140
- return json.dumps({"status": "no_results", "query": query})
141
-
142
- for idx, r in enumerate(ddg_results, 1):
143
- results.append({
144
- "position": idx,
145
- "title": r.get('title', 'No Title'),
146
- "url": r.get('href', '#'),
147
- "snippet": r.get('body', 'No description available')
148
- })
149
-
150
- # Return strictly JSON
151
- return json.dumps(results, indent=2)
152
-
153
- except Exception as e:
154
- logger.error(f"Search failed: {e}")
155
- return json.dumps({"error": "Search Failed", "details": str(e)})
156
-
157
- # ==================== MINI AGENT (CHAT + SEARCH) ====================
158
-
159
- def mini_agent_chat(message, history):
160
- """Lightweight chat agent with robust Web Search"""
161
- if not message.strip():
162
- yield history
163
- return
164
-
165
- if not isinstance(history, list):
166
- history = []
167
-
168
- history.append({"role": "user", "content": message})
169
- yield history
170
-
171
- try:
172
- client = OpenAI(**API_CONFIG)
173
-
174
- # Detect if search is needed
175
- search_keywords = [
176
- "latest", "current", "recent", "today", "news", "who is", "search",
177
- "find", "lookup", "google", "weather", "price", "review", "meaning",
178
- "location", "what happened", "how to", "top", "best"
179
- ]
180
- needs_search = any(keyword in message.lower() for keyword in search_keywords)
181
-
182
- # Build messages
183
- messages = [
184
- {"role": "system", "content": "You are a helpful assistant with web access. Be concise. Mention when searching."}
185
- ]
186
- messages.extend(history[-6:])
187
-
188
- # Stream response
189
- response = client.chat.completions.create(
190
- model=MODEL_ID,
191
- messages=messages,
192
- temperature=0.7,
193
- max_tokens=4000,
194
- stream=True
195
- )
196
-
197
- full_response = ""
198
- history.append({"role": "assistant", "content": ""})
199
-
200
- for chunk in response:
201
- if chunk.choices and chunk.choices[0].delta:
202
- delta_content = chunk.choices[0].delta.content
203
- if delta_content:
204
- full_response += delta_content
205
- clean_response = re.sub(r"<think>.*?</think>", "", full_response, flags=re.DOTALL).strip()
206
- history[-1]["content"] = clean_response
207
- yield history
208
-
209
- # Perform search if needed
210
- if needs_search:
211
- history[-1]["content"] += "\n\n🔍 *Searching Web...*"
212
- yield history
213
-
214
- # Use the new robust search function
215
- search_results = search_web(message)
216
-
217
- # Display as JSON code block
218
- history[-1]["content"] = full_response + f"\n\n---\n```json\n{search_results}\n```"
219
- yield history
220
-
221
- except Exception as e:
222
- if history and history[-1]["role"] == "assistant":
223
- history[-1]["content"] = f"❌ Error: {str(e)}"
224
- else:
225
- history.append({"role": "assistant", "content": f"❌ Error: {str(e)}"})
226
- yield history
227
-
228
- # ==================== SYSTEM PROMPT ====================
229
-
230
- SYSTEM_PROMPT = """
231
- You are the intelligent engine behind Fyodor IDE.
232
- You are running in a FULLY ISOLATED E2B Sandbox with ZERO internet access.
233
-
234
- ### CRITICAL SECURITY NOTICE
235
- 🔒 This sandbox has NO internet connectivity. All network requests are blocked.
236
- - Cannot access external APIs (OpenAI, HuggingFace, etc.)
237
- - Cannot download files from the web
238
- - Cannot make HTTP/HTTPS requests
239
- - Cannot use pip install (packages must be pre-installed)
240
-
241
- ### MISSION
242
- - Write Python code to solve user tasks USING ONLY LOCAL RESOURCES.
243
- - Execute the code to verify results or generate visualizations.
244
- - Accept user feedback to iteratively improve the code.
245
-
246
- ### COMMANDS
247
- 1. **CODE:**
248
- - Use this to write and execute Python code.
249
- - Format:
250
- CODE:
251
- ```python
252
- # code here
253
- ```
254
-
255
- 2. **ANSWER:**
256
- - Use this for explanations or confirming completion.
257
-
258
- ### RULES
259
- - **Fully Isolated**: This sandbox is COMPLETELY OFFLINE. Do NOT attempt any network operations.
260
- - **Pre-installed Libraries Only**: Use standard libraries (numpy, matplotlib, pandas, etc.) that are already installed.
261
- - **Visualization**: Use `matplotlib.pyplot` for plots. Always call `plt.show()`.
262
- - **Self-Correction**: If the user gives feedback (e.g., "Change color to red"), rewrite the code with the fix.
263
- - **No User Input**: Do not use `input()`. Hardcode variables for demonstration.
264
- - **Local Data Only**: Work with generated data, mathematical functions, or data the user provides directly.
265
- """
266
-
267
- # ==================== AGENT LOGIC ====================
268
-
269
- def parse_command(text):
270
- clean_text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL).strip()
271
-
272
  if "```python" in clean_text:
273
  pattern = r"```python\n(.*?)```"
274
  matches = re.findall(pattern, clean_text, re.DOTALL)
275
- code = matches[-1] if matches else None
276
- return "CODE", code, clean_text
277
-
278
- return "ANSWER", None, clean_text
279
-
280
- def init_session():
281
- return {
282
- "history": [],
283
- "start_time": time.time(),
284
- "active": True
285
- }
286
-
287
- def check_session_status(session_data):
288
- """Real-time timer callback with auto-renewal."""
289
- if session_data is None:
290
- session_data = init_session()
291
- return "🟢 Active - 15:00", session_data
292
-
293
- elapsed = time.time() - session_data["start_time"]
294
- remaining = max(0, SESSION_TIMEOUT_SEC - elapsed)
295
-
296
- if remaining == 0:
297
- logger.info("🔄 Session expired - Auto-renewing...")
298
- session_data = init_session()
299
- return "🟢 Active - 15:00 (Auto-renewed)", session_data
300
 
301
- minutes = int(remaining // 60)
302
- seconds = int(remaining % 60)
303
- return f"🟢 Active - {minutes}:{seconds:02d}", session_data
304
-
305
- def process_query(user_input, session_data, current_code):
306
- """Main Interaction Loop with Auto-Renewal Session Management"""
307
- if session_data is None:
308
  session_data = init_session()
309
-
310
- elapsed = time.time() - session_data["start_time"]
311
- if elapsed >= SESSION_TIMEOUT_SEC:
312
- logger.info("🔄 Session expired during query - Auto-renewing...")
313
- session_data = init_session()
314
-
315
- history = session_data["history"]
316
 
 
317
  if not user_input:
318
- yield history, current_code, session_data
319
  return
320
 
321
  history.append({"role": "user", "content": user_input})
322
- history.append({"role": "assistant", "content": "🧠 *Fyodor is thinking...*"})
323
- yield history, current_code, session_data
324
 
325
- messages = [{"role": "system", "content": SYSTEM_PROMPT}]
 
 
326
 
327
- for msg in history[:-2][-10:]:
 
 
 
 
 
 
 
328
  if msg['role'] == 'user':
329
  messages.append({"role": "user", "content": msg['content']})
330
  elif msg['role'] == 'assistant':
@@ -336,43 +192,82 @@ def process_query(user_input, session_data, current_code):
336
  try:
337
  client = OpenAI(**API_CONFIG)
338
  response = client.chat.completions.create(
339
- model=MODEL_ID,
340
- messages=messages,
341
- temperature=0.1,
342
- max_tokens=16000,
343
- stream=True
344
  )
345
 
346
  full_response = ""
347
-
348
  for chunk in response:
349
- if not chunk.choices: continue
350
- delta = chunk.choices[0].delta
351
- if delta and delta.content:
352
- full_response += delta.content
353
- clean_display = re.sub(r"<think>.*?</think>", "", full_response, flags=re.DOTALL)
354
  history[-1]['content'] = clean_display
355
- yield history, current_code, session_data
356
 
357
- action_type, payload, clean_text = parse_command(full_response)
358
- history[-1]['content'] = clean_text
359
- yield history, current_code, session_data
 
 
 
360
 
361
- if action_type == "CODE":
362
- history[-1]['content'] += "\n\n⏳ *Running in Fyodor Sandbox...*"
363
- current_code = payload
364
- yield history, current_code, session_data
365
-
366
- exec_res = execute_code_e2b(payload)
367
- history[-1]['content'] += f"\n\n---\n{exec_res}"
368
- yield history, current_code, session_data
369
 
370
  except Exception as e:
371
  history[-1]['content'] += f"\n\n❌ Error: {str(e)}"
372
- yield history, current_code, session_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
 
374
  # ==================== UI ====================
375
 
 
376
  WINDOWS_TERMINAL_CSS = """
377
  /* Windows Terminal Theme - Campbell Dark */
378
  :root {
@@ -501,7 +396,7 @@ input[type="text"]:focus, textarea:focus {
501
  background-color: #1a1a1a !important;
502
  border: 1px solid var(--wt-border) !important;
503
  border-radius: 6px !important;
504
- padding: 1px !important; /* Reduced padding */
505
  }
506
 
507
  .cm-scroller {
@@ -563,12 +458,12 @@ label {
563
  letter-spacing: 0.5px;
564
  }
565
 
566
- /* IMPROVED SESSION STATUS BAR */
567
  .timer-box {
568
  background: linear-gradient(135deg, #1a1a1a 0%, #0a0a0a 100%) !important;
569
  border: 1px solid var(--wt-border) !important;
570
  border-radius: 6px !important;
571
- padding: 0px !important; /* Reset padding here */
572
  box-shadow: inset 0 0 10px rgba(0,0,0,0.5);
573
  }
574
 
@@ -586,258 +481,130 @@ label {
586
  box-shadow: none !important;
587
  }
588
 
589
- ::-webkit-scrollbar {
590
- width: 12px;
591
- height: 12px;
592
- }
593
-
594
- ::-webkit-scrollbar-track {
595
- background: var(--wt-bg);
596
- }
597
-
598
- ::-webkit-scrollbar-thumb {
599
- background: var(--wt-bright-black);
600
- border-radius: 6px;
601
- border: 2px solid var(--wt-bg);
602
- }
603
-
604
- ::-webkit-scrollbar-thumb:hover {
605
- background: var(--wt-cyan);
606
- }
607
-
608
- @media (max-width: 768px) {
609
- .responsive-row {
610
- flex-direction: column !important;
611
- display: flex !important;
612
- }
613
- .responsive-row > * {
614
- width: 100% !important;
615
- min-width: 100% !important;
616
- margin-bottom: 20px;
617
- }
618
- .gradio-container {
619
- padding: 10px !important;
620
- }
621
- .chatbot {
622
- height: 500px !important;
623
- }
624
- }
625
-
626
- .mini-chat .message.user {
627
- background: linear-gradient(135deg, #881798 0%, #C50F1F 100%) !important;
628
- }
629
-
630
- .mini-chat .message.bot {
631
- background-color: #0a0a0a !important;
632
- border: 1px solid #444 !important;
633
- }
634
-
635
- .accordion {
636
- background-color: #1a1a1a !important;
637
- border: 1px solid var(--wt-border) !important;
638
- border-radius: 8px !important;
639
- margin: 8px 0 !important;
640
- }
641
-
642
- .success-text {
643
- color: var(--wt-green) !important;
644
- }
645
 
646
- .error-text {
647
- color: var(--wt-red) !important;
648
- }
649
-
650
- .warning-text {
651
- color: var(--wt-yellow) !important;
652
- }
653
  """
654
 
655
  def run_app():
656
- with gr.Blocks(title="🛡️ Fyodor IDE | Search Edition") as demo:
657
- gr.Markdown("# 🛡️ Fyodor IDE - Search Edition")
658
-
659
- # Check components status
660
- search_status = "✅ Active" if SEARCH_AVAILABLE else "⚠️ Install `duckduckgo-search`"
661
- e2b_status = '✅ E2B (Isolated)' if E2B_API_KEY else '❌ Missing Key'
662
-
663
- gr.Markdown(
664
- f"**Model:** `{MODEL_ID}` | **Sandbox:** {e2b_status} | "
665
- f"**Search:** {search_status}\n\n"
666
- "🔒 **Main Agent:** Fully isolated sandbox | 🔍 **Mini Agent:** DuckDuckGo Web Search\n\n"
667
- "⚡ Auto-reset: **15 minutes**"
668
- )
669
 
 
670
  with gr.Row(elem_classes="responsive-row"):
671
- # LEFT COLUMN
672
  with gr.Column(scale=1):
673
- with gr.Row():
674
- # UPDATED: No label, text inside, cleaner look
675
- timer_display = gr.Textbox(
676
- value="⏱️ SESSION: Initializing...",
677
- show_label=False,
678
- interactive=False,
679
- elem_classes="timer-box",
680
- container=True
681
- )
682
-
683
- chatbot = gr.Chatbot(
684
- label="💬 Fyodor Terminal",
685
- height=600,
686
- # Removed 'type' argument as previously discussed
687
- render_markdown=True,
688
- elem_classes="chatbot"
689
  )
690
-
691
- with gr.Row():
692
- msg_input = gr.Textbox(
693
- placeholder="PS C:\\Users\\dev> _",
694
- scale=4,
695
- container=False,
696
- show_label=False
697
- )
698
- run_btn = gr.Button("⚡ EXECUTE", variant="primary", scale=1)
699
-
700
- # MIDDLE COLUMN
701
  with gr.Column(scale=1):
702
- with gr.Tabs():
703
- with gr.TabItem("💻 Code Editor", id="code_tab"):
704
- gr.Markdown("**Live Python Editor** | Edit and re-run generated code")
705
- code_view = gr.Code(
706
- language="python",
707
- lines=28,
708
- interactive=True,
709
- show_label=False
710
- )
711
-
712
- with gr.Row():
713
- run_mod_btn = gr.Button("▶️ Run Modified", variant="secondary", scale=1)
714
- export_btn = gr.DownloadButton("⬇️ Export", variant="secondary", scale=1)
715
-
716
- # RIGHT COLUMN
717
- with gr.Column(scale=0.6, min_width=300):
718
- with gr.Accordion("🤖 Mini Agent (Web Search)", open=False):
719
- gr.Markdown("*Robust DuckDuckGo Search*")
720
-
721
- mini_chatbot = gr.Chatbot(
722
- label="",
723
- height=400,
724
- render_markdown=True,
725
- show_label=False,
726
- elem_classes="mini-chat"
727
- )
728
-
729
- with gr.Row():
730
- mini_input = gr.Textbox(
731
- placeholder="Ask or search anything...",
732
- scale=4,
733
- show_label=False,
734
- container=False
735
- )
736
- mini_send = gr.Button("🔍", variant="secondary", scale=1)
737
-
738
- mini_clear = gr.Button("🗑️ Clear", variant="secondary", size="sm")
739
 
740
- with gr.Accordion("ℹ️ Setup & Info", open=False):
741
- gr.Markdown(f"""
742
- ### Fyodor IDE - Search Edition
743
-
744
- **Main Terminal (Isolated):**
745
- - 🔒 E2B sandbox (no internet)
746
- - Python code execution
747
- - Live code editing
748
- - Data visualization
749
-
750
- **Mini Agent (Web Search):**
751
- - 🔍 Real-time Web Search via DuckDuckGo
752
- - No API Keys required for search
753
- - No Proxy needed
754
- - Robust JSON results
755
-
756
- **How It Works:**
757
- 1. User asks question
758
- 2. Agent detects search intent
759
- 3. DuckDuckGo API returns clean JSON
760
- 4. Results are formatted and displayed
761
- """)
762
-
763
- # State Management
764
- session_state = gr.State(init_session())
765
- current_code_state = gr.State("")
766
 
767
- # Timer function UPDATE to match new format
768
- def update_timer(session_data):
769
- if session_data is None:
770
- session_data = init_session()
771
- return "⏱️ SESSION: Active - 15:00", session_data
772
-
773
- elapsed = time.time() - session_data["start_time"]
774
- remaining = max(0, SESSION_TIMEOUT_SEC - elapsed)
775
-
776
- if remaining == 0:
777
- session_data = init_session()
778
- return "⏱️ SESSION: Active - 15:00 (Renewed)", session_data
779
-
780
- minutes = int(remaining // 60)
781
- seconds = int(remaining % 60)
782
- return f"⏱️ SESSION: Active - {minutes}:{seconds:02d}", session_data
783
-
784
- # Timer
785
- timer = gr.Timer(1)
786
- timer.tick(update_timer, inputs=[session_state], outputs=[timer_display, session_state])
787
-
788
- # Main Chat
789
  msg_input.submit(
790
  process_query,
791
- [msg_input, session_state, current_code_state],
792
- [chatbot, code_view, session_state]
793
- ).then(lambda: "", None, [msg_input])
794
-
795
  run_btn.click(
796
  process_query,
797
- [msg_input, session_state, current_code_state],
798
- [chatbot, code_view, session_state]
799
- ).then(lambda: "", None, [msg_input])
800
-
801
- # Code Execution
802
- run_mod_btn.click(
803
- execute_modified_code,
804
- inputs=[code_view, chatbot, session_state],
805
- outputs=[chatbot]
 
 
806
  )
807
 
808
- export_btn.click(
809
- export_script,
810
- inputs=[code_view],
811
- outputs=[export_btn]
812
  )
813
 
814
- # Mini Agent
815
- mini_input.submit(
816
- mini_agent_chat,
817
- [mini_input, mini_chatbot],
818
- [mini_chatbot]
819
- ).then(lambda: "", None, [mini_input])
 
 
 
 
 
 
820
 
821
- mini_send.click(
822
- mini_agent_chat,
823
- [mini_input, mini_chatbot],
824
- [mini_chatbot]
825
- ).then(lambda: "", None, [mini_input])
826
 
827
- mini_clear.click(
828
- lambda: [],
829
- None,
830
- [mini_chatbot]
 
 
 
 
 
 
831
  )
832
 
833
  return demo
834
 
835
  if __name__ == "__main__":
836
- MOBILE_HEAD = '<meta name="viewport" content="width=device-width, initial-scale=1.0">'
837
-
838
- run_app().queue().launch(
839
- server_name="0.0.0.0",
840
- server_port=7860,
841
- css=WINDOWS_TERMINAL_CSS,
842
- head=MOBILE_HEAD
843
- )
 
18
  except ImportError:
19
  E2B_AVAILABLE = False
20
 
21
+ # Import for Web Search
22
  try:
23
  from ddgs import DDGS
24
  SEARCH_AVAILABLE = True
25
  except ImportError:
26
  SEARCH_AVAILABLE = False
 
27
 
28
  # Setup logging
29
  logging.basicConfig(level=logging.INFO)
 
34
  MINIMAX_API_KEY = os.getenv("MINIMAX_API_KEY")
35
  E2B_API_KEY = os.getenv("E2B_API_KEY")
36
 
 
37
  API_CONFIG = {
38
  "api_key": MINIMAX_API_KEY if MINIMAX_API_KEY else "dummy-key",
39
  "base_url": "https://api.minimax.io/v1",
40
  }
41
  MODEL_ID = "MiniMax-M2"
42
+ SESSION_TIMEOUT_SEC = 15 * 60
43
 
44
+ # ==================== HELPER FUNCTIONS ====================
45
+
46
+ def clean_thought_process(text):
47
+ """Removes the <think>...</think> blocks from the text to prevent leaks."""
48
+ return re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL).strip()
49
+
50
+ def export_script(code):
51
+ if not code.strip(): return None
52
+ try:
53
+ with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.py', prefix='fyodor_script_') as f:
54
+ f.write(code)
55
+ return f.name
56
+ except Exception as e:
57
+ return None
58
+
59
+ def robust_search(query):
60
+ if not SEARCH_AVAILABLE: return json.dumps({"error": "Missing duckduckgo-search"})
61
+ try:
62
+ results = []
63
+ with DDGS() as ddgs:
64
+ for idx, r in enumerate(list(ddgs.text(query, max_results=5)), 1):
65
+ results.append({
66
+ "title": r.get('title'),
67
+ "link": r.get('href'),
68
+ "snippet": r.get('body')
69
+ })
70
+ return json.dumps(results, indent=2)
71
+ except Exception as e:
72
+ return json.dumps({"error": str(e)})
73
 
74
+ # ==================== TOOL: E2B SANDBOX (MULTI-FILE) ====================
75
 
76
+ def execute_code_e2b(project_state):
77
+ """
78
+ Writes ALL files in the project to the sandbox, then runs main.py.
79
+ """
80
  if not E2B_AVAILABLE or not E2B_API_KEY:
81
  return "❌ Error: E2B API Key missing. Cannot execute code."
82
 
83
+ code_to_run = project_state.get("main.py", "")
84
+ if not code_to_run.strip():
85
+ return "⚠️ 'main.py' is empty. Nothing to run."
86
+
87
+ logger.info("⚡ Executing in E2B Sandbox (Multi-File)...")
88
  try:
 
89
  with Sandbox.create(allow_internet_access=False) as sandbox:
90
+ # 1. Write all auxiliary files to the sandbox filesystem
91
+ for filename, content in project_state.items():
92
+ if filename != "main.py":
93
+ sandbox.files.write(filename, content)
94
+
95
+ # 2. Run main.py
96
+ execution = sandbox.run_code(code_to_run)
97
 
98
  output_parts = []
99
  if execution.logs.stdout:
 
111
  except Exception as e:
112
  return f"❌ Sandbox Error: {str(e)}"
113
 
114
+ # ==================== CORE LOGIC ====================
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
+ def init_session():
117
+ return {"history": [], "start_time": time.time(), "active": True}
118
+
119
+ def get_system_prompt(mode):
120
+ base_prompt = """
121
+ You are Fyodor, an advanced AI-Native IDE engine.
122
+ You are running in a FULLY ISOLATED E2B Sandbox (No Internet).
123
+ You have access to a Multi-File Project environment.
124
+
125
+ FILESYSTEM RULES:
126
+ - The main entry point is always 'main.py'.
127
+ - You can create helper modules (e.g., 'utils.py', 'data_loader.py') and import them in 'main.py'.
128
+ - When providing code, specify the filename if strictly necessary, but default to 'main.py'.
129
+
130
+ COMMANDS:
131
+ - CODE: Use ```python ... ``` blocks.
132
+ - ANSWER: Plain text explanations.
133
+ """
134
+ if mode == "Data Mode":
135
+ return base_prompt + "\n\nMODE: DATA ANALYSIS\n- Focus on pandas, numpy, and statistical insights.\n- Prefer tabular outputs and data cleaning suggestions.\n- Assume data files might need generation or simulation."
136
+ elif mode == "Visualization Mode":
137
+ return base_prompt + "\n\nMODE: VISUALIZATION\n- Focus on matplotlib/seaborn.\n- Create visually striking plots.\n- Always ensure `plt.show()` is called.\n- Use dark backgrounds to match the IDE theme."
138
+ else: # Code Mode
139
+ return base_prompt + "\n\nMODE: GENERAL CODING\n- Focus on clean, modular, and efficient Python code.\n- Suggest refactoring where appropriate."
140
+
141
+ def parse_code_update(text, current_project, active_file):
 
 
 
 
 
 
142
  """
143
+ Parses LLM output for code blocks.
144
+ Updates the ACTIVE file with the last code block found.
145
  """
146
+ clean_text = clean_thought_process(text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  if "```python" in clean_text:
148
  pattern = r"```python\n(.*?)```"
149
  matches = re.findall(pattern, clean_text, re.DOTALL)
150
+ if matches:
151
+ new_code = matches[-1]
152
+ current_project[active_file] = new_code
153
+ return new_code, current_project
154
+ return None, current_project
155
+
156
+ def process_query(user_input, session_data, project_state, active_file, mode):
157
+ if session_data is None: session_data = init_session()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
+ # Auto-renewal check
160
+ if time.time() - session_data["start_time"] > SESSION_TIMEOUT_SEC:
 
 
 
 
 
161
  session_data = init_session()
 
 
 
 
 
 
 
162
 
163
+ history = session_data["history"]
164
  if not user_input:
165
+ yield history, project_state, project_state[active_file], session_data
166
  return
167
 
168
  history.append({"role": "user", "content": user_input})
169
+ history.append({"role": "assistant", "content": f"🧠 *Fyodor is thinking in {mode}...*"})
170
+ yield history, project_state, project_state[active_file], session_data
171
 
172
+ # Context Construction
173
+ sys_prompt = get_system_prompt(mode)
174
+ messages = [{"role": "system", "content": sys_prompt}]
175
 
176
+ # Inject Project Context
177
+ project_context = "CURRENT PROJECT FILES:\n"
178
+ for fname, content in project_state.items():
179
+ project_context += f"--- {fname} ---\n{content}\n"
180
+ messages.append({"role": "system", "content": project_context})
181
+
182
+ # Chat History
183
+ for msg in history[:-2][-8:]:
184
  if msg['role'] == 'user':
185
  messages.append({"role": "user", "content": msg['content']})
186
  elif msg['role'] == 'assistant':
 
192
  try:
193
  client = OpenAI(**API_CONFIG)
194
  response = client.chat.completions.create(
195
+ model=MODEL_ID, messages=messages, temperature=0.1, stream=True
 
 
 
 
196
  )
197
 
198
  full_response = ""
 
199
  for chunk in response:
200
+ if chunk.choices and chunk.choices[0].delta.content:
201
+ full_response += chunk.choices[0].delta.content
202
+ # Apply filter in real-time
203
+ clean_display = clean_thought_process(full_response)
 
204
  history[-1]['content'] = clean_display
205
+ yield history, project_state, project_state[active_file], session_data
206
 
207
+ # Parse and Update Code
208
+ new_code, updated_project = parse_code_update(full_response, project_state, active_file)
209
+
210
+ if new_code:
211
+ history[-1]['content'] += f"\n\n📝 *Updated {active_file}*"
212
+ yield history, updated_project, updated_project[active_file], session_data
213
 
214
+ # Execution (If Code Mode or Vis Mode, or if explicitly asked)
215
+ # For safety, we execute mainly on specific triggers, but here we run for feedback
216
+ history[-1]['content'] += "\n\n⏳ *Running Project...*"
217
+ exec_result = execute_code_e2b(updated_project)
218
+ history[-1]['content'] += f"\n\n---\n{exec_result}"
219
+ yield history, updated_project, updated_project[active_file], session_data
 
 
220
 
221
  except Exception as e:
222
  history[-1]['content'] += f"\n\n❌ Error: {str(e)}"
223
+ yield history, project_state, project_state[active_file], session_data
224
+
225
+ # ==================== NEW FEATURES LOGIC ====================
226
+
227
+ def run_code_review(code, session_data):
228
+ """Dedicated Code Review Agent with Thought Filtering"""
229
+ if not code.strip(): return session_data["history"]
230
+
231
+ history = session_data["history"]
232
+ history.append({"role": "user", "content": "🕵️ **Requesting Code Review**"})
233
+ history.append({"role": "assistant", "content": "🤔 *Reviewing...*"})
234
+
235
+ messages = [
236
+ {"role": "system", "content": "You are a Senior Python Architect. Review the following code for: Security, Efficiency, Style (PEP8), and Potential Bugs. Be critical but constructive."},
237
+ {"role": "user", "content": code}
238
+ ]
239
+
240
+ try:
241
+ client = OpenAI(**API_CONFIG)
242
+ response = client.chat.completions.create(model=MODEL_ID, messages=messages)
243
+ review = response.choices[0].message.content
244
+
245
+ # FIX: Apply the filter here!
246
+ clean_review = clean_thought_process(review)
247
+
248
+ history[-1]["content"] = f"### 🕵️ Code Review Report\n\n{clean_review}"
249
+ except Exception as e:
250
+ history[-1]["content"] = f"❌ Review Failed: {e}"
251
+
252
+ return history
253
+
254
+ def update_active_file(selected_file, project_data):
255
+ return project_data.get(selected_file, "")
256
+
257
+ def save_file_change(new_content, selected_file, project_data):
258
+ project_data[selected_file] = new_content
259
+ return project_data
260
+
261
+ def create_new_file(new_name, project_data):
262
+ if not new_name: return gr.update(choices=list(project_data.keys())), project_data, ""
263
+ if new_name in project_data: return gr.update(choices=list(project_data.keys())), project_data, "⚠️ File exists"
264
+
265
+ project_data[new_name] = "# New file"
266
+ return gr.update(choices=list(project_data.keys()), value=new_name), project_data, ""
267
 
268
  # ==================== UI ====================
269
 
270
+ # FULL WINDOWS TERMINAL CSS
271
  WINDOWS_TERMINAL_CSS = """
272
  /* Windows Terminal Theme - Campbell Dark */
273
  :root {
 
396
  background-color: #1a1a1a !important;
397
  border: 1px solid var(--wt-border) !important;
398
  border-radius: 6px !important;
399
+ padding: 1px !important;
400
  }
401
 
402
  .cm-scroller {
 
458
  letter-spacing: 0.5px;
459
  }
460
 
461
+ /* SESSION STATUS BAR */
462
  .timer-box {
463
  background: linear-gradient(135deg, #1a1a1a 0%, #0a0a0a 100%) !important;
464
  border: 1px solid var(--wt-border) !important;
465
  border-radius: 6px !important;
466
+ padding: 0px !important;
467
  box-shadow: inset 0 0 10px rgba(0,0,0,0.5);
468
  }
469
 
 
481
  box-shadow: none !important;
482
  }
483
 
484
+ /* Radio Button Styles */
485
+ .mode-radio { background: transparent !important; border: none !important; }
486
+ .mode-radio label { background: #1a1a1a !important; border: 1px solid #333 !important; margin-right: 10px; padding: 5px 15px; border-radius: 4px; cursor: pointer; color: var(--wt-fg); }
487
+ .mode-radio label.selected { background: var(--wt-cyan) !important; color: black !important; border-color: var(--wt-cyan) !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
488
 
489
+ /* Scrollbars */
490
+ ::-webkit-scrollbar { width: 12px; height: 12px; }
491
+ ::-webkit-scrollbar-track { background: var(--wt-bg); }
492
+ ::-webkit-scrollbar-thumb { background: var(--wt-bright-black); border-radius: 6px; border: 2px solid var(--wt-bg); }
493
+ ::-webkit-scrollbar-thumb:hover { background: var(--wt-cyan); }
 
 
494
  """
495
 
496
  def run_app():
497
+ with gr.Blocks(title="Fyodor IDE | Pro") as demo:
498
+ # STATE
499
+ session_state = gr.State(init_session())
500
+ project_state = gr.State({"main.py": "import numpy as np\nprint('Hello Fyodor Pro!')"})
501
+ active_file_state = gr.State("main.py")
502
+
503
+ # HEADER
504
+ with gr.Row():
505
+ gr.Markdown("# 🛡️ Fyodor IDE | Pro")
506
+ timer_display = gr.Textbox(value="⏱️ SESSION: Active", show_label=False, interactive=False, elem_classes="timer-box", container=True)
 
 
 
507
 
508
+ # MAIN LAYOUT
509
  with gr.Row(elem_classes="responsive-row"):
510
+ # LEFT: CHAT
511
  with gr.Column(scale=1):
512
+ mode_radio = gr.Radio(
513
+ choices=["Code Mode", "Data Mode", "Visualization Mode"],
514
+ value="Code Mode",
515
+ label="IDE Mode",
516
+ elem_classes="mode-radio"
 
 
 
 
 
 
 
 
 
 
 
517
  )
518
+ # Removed 'type="messages"' to fix Gradio TypeError
519
+ chatbot = gr.Chatbot(label="Fyodor Terminal", height=500, render_markdown=True, elem_classes="chatbot")
520
+ msg_input = gr.Textbox(placeholder="Instruction...", show_label=False)
521
+ run_btn = gr.Button(" EXECUTE", variant="primary")
522
+
523
+ # MIDDLE: EDITOR & TOOLS
 
 
 
 
 
524
  with gr.Column(scale=1):
525
+ # File Manager Panel
526
+ with gr.Row():
527
+ file_dropdown = gr.Dropdown(choices=["main.py"], value="main.py", label="Active File", scale=2)
528
+ new_file_txt = gr.Textbox(placeholder="new_file.py", show_label=False, scale=1)
529
+ new_file_btn = gr.Button("", scale=0, min_width=40)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530
 
531
+ code_editor = gr.Code(language="python", value="import numpy as np\nprint('Hello Fyodor Pro!')", lines=20, interactive=True)
532
+
533
+ with gr.Row():
534
+ save_btn = gr.Button("💾 Save", size="sm")
535
+ review_btn = gr.Button("🕵️ Review Code", size="sm")
536
+ run_manual_btn = gr.Button("▶️ Run Project", variant="secondary")
537
+
538
+ # RIGHT: MINI AGENT
539
+ with gr.Column(scale=0.5):
540
+ gr.Markdown("### 🤖 Mini Agent")
541
+ # Removed 'type="messages"' here too
542
+ mini_chat = gr.Chatbot(label="Web Search", height=400, elem_classes="mini-chat")
543
+ mini_input = gr.Textbox(placeholder="Search...", show_label=False)
544
+ mini_btn = gr.Button("🔍 Search")
545
+
546
+ # EVENT WIRING
 
 
 
 
 
 
 
 
 
 
547
 
548
+ # 1. Chat & Execution
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
549
  msg_input.submit(
550
  process_query,
551
+ [msg_input, session_state, project_state, active_file_state, mode_radio],
552
+ [chatbot, project_state, code_editor, session_state]
553
+ )
 
554
  run_btn.click(
555
  process_query,
556
+ [msg_input, session_state, project_state, active_file_state, mode_radio],
557
+ [chatbot, project_state, code_editor, session_state]
558
+ )
559
+
560
+ # 2. File Management
561
+ file_dropdown.change(
562
+ update_active_file,
563
+ [file_dropdown, project_state],
564
+ [code_editor]
565
+ ).then(
566
+ lambda f: f, [file_dropdown], [active_file_state]
567
  )
568
 
569
+ code_editor.change(
570
+ save_file_change,
571
+ [code_editor, active_file_state, project_state],
572
+ [project_state]
573
  )
574
 
575
+ new_file_btn.click(
576
+ create_new_file,
577
+ [new_file_txt, project_state],
578
+ [file_dropdown, project_state, new_file_txt]
579
+ )
580
+
581
+ # 3. Manual Run & Review
582
+ run_manual_btn.click(
583
+ lambda p: execute_code_e2b(p),
584
+ [project_state],
585
+ [chatbot]
586
+ )
587
 
588
+ review_btn.click(
589
+ run_code_review,
590
+ [code_editor, session_state],
591
+ [chatbot]
592
+ )
593
 
594
+ # 4. Mini Agent
595
+ mini_input.submit(
596
+ lambda m, h: h + [{"role":"user","content":m}, {"role":"assistant", "content":robust_search(m)}],
597
+ [mini_input, mini_chat],
598
+ [mini_chat]
599
+ )
600
+ mini_btn.click(
601
+ lambda m, h: h + [{"role":"user","content":m}, {"role":"assistant", "content":robust_search(m)}],
602
+ [mini_input, mini_chat],
603
+ [mini_chat]
604
  )
605
 
606
  return demo
607
 
608
  if __name__ == "__main__":
609
+ # Moved CSS to launch() to fix Gradio TypeError
610
+ run_app().queue().launch(server_name="0.0.0.0", server_port=7860, css=WINDOWS_TERMINAL_CSS)