commit 98f0df29d75141fd5ff53b14f17f113314122d72 Author: Sean Sullivan Date: Tue Feb 7 14:25:58 2023 -0800 initial commit diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..969b3b9c --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,38 @@ +module.exports = { + extends: [ + "airbnb-base", + "eslint:recommended", + "prettier", + "plugin:@typescript-eslint/recommended", + ], + parserOptions: { + ecmaVersion: 12, + parser: "@typescript-eslint/parser", + sourceType: "module", + }, + plugins: ["@typescript-eslint"], + rules: { + "@typescript-eslint/explicit-module-boundary-types": 0, + "@typescript-eslint/no-empty-function": 0, + "@typescript-eslint/no-shadow": ["error"], + "@typescript-eslint/no-use-before-define": ["error", "nofunc"], + "@typescript-eslint/no-unused-vars": ["warn", { args: "none" }], + "comma-dangle": ["error", "never"], + "camelcase": 0, + "class-methods-use-this": 0, + "import/extensions": 0, + "import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*.test.ts"]}], + "import/no-unresolved": 0, + "import/prefer-default-export": 0, + "keyword-spacing": "error", + "max-classes-per-file": 0, + "max-len": ["error", { code: 100, tabWidth: 2, ignoreComments: true }], + "no-bitwise": "off", + "no-console": 0, + "no-restricted-syntax": 0, + "no-shadow": 0, + "no-underscore-dangle": 0, + "no-use-before-define": 0, + "semi": ["error", "always"], + }, +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..3c897f3a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +lib/ +.turbo +.eslintcache diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000..be539834 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +yarn precommit diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..b629d375 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +babel.config.js +jest.config.js +.eslintrc.js diff --git a/README.md b/README.md new file mode 100644 index 00000000..e8b0c9b8 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# langchainjs diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 00000000..61a95b56 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,2 @@ +//babel.config.js +module.exports = {presets: ['@babel/preset-env']} diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..c326ea93 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,11 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest/presets/js-with-ts', + testEnvironment: 'node', + transform: { + "^.+\\.(ts|tsx)$": "ts-jest", + "^.+\\.(js)$": "babel-jest", + }, + transformIgnorePatterns: [ + ], +}; diff --git a/package.json b/package.json new file mode 100644 index 00000000..11b5499a --- /dev/null +++ b/package.json @@ -0,0 +1,51 @@ +{ + "name": "langchain", + "version": "0.0.1", + "description": "Typescript bindings for langchain", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build": "tsc --declaration --outDir dist/", + "lint": "eslint src", + "lint:fix": "yarn lint --fix", + "precommit": "tsc --noEmit && lint-staged", + "clean": "rm -rf dist/", + "prepack": "yarn build", + "test": "jest", + "prepare": "husky install" + }, + "author": "Langchain", + "license": "MIT", + "devDependencies": { + "@babel/preset-env": "^7.20.2", + "@jest/globals": "^29.4.2", + "@tsconfig/recommended": "^1.0.2", + "@typescript-eslint/eslint-plugin": "^5.51.0", + "@typescript-eslint/parser": "^5.51.0", + "babel-jest": "^29.4.2", + "eslint": "^8.33.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-prettier": "^8.6.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-prettier": "^4.2.1", + "husky": "^8.0.3", + "jest": "^29.4.2", + "lint-staged": "^13.1.1", + "prettier": "^2.8.3", + "ts-jest": "^29.0.5", + "typescript": "^4.9.5" + }, + "dependencies": { + "node-fetch": "^3.3.0", + "yaml": "^2.2.1" + }, + "lint-staged": { + "src/**/*.{ts,tsx}": [ + "prettier --write --ignore-unknown", + "eslint --cache --fix" + ] + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..3be5fd4f --- /dev/null +++ b/src/index.ts @@ -0,0 +1,5 @@ +export { + PromptTemplate, + BasePromptTemplate, + FewShotPromptTemplate, +} from "./prompt"; diff --git a/src/prompt/base.ts b/src/prompt/base.ts new file mode 100644 index 00000000..ce2aabd7 --- /dev/null +++ b/src/prompt/base.ts @@ -0,0 +1,68 @@ +import { BaseOutputParser } from "./parser"; +import { + PromptTemplateInput, + SerializedPromptTemplate, + PromptTemplate, + FewShotPromptTemplateInput, + SerializedFewShotTemplate, + FewShotPromptTemplate, +} from "./index"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type InputValues = Record; + +type SerializedBasePromptTemplate = + | SerializedPromptTemplate + | SerializedFewShotTemplate + | (Omit & { _type: undefined }); + +export interface BasePromptTemplateInput { + inputVariables: string[]; + outputParser?: BaseOutputParser; +} + +type ConstructorInput = FewShotPromptTemplateInput | PromptTemplateInput; + +export abstract class BasePromptTemplate implements BasePromptTemplateInput { + inputVariables: string[]; + + outputParser?: BaseOutputParser; + + constructor(input: ConstructorInput) { + const { inputVariables } = input; + if (inputVariables.includes("stop")) { + throw new Error( + "Cannot have an input variable named 'stop', as it is used internally, please rename." + ); + } + Object.assign(this, input); + } + + abstract format(values: InputValues): string; + + abstract _getPromptType(): string; + + abstract serialize(): SerializedBasePromptTemplate; + + // Deserializing needs to be async because templates (e.g. few_shot) can + // reference remote resources that we read asynchronously with a web + // request. + static async deserialize( + data: SerializedBasePromptTemplate + ): Promise { + switch (data._type) { + case "prompt": + return PromptTemplate.deserialize(data); + case undefined: + return PromptTemplate.deserialize({ ...data, _type: "prompt" }); + case "few_shot": + return FewShotPromptTemplate.deserialize(data); + default: + throw new Error( + `Invalid prompt type in config: ${ + (data as SerializedBasePromptTemplate)._type + }` + ); + } + } +} diff --git a/src/prompt/few_shot.ts b/src/prompt/few_shot.ts new file mode 100644 index 00000000..ca2f7e8a --- /dev/null +++ b/src/prompt/few_shot.ts @@ -0,0 +1,207 @@ +import fs from "fs"; +import path from "path"; +import * as yaml from "yaml"; + +import { + BasePromptTemplate, + InputValues, + BasePromptTemplateInput, +} from "./index"; +import { TemplateFormat, checkValidTemplate, renderTemplate } from "./template"; +import { resolveTemplate, loadPrompt } from "./load"; +import { PromptTemplate, SerializedPromptTemplate } from "./prompt"; +import { SerializedOutputParser, BaseOutputParser } from "./parser"; + +// TODO: support ExampleSelectors. +type ExampleSelector = null; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Example = Record; + +export type SerializedFewShotTemplate = { + _type: "few_shot"; + input_variables: string[]; + output_parser?: SerializedOutputParser; + examples: string | Example[]; + example_prompt?: SerializedPromptTemplate; + example_prompt_path?: string; + example_separator: string; + prefix?: string; + prefix_path?: string; + suffix?: string; + suffix_path?: string; + template_format: TemplateFormat; +}; + +export interface FewShotPromptTemplateInput extends BasePromptTemplateInput { + examples?: Example[]; + examplePrompt: PromptTemplate; + exampleSelector?: ExampleSelector; + exampleSeparator: string; + prefix: string; + suffix: string; + templateFormat: TemplateFormat; + validateTemplate?: boolean; +} + +export class FewShotPromptTemplate + extends BasePromptTemplate + implements FewShotPromptTemplateInput +{ + examples?: InputValues[]; + + exampleSelector?: ExampleSelector; + + examplePrompt: PromptTemplate; + + suffix: string; + + exampleSeparator: string; + + prefix: string; + + templateFormat: TemplateFormat = "f-string"; + + validateTemplate = true; + + constructor(input: FewShotPromptTemplateInput) { + super(input); + Object.assign(this, input); + + if (this.examples !== undefined && this.exampleSelector !== undefined) { + throw new Error( + "Only one of 'examples' and 'example_selector' should be provided" + ); + } + + if (this.examples === undefined && this.exampleSelector === undefined) { + throw new Error( + "One of 'examples' and 'example_selector' should be provided" + ); + } + + if (this.validateTemplate) { + checkValidTemplate( + this.prefix + this.suffix, + this.templateFormat, + this.inputVariables + ); + } + } + + _getPromptType(): "few_shot" { + return "few_shot"; + } + + private getExamples(_: InputValues): InputValues[] { + if (this.examples !== undefined) { + return this.examples; + } + if (this.exampleSelector !== undefined) { + throw new Error("Example selectors are not yet supported."); + } + + throw new Error( + "One of 'examples' and 'example_selector' should be provided" + ); + } + + format(values: InputValues): string { + const examples = this.getExamples(values); + + const exampleStrings = examples.map((example) => + this.examplePrompt.format(example) + ); + const template = [this.prefix, ...exampleStrings, this.suffix].join( + this.exampleSeparator + ); + return renderTemplate(template, this.templateFormat, values); + } + + serialize(): SerializedFewShotTemplate { + if (this.exampleSelector || !this.examples) { + throw new Error( + "Serializing an example selector is not currently supported" + ); + } + return { + _type: this._getPromptType(), + input_variables: this.inputVariables, + output_parser: this.outputParser?.serialize(), + example_prompt: this.examplePrompt.serialize(), + example_separator: this.exampleSeparator, + suffix: this.suffix, + prefix: this.prefix, + template_format: this.templateFormat, + examples: this.examples, + }; + } + + static async deserialize( + data: SerializedFewShotTemplate + ): Promise { + const { + prefix, + prefix_path, + suffix, + suffix_path, + example_prompt, + example_prompt_path, + } = data; + + if (example_prompt_path !== undefined && example_prompt !== undefined) { + throw new Error( + "Only one of example_prompt and example_prompt_path should be specified." + ); + } + + let examplePrompt: PromptTemplate; + + if (example_prompt_path !== undefined) { + examplePrompt = (await loadPrompt(example_prompt_path)) as PromptTemplate; + } else if (example_prompt !== undefined) { + examplePrompt = await PromptTemplate.deserialize(example_prompt); + } else { + throw new Error( + "One of example_prompt and example_prompt_path should be specified." + ); + } + + let examples: Example[]; + + if (typeof data.examples === "string") { + const content = fs.readFileSync(data.examples).toString(); + switch (path.extname(data.examples)) { + case ".json": + examples = JSON.parse(content); + break; + case ".yml": + case ".yaml": + examples = yaml.parse(content); + break; + default: + throw new Error( + "Invalid file format. Only json or yaml formats are supported." + ); + } + } else if (Array.isArray(data.examples)) { + examples = data.examples; + } else { + throw new Error( + "Invalid examples format. Only list or string are supported." + ); + } + + return new FewShotPromptTemplate({ + inputVariables: data.input_variables, + outputParser: + data.output_parser && BaseOutputParser.deserialize(data.output_parser), + examplePrompt, + examples, + exampleSeparator: data.example_separator, + prefix: resolveTemplate("prefix", prefix, prefix_path), + suffix: resolveTemplate("suffix", suffix, suffix_path), + templateFormat: data.template_format, + }); + } +} diff --git a/src/prompt/index.ts b/src/prompt/index.ts new file mode 100644 index 00000000..77216dea --- /dev/null +++ b/src/prompt/index.ts @@ -0,0 +1,15 @@ +export { + BasePromptTemplate, + BasePromptTemplateInput, + InputValues, +} from "./base"; +export { + PromptTemplate, + PromptTemplateInput, + SerializedPromptTemplate, +} from "./prompt"; +export { + FewShotPromptTemplate, + FewShotPromptTemplateInput, + SerializedFewShotTemplate, +} from "./few_shot"; diff --git a/src/prompt/load.ts b/src/prompt/load.ts new file mode 100644 index 00000000..f8a34365 --- /dev/null +++ b/src/prompt/load.ts @@ -0,0 +1,71 @@ +import path from "path"; +import fs from "fs"; +import * as yaml from "yaml"; + +import { BasePromptTemplate } from "."; +import { loadFromHub } from "../util/hub"; + +export const resolveTemplate = ( + fieldName: string, + template?: string, + templatePath?: string +) => { + if (templatePath !== undefined && template !== undefined) { + throw new Error( + `Both '${fieldName}_path' and '${fieldName}' cannot be provided.` + ); + } + + if (template !== undefined) { + return template; + } + + if (templatePath !== undefined) { + if (path.extname(templatePath) !== ".txt") { + throw new Error("Invalid file type"); + } + + return fs.readFileSync(templatePath).toString(); + } + + throw new Error( + `One of '${fieldName}_path' and '${fieldName}' must be provided.` + ); +}; + +const loadPromptFromFile = async ( + file: string +): Promise => { + const suffix = path.extname(file); + let config; + + if (suffix === ".json") { + const data = fs.readFileSync(file); + config = JSON.parse(data.toString()); + } else if (suffix === ".yaml") { + const data = fs.readFileSync(file); + const str = data.toString(); + config = yaml.parse(str); + } else if (suffix === ".py") { + throw new Error( + "Could not load spec. Loading python resources not yet supported." + ); + } else { + throw new Error(`Got unsupported file type ${suffix}`); + } + return BasePromptTemplate.deserialize(config); +}; + +export const loadPrompt = async (uri: string): Promise => { + const hubResult = await loadFromHub( + uri, + loadPromptFromFile, + "prompts", + new Set(["py", "json", "yaml"]) + ); + if (hubResult) { + return hubResult; + } + + return loadPromptFromFile(uri); +}; diff --git a/src/prompt/parser.ts b/src/prompt/parser.ts new file mode 100644 index 00000000..a9e0ab12 --- /dev/null +++ b/src/prompt/parser.ts @@ -0,0 +1,115 @@ +export type SerializedOutputParser = + | SerializedRegexParser + | SerializedCommaSeparatedListOutputParser; + +export abstract class BaseOutputParser { + abstract parse(text: string): string | string[] | Record; + + _type(): string { + throw new Error("_type not implemented"); + } + + abstract serialize(): SerializedOutputParser; + + static deserialize(data: SerializedOutputParser): BaseOutputParser { + switch (data._type) { + case "regex_parser": + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return RegexParser.deserialize(data); + default: + throw new Error(`Unknown parser type: ${data._type}`); + } + } +} + +export abstract class ListOutputParser extends BaseOutputParser { + abstract parse(text: string): string[]; +} + +type SerializedCommaSeparatedListOutputParser = { + _type: "comma_separated_list"; +}; + +export class CommaSeparatedListOutputParser extends ListOutputParser { + parse(text: string): string[] { + return text.trim().split(", "); + } + + serialize(): SerializedCommaSeparatedListOutputParser { + return { + _type: "comma_separated_list", + }; + } + + static deserialize( + _: SerializedCommaSeparatedListOutputParser + ): CommaSeparatedListOutputParser { + return new CommaSeparatedListOutputParser(); + } +} + +type SerializedRegexParser = { + _type: "regex_parser"; + regex: string; + output_keys: string[]; + default_output_key?: string; +}; + +export class RegexParser extends BaseOutputParser { + regex: string | RegExp; + + outputKeys: string[]; + + defaultOutputKey?: string; + + constructor( + regex: string | RegExp, + outputKeys: string[], + defaultOutputKey?: string + ) { + super(); + this.regex = regex; + this.outputKeys = outputKeys; + this.defaultOutputKey = defaultOutputKey; + } + + _type() { + return "regex_parser"; + } + + parse(text: string): Record { + const match = text.match(this.regex); + if (match) { + return this.outputKeys.reduce((acc, key, index) => { + acc[key] = match[index + 1]; + return acc; + }, {} as Record); + } + + if (this.defaultOutputKey === undefined) { + throw new Error(`Could not parse output: ${text}`); + } + + return this.outputKeys.reduce((acc, key) => { + acc[key] = key === this.defaultOutputKey ? text : ""; + return acc; + }, {} as Record); + } + + serialize() { + return { + _type: "regex_parser" as const, + regex: typeof this.regex === "string" ? this.regex : this.regex.source, + output_keys: this.outputKeys, + default_output_key: this.defaultOutputKey, + }; + } + + static deserialize(data: SerializedRegexParser): RegexParser { + return new RegexParser( + data.regex, + data.output_keys, + data.default_output_key + ); + } +} diff --git a/src/prompt/prompt.ts b/src/prompt/prompt.ts new file mode 100644 index 00000000..45edac0d --- /dev/null +++ b/src/prompt/prompt.ts @@ -0,0 +1,114 @@ +import { + BasePromptTemplate, + BasePromptTemplateInput, + InputValues, +} from "./index"; +import { + TemplateFormat, + checkValidTemplate, + renderTemplate, + parseFString, +} from "./template"; +import { resolveTemplate } from "./load"; +import { SerializedOutputParser, BaseOutputParser } from "./parser"; + +export type SerializedPromptTemplate = { + _type: "prompt"; + input_variables: string[]; + output_parser?: SerializedOutputParser; + template?: string; + template_path?: string; + template_format?: TemplateFormat; +}; + +export interface PromptTemplateInput extends BasePromptTemplateInput { + template: string; + templateFormat?: TemplateFormat; + validateTemplate?: boolean; +} + +export class PromptTemplate + extends BasePromptTemplate + implements PromptTemplateInput +{ + template: string; + + templateFormat: TemplateFormat = "f-string"; + + validateTemplate = true; + + constructor(input: PromptTemplateInput) { + super(input); + Object.assign(this, input); + + if (this.validateTemplate) { + checkValidTemplate( + this.template, + this.templateFormat, + this.inputVariables + ); + } + } + + _getPromptType(): "prompt" { + return "prompt"; + } + + format(values: InputValues): string { + return renderTemplate(this.template, this.templateFormat, values); + } + + static fromExamples( + examples: string[], + suffix: string, + inputVariables: string[], + exampleSeparator = "\n\n", + prefix = "" + ) { + const template = [prefix, ...examples, suffix].join(exampleSeparator); + return new PromptTemplate({ + inputVariables, + template, + }); + } + + static fromTemplate(template: string) { + const names = new Set(); + parseFString(template).forEach((node) => { + if (node.type === "variable") { + names.add(node.name); + } + }); + + return new PromptTemplate({ + inputVariables: [...names], + template, + }); + } + + serialize(): SerializedPromptTemplate { + return { + _type: this._getPromptType(), + input_variables: this.inputVariables, + output_parser: this.outputParser?.serialize(), + template: this.template, + template_format: this.templateFormat, + }; + } + + static async deserialize( + data: SerializedPromptTemplate + ): Promise { + const { template, template_path } = data; + const res = new PromptTemplate({ + inputVariables: data.input_variables, + outputParser: + data.output_parser && BaseOutputParser.deserialize(data.output_parser), + template: resolveTemplate("template", template, template_path), + templateFormat: data.template_format, + }); + return res; + } + + // TODO(from file) +} diff --git a/src/prompt/template.ts b/src/prompt/template.ts new file mode 100644 index 00000000..14f25c05 --- /dev/null +++ b/src/prompt/template.ts @@ -0,0 +1,104 @@ +import { InputValues } from "./index"; + +export type TemplateFormat = "f-string" | "jinja2"; + +type ParsedFStringNode = + | { type: "literal"; text: string } + | { type: "variable"; name: string }; + +export const parseFString = (template: string): ParsedFStringNode[] => { + // Core logic replicated from internals of pythons built in Formatter class. + // https://github.com/python/cpython/blob/135ec7cefbaffd516b77362ad2b2ad1025af462e/Objects/stringlib/unicode_format.h#L700-L706 + const chars = template.split(""); + const nodes: ParsedFStringNode[] = []; + + const nextBracket = (bracket: "}" | "{" | "{}", start: number) => { + for (let i = start; i < chars.length; i += 1) { + if (bracket.includes(chars[i])) { + return i; + } + } + return -1; + }; + + let i = 0; + while (i < chars.length) { + if (chars[i] === "{" && i + 1 < chars.length && chars[i + 1] === "{") { + nodes.push({ type: "literal", text: "{" }); + i += 2; + } else if ( + chars[i] === "}" && + i + 1 < chars.length && + chars[i + 1] === "}" + ) { + nodes.push({ type: "literal", text: "}" }); + i += 2; + } else if (chars[i] === "{") { + const j = nextBracket("}", i); + if (j < 0) { + throw new Error("Unclosed '{' in template."); + } + + nodes.push({ + type: "variable", + name: chars.slice(i + 1, j).join(""), + }); + i = j + 1; + } else if (chars[i] === "}") { + throw new Error("Single '}' in template."); + } else { + const next = nextBracket("{}", i); + const text = (next < 0 ? chars.slice(i) : chars.slice(i, next)).join(""); + nodes.push({ type: "literal", text }); + i = next < 0 ? chars.length : next; + } + } + + return nodes; +}; + +export const interpolateFString = (template: string, values: InputValues) => + parseFString(template).reduce((res, node) => { + if (node.type === "variable") { + if (node.name in values) { + return res + values[node.name]; + } + throw new Error(`Missing value for input ${node.name}`); + } + + return res + node.text; + }, ""); + +type Interpolator = (template: string, values: InputValues) => string; + +export const DEFAULT_FORMATTER_MAPPING: Record = { + "f-string": interpolateFString, + jinja2: (_: string, __: InputValues) => "", +}; + +export const renderTemplate = ( + template: string, + templateFormat: TemplateFormat, + inputValues: InputValues +) => DEFAULT_FORMATTER_MAPPING[templateFormat](template, inputValues); + +export const checkValidTemplate = ( + template: string, + templateFormat: TemplateFormat, + inputVariables: string[] +) => { + if (!(templateFormat in DEFAULT_FORMATTER_MAPPING)) { + const validFormats = Object.keys(DEFAULT_FORMATTER_MAPPING); + throw new Error(`Invalid template format. Got \`${templateFormat}\`; + should be one of ${validFormats}`); + } + try { + const dummyInputs: InputValues = inputVariables.reduce((acc, v) => { + acc[v] = "foo"; + return acc; + }, {} as Record); + renderTemplate(template, templateFormat, dummyInputs); + } catch { + throw new Error("Invalid prompt schema."); + } +}; diff --git a/src/prompt/tests/load.test.ts b/src/prompt/tests/load.test.ts new file mode 100644 index 00000000..dfc6d291 --- /dev/null +++ b/src/prompt/tests/load.test.ts @@ -0,0 +1,20 @@ +import { expect, test } from "@jest/globals"; +import path from "path"; +import { loadPrompt } from "../load"; + +const PROMPTS_DIR = path.join(__dirname, "prompts"); + +test("Load Hello World Prompt", async () => { + const helloWorld = path.join(PROMPTS_DIR, "hello_world.yaml"); + const prompt = await loadPrompt(helloWorld); + expect(prompt._getPromptType()).toBe("prompt"); + expect(prompt.format({})).toBe("Say hello world."); +}); + +test("Load hub prompt", async () => { + const prompt = await loadPrompt( + "lc@abb92d8://prompts/hello-world/prompt.yaml" + ); + expect(prompt._getPromptType()).toBe("prompt"); + expect(prompt.format({})).toBe("Say hello world."); +}); diff --git a/src/prompt/tests/prompts/hello_world.yaml b/src/prompt/tests/prompts/hello_world.yaml new file mode 100644 index 00000000..62f07f09 --- /dev/null +++ b/src/prompt/tests/prompts/hello_world.yaml @@ -0,0 +1,4 @@ +input_variables: [] +output_parser: null +template: "Say hello world." +template_format: f-string diff --git a/src/prompt/tests/template.test.ts b/src/prompt/tests/template.test.ts new file mode 100644 index 00000000..291cc6d2 --- /dev/null +++ b/src/prompt/tests/template.test.ts @@ -0,0 +1,26 @@ +import { expect, test, describe } from "@jest/globals"; +import { interpolateFString } from "../template"; + +describe.each([ + ["{foo}", { foo: "bar" }, "bar"], + ["pre{foo}post", { foo: "bar" }, "prebarpost"], + ["{{pre{foo}post}}", { foo: "bar" }, "{prebarpost}"], + ["text", {}, "text"], + ["}}{{", {}, "}{"], + ["{first}_{second}", { first: "foo", second: "bar" }, "foo_bar"], +])("Valid f-string", (template, variables, result) => { + test(`Interpolation works: ${template}`, () => { + expect(interpolateFString(template, variables)).toBe(result); + }); +}); + +describe.each([ + ["{", {}], + ["}", {}], + ["{foo", {}], + ["foo}", {}], +])("Invalid f-string", (template, variables) => { + test(`Interpolation throws: ${template}`, () => { + expect(() => interpolateFString(template, variables)).toThrow(); + }); +}); diff --git a/src/util/hub.ts b/src/util/hub.ts new file mode 100644 index 00000000..d5de13d5 --- /dev/null +++ b/src/util/hub.ts @@ -0,0 +1,56 @@ +import path from "path"; +import fetch, { RequestInit } from "node-fetch"; +import os from "os"; +import fs from "fs"; + +const HUB_PATH_REGEX = /lc(@[^:]+)?:\/\/(.*)/; +const DEFAULT_REF = process.env.LANGCHAIN_HUB_DEFAULT_REF ?? "master"; +const URL_BASE = + process.env.LANGCHAIN_HUB_URL_BASE ?? + "https://raw.githubusercontent.com/hwchase17/langchain-hub/"; + +const fetchWithTimeout = async ( + url: string, + init: Omit & { timeout: number } +) => { + const { timeout, ...rest } = init; + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + const res = await fetch(url, { ...rest, signal: controller.signal }); + clearTimeout(timeoutId); + return res; +}; + +export const loadFromHub = async ( + uri: string, + loader: (a: string) => T, + validPrefix: string, + validSuffixes: Set +): Promise => { + const match = uri.match(HUB_PATH_REGEX); + if (!match) { + return undefined; + } + const [rawRef, remotePath] = match.slice(1); + const ref = rawRef ? rawRef.slice(1) : DEFAULT_REF; + const parts = remotePath.split(path.sep); + if (parts[0] !== validPrefix) { + return undefined; + } + + if (!validSuffixes.has(path.extname(remotePath).slice(1))) { + throw new Error("Unsupported file type."); + } + + const url = [URL_BASE, ref, remotePath].join("/"); + const res = await fetchWithTimeout(url, { timeout: 5000 }); + if (res.status !== 200) { + throw new Error(`Could not find file at ${url}`); + } + + const text = await res.text(); + const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "langchain")); + const file = path.join(tmpdir, path.basename(remotePath)); + fs.writeFileSync(file, text); + return loader(file); +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..ca05404b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "@tsconfig/recommended", + "compilerOptions": { + "outDir": "dist", + "target": "ESNext", + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "node", + "sourceMap": true, + "allowSyntheticDefaultImports": true, + "baseUrl": "./src", + "declaration": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "useDefineForClassFields": true, + "strictPropertyInitialization": false + }, + "exclude": ["node_modules/", "dist/", "tests/"], + "include": ["./src"] +}