From 81f978abeb28d34bab3af03d5862511187099d15 Mon Sep 17 00:00:00 2001 From: Christian Gunderman Date: Thu, 19 Mar 2026 16:01:37 -0700 Subject: [PATCH] Basic prompt templating. --- .../core/src/prompts/promptTemplating.test.ts | 97 +++++++++++++++++++ packages/core/src/prompts/promptTemplating.ts | 72 ++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 packages/core/src/prompts/promptTemplating.test.ts create mode 100644 packages/core/src/prompts/promptTemplating.ts diff --git a/packages/core/src/prompts/promptTemplating.test.ts b/packages/core/src/prompts/promptTemplating.test.ts new file mode 100644 index 0000000000..6d09f604c3 --- /dev/null +++ b/packages/core/src/prompts/promptTemplating.test.ts @@ -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( + enabledWhen('isInteractive', 'You are operating in an interactive session') +); + +const mainPrompt = promptComponent(identity, interactive); + +const variadicPrompt = promptComponent('A', 'B', 'C'); + +const nestedVariadicPrompt = promptComponent('A', ['B', 'C'], 'D'); + +const enabledWhenPrompt = promptComponent>( + '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('\nBe helpful\n'); + }); + + 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'); + }); +}); diff --git a/packages/core/src/prompts/promptTemplating.ts b/packages/core/src/prompts/promptTemplating.ts new file mode 100644 index 0000000000..6f43a174bb --- /dev/null +++ b/packages/core/src/prompts/promptTemplating.ts @@ -0,0 +1,72 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export type DynamicSnippet = (options: TOption) => string; + +export type Snippet = string | DynamicSnippet | Array>; + +export function promptComponent(...snippets: Array>): Snippet { + return snippets; +} + +export function enabledWhen(optionName: keyof TOption, snippet: Snippet): Snippet { + return (option: TOption) => option[optionName] ? toPrompt(option, snippet) : ''; +} + +export function xmlSection(name: string, snippet: Snippet): Snippet { + return (options: TOption) => { + const content = toPrompt(options, snippet); + return content ? `<${name}>\n${content}\n` : ''; + }; +} + +export interface SectionOptions { + headerLevel?: number; +} + +export function section(name: string, snippet: Snippet, sectionOptions?: SectionOptions): Snippet { + 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( + optionName: keyof TOption, + snippet: Snippet, + separator = '\n' +): Snippet { + 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( + optionName: keyof TOption, + cases: Record> +): Snippet { + return (options: TOption) => { + const value = String(options[optionName]); + const caseSnippet = cases[value]; + return caseSnippet ? toPrompt(options, caseSnippet) : ''; + }; +} + +export function toPrompt(options: TOption, snippet: Snippet): string { + if (typeof snippet === 'string') { + return snippet; + } else if (Array.isArray(snippet)) { + return snippet.map(eachSnippet => toPrompt(options, eachSnippet)).join(); + } else { + return snippet(options); + } +}