mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-24 08:40:58 -07:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1cf5f1f6f0 | |||
| aced1dec56 | |||
| 81f978abeb |
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { promptComponent, promptTemplate, renderSnippet, renderTemplate, enabledWhen, section, xmlSection, each, switchOn, type Snippet, type PromptTemplateBase } 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[] }, string>('items', (item: string) => `Item: ${item}`);
|
||||
|
||||
const eachCustomSeparatorPrompt = each<{ items: string[] }, string>('items', (item: string) => item, ', ');
|
||||
|
||||
const switchOnPrompt = switchOn<{ mode: string | number }>('mode', {
|
||||
fast: 'Speed is key',
|
||||
safe: 'Safety first',
|
||||
1: 'Mode one',
|
||||
});
|
||||
|
||||
// A high-level template.
|
||||
interface SystemPromptOptions { name: string }
|
||||
interface SystemPromptTemplate extends PromptTemplateBase<SystemPromptOptions> {
|
||||
identity: Snippet<SystemPromptOptions>;
|
||||
instructions: Snippet<SystemPromptOptions>;
|
||||
}
|
||||
|
||||
const mainTemplate = promptTemplate<SystemPromptOptions, SystemPromptTemplate>({
|
||||
identity: (opt: SystemPromptOptions) => `Your name is ${opt.name}`,
|
||||
instructions: 'Be helpful',
|
||||
});
|
||||
|
||||
describe('promptTemplating', () => {
|
||||
it('should take variadic arguments', () => {
|
||||
expect(renderSnippet({}, variadicPrompt)).toBe('A,B,C');
|
||||
});
|
||||
|
||||
it('should handle nested arrays (variadic with arrays)', () => {
|
||||
expect(renderSnippet({}, nestedVariadicPrompt)).toBe('A,B,C,D');
|
||||
});
|
||||
|
||||
it('should handle enabledWhen', () => {
|
||||
expect(renderSnippet({ show: true }, enabledWhenPrompt)).toBe('Always,Sometimes');
|
||||
expect(renderSnippet({ show: false }, enabledWhenPrompt)).toBe('Always,');
|
||||
});
|
||||
|
||||
it('should handle xmlSection', () => {
|
||||
expect(renderSnippet({}, xmlPrompt)).toBe('<rules>\nBe helpful\n</rules>');
|
||||
});
|
||||
|
||||
it('should return empty string for xmlSection if content is empty', () => {
|
||||
expect(renderSnippet({}, emptyXmlPrompt)).toBe('');
|
||||
});
|
||||
|
||||
it('should handle markdown section with default header', () => {
|
||||
expect(renderSnippet({}, markdownPrompt)).toBe('# Rules\n\nBe helpful');
|
||||
});
|
||||
|
||||
it('should return empty string for markdown section if content is empty', () => {
|
||||
expect(renderSnippet({}, emptyMarkdownPrompt)).toBe('');
|
||||
});
|
||||
|
||||
it('should handle markdown section with custom header level', () => {
|
||||
expect(renderSnippet({}, customHeaderMarkdownPrompt)).toBe('### Rules\n\nBe helpful');
|
||||
});
|
||||
|
||||
it('should handle each', () => {
|
||||
const options = { items: ['A', 'B'] };
|
||||
expect(renderSnippet(options, eachPrompt)).toBe('Item: A\nItem: B');
|
||||
});
|
||||
|
||||
it('should handle switchOn', () => {
|
||||
expect(renderSnippet({ mode: 'fast' }, switchOnPrompt)).toBe('Speed is key');
|
||||
expect(renderSnippet({ mode: 'safe' }, switchOnPrompt)).toBe('Safety first');
|
||||
expect(renderSnippet({ mode: 'unknown' }, switchOnPrompt)).toBe('');
|
||||
});
|
||||
|
||||
it('should handle switchOn with numeric values', () => {
|
||||
expect(renderSnippet({ mode: 1 }, switchOnPrompt)).toBe('Mode one');
|
||||
});
|
||||
|
||||
it('should handle each with custom separator', () => {
|
||||
const options = { items: ['A', 'B'] };
|
||||
expect(renderSnippet(options, eachCustomSeparatorPrompt)).toBe('A, B');
|
||||
});
|
||||
|
||||
it('should handle each with non-array value', () => {
|
||||
const options = { items: 'not an array' as unknown as string[] };
|
||||
expect(renderSnippet(options, eachPrompt)).toBe('');
|
||||
});
|
||||
|
||||
it('should handle section with different header levels', () => {
|
||||
const h2Section = section('Level 2', 'Content', { headerLevel: 2 });
|
||||
const h6Section = section('Level 6', 'Content', { headerLevel: 6 });
|
||||
expect(renderSnippet({}, h2Section)).toBe('## Level 2\n\nContent');
|
||||
expect(renderSnippet({}, h6Section)).toBe('###### Level 6\n\nContent');
|
||||
});
|
||||
|
||||
it('should handle toPrompt with basic types', () => {
|
||||
expect(renderSnippet({}, 'just a string')).toBe('just a string');
|
||||
expect(renderSnippet({ name: 'test' }, (opt: { name: string }) => opt.name)).toBe('test');
|
||||
});
|
||||
|
||||
it('should handle toPrompt with empty array or object', () => {
|
||||
expect(renderSnippet({}, [])).toBe('');
|
||||
expect(renderSnippet({}, {})).toBe('');
|
||||
});
|
||||
|
||||
it('should handle renderTemplate with a record of snippets (implements required members)', () => {
|
||||
const options = {
|
||||
name: 'Gemini CLI',
|
||||
};
|
||||
expect(renderTemplate(options, mainTemplate)).toBe('Your name is Gemini CLI,Be helpful');
|
||||
});
|
||||
|
||||
it('should output correctly for mainPrompt', () => {
|
||||
const text = renderSnippet({ 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,262 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview
|
||||
* This module provides a functional, type-safe DSL for constructing complex LLM prompts.
|
||||
* It supports dynamic content generation, conditional snippets, structural formatting (XML, Markdown),
|
||||
* and collection iteration, ensuring that prompt logic remains modular and maintainable.
|
||||
*
|
||||
* Key Features:
|
||||
* - **Type Safety**: Prompts are tied to an options interface, ensuring all dynamic data is validated.
|
||||
* - **Composition**: Small, reusable snippets can be combined. Templates (`promptTemplate`) ensure that like components have the same required elements, while components (`promptComponent`) are used for constructing the prompt.
|
||||
* - **Conditional Logic**: Use `enabledWhen` and `switchOn` to prune or swap prompt sections dynamically.
|
||||
* - **Structural Helpers**: `xmlSection` and `section` (Markdown) handle boilerplate formatting and empty-state pruning.
|
||||
* - **Iteration**: `each` simplifies rendering lists of tools, rules, or historical context.
|
||||
*
|
||||
* @example
|
||||
* interface SystemOptions {
|
||||
* name: string;
|
||||
* isInteractive: boolean;
|
||||
* rules: string[];
|
||||
* mode: 'fast' | 'creative';
|
||||
* }
|
||||
*
|
||||
* interface SystemTemplate extends PromptTemplateBase<SystemOptions> {
|
||||
* identity: Snippet<SystemOptions>;
|
||||
* constraints: Snippet<SystemOptions>;
|
||||
* capabilities: Snippet<SystemOptions>;
|
||||
* }
|
||||
*
|
||||
* const systemPrompt = promptTemplate<SystemOptions, SystemTemplate>({
|
||||
* identity: (opt) => `Your name is ${opt.name}.`,
|
||||
* constraints: xmlSection("rules", each<SystemOptions, string>("rules", (r) => `- ${r}`)),
|
||||
* capabilities: promptComponent(
|
||||
* switchOn<SystemOptions>("mode", {
|
||||
* fast: "Optimize for speed and brevity.",
|
||||
* creative: "Optimize for detail and exploration."
|
||||
* }),
|
||||
* enabledWhen<SystemOptions>("isInteractive", "You are in a live session; be brief.")
|
||||
* )
|
||||
* });
|
||||
*
|
||||
* const output = renderTemplate({
|
||||
* name: "Gemini CLI",
|
||||
* isInteractive: true,
|
||||
* rules: ["No spoilers", "Be helpful"],
|
||||
* mode: "fast"
|
||||
* }, systemPrompt);
|
||||
*/
|
||||
|
||||
/**
|
||||
* A function that generates a string snippet based on the provided options.
|
||||
*
|
||||
* @example
|
||||
* interface Options { name: string; }
|
||||
* const snippet: DynamicSnippet<Options> = (options) => `Hello, ${options.name}!`;
|
||||
*/
|
||||
export type DynamicSnippet<TOption> = (options: TOption) => string;
|
||||
|
||||
/**
|
||||
* A snippet can be a string, a dynamic snippet function, an array of snippets,
|
||||
* or a full prompt template base object.
|
||||
*
|
||||
* @example
|
||||
* interface Options { name: string; }
|
||||
* const simple: Snippet<Options> = "Hello World";
|
||||
* const dynamic: Snippet<Options> = (opt) => opt.name;
|
||||
* const combined: Snippet<Options> = ["Hello ", (opt) => opt.name];
|
||||
*/
|
||||
export type Snippet<TOption> =
|
||||
| string
|
||||
| DynamicSnippet<TOption>
|
||||
| Array<Snippet<TOption>>
|
||||
| PromptTemplateBase<TOption>;
|
||||
|
||||
/**
|
||||
* The base interface for a prompt template, where each key is a snippet.
|
||||
*/
|
||||
export interface PromptTemplateBase<TOption> {
|
||||
[key: string]: Snippet<TOption>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a complete prompt template defined by TTemplate.
|
||||
*/
|
||||
export type PromptTemplate<
|
||||
TOption,
|
||||
TTemplate extends PromptTemplateBase<TOption>,
|
||||
> = TTemplate;
|
||||
|
||||
/**
|
||||
* Defines a prompt template.
|
||||
*
|
||||
* @example
|
||||
* interface Options { name: string; }
|
||||
* interface Template extends PromptTemplateBase<Options> { greeting: Snippet<Options>; }
|
||||
* const myTemplate = promptTemplate<Options, Template>({
|
||||
* greeting: (opt) => `Hello ${opt.name}`
|
||||
* });
|
||||
*/
|
||||
export function promptTemplate<
|
||||
TOption,
|
||||
TTemplate extends PromptTemplateBase<TOption>,
|
||||
>(template: TTemplate): PromptTemplate<TOption, TTemplate> {
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines multiple snippets into a single component.
|
||||
*
|
||||
* @example
|
||||
* interface Options { second: string; }
|
||||
* const component = promptComponent<Options>("First part. ", (opt) => opt.second);
|
||||
*/
|
||||
export function promptComponent<TOption>(
|
||||
...snippets: Array<Snippet<TOption>>
|
||||
): Snippet<TOption> {
|
||||
return snippets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditionally includes a snippet if a specific option is truthy.
|
||||
*
|
||||
* @example
|
||||
* interface Options { verbose: boolean; }
|
||||
* const conditional = enabledWhen<Options>("verbose", "Running in verbose mode.");
|
||||
*/
|
||||
export function enabledWhen<TOption>(
|
||||
optionName: keyof TOption,
|
||||
snippet: Snippet<TOption>,
|
||||
): Snippet<TOption> {
|
||||
return (option: TOption) =>
|
||||
option[optionName] ? renderSnippet(option, snippet) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a snippet in XML tags.
|
||||
*
|
||||
* @example
|
||||
* interface Options { content: string; }
|
||||
* const xml = xmlSection<Options>("details", (opt) => opt.content);
|
||||
* // Output: <details>\nSome content\n</details>
|
||||
*/
|
||||
export function xmlSection<TOption>(
|
||||
name: string,
|
||||
snippet: Snippet<TOption>,
|
||||
): Snippet<TOption> {
|
||||
return (options: TOption) => {
|
||||
const content = renderSnippet(options, snippet);
|
||||
return content ? `<${name}>\n${content}\n</${name}>` : '';
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for a markdown section.
|
||||
*/
|
||||
export interface SectionOptions {
|
||||
/** The level of the markdown header (default is 1). */
|
||||
headerLevel?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a markdown-style section with a header.
|
||||
*
|
||||
* @example
|
||||
* interface Options { title: string; }
|
||||
* const mdSection = section<Options>("Introduction", "This is the content.", { headerLevel: 2 });
|
||||
* // Output: ## Introduction\n\nThis is the content.
|
||||
*/
|
||||
export function section<TOption>(
|
||||
name: string,
|
||||
snippet: Snippet<TOption>,
|
||||
sectionOptions?: SectionOptions,
|
||||
): Snippet<TOption> {
|
||||
return (options: TOption) => {
|
||||
const content = renderSnippet(options, snippet);
|
||||
const level = sectionOptions?.headerLevel ?? 1;
|
||||
const hashes = '#'.repeat(level);
|
||||
return content ? `${hashes} ${name}\n\n${content}` : '';
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over an array in the options and renders a snippet for each item.
|
||||
*
|
||||
* @example
|
||||
* interface Options { items: string[]; }
|
||||
* const list = each<Options, string>("items", (item) => `- ${item}`);
|
||||
*/
|
||||
export function each<TOption, TItem = unknown>(
|
||||
optionName: keyof TOption,
|
||||
snippet: Snippet<TItem>,
|
||||
separator = '\n',
|
||||
): Snippet<TOption> {
|
||||
return (options: TOption) => {
|
||||
const items = options[optionName];
|
||||
if (Array.isArray(items)) {
|
||||
return items
|
||||
.map((item: TItem) => renderSnippet(item, snippet))
|
||||
.join(separator);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a snippet based on the value of a specific option.
|
||||
*
|
||||
* @example
|
||||
* interface Options { mode: 'a' | 'b'; }
|
||||
* const router = switchOn<Options>("mode", {
|
||||
* a: "Mode A selected",
|
||||
* b: "Mode B selected"
|
||||
* });
|
||||
*/
|
||||
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 ? renderSnippet(options, caseSnippet) : '';
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an entire template into a single string.
|
||||
*/
|
||||
export function renderTemplate<
|
||||
TOption,
|
||||
TTemplate extends PromptTemplateBase<TOption>,
|
||||
>(options: TOption, implementation: TTemplate): string {
|
||||
return Object.values(implementation)
|
||||
.map((eachSnippet) => renderSnippet<TOption>(options, eachSnippet))
|
||||
.join();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a snippet into a string.
|
||||
*/
|
||||
export function renderSnippet<TOption>(
|
||||
options: TOption,
|
||||
snippet: Snippet<TOption>,
|
||||
): string {
|
||||
if (typeof snippet === 'string') {
|
||||
return snippet;
|
||||
} else if (Array.isArray(snippet)) {
|
||||
return snippet
|
||||
.map((eachSnippet) => renderSnippet<TOption>(options, eachSnippet))
|
||||
.join();
|
||||
} else if (typeof snippet === 'function') {
|
||||
return snippet(options);
|
||||
} else {
|
||||
return Object.values(snippet)
|
||||
.map((eachSnippet) => renderSnippet<TOption>(options, eachSnippet))
|
||||
.join();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user