diff --git a/apps/sim/blocks/blocks/browser_use.ts b/apps/sim/blocks/blocks/browser_use.ts index d54628fb7..b9f364e2b 100644 --- a/apps/sim/blocks/blocks/browser_use.ts +++ b/apps/sim/blocks/blocks/browser_use.ts @@ -57,6 +57,12 @@ export const BrowserUseBlock: BlockConfig = { type: 'switch', placeholder: 'Save browser data', }, + { + id: 'profile_id', + title: 'Profile ID', + type: 'short-input', + placeholder: 'Enter browser profile ID (optional)', + }, { id: 'apiKey', title: 'API Key', @@ -75,6 +81,7 @@ export const BrowserUseBlock: BlockConfig = { variables: { type: 'json', description: 'Task variables' }, model: { type: 'string', description: 'AI model to use' }, save_browser_data: { type: 'boolean', description: 'Save browser data' }, + profile_id: { type: 'string', description: 'Browser profile ID for persistent sessions' }, }, outputs: { id: { type: 'string', description: 'Task execution identifier' }, diff --git a/apps/sim/tools/browser_use/run_task.ts b/apps/sim/tools/browser_use/run_task.ts index dff20bd12..e5a6f5381 100644 --- a/apps/sim/tools/browser_use/run_task.ts +++ b/apps/sim/tools/browser_use/run_task.ts @@ -1,11 +1,214 @@ import { createLogger } from '@sim/logger' import type { BrowserUseRunTaskParams, BrowserUseRunTaskResponse } from '@/tools/browser_use/types' -import type { ToolConfig } from '@/tools/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' const logger = createLogger('BrowserUseTool') -const POLL_INTERVAL_MS = 5000 // 5 seconds between polls -const MAX_POLL_TIME_MS = 180000 // 3 minutes maximum polling time +const POLL_INTERVAL_MS = 5000 +const MAX_POLL_TIME_MS = 180000 +const MAX_CONSECUTIVE_ERRORS = 3 + +async function createSessionWithProfile( + profileId: string, + apiKey: string +): Promise<{ sessionId: string } | { error: string }> { + try { + const response = await fetch('https://api.browser-use.com/api/v2/sessions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Browser-Use-API-Key': apiKey, + }, + body: JSON.stringify({ + profileId: profileId.trim(), + }), + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error(`Failed to create session with profile: ${errorText}`) + return { error: `Failed to create session with profile: ${response.statusText}` } + } + + const data = (await response.json()) as { id: string } + logger.info(`Created session ${data.id} with profile ${profileId}`) + return { sessionId: data.id } + } catch (error: any) { + logger.error('Error creating session with profile:', error) + return { error: `Error creating session: ${error.message}` } + } +} + +async function stopSession(sessionId: string, apiKey: string): Promise { + try { + const response = await fetch(`https://api.browser-use.com/api/v2/sessions/${sessionId}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'X-Browser-Use-API-Key': apiKey, + }, + body: JSON.stringify({ action: 'stop' }), + }) + + if (response.ok) { + logger.info(`Stopped session ${sessionId}`) + } else { + logger.warn(`Failed to stop session ${sessionId}: ${response.statusText}`) + } + } catch (error: any) { + logger.warn(`Error stopping session ${sessionId}:`, error) + } +} + +function buildRequestBody( + params: BrowserUseRunTaskParams, + sessionId?: string +): Record { + const requestBody: Record = { + task: params.task, + } + + if (sessionId) { + requestBody.sessionId = sessionId + logger.info(`Using session ${sessionId} for task`) + } + + if (params.variables) { + let secrets: Record = {} + + if (Array.isArray(params.variables)) { + logger.info('Converting variables array to dictionary format') + params.variables.forEach((row: any) => { + if (row.cells?.Key && row.cells.Value !== undefined) { + secrets[row.cells.Key] = row.cells.Value + logger.info(`Added secret for key: ${row.cells.Key}`) + } else if (row.Key && row.Value !== undefined) { + secrets[row.Key] = row.Value + logger.info(`Added secret for key: ${row.Key}`) + } + }) + } else if (typeof params.variables === 'object' && params.variables !== null) { + logger.info('Using variables object directly') + secrets = params.variables + } + + if (Object.keys(secrets).length > 0) { + logger.info(`Found ${Object.keys(secrets).length} secrets to include`) + requestBody.secrets = secrets + } else { + logger.warn('No usable secrets found in variables') + } + } + + if (params.model) { + requestBody.llm_model = params.model + } + + if (params.save_browser_data) { + requestBody.save_browser_data = params.save_browser_data + } + + requestBody.use_adblock = true + requestBody.highlight_elements = true + + return requestBody +} + +async function fetchTaskStatus( + taskId: string, + apiKey: string +): Promise<{ ok: true; data: any } | { ok: false; error: string }> { + try { + const response = await fetch(`https://api.browser-use.com/api/v2/tasks/${taskId}`, { + method: 'GET', + headers: { + 'X-Browser-Use-API-Key': apiKey, + }, + }) + + if (!response.ok) { + return { ok: false, error: `HTTP ${response.status}: ${response.statusText}` } + } + + const data = await response.json() + return { ok: true, data } + } catch (error: any) { + return { ok: false, error: error.message || 'Network error' } + } +} + +async function pollForCompletion( + taskId: string, + apiKey: string +): Promise<{ success: boolean; output: any; steps: any[]; error?: string }> { + let liveUrlLogged = false + let consecutiveErrors = 0 + const startTime = Date.now() + + while (Date.now() - startTime < MAX_POLL_TIME_MS) { + const result = await fetchTaskStatus(taskId, apiKey) + + if (!result.ok) { + consecutiveErrors++ + logger.warn( + `Error polling task ${taskId} (attempt ${consecutiveErrors}/${MAX_CONSECUTIVE_ERRORS}): ${result.error}` + ) + + if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { + logger.error(`Max consecutive errors reached for task ${taskId}`) + return { + success: false, + output: null, + steps: [], + error: `Failed to poll task status after ${MAX_CONSECUTIVE_ERRORS} attempts: ${result.error}`, + } + } + + await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)) + continue + } + + consecutiveErrors = 0 + const taskData = result.data + const status = taskData.status + + logger.info(`BrowserUse task ${taskId} status: ${status}`) + + if (['finished', 'failed', 'stopped'].includes(status)) { + return { + success: status === 'finished', + output: taskData.output ?? null, + steps: taskData.steps || [], + } + } + + if (!liveUrlLogged && taskData.live_url) { + logger.info(`BrowserUse task ${taskId} live URL: ${taskData.live_url}`) + liveUrlLogged = true + } + + await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)) + } + + const finalResult = await fetchTaskStatus(taskId, apiKey) + if (finalResult.ok && ['finished', 'failed', 'stopped'].includes(finalResult.data.status)) { + return { + success: finalResult.data.status === 'finished', + output: finalResult.data.output ?? null, + steps: finalResult.data.steps || [], + } + } + + logger.warn( + `Task ${taskId} did not complete within the maximum polling time (${MAX_POLL_TIME_MS / 1000}s)` + ) + return { + success: false, + output: null, + steps: [], + error: `Task did not complete within the maximum polling time (${MAX_POLL_TIME_MS / 1000}s)`, + } +} export const runTaskTool: ToolConfig = { id: 'browser_use_run_task', @@ -44,7 +247,14 @@ export const runTaskTool: ToolConfig { - const requestBody: Record = { - task: params.task, - } - - if (params.variables) { - let secrets: Record = {} - - if (Array.isArray(params.variables)) { - logger.info('Converting variables array to dictionary format') - params.variables.forEach((row) => { - if (row.cells?.Key && row.cells.Value !== undefined) { - secrets[row.cells.Key] = row.cells.Value - logger.info(`Added secret for key: ${row.cells.Key}`) - } else if (row.Key && row.Value !== undefined) { - secrets[row.Key] = row.Value - logger.info(`Added secret for key: ${row.Key}`) - } - }) - } else if (typeof params.variables === 'object' && params.variables !== null) { - logger.info('Using variables object directly') - secrets = params.variables - } - - if (Object.keys(secrets).length > 0) { - logger.info(`Found ${Object.keys(secrets).length} secrets to include`) - requestBody.secrets = secrets - } else { - logger.warn('No usable secrets found in variables') - } - } - - if (params.model) { - requestBody.llm_model = params.model - } - - if (params.save_browser_data) { - requestBody.save_browser_data = params.save_browser_data - } - - requestBody.use_adblock = true - requestBody.highlight_elements = true - - return requestBody - }, }, - transformResponse: async (response: Response) => { - const data = (await response.json()) as { id: string } - return { - success: true, - output: { - id: data.id, - success: true, - output: null, - steps: [], - }, - } - }, + directExecution: async (params: BrowserUseRunTaskParams): Promise => { + let sessionId: string | undefined - postProcess: async (result, params) => { - if (!result.success) { - return result + if (params.profile_id) { + logger.info(`Creating session with profile ID: ${params.profile_id}`) + const sessionResult = await createSessionWithProfile(params.profile_id, params.apiKey) + if ('error' in sessionResult) { + return { + success: false, + output: { + id: null, + success: false, + output: null, + steps: [], + }, + error: sessionResult.error, + } + } + sessionId = sessionResult.sessionId } - const taskId = result.output.id - let liveUrlLogged = false + const requestBody = buildRequestBody(params, sessionId) + logger.info('Creating BrowserUse task', { hasSession: !!sessionId }) try { - const initialTaskResponse = await fetch( - `https://api.browser-use.com/api/v2/tasks/${taskId}`, - { - method: 'GET', - headers: { - 'X-Browser-Use-API-Key': params.apiKey, - }, - } - ) - - if (initialTaskResponse.ok) { - const initialTaskData = await initialTaskResponse.json() - if (initialTaskData.live_url) { - logger.info( - `BrowserUse task ${taskId} launched with live URL: ${initialTaskData.live_url}` - ) - liveUrlLogged = true - } - } - } catch (error) { - logger.warn(`Failed to get initial task details for ${taskId}:`, error) - } - - let elapsedTime = 0 - - while (elapsedTime < MAX_POLL_TIME_MS) { - try { - const statusResponse = await fetch(`https://api.browser-use.com/api/v2/tasks/${taskId}`, { - method: 'GET', - headers: { - 'X-Browser-Use-API-Key': params.apiKey, - }, - }) - - if (!statusResponse.ok) { - throw new Error(`Failed to get task status: ${statusResponse.statusText}`) - } - - const taskData = await statusResponse.json() - const status = taskData.status - - logger.info(`BrowserUse task ${taskId} status: ${status}`) - - if (['finished', 'failed', 'stopped'].includes(status)) { - result.output = { - id: taskId, - success: status === 'finished', - output: taskData.output ?? null, - steps: taskData.steps || [], - } - - return result - } - - if (!liveUrlLogged && status === 'running' && taskData.live_url) { - logger.info(`BrowserUse task ${taskId} running with live URL: ${taskData.live_url}`) - liveUrlLogged = true - } - - await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)) - elapsedTime += POLL_INTERVAL_MS - } catch (error: any) { - logger.error('Error polling for task status:', { - message: error.message || 'Unknown error', - taskId, - }) + const response = await fetch('https://api.browser-use.com/api/v2/tasks', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Browser-Use-API-Key': params.apiKey, + }, + body: JSON.stringify(requestBody), + }) + if (!response.ok) { + const errorText = await response.text() + logger.error(`Failed to create task: ${errorText}`) return { - ...result, - error: `Error polling for task status: ${error.message || 'Unknown error'}`, + success: false, + output: { + id: null, + success: false, + output: null, + steps: [], + }, + error: `Failed to create task: ${response.statusText}`, } } - } - logger.warn( - `Task ${taskId} did not complete within the maximum polling time (${MAX_POLL_TIME_MS / 1000}s)` - ) - return { - ...result, - error: `Task did not complete within the maximum polling time (${MAX_POLL_TIME_MS / 1000}s)`, + const data = (await response.json()) as { id: string } + const taskId = data.id + logger.info(`Created BrowserUse task: ${taskId}`) + + const result = await pollForCompletion(taskId, params.apiKey) + + if (sessionId) { + await stopSession(sessionId, params.apiKey) + } + + return { + success: result.success && !result.error, + output: { + id: taskId, + success: result.success, + output: result.output, + steps: result.steps, + }, + error: result.error, + } + } catch (error: any) { + logger.error('Error creating BrowserUse task:', error) + + if (sessionId) { + await stopSession(sessionId, params.apiKey) + } + + return { + success: false, + output: { + id: null, + success: false, + output: null, + steps: [], + }, + error: `Error creating task: ${error.message}`, + } } }, diff --git a/apps/sim/tools/browser_use/types.ts b/apps/sim/tools/browser_use/types.ts index 293bcbfa7..f38c4524e 100644 --- a/apps/sim/tools/browser_use/types.ts +++ b/apps/sim/tools/browser_use/types.ts @@ -6,6 +6,7 @@ export interface BrowserUseRunTaskParams { variables?: Record model?: string save_browser_data?: boolean + profile_id?: string } export interface BrowserUseTaskStep {