diff --git a/packages/core/src/prompts/promptTemplating.ts b/packages/core/src/prompts/promptTemplating.ts index 16fa62ef08..9f9efdc455 100644 --- a/packages/core/src/prompts/promptTemplating.ts +++ b/packages/core/src/prompts/promptTemplating.ts @@ -4,49 +4,176 @@ * 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 { + * identity: Snippet; + * constraints: Snippet; + * capabilities: Snippet; + * } + * + * const systemPrompt = promptTemplate({ + * identity: (opt) => `Your name is ${opt.name}.`, + * constraints: xmlSection("rules", each("rules", (r) => `- ${r}`)), + * capabilities: promptComponent( + * switchOn("mode", { + * fast: "Optimize for speed and brevity.", + * creative: "Optimize for detail and exploration." + * }), + * enabledWhen("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) => `Hello, ${options.name}!`; + */ export type DynamicSnippet = (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 = "Hello World"; + * const dynamic: Snippet = (opt) => opt.name; + * const combined: Snippet = ["Hello ", (opt) => opt.name]; + */ export type Snippet = | string | DynamicSnippet | Array> | PromptTemplateBase; +/** + * The base interface for a prompt template, where each key is a snippet. + */ export interface PromptTemplateBase { [key: string]: Snippet; } -export type PromptTemplate> = TTemplate; +/** + * Represents a complete prompt template defined by TTemplate. + */ +export type PromptTemplate< + TOption, + TTemplate extends PromptTemplateBase, +> = TTemplate; -export function promptTemplate>( - template: TTemplate -): PromptTemplate { +/** + * Defines a prompt template. + * + * @example + * interface Options { name: string; } + * interface Template extends PromptTemplateBase { greeting: Snippet; } + * const myTemplate = promptTemplate({ + * greeting: (opt) => `Hello ${opt.name}` + * }); + */ +export function promptTemplate< + TOption, + TTemplate extends PromptTemplateBase, +>(template: TTemplate): PromptTemplate { return template; } -export function promptComponent(...snippets: Array>): Snippet { +/** + * Combines multiple snippets into a single component. + * + * @example + * interface Options { second: string; } + * const component = promptComponent("First part. ", (opt) => opt.second); + */ +export function promptComponent( + ...snippets: Array> +): Snippet { return snippets; } -export function enabledWhen(optionName: keyof TOption, snippet: Snippet): Snippet { - return (option: TOption) => (option[optionName] ? renderSnippet(option, snippet) : ''); +/** + * Conditionally includes a snippet if a specific option is truthy. + * + * @example + * interface Options { verbose: boolean; } + * const conditional = enabledWhen("verbose", "Running in verbose mode."); + */ +export function enabledWhen( + optionName: keyof TOption, + snippet: Snippet, +): Snippet { + return (option: TOption) => + option[optionName] ? renderSnippet(option, snippet) : ''; } -export function xmlSection(name: string, snippet: Snippet): Snippet { +/** + * Wraps a snippet in XML tags. + * + * @example + * interface Options { content: string; } + * const xml = xmlSection("details", (opt) => opt.content); + * // Output:
\nSome content\n
+ */ +export function xmlSection( + name: string, + snippet: Snippet, +): Snippet { return (options: TOption) => { const content = renderSnippet(options, snippet); return content ? `<${name}>\n${content}\n` : ''; }; } +/** + * 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("Introduction", "This is the content.", { headerLevel: 2 }); + * // Output: ## Introduction\n\nThis is the content. + */ export function section( name: string, snippet: Snippet, - sectionOptions?: SectionOptions + sectionOptions?: SectionOptions, ): Snippet { return (options: TOption) => { const content = renderSnippet(options, snippet); @@ -56,23 +183,42 @@ export function section( }; } +/** + * Iterates over an array in the options and renders a snippet for each item. + * + * @example + * interface Options { items: string[]; } + * const list = each("items", (item) => `- ${item}`); + */ export function each( optionName: keyof TOption, snippet: Snippet, - separator = '\n' + separator = '\n', ): Snippet { return (options: TOption) => { const items = options[optionName]; if (Array.isArray(items)) { - return items.map((item: TItem) => renderSnippet(item, snippet)).join(separator); + 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("mode", { + * a: "Mode A selected", + * b: "Mode B selected" + * }); + */ export function switchOn( optionName: keyof TOption, - cases: Record> + cases: Record>, ): Snippet { return (options: TOption) => { const value = String(options[optionName]); @@ -81,25 +227,36 @@ export function switchOn( }; } -export function renderTemplate>( - options: TOption, - implementation: TTemplate -): string { +/** + * Renders an entire template into a single string. + */ +export function renderTemplate< + TOption, + TTemplate extends PromptTemplateBase, +>(options: TOption, implementation: TTemplate): string { return Object.values(implementation) - .map(eachSnippet => renderSnippet(options, eachSnippet)) + .map((eachSnippet) => renderSnippet(options, eachSnippet)) .join(); } -export function renderSnippet(options: TOption, snippet: Snippet): string { +/** + * Renders a snippet into a string. + */ +export function renderSnippet( + options: TOption, + snippet: Snippet, +): string { if (typeof snippet === 'string') { return snippet; } else if (Array.isArray(snippet)) { - return snippet.map(eachSnippet => renderSnippet(options, eachSnippet)).join(); + return snippet + .map((eachSnippet) => renderSnippet(options, eachSnippet)) + .join(); } else if (typeof snippet === 'function') { return snippet(options); } else { return Object.values(snippet) - .map(eachSnippet => renderSnippet(options, eachSnippet)) + .map((eachSnippet) => renderSnippet(options, eachSnippet)) .join(); } }