plato-sdk
Version:
JavaScript client for interacting with the Plato API
374 lines (373 loc) • 13.9 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Plato = exports.PlatoEnvironment = exports.PlatoTaskSchema = void 0;
const axios_1 = __importDefault(require("axios"));
const exceptions_1 = require("./exceptions");
const zod_1 = require("zod");
exports.PlatoTaskSchema = zod_1.z.object({
name: zod_1.z.string(),
prompt: zod_1.z.string(),
start_url: zod_1.z.string(),
});
class PlatoEnvironment {
constructor(client, id, alias, fast = false) {
this.alias = null;
this.fast = false;
this.heartbeatInterval = null;
this.heartbeatIntervalMs = 30000; // 30 seconds
this.runSessionId = null;
this.client = client;
this.id = id;
this.alias = alias || null;
this.fast = fast;
this.startHeartbeat();
}
async getStatus() {
return this.client.getJobStatus(this.id);
}
async getCdpUrl() {
return this.client.getCdpUrl(this.id);
}
async close() {
this.stopHeartbeat();
this.runSessionId = null;
return this.client.closeEnvironment(this.id);
}
async evaluate() {
if (!this.runSessionId) {
throw new exceptions_1.PlatoClientError('No run session ID found');
}
return this.client.evaluate(this.runSessionId);
}
/**
* Resets the environment with a new task
* @param task The task to run in the environment, or a simplified object with just name, prompt, and start_url
* @param testCasePublicId The public ID of the test case
* @param loadAuthenticated Whether to load authenticated browser state
* @returns The response from the server
*/
async reset(task, testCasePublicId, loadAuthenticated = false) {
try {
const result = await this.client.resetEnvironment(this.id, task, testCasePublicId, loadAuthenticated);
// Ensure heartbeat is running after reset
this.startHeartbeat();
this.runSessionId = result?.data?.run_session_id || result?.run_session_id;
return this.runSessionId;
}
catch (error) {
throw new exceptions_1.PlatoClientError('Failed to reset environment: ' + error);
}
}
async getState() {
return this.client.getEnvironmentState(this.id);
}
async getLiveViewUrl() {
return this.client.getLiveViewUrl(this.id);
}
async backup() {
return this.client.backupEnvironment(this.id);
}
/**
* Starts the heartbeat interval to keep the environment alive
* @private
*/
startHeartbeat() {
if (this.heartbeatInterval) {
return; // Already running
}
this.heartbeatInterval = setInterval(async () => {
try {
await this.client.sendHeartbeat(this.id);
}
catch (error) {
console.error('Failed to send heartbeat:', error);
}
}, this.heartbeatIntervalMs);
// Make the interval not prevent Node.js from exiting
if (this.heartbeatInterval.unref) {
this.heartbeatInterval.unref();
}
}
/**
* Stops the heartbeat interval
* @private
*/
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
async waitForReady(timeout) {
const startTime = Date.now();
let baseDelay = 500; // Starting delay in milliseconds
const maxDelay = 8000; // Maximum delay between retries
// Wait for the job to be running
let currentDelay = baseDelay;
while (true) {
const status = await this.client.getJobStatus(this.id);
if (status.status.toLowerCase() === 'running') {
break;
}
// Add jitter (±25% of current delay)
const jitter = (Math.random() - 0.5) * 0.5 * currentDelay;
await new Promise(resolve => setTimeout(resolve, currentDelay + jitter));
if (timeout && Date.now() - startTime > timeout) {
throw new exceptions_1.PlatoClientError('Environment failed to start - job never entered running state');
}
// Exponential backoff
currentDelay = Math.min(currentDelay * 2, maxDelay);
}
// Wait for the worker to be ready and healthy
currentDelay = baseDelay; // Reset delay for worker health check
while (true) {
const workerStatus = await this.client.getWorkerReady(this.id);
if (workerStatus.ready) {
break;
}
// Add jitter (±25% of current delay)
const jitter = (Math.random() - 0.5) * 0.5 * currentDelay;
await new Promise(resolve => setTimeout(resolve, currentDelay + jitter));
if (timeout && Date.now() - startTime > timeout) {
const errorMsg = workerStatus.error || 'Unknown error';
throw new exceptions_1.PlatoClientError(`Environment failed to start - worker not ready: ${errorMsg}`);
}
// Exponential backoff
currentDelay = Math.min(currentDelay * 2, maxDelay);
}
}
/**
* Get the public URL for accessing this environment.
*
* @returns The public URL for this environment based on the deployment environment.
* Uses alias if available, otherwise uses environment ID.
* - Dev: https://{alias|env.id}.dev.sims.plato.so
* - Staging: https://{alias|env.id}.staging.sims.plato.so
* - Production: https://{alias|env.id}.sims.plato.so
* - Local: http://localhost:8081/{alias|env.id}
* @throws PlatoClientError If unable to determine the environment type.
*/
getPublicUrl() {
try {
// Use alias if available, otherwise use environment ID
const identifier = this.alias || this.id;
// Determine environment based on base_url
if (this.client.baseUrl.includes('localhost:8080')) {
return `http://localhost:8081/${identifier}`;
}
else if (this.client.baseUrl.includes('dev.plato.so')) {
return `https://${identifier}.dev.sims.plato.so`;
}
else if (this.client.baseUrl.includes('staging.plato.so')) {
return `https://${identifier}.staging.sims.plato.so`;
}
else if (this.client.baseUrl.includes('plato.so') && !this.client.baseUrl.includes('staging') && !this.client.baseUrl.includes('dev')) {
return `https://${identifier}.sims.plato.so`;
}
else {
throw new exceptions_1.PlatoClientError('Unknown base URL');
}
}
catch (error) {
throw new exceptions_1.PlatoClientError(String(error));
}
}
}
exports.PlatoEnvironment = PlatoEnvironment;
class Plato {
constructor(apiKey, baseUrl) {
if (!apiKey) {
throw new exceptions_1.PlatoClientError('API key is required');
}
this.apiKey = apiKey;
this.baseUrl = baseUrl || 'https://plato.so/api';
this.http = axios_1.default.create({
baseURL: this.baseUrl,
headers: {
'X-API-Key': this.apiKey,
'Content-Type': 'application/json',
},
});
}
/**
* Create a new Plato environment for the given environment ID.
*
* @param envId The environment ID to create
* @param openPageOnStart Whether to open the page on start
* @param recordActions Whether to record actions
* @param keepalive If true, jobs will not be killed due to heartbeat failures
* @param alias Optional alias for the job group
* @param fast Fast mode flag
* @returns The created environment instance
* @throws PlatoClientError If the API request fails
*/
async makeEnvironment(envId, openPageOnStart = false, recordActions = false, keepalive = false, alias, fast = false) {
if (fast) {
console.log('Running in fast mode');
}
try {
const response = await this.http.post('/env/make2', {
interface_type: "browser",
interface_width: 1280,
interface_height: 720,
source: "SDK",
open_page_on_start: openPageOnStart,
env_id: envId,
env_config: {},
record_actions: recordActions,
keepalive: keepalive,
alias: alias,
fast: fast,
});
return new PlatoEnvironment(this, response.data.job_id, response.data.alias, fast);
}
catch (error) {
if (axios_1.default.isAxiosError(error)) {
throw new exceptions_1.PlatoClientError(error.message);
}
throw error;
}
}
async getJobStatus(jobId) {
try {
const response = await this.http.get(`/env/${jobId}/status`);
return response.data;
}
catch (error) {
if (axios_1.default.isAxiosError(error)) {
throw new exceptions_1.PlatoClientError(error.message);
}
throw error;
}
}
async getCdpUrl(jobId) {
try {
const response = await this.http.get(`/env/${jobId}/cdp_url`);
if (response.data.error) {
throw new exceptions_1.PlatoClientError(response.data.error);
}
return response.data.data.cdp_url;
}
catch (error) {
if (axios_1.default.isAxiosError(error)) {
throw new exceptions_1.PlatoClientError(error.message);
}
throw error;
}
}
async closeEnvironment(jobId) {
try {
const response = await this.http.post(`/env/${jobId}/close`);
return response.data;
}
catch (error) {
if (axios_1.default.isAxiosError(error)) {
throw new exceptions_1.PlatoClientError(error.message);
}
throw error;
}
}
async evaluate(sessionId) {
try {
const response = await this.http.post(`/env/session/${sessionId}/evaluate`);
return response.data;
}
catch (error) {
if (axios_1.default.isAxiosError(error)) {
throw new exceptions_1.PlatoClientError(error.message);
}
throw error;
}
}
/**
* Resets an environment with a new task
* @param jobId The ID of the job to reset
* @param task The task to run in the environment, or a simplified object with just name, prompt, and start_url
* @param testCasePublicId The public ID of the test case
* @param loadAuthenticated Whether to load authenticated browser state
* @returns The response from the server
*/
async resetEnvironment(jobId, task, testCasePublicId, loadAuthenticated = false) {
try {
const response = await this.http.post(`/env/${jobId}/reset`, {
task: task || null,
test_case_public_id: testCasePublicId || null,
load_browser_state: loadAuthenticated,
});
return response.data;
}
catch (error) {
if (axios_1.default.isAxiosError(error)) {
throw new exceptions_1.PlatoClientError(error.message);
}
throw error;
}
}
async getEnvironmentState(jobId) {
try {
const response = await this.http.get(`/env/${jobId}/state`);
return response.data;
}
catch (error) {
if (axios_1.default.isAxiosError(error)) {
throw new exceptions_1.PlatoClientError(error.message);
}
throw error;
}
}
async getWorkerReady(jobId) {
try {
const response = await this.http.get(`/env/${jobId}/worker_ready`);
return response.data;
}
catch (error) {
if (axios_1.default.isAxiosError(error)) {
throw new exceptions_1.PlatoClientError(error.message);
}
throw error;
}
}
async getLiveViewUrl(jobId) {
try {
const workerStatus = await this.getWorkerReady(jobId);
if (!workerStatus.ready) {
throw new exceptions_1.PlatoClientError('Worker is not ready yet');
}
return `${this.baseUrl}/live/${jobId}/`;
}
catch (error) {
if (axios_1.default.isAxiosError(error)) {
throw new exceptions_1.PlatoClientError(error.message);
}
throw error;
}
}
async sendHeartbeat(jobId) {
try {
const response = await this.http.post(`/env/${jobId}/heartbeat`);
return response.data;
}
catch (error) {
if (axios_1.default.isAxiosError(error)) {
throw new exceptions_1.PlatoClientError(error.message);
}
throw error;
}
}
async backupEnvironment(jobId) {
try {
const response = await this.http.post(`/env/${jobId}/backup`);
return response.data;
}
catch (error) {
if (axios_1.default.isAxiosError(error)) {
throw new exceptions_1.PlatoClientError(error.message);
}
throw error;
}
}
}
exports.Plato = Plato;