From 6829fef6994e66fa1957b1798ed1e73c21c8e36e Mon Sep 17 00:00:00 2001 From: marko-kraemer Date: Wed, 23 Apr 2025 05:20:05 +0100 Subject: [PATCH] wip --- backend/agent/api.py | 186 ---------------- .../src/app/(dashboard)/dashboard/page.tsx | 200 +++++++++--------- frontend/src/lib/api.ts | 35 --- 3 files changed, 96 insertions(+), 325 deletions(-) diff --git a/backend/agent/api.py b/backend/agent/api.py index 3aae0bcb..c42ff458 100644 --- a/backend/agent/api.py +++ b/backend/agent/api.py @@ -372,192 +372,6 @@ async def start_agent( return {"agent_run_id": agent_run_id, "status": "running"} -@router.post("/create-and-start") -async def create_project_and_start_agent( - request: Request, - user_id: str = Depends(get_current_user_id) -): - """ - Create a project, thread, and start an agent in a single atomic operation. - This prevents race conditions in sandbox creation. - """ - logger.info(f"Creating project and starting agent for user: {user_id}") - - # Parse request body - body = await request.json() - - # Extract parameters - project_name = body.get("name", "New Project") - project_description = body.get("description", "") - user_message = body.get("message", "") - model_name = body.get("model_name", "anthropic/claude-3-7-sonnet-latest") - enable_thinking = body.get("enable_thinking", False) - reasoning_effort = body.get("reasoning_effort", "low") - stream = body.get("stream", True) - enable_context_manager = body.get("enable_context_manager", False) - - # Connect to the database - client = await db.client - - # Check if the user has access to an account - user_accounts = await client.table('account_users').select('account_id').eq('user_id', user_id).execute() - if not user_accounts.data: - raise HTTPException(status_code=403, detail="User has no account access") - - # Get the user's personal account (or first account if no personal account) - account_id = None - for acc in user_accounts.data: - account_data = await client.table('accounts').select('personal_account').eq('account_id', acc['account_id']).execute() - if account_data.data and account_data.data[0].get('personal_account'): - account_id = acc['account_id'] - break - - if not account_id: - # If no personal account, use first one - account_id = user_accounts.data[0]['account_id'] - - # Check billing status - can_run, message, subscription = await check_billing_status(client, account_id) - if not can_run: - raise HTTPException(status_code=402, detail={ - "message": message, - "subscription": subscription - }) - - try: - # Create the project - project = await client.table('projects').insert({ - "name": project_name, - "description": project_description, - "account_id": account_id, - "created_by": user_id, - "created_at": datetime.now(timezone.utc).isoformat() - }).execute() - - project_id = project.data[0]['project_id'] - logger.info(f"Created new project: {project_id}") - - # Create a sandbox for the project (with race condition protection) - sandbox_pass = str(uuid.uuid4()) - sandbox = create_sandbox(sandbox_pass) - logger.info(f"Created new sandbox for project {project_id} with preview: {sandbox.get_preview_link(6080)}/vnc_lite.html?password={sandbox_pass}") - sandbox_id = sandbox.id - - # Get preview links - vnc_link = sandbox.get_preview_link(6080) - website_link = sandbox.get_preview_link(8080) - - # Extract the actual URLs and token from the preview link objects - 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] - - # Extract token if available - 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] - - # Immediately update the project with sandbox info to avoid race conditions - 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() - - # Create a thread for the project - thread = await client.table('threads').insert({ - "name": project_name, - "project_id": project_id, - "account_id": account_id, - "created_by": user_id, - "created_at": datetime.now(timezone.utc).isoformat() - }).execute() - - thread_id = thread.data[0]['thread_id'] - logger.info(f"Created new thread: {thread_id} for project: {project_id}") - - # Add the user message to the thread - if user_message: - await client.table('messages').insert({ - "thread_id": thread_id, - "role": "user", - "content": user_message, - "created_at": datetime.now(timezone.utc).isoformat() - }).execute() - logger.info(f"Added user message to thread: {thread_id}") - - # Check if there is already an active agent run for this project - active_run_id = await check_for_active_project_agent_run(client, project_id) - - # If there's an active run, stop it first - if active_run_id: - logger.info(f"Stopping existing agent run {active_run_id} before starting new one") - await stop_agent_run(active_run_id) - - # Create a new agent run - agent_run = await client.table('agent_runs').insert({ - "thread_id": thread_id, - "status": "running", - "started_at": datetime.now(timezone.utc).isoformat() - }).execute() - - agent_run_id = agent_run.data[0]['id'] - logger.info(f"Created new agent run: {agent_run_id}") - - # Initialize in-memory storage for this agent run - active_agent_runs[agent_run_id] = [] - - # Register this run in Redis with TTL - try: - await redis.set( - f"active_run:{instance_id}:{agent_run_id}", - "running", - ex=redis.REDIS_KEY_TTL - ) - except Exception as e: - logger.warning(f"Failed to register agent run in Redis, continuing without Redis tracking: {str(e)}") - - # Run the agent in the background - task = asyncio.create_task( - run_agent_background( - agent_run_id=agent_run_id, - thread_id=thread_id, - instance_id=instance_id, - project_id=project_id, - sandbox=sandbox, - model_name=MODEL_NAME_ALIASES.get(model_name, model_name), - enable_thinking=enable_thinking, - reasoning_effort=reasoning_effort, - stream=stream, - enable_context_manager=enable_context_manager - ) - ) - - # Set a callback to clean up when task is done - task.add_done_callback( - lambda _: asyncio.create_task( - _cleanup_agent_run(agent_run_id) - ) - ) - - # Return all the created resources - return { - "project_id": project_id, - "thread_id": thread_id, - "agent_run_id": agent_run_id, - "status": "running" - } - - except Exception as e: - logger.error(f"Error in create_project_and_start_agent: {str(e)}", exc_info=True) - # Re-raise as HTTP exception - raise HTTPException(status_code=500, detail=f"Failed to create project and start agent: {str(e)}") - @router.post("/agent-run/{agent_run_id}/stop") async def stop_agent(agent_run_id: str, user_id: str = Depends(get_current_user_id)): """Stop a running agent.""" diff --git a/frontend/src/app/(dashboard)/dashboard/page.tsx b/frontend/src/app/(dashboard)/dashboard/page.tsx index e8e8ecef..abb5ad54 100644 --- a/frontend/src/app/(dashboard)/dashboard/page.tsx +++ b/frontend/src/app/(dashboard)/dashboard/page.tsx @@ -5,7 +5,7 @@ import { Skeleton } from "@/components/ui/skeleton"; import { useRouter } from 'next/navigation'; import { Menu } from "lucide-react"; import { ChatInput } from '@/components/thread/chat-input'; -import { createProject, addUserMessage, startAgent, createThread, createProjectAndStartAgent } from "@/lib/api"; +import { createProject, addUserMessage, startAgent, createThread } from "@/lib/api"; import { generateThreadName } from "@/lib/actions/threads"; import { useIsMobile } from "@/hooks/use-mobile"; import { useSidebar } from "@/components/ui/sidebar"; @@ -22,7 +22,6 @@ function DashboardContent() { const [inputValue, setInputValue] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const [autoSubmit, setAutoSubmit] = useState(false); - const [isSubmitDisabled, setIsSubmitDisabled] = useState(false); const { billingError, handleBillingError, clearBillingError } = useBillingError(); const router = useRouter(); const isMobile = useIsMobile(); @@ -31,134 +30,128 @@ function DashboardContent() { const personalAccount = accounts?.find(account => account.personal_account); const handleSubmit = async (message: string, options?: { model_name?: string; enable_thinking?: boolean }) => { - if (!message.trim() || isSubmitting || isSubmitDisabled) return; + if (!message.trim() || isSubmitting) return; setIsSubmitting(true); - setIsSubmitDisabled(true); // Disable submit button immediately try { // Generate a name for the project using GPT const projectName = await generateThreadName(message); - // Use the new unified API endpoint to create project, thread, and start agent in one call - const result = await createProjectAndStartAgent({ + // 1. Create a new project with the GPT-generated name + const newAgent = await createProject({ name: projectName, description: "", - message: message.trim(), - model_name: options?.model_name, - enable_thinking: options?.enable_thinking, - stream: true }); - // If successful, clear the pending prompt - localStorage.removeItem(PENDING_PROMPT_KEY); + // 2. Create a new thread for this project + const thread = await createThread(newAgent.id); + + // 3. Add the user message to the thread + await addUserMessage(thread.thread_id, message.trim()); - // Navigate to the new agent's thread page - router.push(`/agents/${result.thread_id}`); - - } catch (error: any) { - // Check specifically for billing errors (402 Payment Required) - if (error.message?.includes('(402)') || error?.status === 402) { - console.log("Billing error detected:", error); + try { + // 4. Start the agent with the thread ID + const agentRun = await startAgent(thread.thread_id, { + model_name: options?.model_name, + enable_thinking: options?.enable_thinking, + stream: true + }); - // Try to extract the error details from the error object - try { - // Try to parse the error.response or the error itself - let errorDetails; + // If successful, clear the pending prompt + localStorage.removeItem(PENDING_PROMPT_KEY); + + // 5. Navigate to the new agent's thread page + router.push(`/agents/${thread.thread_id}`); + } catch (error: any) { + // Check specifically for billing errors (402 Payment Required) + if (error.message?.includes('(402)') || error?.status === 402) { + console.log("Billing error detected:", error); - // First attempt: check if error.data exists and has a detail property - if (error.data?.detail) { - errorDetails = error.data.detail; - console.log("Extracted billing error details from error.data.detail:", errorDetails); - } - // Second attempt: check if error.detail exists directly - else if (error.detail) { - errorDetails = error.detail; - console.log("Extracted billing error details from error.detail:", errorDetails); - } - // Third attempt: try to parse the error text if it's JSON - else if (typeof error.text === 'function') { - const text = await error.text(); - console.log("Extracted error text:", text); - try { - const parsed = JSON.parse(text); - errorDetails = parsed.detail || parsed; - console.log("Parsed error text as JSON:", errorDetails); - } catch (e) { - // Not JSON, use regex to extract info - console.log("Error text is not valid JSON"); + // Try to extract the error details from the error object + try { + // Try to parse the error.response or the error itself + let errorDetails; + + // First attempt: check if error.data exists and has a detail property + if (error.data?.detail) { + errorDetails = error.data.detail; + console.log("Extracted billing error details from error.data.detail:", errorDetails); + } + // Second attempt: check if error.detail exists directly + else if (error.detail) { + errorDetails = error.detail; + console.log("Extracted billing error details from error.detail:", errorDetails); } - } - - // If we still don't have details, try to extract from the error message - if (!errorDetails && error.message) { - const match = error.message.match(/Monthly limit of (\d+) minutes reached/); - if (match) { - const minutes = parseInt(match[1]); - errorDetails = { - message: error.message, - subscription: { - price_id: "price_1RGJ9GG6l1KZGqIroxSqgphC", // Free tier by default - plan_name: "Free", - current_usage: minutes / 60, // Convert to hours - limit: minutes / 60 // Convert to hours - } - }; - console.log("Extracted billing error details from error message:", errorDetails); - } - } - - // Handle the billing error with the details we extracted - if (errorDetails) { - console.log("Handling billing error with extracted details:", errorDetails); - handleBillingError(errorDetails); - } else { - // Fallback with generic billing error - console.log("Using fallback generic billing error"); - handleBillingError({ - message: "You've reached your monthly usage limit. Please upgrade your plan.", - subscription: { - price_id: "price_1RGJ9GG6l1KZGqIroxSqgphC", // Free tier - plan_name: "Free" + // Third attempt: try to parse the error text if it's JSON + else if (typeof error.text === 'function') { + const text = await error.text(); + console.log("Extracted error text:", text); + try { + const parsed = JSON.parse(text); + errorDetails = parsed.detail || parsed; + console.log("Parsed error text as JSON:", errorDetails); + } catch (e) { + // Not JSON, use regex to extract info + console.log("Error text is not valid JSON"); } + } + + // If we still don't have details, try to extract from the error message + if (!errorDetails && error.message) { + const match = error.message.match(/Monthly limit of (\d+) minutes reached/); + if (match) { + const minutes = parseInt(match[1]); + errorDetails = { + message: error.message, + subscription: { + price_id: "price_1RGJ9GG6l1KZGqIroxSqgphC", // Free tier by default + plan_name: "Free", + current_usage: minutes / 60, // Convert to hours + limit: minutes / 60 // Convert to hours + } + }; + console.log("Extracted billing error details from error message:", errorDetails); + } + } + + // Handle the billing error with the details we extracted + if (errorDetails) { + console.log("Handling billing error with extracted details:", errorDetails); + handleBillingError(errorDetails); + } else { + // Fallback with generic billing error + console.log("Using fallback generic billing error"); + handleBillingError({ + message: "You've reached your monthly usage limit. Please upgrade your plan.", + subscription: { + price_id: "price_1RGJ9GG6l1KZGqIroxSqgphC", // Free tier + plan_name: "Free" + } + }); + } + } catch (parseError) { + console.error("Error parsing billing error details:", parseError); + // Fallback with generic error + handleBillingError({ + message: "You've reached your monthly usage limit. Please upgrade your plan." }); } - } catch (parseError) { - console.error("Error parsing billing error details:", parseError); - // Fallback with generic error - handleBillingError({ - message: "You've reached your monthly usage limit. Please upgrade your plan." - }); + + // Don't rethrow - we've handled this error with the billing alert + setIsSubmitting(false); + return; // Exit handleSubmit } - // Don't rethrow - we've handled this error with the billing alert - setIsSubmitting(false); - setIsSubmitDisabled(false); // Re-enable submit button on error - return; // Exit handleSubmit + // Rethrow any non-billing errors + throw error; } - - // Rethrow any non-billing errors - throw error; } catch (error) { console.error("Error creating agent:", error); setIsSubmitting(false); - setIsSubmitDisabled(false); // Re-enable submit button on error } }; - // Reset the submit disabled state after 2 seconds to prevent accidental double submission - useEffect(() => { - let timer: NodeJS.Timeout; - if (isSubmitDisabled) { - timer = setTimeout(() => { - setIsSubmitDisabled(false); - }, 2000); - } - return () => { - if (timer) clearTimeout(timer); - }; - }, [isSubmitDisabled]); - // Check for pending prompt in localStorage on mount useEffect(() => { // Use a small delay to ensure we're fully mounted @@ -257,4 +250,3 @@ export default function DashboardPage() { ); } - diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 6edf1d02..9926c90b 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1039,38 +1039,3 @@ export const getPublicProjects = async (): Promise => { } }; -export async function createProjectAndStartAgent(data: { - name: string; - description: string; - message: string; - model_name?: string; - enable_thinking?: boolean; - reasoning_effort?: string; - stream?: boolean; - enable_context_manager?: boolean; -}) { - const supabase = createClient(); - const { data: { session } } = await supabase.auth.getSession(); - - if (!session?.access_token) { - throw new Error('No access token available'); - } - - const response = await fetch(`${API_URL}/agent/create-and-start`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${session.access_token}`, - }, - body: JSON.stringify(data), - }); - - if (!response.ok) { - const error = await response.text(); - console.error("Error creating project and starting agent:", error); - throw new Error(`Failed to create project and start agent (${response.status}): ${error}`); - } - - return await response.json(); -} -