├── src ├── rpc │ ├── index.ts │ ├── types.ts │ ├── procedures.ts │ └── server.ts ├── index.rpc.ts ├── parser │ ├── utils │ │ ├── regex.ts │ │ ├── full_char_code_at.ts │ │ ├── acorn.ts │ │ ├── bracket.ts │ │ └── html.ts │ ├── state │ │ ├── multi_line_comment.ts │ │ ├── config.ts │ │ ├── fragment.ts │ │ └── text.ts │ ├── read │ │ ├── expression.ts │ │ └── context.ts │ ├── interfaces.ts │ └── index.ts ├── types │ ├── acorn.d.ts │ ├── index.ts │ ├── customTypes.ts │ └── message.ts ├── compiler │ ├── base │ │ ├── nodes │ │ │ ├── comment.ts │ │ │ ├── text.ts │ │ │ ├── config.ts │ │ │ ├── fragment.ts │ │ │ ├── tags │ │ │ │ ├── scope.ts │ │ │ │ ├── ref.ts │ │ │ │ ├── step.ts │ │ │ │ ├── message.ts │ │ │ │ ├── scope.test.ts │ │ │ │ ├── content.ts │ │ │ │ └── step.test.ts │ │ │ ├── if.ts │ │ │ ├── mustache.ts │ │ │ ├── for.ts │ │ │ ├── for.test.ts │ │ │ ├── if.test.ts │ │ │ └── tag.ts │ │ └── types.ts │ ├── logic │ │ ├── nodes │ │ │ ├── literal.ts │ │ │ ├── chainExpression.ts │ │ │ ├── sequenceExpression.ts │ │ │ ├── conditionalExpression.ts │ │ │ ├── identifier.ts │ │ │ ├── memberExpression.ts │ │ │ ├── unaryExpression.ts │ │ │ ├── binaryExpression.ts │ │ │ ├── arrayExpression.ts │ │ │ ├── callExpression.ts │ │ │ ├── objectExpression.ts │ │ │ ├── updateExpression.ts │ │ │ ├── index.ts │ │ │ └── assignmentExpression.ts │ │ ├── index.ts │ │ ├── types.ts │ │ └── operators.ts │ ├── types.ts │ ├── deserializeChain.test.ts │ ├── test │ │ └── helpers.ts │ ├── index.ts │ ├── deserializeChain.ts │ ├── utils.test.ts │ ├── scope.ts │ ├── chain-serialize.test.ts │ ├── errors.test.ts │ └── utils.ts ├── index.ts ├── test │ └── helpers.ts ├── providers │ ├── adapter.ts │ ├── openai-responses │ │ ├── utils │ │ │ └── parsers │ │ │ │ ├── reasoning.ts │ │ │ │ ├── webSearch.ts │ │ │ │ ├── fileSearch.ts │ │ │ │ ├── inputMessage.ts │ │ │ │ ├── functionCall.ts │ │ │ │ ├── parseSimpleContent.ts │ │ │ │ └── outputMessage.ts │ │ └── types.ts │ ├── index.ts │ ├── openai │ │ ├── types.ts │ │ └── adapter.test.ts │ ├── anthropic │ │ ├── types.ts │ │ └── adapter.test.ts │ ├── vercel-ai │ │ ├── types.ts │ │ └── adapter.test.ts │ └── utils │ │ └── getMimeType.ts ├── utils │ └── names.ts ├── constants.ts ├── error │ └── error.ts └── build.test.ts ├── .prettierignore ├── .prettierrc ├── vitest.config.ts ├── examples ├── lib.ts └── rpc.ts ├── .gitignore ├── LICENSE ├── tsconfig.json ├── .github └── workflows │ ├── test.yml │ ├── lint.yml │ └── publish.yml ├── eslint.config.mjs ├── rollup.config.rpc.mjs ├── rollup.config.mjs ├── package.json └── README.md /src/rpc/index.ts: -------------------------------------------------------------------------------- 1 | export { serve } from './server' 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .* 2 | dist 3 | build 4 | **/tests/fixtures 5 | -------------------------------------------------------------------------------- /src/index.rpc.ts: -------------------------------------------------------------------------------- 1 | import { serve } from './rpc' 2 | 3 | serve() 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "jsxSingleQuote": true, 4 | "singleQuote": true, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /src/parser/utils/regex.ts: -------------------------------------------------------------------------------- 1 | import { RESERVED_TAGS } from "$promptl/constants"; 2 | 3 | export const RESERVED_TAG_REGEX = new RegExp(`^|$)`) -------------------------------------------------------------------------------- /src/types/acorn.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'acorn' { 2 | export function isIdentifierStart(code: number, astral?: boolean): boolean 3 | export function isIdentifierChar(code: number, astral?: boolean): boolean 4 | } 5 | -------------------------------------------------------------------------------- /src/compiler/base/nodes/comment.ts: -------------------------------------------------------------------------------- 1 | import { Comment } from '$promptl/parser/interfaces' 2 | 3 | import { CompileNodeContext } from '../types' 4 | 5 | export async function compile(_: CompileNodeContext) { 6 | /* do nothing */ 7 | } 8 | -------------------------------------------------------------------------------- /src/compiler/base/nodes/text.ts: -------------------------------------------------------------------------------- 1 | import { Text } from '$promptl/parser/interfaces' 2 | 3 | import { CompileNodeContext } from '../types' 4 | 5 | export async function compile({ 6 | node, 7 | addStrayText, 8 | }: CompileNodeContext) { 9 | addStrayText(node.data) 10 | } 11 | -------------------------------------------------------------------------------- /src/compiler/base/nodes/config.ts: -------------------------------------------------------------------------------- 1 | import { Config as ConfigNode } from '$promptl/parser/interfaces' 2 | import yaml from 'yaml' 3 | 4 | import { CompileNodeContext } from '../types' 5 | 6 | export async function compile({ 7 | node, 8 | setConfig, 9 | }: CompileNodeContext): Promise { 10 | setConfig(yaml.parse(node.value)) 11 | } 12 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types' 2 | export * from './compiler' 3 | export * from './parser' 4 | export * from './providers' 5 | 6 | import * as parserInterfaces from './parser/interfaces' 7 | type Fragment = parserInterfaces.Fragment 8 | export type { Fragment, parserInterfaces as IParser } 9 | 10 | export { default as CompileError } from './error/error' 11 | -------------------------------------------------------------------------------- /src/compiler/logic/nodes/literal.ts: -------------------------------------------------------------------------------- 1 | import { type ResolveNodeProps } from '$promptl/compiler/logic/types' 2 | import { type Literal } from 'estree' 3 | 4 | /** 5 | * ### Literal 6 | * Represents a literal value. 7 | */ 8 | export async function resolve({ node }: ResolveNodeProps) { 9 | return node.value 10 | } 11 | 12 | export function updateScopeContext() { 13 | // Do nothing 14 | } 15 | -------------------------------------------------------------------------------- /src/test/helpers.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'vitest' 2 | 3 | export async function getExpectedError( 4 | action: () => unknown, 5 | errorClass: new (...args: any[]) => T, 6 | ): Promise { 7 | try { 8 | await action() 9 | } catch (err) { 10 | expect(err).toBeInstanceOf(errorClass) 11 | return err as T 12 | } 13 | throw new Error('Expected an error to be thrown') 14 | } 15 | -------------------------------------------------------------------------------- /src/parser/utils/full_char_code_at.ts: -------------------------------------------------------------------------------- 1 | // Adapted from https://github.com/acornjs/acorn/blob/6584815dca7440e00de841d1dad152302fdd7ca5/src/tokenize.js 2 | 3 | export default function fullCharCodeAt(str: string, i: number): number { 4 | const code = str.charCodeAt(i) 5 | if (code <= 0xd7ff || code >= 0xe000) return code 6 | 7 | const next = str.charCodeAt(i + 1) 8 | return (code << 10) + next - 0x35fdc00 9 | } 10 | -------------------------------------------------------------------------------- /src/parser/utils/acorn.ts: -------------------------------------------------------------------------------- 1 | import * as code_red from 'code-red' 2 | 3 | export const parse = (source: string) => 4 | code_red.parse(source, { 5 | sourceType: 'module', 6 | ecmaVersion: 13, 7 | locations: true, 8 | }) 9 | 10 | export const parseExpressionAt = (source: string, index: number) => 11 | code_red.parseExpressionAt(source, index, { 12 | sourceType: 'module', 13 | ecmaVersion: 13, 14 | locations: true, 15 | }) 16 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'path' 2 | import { fileURLToPath } from 'url' 3 | 4 | import { defineConfig } from 'vitest/config' 5 | 6 | const filename = fileURLToPath(import.meta.url) 7 | const root = dirname(filename) 8 | 9 | export default defineConfig({ 10 | resolve: { 11 | alias: { 12 | $promptl: `${root}/src`, 13 | }, 14 | }, 15 | test: { 16 | globals: true, 17 | environment: 'node', 18 | }, 19 | }) 20 | -------------------------------------------------------------------------------- /src/compiler/base/nodes/fragment.ts: -------------------------------------------------------------------------------- 1 | import { Fragment } from '$promptl/parser/interfaces' 2 | 3 | import { CompileNodeContext } from '../types' 4 | 5 | export async function compile({ 6 | node, 7 | scope, 8 | isInsideStepTag, 9 | isInsideContentTag, 10 | isInsideMessageTag, 11 | fullPath, 12 | resolveBaseNode, 13 | }: CompileNodeContext) { 14 | for await (const childNode of node.children ?? []) { 15 | await resolveBaseNode({ 16 | node: childNode, 17 | scope, 18 | isInsideStepTag, 19 | isInsideMessageTag, 20 | isInsideContentTag, 21 | fullPath, 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/compiler/base/nodes/tags/scope.ts: -------------------------------------------------------------------------------- 1 | import Scope from '$promptl/compiler/scope' 2 | import { ScopeTag } from '$promptl/parser/interfaces' 3 | 4 | import { CompileNodeContext } from '../../types' 5 | 6 | export async function compile( 7 | props: CompileNodeContext, 8 | attributes: Record, 9 | ) { 10 | const { 11 | node, 12 | resolveBaseNode, 13 | } = props 14 | 15 | const childScope = new Scope(attributes) 16 | 17 | for await (const childNode of node.children) { 18 | await resolveBaseNode({ 19 | ...props, 20 | node: childNode, 21 | scope: childScope, 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/providers/adapter.ts: -------------------------------------------------------------------------------- 1 | import { AdapterKey } from '$promptl/providers' 2 | import { Message, Conversation as PromptlConversation } from '$promptl/types' 3 | 4 | export type ProviderConversation = { 5 | config: PromptlConversation['config'] 6 | messages: M[] 7 | } 8 | 9 | export type ProviderAdapter = { 10 | type: AdapterKey 11 | toPromptl(conversation: ProviderConversation): PromptlConversation 12 | fromPromptl(conversation: PromptlConversation): ProviderConversation 13 | } 14 | 15 | export const defaultAdapter: ProviderAdapter = { 16 | type: 'default', 17 | toPromptl: (c) => c, 18 | fromPromptl: (c) => c, 19 | } 20 | -------------------------------------------------------------------------------- /examples/lib.ts: -------------------------------------------------------------------------------- 1 | // Run `pnpm build:lib` before running this example 2 | 3 | import assert from 'node:assert' 4 | import { inspect } from 'node:util' 5 | import { Chain } from '../dist/index.js' 6 | 7 | const prompt = ` 8 | 9 | 10 | You are a helpful assistant. 11 | 12 | 13 | Say hello. 14 | 15 | 16 | 17 | 18 | Now say goodbye. 19 | 20 | 21 | ` 22 | 23 | const chain = new Chain({ prompt }) 24 | let conversation = await chain.step() 25 | conversation = await chain.step('Hello!') 26 | conversation = await chain.step('Goodbye!') 27 | 28 | assert(chain.completed) 29 | assert(conversation.completed) 30 | 31 | console.log(inspect(conversation.messages, { depth: null })) 32 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import CompileError from '$promptl/error/error' 2 | import { Fragment } from '$promptl/index' 3 | 4 | import { Message } from './message' 5 | 6 | export type Config = Record 7 | 8 | export type Conversation = { 9 | config: Config 10 | messages: Message[] 11 | } 12 | 13 | export type ConversationMetadata = { 14 | hash: string 15 | resolvedPrompt: string 16 | ast: Fragment 17 | config: Config 18 | errors: CompileError[] 19 | parameters: Set // Variables used in the prompt that have not been defined in runtime 20 | isChain: boolean 21 | setConfig: (config: Config) => string 22 | includedPromptPaths: Set 23 | } 24 | 25 | export { type SerializedChain } from '$promptl/compiler' 26 | export * from './message' 27 | export * from './customTypes' 28 | -------------------------------------------------------------------------------- /src/compiler/types.ts: -------------------------------------------------------------------------------- 1 | import { TemplateNode } from '$promptl/parser/interfaces' 2 | import { MessageRole } from '$promptl/types' 3 | 4 | import type Scope from './scope' 5 | 6 | export type Document = { 7 | path: string 8 | content: string 9 | } 10 | export type ReferencePromptFn = ( 11 | path: string, 12 | from?: string, 13 | ) => Promise 14 | 15 | export type ResolveBaseNodeProps = { 16 | node: N 17 | scope: Scope 18 | isInsideStepTag: boolean 19 | isInsideMessageTag: boolean 20 | isInsideContentTag: boolean 21 | completedValue?: unknown 22 | fullPath?: string | undefined 23 | } 24 | 25 | export type CompileOptions = { 26 | referenceFn?: ReferencePromptFn 27 | fullPath?: string 28 | defaultRole?: MessageRole 29 | includeSourceMap?: boolean 30 | } 31 | -------------------------------------------------------------------------------- /src/compiler/logic/nodes/chainExpression.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ResolveNodeProps, 3 | UpdateScopeContextProps, 4 | } from '$promptl/compiler/logic/types' 5 | import type { ChainExpression } from 'estree' 6 | 7 | import { resolveLogicNode, updateScopeContextForNode } from '..' 8 | 9 | /** 10 | * ### Chain Expression 11 | * Represents a chain of operations. This is only being used for optional member expressions '?.' 12 | */ 13 | export async function resolve({ 14 | node, 15 | ...props 16 | }: ResolveNodeProps) { 17 | return resolveLogicNode({ 18 | node: node.expression, 19 | ...props, 20 | }) 21 | } 22 | 23 | export function updateScopeContext({ 24 | node, 25 | ...props 26 | }: UpdateScopeContextProps) { 27 | updateScopeContextForNode({ node: node.expression, ...props }) 28 | } 29 | -------------------------------------------------------------------------------- /src/compiler/base/nodes/if.ts: -------------------------------------------------------------------------------- 1 | import { IfBlock } from '$promptl/parser/interfaces' 2 | 3 | import { CompileNodeContext } from '../types' 4 | 5 | export async function compile({ 6 | node, 7 | scope, 8 | isInsideStepTag, 9 | isInsideContentTag, 10 | isInsideMessageTag, 11 | fullPath, 12 | resolveBaseNode, 13 | resolveExpression, 14 | }: CompileNodeContext) { 15 | const condition = await resolveExpression(node.expression, scope) 16 | const children = (condition ? node.children : node.else?.children) ?? [] 17 | const childScope = scope.copy() 18 | for await (const childNode of children ?? []) { 19 | await resolveBaseNode({ 20 | node: childNode, 21 | scope: childScope, 22 | isInsideStepTag, 23 | isInsideMessageTag, 24 | isInsideContentTag, 25 | fullPath, 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/providers/openai-responses/utils/parsers/reasoning.ts: -------------------------------------------------------------------------------- 1 | import { MessageInputItem } from '$promptl/providers/openai-responses/types' 2 | import { AssistantMessage, ContentType, MessageRole } from '$promptl/types' 3 | import { ResponseReasoningItem } from 'openai/resources/responses/responses' 4 | 5 | export function isReasoning( 6 | message: MessageInputItem, 7 | ): message is ResponseReasoningItem { 8 | return message.type === 'reasoning' 9 | } 10 | 11 | export function parseReasoning(message: ResponseReasoningItem) { 12 | return { 13 | id: message.id, 14 | role: MessageRole.assistant, 15 | content: message.summary.map((summary) => ({ 16 | type: ContentType.text, 17 | text: summary.text, 18 | })), 19 | status: message.status, 20 | encrypted_content: message.encrypted_content, 21 | } satisfies AssistantMessage 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/names.ts: -------------------------------------------------------------------------------- 1 | export const reserved = new Set([ 2 | 'arguments', 3 | 'as', 4 | 'await', 5 | 'break', 6 | 'case', 7 | 'catch', 8 | 'class', 9 | 'const', 10 | 'continue', 11 | 'debugger', 12 | 'default', 13 | 'delete', 14 | 'do', 15 | 'else', 16 | 'endfor', 17 | 'endif', 18 | 'enum', 19 | 'eval', 20 | 'export', 21 | 'extends', 22 | 'false', 23 | 'finally', 24 | 'for', 25 | 'function', 26 | 'if', 27 | 'implements', 28 | 'import', 29 | 'in', 30 | 'instanceof', 31 | 'interface', 32 | 'let', 33 | 'new', 34 | 'null', 35 | 'package', 36 | 'private', 37 | 'protected', 38 | 'public', 39 | 'return', 40 | 'static', 41 | 'super', 42 | 'switch', 43 | 'this', 44 | 'throw', 45 | 'true', 46 | 'try', 47 | 'typeof', 48 | 'var', 49 | 'void', 50 | 'while', 51 | 'with', 52 | 'yield', 53 | ]) 54 | -------------------------------------------------------------------------------- /src/parser/state/multi_line_comment.ts: -------------------------------------------------------------------------------- 1 | import PARSER_ERRORS from '$promptl/error/errors' 2 | import type { Comment } from '$promptl/parser/interfaces' 3 | 4 | import { Parser } from '..' 5 | 6 | export function multiLineComment(parser: Parser) { 7 | if (parser.match('*/')) { 8 | parser.error(PARSER_ERRORS.unexpectedEndOfComment) 9 | } 10 | 11 | const start = parser.index 12 | 13 | while (parser.index < parser.template.length) { 14 | if (parser.matchRegex(/\*\//)) { 15 | parser.index += 2 16 | break 17 | } 18 | parser.index++ 19 | } 20 | 21 | const data = parser.template.substring(start, parser.index) 22 | 23 | const node = { 24 | start, 25 | end: parser.index, 26 | type: 'Comment', 27 | raw: data, 28 | data: data.substring(2, data.length - 2), 29 | } as Comment 30 | 31 | parser.current().children!.push(node) 32 | } 33 | -------------------------------------------------------------------------------- /src/parser/state/config.ts: -------------------------------------------------------------------------------- 1 | import PARSER_ERRORS from '$promptl/error/errors' 2 | import { Parser } from '$promptl/parser' 3 | import type { Config } from '$promptl/parser/interfaces' 4 | 5 | export function config(parser: Parser) { 6 | const start = parser.index 7 | parser.eat('---') 8 | 9 | // Read until there is a line break followed by a triple dash 10 | const currentIndex = parser.index 11 | const data = parser.readUntil(/\n\s*---\s*/) 12 | if (parser.index === parser.template.length) { 13 | parser.error(PARSER_ERRORS.unexpectedToken('---'), currentIndex + 1) 14 | } 15 | 16 | parser.allowWhitespace() 17 | parser.eat('---', true) 18 | parser.eat('\n') 19 | 20 | const node = { 21 | start, 22 | end: parser.index, 23 | type: 'Config', 24 | raw: data, 25 | value: data, 26 | } as Config 27 | 28 | parser.current().children!.push(node) 29 | } 30 | -------------------------------------------------------------------------------- /src/parser/state/fragment.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CUSTOM_TAG_END, 3 | CUSTOM_TAG_START, 4 | } from '$promptl/constants' 5 | 6 | import { Parser } from '..' 7 | import { RESERVED_TAG_REGEX } from '../utils/regex' 8 | import { config } from './config' 9 | import { multiLineComment } from './multi_line_comment' 10 | import { mustache } from './mustache' 11 | import { tag } from './tag' 12 | import { text } from './text' 13 | 14 | export default function fragment(parser: Parser): (parser: Parser) => void { 15 | if ( 16 | parser.matchRegex(RESERVED_TAG_REGEX) || 17 | parser.match('