mirror of
https://github.com/Mintplex-Labs/langchainjs.git
synced 2026-07-01 12:17:38 -04:00
initial commit
This commit is contained in:
@@ -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"],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
lib/
|
||||
.turbo
|
||||
.eslintcache
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
yarn precommit
|
||||
@@ -0,0 +1,3 @@
|
||||
babel.config.js
|
||||
jest.config.js
|
||||
.eslintrc.js
|
||||
@@ -0,0 +1,2 @@
|
||||
//babel.config.js
|
||||
module.exports = {presets: ['@babel/preset-env']}
|
||||
@@ -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: [
|
||||
],
|
||||
};
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
PromptTemplate,
|
||||
BasePromptTemplate,
|
||||
FewShotPromptTemplate,
|
||||
} from "./prompt";
|
||||
@@ -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<string, any>;
|
||||
|
||||
type SerializedBasePromptTemplate =
|
||||
| SerializedPromptTemplate
|
||||
| SerializedFewShotTemplate
|
||||
| (Omit<SerializedPromptTemplate, "_type"> & { _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<BasePromptTemplate> {
|
||||
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
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<string, any>;
|
||||
|
||||
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<FewShotPromptTemplate> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export {
|
||||
BasePromptTemplate,
|
||||
BasePromptTemplateInput,
|
||||
InputValues,
|
||||
} from "./base";
|
||||
export {
|
||||
PromptTemplate,
|
||||
PromptTemplateInput,
|
||||
SerializedPromptTemplate,
|
||||
} from "./prompt";
|
||||
export {
|
||||
FewShotPromptTemplate,
|
||||
FewShotPromptTemplateInput,
|
||||
SerializedFewShotTemplate,
|
||||
} from "./few_shot";
|
||||
@@ -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<BasePromptTemplate> => {
|
||||
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<BasePromptTemplate> => {
|
||||
const hubResult = await loadFromHub(
|
||||
uri,
|
||||
loadPromptFromFile,
|
||||
"prompts",
|
||||
new Set(["py", "json", "yaml"])
|
||||
);
|
||||
if (hubResult) {
|
||||
return hubResult;
|
||||
}
|
||||
|
||||
return loadPromptFromFile(uri);
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
export type SerializedOutputParser =
|
||||
| SerializedRegexParser
|
||||
| SerializedCommaSeparatedListOutputParser;
|
||||
|
||||
export abstract class BaseOutputParser {
|
||||
abstract parse(text: string): string | string[] | Record<string, string>;
|
||||
|
||||
_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<string, string> {
|
||||
const match = text.match(this.regex);
|
||||
if (match) {
|
||||
return this.outputKeys.reduce((acc, key, index) => {
|
||||
acc[key] = match[index + 1];
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
}
|
||||
|
||||
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<string, string>);
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<string>();
|
||||
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<PromptTemplate> {
|
||||
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)
|
||||
}
|
||||
@@ -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<TemplateFormat, Interpolator> = {
|
||||
"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<string, string>);
|
||||
renderTemplate(template, templateFormat, dummyInputs);
|
||||
} catch {
|
||||
throw new Error("Invalid prompt schema.");
|
||||
}
|
||||
};
|
||||
@@ -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.");
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
input_variables: []
|
||||
output_parser: null
|
||||
template: "Say hello world."
|
||||
template_format: f-string
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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<RequestInit, "signal"> & { 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 <T>(
|
||||
uri: string,
|
||||
loader: (a: string) => T,
|
||||
validPrefix: string,
|
||||
validSuffixes: Set<string>
|
||||
): Promise<T | undefined> => {
|
||||
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);
|
||||
};
|
||||
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user