Basic prompt templating.

This commit is contained in:
Christian Gunderman
2026-03-19 16:01:37 -07:00
parent e6cd5d208c
commit 81f978abeb
2 changed files with 169 additions and 0 deletions
@@ -0,0 +1,97 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect, it } from 'vitest';
import { promptComponent, toPrompt, enabledWhen, section, xmlSection, each, switchOn } from './promptTemplating.js';
// Sample prompt components.
type MainPromptOptions = { isInteractive: boolean };
const identity = promptComponent('Your name is Gemini CLI and you are a coding agent');
const interactive = promptComponent<MainPromptOptions>(
enabledWhen('isInteractive', 'You are operating in an interactive session')
);
const mainPrompt = promptComponent<MainPromptOptions>(identity, interactive);
const variadicPrompt = promptComponent('A', 'B', 'C');
const nestedVariadicPrompt = promptComponent('A', ['B', 'C'], 'D');
const enabledWhenPrompt = promptComponent<Record<string, boolean>>(
'Always',
enabledWhen('show', 'Sometimes')
);
const xmlPrompt = xmlSection('rules', 'Be helpful');
const emptyXmlPrompt = xmlSection('rules', '');
const markdownPrompt = section('Rules', 'Be helpful');
const emptyMarkdownPrompt = section('Rules', '');
const customHeaderMarkdownPrompt = section('Rules', 'Be helpful', { headerLevel: 3 });
const eachPrompt = each<{ items: string[] }>('items', (item) => `Item: ${item}`);
const switchOnPrompt = switchOn<{ mode: string }>('mode', {
fast: 'Speed is key',
safe: 'Safety first',
});
describe('promptTemplating', () => {
it('should take variadic arguments', () => {
expect(toPrompt({}, variadicPrompt)).toBe('A,B,C');
});
it('should handle nested arrays (variadic with arrays)', () => {
expect(toPrompt({}, nestedVariadicPrompt)).toBe('A,B,C,D');
});
it('should handle enabledWhen', () => {
expect(toPrompt({ show: true }, enabledWhenPrompt)).toBe('Always,Sometimes');
expect(toPrompt({ show: false }, enabledWhenPrompt)).toBe('Always,');
});
it('should handle xmlSection', () => {
expect(toPrompt({}, xmlPrompt)).toBe('<rules>\nBe helpful\n</rules>');
});
it('should return empty string for xmlSection if content is empty', () => {
expect(toPrompt({}, emptyXmlPrompt)).toBe('');
});
it('should handle markdown section with default header', () => {
expect(toPrompt({}, markdownPrompt)).toBe('# Rules\n\nBe helpful');
});
it('should return empty string for markdown section if content is empty', () => {
expect(toPrompt({}, emptyMarkdownPrompt)).toBe('');
});
it('should handle markdown section with custom header level', () => {
expect(toPrompt({}, customHeaderMarkdownPrompt)).toBe('### Rules\n\nBe helpful');
});
it('should handle each', () => {
const options = { items: ['A', 'B'] };
expect(toPrompt(options, eachPrompt)).toBe('Item: A\nItem: B');
});
it('should handle switchOn', () => {
expect(toPrompt({ mode: 'fast' }, switchOnPrompt)).toBe('Speed is key');
expect(toPrompt({ mode: 'safe' }, switchOnPrompt)).toBe('Safety first');
expect(toPrompt({ mode: 'unknown' }, switchOnPrompt)).toBe('');
});
it('should output correctly for mainPrompt', () => {
const text = toPrompt({ isInteractive: true }, mainPrompt);
expect(text).toContain('Your name is Gemini CLI and you are a coding agent');
expect(text).toContain('You are operating in an interactive session');
});
});
@@ -0,0 +1,72 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
export type DynamicSnippet<TOption> = (options: TOption) => string;
export type Snippet<TOption> = string | DynamicSnippet<TOption> | Array<Snippet<TOption>>;
export function promptComponent<TOption>(...snippets: Array<Snippet<TOption>>): Snippet<TOption> {
return snippets;
}
export function enabledWhen<TOption>(optionName: keyof TOption, snippet: Snippet<TOption>): Snippet<TOption> {
return (option: TOption) => option[optionName] ? toPrompt(option, snippet) : '';
}
export function xmlSection<TOption>(name: string, snippet: Snippet<TOption>): Snippet<TOption> {
return (options: TOption) => {
const content = toPrompt(options, snippet);
return content ? `<${name}>\n${content}\n</${name}>` : '';
};
}
export interface SectionOptions {
headerLevel?: number;
}
export function section<TOption>(name: string, snippet: Snippet<TOption>, sectionOptions?: SectionOptions): Snippet<TOption> {
return (options: TOption) => {
const content = toPrompt(options, snippet);
const level = sectionOptions?.headerLevel ?? 1;
const hashes = '#'.repeat(level);
return content ? `${hashes} ${name}\n\n${content}` : '';
};
}
export function each<TOption>(
optionName: keyof TOption,
snippet: Snippet<TOption>,
separator = '\n'
): Snippet<TOption> {
return (options: TOption) => {
const items = options[optionName];
if (Array.isArray(items)) {
return items.map((item: any) => toPrompt(item, snippet)).join(separator);
}
return '';
};
}
export function switchOn<TOption>(
optionName: keyof TOption,
cases: Record<string, Snippet<TOption>>
): Snippet<TOption> {
return (options: TOption) => {
const value = String(options[optionName]);
const caseSnippet = cases[value];
return caseSnippet ? toPrompt(options, caseSnippet) : '';
};
}
export function toPrompt<TOption>(options: TOption, snippet: Snippet<TOption>): string {
if (typeof snippet === 'string') {
return snippet;
} else if (Array.isArray(snippet)) {
return snippet.map(eachSnippet => toPrompt<TOption>(options, eachSnippet)).join();
} else {
return snippet(options);
}
}