| 1 | export type PromptTemplateVars = Record<string, string | number | boolean> |
| 2 | |
| 3 | const PLACEHOLDER_RE = /{{\s*([A-Za-z][A-Za-z0-9_.-]*)\s*}}/g |
| 4 | const RESIDUAL_PLACEHOLDER_RE = /{{[^{}]*}}/ |
| 5 | |
| 6 | const collectPlaceholderNames = (template: string): Set<string> => { |
| 7 | const names = new Set<string>() |
| 8 | PLACEHOLDER_RE.lastIndex = 0 |
| 9 | let match: RegExpExecArray | null |
| 10 | while ((match = PLACEHOLDER_RE.exec(template)) !== null) { |
| 11 | names.add(match[1]) |
| 12 | } |
| 13 | return names |
| 14 | } |
| 15 | |
| 16 | /** |
| 17 | * Render a static prompt template. Composition (branches, arrays and JSON) stays |
| 18 | * in TypeScript composers; this function deliberately supports scalar replacement |
| 19 | * only so typos fail before a request reaches a model. |
| 20 | */ |
| 21 | export const renderPromptTemplate = (template: string, vars: PromptTemplateVars): string => { |
| 22 | const expected = collectPlaceholderNames(template) |
| 23 | const unknown = Object.keys(vars).filter((key) => !expected.has(key)) |
| 24 | if (unknown.length > 0) { |
| 25 | throw new Error(`Prompt template received unknown variables: ${unknown.join(', ')}`) |
| 26 | } |
| 27 | |
| 28 | const missing = Array.from(expected).filter( |
| 29 | (key) => !Object.prototype.hasOwnProperty.call(vars, key) |
| 30 | ) |
| 31 | if (missing.length > 0) { |
| 32 | throw new Error(`Prompt template is missing variables: ${missing.join(', ')}`) |
| 33 | } |
| 34 | |
| 35 | const rendered = template.replace(PLACEHOLDER_RE, (_match, key: string) => String(vars[key])) |
| 36 | if (RESIDUAL_PLACEHOLDER_RE.test(rendered)) { |
| 37 | throw new Error('Prompt template contains an invalid or unresolved {{...}} placeholder') |
| 38 | } |
| 39 | return rendered |
| 40 | } |
| 41 |