UNPKG

@humanlayer/sdk

Version:

typescript client for humanlayer.dev

401 lines (392 loc) 12.3 kB
import require$$0 from 'readline'; import crypto from 'crypto'; class HumanLayerException extends Error { } class HumanLayerCloudConnection { constructor(api_key, api_base_url) { this.apiKey = api_key; this.apiBaseURL = api_base_url; if (!this.apiKey) { throw new Error("HUMANLAYER_API_KEY is required for cloud approvals"); } this.apiBaseURL = this.apiBaseURL || "https://api.humanlayer.dev/humanlayer/v1"; } async request({ method, path, body }) { const resp = await fetch(`${this.apiBaseURL}${path}`, { method, headers: { Authorization: `Bearer ${this.apiKey}`, "content-type": "application/json" }, body: JSON.stringify(body) }); if (resp.status >= 400) { const err = new HumanLayerException(`${method} ${path} ${resp.status}: ${await resp.text()}`); throw err; } return resp; } } class CloudFunctionCallStore { constructor(connection) { this.connection = connection; } async add(item) { const resp = await this.connection.request({ method: "POST", path: "/function_calls", body: item }); const data = await resp.json(); return data; } async get(call_id) { const resp = await this.connection.request({ method: "GET", path: `/function_calls/${call_id}` }); const data = await resp.json(); return data; } async respond(call_id, status) { const resp = await this.connection.request({ method: "POST", path: `/agent/function_calls/${call_id}/respond`, body: status }); const data = await resp.json(); return data; } async escalateEmail(call_id, escalation) { const resp = await this.connection.request({ method: "POST", path: `/agent/function_calls/${call_id}/escalate_email`, body: escalation }); const data = await resp.json(); return data; } } class CloudHumanContactStore { constructor(connection) { this.connection = connection; } async add(item) { const resp = await this.connection.request({ method: "POST", path: "/contact_requests", body: item }); const data = await resp.json(); return data; } async get(call_id) { const resp = await this.connection.request({ method: "GET", path: `/contact_requests/${call_id}` }); const data = await resp.json(); return data; } async respond(call_id, status) { const resp = await this.connection.request({ method: "POST", path: `/agent/human_contacts/${call_id}/respond`, body: status }); const data = await resp.json(); return data; } async escalateEmail(call_id, escalation) { const resp = await this.connection.request({ method: "POST", path: `/agent/human_contacts/${call_id}/escalate_email`, body: escalation }); const data = await resp.json(); return data; } } class CloudHumanLayerBackend { constructor(connection) { this.connection = connection; this._function_calls = new CloudFunctionCallStore(connection); this._human_contacts = new CloudHumanContactStore(connection); } functions() { return this._function_calls; } contacts() { return this._human_contacts; } } const logger = { debug: (...args) => { console.debug(...args); }, info: (...args) => { console.info(...args); }, error: (...args) => { console.error(...args); } }; var ApprovalMethod = /* @__PURE__ */ ((ApprovalMethod2) => { ApprovalMethod2["cli"] = "cli"; ApprovalMethod2["backend"] = "backend"; return ApprovalMethod2; })(ApprovalMethod || {}); const nullIsh = (value) => value === null || typeof value === "undefined"; const defaultGenid = (prefix) => { return `${prefix}-${crypto.randomUUID().slice(0, 8)}`; }; const humanlayer = (params) => { return new HumanLayer(params); }; class HumanLayer { constructor(params) { const { runId, approvalMethod, backend, agentName, genid = defaultGenid, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), contactChannel, apiKey, apiBaseUrl, verbose = false, httpTimeoutSeconds = parseInt(process.env.HUMANLAYER_HTTP_TIMEOUT_SECONDS || "10") } = params || {}; this.genid = genid; this.sleep = sleep; this.verbose = verbose; this.contactChannel = contactChannel; this.approvalMethod = approvalMethod || (backend || apiKey || process.env.HUMANLAYER_API_KEY ? "backend" /* backend */ : "cli" /* cli */); if (this.approvalMethod === "backend" /* backend */) { this.backend = backend || new CloudHumanLayerBackend( new HumanLayerCloudConnection( apiKey || process.env.HUMANLAYER_API_KEY, apiBaseUrl || process.env.HUMANLAYER_API_BASE ) ); } this.agentName = agentName || "agent"; this.genid = genid || defaultGenid; this.runId = runId || this.genid(this.agentName); } static cloud(params) { let { connection, apiKey, apiBaseUrl } = params || {}; if (!connection) { connection = new HumanLayerCloudConnection(apiKey, apiBaseUrl); } return new HumanLayer({ approvalMethod: "backend" /* backend */, backend: new CloudHumanLayerBackend(connection) }); } static cli() { return new HumanLayer({ approvalMethod: "cli" /* cli */ }); } requireApproval(contactChannel) { return (fn) => { if (this.approvalMethod === "cli" /* cli */) { return this.approveCli(fn); } return this.approveWithBackend(fn, contactChannel); }; } approveCli(fn) { const name = fn.name; const f = async (kwargs) => { logger.info(`Agent ${this.runId} wants to call ${fn.name}(${JSON.stringify(kwargs, null, 2)}) ${kwargs.length ? " with args: " + JSON.stringify(kwargs, null, 2) : ""}`); const readline = require$$0.createInterface({ input: process.stdin, output: process.stdout }); const feedback = await new Promise((resolve) => { readline.question( "Hit ENTER to proceed, or provide feedback to the agent to deny: \n\n", (answer) => { readline.close(); resolve(answer); } ); }); if (feedback !== null && feedback !== "") { return `User denied ${fn.name} with feedback: ${feedback}`; } try { return await fn(kwargs); } catch (e) { return `Error running ${fn.name}: ${e}`; } }; Object.defineProperty(f, "name", { value: name, writable: false }); return f; } approveWithBackend(fn, contactChannel) { const channel = contactChannel || this.contactChannel; const name = fn.name; const f = async (kwargs) => { var _a, _b, _c, _d; const backend = this.backend; const callId = this.genid("call"); await backend.functions().add({ run_id: this.runId, call_id: callId, spec: { fn: fn.name, kwargs, channel } }); if (this.verbose) { logger.info(`HumanLayer: Requested approval for function ${name}`); } while (true) { await this.sleep(3e3); const functionCall = await backend.functions().get(callId); if (((_a = functionCall.status) == null ? void 0 : _a.approved) === null || typeof ((_b = functionCall.status) == null ? void 0 : _b.approved) === "undefined") { continue; } if ((_c = functionCall.status) == null ? void 0 : _c.approved) { if (this.verbose) { logger.info(`HumanLayer: User approved function ${functionCall.spec.fn}`); } return fn(kwargs); } else { return `User denied function ${functionCall.spec.fn} with comment: ${(_d = functionCall.status) == null ? void 0 : _d.comment}`; } } }; Object.defineProperty(f, "name", { value: name, writable: false }); return f; } humanAsTool(contactChannel) { if (this.approvalMethod === "cli" /* cli */) { return this.humanAsToolCli(); } return this.humanAsToolBackend(contactChannel); } humanAsToolCli() { return async ({ message }) => { logger.info(`Agent ${this.runId} requests assistance: ${message} `); const feedback = prompt("Please enter a response: \n\n"); return feedback || ""; }; } humanAsToolBackend(contactChannel) { const channel = contactChannel || this.contactChannel; return async ({ message }) => { return this.fetchHumanResponse({ spec: { msg: message, channel } }); }; } async fetchHumanResponse({ spec }) { var _a; spec.channel = nullIsh(spec.channel) ? this.contactChannel : spec.channel; let humanContact = await this.createHumanContact({ spec }); if (this.verbose) { logger.info(`HumanLayer: Requested human response from HumanLayer cloud`); } while (true) { await this.sleep(3e3); humanContact = await this.getHumanContact(humanContact.call_id); if (!((_a = humanContact.status) == null ? void 0 : _a.response)) { continue; } if (this.verbose) { logger.info(`HumanLayer: Received human response: ${humanContact.status.response}`); } return humanContact.status.response; } } async createHumanContact({ spec }) { spec.channel = nullIsh(spec.channel) ? this.contactChannel : spec.channel; if (!this.backend) { throw new HumanLayerException("createHumanContact requires a backend"); } const callId = this.genid("human_call"); const ret = await this.backend.contacts().add({ run_id: this.runId, call_id: callId, spec }); return ret; } async escalateEmailHumanContact(call_id, escalation) { if (!this.backend) { throw new HumanLayerException("escalateEmailFunctionCall requires a backend"); } return this.backend.contacts().escalateEmail(call_id, escalation); } getHumanContact(call_id) { if (!this.backend) { throw new HumanLayerException("getHumanContact requires a backend"); } return this.backend.contacts().get(call_id); } async fetchHumanApproval({ spec }) { var _a, _b, _c, _d, _e; spec.channel = nullIsh(spec.channel) ? this.contactChannel : spec.channel; let functionCall = await this.createFunctionCall({ spec }); if (this.verbose) { logger.info(`HumanLayer: Requested human approval from HumanLayer cloud`); } while (true) { await this.sleep(3e3); functionCall = await this.getFunctionCall(functionCall.call_id); if (((_a = functionCall.status) == null ? void 0 : _a.approved) === null || typeof ((_b = functionCall.status) == null ? void 0 : _b.approved) === "undefined") { continue; } if (this.verbose) { logger.info( `HumanLayer: Received response ${((_c = functionCall.status) == null ? void 0 : _c.approved) ? " (approved)" : " (denied)"} ${((_d = functionCall.status) == null ? void 0 : _d.comment) ? `with comment: ${(_e = functionCall.status) == null ? void 0 : _e.comment}` : ""}` ); } return functionCall.status; } } async createFunctionCall({ spec }) { spec.channel = nullIsh(spec.channel) ? this.contactChannel : spec.channel; if (!this.backend) { throw new HumanLayerException("createFunctionCall requires a backend"); } const callId = this.genid("call"); return this.backend.functions().add({ run_id: this.runId, call_id: callId, spec }); } async escalateEmailFunctionCall(call_id, escalation) { if (!this.backend) { throw new HumanLayerException("escalateEmailFunctionCall requires a backend"); } return this.backend.functions().escalateEmail(call_id, escalation); } async getFunctionCall(call_id) { if (!this.backend) { throw new HumanLayerException("getFunctionCall requires a backend"); } return this.backend.functions().get(call_id); } } export { ApprovalMethod, CloudFunctionCallStore, CloudHumanContactStore, CloudHumanLayerBackend, HumanLayer, HumanLayerCloudConnection, humanlayer };