UNPKG

plato-sdk

Version:

JavaScript client for interacting with the Plato API

374 lines (373 loc) 13.9 kB
"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;