refactor(api): streamline sandbox handling

- Removed the `get_or_create_project_sandbox` function from `agent/api.py` and replaced its usage with direct sandbox retrieval logic.
- Updated `start_agent` and `ensure_project_sandbox_active` functions to directly access sandbox information from project data.
- Cleaned up unnecessary comments and improved logging for sandbox operations.
- Adjusted frontend form submission to ensure consistent handling of FormData and improved error handling.
This commit is contained in:
marko-kraemer 2025-05-18 03:23:46 +02:00
parent 8e515ba2af
commit a77f687a42
10 changed files with 620139 additions and 107 deletions

View File

@ -241,52 +241,6 @@ async def get_agent_run_with_access_check(client, agent_run_id: str, user_id: st
await verify_thread_access(client, thread_id, user_id)
return agent_run_data
async def get_or_create_project_sandbox(client, project_id: str):
"""Get or create a sandbox for a project."""
project = await client.table('projects').select('*').eq('project_id', project_id).execute()
if not project.data:
raise ValueError(f"Project {project_id} not found")
project_data = project.data[0]
if project_data.get('sandbox', {}).get('id'):
sandbox_id = project_data['sandbox']['id']
sandbox_pass = project_data['sandbox']['pass']
logger.info(f"Project {project_id} already has sandbox {sandbox_id}, retrieving it")
try:
sandbox = await get_or_start_sandbox(sandbox_id)
return sandbox, sandbox_id, sandbox_pass
except Exception as e:
logger.error(f"Failed to retrieve existing sandbox {sandbox_id}: {str(e)}. Creating a new one.")
logger.info(f"Creating new sandbox for project {project_id}")
sandbox_pass = str(uuid.uuid4())
sandbox = create_sandbox(sandbox_pass, project_id)
sandbox_id = sandbox.id
logger.info(f"Created new sandbox {sandbox_id}")
vnc_link = sandbox.get_preview_link(6080)
website_link = sandbox.get_preview_link(8080)
vnc_url = vnc_link.url if hasattr(vnc_link, 'url') else str(vnc_link).split("url='")[1].split("'")[0]
website_url = website_link.url if hasattr(website_link, 'url') else str(website_link).split("url='")[1].split("'")[0]
token = None
if hasattr(vnc_link, 'token'):
token = vnc_link.token
elif "token='" in str(vnc_link):
token = str(vnc_link).split("token='")[1].split("'")[0]
update_result = await client.table('projects').update({
'sandbox': {
'id': sandbox_id, 'pass': sandbox_pass, 'vnc_preview': vnc_url,
'sandbox_url': website_url, 'token': token
}
}).eq('project_id', project_id).execute()
if not update_result.data:
logger.error(f"Failed to update project {project_id} with new sandbox {sandbox_id}")
raise Exception("Database update failed")
return sandbox, sandbox_id, sandbox_pass
@router.post("/thread/{thread_id}/agent/start")
async def start_agent(
thread_id: str,
@ -338,9 +292,21 @@ async def start_agent(
await stop_agent_run(active_run_id)
try:
sandbox, sandbox_id, sandbox_pass = await get_or_create_project_sandbox(client, project_id)
# Get project data to find sandbox ID
project_result = await client.table('projects').select('*').eq('project_id', project_id).execute()
if not project_result.data:
raise HTTPException(status_code=404, detail="Project not found")
project_data = project_result.data[0]
sandbox_info = project_data.get('sandbox', {})
if not sandbox_info.get('id'):
raise HTTPException(status_code=404, detail="No sandbox found for this project")
sandbox_id = sandbox_info['id']
sandbox = await get_or_start_sandbox(sandbox_id)
logger.info(f"Successfully started sandbox {sandbox_id} for project {project_id}")
except Exception as e:
logger.error(f"Failed to get/create sandbox for project {project_id}: {str(e)}")
logger.error(f"Failed to start sandbox for project {project_id}: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to initialize sandbox: {str(e)}")
agent_run = await client.table('agent_runs').insert({
@ -366,7 +332,6 @@ async def start_agent(
stream=body.stream, enable_context_manager=body.enable_context_manager
)
# Set a callback to clean up Redis instance key when task is done
return {"agent_run_id": agent_run_id, "status": "running"}
@router.post("/agent-run/{agent_run_id}/stop")
@ -693,8 +658,33 @@ async def initiate_agent_with_files(
asyncio.create_task(generate_and_update_project_name(project_id=project_id, prompt=prompt))
# 3. Create Sandbox
sandbox, sandbox_id, sandbox_pass = await get_or_create_project_sandbox(client, project_id)
logger.info(f"Using sandbox {sandbox_id} for new project {project_id}")
sandbox_pass = str(uuid.uuid4())
sandbox = create_sandbox(sandbox_pass, project_id)
sandbox_id = sandbox.id
logger.info(f"Created new sandbox {sandbox_id} for project {project_id}")
# Get preview links
vnc_link = sandbox.get_preview_link(6080)
website_link = sandbox.get_preview_link(8080)
vnc_url = vnc_link.url if hasattr(vnc_link, 'url') else str(vnc_link).split("url='")[1].split("'")[0]
website_url = website_link.url if hasattr(website_link, 'url') else str(website_link).split("url='")[1].split("'")[0]
token = None
if hasattr(vnc_link, 'token'):
token = vnc_link.token
elif "token='" in str(vnc_link):
token = str(vnc_link).split("token='")[1].split("'")[0]
# Update project with sandbox info
update_result = await client.table('projects').update({
'sandbox': {
'id': sandbox_id, 'pass': sandbox_pass, 'vnc_preview': vnc_url,
'sandbox_url': website_url, 'token': token
}
}).eq('project_id', project_id).execute()
if not update_result.data:
logger.error(f"Failed to update project {project_id} with new sandbox {sandbox_id}")
raise Exception("Database update failed")
# 4. Upload Files to Sandbox (if any)
message_content = prompt
@ -753,7 +743,6 @@ async def initiate_agent_with_files(
message_content += "\n\nThe following files failed to upload:\n"
for failed_file in failed_uploads: message_content += f"- {failed_file}\n"
# 5. Add initial user message to thread
message_id = str(uuid.uuid4())
message_payload = {"role": "user", "content": message_content}

View File

@ -225,7 +225,6 @@ async def run_agent_background(
logger.info(f"Agent run background task fully completed for: {agent_run_id} (Instance: {instance_id}) with final status: {final_status}")
async def _cleanup_redis_instance_key(agent_run_id: str):
"""Clean up the instance-specific Redis key for an agent run."""
if not instance_id:

View File

@ -10,7 +10,6 @@ from sandbox.sandbox import get_or_start_sandbox
from utils.logger import logger
from utils.auth_utils import get_optional_user_id
from services.supabase import DBConnection
from agent.api import get_or_create_project_sandbox
# Initialize shared resources
router = APIRouter(tags=["sandbox"])
@ -342,9 +341,16 @@ async def ensure_project_sandbox_active(
raise HTTPException(status_code=403, detail="Not authorized to access this project")
try:
# Get or create the sandbox
# Get sandbox ID from project data
sandbox_info = project_data.get('sandbox', {})
if not sandbox_info.get('id'):
raise HTTPException(status_code=404, detail="No sandbox found for this project")
sandbox_id = sandbox_info['id']
# Get or start the sandbox
logger.info(f"Ensuring sandbox is active for project {project_id}")
sandbox, sandbox_id, sandbox_pass = await get_or_create_project_sandbox(client, project_id)
sandbox = await get_or_start_sandbox(sandbox_id)
logger.info(f"Successfully ensured sandbox {sandbox_id} is active for project {project_id}")

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -73,64 +73,36 @@ function DashboardContent() {
const files = chatInputRef.current?.getPendingFiles() || [];
localStorage.removeItem(PENDING_PROMPT_KEY);
if (files.length > 0) {
// ---- Handle submission WITH files ----
console.log(
`Submitting with message: "${message}" and ${files.length} files.`,
);
const formData = new FormData();
// Always use FormData for consistency
const formData = new FormData();
formData.append('prompt', message);
// Use 'prompt' key instead of 'message'
formData.append('prompt', message);
// Append files if present
files.forEach((file, index) => {
formData.append('files', file, file.name);
});
// Append files
files.forEach((file, index) => {
formData.append('files', file, file.name);
});
// Append options
if (options?.model_name) formData.append('model_name', options.model_name);
formData.append('enable_thinking', String(options?.enable_thinking ?? false));
formData.append('reasoning_effort', options?.reasoning_effort ?? 'low');
formData.append('stream', String(options?.stream ?? true));
formData.append('enable_context_manager', String(options?.enable_context_manager ?? false));
// Append options individually instead of bundled 'options' field
if (options?.model_name)
formData.append('model_name', options.model_name);
// Default values from backend signature if not provided in options:
formData.append(
'enable_thinking',
String(options?.enable_thinking ?? false),
);
formData.append('reasoning_effort', options?.reasoning_effort ?? 'low');
formData.append('stream', String(options?.stream ?? true));
formData.append(
'enable_context_manager',
String(options?.enable_context_manager ?? false),
);
console.log('FormData content:', Array.from(formData.entries()));
console.log('FormData content:', Array.from(formData.entries()));
const result = await initiateAgent(formData);
console.log('Agent initiated:', result);
const result = await initiateAgent(formData);
console.log('Agent initiated with files:', result);
if (result.thread_id) {
router.push(`/agents/${result.thread_id}`);
} else {
throw new Error('Agent initiation did not return a thread_id.');
}
chatInputRef.current?.clearPendingFiles();
if (result.thread_id) {
router.push(`/agents/${result.thread_id}`);
} else {
// ---- Handle text-only messages (NO CHANGES NEEDED HERE) ----
console.log(`Submitting text-only message: "${message}"`);
const projectName = await generateThreadName(message);
const newProject = await createProject({
name: projectName,
description: '',
});
const thread = await createThread(newProject.id);
await addUserMessage(thread.thread_id, message);
await startAgent(thread.thread_id, options); // Pass original options here
router.push(`/agents/${thread.thread_id}`);
throw new Error('Agent initiation did not return a thread_id.');
}
chatInputRef.current?.clearPendingFiles();
} catch (error: any) {
console.error('Error during submission process:', error);
if (error instanceof BillingError) {
// Delegate billing error handling
console.log('Handling BillingError:', error.detail);
handleBillingError({
message:
@ -144,16 +116,15 @@ function DashboardContent() {
},
});
setIsSubmitting(false);
return; // Stop further processing for billing errors
return;
}
// Handle other errors
const isConnectionError =
error instanceof TypeError && error.message.includes('Failed to fetch');
if (!isLocalMode() || isConnectionError) {
toast.error(error.message || 'An unexpected error occurred');
}
setIsSubmitting(false); // Reset submitting state on all errors
setIsSubmitting(false);
}
};