UNPKG

@hyperbrowser/agent

Version:

Hyperbrowsers Web Agent

467 lines (466 loc) 16.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.HyperAgent = void 0; const openai_1 = require("@langchain/openai"); const uuid_1 = require("uuid"); const types_1 = require("../types"); const actions_1 = require("./actions"); const browser_providers_1 = require("../browser-providers"); const error_1 = require("./error"); const client_1 = require("./mcp/client"); const agent_1 = require("./tools/agent"); class HyperAgent { get currentPage() { if (this._currentPage) { return this.setupPage(this._currentPage); } return null; } set currentPage(page) { this._currentPage = page; } constructor(params = {}) { this.tasks = {}; this.tokenLimit = 128000; this.debug = false; this.actions = [...actions_1.DEFAULT_ACTIONS]; this.browser = null; this.context = null; this._currentPage = null; this._variables = {}; if (!params.llm) { if (process.env.OPENAI_API_KEY) { this.llm = new openai_1.ChatOpenAI({ openAIApiKey: process.env.OPENAI_API_KEY, modelName: "gpt-4o", temperature: 0, }); } else { throw new error_1.HyperagentError("No LLM provider provided", 400); } } else { this.llm = params.llm; } this.browserProviderType = (params.browserProvider ?? "Local"); this.browserProvider = (this.browserProviderType === "Hyperbrowser" ? new browser_providers_1.HyperbrowserProvider({ ...(params.hyperbrowserConfig ?? {}), debug: params.debug, }) : new browser_providers_1.LocalBrowserProvider(params.localConfig)); if (params.customActions) { params.customActions.forEach(this.registerAction, this); } this.debug = params.debug ?? false; } /** * This is just exposed as a utility function. You don't need to call it explicitly. * @returns A reference to the current Playwright browser instance. */ async initBrowser() { if (!this.browser) { this.browser = await this.browserProvider.start(); this.context = await this.browser.newContext({ viewport: null, }); // Inject script to track event listeners await this.context.addInitScript(() => { // TODO: Check this list of events const interactiveEvents = new Set([ "click", "mousedown", "mouseup", "keydown", "keyup", "keypress", "submit", "change", "input", "focus", "blur", ]); // Add more events as needed const originalAddEventListener = Element.prototype.addEventListener; Element.prototype.addEventListener = function (type, listener, options) { if (interactiveEvents.has(type.toLowerCase())) { this.setAttribute("data-has-interactive-listener", "true"); } originalAddEventListener.call(this, type, listener, options); }; }); return this.browser; } return this.browser; } /** * Use this function instead of accessing this.actions directly. * This function configures if there is a need for an output schema as a part of the complete action. * @param outputSchema * @returns */ getActions(outputSchema) { if (outputSchema) { return [ ...this.actions, (0, actions_1.generateCompleteActionWithOutputDefinition)(outputSchema), ]; } else { return [...this.actions, actions_1.CompleteActionDefinition]; } } /** * Get all variables * @returns Record of variables */ getVariables() { return this._variables; } /** * Set a variable * @param key Key of the variable * @param value Value of the variable */ addVariable(variable) { this._variables[variable.key] = variable; } /** * Get a variable * @param key Key of the variable * @returns Value of the variable */ getVariable(key) { return this._variables[key]; } /** * Delete a variable * @param key Key of the variable */ deleteVariable(key) { delete this._variables[key]; } /** * Get all pages in the context * @returns Array of HyperPage objects */ async getPages() { if (!this.browser) { await this.initBrowser(); } if (!this.context) { throw new error_1.HyperagentError("No context found"); } return this.context.pages().map(this.setupPage.bind(this), this); } /** * Create a new page in the context * @returns HyperPage object */ async newPage() { if (!this.browser) { await this.initBrowser(); } if (!this.context) { throw new error_1.HyperagentError("No context found"); } const page = await this.context.newPage(); return this.setupPage(page); } /** * Close the agent and all associated resources */ async closeAgent() { for (const taskId in this.tasks) { const task = this.tasks[taskId]; if (!types_1.endTaskStatuses.has(task.status)) { task.status = types_1.TaskStatus.CANCELLED; } } if (this.mcpClient) { await this.mcpClient.disconnect(); this.mcpClient = undefined; } if (this.browser) { await this.browserProvider.close(); this.browser = null; this.context = null; } } /** * Get the current page or create a new one if none exists * @returns The current page */ async getCurrentPage() { if (!this.browser) { await this.initBrowser(); } if (!this.context) { throw new error_1.HyperagentError("No context found"); } if (!this.currentPage || this.currentPage.isClosed()) { this._currentPage = await this.context.newPage(); return this.setupPage(this._currentPage); } return this.currentPage; } /** * Get task control object for a specific task * @param taskId ID of the task * @returns Task control object */ getTaskControl(taskId) { const taskState = this.tasks[taskId]; if (!taskState) { throw new error_1.HyperagentError(`Task ${taskId} not found`); } return { getStatus: () => taskState.status, pause: () => { if (taskState.status === types_1.TaskStatus.RUNNING) { taskState.status = types_1.TaskStatus.PAUSED; } return taskState.status; }, resume: () => { if (taskState.status === types_1.TaskStatus.PAUSED) { taskState.status = types_1.TaskStatus.RUNNING; } return taskState.status; }, cancel: () => { if (taskState.status !== types_1.TaskStatus.COMPLETED) { taskState.status = types_1.TaskStatus.CANCELLED; } return taskState.status; }, }; } /** * Execute a task asynchronously and return a Task control object * @param task The task to execute * @param params Optional parameters for the task * @param initPage Optional page to use for the task * @returns A promise that resolves to a Task control object for managing the running task */ async executeTaskAsync(task, params, initPage) { const taskId = (0, uuid_1.v4)(); const page = initPage || (await this.getCurrentPage()); const taskState = { id: taskId, task: task, status: types_1.TaskStatus.PENDING, startingPage: page, steps: [], }; this.tasks[taskId] = taskState; (0, agent_1.runAgentTask)({ llm: this.llm, actions: this.getActions(params?.outputSchema), tokenLimit: this.tokenLimit, debug: this.debug, mcpClient: this.mcpClient, variables: this._variables, }, taskState, params).catch((error) => { taskState.status = types_1.TaskStatus.FAILED; taskState.error = error.message; }); return this.getTaskControl(taskId); } /** * Execute a task and wait for completion * @param task The task to execute * @param params Optional parameters for the task * @param initPage Optional page to use for the task * @returns A promise that resolves to the task output */ async executeTask(task, params, initPage) { const taskId = (0, uuid_1.v4)(); const page = initPage || (await this.getCurrentPage()); const taskState = { id: taskId, task: task, status: types_1.TaskStatus.PENDING, startingPage: page, steps: [], }; this.tasks[taskId] = taskState; try { return await (0, agent_1.runAgentTask)({ llm: this.llm, actions: this.getActions(params?.outputSchema), tokenLimit: this.tokenLimit, debug: this.debug, mcpClient: this.mcpClient, variables: this._variables, }, taskState, params); } catch (error) { taskState.status = types_1.TaskStatus.FAILED; throw error; } } /** * Register a new action with the agent * @param action The action to register */ async registerAction(action) { if (action.type === "complete") { throw new error_1.HyperagentError("Could not add an action with the name 'complete'. Complete is a reserved action.", 400); } const actionsList = new Set(this.actions.map((registeredAction) => registeredAction.type)); if (actionsList.has(action.type)) { throw new Error(`Could not register action of type ${action.type}. Action with the same name is already registered`); } else { this.actions.push(action); } } /** * Initialize the MCP client with the given configuration * @param config The MCP configuration */ async initializeMCPClient(config) { if (!config || config.servers.length === 0) { return; } this.mcpClient = new client_1.MCPClient(this.debug); try { for (const serverConfig of config.servers) { try { const { serverId, actions } = await this.mcpClient.connectToServer(serverConfig); for (const action of actions) { this.registerAction(action); } console.log(`MCP server ${serverId} initialized successfully`); } catch (error) { console.error(`Failed to initialize MCP server ${serverConfig.id || "unknown"}:`, error); } } const serverIds = this.mcpClient.getServerIds(); console.log(`Successfully connected to ${serverIds.length} MCP servers`); } catch (error) { console.error("Failed to initialize MCP client:", error); this.mcpClient = undefined; } } /** * Connect to an MCP server at runtime * @param serverConfig Configuration for the MCP server * @returns Server ID if connection was successful */ async connectToMCPServer(serverConfig) { if (!this.mcpClient) { this.mcpClient = new client_1.MCPClient(this.debug); } try { const { serverId, actions } = await this.mcpClient.connectToServer(serverConfig); // Register the actions from this server for (const action of actions) { this.registerAction(action); } console.log(`Connected to MCP server with ID: ${serverId}`); return serverId; } catch (error) { console.error(`Failed to connect to MCP server:`, error); return null; } } /** * Disconnect from a specific MCP server * @param serverId ID of the server to disconnect from * @returns Boolean indicating if the disconnection was successful */ disconnectFromMCPServer(serverId) { if (!this.mcpClient) { return false; } try { this.mcpClient.disconnectServer(serverId); return true; } catch (error) { console.error(`Failed to disconnect from MCP server ${serverId}:`, error); return false; } } /** * Check if a specific MCP server is connected * @param serverId ID of the server to check * @returns Boolean indicating if the server is connected */ isMCPServerConnected(serverId) { if (!this.mcpClient) { return false; } return this.mcpClient.getServerIds().includes(serverId); } /** * Get all connected MCP server IDs * @returns Array of server IDs */ getMCPServerIds() { if (!this.mcpClient) { return []; } return this.mcpClient.getServerIds(); } /** * Get information about all connected MCP servers * @returns Array of server information objects or null if no MCP client is initialized */ getMCPServerInfo() { if (!this.mcpClient) { return null; } return this.mcpClient.getServerInfo(); } /** * Pretty print an action * @param action The action to print * @returns Formatted string representation of the action */ pprintAction(action) { const foundAction = this.actions.find((actions) => actions.type === action.type); if (foundAction && foundAction.pprintAction) { return foundAction.pprintAction(action.params); } return ""; } getSession() { const session = this.browserProvider.getSession(); if (!session) { return null; } return session; } setupPage(page) { const hyperPage = page; hyperPage.ai = (task, params) => this.executeTask(task, params, page); hyperPage.aiAsync = (task, params) => this.executeTaskAsync(task, params, page); hyperPage.extract = async (task, outputSchema) => { if (!task && !outputSchema) { throw new error_1.HyperagentError("No task description or output schema specified", 400); } if (task) { const res = await this.executeTask(`You have to perform an extraction on the current page. You have to perform the extraction according to the task: ${task}. Make sure your final response only contains the extracted content`, { maxSteps: 2, outputSchema, }, page); if (outputSchema) { return JSON.parse(res.output); } return res.output; } else { const res = await this.executeTask("You have to perform a data extraction on the current page. Make sure your final response only contains the extracted content", { maxSteps: 2, outputSchema }, page); return JSON.parse(res.output); } }; return hyperPage; } } exports.HyperAgent = HyperAgent;