@humanlayer/sdk
Version:
typescript client for humanlayer.dev
401 lines (392 loc) • 12.3 kB
JavaScript
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 };