TypeScript SDK: skeleton (#4236)

This commit is contained in:
Stanislav Novosad
2025-12-10 11:12:47 -07:00
committed by GitHub
parent 86ec31f556
commit 2be36c7738
14 changed files with 1240 additions and 17 deletions

View File

@@ -1,5 +1,7 @@
export * as Skyvern from "./api/index.js";
export * as SkyvernApi from "./api/index.js";
export type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js";
export { SkyvernClient } from "./Client.js";
export { SkyvernEnvironment } from "./environments.js";
export { SkyvernError, SkyvernTimeoutError } from "./errors/index.js";
export { Skyvern, SkyvernBrowser, SkyvernBrowserPageAgent, SkyvernBrowserPageAi } from "./library/index.js";
export type { SkyvernOptions, SkyvernBrowserPage } from "./library/index.js";

View File

@@ -0,0 +1,279 @@
import { chromium } from "playwright";
import type * as SkyvernApi from "../api/index.js";
import type { BaseClientOptions } from "../BaseClient.js";
import { SkyvernClient } from "../Client.js";
import { SkyvernEnvironment } from "../environments.js";
import { SkyvernBrowser } from "./SkyvernBrowser.js";
import type { GetRunResponse, ProxyLocation } from "../api/index.js";
import { LOG } from "./logger.js";
import * as core from "../core/index.js";
export interface SkyvernOptions extends BaseClientOptions {
apiKey: string;
}
export interface RunTaskOptions extends SkyvernApi.RunTaskRequest {
waitForCompletion?: boolean;
timeout?: number;
}
export interface RunWorkflowOptions extends SkyvernApi.RunWorkflowRequest {
waitForCompletion?: boolean;
timeout?: number;
}
export interface LoginOptions extends SkyvernApi.LoginRequest {
waitForCompletion?: boolean;
timeout?: number;
}
export interface DownloadFilesOptions extends SkyvernApi.DownloadFilesRequest {
waitForCompletion?: boolean;
timeout?: number;
}
export class Skyvern extends SkyvernClient {
private readonly _apiKey: string;
private readonly _environment: SkyvernEnvironment | string;
private readonly _browsers: Set<SkyvernBrowser> = new Set();
constructor(options: SkyvernOptions) {
super({
...options,
environment: options.environment ?? SkyvernEnvironment.Cloud,
});
this._apiKey = options.apiKey;
this._environment = (options.environment ?? SkyvernEnvironment.Cloud) as SkyvernEnvironment | string;
}
get environment(): SkyvernEnvironment | string {
return this._environment;
}
runTask(
request: RunTaskOptions,
requestOptions?: SkyvernClient.RequestOptions,
): core.HttpResponsePromise<SkyvernApi.TaskRunResponse> {
return core.HttpResponsePromise.fromPromise(this.__runTaskWithCompletion(request, requestOptions));
}
private async __runTaskWithCompletion(
request: RunTaskOptions,
requestOptions?: SkyvernClient.RequestOptions,
): Promise<core.WithRawResponse<SkyvernApi.TaskRunResponse>> {
const { waitForCompletion, timeout, ...taskRequest } = request;
const response = await super.runTask(taskRequest, requestOptions).withRawResponse();
if (waitForCompletion) {
const completedRun = await this._waitForRunCompletion(
response.data.run_id,
timeout ?? 1800,
) as SkyvernApi.TaskRunResponse;
return { data: completedRun, rawResponse: response.rawResponse };
}
return response;
}
runWorkflow(
request: RunWorkflowOptions,
requestOptions?: SkyvernClient.RequestOptions,
): core.HttpResponsePromise<SkyvernApi.WorkflowRunResponse> {
return core.HttpResponsePromise.fromPromise(this.__runWorkflowWithCompletion(request, requestOptions));
}
private async __runWorkflowWithCompletion(
request: RunWorkflowOptions,
requestOptions?: SkyvernClient.RequestOptions,
): Promise<core.WithRawResponse<SkyvernApi.WorkflowRunResponse>> {
const { waitForCompletion, timeout, ...workflowRequest } = request;
const response = await super.runWorkflow(workflowRequest, requestOptions).withRawResponse();
if (waitForCompletion) {
const completedRun = await this._waitForRunCompletion(
response.data.run_id,
timeout ?? 1800,
) as SkyvernApi.WorkflowRunResponse;
return { data: completedRun, rawResponse: response.rawResponse };
}
return response;
}
login(
request: LoginOptions,
requestOptions?: SkyvernClient.RequestOptions,
): core.HttpResponsePromise<SkyvernApi.WorkflowRunResponse> {
return core.HttpResponsePromise.fromPromise(this.__loginWithCompletion(request, requestOptions));
}
private async __loginWithCompletion(
request: LoginOptions,
requestOptions?: SkyvernClient.RequestOptions,
): Promise<core.WithRawResponse<SkyvernApi.WorkflowRunResponse>> {
const { waitForCompletion, timeout, ...loginRequest } = request;
const response = await super.login(loginRequest, requestOptions).withRawResponse();
if (waitForCompletion) {
const completedRun = await this._waitForRunCompletion(
response.data.run_id,
timeout ?? 1800,
) as SkyvernApi.WorkflowRunResponse;
return { data: completedRun, rawResponse: response.rawResponse };
}
return response;
}
downloadFiles(
request: DownloadFilesOptions,
requestOptions?: SkyvernClient.RequestOptions,
): core.HttpResponsePromise<SkyvernApi.WorkflowRunResponse> {
return core.HttpResponsePromise.fromPromise(this.__downloadFilesWithCompletion(request, requestOptions));
}
private async __downloadFilesWithCompletion(
request: DownloadFilesOptions,
requestOptions?: SkyvernClient.RequestOptions,
): Promise<core.WithRawResponse<SkyvernApi.WorkflowRunResponse>> {
const { waitForCompletion, timeout, ...downloadFilesRequest } = request;
const response = await super.downloadFiles(downloadFilesRequest, requestOptions).withRawResponse();
if (waitForCompletion) {
const completedRun = await this._waitForRunCompletion(
response.data.run_id,
timeout ?? 1800,
) as SkyvernApi.WorkflowRunResponse;
return { data: completedRun, rawResponse: response.rawResponse };
}
return response;
}
async launchCloudBrowser(options?: {
timeout?: number;
proxyLocation?: SkyvernApi.ProxyLocation;
}): Promise<SkyvernBrowser> {
this._ensureCloudEnvironment();
const browserSession = await this.createBrowserSession({
timeout: options?.timeout,
proxy_location: options?.proxyLocation,
});
LOG.info("Launched new cloud browser session", { browser_session_id: browserSession.browser_session_id });
return this._connectToCloudBrowserSession(browserSession);
}
async connectToCloudBrowserSession(browserSessionId: string): Promise<SkyvernBrowser> {
this._ensureCloudEnvironment();
const browserSession = await this.getBrowserSession(browserSessionId);
LOG.info("Connecting to existing cloud browser session", { browser_session_id: browserSession.browser_session_id });
return this._connectToCloudBrowserSession(browserSession);
}
async useCloudBrowser(options?: { timeout?: number; proxyLocation?: ProxyLocation }): Promise<SkyvernBrowser> {
this._ensureCloudEnvironment();
const browserSessions = await this.getBrowserSessions();
const browserSession = browserSessions
.filter((s) => s.runnable_id == null)
.sort((a, b) => {
const aTime = a.started_at ? new Date(a.started_at).getTime() : 0;
const bTime = b.started_at ? new Date(b.started_at).getTime() : 0;
return bTime - aTime;
})[0];
if (!browserSession) {
LOG.info("No existing cloud browser session found, launching a new session");
return this.launchCloudBrowser(options);
}
LOG.info("Reusing existing cloud browser session", { browser_session_id: browserSession.browser_session_id });
return this._connectToCloudBrowserSession(browserSession);
}
async connectToBrowserOverCdp(cdpUrl: string): Promise<SkyvernBrowser> {
const browser = await chromium.connectOverCDP(cdpUrl);
const browserContext = browser.contexts()[0] ?? (await browser.newContext());
const skyvernBrowser = new SkyvernBrowser(this, browserContext, { browser, browserAddress: cdpUrl });
this._browsers.add(skyvernBrowser);
return skyvernBrowser;
}
async close(): Promise<void> {
await Promise.all(Array.from(this._browsers).map((browser) => browser.close()));
this._browsers.clear();
}
_untrackBrowser(browser: SkyvernBrowser): void {
this._browsers.delete(browser);
}
private _ensureCloudEnvironment(): void {
if (this._environment !== SkyvernEnvironment.Cloud && this._environment !== SkyvernEnvironment.Staging) {
throw new Error("Cloud browser sessions are supported only in the cloud environment");
}
}
private async _connectToCloudBrowserSession(
browserSession: SkyvernApi.BrowserSessionResponse,
): Promise<SkyvernBrowser> {
if (!browserSession.browser_address) {
throw new Error(`Browser address is missing for session ${browserSession.browser_session_id}`);
}
const browser = await chromium.connectOverCDP(browserSession.browser_address, {
headers: { "x-api-key": this._apiKey },
});
const browserContext = browser.contexts()[0] ?? (await browser.newContext());
const skyvernBrowser = new SkyvernBrowser(this, browserContext, {
browser,
browserSessionId: browserSession.browser_session_id,
});
this._browsers.add(skyvernBrowser);
return skyvernBrowser;
}
private async _waitForRunCompletion(runId: string, timeoutSeconds: number): Promise<GetRunResponse> {
const startTime = Date.now();
const timeoutMs = timeoutSeconds * 1000;
while (true) {
const run = await this.getRun(runId);
// Check if the run is in a final state
const status = run.status;
if (
status === "completed" ||
status === "failed" ||
status === "terminated" ||
status === "timed_out" ||
status === "canceled"
) {
return run;
}
// Check timeout
if (Date.now() - startTime >= timeoutMs) {
throw new Error(`Timeout waiting for run ${runId} to complete after ${timeoutSeconds} seconds`);
}
// Wait before polling again
await new Promise((resolve) => setTimeout(resolve, 10000)); // 10 seconds
}
}
}

View File

@@ -0,0 +1,78 @@
import type { Browser, BrowserContext, Page } from "playwright";
import type { Skyvern } from "./Skyvern.js";
import { SkyvernBrowserPageCore, type SkyvernBrowserPage } from "./SkyvernBrowserPage.js";
export class SkyvernBrowser {
private readonly _skyvern: Skyvern;
private readonly _browserContext: BrowserContext;
private readonly _browser?: Browser;
private readonly _browserSessionId?: string;
private readonly _browserAddress?: string;
public workflowRunId?: string;
constructor(
skyvern: Skyvern,
browserContext: BrowserContext,
options?: {
browser?: Browser;
browserSessionId?: string;
browserAddress?: string;
},
) {
this._skyvern = skyvern;
this._browserContext = browserContext;
this._browser = options?.browser;
this._browserSessionId = options?.browserSessionId;
this._browserAddress = options?.browserAddress;
}
get browserSessionId(): string | undefined {
return this._browserSessionId;
}
get browserAddress(): string | undefined {
return this._browserAddress;
}
get skyvern(): Skyvern {
return this._skyvern;
}
get context(): BrowserContext {
return this._browserContext;
}
async getWorkingPage(): Promise<SkyvernBrowserPage> {
const pages = this._browserContext.pages();
const page = pages.length > 0 ? pages[pages.length - 1] : await this._browserContext.newPage();
return this._createSkyvernPage(page);
}
async newPage(): Promise<SkyvernBrowserPage> {
const page = await this._browserContext.newPage();
return this._createSkyvernPage(page);
}
pages(): SkyvernBrowserPage[] {
return this._browserContext.pages().map((page) => SkyvernBrowserPageCore.create(this, page));
}
async close(): Promise<void> {
if (this._browser) {
await this._browser.close();
} else {
await this._browserContext.close();
}
if (this._browserSessionId) {
await this._skyvern.closeBrowserSession(this._browserSessionId);
}
this._skyvern._untrackBrowser(this);
}
private _createSkyvernPage(page: Page): SkyvernBrowserPage {
return SkyvernBrowserPageCore.create(this, page);
}
}

View File

@@ -0,0 +1,149 @@
import type { Page } from "playwright";
import type { SkyvernBrowser } from "./SkyvernBrowser.js";
import { SkyvernBrowserPageAgent } from "./SkyvernBrowserPageAgent.js";
import { SkyvernBrowserPageAi } from "./SkyvernBrowserPageAi.js";
export class SkyvernBrowserPageCore {
private readonly _browser: SkyvernBrowser;
private readonly _page: Page;
private readonly _ai: SkyvernBrowserPageAi;
public readonly agent: SkyvernBrowserPageAgent;
private readonly _proxy: SkyvernBrowserPage;
private constructor(browser: SkyvernBrowser, page: Page) {
this._browser = browser;
this._page = page;
this._ai = new SkyvernBrowserPageAi(browser, page);
this.agent = new SkyvernBrowserPageAgent(browser, page);
this._proxy = new Proxy(this, {
get(target, prop, receiver) {
if (prop in target) {
return Reflect.get(target, prop, receiver);
}
const value = Reflect.get(target._page, prop, target._page);
if (typeof value === "function") {
return value.bind(target._page);
}
return value;
},
}) as unknown as SkyvernBrowserPage;
}
static create(browser: SkyvernBrowser, page: Page): SkyvernBrowserPage {
const instance = new SkyvernBrowserPageCore(browser, page);
return instance._proxy;
}
get page(): Page {
return this._page;
}
get browser(): SkyvernBrowser {
return this._browser;
}
async click(selector: string, options?: Parameters<Page["click"]>[1]): Promise<void>;
async click(options: { prompt: string } & Partial<Parameters<Page["click"]>[1]>): Promise<void>;
async click(
selectorOrOptions: string | ({ prompt: string } & Partial<Parameters<Page["click"]>[1]>),
options?: Parameters<Page["click"]>[1],
): Promise<void> {
if (typeof selectorOrOptions === "string") {
return this._page.click(selectorOrOptions, options);
} else {
const { prompt, timeout, ...data } = selectorOrOptions;
await this._ai.aiClick({
intention: prompt,
timeout,
data: Object.keys(data).length > 0 ? data : undefined,
});
}
}
async fill(selector: string, value: string, options?: Parameters<Page["fill"]>[2]): Promise<void>;
async fill(options: { prompt: string; value?: string } & Partial<Parameters<Page["fill"]>[2]>): Promise<void>;
async fill(
selectorOrOptions: string | ({ prompt: string; value?: string } & Partial<Parameters<Page["fill"]>[2]>),
value?: string,
options?: Parameters<Page["fill"]>[2],
): Promise<void> {
if (typeof selectorOrOptions === "string") {
if (value === undefined) {
throw new Error("value is required when selector is provided");
}
return this._page.fill(selectorOrOptions, value, options);
} else {
const { prompt, value: fillValue, timeout, ...data } = selectorOrOptions;
await this._ai.aiInputText({
value: fillValue,
intention: prompt,
timeout,
data: Object.keys(data).length > 0 ? data : undefined,
});
}
}
async selectOption(
selector: string,
values: string | string[],
options?: Parameters<Page["selectOption"]>[2],
): Promise<string[]>;
async selectOption(
options: { prompt: string; value?: string } & Partial<Parameters<Page["selectOption"]>[2]>,
): Promise<string[]>;
async selectOption(
selectorOrOptions: string | ({ prompt: string; value?: string } & Partial<Parameters<Page["selectOption"]>[2]>),
values?: string | string[],
options?: Parameters<Page["selectOption"]>[2],
): Promise<string[]> {
if (typeof selectorOrOptions === "string") {
if (values === undefined) {
throw new Error("value is required when selector is provided");
}
return this._page.selectOption(selectorOrOptions, values, options);
} else {
const { prompt, value, timeout, ...data } = selectorOrOptions;
await this._ai.aiSelectOption({
value,
intention: prompt,
timeout,
data: Object.keys(data).length > 0 ? data : undefined,
});
return value ? [value] : [];
}
}
async act(prompt: string): Promise<void> {
return this._ai.aiAct(prompt);
}
async extract(options: {
prompt: string;
schema?: Record<string, unknown> | unknown[] | string;
errorCodeMapping?: Record<string, string>;
intention?: string;
data?: string | Record<string, unknown>;
}): Promise<Record<string, unknown> | unknown[] | string | null> {
return this._ai.aiExtract(options);
}
async validate(prompt: string, model?: Record<string, unknown> | string): Promise<boolean> {
const normalizedModel: Record<string, unknown> | undefined =
typeof model === "string" ? { modelName: model } : model;
return this._ai.aiValidate({ prompt, model: normalizedModel });
}
async prompt(
prompt: string,
schema?: Record<string, unknown>,
model?: Record<string, unknown> | string,
): Promise<Record<string, unknown> | unknown[] | string | null> {
const normalizedModel: Record<string, unknown> | undefined =
typeof model === "string" ? { modelName: model } : model;
return this._ai.aiPrompt({ prompt, schema, model: normalizedModel });
}
}
export type SkyvernBrowserPage = SkyvernBrowserPageCore & Page;

View File

@@ -0,0 +1,260 @@
import type { Page } from "playwright";
import type * as Skyvern from "../api/index.js";
import { SkyvernEnvironment } from "../environments.js";
import { DEFAULT_AGENT_HEARTBEAT_INTERVAL, DEFAULT_AGENT_TIMEOUT } from "./constants.js";
import type { SkyvernBrowser } from "./SkyvernBrowser.js";
import { LOG } from "./logger.js";
function getAppUrlForRun(runId: string): string {
return `https://app.skyvern.com/runs/${runId}`;
}
export class SkyvernBrowserPageAgent {
private readonly _browser: SkyvernBrowser;
private readonly _page: Page;
constructor(browser: SkyvernBrowser, page: Page) {
this._browser = browser;
this._page = page;
}
async runTask(
prompt: string,
options?: {
engine?: Skyvern.RunEngine;
model?: Record<string, unknown>;
url?: string;
webhookUrl?: string;
totpIdentifier?: string;
totpUrl?: string;
title?: string;
errorCodeMapping?: Record<string, string>;
dataExtractionSchema?: Record<string, unknown> | string;
maxSteps?: number;
timeout?: number;
},
): Promise<Skyvern.TaskRunResponse> {
LOG.info("AI run task", { prompt });
const taskRun = await this._browser.skyvern.runTask({
"x-user-agent": "skyvern-sdk",
body: {
prompt: prompt,
engine: options?.engine,
model: options?.model,
url: options?.url ?? this._getPageUrl(),
webhook_url: options?.webhookUrl,
totp_identifier: options?.totpIdentifier,
totp_url: options?.totpUrl,
title: options?.title,
error_code_mapping: options?.errorCodeMapping,
data_extraction_schema: options?.dataExtractionSchema,
max_steps: options?.maxSteps,
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
},
});
if (this._browser.skyvern.environment === SkyvernEnvironment.Cloud) {
LOG.info("AI task is running, this may take a while", { url: getAppUrlForRun(taskRun.run_id), run_id: taskRun.run_id });
} else {
LOG.info("AI task is running, this may take a while", { run_id: taskRun.run_id });
}
const completedRun = await this._waitForRunCompletion(
taskRun.run_id,
options?.timeout ?? DEFAULT_AGENT_TIMEOUT,
);
LOG.info("AI task finished", { run_id: completedRun.run_id, status: completedRun.status });
return completedRun as Skyvern.TaskRunResponse;
}
async login(
credentialType: string,
options?: {
url?: string;
credentialId?: string;
bitwardenCollectionId?: string;
bitwardenItemId?: string;
onepasswordVaultId?: string;
onepasswordItemId?: string;
prompt?: string;
webhookUrl?: string;
totpIdentifier?: string;
totpUrl?: string;
extraHttpHeaders?: Record<string, string>;
timeout?: number;
},
): Promise<Skyvern.WorkflowRunResponse> {
LOG.info("Starting AI login workflow", { credential_type: credentialType });
const workflowRun = await this._browser.skyvern.login(
{
credential_type: credentialType as Skyvern.SkyvernSchemasRunBlocksCredentialType,
url: options?.url ?? this._getPageUrl(),
credential_id: options?.credentialId,
bitwarden_collection_id: options?.bitwardenCollectionId,
bitwarden_item_id: options?.bitwardenItemId,
onepassword_vault_id: options?.onepasswordVaultId,
onepassword_item_id: options?.onepasswordItemId,
prompt: options?.prompt,
webhook_url: options?.webhookUrl,
totp_identifier: options?.totpIdentifier,
totp_url: options?.totpUrl,
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
extra_http_headers: options?.extraHttpHeaders,
},
{
headers: { "x-user-agent": "skyvern-sdk" },
},
);
if (this._browser.skyvern.environment === SkyvernEnvironment.Cloud) {
LOG.info("AI login workflow is running, this may take a while", { url: getAppUrlForRun(workflowRun.run_id), run_id: workflowRun.run_id });
} else {
LOG.info("AI login workflow is running, this may take a while", { run_id: workflowRun.run_id });
}
const completedRun = await this._waitForRunCompletion(
workflowRun.run_id,
options?.timeout ?? DEFAULT_AGENT_TIMEOUT,
);
LOG.info("AI login workflow finished", { run_id: completedRun.run_id, status: completedRun.status });
return completedRun as Skyvern.WorkflowRunResponse;
}
async downloadFiles(
prompt: string,
options?: {
url?: string;
downloadSuffix?: string;
downloadTimeout?: number;
maxStepsPerRun?: number;
webhookUrl?: string;
totpIdentifier?: string;
totpUrl?: string;
extraHttpHeaders?: Record<string, string>;
timeout?: number;
},
): Promise<Skyvern.WorkflowRunResponse> {
LOG.info("Starting AI file download workflow", { navigation_goal: prompt });
const workflowRun = await this._browser.skyvern.downloadFiles(
{
navigation_goal: prompt,
url: options?.url ?? this._getPageUrl(),
download_suffix: options?.downloadSuffix,
download_timeout: options?.downloadTimeout,
max_steps_per_run: options?.maxStepsPerRun,
webhook_url: options?.webhookUrl,
totp_identifier: options?.totpIdentifier,
totp_url: options?.totpUrl,
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
extra_http_headers: options?.extraHttpHeaders,
},
{
headers: { "x-user-agent": "skyvern-sdk" },
},
);
LOG.info("AI file download workflow is running, this may take a while", { run_id: workflowRun.run_id });
const completedRun = await this._waitForRunCompletion(
workflowRun.run_id,
options?.timeout ?? DEFAULT_AGENT_TIMEOUT,
);
LOG.info("AI file download workflow finished", { run_id: completedRun.run_id, status: completedRun.status });
return completedRun as Skyvern.WorkflowRunResponse;
}
async runWorkflow(
workflowId: string,
options?: {
parameters?: Record<string, unknown>;
template?: boolean;
title?: string;
webhookUrl?: string;
totpUrl?: string;
totpIdentifier?: string;
timeout?: number;
},
): Promise<Skyvern.WorkflowRunResponse> {
LOG.info("Starting AI workflow", { workflow_id: workflowId });
const workflowRun = await this._browser.skyvern.runWorkflow(
{
"x-user-agent": "skyvern-sdk",
template: options?.template,
body: {
workflow_id: workflowId,
parameters: options?.parameters,
title: options?.title,
webhook_url: options?.webhookUrl,
totp_url: options?.totpUrl,
totp_identifier: options?.totpIdentifier,
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
},
},
{
headers: { "x-user-agent": "skyvern-sdk" },
},
);
if (this._browser.skyvern.environment === SkyvernEnvironment.Cloud) {
LOG.info("AI workflow is running, this may take a while", { url: getAppUrlForRun(workflowRun.run_id), run_id: workflowRun.run_id });
} else {
LOG.info("AI workflow is running, this may take a while", { run_id: workflowRun.run_id });
}
const completedRun = await this._waitForRunCompletion(
workflowRun.run_id,
options?.timeout ?? DEFAULT_AGENT_TIMEOUT,
);
LOG.info("AI workflow finished", { run_id: completedRun.run_id, status: completedRun.status });
return completedRun as Skyvern.WorkflowRunResponse;
}
private async _waitForRunCompletion(runId: string, timeoutSeconds: number): Promise<Skyvern.GetRunResponse> {
const startTime = Date.now();
const timeoutMs = timeoutSeconds * 1000;
while (true) {
const run = await this._browser.skyvern.getRun(runId);
// Check if the run is in a final state
const status = run.status;
if (
status === "completed" ||
status === "failed" ||
status === "terminated" ||
status === "timed_out" ||
status === "canceled"
) {
return run;
}
// Check timeout
if (Date.now() - startTime >= timeoutMs) {
throw new Error(`Timeout waiting for run ${runId} to complete after ${timeoutSeconds} seconds`);
}
// Wait before polling again
await new Promise((resolve) => setTimeout(resolve, DEFAULT_AGENT_HEARTBEAT_INTERVAL * 1000));
}
}
private _getPageUrl(): string | undefined {
const url = this._page.url();
if (url === "about:blank") {
return undefined;
}
return url;
}
}

View File

@@ -0,0 +1,264 @@
import type { Page } from "playwright";
import type * as Skyvern from "../api/index.js";
import type { SkyvernBrowser } from "./SkyvernBrowser.js";
import { LOG } from "./logger.js";
export class SkyvernBrowserPageAi {
private readonly _browser: SkyvernBrowser;
private readonly _page: Page;
constructor(browser: SkyvernBrowser, page: Page) {
this._browser = browser;
this._page = page;
}
async aiClick(options: {
selector?: string;
intention: string;
data?: string | Record<string, unknown>;
timeout?: number;
}): Promise<string | null> {
LOG.info("AI click", { intention: options.intention, workflow_run_id: this._browser.workflowRunId });
const response = await this._browser.skyvern.runSdkAction({
url: this._page.url(),
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
workflow_run_id: this._browser.workflowRunId,
action: {
type: "ai_click",
selector: options.selector,
intention: options.intention,
data: options.data,
timeout: options.timeout,
} as Skyvern.RunSdkActionRequestAction,
});
if (response.workflow_run_id) {
this._browser.workflowRunId = response.workflow_run_id;
}
return response.result ? String(response.result) : options.selector || null;
}
async aiInputText(options: {
selector?: string;
value?: string;
intention: string;
data?: string | Record<string, unknown>;
totpIdentifier?: string;
totpUrl?: string;
timeout?: number;
}): Promise<string> {
LOG.info("AI input text", { intention: options.intention, workflow_run_id: this._browser.workflowRunId });
const response = await this._browser.skyvern.runSdkAction({
url: this._page.url(),
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
workflow_run_id: this._browser.workflowRunId,
action: {
type: "ai_input_text",
selector: options.selector,
value: options.value,
intention: options.intention,
data: options.data,
totp_identifier: options.totpIdentifier,
totp_url: options.totpUrl,
timeout: options.timeout,
} as Skyvern.RunSdkActionRequestAction,
});
if (response.workflow_run_id) {
this._browser.workflowRunId = response.workflow_run_id;
}
return response.result ? String(response.result) : options.value || "";
}
async aiSelectOption(options: {
selector?: string;
value?: string;
intention: string;
data?: string | Record<string, unknown>;
timeout?: number;
}): Promise<string> {
LOG.info("AI select option", { intention: options.intention, workflow_run_id: this._browser.workflowRunId });
const response = await this._browser.skyvern.runSdkAction({
url: this._page.url(),
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
workflow_run_id: this._browser.workflowRunId,
action: {
type: "ai_select_option",
selector: options.selector,
value: options.value,
intention: options.intention,
data: options.data,
timeout: options.timeout,
} as Skyvern.RunSdkActionRequestAction,
});
if (response.workflow_run_id) {
this._browser.workflowRunId = response.workflow_run_id;
}
return response.result ? String(response.result) : options.value || "";
}
async aiUploadFile(options: {
selector?: string;
fileUrl?: string;
intention: string;
data?: string | Record<string, unknown>;
timeout?: number;
}): Promise<string> {
LOG.info("AI upload file", { intention: options.intention, workflow_run_id: this._browser.workflowRunId });
const response = await this._browser.skyvern.runSdkAction({
url: this._page.url(),
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
workflow_run_id: this._browser.workflowRunId,
action: {
type: "ai_upload_file",
selector: options.selector,
file_url: options.fileUrl,
intention: options.intention,
data: options.data,
timeout: options.timeout,
} as Skyvern.RunSdkActionRequestAction,
});
if (response.workflow_run_id) {
this._browser.workflowRunId = response.workflow_run_id;
}
return response.result ? String(response.result) : options.fileUrl || "";
}
async aiExtract(options: {
prompt: string;
extractSchema?: Record<string, unknown> | unknown[] | string;
errorCodeMapping?: Record<string, string>;
intention?: string;
data?: string | Record<string, unknown>;
}): Promise<Record<string, unknown> | unknown[] | string | null> {
LOG.info("AI extract", { prompt: options.prompt, workflow_run_id: this._browser.workflowRunId });
const response = await this._browser.skyvern.runSdkAction({
url: this._page.url(),
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
workflow_run_id: this._browser.workflowRunId,
action: {
type: "extract",
prompt: options.prompt,
extract_schema: options.extractSchema,
error_code_mapping: options.errorCodeMapping,
intention: options.intention,
data: options.data,
} as Skyvern.RunSdkActionRequestAction,
});
if (response.workflow_run_id) {
this._browser.workflowRunId = response.workflow_run_id;
}
return (response.result as Record<string, unknown> | unknown[] | string) || null;
}
async aiValidate(options: { prompt: string; model?: Record<string, unknown> }): Promise<boolean> {
LOG.info("AI validate", { prompt: options.prompt, model: options.model, workflow_run_id: this._browser.workflowRunId });
const response = await this._browser.skyvern.runSdkAction({
url: this._page.url(),
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
workflow_run_id: this._browser.workflowRunId,
action: {
type: "validate",
prompt: options.prompt,
model: options.model,
} as Skyvern.RunSdkActionRequestAction,
});
if (response.workflow_run_id) {
this._browser.workflowRunId = response.workflow_run_id;
}
return response.result != null ? Boolean(response.result) : false;
}
async aiAct(prompt: string): Promise<void> {
LOG.info("AI act", { prompt, workflow_run_id: this._browser.workflowRunId });
const response = await this._browser.skyvern.runSdkAction({
url: this._page.url(),
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
workflow_run_id: this._browser.workflowRunId,
action: {
type: "ai_act",
intention: prompt,
} as Skyvern.RunSdkActionRequestAction,
});
if (response.workflow_run_id) {
this._browser.workflowRunId = response.workflow_run_id;
}
}
async aiLocateElement(prompt: string): Promise<string | null> {
LOG.info("AI locate element", { prompt, workflow_run_id: this._browser.workflowRunId });
const response = await this._browser.skyvern.runSdkAction({
url: this._page.url(),
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
workflow_run_id: this._browser.workflowRunId,
action: {
type: "locate_element",
prompt,
} as Skyvern.RunSdkActionRequestAction,
});
if (response.workflow_run_id) {
this._browser.workflowRunId = response.workflow_run_id;
}
if (response.result && typeof response.result === "string") {
return response.result;
}
return null;
}
async aiPrompt(options: {
prompt: string;
schema?: Record<string, unknown>;
model?: Record<string, unknown>;
}): Promise<Record<string, unknown> | unknown[] | string | null> {
LOG.info("AI prompt", { prompt: options.prompt, model: options.model, workflow_run_id: this._browser.workflowRunId });
const response = await this._browser.skyvern.runSdkAction({
url: this._page.url(),
browser_session_id: this._browser.browserSessionId,
browser_address: this._browser.browserAddress,
workflow_run_id: this._browser.workflowRunId,
action: {
type: "prompt",
prompt: options.prompt,
schema: options.schema,
model: options.model,
} as Skyvern.RunSdkActionRequestAction,
});
if (response.workflow_run_id) {
this._browser.workflowRunId = response.workflow_run_id;
}
return (response.result as Record<string, unknown> | unknown[] | string) || null;
}
}

View File

@@ -0,0 +1,3 @@
export const DEFAULT_AGENT_TIMEOUT = 1800; // 30 minutes in seconds
export const DEFAULT_AGENT_HEARTBEAT_INTERVAL = 10; // 10 seconds
export const DEFAULT_CDP_PORT = 9222;

View File

@@ -0,0 +1,10 @@
export { Skyvern } from "./Skyvern.js";
export type { SkyvernOptions, RunTaskOptions, RunWorkflowOptions, LoginOptions, DownloadFilesOptions } from "./Skyvern.js";
export { SkyvernBrowser } from "./SkyvernBrowser.js";
export { SkyvernBrowserPageCore as SkyvernBrowserPageIml } from "./SkyvernBrowserPage.js";
export type { SkyvernBrowserPage } from "./SkyvernBrowserPage.js";
export { SkyvernBrowserPageAgent } from "./SkyvernBrowserPageAgent.js";
export { SkyvernBrowserPageAi } from "./SkyvernBrowserPageAi.js";
export { DEFAULT_AGENT_TIMEOUT, DEFAULT_AGENT_HEARTBEAT_INTERVAL, DEFAULT_CDP_PORT } from "./constants.js";
export type { Logger } from "./logger.js";
export { setLogger, LOG } from "./logger.js";

View File

@@ -0,0 +1,71 @@
export interface Logger {
debug(message: string, context?: Record<string, unknown>): void;
info(message: string, context?: Record<string, unknown>): void;
warn(message: string, context?: Record<string, unknown>): void;
error(message: string, context?: Record<string, unknown>): void;
}
function formatLogMessage(message: string, context?: Record<string, unknown>): string {
const prefix = "[Skyvern]";
if (!context || Object.keys(context).length === 0) {
return `${prefix} ${message}`;
}
const contextParts = Object.entries(context)
.filter(([, value]) => value !== null && value !== undefined)
.map(([key, value]) => {
if (typeof value === "object") {
return `${key}=${JSON.stringify(value)}`;
}
return `${key}=${value}`;
})
.join(" ");
if (contextParts.length === 0) {
return `${prefix} ${message}`;
}
return `${prefix} ${message}\t${contextParts}`;
}
const defaultLogger: Logger = {
debug: (message: string, context?: Record<string, unknown>) => {
console.debug(formatLogMessage(message, context));
},
info: (message: string, context?: Record<string, unknown>) => {
console.info(formatLogMessage(message, context));
},
warn: (message: string, context?: Record<string, unknown>) => {
console.warn(formatLogMessage(message, context));
},
error: (message: string, context?: Record<string, unknown>) => {
console.error(formatLogMessage(message, context));
},
};
let currentLogger: Logger = defaultLogger;
export function setLogger(logger: Logger): void {
currentLogger = logger;
}
class StructuredLogger {
debug(message: string, context?: Record<string, unknown>): void {
currentLogger.debug(message, context);
}
info(message: string, context?: Record<string, unknown>): void {
currentLogger.info(message, context);
}
warn(message: string, context?: Record<string, unknown>): void {
currentLogger.warn(message, context);
}
error(message: string, context?: Record<string, unknown>): void {
currentLogger.error(message, context);
}
}
export const LOG: StructuredLogger = new StructuredLogger();