├── .gitignore ├── LICENSE.md ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── index.ts ├── lib │ ├── Generator.ts │ ├── Prompt.ts │ └── index.ts └── tests │ ├── Generator.test.ts │ └── Prompt.test.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | .DS_Store 133 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jacob Simon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # prompting 2 | 3 | [![npm version](http://img.shields.io/npm/v/prompting.svg)](https://npmjs.org/package/prompting) 4 | 5 | > *A batteries-included, model-agnostic prompt engineering library for Node.js and TypeScript.* 6 | 7 | Build dynamic, reusable prompts that output structured data that's ready to use in your application or API. Compatible with all text-based generative language models such as OpenAI GPT. 8 | 9 | ## Features 10 | 11 | * Intuitive, flexible `Prompt` builder 12 | * Reusable prompt templates with variables 13 | * Validated output in JSON or CSV 14 | * Model-agnostic, extensible generation API 15 | * Serializable to database and files 16 | 17 | ## Installation 18 | 19 | To install `prompting`, use npm: 20 | 21 | ```bash 22 | npm install prompting 23 | ``` 24 | 25 | ## Examples 26 | 27 | ### Simple text prompt 28 | 29 | ```typescript 30 | import {Prompt} from 'prompting'; 31 | 32 | const prompt = Prompt().text('What is your favorite animal?') 33 | 34 | console.log(prompt.toString(); // 'What is your favorite animal?' 35 | ``` 36 | 37 | ### Using template variables and default values 38 | 39 | ```typescript 40 | import {Prompt} from 'prompting'; 41 | 42 | const prompt = Prompt() 43 | .text('What is your favorite {{topic}}?') 44 | .defaults({topic: 'animal'}); 45 | 46 | prompt.toString(); // 'What is your favorite animal?' 47 | prompt.vars({topic: 'color'}).toString(); // 'What is your favorite color?' 48 | ``` 49 | 50 | ### Generating prompt responses 51 | 52 | The library also contains a flexible `Generator` class for generating responses to a `Prompt`. For convenience, the `Generator.prompt()` method creates a new prompt that is bound to the `Generator` instance and can be invoked by calling `generate()`. 53 | 54 | Here's an example using the `OpenAIGenerator`: 55 | 56 | ```typescript 57 | import {OpenAIGenerator} from 'prompting'; 58 | 59 | const gpt = new OpenAIGenerator({apiKey: 'my_api_key'}); 60 | 61 | const prompt = gpt.prompt().text('What is your favorite {{topic}}?'); 62 | 63 | const result = await prompt.generate({topic: 'color'}); 64 | ``` 65 | 66 | The `generate` method returns a Promise that resolves to the model's response for the prompt. 67 | 68 | ### Structured JSON data with validation 69 | 70 | To output a structured object and validate the result automatically, construct your prompt using the `schema` method. The `Prompt` class leverages the power of JSON Schema and the battle-tested validation library [`ajv`](https://ajv.js.org/) to validate the response. 71 | 72 | ```typescript 73 | const prompt = Prompt() 74 | .text('List {{num}} books by the author {{author}}.') 75 | .defaults({num: 3}) 76 | .schema({ 77 | type: 'array', 78 | items: { 79 | type: 'object', 80 | properties: { 81 | title: {type: 'string'}, 82 | year: {type: 'string'}, 83 | }, 84 | required: ['title', 'year'], 85 | }, 86 | }); 87 | 88 | const result = await prompt.generate({author: 'George Orwell'}); 89 | ``` 90 | 91 | The `generate` method returns a Promise that resolves to the model's response if it matches the schema, or rejects with a validation error if the model's response doesn't match the schema. 92 | 93 | ## TypeScript Support 94 | 95 | The library supports strongly typed prompts, arguments, and return types when used with TypeScript. The `Prompt` class supports generics to specify the expected arguments and return type. 96 | 97 | Here's an example: 98 | 99 | ```typescript 100 | import {Prompt} from 'prompting'; 101 | 102 | type BookVars = {author: string}; 103 | type Book = {title: string, year: string}; 104 | 105 | const prompt = Prompt() 106 | .text('What is the most popular book by {{author}}?') 107 | .schema({ 108 | type: 'object', 109 | properties: { 110 | title: {type: 'string'}, 111 | year: {type: 'string'}, 112 | }, 113 | required: ['title', 'year'], 114 | }); 115 | 116 | const result: Book = await prompt.generate({author: 'George Orwell'}); 117 | ``` 118 | 119 | In this example, the `generate` method takes an argument of type `BookVars` and returns a Promise that resolves to a `Book` object, or rejects with a validation error if the model fails to generate a valid response. 120 | 121 | ## Prompt API 122 | 123 | | Method | Description | Usage 124 | | --- | --- | --- 125 | | `Prompt(options?: PromptOptions)` | Creates a new instance of the Prompt class. | Prompt() 126 | | `text(template: string)` | Sets the text template for the prompt. | prompt.text('What is your favorite {{topic}}?') 127 | | `defaults(defaults: object)` | Sets default values for the variables in the text template. | prompt.defaults({topic: 'animal'}) 128 | | `schema(schema: object)` | Sets the JSON schema for validating the generated result. | prompt.schema({type: 'string'}) 129 | | `generate(vars?: object)` | Generates the final prompt text by replacing variables in the template, then executes the generator to get the AI response. | prompt.generate({color: 'red'}) 130 | | `vars(vars: object)` | Returns a copy of the Prompt with variables preset but does not generate the result, e.g. in order to call `toString` | prompt.vars({topic: 'animal'}) 131 | | `using(generator: Generator)` | Sets the generator for the prompt so that `generate` can be called. | prompt.using(generator) 132 | | `toString()` | Returns the final prompt text by replacing variables in the template. | prompt.toString() 133 | | `toJSON()` | Returns the prompt as a JSON object, useful for serializing to a file or database. | prompt.toJSON() 134 | 135 | ### PromptOptions 136 | 137 | | Property | Type | Description 138 | | --- | --- | --- 139 | | `text` | string | The text template for the prompt. 140 | | `defaults` | object | Default values for the variables in the text template. 141 | | `schema` | object | The JSON schema for validating the generated result. 142 | | `generator` | Generator | The generator instance to use for executing the prompt. 143 | 144 | ## Contributing 145 | 146 | Contributions to `prompting` are welcome! To contribute, please fork the repository and make your changes, then submit a pull request. 147 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prompting", 3 | "version": "0.2.0", 4 | "description": "A prompt engineering library for Node.js and TypeScript", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "test": "jest" 9 | }, 10 | "keywords": [], 11 | "author": "Jacob Simon", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "@types/jest": "^27.5.2", 15 | "jest": "^27.1.1", 16 | "ts-jest": "^27.0.5", 17 | "typescript": "^4.4.3" 18 | }, 19 | "dependencies": { 20 | "ajv": "^8.12.0", 21 | "axios": "^0.21.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | Prompt, 3 | Generator, 4 | OpenAIGenerator, 5 | } from './lib'; 6 | -------------------------------------------------------------------------------- /src/lib/Generator.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import {PromptClass, PromptOptions} from './Prompt'; 4 | 5 | export interface GeneratorOptions { 6 | model?: string; 7 | baseUrl?: string; 8 | apiKey: string; 9 | execute?: (prompt: string) => Promise; 10 | } 11 | 12 | export class Generator { 13 | model?: string; 14 | baseUrl?: string; 15 | apiKey: string; 16 | 17 | constructor(options: GeneratorOptions) { 18 | this.model = options.model; 19 | this.apiKey = options.apiKey; 20 | this.baseUrl = options.baseUrl; 21 | this.execute = options.execute || this.execute; 22 | } 23 | 24 | public prompt(options?: PromptOptions): PromptClass { 25 | return new PromptClass({...options, generator: this}); 26 | } 27 | 28 | public async execute(prompt: string): Promise { 29 | throw new Error('Not implemented.'); 30 | } 31 | } 32 | 33 | export const DEFAULT_OPENAI_MODEL = 'text-davinci-003'; 34 | 35 | export function OpenAIGenerator(options: GeneratorOptions) { 36 | const execute = async (prompt: string, maxTokens = 300) => { 37 | const url = `https://api.openai.com/v1/completions`; 38 | const headers = { Authorization: `Bearer ${options.apiKey}`, 'Content-Type': 'application/json' }; 39 | const data = { 40 | prompt, 41 | model: options.model || DEFAULT_OPENAI_MODEL, 42 | max_tokens: maxTokens, 43 | }; 44 | 45 | const response = await axios.post(url, data, {headers}); 46 | 47 | if (!response.data?.choices?.[0]?.text) { 48 | throw new Error('Invalid response from OpenAI API'); 49 | } 50 | 51 | return response.data.choices[0].text.trim(); 52 | } 53 | 54 | return new Generator({...options, execute}); 55 | } 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/lib/Prompt.ts: -------------------------------------------------------------------------------- 1 | import Ajv, {JSONSchemaType, ValidateFunction} from 'ajv'; 2 | 3 | import {Generator} from './Generator'; 4 | 5 | export type PromptOptions = { 6 | generator?: Generator, 7 | text?: string, 8 | schema?: JSONSchemaType, 9 | defaults?: Partial, 10 | format?: string, 11 | }; 12 | 13 | export class PromptClass { 14 | private _text?: string; 15 | private _defaults: Partial; 16 | private _schema?: JSONSchemaType; 17 | private _vars?: T; 18 | private _generator?: Generator; 19 | private _validator?: ValidateFunction; 20 | private _format?: string; 21 | 22 | constructor(options: PromptOptions = {}) { 23 | this._generator = options.generator; 24 | this._text = options.text; 25 | this._defaults = options.defaults || {}; 26 | this._schema = options.schema; 27 | this._format = options.format || 'json'; 28 | 29 | if (options.schema) { 30 | this._validator = new Ajv().compile(options.schema); 31 | } 32 | } 33 | 34 | private describeSchema(schema: any = this._schema): any { 35 | if (!schema) return 'undefined'; 36 | 37 | switch (schema.type) { 38 | case 'object': { 39 | const props: any = {}; 40 | 41 | for (const key in schema.properties) { 42 | props[key] = this.describeSchema(schema.properties[key]); 43 | } 44 | 45 | return props; 46 | } 47 | case 'array': { 48 | const itemSchema = schema.items; 49 | return itemSchema.type === "object" 50 | ? [this.describeSchema(itemSchema)] 51 | : [itemSchema.type]; 52 | } 53 | default: 54 | return schema.type; 55 | } 56 | } 57 | 58 | private resolvePrompt(vars?: Partial): string { 59 | let prompt = this._text || ''; 60 | 61 | // Replace template variables with provided arguments or default values 62 | const allVars = {...this._defaults, ...(vars || this._vars)}; 63 | for (const [key, value] of Object.entries(allVars)) { 64 | const regex = new RegExp(`{{\\s*${key}\\s*}}`, 'g'); 65 | prompt = prompt.replace(regex, value as string); 66 | } 67 | 68 | // Check for any remaining template variables that were not replaced 69 | const unboundVars = prompt.match(/{{\s*[\w]+\s*}}/g); 70 | if (unboundVars) { 71 | throw new Error(`Missing template variables or default values: ${unboundVars.join(', ')}`); 72 | } 73 | 74 | if (this._schema) { 75 | prompt += ` Please provide a ${this._format} response with the following structure:\n`; 76 | prompt += JSON.stringify(this.describeSchema(), null, 2); 77 | } 78 | 79 | return prompt; 80 | } 81 | 82 | private validate(data: unknown): data is U { 83 | if (!this._validator) { 84 | throw new Error('No validator set up.'); 85 | } 86 | const valid = this._validator(data); 87 | if (!valid) { 88 | throw new Error(`Invalid data: ${this._validator.errors?.map(err => err.message).join(', ')}`); 89 | } 90 | return true; 91 | } 92 | 93 | public text(text: string): PromptClass { 94 | this._text = text; 95 | return this; 96 | } 97 | 98 | public defaults(values: Partial): PromptClass { 99 | this._defaults = values; 100 | return this; 101 | } 102 | 103 | public schema(schema: JSONSchemaType): PromptClass { 104 | this._schema = schema; 105 | this._validator = new Ajv().compile(schema); 106 | return this; 107 | } 108 | 109 | public async generate(vars?: T): Promise { 110 | if (!this._generator) { 111 | throw new Error('No generator was provided yet. Use prompt.using(generator) to set a generator or use Generator.prompt() to create new prompts.'); 112 | } 113 | 114 | const prompt = this.resolvePrompt(vars); 115 | const generatedText = await this._generator.execute(prompt); 116 | 117 | if (this._schema && this._validator) { 118 | const data: unknown = JSON.parse(generatedText); 119 | if (this.validate(data)) { 120 | return data; 121 | } 122 | } 123 | 124 | // Return string type if no schema is provided 125 | return generatedText as U; 126 | } 127 | 128 | public vars(vars: T): PromptClass { 129 | const newPrompt = new PromptClass({...this.toJSON()}); 130 | newPrompt._vars = vars; 131 | return newPrompt; 132 | } 133 | 134 | public using(generator: Generator): PromptClass { 135 | this._generator = generator; 136 | return this; 137 | } 138 | 139 | public toString(): string { 140 | return this.resolvePrompt(); 141 | } 142 | 143 | public toJSON(): PromptOptions { 144 | return { 145 | text: this._text, 146 | defaults: this._defaults, 147 | schema: this._schema, 148 | }; 149 | } 150 | 151 | public static fromJSON( 152 | data: PromptOptions, 153 | generator: Generator, 154 | ): PromptClass { 155 | return new PromptClass({...data}).using(generator); 156 | } 157 | } 158 | 159 | export function Prompt(options?: PromptOptions): PromptClass { 160 | return new PromptClass(options); 161 | } 162 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export {Prompt} from './Prompt'; 2 | export {Generator, OpenAIGenerator} from './Generator'; 3 | -------------------------------------------------------------------------------- /src/tests/Generator.test.ts: -------------------------------------------------------------------------------- 1 | import {OpenAIGenerator} from "../lib"; 2 | 3 | describe('Generator', () => { 4 | it.todo('should be tested'); 5 | }); 6 | -------------------------------------------------------------------------------- /src/tests/Prompt.test.ts: -------------------------------------------------------------------------------- 1 | import {Prompt} from '../lib'; 2 | 3 | describe('Prompt', () => { 4 | it('should generate a prompt', async () => { 5 | type AuthorPrompt = { 6 | name: string; 7 | num?: number; 8 | } 9 | 10 | type Book = { 11 | title: string; 12 | year: string; 13 | }; 14 | 15 | const prompt = Prompt({}) 16 | .text('List {{num}} books by {{name}}.') 17 | .defaults({num: 3}) 18 | .schema({ 19 | type: 'array', 20 | items: { 21 | type: 'object', 22 | properties: { 23 | title: {type: 'string'}, 24 | year: {type: 'string'}, 25 | }, 26 | required: ['title', 'year'], 27 | }, 28 | }).vars({name: 'George Orwell'}); 29 | 30 | expect(prompt.toString()).toEqual( 31 | `List 3 books by George Orwell. Please provide a json response with the following structure: 32 | [ 33 | { 34 | "title": "string", 35 | "year": "string" 36 | } 37 | ]` 38 | ); 39 | }); 40 | 41 | it('should throw error when required values are missing', () => { 42 | type TestPrompt = { value: string }; 43 | type TestResponse = { result: string }; 44 | const prompt = Prompt({}) 45 | .text('Hello, {{value}}.') 46 | .schema({ 47 | type: 'object', 48 | properties: { 49 | result: { type: 'string' }, 50 | }, 51 | required: ['result'], 52 | }); 53 | expect(() => prompt.toString()).toThrow(/missing/i); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "strict": true, 6 | "outDir": "dist", 7 | "rootDir": "src", 8 | "esModuleInterop": true 9 | }, 10 | "include": ["src"], 11 | "exclude": ["node_modules", "**/*.test.ts"] 12 | } 13 | --------------------------------------------------------------------------------