suna/backend/agent/tools/utils/daytona_sandbox.py

361 lines
12 KiB
Python
Raw Normal View History

2025-04-09 04:09:45 +08:00
import os
import requests
from time import sleep
from daytona_sdk import Daytona, DaytonaConfig, CreateSandboxParams, SessionExecuteRequest
2025-04-09 07:37:06 +08:00
from dotenv import load_dotenv
2025-04-09 04:09:45 +08:00
from agentpress.tool import Tool
from utils.logger import logger
2025-04-09 07:37:06 +08:00
load_dotenv()
2025-04-10 00:44:17 +08:00
logger.info("Initializing Daytona sandbox configuration")
2025-04-09 04:09:45 +08:00
config = DaytonaConfig(
api_key=os.getenv("DAYTONA_API_KEY"),
server_url=os.getenv("DAYTONA_SERVER_URL"),
target=os.getenv("DAYTONA_TARGET")
)
2025-04-10 00:44:17 +08:00
if config.api_key:
logger.debug("Daytona API key configured successfully")
else:
logger.warning("No Daytona API key found in environment variables")
if config.server_url:
logger.debug(f"Daytona server URL set to: {config.server_url}")
else:
logger.warning("No Daytona server URL found in environment variables")
if config.target:
logger.debug(f"Daytona target set to: {config.target}")
else:
logger.warning("No Daytona target found in environment variables")
2025-04-09 04:09:45 +08:00
daytona = Daytona(config)
2025-04-10 00:44:17 +08:00
logger.debug("Daytona client initialized")
2025-04-09 04:09:45 +08:00
2025-04-09 07:37:06 +08:00
sandbox_browser_api = b'''
2025-04-09 04:09:45 +08:00
import traceback
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from browser_use import Agent, Browser
from browser_use.browser.context import BrowserContextConfig, BrowserContextWindowSize
from langchain_openai import ChatOpenAI
import uvicorn
from contextlib import asynccontextmanager
import logging
import logging.handlers
import os
# Configure logging
log_dir = "/var/log/kortix"
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, "kortix_api.log")
logger = logging.getLogger("kortix_api")
logger.setLevel(logging.INFO)
# Create rotating file handler
file_handler = logging.handlers.RotatingFileHandler(
log_file,
maxBytes=10485760, # 10MB
backupCount=5
)
file_handler.setFormatter(
logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
)
logger.addHandler(file_handler)
@asynccontextmanager
async def lifespan(app: FastAPI):
# Initialize browser on startup
global browser
try:
browser = Browser()
logger.info("Browser initialized successfully")
except Exception as e:
logger.error(f"Error initializing browser: {str(e)}")
logger.error(traceback.format_exc())
yield
# Clean up resources at shutdown if needed
app = FastAPI(lifespan=lifespan)
# Global variables to maintain browser state
browser = None
browser_context = None
agent = None
class TaskRequest(BaseModel):
task_description: str
@app.post("/run-task")
async def run_task(request: TaskRequest):
global browser, browser_context, agent
if not browser:
try:
browser = Browser()
except Exception as e:
error_msg = f"Failed to initialize browser: {str(e)}"
logger.error(error_msg)
raise HTTPException(status_code=500, detail=error_msg)
try:
# Create a browser context if it doesn't exist
if not browser_context:
browser_context = await browser.new_context(
config=BrowserContextConfig(
browser_window_size=BrowserContextWindowSize(
width=1280, height=800
),
)
)
logger.info("Created new browser context")
# Create a new agent for each task
agent = Agent(
task=request.task_description,
llm=ChatOpenAI(model="gpt-4o"),
browser=browser,
browser_context=browser_context
)
logger.info(f"Starting task: {request.task_description}")
result = await agent.run()
# Format the history for response
history = []
for h in result.history:
logger.debug(f"Task history entry: {h}")
history.append(str(h))
logger.info("Task completed successfully")
return {
"status": "success",
"history": history
}
except Exception as e:
error_traceback = traceback.format_exc()
logger.error(f"Error during task execution: {str(e)}")
logger.error(error_traceback)
raise HTTPException(
status_code=500,
detail={
"status": "error",
"error": str(e),
"traceback": error_traceback
}
)
@app.get("/health")
async def health_check():
status = {
"status": "healthy",
"browser_initialized": browser is not None,
"context_initialized": browser_context is not None
}
logger.debug(f"Health check: {status}")
return status
if __name__ == "__main__":
logger.info("Starting Kortix API server")
uvicorn.run(app, host="0.0.0.0", port=8000)
'''
2025-04-09 07:37:06 +08:00
sandbox_website_server = b'''
import os
from fastapi import FastAPI, HTTPException, Request
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
import uvicorn
import logging
import logging.handlers
# Configure logging
log_dir = "/var/log/kortix"
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, "website_server.log")
logger = logging.getLogger("website_server")
logger.setLevel(logging.INFO)
# Create rotating file handler
file_handler = logging.handlers.RotatingFileHandler(
log_file,
maxBytes=10485760, # 10MB
backupCount=5
)
file_handler.setFormatter(
logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
)
logger.addHandler(file_handler)
app = FastAPI()
# Create site directory if it doesn't exist
site_dir = "/workspace/site"
os.makedirs(site_dir, exist_ok=True)
# Mount the static files directory
app.mount("/", StaticFiles(directory=site_dir, html=True), name="site")
@app.get("/health")
async def health_check():
status = {
"status": "healthy"
}
logger.debug(f"Health check: {status}")
return status
if __name__ == "__main__":
logger.info("Starting website server")
2025-04-09 20:48:46 +08:00
uvicorn.run(app, host="0.0.0.0", port=8080, reload=True)
2025-04-09 07:37:06 +08:00
'''
2025-04-09 04:09:45 +08:00
def create_sandbox(password: str):
2025-04-10 00:44:17 +08:00
logger.info("Creating new Daytona sandbox environment")
logger.debug("Configuring sandbox with browser-use image and environment variables")
openai_api_key = os.getenv("OPENAI_API_KEY")
if not openai_api_key:
logger.warning("OPENAI_API_KEY not found in environment variables")
else:
logger.debug("OPENAI_API_KEY configured for sandbox")
2025-04-09 04:09:45 +08:00
sandbox = daytona.create(CreateSandboxParams(
image="adamcohenhillel/kortix-browser-use:0.0.1",
env_vars={
"CHROME_PERSISTENT_SESSION": "true",
"RESOLUTION": "1920x1080x24",
"RESOLUTION_WIDTH": "1920",
"RESOLUTION_HEIGHT": "1080",
"VNC_PASSWORD": password,
"OPENAI_ENDPOINT": "https://api.openai.com/v1",
2025-04-10 00:44:17 +08:00
"OPENAI_API_KEY": openai_api_key,
2025-04-09 04:09:45 +08:00
"ANTHROPIC_API_KEY": "",
"ANTHROPIC_ENDPOINT": "https://api.anthropic.com",
"GOOGLE_API_KEY": "",
"AZURE_OPENAI_ENDPOINT": "",
"AZURE_OPENAI_API_KEY": "",
"AZURE_OPENAI_API_VERSION": "2025-01-01-preview",
"DEEPSEEK_ENDPOINT": "https://api.deepseek.com",
"DEEPSEEK_API_KEY": "",
"OLLAMA_ENDPOINT": "http://localhost:11434",
"ANONYMIZED_TELEMETRY": "false",
"BROWSER_USE_LOGGING_LEVEL": "info",
"CHROME_PATH": "",
"CHROME_USER_DATA": "",
"CHROME_DEBUGGING_PORT": "9222",
"CHROME_DEBUGGING_HOST": "localhost",
"CHROME_CDP": ""
},
ports=[
7788, # Gradio default port
6080, # noVNC web interface
5901, # VNC port
9222, # Chrome remote debugging port
8000, # FastAPI port
8080 # HTTP website port
]
))
2025-04-10 00:44:17 +08:00
logger.info(f"Sandbox created with ID: {sandbox.id}")
logger.debug("Uploading browser API script to sandbox")
2025-04-09 07:37:06 +08:00
sandbox.fs.upload_file(sandbox.get_user_root_dir() + "/browser_api.py", sandbox_browser_api)
2025-04-10 00:44:17 +08:00
logger.debug("Uploading website server script to sandbox")
2025-04-09 07:37:06 +08:00
sandbox.fs.upload_file(sandbox.get_user_root_dir() + "/website_server.py", sandbox_website_server)
2025-04-10 00:44:17 +08:00
logger.debug("Creating sandbox browser API session")
2025-04-09 07:37:06 +08:00
sandbox.process.create_session('sandbox_browser_api')
2025-04-10 00:44:17 +08:00
logger.debug("Creating sandbox website server session")
2025-04-09 07:37:06 +08:00
sandbox.process.create_session('sandbox_website_server')
2025-04-10 00:44:17 +08:00
logger.debug("Executing browser API command in sandbox")
2025-04-09 07:37:06 +08:00
rsp = sandbox.process.execute_session_command('sandbox_browser_api', SessionExecuteRequest(
command="python " + sandbox.get_user_root_dir() + "/browser_api.py",
var_async=True
))
2025-04-10 00:44:17 +08:00
logger.debug(f"Browser API command execution result: {rsp}")
logger.debug("Executing website server command in sandbox")
2025-04-09 07:37:06 +08:00
rsp2 = sandbox.process.execute_session_command('sandbox_website_server', SessionExecuteRequest(
command="python " + sandbox.get_user_root_dir() + "/website_server.py",
2025-04-09 04:09:45 +08:00
var_async=True
))
2025-04-10 00:44:17 +08:00
logger.debug(f"Website server command execution result: {rsp2}")
2025-04-09 04:09:45 +08:00
times = 0
success = False
api_url = sandbox.get_preview_link(8000)
2025-04-10 00:44:17 +08:00
logger.info(f"Sandbox API URL: {api_url}")
2025-04-09 04:09:45 +08:00
while times < 10:
times += 1
2025-04-10 00:44:17 +08:00
logger.info(f"Waiting for API to be ready... Attempt {times}/10")
try:
# Make the API call to our FastAPI endpoint
response = requests.get(f"{api_url}/health")
if response.status_code == 200:
logger.info(f"API call completed successfully: {response.status_code}")
success = True
break
else:
logger.warning(f"API health check failed with status code: {response.status_code}")
sleep(1)
except requests.exceptions.RequestException as e:
logger.warning(f"API request error on attempt {times}: {str(e)}")
2025-04-09 04:09:45 +08:00
sleep(1)
if not success:
2025-04-10 00:44:17 +08:00
logger.error("API health check failed after maximum attempts")
raise Exception("API call failed after maximum attempts")
2025-04-09 04:09:45 +08:00
2025-04-10 00:44:17 +08:00
logger.info(f"Sandbox environment successfully initialized")
2025-04-09 04:09:45 +08:00
return sandbox
class SandboxToolsBase(Tool):
"""Tool for executing tasks in a Daytona sandbox with browser-use capabilities."""
def __init__(self, sandbox_id: str, password: str):
super().__init__()
self.sandbox = None
self.daytona = daytona
2025-04-09 20:46:13 +08:00
self.workspace_path = "/workspace"
2025-04-09 04:09:45 +08:00
self.sandbox_id = sandbox_id
2025-04-10 00:44:17 +08:00
logger.info(f"Initializing SandboxToolsBase with sandbox ID: {sandbox_id}")
2025-04-09 04:09:45 +08:00
try:
2025-04-10 00:44:17 +08:00
logger.debug(f"Retrieving sandbox with ID: {sandbox_id}")
2025-04-09 04:09:45 +08:00
self.sandbox = self.daytona.get_current_sandbox(self.sandbox_id)
2025-04-10 00:44:17 +08:00
logger.info(f"Successfully retrieved sandbox: {self.sandbox.id}")
2025-04-09 04:09:45 +08:00
except Exception as e:
2025-04-10 00:44:17 +08:00
logger.error(f"Error retrieving sandbox: {str(e)}", exc_info=True)
2025-04-09 04:09:45 +08:00
raise e
self.api_url = self.sandbox.get_preview_link(8000)
2025-04-10 00:44:17 +08:00
logger.debug(f"Sandbox API URL: {self.api_url}")
# Get and log preview links
vnc_url = self.sandbox.get_preview_link(6080)
website_url = self.sandbox.get_preview_link(8080)
logger.info(f"Sandbox VNC URL: {vnc_url}")
logger.info(f"Sandbox Website URL: {website_url}")
2025-04-09 04:09:45 +08:00
print("\033[95m***")
2025-04-10 00:44:17 +08:00
print(vnc_url)
print(website_url)
2025-04-09 20:46:13 +08:00
print("***\033[0m")
def clean_path(self, path: str) -> str:
2025-04-10 00:44:17 +08:00
cleaned_path = path.replace(self.workspace_path, "").lstrip("/")
logger.debug(f"Cleaned path: {path} -> {cleaned_path}")
return cleaned_path