OpenMAIC-React / src /lib /prompts /loader.ts
muthuk1's picture
Convert OpenMAIC from Next.js to React (Vite)
f56a29b verified
/**
* Prompt Loader - Loads prompts from markdown files
*
* Supports:
* - Loading prompts from templates/{promptId}/ directory
* - Snippet inclusion via {{snippet:name}} syntax
* - Conditional blocks via {{#if condition}}...{{/if}} syntax
* - Variable interpolation via {{variable}} syntax
*/
import fs from 'fs';
import path from 'path';
import type { PromptId, LoadedPrompt, SnippetId } from './types';
import { createLogger } from '@/lib/logger';
const log = createLogger('PromptLoader');
/**
* Get the prompts directory path
*/
function getPromptsDir(): string {
// In Next.js, use process.cwd() for the project root
return path.join(process.cwd(), 'lib', 'prompts');
}
/**
* Load a snippet by ID
*/
export function loadSnippet(snippetId: SnippetId): string {
const snippetPath = path.join(getPromptsDir(), 'snippets', `${snippetId}.md`);
try {
return fs.readFileSync(snippetPath, 'utf-8').trim();
} catch {
// Fail loud rather than silently shipping `{{snippet:foo}}` to the LLM.
// A missing snippet is always a config/typo bug β€” surface at load time.
throw new Error(`Snippet not found: ${snippetId}`);
}
}
/**
* Process snippet includes in a template.
* Replaces {{snippet:name}} with actual snippet content.
*/
export function processSnippets(template: string): string {
return template.replace(/\{\{snippet:(\w[\w-]*)\}\}/g, (_, snippetId) => {
return loadSnippet(snippetId as SnippetId);
});
}
/**
* Process conditional blocks in a template.
* Replaces {{#if conditionName}}...{{/if}} with the inner content when the
* named condition is truthy, or removes the entire block when it is falsy.
*
* Blocks do not nest β€” this is intentional to keep the prompt templating
* language simple and reviewable.
*/
export function processConditionalBlocks(
template: string,
conditions: Record<string, unknown>,
): string {
return template.replace(
/\{\{#if (\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g,
(_, conditionName: string, content: string) => {
return conditions[conditionName] ? content : '';
},
);
}
/**
* Load a prompt by ID
*/
export function loadPrompt(promptId: PromptId): LoadedPrompt | null {
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
}
return {
id: promptId,
systemPrompt,
userPromptTemplate,
};
} 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, unknown>): string {
// `\w+` only matches [A-Za-z0-9_], so kebab-case placeholders like
// `{{next-agent}}` pass through unchanged. Convention (per README) is
// camelCase; tests in tests/prompts/templates.test.ts scan templates
// for non-conforming placeholders.
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.
*
* Processing order:
* 1. Snippet includes ({{snippet:name}}) β€” file content spliced in
* 2. Conditional blocks ({{#if flag}}...{{/if}}) β€” gated on `variables`
* 3. Variable interpolation ({{varName}}) β€” values substituted
*/
export function buildPrompt(
promptId: PromptId,
variables: Record<string, unknown>,
): { system: string; user: string } | null {
const prompt = loadPrompt(promptId);
if (!prompt) return null;
return {
system: interpolateVariables(
processConditionalBlocks(prompt.systemPrompt, variables),
variables,
),
user: interpolateVariables(
processConditionalBlocks(prompt.userPromptTemplate, variables),
variables,
),
};
}