diff --git a/backend/agent/run.py b/backend/agent/run.py index 33862c9f..10329aa5 100644 --- a/backend/agent/run.py +++ b/backend/agent/run.py @@ -18,7 +18,6 @@ from agent.tools.sb_files_tool import SandboxFilesTool from agent.tools.sb_browser_tool import SandboxBrowserTool from agent.tools.data_providers_tool import DataProvidersTool from agent.prompt import get_system_prompt -from sandbox.sandbox import create_sandbox, get_or_start_sandbox from utils import logger from utils.billing import check_billing_status, get_account_id_from_thread @@ -39,7 +38,7 @@ async def run_agent( """Run the development agent with specified configuration.""" thread_manager = ThreadManager() - + client = await thread_manager.db.client # Get account ID from thread for billing checks diff --git a/backend/agent/tools/sb_deploy_tool.py b/backend/agent/tools/sb_deploy_tool.py index 3293aa7d..adce2ce0 100644 --- a/backend/agent/tools/sb_deploy_tool.py +++ b/backend/agent/tools/sb_deploy_tool.py @@ -3,7 +3,6 @@ from dotenv import load_dotenv from agentpress.tool import ToolResult, openapi_schema, xml_schema from sandbox.sandbox import SandboxToolsBase, Sandbox from utils.files_utils import clean_path -from agent.tools.sb_shell_tool import SandboxShellTool from agentpress.thread_manager import ThreadManager # Load environment variables @@ -16,7 +15,6 @@ class SandboxDeployTool(SandboxToolsBase): super().__init__(project_id, thread_manager) self.workspace_path = "/workspace" # Ensure we're always operating in /workspace self.cloudflare_api_token = os.getenv("CLOUDFLARE_API_TOKEN") - self.shell_tool = SandboxShellTool(project_id, thread_manager) def clean_path(self, path: str) -> str: """Clean and normalize a path to be relative to /workspace""" @@ -99,27 +97,23 @@ class SandboxDeployTool(SandboxToolsBase): # Single command that creates the project if it doesn't exist and then deploys project_name = f"{self.sandbox_id}-{name}" - deploy_cmd = f'''export CLOUDFLARE_API_TOKEN={self.cloudflare_api_token} && + deploy_cmd = f'''cd {self.workspace_path} && export CLOUDFLARE_API_TOKEN={self.cloudflare_api_token} && (npx wrangler pages deploy {full_path} --project-name {project_name} || (npx wrangler pages project create {project_name} --production-branch production && npx wrangler pages deploy {full_path} --project-name {project_name}))''' - # Execute command using shell_tool.execute_command - response = await self.shell_tool.execute_command( - command=deploy_cmd, - folder=None, # Use the workspace root - timeout=300 # Increased timeout for deployments - ) + # Execute the command directly using the sandbox's process.exec method + response = self.sandbox.process.exec(deploy_cmd, timeout=300) - print(f"Deployment response: {response}") + print(f"Deployment command output: {response.result}") - if response.success: + if response.exit_code == 0: return self.success_response({ "message": f"Website deployed successfully", - "output": response.output + "output": response.result }) else: - return self.fail_response(f"Deployment failed: {response.output}") + return self.fail_response(f"Deployment failed with exit code {response.exit_code}: {response.result}") except Exception as e: return self.fail_response(f"Error during deployment: {str(e)}") except Exception as e: diff --git a/backend/api.py b/backend/api.py index 346ebdb6..1fc97184 100644 --- a/backend/api.py +++ b/backend/api.py @@ -1,5 +1,6 @@ -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse from contextlib import asynccontextmanager from agentpress.thread_manager import ThreadManager from services.supabase import DBConnection @@ -8,6 +9,8 @@ from dotenv import load_dotenv import asyncio from utils.logger import logger import uuid +import time +from collections import OrderedDict # Import the agent API module from agent import api as agent_api @@ -21,6 +24,10 @@ db = DBConnection() thread_manager = None instance_id = str(uuid.uuid4())[:8] # Generate instance ID at module load time +# Rate limiter state +ip_tracker = OrderedDict() +MAX_CONCURRENT_IPS = 25 + @asynccontextmanager async def lifespan(app: FastAPI): # Startup @@ -57,9 +64,56 @@ async def lifespan(app: FastAPI): app = FastAPI(lifespan=lifespan) +# @app.middleware("http") +# async def log_requests_middleware(request: Request, call_next): +# client_ip = request.client.host +# logger.info(f"Request from IP {client_ip} to {request.method} {request.url.path}") +# response = await call_next(request) +# return response + +# @app.middleware("http") +# async def throw_error_middleware(request: Request, call_next): +# client_ip = request.client.host +# if client_ip != "109.49.168.102": +# logger.warning(f"Request blocked from IP {client_ip} to {request.method} {request.url.path}") +# return JSONResponse( +# status_code=403, +# content={"error": "Request blocked", "message": "Test DDoS protection"} +# ) +# return await call_next(request) + +# @app.middleware("http") +# async def rate_limit_middleware(request: Request, call_next): +# global ip_tracker +# client_ip = request.client.host + +# # Clean up old entries (older than 5 minutes) +# current_time = time.time() +# ip_tracker = OrderedDict((ip, ts) for ip, ts in ip_tracker.items() +# if current_time - ts < 300) + +# # Check if IP is already tracked +# if client_ip in ip_tracker: +# ip_tracker[client_ip] = current_time +# return await call_next(request) + +# # Check if we've hit the limit +# if len(ip_tracker) >= MAX_CONCURRENT_IPS: +# logger.warning(f"Rate limit exceeded. Current IPs: {len(ip_tracker)}") +# return JSONResponse( +# status_code=429, +# content={"error": "Too many concurrent connections", +# "message": "Maximum number of concurrent connections reached"} +# ) + +# # Add new IP +# ip_tracker[client_ip] = current_time +# logger.info(f"New connection from IP {client_ip}. Total connections: {len(ip_tracker)}") +# return await call_next(request) + app.add_middleware( CORSMiddleware, - allow_origins=["https://www.suna.so", "https://suna.so", "https://staging.suna.so", "http://localhost:3000"], + allow_origins=["https://www.suna.so", "https://suna.so", "https://staging.suna.so"], #http://localhost:3000 allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["Content-Type", "Authorization"], diff --git a/backend/docker/Dockerfile b/backend/docker/Dockerfile index 68cca4cc..44ca03af 100644 --- a/backend/docker/Dockerfile +++ b/backend/docker/Dockerfile @@ -14,13 +14,16 @@ COPY . . ENV PYTHONPATH=/app # Default environment variables -# These should be overridden at runtime with actual values ENV DAYTONA_API_KEY="" ENV DAYTONA_SERVER_URL="" ENV DAYTONA_TARGET="" ENV ANTHROPIC_API_KEY="" ENV OPENAI_API_KEY="" ENV MODEL_TO_USE="" +ENV TAVILY_API_KEY="" +ENV RAPID_API_KEY="" +ENV CLOUDFLARE_API_TOKEN="" + ENV SUPABASE_URL="" ENV SUPABASE_ANON_KEY="" @@ -35,4 +38,4 @@ ENV REDIS_SSL="" EXPOSE 8000 # Command to run the application with Uvicorn directly -CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "32", "--log-level", "info", "--timeout-keep-alive", "500", "--proxy-headers", "--forwarded-allow-ips", "*"] \ No newline at end of file +CMD ["gunicorn", "api:app", "--workers", "24", "--worker-class", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000", "--timeout", "600", "--graceful-timeout", "300", "--keep-alive", "250", "--max-requests", "1000", "--max-requests-jitter", "50", "--log-level", "info", "--forwarded-allow-ips", "*", "--worker-connections", "1000", "--worker-tmp-dir", "/dev/shm", "--preload"] \ No newline at end of file