@hyperbrowser/agent
Version:
Hyperbrowsers Web Agent
467 lines (466 loc) • 16.3 kB
JavaScript
"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;