Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import os | |
| import logging | |
| import re | |
| import sys | |
| import io | |
| import json | |
| import time | |
| import base64 | |
| import tempfile | |
| from datetime import datetime, timedelta | |
| from openai import OpenAI | |
| # Try importing E2B | |
| try: | |
| from e2b_code_interpreter import Sandbox | |
| E2B_AVAILABLE = True | |
| except ImportError: | |
| E2B_AVAILABLE = False | |
| # Import for Web Search | |
| try: | |
| from ddgs import DDGS | |
| SEARCH_AVAILABLE = True | |
| except ImportError: | |
| SEARCH_AVAILABLE = False | |
| # Setup logging | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger("FyodorIDE") | |
| # ==================== CONFIGURATION ==================== | |
| MINIMAX_API_KEY = os.getenv("MINIMAX_API_KEY") | |
| E2B_API_KEY = os.getenv("E2B_API_KEY") | |
| API_CONFIG = { | |
| "api_key": MINIMAX_API_KEY if MINIMAX_API_KEY else "dummy-key", | |
| "base_url": "https://api.minimax.io/v1", | |
| } | |
| MODEL_ID = "MiniMax-M2" | |
| SESSION_TIMEOUT_SEC = 15 * 60 | |
| # ==================== HELPER FUNCTIONS ==================== | |
| def clean_thought_process(text): | |
| """Removes the <think>...</think> blocks from the text to prevent leaks.""" | |
| return re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL).strip() | |
| def export_script(code): | |
| if not code.strip(): return None | |
| try: | |
| with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.py', prefix='fyodor_script_') as f: | |
| f.write(code) | |
| return f.name | |
| except Exception as e: | |
| return None | |
| def robust_search(query): | |
| if not SEARCH_AVAILABLE: return json.dumps({"error": "Missing duckduckgo-search"}) | |
| try: | |
| results = [] | |
| with DDGS() as ddgs: | |
| for idx, r in enumerate(list(ddgs.text(query, max_results=5)), 1): | |
| results.append({ | |
| "title": r.get('title'), | |
| "link": r.get('href'), | |
| "snippet": r.get('body') | |
| }) | |
| return json.dumps(results, indent=2) | |
| except Exception as e: | |
| return json.dumps({"error": str(e)}) | |
| # ==================== TOOL: E2B SANDBOX (MULTI-FILE) ==================== | |
| def execute_code_e2b(project_state): | |
| """ | |
| Writes ALL files in the project to the sandbox, then runs main.py. | |
| """ | |
| if not E2B_AVAILABLE or not E2B_API_KEY: | |
| return "❌ Error: E2B API Key missing. Cannot execute code." | |
| code_to_run = project_state.get("main.py", "") | |
| if not code_to_run.strip(): | |
| return "⚠️ 'main.py' is empty. Nothing to run." | |
| logger.info("⚡ Executing in E2B Sandbox (Multi-File)...") | |
| try: | |
| with Sandbox.create(allow_internet_access=False) as sandbox: | |
| # 1. Write all auxiliary files to the sandbox filesystem | |
| for filename, content in project_state.items(): | |
| if filename != "main.py": | |
| sandbox.files.write(filename, content) | |
| # 2. Run main.py | |
| execution = sandbox.run_code(code_to_run) | |
| output_parts = [] | |
| if execution.logs.stdout: | |
| output_parts.append(f"📄 **STDOUT**:\n```\n{execution.logs.stdout}\n```") | |
| if execution.logs.stderr: | |
| output_parts.append(f"⚠️ **STDERR**:\n```\n{execution.logs.stderr}\n```") | |
| for result in execution.results: | |
| if hasattr(result, 'png') and result.png: | |
| img_md = f"" | |
| output_parts.append(f"🖼️ **Plot**:\n{img_md}") | |
| return "\n\n".join(output_parts) if output_parts else "✅ Executed (No Output)" | |
| except Exception as e: | |
| return f"❌ Sandbox Error: {str(e)}" | |
| # ==================== CORE LOGIC ==================== | |
| def init_session(): | |
| return {"history": [], "start_time": time.time(), "active": True} | |
| def get_system_prompt(mode): | |
| base_prompt = """ | |
| You are Fyodor, an advanced AI-Native IDE engine. | |
| You are running in a FULLY ISOLATED E2B Sandbox (No Internet). | |
| You have access to a Multi-File Project environment. | |
| FILESYSTEM RULES: | |
| - The main entry point is always 'main.py'. | |
| - You can create helper modules (e.g., 'utils.py', 'data_loader.py') and import them in 'main.py'. | |
| - When providing code, specify the filename if strictly necessary, but default to 'main.py'. | |
| COMMANDS: | |
| - CODE: Use ```python ... ``` blocks. | |
| - ANSWER: Plain text explanations. | |
| """ | |
| if mode == "Data Mode": | |
| 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." | |
| elif mode == "Visualization Mode": | |
| 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." | |
| else: # Code Mode | |
| return base_prompt + "\n\nMODE: GENERAL CODING\n- Focus on clean, modular, and efficient Python code.\n- Suggest refactoring where appropriate." | |
| def parse_code_update(text, current_project, active_file): | |
| """ | |
| Parses LLM output for code blocks. | |
| Updates the ACTIVE file with the last code block found. | |
| """ | |
| clean_text = clean_thought_process(text) | |
| if "```python" in clean_text: | |
| pattern = r"```python\n(.*?)```" | |
| matches = re.findall(pattern, clean_text, re.DOTALL) | |
| if matches: | |
| new_code = matches[-1] | |
| current_project[active_file] = new_code | |
| return new_code, current_project | |
| return None, current_project | |
| def process_query(user_input, session_data, project_state, active_file, mode): | |
| if session_data is None: session_data = init_session() | |
| # Auto-renewal check | |
| if time.time() - session_data["start_time"] > SESSION_TIMEOUT_SEC: | |
| session_data = init_session() | |
| history = session_data["history"] | |
| if not user_input: | |
| yield history, project_state, project_state[active_file], session_data | |
| return | |
| history.append({"role": "user", "content": user_input}) | |
| history.append({"role": "assistant", "content": f"🧠 *Fyodor is thinking in {mode}...*"}) | |
| yield history, project_state, project_state[active_file], session_data | |
| # Context Construction | |
| sys_prompt = get_system_prompt(mode) | |
| messages = [{"role": "system", "content": sys_prompt}] | |
| # Inject Project Context | |
| project_context = "CURRENT PROJECT FILES:\n" | |
| for fname, content in project_state.items(): | |
| project_context += f"--- {fname} ---\n{content}\n" | |
| messages.append({"role": "system", "content": project_context}) | |
| # Chat History | |
| for msg in history[:-2][-8:]: | |
| if msg['role'] == 'user': | |
| messages.append({"role": "user", "content": msg['content']}) | |
| elif msg['role'] == 'assistant': | |
| clean_content = msg['content'].split("---")[0].strip() | |
| messages.append({"role": "assistant", "content": clean_content}) | |
| messages.append({"role": "user", "content": user_input}) | |
| try: | |
| client = OpenAI(**API_CONFIG) | |
| response = client.chat.completions.create( | |
| model=MODEL_ID, messages=messages, temperature=0.1, stream=True | |
| ) | |
| full_response = "" | |
| for chunk in response: | |
| if chunk.choices and chunk.choices[0].delta.content: | |
| full_response += chunk.choices[0].delta.content | |
| # Apply filter in real-time | |
| clean_display = clean_thought_process(full_response) | |
| history[-1]['content'] = clean_display | |
| yield history, project_state, project_state[active_file], session_data | |
| # Parse and Update Code | |
| new_code, updated_project = parse_code_update(full_response, project_state, active_file) | |
| if new_code: | |
| history[-1]['content'] += f"\n\n📝 *Updated {active_file}*" | |
| yield history, updated_project, updated_project[active_file], session_data | |
| # Execution (If Code Mode or Vis Mode, or if explicitly asked) | |
| # For safety, we execute mainly on specific triggers, but here we run for feedback | |
| history[-1]['content'] += "\n\n⏳ *Running Project...*" | |
| exec_result = execute_code_e2b(updated_project) | |
| history[-1]['content'] += f"\n\n---\n{exec_result}" | |
| yield history, updated_project, updated_project[active_file], session_data | |
| except Exception as e: | |
| history[-1]['content'] += f"\n\n❌ Error: {str(e)}" | |
| yield history, project_state, project_state[active_file], session_data | |
| # ==================== NEW FEATURES LOGIC ==================== | |
| def run_code_review(code, session_data): | |
| """Dedicated Code Review Agent with Thought Filtering""" | |
| if not code.strip(): return session_data["history"] | |
| history = session_data["history"] | |
| history.append({"role": "user", "content": "🕵️ **Requesting Code Review**"}) | |
| history.append({"role": "assistant", "content": "🤔 *Reviewing...*"}) | |
| messages = [ | |
| {"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."}, | |
| {"role": "user", "content": code} | |
| ] | |
| try: | |
| client = OpenAI(**API_CONFIG) | |
| response = client.chat.completions.create(model=MODEL_ID, messages=messages) | |
| review = response.choices[0].message.content | |
| # FIX: Apply the filter here! | |
| clean_review = clean_thought_process(review) | |
| history[-1]["content"] = f"### 🕵️ Code Review Report\n\n{clean_review}" | |
| except Exception as e: | |
| history[-1]["content"] = f"❌ Review Failed: {e}" | |
| return history | |
| def update_active_file(selected_file, project_data): | |
| return project_data.get(selected_file, "") | |
| def save_file_change(new_content, selected_file, project_data): | |
| project_data[selected_file] = new_content | |
| return project_data | |
| def create_new_file(new_name, project_data): | |
| if not new_name: return gr.update(choices=list(project_data.keys())), project_data, "" | |
| if new_name in project_data: return gr.update(choices=list(project_data.keys())), project_data, "⚠️ File exists" | |
| project_data[new_name] = "# New file" | |
| return gr.update(choices=list(project_data.keys()), value=new_name), project_data, "" | |
| # ==================== UI ==================== | |
| # FULL WINDOWS TERMINAL CSS | |
| WINDOWS_TERMINAL_CSS = """ | |
| /* Windows Terminal Theme - Campbell Dark */ | |
| :root { | |
| --wt-bg: #0C0C0C; | |
| --wt-fg: #CCCCCC; | |
| --wt-blue: #0037DA; | |
| --wt-cyan: #3A96DD; | |
| --wt-green: #13A10E; | |
| --wt-purple: #881798; | |
| --wt-red: #C50F1F; | |
| --wt-yellow: #C19C00; | |
| --wt-bright-black: #767676; | |
| --wt-bright-white: #F2F2F2; | |
| --wt-selection: #264F78; | |
| --wt-border: #333333; | |
| } | |
| body, .gradio-container { | |
| background-color: var(--wt-bg) !important; | |
| color: var(--wt-fg) !important; | |
| font-family: 'Cascadia Code', 'Consolas', 'Courier New', monospace !important; | |
| } | |
| .gradio-container > .main { | |
| background-color: var(--wt-bg) !important; | |
| border: 1px solid var(--wt-border); | |
| border-radius: 8px; | |
| padding: 20px; | |
| } | |
| h1 { | |
| color: var(--wt-cyan) !important; | |
| font-weight: 700 !important; | |
| text-shadow: 0 0 10px rgba(58, 150, 221, 0.3); | |
| margin-bottom: 5px !important; | |
| } | |
| .markdown { | |
| color: var(--wt-fg) !important; | |
| } | |
| .message-wrap { | |
| background-color: transparent !important; | |
| } | |
| .user.message { | |
| background: linear-gradient(135deg, var(--wt-blue) 0%, var(--wt-purple) 100%) !important; | |
| color: var(--wt-bright-white) !important; | |
| border: none !important; | |
| border-radius: 8px !important; | |
| padding: 12px 16px !important; | |
| box-shadow: 0 2px 8px rgba(0, 55, 218, 0.3) !important; | |
| } | |
| .bot.message { | |
| background-color: #1a1a1a !important; | |
| color: var(--wt-fg) !important; | |
| border: 1px solid var(--wt-border) !important; | |
| border-radius: 8px !important; | |
| padding: 12px 16px !important; | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important; | |
| } | |
| .bot.message code { | |
| background-color: var(--wt-bg) !important; | |
| color: var(--wt-green) !important; | |
| border: 1px solid var(--wt-border); | |
| padding: 2px 6px; | |
| border-radius: 3px; | |
| font-family: 'Cascadia Code', monospace !important; | |
| } | |
| .bot.message pre { | |
| background-color: var(--wt-bg) !important; | |
| border: 1px solid var(--wt-border) !important; | |
| border-radius: 6px !important; | |
| padding: 12px !important; | |
| overflow-x: auto; | |
| } | |
| input[type="text"], textarea, .input-text { | |
| background-color: #1a1a1a !important; | |
| color: var(--wt-fg) !important; | |
| border: 1px solid var(--wt-border) !important; | |
| border-radius: 4px !important; | |
| font-family: 'Cascadia Code', monospace !important; | |
| } | |
| input[type="text"]:focus, textarea:focus { | |
| border-color: var(--wt-cyan) !important; | |
| box-shadow: 0 0 0 2px rgba(58, 150, 221, 0.2) !important; | |
| } | |
| .primary-button, button.primary { | |
| background: linear-gradient(135deg, var(--wt-cyan) 0%, var(--wt-blue) 100%) !important; | |
| color: var(--wt-bright-white) !important; | |
| border: none !important; | |
| border-radius: 4px !important; | |
| font-weight: 600 !important; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| transition: all 0.3s ease !important; | |
| box-shadow: 0 2px 8px rgba(58, 150, 221, 0.3) !important; | |
| } | |
| .primary-button:hover, button.primary:hover { | |
| box-shadow: 0 4px 12px rgba(58, 150, 221, 0.5) !important; | |
| transform: translateY(-1px); | |
| } | |
| .secondary-button, button.secondary { | |
| background-color: #1a1a1a !important; | |
| color: var(--wt-cyan) !important; | |
| border: 1px solid var(--wt-cyan) !important; | |
| border-radius: 4px !important; | |
| font-weight: 600 !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| .secondary-button:hover, button.secondary:hover { | |
| background-color: var(--wt-cyan) !important; | |
| color: var(--wt-bg) !important; | |
| } | |
| .code-wrap, .cm-editor { | |
| background-color: #1a1a1a !important; | |
| border: 1px solid var(--wt-border) !important; | |
| border-radius: 6px !important; | |
| padding: 1px !important; | |
| } | |
| .cm-scroller { | |
| font-family: 'Cascadia Code', 'Consolas', monospace !important; | |
| font-size: 14px !important; | |
| line-height: 1.5 !important; | |
| } | |
| .cm-gutters { | |
| background-color: var(--wt-bg) !important; | |
| border-right: 1px solid var(--wt-border) !important; | |
| color: var(--wt-bright-black) !important; | |
| } | |
| .cm-activeLineGutter { | |
| background-color: #1a1a1a !important; | |
| } | |
| .cm-line { | |
| color: var(--wt-fg) !important; | |
| } | |
| .tab-nav { | |
| background-color: var(--wt-bg) !important; | |
| border-bottom: 1px solid var(--wt-border) !important; | |
| } | |
| .tab-nav button { | |
| background-color: transparent !important; | |
| color: var(--wt-fg) !important; | |
| border: none !important; | |
| border-bottom: 2px solid transparent !important; | |
| padding: 10px 20px !important; | |
| font-family: 'Cascadia Code', monospace !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| .tab-nav button:hover { | |
| color: var(--wt-cyan) !important; | |
| } | |
| .tab-nav button.selected { | |
| color: var(--wt-cyan) !important; | |
| border-bottom: 2px solid var(--wt-cyan) !important; | |
| } | |
| .panel, .block { | |
| background-color: #1a1a1a !important; | |
| border: 1px solid var(--wt-border) !important; | |
| border-radius: 6px !important; | |
| } | |
| label { | |
| color: var(--wt-cyan) !important; | |
| font-weight: 600 !important; | |
| font-family: 'Cascadia Code', monospace !important; | |
| text-transform: uppercase; | |
| font-size: 12px; | |
| letter-spacing: 0.5px; | |
| } | |
| /* SESSION STATUS BAR */ | |
| .timer-box { | |
| background: linear-gradient(135deg, #1a1a1a 0%, #0a0a0a 100%) !important; | |
| border: 1px solid var(--wt-border) !important; | |
| border-radius: 6px !important; | |
| padding: 0px !important; | |
| box-shadow: inset 0 0 10px rgba(0,0,0,0.5); | |
| } | |
| .timer-box textarea, .timer-box input { | |
| background-color: transparent !important; | |
| border: none !important; | |
| text-align: center !important; | |
| font-family: 'Cascadia Code', monospace !important; | |
| font-weight: 700 !important; | |
| font-size: 14px !important; | |
| color: var(--wt-green) !important; | |
| text-shadow: 0 0 8px rgba(19, 161, 14, 0.5); | |
| padding: 10px !important; | |
| height: auto !important; | |
| box-shadow: none !important; | |
| } | |
| /* Radio Button Styles */ | |
| .mode-radio { background: transparent !important; border: none !important; } | |
| .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); } | |
| .mode-radio label.selected { background: var(--wt-cyan) !important; color: black !important; border-color: var(--wt-cyan) !important; } | |
| /* Scrollbars */ | |
| ::-webkit-scrollbar { width: 12px; height: 12px; } | |
| ::-webkit-scrollbar-track { background: var(--wt-bg); } | |
| ::-webkit-scrollbar-thumb { background: var(--wt-bright-black); border-radius: 6px; border: 2px solid var(--wt-bg); } | |
| ::-webkit-scrollbar-thumb:hover { background: var(--wt-cyan); } | |
| """ | |
| def run_app(): | |
| with gr.Blocks(title="Fyodor IDE | Pro") as demo: | |
| # STATE | |
| session_state = gr.State(init_session()) | |
| project_state = gr.State({"main.py": "import numpy as np\nprint('Hello Fyodor Pro!')"}) | |
| active_file_state = gr.State("main.py") | |
| # HEADER | |
| with gr.Row(): | |
| gr.Markdown("# 🛡️ Fyodor IDE | Pro") | |
| timer_display = gr.Textbox(value="⏱️ SESSION: Active", show_label=False, interactive=False, elem_classes="timer-box", container=True) | |
| # MAIN LAYOUT | |
| with gr.Row(elem_classes="responsive-row"): | |
| # LEFT: CHAT | |
| with gr.Column(scale=1): | |
| mode_radio = gr.Radio( | |
| choices=["Code Mode", "Data Mode", "Visualization Mode"], | |
| value="Code Mode", | |
| label="IDE Mode", | |
| elem_classes="mode-radio" | |
| ) | |
| # Removed 'type="messages"' to fix Gradio TypeError | |
| chatbot = gr.Chatbot(label="Fyodor Terminal", height=500, render_markdown=True, elem_classes="chatbot") | |
| msg_input = gr.Textbox(placeholder="Instruction...", show_label=False) | |
| run_btn = gr.Button("⚡ EXECUTE", variant="primary") | |
| # MIDDLE: EDITOR & TOOLS | |
| with gr.Column(scale=1): | |
| # File Manager Panel | |
| with gr.Row(): | |
| file_dropdown = gr.Dropdown(choices=["main.py"], value="main.py", label="Active File", scale=2) | |
| new_file_txt = gr.Textbox(placeholder="new_file.py", show_label=False, scale=1) | |
| new_file_btn = gr.Button("➕", scale=0, min_width=40) | |
| code_editor = gr.Code(language="python", value="import numpy as np\nprint('Hello Fyodor Pro!')", lines=20, interactive=True) | |
| with gr.Row(): | |
| save_btn = gr.Button("💾 Save", size="sm") | |
| review_btn = gr.Button("🕵️ Review Code", size="sm") | |
| run_manual_btn = gr.Button("▶️ Run Project", variant="secondary") | |
| # RIGHT: MINI AGENT | |
| with gr.Column(scale=0.5): | |
| gr.Markdown("### 🤖 Mini Agent") | |
| # Removed 'type="messages"' here too | |
| mini_chat = gr.Chatbot(label="Web Search", height=400, elem_classes="mini-chat") | |
| mini_input = gr.Textbox(placeholder="Search...", show_label=False) | |
| mini_btn = gr.Button("🔍 Search") | |
| # EVENT WIRING | |
| # 1. Chat & Execution | |
| msg_input.submit( | |
| process_query, | |
| [msg_input, session_state, project_state, active_file_state, mode_radio], | |
| [chatbot, project_state, code_editor, session_state] | |
| ) | |
| run_btn.click( | |
| process_query, | |
| [msg_input, session_state, project_state, active_file_state, mode_radio], | |
| [chatbot, project_state, code_editor, session_state] | |
| ) | |
| # 2. File Management | |
| file_dropdown.change( | |
| update_active_file, | |
| [file_dropdown, project_state], | |
| [code_editor] | |
| ).then( | |
| lambda f: f, [file_dropdown], [active_file_state] | |
| ) | |
| code_editor.change( | |
| save_file_change, | |
| [code_editor, active_file_state, project_state], | |
| [project_state] | |
| ) | |
| new_file_btn.click( | |
| create_new_file, | |
| [new_file_txt, project_state], | |
| [file_dropdown, project_state, new_file_txt] | |
| ) | |
| # 3. Manual Run & Review | |
| run_manual_btn.click( | |
| lambda p: execute_code_e2b(p), | |
| [project_state], | |
| [chatbot] | |
| ) | |
| review_btn.click( | |
| run_code_review, | |
| [code_editor, session_state], | |
| [chatbot] | |
| ) | |
| # 4. Mini Agent | |
| mini_input.submit( | |
| lambda m, h: h + [{"role":"user","content":m}, {"role":"assistant", "content":robust_search(m)}], | |
| [mini_input, mini_chat], | |
| [mini_chat] | |
| ) | |
| mini_btn.click( | |
| lambda m, h: h + [{"role":"user","content":m}, {"role":"assistant", "content":robust_search(m)}], | |
| [mini_input, mini_chat], | |
| [mini_chat] | |
| ) | |
| return demo | |
| if __name__ == "__main__": | |
| # Moved CSS to launch() to fix Gradio TypeError | |
| run_app().queue().launch(server_name="0.0.0.0", server_port=7860, css=WINDOWS_TERMINAL_CSS) |