From 5f5bf24d64264ee4fbb7f2cee6ba370a1cb73e4c Mon Sep 17 00:00:00 2001 From: root Date: Sun, 8 Mar 2026 16:17:56 +0000 Subject: [PATCH] feat: add prompt template system for LLM variable substitution Co-Authored-By: Claude Opus 4.6 --- .../src/llm/__tests__/promptTemplate.test.ts | 61 +++++++++++++++++++ server/src/llm/promptTemplate.ts | 26 ++++++++ 2 files changed, 87 insertions(+) create mode 100644 server/src/llm/__tests__/promptTemplate.test.ts create mode 100644 server/src/llm/promptTemplate.ts diff --git a/server/src/llm/__tests__/promptTemplate.test.ts b/server/src/llm/__tests__/promptTemplate.test.ts new file mode 100644 index 0000000..86fd08a --- /dev/null +++ b/server/src/llm/__tests__/promptTemplate.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect } from 'vitest'; +import { PromptTemplate, renderTemplate } from '../promptTemplate.js'; + +describe('promptTemplate', () => { + it('renders a template with variable substitution', () => { + const template: PromptTemplate = { + name: 'test', + systemPrompt: 'You are a narrator for {{setting}}.', + userPrompt: '{{npcName}} is feeling {{mood}}. Describe their thoughts.', + }; + const result = renderTemplate(template, { + setting: 'a medieval village', + npcName: 'Elara', + mood: 'anxious', + }); + expect(result.system).toBe('You are a narrator for a medieval village.'); + expect(result.user).toBe('Elara is feeling anxious. Describe their thoughts.'); + }); + + it('leaves unmatched variables as-is', () => { + const template: PromptTemplate = { + name: 'test', + systemPrompt: 'Hello {{name}}, welcome to {{place}}.', + userPrompt: '', + }; + const result = renderTemplate(template, { name: 'Brynn' }); + expect(result.system).toBe('Hello Brynn, welcome to {{place}}.'); + }); + + it('handles empty variables object', () => { + const template: PromptTemplate = { + name: 'test', + systemPrompt: 'No vars here.', + userPrompt: 'Also none.', + }; + const result = renderTemplate(template, {}); + expect(result.system).toBe('No vars here.'); + expect(result.user).toBe('Also none.'); + }); + + it('replaces multiple occurrences of same variable', () => { + const template: PromptTemplate = { + name: 'test', + systemPrompt: '{{name}} meets {{name}}.', + userPrompt: '', + }; + const result = renderTemplate(template, { name: 'Thom' }); + expect(result.system).toBe('Thom meets Thom.'); + }); + + it('handles multiline templates', () => { + const template: PromptTemplate = { + name: 'test', + systemPrompt: 'Line 1: {{a}}\nLine 2: {{b}}', + userPrompt: '{{c}}', + }; + const result = renderTemplate(template, { a: 'x', b: 'y', c: 'z' }); + expect(result.system).toBe('Line 1: x\nLine 2: y'); + expect(result.user).toBe('z'); + }); +}); diff --git a/server/src/llm/promptTemplate.ts b/server/src/llm/promptTemplate.ts new file mode 100644 index 0000000..61d284f --- /dev/null +++ b/server/src/llm/promptTemplate.ts @@ -0,0 +1,26 @@ +export interface PromptTemplate { + name: string; + systemPrompt: string; + userPrompt: string; +} + +export interface RenderedPrompt { + system: string; + user: string; +} + +export function renderTemplate( + template: PromptTemplate, + variables: Record, +): RenderedPrompt { + let system = template.systemPrompt; + let user = template.userPrompt; + + for (const [key, value] of Object.entries(variables)) { + const pattern = `{{${key}}}`; + system = system.replaceAll(pattern, value); + user = user.replaceAll(pattern, value); + } + + return { system, user }; +}