/** * Prompt Loader - Loads prompts from markdown files * * Supports: * - Loading prompts from templates/{promptId}/ directory * - Snippet inclusion via {{snippet:name}} syntax * - Variable interpolation via {{variable}} syntax * - Caching for performance */ import fs from 'fs'; import path from 'path'; import type { PromptId, LoadedPrompt, SnippetId } from './types'; import { createLogger } from '@/lib/logger'; const log = createLogger('PromptLoader'); // Cache for loaded prompts and snippets const promptCache = new Map(); const snippetCache = new Map(); /** * Get the prompts directory path */ function getPromptsDir(): string { // In Next.js, use process.cwd() for the project root return path.join(process.cwd(), 'lib', 'generation', 'prompts'); } /** * Load a snippet by ID */ export function loadSnippet(snippetId: SnippetId): string { const cached = snippetCache.get(snippetId); if (cached) return cached; const snippetPath = path.join(getPromptsDir(), 'snippets', `${snippetId}.md`); try { const content = fs.readFileSync(snippetPath, 'utf-8').trim(); snippetCache.set(snippetId, content); return content; } catch { log.warn(`Snippet not found: ${snippetId}`); return `{{snippet:${snippetId}}}`; } } /** * Process snippet includes in a template * Replaces {{snippet:name}} with actual snippet content */ function processSnippets(template: string): string { return template.replace(/\{\{snippet:(\w[\w-]*)\}\}/g, (_, snippetId) => { return loadSnippet(snippetId as SnippetId); }); } /** * Load a prompt by ID */ export function loadPrompt(promptId: PromptId): LoadedPrompt | null { const cached = promptCache.get(promptId); if (cached) return cached; const promptDir = path.join(getPromptsDir(), 'templates', promptId); try { // Load system.md const systemPath = path.join(promptDir, 'system.md'); let systemPrompt = fs.readFileSync(systemPath, 'utf-8').trim(); systemPrompt = processSnippets(systemPrompt); // Load user.md (optional, may not exist) const userPath = path.join(promptDir, 'user.md'); let userPromptTemplate = ''; try { userPromptTemplate = fs.readFileSync(userPath, 'utf-8').trim(); userPromptTemplate = processSnippets(userPromptTemplate); } catch { // user.md is optional } const loaded: LoadedPrompt = { id: promptId, systemPrompt, userPromptTemplate, }; promptCache.set(promptId, loaded); return loaded; } catch (error) { log.error(`Failed to load prompt ${promptId}:`, error); return null; } } /** * Interpolate variables in a template * Replaces {{variable}} with values from the variables object */ export function interpolateVariables(template: string, variables: Record): string { return template.replace(/\{\{(\w+)\}\}/g, (match, key) => { const value = variables[key]; if (value === undefined) return match; if (typeof value === 'object') return JSON.stringify(value, null, 2); return String(value); }); } /** * Build a complete prompt with variables */ export function buildPrompt( promptId: PromptId, variables: Record, ): { system: string; user: string } | null { const prompt = loadPrompt(promptId); if (!prompt) return null; return { system: interpolateVariables(prompt.systemPrompt, variables), user: interpolateVariables(prompt.userPromptTemplate, variables), }; } /** * Clear all caches (useful for development/testing) */ export function clearPromptCache(): void { promptCache.clear(); snippetCache.clear(); }