retort-js
Version:
Intuitive, production-ready prompt chaining in Javascript
237 lines (236 loc) • 9.03 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.unescapeSegment = exports.unescape = exports.isTemplateStringsArray = exports.templateContent = exports.RetortMessage = void 0;
const create_stream_cloner_1 = __importDefault(require("./create-stream-cloner"));
const id_1 = require("./id");
class RetortMessage {
get content() {
if (this._data === null) {
throw new Error("Message not yet resolved; To fix this, you can await message.promise");
}
return this._data.content;
}
get result() {
if (this.json) {
return JSON.parse(this.content);
}
else {
return this.content;
}
}
static createId() {
return (0, id_1.id)("msg");
}
static async *createStreamFromPromise(promise) {
yield { content: (await promise).content, contentDelta: (await promise).content };
}
constructor(options) {
this._data = null;
this.json = options.json ?? false;
this.id = options.id || RetortMessage.createId();
this.role = options.role;
if ("content" in options) {
this._data = { content: options.content };
this.promise = Promise.resolve(this);
this.promise.getStream = async function* () {
yield { content: options.content, contentDelta: options.content };
return;
};
}
else if ("stream" in options) {
let content = "";
let getStream = (0, create_stream_cloner_1.default)(options.stream);
this.promise = (async () => {
for await (const chunk of getStream()) {
content += chunk.contentDelta;
}
this._data = { content };
return this;
})();
this.promise.getStream = getStream;
}
else if ("promise" in options) {
this.promise = options.promise.then((content) => {
this._data = { content };
return this;
});
let promise = this.promise;
this.promise.getStream = async function* () {
yield { content: (await promise).content, contentDelta: (await promise).content };
return;
};
}
else {
throw new Error("Invalid options passed to RetortMessage constructor; must include either 'content' or 'stream'");
}
this.promise.id = this.id;
this.promise.role = this.role;
this.promise.message = this;
let stream = this.promise.getStream();
this.promise.stream = (async function* () {
for await (let chunk of stream) {
yield chunk.content;
}
})();
let self = this;
(async () => {
for await (let chunk of self.promise.getStream()) {
if (chunk.promptTokens) {
self.promptTokens = chunk.promptTokens;
}
if (chunk.completionTokens) {
self.completionTokens = chunk.completionTokens;
}
if (chunk.totalTokens) {
self.totalTokens = chunk.totalTokens;
}
}
})();
}
toString() {
return this.content;
}
toJSON() {
return {
id: this.id,
role: this.role,
content: this.content,
};
}
}
exports.RetortMessage = RetortMessage;
function templateContent(templateStrings, ...values) {
// Get the strings in raw form.
let strings = templateStrings.raw.map((x) => x);
// Remove carriage returns from strings.
strings = strings.map((str) => str.replace(/\r/g, ""));
// Remove any whitespace at the start of a line in each of the strings - except that which is explicitly specified.
strings = strings.map((str) => str.replace(/\n[^\S\r\n]+/g, "\n"));
// Remove any whitespace at the end of a line in each of the strings - except that which is explicitly specified.
strings = strings.map((str) => str.replace(/[^\S\r\n]+\n/g, "\n"));
// Allow line continuations. TODO: Allow line continuations with an even number of preceding backslashes.
strings = strings.map((str) => str.replace(/(?<!\\)((?:\\\\)*)\\\n/g, "$1"));
// Remove leading whitespace from the first string, if any.
strings[0] = (strings[0] || "").trimStart();
// Remove trailing whitespace from the last string, if any.
strings[strings.length - 1] = (strings[strings.length - 1] || "").trimEnd();
// Now, finally, encode the strings.
strings = strings.map((str) => unescape(str));
let content = strings[0] || "";
for (let i = 1, l = strings.length; i < l; i++) {
let currentValue = values[i - 1];
let insertion = retortValueToString(currentValue);
content += insertion;
content += strings[i] || "";
}
return content;
}
exports.templateContent = templateContent;
function retortValueToString(currentValue) {
let insertion = "";
if (currentValue === null) {
insertion = "";
}
else if (typeof currentValue === "number") {
insertion = currentValue.toString();
}
else if (typeof currentValue === "string") {
insertion = currentValue;
}
else if (typeof currentValue === "boolean") {
insertion = currentValue.toString();
}
else if (typeof currentValue === "object") {
if (currentValue.toString === {}.toString) {
// Check if the object is thenable
if (currentValue.then && typeof currentValue.then === "function") {
throw new Error("Promise passed to retort template. Use 'await' on the promise.");
}
else {
throw new Error("Plain object passed to retort template. If you want to see the object, you should use JSON.stringify.");
}
}
insertion = currentValue.toString();
}
else if (currentValue === undefined) {
throw new Error("Undefined passed to retort template");
}
else if (typeof currentValue === "function") {
throw new Error("Function passed to retort template");
}
else if (typeof currentValue === "symbol") {
throw new Error("Symbol passed to retort template");
}
else if (typeof currentValue === "bigint") {
throw new Error("BigInt not yet supported");
}
else {
throw new Error("Unsupported value inserted into template");
}
return insertion;
}
function isTemplateStringsArray(templateStrings) {
return (Array.isArray(templateStrings) &&
Array.isArray(templateStrings.raw));
}
exports.isTemplateStringsArray = isTemplateStringsArray;
function unescape(str) {
let segments = str
.split(/(\\x[a-fA-F0-9]{2}|\\u[a-fA-F0-9]{4}|\\u\{[a-fA-F0-9]{1,6}\}|\\.)/g)
.filter((x) => x);
return segments.map(unescapeSegment).join("");
}
exports.unescape = unescape;
function unescapeSegment(str) {
if (str.startsWith("\\")) {
if (str[1] === "x") {
// Use a regex to check that this is a valid escape sequence
if (!/\\x[a-fA-F0-9][a-fA-F0-9]/.test(str)) {
throw new Error(`Malformed Latin-1 escape sequence; should be like "\\x00"`);
}
return String.fromCharCode(parseInt(str.slice(2), 16));
}
else if (str[1] === "u") {
if (str[2] === "{") {
// Use a regex to check that this is a valid escape sequence
if (!/\\u\{[a-fA-F0-9]{1,6}\}/.test(str)) {
throw new Error(`Malformed unicode escape sequence; should be like "\\u{000000} or "\\u0000`);
}
return String.fromCodePoint(parseInt(str.slice(3, -1), 16));
}
else {
// Use a regex to check that this is a valid escape sequence
if (!/\\u[a-fA-F0-9]{4}/.test(str)) {
throw new Error(`Malformed unicode escape sequence; should be like "\\u{000000} or "\\u0000`);
}
return String.fromCharCode(parseInt(str.slice(2, 6), 16));
}
}
else if (str[1] === "n") {
return "\n";
}
else if (str[1] === "r") {
return "\r";
}
else if (str[1] === "t") {
return "\t";
}
else if (str[1] === "\\") {
return "\\";
}
else if (str[1] === "s") {
// Special space escaping behaviour.
return " ";
}
else {
throw new Error(`Unsupported escape sequence: "${str}"`);
}
}
else {
return str;
}
}
exports.unescapeSegment = unescapeSegment;