mirror of https://github.com/kortix-ai/suna.git
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:
parent
8e515ba2af
commit
a77f687a42
|
@ -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}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue