puppeteer-browser-agent
Version:
Puppeteer browser agent for use in AI agent systems
184 lines (152 loc) • 4.19 kB
text/typescript
import { TimeoutError, type Page } from "puppeteer";
import type { IAgentDriver } from "../agent-driver";
import type { IDriverAction } from "./base-action";
import { PageNavigator, type IPageNavigator } from "../document";
import { ElementAction } from "./element-action";
import { clickLink } from "./definitions/click-link";
export interface IClickLinkAction extends IDriverAction {}
export class ClickLinkAction extends ElementAction implements IClickLinkAction {
message = "";
link?: HTMLAnchorElement;
linkId?: string;
linkText?: string;
userMsg?: string;
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
interactiveElements: any;
url?: string;
requestCount = 0;
responseCount = 0;
downloadStarted = false;
navigator: IPageNavigator;
name = "click_link";
definition = clickLink;
constructor(
driver: IAgentDriver,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
interactiveElements: any[],
) {
super(driver);
this.interactiveElements = interactiveElements;
this.navigator = this.createNavigator();
}
protected updateState() {
this.linkId = this.fnArgs.pp_id;
this.linkText = this.fnArgs.text;
}
protected createNavigator() {
return new PageNavigator(this.driver);
}
protected missingLinkId() {
if (this.linkId) {
return;
}
this.message = "ERROR: Missing parameter pp_id";
}
protected missingLinkText() {
if (this.linkText) {
return;
}
this.message = "";
this.context.pop();
this.userMsg =
"Please the correct link on the page. Remember to set both the text and the pp_id parameter.";
}
protected findLink() {
return this.interactiveElements.find(
(elem: Element) => elem && elem.id === this.linkId,
);
}
protected get page() {
return this.driver.page;
}
protected onStart(linkText?: string) {
this.logTask(`Clicking link "${linkText}"`);
}
protected initAction() {
this.onStart(this.link?.text);
this.requestCount = 0;
this.responseCount = 0;
this.downloadStarted = false;
if (!this.page?.$(this.linkElementSelector)) {
throw new Error("Element not found");
}
}
protected get linkElementSelector() {
if (!this.linkId) {
throw new Error("Missing linkId");
}
return this.markerClass(this.linkId);
}
action() {
this.link = this.findLink();
try {
this.clickLink();
} catch (error) {
this.log(error);
if (error instanceof TimeoutError) {
return this.onTimeOutError(this.link);
}
this.onError(this.link);
}
}
onTimeOutError(_link?: HTMLAnchorElement) {
this.setMessage("NOTICE: The click did not cause a navigation.");
}
onError(link: any) {
const linkText = link ? link.text : "";
const message = `Sorry, but link number ${this.linkId} (${linkText}) is not clickable, please select another link or another command. You can also try to go to the link URL directly with "goto_url".`;
this.setMessage(message);
}
async clickLink() {
this.validatePage();
this.clickOnPage();
await this.waitForNavigation();
this.url = await this.page?.url();
this.onDownloadStarted() || this.onLinkNavigation();
}
clickOnPage() {
this.page?.click(this.linkElementSelector);
}
async waitForNavigation() {
await this.navigator.waitForNavigation;
}
onLinkNavigation() {
const message = `Link clicked! You are now on ${this.url}`;
this.setMessage(message);
}
onDownloadStarted() {
if (!this.downloadStarted) {
return;
}
this.downloadStarted = false;
this.noContent = true;
const message = "Link clicked and file download started successfully!";
this.setMessage(message);
return true;
}
set noContent(val: boolean) {
this.driver.setNoContent(val);
}
get noContent() {
return this.driver.noContent;
}
async execute() {
this.missingLinkId();
this.missingLinkText();
return await this.scrapePage();
}
validatePage() {
if (!this.page) {
throw new Error("Missing page to scrape");
}
}
async scrapePage() {
this.validatePage();
this.logTask("Scraping page...");
this.interactiveElements =
this.page && (await this.getElementsOn(this.page));
}
getElementsOn(page: Page) {
return this.elementSelector?.getElements(page);
}
}