1 line
75 KiB
JSON
1 line
75 KiB
JSON
{"repo": "adithya-s-k/eurekaagent", "files": {"app.py": {"9fb95d3a68beaeae8af1e8c6083edd257551957a": {"t": 1785674392.5770576, "n": 73434, "h": "5836eb90663a20da", "c": "import os\nimport logging\nimport gradio as gr\nfrom gradio.utils import get_space\nfrom modal_sandbox import create_modal_sandbox\nfrom pathlib import Path\nimport json\nfrom datetime import datetime\nimport threading\nimport re\nfrom openai import OpenAI, AzureOpenAI\nfrom jupyter_handler import JupyterNotebook\n\nif not get_space():\n try:\n from dotenv import load_dotenv\n\n load_dotenv()\n except (ImportError, ModuleNotFoundError):\n pass\nfrom jupyter_agent import (\n run_interactive_notebook_with_session_state,\n SessionStateManager,\n)\n\nTMP_DIR = './temp/'\n\n# Environment and API key management utilities\ndef get_environment():\n \"\"\"Get the current environment (dev/prod)\"\"\"\n return os.environ.get(\"ENVIRONMENT\", \"prod\").lower()\n\ndef is_dev_environment():\n \"\"\"Check if running in development environment\"\"\"\n return get_environment() == \"dev\"\n\ndef get_required_api_keys():\n \"\"\"Get dictionary of required API keys and their current status\"\"\"\n required_keys = {\n \"MODAL_TOKEN_ID\": {\n \"value\": os.environ.get(\"MODAL_TOKEN_ID\"),\n \"required\": True,\n \"description\": \"Modal Token ID for sandbox access\"\n },\n \"MODAL_TOKEN_SECRET\": {\n \"value\": os.environ.get(\"MODAL_TOKEN_SECRET\"),\n \"required\": True,\n \"description\": \"Modal Token Secret for sandbox access\"\n },\n \"HF_TOKEN\": {\n \"value\": os.environ.get(\"HF_TOKEN\"),\n \"required\": False,\n \"description\": \"Hugging Face Token for model access\"\n },\n \"PROVIDER_API_KEY\": {\n \"value\": os.environ.get(\"PROVIDER_API_KEY\"),\n \"required\": True,\n \"description\": \"AI Provider API Key (Anthropic, OpenAI, etc.)\"\n },\n \"PROVIDER_API_ENDPOINT\": {\n \"value\": os.environ.get(\"PROVIDER_API_ENDPOINT\"),\n \"required\": True,\n \"description\": \"AI Provider API Endpoint\"\n },\n \"MODEL_NAME\": {\n \"value\": os.environ.get(\"MODEL_NAME\"),\n \"required\": True,\n \"description\": \"Model name to use\"\n },\n \"TAVILY_API_KEY\": {\n \"value\": os.environ.get(\"TAVILY_API_KEY\"),\n \"required\": False,\n \"description\": \"Tavily API Key for web search functionality\"\n }\n }\n return required_keys\n\ndef get_missing_api_keys():\n \"\"\"Get list of missing required API keys\"\"\"\n required_keys = get_required_api_keys()\n missing_keys = {}\n \n for key, config in required_keys.items():\n if config[\"required\"] and not config[\"value\"]:\n missing_keys[key] = config\n \n return missing_keys\n\ndef validate_api_key_format(key_name, key_value):\n \"\"\"Basic validation for API key formats\"\"\"\n if not key_value or not key_value.strip():\n return False, \"API key cannot be empty\"\n \n key_value = key_value.strip()\n \n # Basic format validation\n if key_name == \"MODAL_TOKEN_ID\" and not key_value.startswith(\"ak-\"):\n return False, \"Modal Token ID should start with 'ak-'\"\n elif key_name == \"MODAL_TOKEN_SECRET\" and not key_value.startswith(\"as-\"):\n return False, \"Modal Token Secret should start with 'as-'\"\n elif key_name == \"HF_TOKEN\" and not key_value.startswith(\"hf_\"):\n return False, \"Hugging Face token should start with 'hf_'\"\n elif key_name == \"PROVIDER_API_KEY\":\n # Check for common API key prefixes\n valid_prefixes = [\"sk-\", \"gsk_\", \"csk-\"]\n if not any(key_value.startswith(prefix) for prefix in valid_prefixes):\n return False, \"API key format may be invalid (expected prefixes: sk-, gsk_, csk-)\"\n elif key_name == \"PROVIDER_API_ENDPOINT\" and not (key_value.startswith(\"http://\") or key_value.startswith(\"https://\")):\n return False, \"API endpoint should start with http:// or https://\"\n elif key_name == \"TAVILY_API_KEY\" and not key_value.startswith(\"tvly-\"):\n return False, \"Tavily API key should start with 'tvly-'\"\n \n return True, \"Valid format\"\n\ndef apply_user_api_keys(api_keys_dict):\n \"\"\"Apply user-provided API keys to environment\"\"\"\n for key, value in api_keys_dict.items():\n if value and value.strip():\n os.environ[key] = value.strip()\n logger.info(f\"Applied user-provided API key: {key}\")\n\ndef get_previous_notebooks():\n \"\"\"Get list of previous notebook sessions (dev only)\"\"\"\n if not is_dev_environment():\n return []\n \n notebooks = []\n tmp_dir = Path(TMP_DIR)\n \n if not tmp_dir.exists():\n return notebooks\n \n for session_dir in tmp_dir.iterdir():\n if session_dir.is_dir() and session_dir.name != \".\":\n notebook_file = session_dir / \"jupyter-agent.ipynb\"\n if notebook_file.exists():\n try:\n # Get creation time and basic info\n stat = notebook_file.stat()\n size = stat.st_size\n modified = stat.st_mtime\n \n # Try to read basic notebook info\n with open(notebook_file, 'r') as f:\n notebook_data = json.load(f)\n cell_count = len(notebook_data.get('cells', []))\n \n # Format timestamp\n formatted_time = datetime.fromtimestamp(modified).strftime(\"%Y-%m-%d %H:%M\")\n \n # Try to load session state for additional info\n config_info = \"\"\n try:\n session_manager = SessionStateManager(session_dir.name, TMP_DIR)\n session_state = session_manager.load_state()\n if session_state:\n hardware = session_state.get(\"hardware_config\", {})\n gpu = hardware.get(\"gpu_type\", \"unknown\")\n config_info = f\", {gpu}\"\n except Exception:\n pass\n \n notebooks.append({\n 'session_id': session_dir.name,\n 'path': str(notebook_file),\n 'modified': modified,\n 'size': size,\n 'cell_count': cell_count,\n 'display_name': f\"{session_dir.name} ({cell_count} cells{config_info}, {formatted_time})\"\n })\n except Exception as e:\n logger.warning(f\"Failed to read notebook info for {session_dir.name}: {e}\")\n \n # Sort by modification time (newest first)\n notebooks.sort(key=lambda x: x['modified'], reverse=True)\n return notebooks\n\ndef parse_environment_variables(env_vars_text):\n \"\"\"\n Parse environment variables from text input\n \n Args:\n env_vars_text: String containing environment variables in KEY=value format, one per line\n \n Returns:\n dict: Dictionary of parsed environment variables\n \"\"\"\n env_dict = {}\n if not env_vars_text or not env_vars_text.strip():\n return env_dict\n \n for line in env_vars_text.strip().split('\\n'):\n line = line.strip()\n if not line or line.startswith('#'): # Skip empty lines and comments\n continue\n \n if '=' in line:\n key, value = line.split('=', 1) # Split only on first =\n key = key.strip()\n value = value.strip()\n if key: # Only add if key is not empty\n env_dict[key] = value\n else:\n logger.warning(f\"Skipping invalid environment variable format: {line}\")\n \n return env_dict\n\ndef create_notification_html(message, notification_type=\"info\", show_spinner=False):\n \"\"\"\n Create HTML for notification messages\n \n Args:\n message: The notification message\n notification_type: Type of notification ('info', 'success', 'warning', 'error')\n show_spinner: Whether to show a loading spinner\n \"\"\"\n colors = {\n 'info': '#3498db',\n 'success': '#27ae60', \n 'warning': '#f39c12',\n 'error': '#e74c3c',\n 'loading': '#6c5ce7'\n }\n \n icons = {\n 'info': '🔄',\n 'success': '✅',\n 'warning': '⚠️', \n 'error': '❌',\n 'loading': '⏳'\n }\n \n color = colors.get(notification_type, colors['info'])\n icon = icons.get(notification_type, icons['info'])\n \n spinner_html = \"\"\n if show_spinner or notification_type == 'loading':\n spinner_html = \"\"\"\n <div style=\"\n display: inline-block;\n width: 20px;\n height: 20px;\n border: 2px solid #f3f3f3;\n border-top: 2px solid {color};\n border-radius: 50%;\n animation: spin 1s linear infinite;\n margin-right: 8px;\n \"></div>\n <style>\n @keyframes spin {{\n 0% {{ transform: rotate(0deg); }}\n 100% {{ transform: rotate(360deg); }}\n }}\n </style>\n \"\"\".format(color=color)\n \n return f\"\"\"\n <div style=\"\n background-color: {color}20;\n border-left: 4px solid {color};\n padding: 12px 16px;\n margin: 10px 0;\n border-radius: 4px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #2c3e50;\n display: flex;\n align-items: center;\n \">\n {spinner_html}\n <strong>{icon} {message}</strong>\n </div>\n \"\"\"\n\ndef create_progress_notification(message, progress_percent=None):\n \"\"\"Create a progress notification with optional progress bar\"\"\"\n progress_html = \"\"\n if progress_percent is not None:\n progress_html = f\"\"\"\n <div style=\"\n width: 100%;\n background-color: #e0e0e0;\n border-radius: 5px;\n margin-top: 8px;\n height: 8px;\n \">\n <div style=\"\n width: {progress_percent}%;\n background-color: #3498db;\n height: 8px;\n border-radius: 5px;\n transition: width 0.3s ease;\n \"></div>\n </div>\n <small style=\"color: #666; margin-top: 4px; display: block;\">{progress_percent}% complete</small>\n \"\"\"\n \n return create_notification_html(message, \"loading\", show_spinner=True) + progress_html\n\n\ndef initialize_phoenix_tracing():\n \"\"\"Initialize Phoenix tracing with proper error handling and session support\"\"\"\n try:\n from phoenix.otel import register\n \n phoenix_api_key = os.getenv(\"PHOENIX_API_KEY\")\n collector_endpoint = os.getenv(\"PHOENIX_COLLECTOR_ENDPOINT\")\n \n if not phoenix_api_key:\n logger.info(\"Phoenix API key not found, skipping Phoenix tracing initialization\")\n return None\n \n if not collector_endpoint:\n logger.info(\"Phoenix collector endpoint not found, skipping Phoenix tracing initialization\")\n return None\n \n logger.info(\"Initializing Phoenix tracing with session support...\")\n \n # Set required environment variables\n os.environ[\"PHOENIX_API_KEY\"] = phoenix_api_key\n os.environ[\"PHOENIX_COLLECTOR_ENDPOINT\"] = collector_endpoint\n os.environ[\"OTEL_EXPORTER_OTLP_HEADERS\"] = f\"api_key={phoenix_api_key}\"\n os.environ[\"PHOENIX_CLIENT_HEADERS\"] = f\"api_key={phoenix_api_key}\"\n\n # Configure the Phoenix tracer with OpenAI instrumentation enabled\n tracer_provider = register(\n project_name=\"eureka-agent\",\n auto_instrument=True, # Keep auto-instrument enabled for OpenAI tracing\n set_global_tracer_provider=True\n )\n \n # Additional instrumentation setup for session tracking\n try:\n from openinference.instrumentation.openai import OpenAIInstrumentor\n \n # Ensure OpenAI instrumentation is properly configured\n if not OpenAIInstrumentor().is_instrumented_by_opentelemetry:\n OpenAIInstrumentor().instrument()\n logger.info(\"OpenAI instrumentation configured for Phoenix session tracking\")\n else:\n logger.info(\"OpenAI instrumentation already active\")\n \n except ImportError:\n logger.warning(\"OpenAI instrumentation not available - session grouping may not work optimally\")\n except Exception as e:\n logger.warning(f\"Failed to configure OpenAI instrumentation: {str(e)}\")\n \n logger.info(\"Phoenix tracing initialized successfully with session support\")\n return tracer_provider\n \n except ImportError:\n logger.info(\"Phoenix not installed, skipping tracing initialization\")\n return None\n except Exception as e:\n logger.warning(f\"Failed to initialize Phoenix tracer (non-critical): {str(e)}\")\n return None\n\n\n\n# Configure logging\nlogging.basicConfig(\n level=logging.INFO,\n format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',\n handlers=[\n logging.FileHandler('jupyter_agent.log'),\n logging.StreamHandler()\n ]\n)\nlogger = logging.getLogger(__name__)\n\n# Initialize Phoenix tracing\ntracer_provider = initialize_phoenix_tracing()\n\n\nMODAL_TOKEN_ID = os.environ.get(\"MODAL_TOKEN_ID\")\nMODAL_TOKEN_SECRET = os.environ.get(\"MODAL_TOKEN_SECRET\")\nHF_TOKEN = os.environ.get(\"HF_TOKEN\")\nSANDBOXES = {}\nSANDBOX_TIMEOUT = 300\nSTOP_EVENTS = {} # Store stop events for each session\nEXECUTION_STATES = {} # Store execution states for each session\n\n# GPU configuration options for the UI\nGPU_OPTIONS = [\n (\"CPU Only\", \"cpu\"),\n (\"NVIDIA T4 (16GB)\", \"T4\"),\n (\"NVIDIA L4 (24GB)\", \"L4\"), \n (\"NVIDIA A100 40GB\", \"A100-40GB\"),\n (\"NVIDIA A100 80GB\", \"A100-80GB\"),\n (\"NVIDIA H100 (80GB)\", \"H100\")\n]\n\n\ndef initialize_openai_client():\n \"\"\"Initialize OpenAI client with proper error handling and fallbacks\"\"\"\n client = None\n model_name = None\n \n # Check if we have any API keys configured\n has_azure = os.environ.get(\"AZURE_OPENAI_ENDPOINT\") and os.environ.get(\"AZURE_OPENAI_API_KEY\")\n has_provider = os.environ.get(\"PROVIDER_API_ENDPOINT\") and os.environ.get(\"PROVIDER_API_KEY\") \n has_openai = os.environ.get(\"OPENAI_API_KEY\")\n \n if not (has_azure or has_provider or has_openai):\n logger.warning(\"No API keys found in environment - client will be initialized later when user provides keys\")\n return None, None\n \n try:\n # Option 1: Azure OpenAI\n if has_azure:\n logger.info(\"Initializing Azure OpenAI client\")\n client = AzureOpenAI(\n api_version=\"2024-12-01-preview\",\n azure_endpoint=os.environ.get(\"AZURE_OPENAI_ENDPOINT\"),\n api_key=os.environ.get(\"AZURE_OPENAI_API_KEY\")\n )\n model_name = os.environ.get(\"MODEL_NAME\", \"gpt-4\") # Default fallback\n logger.info(f\"Azure OpenAI client initialized with model: {model_name}\")\n \n # Option 2: Custom Provider (Cerebras, etc.) \n elif has_provider:\n logger.info(\"Initializing custom provider OpenAI client\")\n client = OpenAI(\n base_url=os.environ.get(\"PROVIDER_API_ENDPOINT\"),\n api_key=os.environ.get(\"PROVIDER_API_KEY\")\n )\n model_name = os.environ.get(\"MODEL_NAME\", \"gpt-4\") # Default fallback\n logger.info(f\"Custom provider client initialized with model: {model_name}\")\n \n # Option 3: Standard OpenAI\n elif has_openai:\n logger.info(\"Initializing standard OpenAI client\")\n client = OpenAI(\n api_key=os.environ.get(\"OPENAI_API_KEY\")\n )\n model_name = os.environ.get(\"MODEL_NAME\", \"gpt-4\") # Default fallback\n logger.info(f\"OpenAI client initialized with model: {model_name}\")\n \n # Test the client with a simple request (optional - skip if client initialization should be fast)\n if client:\n logger.info(\"Testing client connection...\")\n try:\n # Simple test to verify the client works\n _ = client.chat.completions.create(\n model=model_name,\n messages=[{\"role\": \"user\", \"content\": \"Hello\"}],\n max_tokens=5\n )\n logger.info(\"Client connection test successful\")\n except Exception as test_error:\n logger.error(f\"Client connection test failed: {str(test_error)}\")\n # Don't raise here, let the main application handle it\n \n return client, model_name\n \n except Exception as e:\n logger.error(f\"Failed to initialize OpenAI client: {str(e)}\")\n logger.warning(\"Client will be initialized later when user provides valid API keys\")\n return None, None\n\nclient, model_name = initialize_openai_client()\n\n# If no client was initialized, it means no API keys are available\nif client is None:\n logger.info(\"No OpenAI client initialized - waiting for user to provide API keys through UI\")\n\n\n\ninit_notebook = JupyterNotebook()\n\nif not os.path.exists(TMP_DIR):\n os.makedirs(TMP_DIR)\n logger.info(f\"Created temporary directory: {TMP_DIR}\")\nelse:\n logger.info(f\"Using existing temporary directory: {TMP_DIR}\")\n\nwith open(TMP_DIR+\"jupyter-agent.ipynb\", 'w', encoding='utf-8') as f:\n json.dump(JupyterNotebook().data, f, indent=2)\nlogger.info(f\"Initialized default notebook file: {TMP_DIR}jupyter-agent.ipynb\")\n\ntry:\n with open(\"system_prompt.txt\", \"r\") as f:\n DEFAULT_SYSTEM_PROMPT = f.read()\n logger.info(\"Loaded system prompt from ds-system-prompt.txt\")\nexcept FileNotFoundError:\n logger.warning(\"ds-system-prompt.txt not found, using fallback system prompt\")\n\n\ndef execute_jupyter_agent(\n user_input, files, message_history, gpu_type, cpu_cores, memory_gb, timeout_sec, env_vars_text, \n modal_token_id, modal_token_secret, hf_token, provider_api_key, provider_api_endpoint, user_model_name,\n tavily_api_key, enable_web_search, request: gr.Request\n):\n session_id = request.session_hash\n logger.info(f\"Starting execution for session {session_id}\")\n logger.info(f\"Hardware config: GPU={gpu_type}, CPU={cpu_cores}, Memory={memory_gb}GB, Timeout={timeout_sec}s\")\n logger.info(f\"User input length: {len(user_input)} characters\")\n \n # Check if execution is already running for this session\n if session_id in EXECUTION_STATES and EXECUTION_STATES[session_id].get(\"running\", False):\n error_message = \"❌ Execution already in progress for this session. Please wait for it to complete or stop it first.\"\n error_notification = create_notification_html(error_message, \"warning\")\n \n # Return current state without starting new execution\n session_dir = os.path.join(TMP_DIR, session_id)\n save_dir = os.path.join(session_dir, 'jupyter-agent.ipynb')\n if os.path.exists(save_dir):\n yield error_notification, message_history, save_dir\n else:\n yield error_notification, message_history, TMP_DIR + \"jupyter-agent.ipynb\"\n return\n \n # Initialize session state manager\n session_manager = SessionStateManager(session_id, TMP_DIR)\n \n # Check if this is a continuing session\n existing_session_state = session_manager.load_state()\n is_continuing_session = existing_session_state is not None\n \n if is_continuing_session:\n logger.info(f\"Found existing session state for {session_id} - continuing from previous state\")\n else:\n logger.info(f\"No existing session state found for {session_id} - starting new session\")\n \n # Apply user-provided API keys if any are provided\n user_api_keys = {}\n if modal_token_id:\n user_api_keys[\"MODAL_TOKEN_ID\"] = modal_token_id\n if modal_token_secret:\n user_api_keys[\"MODAL_TOKEN_SECRET\"] = modal_token_secret\n if hf_token:\n user_api_keys[\"HF_TOKEN\"] = hf_token\n if provider_api_key:\n user_api_keys[\"PROVIDER_API_KEY\"] = provider_api_key\n if provider_api_endpoint:\n user_api_keys[\"PROVIDER_API_ENDPOINT\"] = provider_api_endpoint\n if user_model_name:\n user_api_keys[\"MODEL_NAME\"] = user_model_name\n if tavily_api_key:\n user_api_keys[\"TAVILY_API_KEY\"] = tavily_api_key\n \n # Check if we have a client or need to initialize one with user keys\n global client, model_name\n if client is None and not user_api_keys:\n missing_keys = get_missing_api_keys()\n if missing_keys:\n error_message = f\"\"\"❌ Missing Required API Keys\n\nPlease provide the following API keys to continue:\n{chr(10).join([f\"• {key}: {config['description']}\" for key, config in missing_keys.items()])}\n\nYou can either:\n1. Add them to your .env file, or \n2. Enter them in the API Keys section above\"\"\"\n error_notification = create_notification_html(error_message, \"error\")\n yield error_notification, message_history, TMP_DIR + \"jupyter-agent.ipynb\"\n return\n \n # Validate user-provided API keys\n if user_api_keys:\n validation_message = \"🔍 Validating API keys...\"\n validation_notification = create_progress_notification(validation_message)\n yield validation_notification, message_history, TMP_DIR + \"jupyter-agent.ipynb\"\n \n validation_errors = []\n for key, value in user_api_keys.items():\n is_valid, message = validate_api_key_format(key, value)\n if not is_valid:\n validation_errors.append(f\"{key}: {message}\")\n \n if validation_errors:\n error_message = f\"❌ API Key Validation Failed:\\n\" + \"\\n\".join(f\"• {error}\" for error in validation_errors)\n error_notification = create_notification_html(error_message, \"error\")\n yield error_notification, message_history, TMP_DIR + \"jupyter-agent.ipynb\"\n return\n \n logger.info(f\"Applying user-provided API keys: {list(user_api_keys.keys())}\")\n apply_user_api_keys(user_api_keys)\n \n # Reinitialize OpenAI client with new keys if provider keys were updated\n if any(key in user_api_keys for key in [\"PROVIDER_API_KEY\", \"PROVIDER_API_ENDPOINT\", \"MODEL_NAME\"]):\n try:\n reinit_message = \"🔄 Reinitializing AI client with new credentials...\"\n reinit_notification = create_progress_notification(reinit_message)\n yield reinit_notification, message_history, TMP_DIR + \"jupyter-agent.ipynb\"\n \n client, model_name = initialize_openai_client()\n if client is None:\n error_message = \"Failed to initialize client with provided API keys. Please check your credentials.\"\n logger.error(error_message)\n error_notification = create_notification_html(error_message, \"error\")\n yield error_notification, message_history, TMP_DIR + \"jupyter-agent.ipynb\"\n return\n logger.info(\"Reinitialized OpenAI client with user-provided keys\")\n \n success_message = \"✅ API credentials validated and applied successfully!\"\n success_notification = create_notification_html(success_message, \"success\")\n yield success_notification, message_history, TMP_DIR + \"jupyter-agent.ipynb\"\n except Exception as e:\n error_message = f\"Failed to initialize client with provided API keys: {str(e)}\"\n logger.error(error_message)\n error_notification = create_notification_html(error_message, \"error\")\n yield error_notification, message_history, TMP_DIR + \"jupyter-agent.ipynb\"\n return\n \n # Initialize or reset stop event for this session\n STOP_EVENTS[session_id] = threading.Event()\n EXECUTION_STATES[session_id] = {\"running\": True, \"paused\": False, \"current_phase\": \"initializing\"}\n\n # Set up save directory early for notifications\n session_dir = os.path.join(TMP_DIR, request.session_hash)\n os.makedirs(session_dir, exist_ok=True)\n save_dir = os.path.join(session_dir, 'jupyter-agent.ipynb')\n \n # Create initial notebook file so it exists for Gradio\n with open(save_dir, 'w', encoding='utf-8') as f:\n json.dump(init_notebook.data, f, indent=2)\n logger.info(f\"Initialized notebook for session {session_id}\")\n \n # Session configuration is now handled by SessionStateManager\n\n if request.session_hash not in SANDBOXES:\n logger.info(f\"Creating new Modal sandbox for session {session_id}\")\n \n # Show initialization notification with spinner\n gpu_info = gpu_type.upper() if gpu_type != \"cpu\" else \"CPU Only\"\n if gpu_type in [\"T4\", \"L4\", \"A100-40GB\", \"A100-80GB\", \"H100\"]:\n gpu_info = f\"NVIDIA {gpu_type}\"\n \n init_message = f\"Initializing {gpu_info} sandbox with {cpu_cores} CPU cores and {memory_gb}GB RAM...\"\n notification_html = create_progress_notification(init_message)\n yield notification_html, message_history, save_dir\n \n # Create Modal sandbox with user-specified configuration\n environment_vars = {}\n if MODAL_TOKEN_ID and MODAL_TOKEN_SECRET:\n environment_vars.update({\n \"MODAL_TOKEN_ID\": MODAL_TOKEN_ID,\n \"MODAL_TOKEN_SECRET\": MODAL_TOKEN_SECRET\n })\n logger.debug(f\"Modal credentials configured for session {session_id}\")\n \n # Parse and add user-provided environment variables\n user_env_vars = parse_environment_variables(env_vars_text)\n if user_env_vars:\n environment_vars.update(user_env_vars)\n logger.info(f\"Added {len(user_env_vars)} custom environment variables for session {session_id}\")\n logger.debug(f\"Custom environment variables: {list(user_env_vars.keys())}\")\n \n try:\n SANDBOXES[request.session_hash] = create_modal_sandbox(\n gpu_config=gpu_type,\n cpu_cores=cpu_cores,\n memory_gb=memory_gb,\n timeout=int(timeout_sec),\n environment_vars=environment_vars\n )\n logger.info(f\"Successfully created Modal sandbox for session {session_id}\")\n \n # Show success notification\n success_message = f\"✨ {gpu_info} sandbox ready! Environment initialized with all packages.\"\n success_notification_html = create_notification_html(success_message, \"success\")\n yield success_notification_html, message_history, save_dir\n \n except Exception as e:\n logger.error(f\"Failed to create Modal sandbox for session {session_id}: {str(e)}\")\n # Show error notification\n error_message = f\"Failed to initialize sandbox: {str(e)}\"\n error_notification_html = create_notification_html(error_message, \"error\")\n yield error_notification_html, message_history, save_dir\n raise\n else:\n logger.info(f\"Reusing existing Modal sandbox for session {session_id}\")\n # Show reuse notification\n gpu_info = gpu_type.upper() if gpu_type != \"cpu\" else \"CPU Only\"\n if gpu_type in [\"T4\", \"L4\", \"A100-40GB\", \"A100-80GB\", \"H100\"]:\n gpu_info = f\"NVIDIA {gpu_type}\"\n reuse_message = f\"Using existing {gpu_info} sandbox - ready to execute!\"\n reuse_notification_html = create_notification_html(reuse_message, \"success\")\n yield reuse_notification_html, message_history, save_dir\n \n sbx = SANDBOXES[request.session_hash]\n logger.debug(f\"Notebook will be saved to: {save_dir}\")\n \n # Initial notebook render\n yield init_notebook.render(), message_history, save_dir\n\n\n\n filenames = []\n if files is not None:\n logger.info(f\"Processing {len(files)} uploaded files for session {session_id}\")\n for filepath in files:\n filpath = Path(filepath)\n try:\n # Get file size for verification\n file_size = os.path.getsize(filepath)\n \n with open(filepath, \"rb\") as file:\n logger.info(f\"Uploading file {filepath} ({file_size} bytes) to session {session_id}\")\n sbx.files.write(filpath.name, file)\n \n # Verify upload succeeded\n if sbx.files.verify_file_upload(filpath.name, file_size):\n filenames.append(filpath.name)\n logger.debug(f\"Successfully uploaded and verified {filpath.name}\")\n else:\n logger.error(f\"File upload verification failed for {filpath.name}\")\n raise RuntimeError(f\"File upload verification failed for {filpath.name}\")\n \n except Exception as e:\n logger.error(f\"Failed to upload file {filepath} for session {session_id}: {str(e)}\")\n raise\n else:\n logger.info(f\"No files to upload for session {session_id}\")\n\n # Initialize or continue session state\n if is_continuing_session:\n # Load existing session state\n session_state = existing_session_state\n \n # Validate and repair conversation history to prevent API errors\n session_manager.validate_and_repair_conversation(session_state)\n \n message_history = session_manager.get_conversation_history(session_state)\n logger.info(f\"Continuing session {session_id} with {len(message_history)} existing messages\")\n \n # Add new user input if provided\n if user_input and user_input.strip():\n # Check if this input was already added by comparing with the last message\n last_message = message_history[-1] if message_history else None\n should_add_input = True\n \n if last_message and last_message.get(\"role\") == \"user\":\n # If the last message is from user and has the same content, don't add duplicate\n if last_message.get(\"content\") == user_input:\n should_add_input = False\n logger.debug(f\"User input already present in session {session_id}\")\n \n if should_add_input:\n session_manager.add_message(session_state, \"user\", user_input)\n message_history = session_manager.get_conversation_history(session_state)\n logger.info(f\"Added new user input to existing session {session_id}\")\n \n # Show notification that we're continuing the conversation\n continue_message = \"🔄 Continuing conversation with new input...\"\n continue_notification = create_progress_notification(continue_message)\n yield continue_notification, message_history, save_dir\n else:\n # Create new session state\n logger.info(f\"Initializing new session {session_id}\")\n \n # Format files section\n if files is None:\n files_section = \"- None\"\n else:\n files_section = \"- \" + \"\\n- \".join(filenames)\n logger.info(f\"System prompt includes {len(filenames)} files: {filenames}\")\n \n # Format GPU information\n gpu_info = gpu_type.upper() if gpu_type != \"cpu\" else \"CPU Only\"\n if gpu_type in [\"T4\", \"L4\", \"A100-40GB\", \"A100-80GB\", \"H100\"]:\n gpu_info = f\"NVIDIA {gpu_type}\"\n \n # Format available packages based on hardware configuration\n packages_list = sbx.available_packages\n packages_section = \"\\n\".join([f\"- {package}\" for package in packages_list])\n \n # Format the complete system prompt with named placeholders\n system_prompt = DEFAULT_SYSTEM_PROMPT.replace(\"{AVAILABLE_FILES}\", files_section)\n system_prompt = system_prompt.replace(\"{GPU_TYPE}\", gpu_info)\n system_prompt = system_prompt.replace(\"{CPU_CORES}\", str(cpu_cores))\n system_prompt = system_prompt.replace(\"{MEMORY_GB}\", str(memory_gb))\n system_prompt = system_prompt.replace(\"{TIMEOUT_SECONDS}\", str(timeout_sec))\n system_prompt = system_prompt.replace(\"{AVAILABLE_PACKAGES}\", packages_section)\n \n # Create session state with configuration\n hardware_config = {\n \"gpu_type\": gpu_type,\n \"cpu_cores\": cpu_cores,\n \"memory_gb\": memory_gb,\n \"timeout_sec\": timeout_sec\n }\n \n api_config = {\n \"model_name\": model_name or user_model_name or \"unknown\",\n \"provider_endpoint\": os.environ.get(\"PROVIDER_API_ENDPOINT\") or provider_api_endpoint,\n \"provider_type\": \"openai_compatible\"\n }\n \n environment_config = {\n \"variables\": env_vars_text or \"\",\n \"files_uploaded\": filenames if filenames else []\n }\n \n # Create initial session state\n session_state = session_manager.create_initial_state(\n hardware_config, api_config, environment_config, system_prompt\n )\n \n # Add user input if provided\n if user_input and user_input.strip():\n session_manager.add_message(session_state, \"user\", user_input)\n \n # Get conversation history\n message_history = session_manager.get_conversation_history(session_state)\n \n # Save initial state\n session_manager.save_state(session_state)\n \n logger.info(f\"Created new session {session_id} with {len(message_history)} messages\")\n \n logger.debug(f\"Session {session_id} ready with {len(message_history)} messages\")\n\n # Determine which tools to use based on web search toggle\n from jupyter_agent import TOOLS\n if enable_web_search:\n # Check if Tavily API key is available\n tavily_key = os.environ.get(\"TAVILY_API_KEY\") or tavily_api_key\n if tavily_key:\n selected_tools = TOOLS # Use all tools (code + search)\n logger.info(f\"Web search enabled for session {session_id} - using all tools\")\n else:\n selected_tools = TOOLS[:1] # Use only code execution tool\n logger.warning(f\"Web search enabled but no Tavily API key found for session {session_id} - using code tool only\")\n else:\n selected_tools = TOOLS[:1] # Use only code execution tool \n logger.info(f\"Web search disabled for session {session_id} - using code tool only\")\n\n logger.info(f\"Starting interactive notebook execution for session {session_id}\")\n \n # Import Phoenix session context if available\n try:\n from jupyter_agent import create_phoenix_session_context\n phoenix_available = True\n except ImportError:\n phoenix_available = False\n \n # Prepare session metadata for Phoenix tracing at the session level\n if phoenix_available:\n session_level_metadata = {\n \"agent_type\": \"eureka-agent\",\n \"session_type\": \"jupyter_execution\",\n \"gpu_type\": gpu_type,\n \"cpu_cores\": cpu_cores,\n \"memory_gb\": memory_gb,\n \"timeout_sec\": timeout_sec,\n \"web_search_enabled\": enable_web_search,\n \"tools_available\": len(selected_tools)\n }\n \n # Add API provider info if available\n if model_name:\n session_level_metadata[\"model\"] = model_name\n \n session_context = create_phoenix_session_context(\n session_id=session_id,\n user_id=None, # Could add user identification if available\n metadata=session_level_metadata\n )\n else:\n from contextlib import nullcontext\n session_context = nullcontext()\n \n # Wrap the entire execution in a Phoenix session context\n with session_context:\n logger.debug(f\"Starting session-level Phoenix tracing for {session_id}\")\n try:\n for notebook_html, notebook_data, messages in run_interactive_notebook_with_session_state(\n client, model_name, session_manager, session_state, sbx, STOP_EVENTS[session_id], selected_tools\n ):\n message_history = messages\n logger.debug(f\"Interactive notebook yield for session {session_id}\")\n # Update session state and yield with legacy notebook file for UI compatibility\n session_manager.update_notebook_data(session_state, notebook_data)\n session_manager.save_state(session_state)\n \n # Create legacy notebook file for UI download compatibility\n with open(save_dir, 'w', encoding='utf-8') as f:\n json.dump(notebook_data, f, indent=2)\n \n yield notebook_html, message_history, save_dir\n \n except Exception as e:\n logger.error(f\"Error during interactive notebook execution for session {session_id}: {str(e)}\")\n # Save error state\n session_manager.update_execution_state(session_state, is_running=False, last_execution_successful=False)\n session_manager.save_state(session_state)\n raise\n \n # Final save and cleanup\n try:\n session_manager.update_execution_state(session_state, is_running=False)\n session_manager.save_state(session_state)\n logger.info(f\"Final session state saved for session {session_id}\")\n \n # Create final legacy notebook file for UI\n with open(save_dir, 'w', encoding='utf-8') as f:\n json.dump(notebook_data, f, indent=2)\n \n except Exception as e:\n logger.error(f\"Failed to save final session state for session {session_id}: {str(e)}\")\n raise\n \n yield notebook_html, message_history, save_dir\n logger.info(f\"Completed execution for session {session_id}\")\n \n # Update legacy execution state for compatibility\n if session_id in EXECUTION_STATES:\n EXECUTION_STATES[session_id][\"running\"] = False\n\ndef clear(msg_state, request: gr.Request):\n \"\"\"Clear notebook but keep session data (less destructive than shutdown)\"\"\"\n session_id = request.session_hash\n logger.info(f\"Clearing notebook for session {session_id}\")\n \n # Stop any running execution\n if session_id in STOP_EVENTS:\n STOP_EVENTS[session_id].set()\n \n # Clear execution states but keep session data\n if session_id in EXECUTION_STATES:\n EXECUTION_STATES[session_id][\"running\"] = False\n EXECUTION_STATES[session_id][\"paused\"] = False\n EXECUTION_STATES[session_id][\"current_phase\"] = \"ready\"\n \n # Reset message state for UI\n msg_state = []\n logger.info(f\"Reset notebook display for session {session_id}\")\n \n return init_notebook.render(), msg_state\n\ndef stop_execution(request: gr.Request):\n \"\"\"Stop the current execution for this session\"\"\"\n session_id = request.session_hash\n logger.info(f\"Stopping execution for session {session_id}\")\n \n if session_id in STOP_EVENTS and session_id in EXECUTION_STATES:\n # Check if execution is actually running\n if EXECUTION_STATES[session_id].get(\"running\", False):\n STOP_EVENTS[session_id].set()\n logger.info(f\"Stop signal sent for session {session_id}\")\n \n # Update execution state\n EXECUTION_STATES[session_id][\"running\"] = False\n EXECUTION_STATES[session_id][\"paused\"] = True\n EXECUTION_STATES[session_id][\"current_phase\"] = \"stopping\"\n \n # Also update session state if available\n session_manager = SessionStateManager(session_id, TMP_DIR)\n session_state = session_manager.load_state()\n if session_state:\n session_manager.update_execution_state(\n session_state, is_running=False, is_paused=True, current_phase=\"stopping\"\n )\n session_manager.save_state(session_state)\n \n return \"⏸️ Execution stopped - click Run to resume with new input\"\n else:\n logger.info(f\"No active execution to stop for session {session_id}\")\n return \"⚪ No active execution to stop\"\n else:\n logger.warning(f\"No execution session found for {session_id}\")\n return \"❌ No execution session found\"\n\ndef shutdown_sandbox(request: gr.Request):\n \"\"\"Shutdown the sandbox while preserving all session data and files\"\"\"\n session_id = request.session_hash\n logger.info(f\"Shutting down sandbox for {session_id} (preserving all session data and files)\")\n \n try:\n # 1. Stop any running execution first\n if session_id in STOP_EVENTS:\n STOP_EVENTS[session_id].set()\n logger.info(f\"Stopped execution for session {session_id}\")\n \n # 2. Shutdown Modal sandbox only\n if session_id in SANDBOXES:\n logger.info(f\"Killing Modal sandbox for session {session_id}\")\n SANDBOXES[session_id].kill()\n SANDBOXES.pop(session_id)\n logger.info(f\"Successfully shutdown sandbox for session {session_id}\")\n \n # 3. Log what's being preserved (but don't remove anything)\n session_manager = SessionStateManager(session_id, TMP_DIR)\n if session_manager.session_exists():\n logger.info(f\"Preserving session data for {session_id}\")\n \n # Load session state to show what's being preserved\n session_state = session_manager.load_state()\n if session_state:\n # Log what we're preserving\n stats = session_state.get(\"session_stats\", {})\n llm_interactions = len(session_state.get(\"llm_interactions\", []))\n tool_executions = len(session_state.get(\"tool_executions\", []))\n \n logger.info(f\"Preserving session {session_id}: \"\n f\"{stats.get('total_messages', 0)} messages, \"\n f\"{llm_interactions} LLM interactions, \"\n f\"{tool_executions} tool executions, \"\n f\"{stats.get('total_code_executions', 0)} code runs\")\n \n # Log all preserved files\n if session_manager.session_dir.exists():\n try:\n preserved_files = []\n for file_path in session_manager.session_dir.iterdir():\n if file_path.is_file():\n preserved_files.append(file_path.name)\n \n if preserved_files:\n logger.info(f\"Preserving {len(preserved_files)} files in {session_id}: {preserved_files}\")\n else:\n logger.info(f\"No files found in session {session_id}\")\n \n except OSError as e:\n logger.warning(f\"Could not check session directory {session_id}: {e}\")\n \n # 4. Keep execution tracking data (don't clear anything)\n logger.info(f\"Preserving execution state and stop events for {session_id}\")\n \n logger.info(f\"Sandbox shutdown completed for session {session_id} (all data preserved)\")\n return gr.Button(visible=False)\n \n except Exception as e:\n logger.error(f\"Error during shutdown for session {session_id}: {str(e)}\")\n return f\"❌ Error during shutdown: {str(e)}\", gr.Button(visible=True)\n\n# continue_execution function removed - functionality integrated into execute_jupyter_agent\n\ndef get_execution_status(request: gr.Request):\n \"\"\"Get the current execution status for UI updates\"\"\"\n session_id = request.session_hash\n \n if session_id not in EXECUTION_STATES:\n return \"⚪ Ready\"\n \n state = EXECUTION_STATES[session_id]\n if state[\"running\"]:\n if session_id in STOP_EVENTS and STOP_EVENTS[session_id].is_set():\n return \"⏸️ Stopping...\"\n else:\n # Check if we have more detailed phase information\n phase = state.get(\"current_phase\", \"running\")\n if phase == \"generating\":\n return \"🟢 Generating response...\"\n elif phase == \"executing_code\":\n return \"🟢 Executing code...\"\n elif phase == \"searching\":\n return \"🟢 Searching web...\"\n else:\n return \"🟢 Running\"\n elif state.get(\"paused\", False):\n return \"⏸️ Paused - Click Run to continue\"\n else:\n return \"⚪ Ready\"\n\ndef is_sandbox_active(request: gr.Request):\n \"\"\"Check if sandbox is active for the current session\"\"\"\n session_id = request.session_hash\n return session_id in SANDBOXES\n\ndef get_sandbox_status_and_visibility(request: gr.Request):\n \"\"\"Get sandbox status message and button visibility\"\"\"\n session_id = request.session_hash\n if session_id in SANDBOXES:\n return \"🟢 Sandbox active\", gr.Button(visible=True)\n else:\n return \"⚪ No sandbox active\", gr.Button(visible=False)\n\ndef update_sandbox_button_visibility(request: gr.Request):\n \"\"\"Update only the button visibility based on sandbox status\"\"\"\n session_id = request.session_hash\n return gr.Button(visible=session_id in SANDBOXES)\n\ndef reset_ui_after_shutdown(request: gr.Request):\n \"\"\"Reset UI components after complete shutdown\"\"\"\n session_id = request.session_hash\n \n # Check if session is truly cleared\n is_cleared = (session_id not in SANDBOXES and \n session_id not in EXECUTION_STATES and \n session_id not in STOP_EVENTS)\n \n if is_cleared:\n # Return reset state for all UI components\n return (\n init_notebook.render(), # Reset notebook display\n [], # Clear message state\n \"⚪ Ready\", # Reset status\n \"⚪ No sandbox active\", # Reset sandbox status\n gr.Button(visible=False) # Hide shutdown button\n )\n else:\n # Return current state if not fully cleared\n status = get_execution_status(request)\n sandbox_status, button_vis = get_sandbox_status_and_visibility(request)\n return (\n init_notebook.render(), # Still reset notebook display\n [], # Still clear message state\n status,\n sandbox_status,\n button_vis\n )\n\ndef reconstruct_message_history_from_notebook(notebook_data):\n \"\"\"Reconstruct message history from notebook cells\"\"\"\n message_history = []\n cells = notebook_data.get('cells', [])\n \n system_prompt = None\n current_conversation = []\n \n for cell in cells:\n cell_type = cell.get('cell_type', '')\n \n if cell_type == 'markdown':\n content = cell.get('source', '')\n if isinstance(content, list):\n content = ''.join(content)\n \n # Check if this is a system message\n if 'System' in content and 'IMPORTANT EXECUTION GUIDELINES' in content:\n # Extract the system prompt content\n system_content = content\n # Clean up the HTML and extract the actual content\n # Remove HTML tags and extract the text content\n clean_content = re.sub(r'<[^>]+>', '', system_content)\n clean_content = re.sub(r'\\n+', '\\n', clean_content).strip()\n system_prompt = clean_content\n \n elif 'User' in content and not any(word in content for word in ['Assistant', 'System']):\n # This is a user message\n # Extract the user content after the User header\n user_content = content.split('User')[1] if 'User' in content else content\n # Clean up HTML and formatting\n user_content = re.sub(r'<[^>]+>', '', user_content)\n user_content = re.sub(r'-{3,}', '', user_content)\n user_content = user_content.strip()\n \n if user_content:\n current_conversation.append({\n \"role\": \"user\", \n \"content\": user_content\n })\n \n elif 'Assistant' in content:\n # This is an assistant message\n assistant_content = content.split('Assistant')[1] if 'Assistant' in content else content\n # Clean up HTML and formatting\n assistant_content = re.sub(r'<[^>]+>', '', assistant_content)\n assistant_content = re.sub(r'-{3,}', '', assistant_content)\n assistant_content = assistant_content.strip()\n \n if assistant_content:\n current_conversation.append({\n \"role\": \"assistant\",\n \"content\": assistant_content\n })\n \n # Build the final message history\n if system_prompt:\n message_history.append({\n \"role\": \"system\",\n \"content\": system_prompt\n })\n \n # Add the conversation messages\n message_history.extend(current_conversation)\n \n return message_history\n\ndef load_previous_notebook(notebook_choice, request: gr.Request):\n \"\"\"Load a previous notebook with complete session configuration (dev only)\"\"\"\n if not is_dev_environment():\n return (init_notebook.render(), [], \"Load previous notebooks is only available in development mode\",\n None, None, None, None, None, \"\", \"\", \"\", \"\", \"\", \"\", \"\", False)\n \n if not notebook_choice or notebook_choice == \"None\":\n return (init_notebook.render(), [], \"Please select a notebook to load\",\n None, None, None, None, None, \"\", \"\", \"\", \"\", \"\", \"\", \"\", False)\n \n try:\n # Parse the notebook choice to get the session ID\n session_id = notebook_choice.split(\" \")[0]\n notebook_path = Path(TMP_DIR) / session_id / \"jupyter-agent.ipynb\"\n \n if not notebook_path.exists():\n return (init_notebook.render(), [], f\"Notebook file not found: {notebook_path}\",\n None, None, None, None, None, \"\", \"\", \"\", \"\", \"\", \"\", \"\", False)\n \n # Load the notebook\n with open(notebook_path, 'r') as f:\n notebook_data = json.load(f)\n \n # Load session state \n temp_session_manager = SessionStateManager(session_id, TMP_DIR)\n session_state = temp_session_manager.load_state()\n session_config = None # For backward compatibility\n \n # Extract config from session state for UI restoration\n if session_state:\n session_config = {\n \"hardware\": session_state.get(\"hardware_config\", {}),\n \"environment_vars\": session_state.get(\"environment\", {}).get(\"variables\", \"\"),\n \"api_keys\": {\n \"model_name\": session_state.get(\"api_config\", {}).get(\"model_name\", \"\")\n }\n }\n \n # Create a new JupyterNotebook instance with the loaded data\n loaded_notebook = JupyterNotebook()\n loaded_notebook.data = notebook_data\n \n # Reconstruct message history from notebook cells\n message_history = reconstruct_message_history_from_notebook(notebook_data)\n \n # Store the loaded notebook info in session for continue functionality\n session_id_hash = request.session_hash\n if session_id_hash not in EXECUTION_STATES:\n EXECUTION_STATES[session_id_hash] = {}\n \n EXECUTION_STATES[session_id_hash][\"loaded_notebook\"] = {\n \"notebook_data\": notebook_data,\n \"message_history\": message_history,\n \"original_session\": session_id,\n \"session_config\": session_config\n }\n \n logger.info(f\"Successfully loaded notebook from {notebook_path}\")\n logger.info(f\"Reconstructed message history with {len(message_history)} messages\")\n \n # Prepare configuration values to restore UI state\n config_loaded = \"\"\n gpu_type = None\n cpu_cores = None\n memory_gb = None\n timeout_sec = None\n env_vars = \"\"\n modal_token_id = \"\"\n modal_token_secret = \"\"\n hf_token = \"\"\n provider_api_key = \"\"\n provider_api_endpoint = \"\"\n model_name = \"\"\n \n if session_config:\n hardware = session_config.get(\"hardware\", {})\n gpu_type = hardware.get(\"gpu_type\")\n cpu_cores = hardware.get(\"cpu_cores\")\n memory_gb = hardware.get(\"memory_gb\")\n timeout_sec = hardware.get(\"timeout_sec\")\n env_vars = session_config.get(\"environment_vars\", \"\")\n \n api_keys = session_config.get(\"api_keys\", {})\n modal_token_id = api_keys.get(\"modal_token_id\", \"\")\n modal_token_secret = api_keys.get(\"modal_token_secret\", \"\")\n hf_token = api_keys.get(\"hf_token\", \"\")\n provider_api_key = api_keys.get(\"provider_api_key\", \"\")\n provider_api_endpoint = api_keys.get(\"provider_api_endpoint\", \"\")\n model_name = api_keys.get(\"model_name\", \"\")\n \n config_loaded = f\"✅ Configuration restored: GPU={gpu_type}, CPU={cpu_cores}, Memory={memory_gb}GB, Timeout={timeout_sec}s\"\n \n success_message = f\"✅ Loaded notebook: {session_id} ({len(notebook_data.get('cells', []))} cells, {len(message_history)} messages)\"\n if config_loaded:\n success_message += f\"\\n{config_loaded}\"\n \n return (loaded_notebook.render(), message_history, success_message,\n gpu_type, cpu_cores, memory_gb, timeout_sec, env_vars,\n modal_token_id, modal_token_secret, hf_token, provider_api_key, provider_api_endpoint, model_name,\n \"\", False) # Default empty tavily_api_key and False for enable_web_search\n \n except Exception as e:\n logger.error(f\"Failed to load notebook {notebook_choice}: {str(e)}\")\n error_message = f\"❌ Failed to load notebook: {str(e)}\"\n return (init_notebook.render(), [], error_message,\n None, None, None, None, None, \"\", \"\", \"\", \"\", \"\", \"\", \"\", False)\n\ndef get_notebook_options():\n \"\"\"Get options for notebook dropdown (dev only)\"\"\"\n if not is_dev_environment():\n return [\"Load previous notebooks is only available in development mode\"]\n \n notebooks = get_previous_notebooks()\n if not notebooks:\n return [\"No previous notebooks found\"]\n \n options = [\"None\"] + [nb['display_name'] for nb in notebooks[:20]] # Limit to 20 most recent\n return options\n\ndef refresh_notebook_options():\n \"\"\"Refresh the notebook options dropdown\"\"\"\n return gr.Dropdown(choices=get_notebook_options(), value=\"None\")\n\n# Legacy session configuration functions removed - replaced by SessionStateManager\n# All session data is now stored in a single comprehensive session_state.json file\n\n\ncss = \"\"\"\n#component-0 {\n height: 100vh;\n overflow-y: auto;\n padding: 20px;\n}\n\n.gradio-container {\n height: 100vh !important;\n}\n\n.contain {\n height: 100vh !important;\n}\n\n/* Button states for execution control */\n.button-executing {\n opacity: 0.6 !important;\n pointer-events: none !important;\n cursor: not-allowed !important;\n}\n\n.button-executing::after {\n content: \" ⏳\";\n}\n\n.status-running {\n animation: pulse 2s infinite;\n}\n\n@keyframes pulse {\n 0% { opacity: 1; }\n 50% { opacity: 0.5; }\n 100% { opacity: 1; }\n}\n\"\"\"\n\n\n# Create the interface\nwith gr.Blocks() as demo:\n msg_state = gr.State(value=[])\n\n # Environment info display\n env_info = gr.Markdown(f\"\"\"\n **Environment**: {get_environment().upper()} | **Features**: {\"Development features enabled\" if is_dev_environment() else \"Production mode\"}\n \"\"\")\n\n html_output = gr.HTML(value=JupyterNotebook().render())\n \n user_input = gr.Textbox(\n # value=\"train a 5 neuron neural network to classify the iris dataset\",\n value=\"can you finetune llama 3.2 1b on tiny stories dataset and using unsloth\",\n lines=3,\n label=\"Agent task\"\n )\n \n with gr.Accordion(\"Upload files ⬆ | Download notebook⬇\", open=False):\n files = gr.File(label=\"Upload files to use\", file_count=\"multiple\")\n file = gr.File(TMP_DIR+\"jupyter-agent.ipynb\", label=\"Download Jupyter Notebook\")\n\n\n with gr.Row():\n # Web Search Configuration\n with gr.Accordion(\"🔍 Web Search Settings\", open=False):\n with gr.Row():\n enable_web_search = gr.Checkbox(\n label=\"Enable Web Search\",\n value=bool(os.environ.get(\"TAVILY_API_KEY\")), # Default to True if API key is available\n info=\"Allow the agent to search the web for current information and documentation\"\n )\n \n # Show web search status with better formatting\n tavily_status = \"✅ Available\" if os.environ.get(\"TAVILY_API_KEY\") else \"❌ API Key Required\"\n gr.Markdown(f\"**Status:** {tavily_status}\")\n \n gr.Markdown(\"\"\"\n **Web Search Features:**\n - 🌐 Search for current tutorials, documentation, and best practices\n - 🐛 Find solutions to error messages and debugging help\n - 📚 Access up-to-date library documentation and examples\n - 💡 Get recent examples and code snippets from the web\n \n ⚠️ **Note**: Web search requires a Tavily API key. Get one free at [tavily.com](https://tavily.com)\n \"\"\")\n # Previous notebooks section (dev only)\n if is_dev_environment():\n with gr.Accordion(\"📂 Load Previous Notebook (Dev Only)\", open=False):\n notebook_dropdown = gr.Dropdown(\n choices=get_notebook_options(),\n value=\"None\",\n label=\"Select Previous Notebook\",\n info=\"Load a previously created notebook session\"\n )\n with gr.Row():\n load_notebook_btn = gr.Button(\"📖 Load Selected\", variant=\"secondary\")\n refresh_notebooks_btn = gr.Button(\"🔄 Refresh List\", variant=\"secondary\")\n \n load_status = gr.Textbox(\n label=\"Load Status\",\n interactive=False,\n visible=False\n ) \n # Check for missing API keys and show input fields conditionally\n missing_keys = get_missing_api_keys()\n \n # API Key Configuration (shown only if keys are missing)\n if missing_keys:\n with gr.Accordion(\"🔑 Required API Keys (Missing from .env)\", open=True):\n gr.Markdown(\"\"\"\n **⚠️ Some required API keys are missing from your .env file.**\n Please provide them below to use the application:\n \"\"\")\n \n api_key_components = {}\n \n if \"MODAL_TOKEN_ID\" in missing_keys:\n api_key_components[\"modal_token_id\"] = gr.Textbox(\n label=\"Modal Token ID\",\n placeholder=\"ak-...\",\n info=\"Modal Token ID for sandbox access\",\n type=\"password\"\n )\n else:\n api_key_components[\"modal_token_id\"] = gr.Textbox(visible=False)\n \n if \"MODAL_TOKEN_SECRET\" in missing_keys:\n api_key_components[\"modal_token_secret\"] = gr.Textbox(\n label=\"Modal Token Secret\", \n placeholder=\"as-...\",\n info=\"Modal Token Secret for sandbox access\",\n type=\"password\"\n )\n else:\n api_key_components[\"modal_token_secret\"] = gr.Textbox(visible=False)\n \n if \"HF_TOKEN\" in missing_keys:\n api_key_components[\"hf_token\"] = gr.Textbox(\n label=\"Hugging Face Token (Optional)\",\n placeholder=\"hf_...\",\n info=\"Hugging Face Token for model access\",\n type=\"password\"\n )\n else:\n api_key_components[\"hf_token\"] = gr.Textbox(visible=False)\n \n if \"PROVIDER_API_KEY\" in missing_keys:\n api_key_components[\"provider_api_key\"] = gr.Textbox(\n label=\"AI Provider API Key\",\n placeholder=\"sk-, gsk_, or csk-...\",\n info=\"API Key for your AI provider (Anthropic, OpenAI, Cerebras, etc.)\",\n type=\"password\"\n )\n else:\n api_key_components[\"provider_api_key\"] = gr.Textbox(visible=False)\n \n if \"PROVIDER_API_ENDPOINT\" in missing_keys:\n api_key_components[\"provider_api_endpoint\"] = gr.Textbox(\n label=\"AI Provider API Endpoint\",\n placeholder=\"https://api.anthropic.com/v1/\",\n info=\"API endpoint for your AI provider\"\n )\n else:\n api_key_components[\"provider_api_endpoint\"] = gr.Textbox(visible=False)\n \n if \"MODEL_NAME\" in missing_keys:\n api_key_components[\"model_name\"] = gr.Textbox(\n label=\"Model Name\",\n placeholder=\"claude-sonnet-4-20250514\",\n info=\"Name of the model to use\"\n )\n else:\n api_key_components[\"model_name\"] = gr.Textbox(visible=False)\n \n if \"TAVILY_API_KEY\" in missing_keys:\n api_key_components[\"tavily_api_key\"] = gr.Textbox(\n label=\"Tavily API Key (Optional)\",\n placeholder=\"tvly-...\",\n info=\"Tavily API Key for web search functionality\",\n type=\"password\"\n )\n else:\n api_key_components[\"tavily_api_key\"] = gr.Textbox(visible=False)\n else:\n # Create hidden components when no keys are missing\n api_key_components = {\n \"modal_token_id\": gr.Textbox(visible=False),\n \"modal_token_secret\": gr.Textbox(visible=False),\n \"hf_token\": gr.Textbox(visible=False),\n \"provider_api_key\": gr.Textbox(visible=False),\n \"provider_api_endpoint\": gr.Textbox(visible=False),\n \"model_name\": gr.Textbox(visible=False),\n \"tavily_api_key\": gr.Textbox(visible=False)\n }\n \n\n \n\n \n with gr.Accordion(\"Hardware Configuration ⚙️\", open=False):\n with gr.Row():\n with gr.Column():\n env_vars = gr.Textbox(\n label=\"Environment Variables\",\n placeholder=\"Enter environment variables (one per line):\\nAPI_KEY=your_key_here\\nDATA_PATH=/path/to/data\\nDEBUG=true\",\n lines=5,\n info=\"Add custom environment variables for the sandbox. Format: KEY=value (one per line)\"\n )\n \n env_info = gr.Markdown(\"\"\"\n **Environment Variables Info:**\n - Variables will be available in the sandbox environment\n - Use KEY=value format, one per line\n - Common examples: API keys, data paths, configuration flags\n - Variables are session-specific and not persisted between sessions\n \n ⚠️ **Security**: Avoid sensitive credentials in shared environments\n \"\"\")\n with gr.Column():\n with gr.Row():\n gpu_type = gr.Dropdown(\n choices=GPU_OPTIONS,\n value=\"cpu\",\n label=\"GPU Type\",\n info=\"Select hardware acceleration\"\n )\n cpu_cores = gr.Slider(\n minimum=0.25,\n maximum=16,\n value=2.0,\n step=0.25,\n label=\"CPU Cores\",\n info=\"Number of CPU cores\"\n )\n with gr.Row():\n memory_gb = gr.Slider(\n minimum=0.5,\n maximum=64,\n value=8.0,\n step=0.5,\n label=\"Memory (GB)\",\n info=\"RAM allocation\"\n )\n timeout_sec = gr.Slider(\n minimum=60,\n maximum=1800,\n value=300,\n step=60,\n label=\"Timeout (seconds)\",\n info=\"Maximum execution time\"\n )\n \n hardware_info = gr.Markdown(\"\"\"\n **Hardware Options:**\n - **CPU Only**: Free, good for basic tasks\n - **T4**: Low-cost GPU, good for small models\n - **L4**: Mid-range GPU, better performance\n - **A100 40/80GB**: High-end GPU for large models\n - **H100**: Latest flagship GPU for maximum performance\n \n ⚠️ **Note**: GPU instances cost more. Choose based on your workload.\n \"\"\")\n \n # with gr.Accordion(\"Environment Variables 🔧\", open=False):\n \n \n with gr.Row():\n generate_btn = gr.Button(\"Run!\", variant=\"primary\")\n stop_btn = gr.Button(\"⏸️ Stop\", variant=\"secondary\")\n # continue_btn removed - Run button handles continuation automatically\n clear_btn = gr.Button(\"Clear Notebook\", variant=\"stop\")\n shutdown_btn = gr.Button(\"🔴 Shutdown Sandbox\", variant=\"stop\", visible=False)\n \n # Status display\n status_display = gr.Textbox(\n value=\"⚪ Ready\", \n label=\"Execution Status\", \n interactive=False,\n max_lines=1\n ) \n\n generate_btn.click(\n fn=execute_jupyter_agent,\n inputs=[\n user_input, files, msg_state, gpu_type, cpu_cores, memory_gb, timeout_sec, env_vars,\n api_key_components[\"modal_token_id\"], api_key_components[\"modal_token_secret\"], \n api_key_components[\"hf_token\"], api_key_components[\"provider_api_key\"], \n api_key_components[\"provider_api_endpoint\"], api_key_components[\"model_name\"],\n api_key_components[\"tavily_api_key\"], enable_web_search\n ],\n outputs=[html_output, msg_state, file],\n show_progress=\"hidden\",\n )\n\n stop_btn.click(\n fn=stop_execution,\n outputs=[status_display],\n show_progress=\"hidden\",\n )\n\n # continue_btn.click handler removed - Run button handles continuation automatically\n\n clear_btn.click(fn=clear, inputs=[msg_state], outputs=[html_output, msg_state])\n\n shutdown_btn.click(\n fn=shutdown_sandbox,\n outputs=[shutdown_btn],\n show_progress=\"hidden\",\n )\n \n # Add event handlers for notebook loading (dev only)\n if is_dev_environment():\n load_notebook_btn.click(\n fn=load_previous_notebook,\n inputs=[notebook_dropdown],\n outputs=[\n html_output, msg_state, load_status,\n gpu_type, cpu_cores, memory_gb, timeout_sec, env_vars,\n api_key_components[\"modal_token_id\"], api_key_components[\"modal_token_secret\"], \n api_key_components[\"hf_token\"], api_key_components[\"provider_api_key\"], \n api_key_components[\"provider_api_endpoint\"], api_key_components[\"model_name\"],\n api_key_components[\"tavily_api_key\"], enable_web_search\n ],\n show_progress=\"hidden\"\n )\n \n refresh_notebooks_btn.click(\n fn=refresh_notebook_options,\n outputs=[notebook_dropdown],\n show_progress=\"hidden\"\n )\n \n # Show/hide load status based on selection\n notebook_dropdown.change(\n fn=lambda choice: gr.Textbox(visible=choice != \"None\"),\n inputs=[notebook_dropdown],\n outputs=[load_status]\n )\n\n # Periodic status update using timer\n status_timer = gr.Timer(2.0) # Update every 2 seconds\n status_timer.tick(\n fn=get_execution_status,\n outputs=[status_display],\n show_progress=\"hidden\"\n )\n\n # Update button visibility periodically\n button_timer = gr.Timer(3.0) # Check every 3 seconds\n button_timer.tick(\n fn=update_sandbox_button_visibility,\n outputs=[shutdown_btn],\n show_progress=\"hidden\"\n )\n\n demo.load(\n fn=None,\n inputs=None,\n outputs=None,\n js=\"\"\" () => {\n if (document.querySelectorAll('.dark').length) {\n document.querySelectorAll('.dark').forEach(el => el.classList.remove('dark'));\n }\n \n // Add execution state management functions\n window.setExecutionState = function(isExecuting) {\n // Find Run button by text content since variant attribute might not be reliable\n const buttons = document.querySelectorAll('button');\n let runButton = null;\n let stopButton = null;\n \n buttons.forEach(button => {\n const text = button.textContent.trim().toLowerCase();\n if (text.includes('run') && !text.includes('stop')) {\n runButton = button;\n } else if (text.includes('stop') || text.includes('⏸️')) {\n stopButton = button;\n }\n });\n \n if (runButton) {\n if (isExecuting) {\n runButton.classList.add('button-executing');\n runButton.disabled = true;\n runButton.style.opacity = '0.6';\n runButton.style.cursor = 'not-allowed';\n runButton.style.pointerEvents = 'none';\n if (runButton.textContent.indexOf('⏳') === -1) {\n runButton.textContent = runButton.textContent.replace('!', '! ⏳');\n }\n } else {\n runButton.classList.remove('button-executing');\n runButton.disabled = false;\n runButton.style.opacity = '1';\n runButton.style.cursor = 'pointer';\n runButton.style.pointerEvents = 'auto';\n runButton.textContent = runButton.textContent.replace(' ⏳', '');\n }\n }\n \n // Also update stop button visibility/state\n if (stopButton) {\n stopButton.style.display = isExecuting ? 'block' : 'inline-block';\n }\n };\n \n // Monitor for status changes and update button states\n window.monitorExecutionStatus = function() {\n // Try multiple ways to find the status element\n let statusElement = document.querySelector('input[label*=\"Execution Status\"], input[label*=\"Status\"], textarea[label*=\"Status\"]');\n \n if (!statusElement) {\n // Fallback: look for any input that might contain status\n const allInputs = document.querySelectorAll('input, textarea');\n allInputs.forEach(input => {\n if (input.value && (input.value.includes('🟢') || input.value.includes('⚪') || input.value.includes('⏸️'))) {\n statusElement = input;\n }\n });\n }\n \n if (statusElement) {\n const status = statusElement.value || '';\n const isRunning = status.includes('🟢') || status.includes('Running') || status.includes('Generating') || status.includes('Executing');\n const isReady = status.includes('⚪') || status.includes('Ready');\n \n window.setExecutionState(isRunning);\n \n // Add visual indicator to status element\n if (isRunning) {\n statusElement.style.background = '#e3f2fd';\n statusElement.style.borderColor = '#2196f3';\n } else if (isReady) {\n statusElement.style.background = '#f5f5f5';\n statusElement.style.borderColor = '#ccc';\n } else {\n statusElement.style.background = '#fff3e0';\n statusElement.style.borderColor = '#ff9800';\n }\n }\n };\n \n // Set up mutation observer to watch for status changes\n const observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(mutation) {\n if (mutation.type === 'childList' || mutation.type === 'attributes') {\n setTimeout(window.monitorExecutionStatus, 100);\n }\n });\n });\n \n // Start observing\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n attributes: true\n });\n}\n\"\"\"\n )\n\nlogger.info(\"Starting Gradio application\")\ndemo.launch(ssr_mode=False)\n"}}}} |