diff --git a/README.md b/README.md index 1eef61a..88cd34b 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,9 @@ steps: var3: | Lorem Ipsum Hello World + file_input: | + var4: ./path/to/long-text.txt + var5: ./path/to/config.json ``` #### Simple prompt.yml example @@ -116,7 +119,9 @@ jsonSchema: |- ``` Variables in prompt.yml files are templated using `{{variable}}` format and are -supplied via the `input` parameter in YAML format. +supplied via the `input` parameter in YAML format. Additionally, you can +provide file-based variables via `file_input`, where each key maps to a file +path. ### Using a system prompt file @@ -197,6 +202,7 @@ the action: | `prompt` | The prompt to send to the model | N/A | | `prompt-file` | Path to a file containing the prompt (supports .txt and .prompt.yml formats). If both `prompt` and `prompt-file` are provided, `prompt-file` takes precedence | `""` | | `input` | Template variables in YAML format for .prompt.yml files (e.g., `var1: value1` on separate lines) | `""` | +| `file_input` | Template variables in YAML where values are file paths. The file contents are read and used for templating | `""` | | `system-prompt` | The system prompt to send to the model | `"You are a helpful assistant"` | | `system-prompt-file` | Path to a file containing the system prompt. If both `system-prompt` and `system-prompt-file` are provided, `system-prompt-file` takes precedence | `""` | | `model` | The model to use for inference. Must be available in the [GitHub Models](https://github.com/marketplace?type=models) catalog | `openai/gpt-4o` | diff --git a/__tests__/main-prompt-integration.test.ts b/__tests__/main-prompt-integration.test.ts index 5bba694..e1076eb 100644 --- a/__tests__/main-prompt-integration.test.ts +++ b/__tests__/main-prompt-integration.test.ts @@ -130,6 +130,49 @@ model: openai/gpt-4o expect(core.setOutput).toHaveBeenCalledWith('response-file', expect.any(String)) }) + it('supports file_input variables to load file contents', async () => { + mockExistsSync.mockReturnValue(true) + + // First call: reading the prompt file. Second call: reading file_input referenced file contents. + const externalFilePath = 'vars.txt' + mockReadFileSync.mockImplementation((path: string) => { + if (path === 'test.prompt.yml') { + return `messages:\n - role: user\n content: 'Here is the data: {{blob}}'\nmodel: openai/gpt-4o\n` + } + if (path === externalFilePath) { + return 'FILE_CONTENTS' + } + return '' + }) + + core.getInput.mockImplementation((name: string) => { + switch (name) { + case 'prompt-file': + return 'test.prompt.yml' + case 'file_input': + return `blob: ${externalFilePath}` + case 'model': + return 'openai/gpt-4o' + case 'max-tokens': + return '200' + case 'endpoint': + return 'https://models.github.ai/inference' + case 'enable-github-mcp': + return 'false' + default: + return '' + } + }) + + await run() + + expect(mockSimpleInference).toHaveBeenCalledWith( + expect.objectContaining({ + messages: [{role: 'user', content: 'Here is the data: FILE_CONTENTS'}], + }), + ) + }) + it('should fall back to legacy format when not using prompt YAML', async () => { mockExistsSync.mockReturnValue(false) core.getInput.mockImplementation((name: string) => { diff --git a/__tests__/prompt.test.ts b/__tests__/prompt.test.ts index 0bfaf11..2eba81d 100644 --- a/__tests__/prompt.test.ts +++ b/__tests__/prompt.test.ts @@ -1,7 +1,13 @@ import {describe, it, expect} from 'vitest' import * as path from 'path' import {fileURLToPath} from 'url' -import {parseTemplateVariables, replaceTemplateVariables, loadPromptFile, isPromptYamlFile} from '../src/prompt' +import { + parseTemplateVariables, + replaceTemplateVariables, + loadPromptFile, + isPromptYamlFile, + parseFileTemplateVariables, +} from '../src/prompt' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) @@ -10,8 +16,8 @@ describe('prompt.ts', () => { describe('parseTemplateVariables', () => { it('should parse simple YAML variables', () => { const input = ` -a: hello -b: world + a: hello + b: world ` const result = parseTemplateVariables(input) expect(result).toEqual({a: 'hello', b: 'world'}) @@ -19,10 +25,10 @@ b: world it('should parse multiline variables', () => { const input = ` -var1: hello -var2: | - This is a - multiline string + var1: hello + var2: | + This is a + multiline string ` const result = parseTemplateVariables(input) expect(result.var1).toBe('hello') @@ -117,4 +123,17 @@ var2: | expect(() => loadPromptFile('non-existent.prompt.yml')).toThrow('Prompt file not found') }) }) + + describe('parseFileTemplateVariables', () => { + it('reads file contents for variables', () => { + const configPath = path.join(__dirname, '../__fixtures__/prompts/json-schema.prompt.yml') + const data = parseFileTemplateVariables(`sample: ${configPath}`) + expect(data.sample).toContain('messages:') + expect(data.sample).toContain('responseFormat:') + }) + + it('errors on missing files', () => { + expect(() => parseFileTemplateVariables('x: ./does-not-exist.txt')).toThrow('was not found') + }) + }) }) diff --git a/action.yml b/action.yml index 4c3c91c..bcaf783 100644 --- a/action.yml +++ b/action.yml @@ -22,6 +22,10 @@ inputs: description: Template variables in YAML format for .prompt.yml files required: false default: '' + file_input: + description: Template variables in YAML format mapping variable names to file paths. The file contents will be used for templating. + required: false + default: '' model: description: The model to use required: false diff --git a/src/main.ts b/src/main.ts index da05ad8..565e925 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,7 +5,13 @@ import * as path from 'path' import {connectToGitHubMCP} from './mcp.js' import {simpleInference, mcpInference} from './inference.js' import {loadContentFromFileOrInput, buildInferenceRequest} from './helpers.js' -import {loadPromptFile, parseTemplateVariables, isPromptYamlFile, PromptConfig} from './prompt.js' +import { + loadPromptFile, + parseTemplateVariables, + isPromptYamlFile, + PromptConfig, + parseFileTemplateVariables, +} from './prompt.js' const RESPONSE_FILE = 'modelResponse.txt' @@ -18,6 +24,7 @@ export async function run(): Promise { try { const promptFilePath = core.getInput('prompt-file') const inputVariables = core.getInput('input') + const fileInputVariables = core.getInput('file_input') let promptConfig: PromptConfig | undefined = undefined let systemPrompt: string | undefined = undefined @@ -27,8 +34,10 @@ export async function run(): Promise { if (promptFilePath && isPromptYamlFile(promptFilePath)) { core.info('Using prompt YAML file format') - // Parse template variables - const templateVariables = parseTemplateVariables(inputVariables) + // Parse template variables from both string inputs and file-based inputs + const stringVars = parseTemplateVariables(inputVariables) + const fileVars = parseFileTemplateVariables(fileInputVariables) + const templateVariables = {...stringVars, ...fileVars} // Load and process prompt file promptConfig = loadPromptFile(promptFilePath, templateVariables) diff --git a/src/prompt.ts b/src/prompt.ts index 1683bea..2af78e4 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -37,6 +37,41 @@ export function parseTemplateVariables(input: string): TemplateVariables { } } +/** + * Parse file-based template variables from YAML input string. The YAML should map + * variable names to file paths. File contents are read and returned as variables. + */ +export function parseFileTemplateVariables(fileInput: string): TemplateVariables { + if (!fileInput.trim()) { + return {} + } + + try { + const parsed = yaml.load(fileInput) as Record + if (typeof parsed !== 'object' || parsed === null) { + throw new Error('File template variables must be a YAML object') + } + + const result: TemplateVariables = {} + for (const [key, value] of Object.entries(parsed)) { + if (typeof value !== 'string') { + throw new Error(`File template variable '${key}' must be a string file path`) + } + const filePath = value + if (!fs.existsSync(filePath)) { + throw new Error(`File for template variable '${key}' was not found: ${filePath}`) + } + result[key] = fs.readFileSync(filePath, 'utf-8') + } + + return result + } catch (error) { + throw new Error( + `Failed to parse file template variables: ${error instanceof Error ? error.message : 'Unknown error'}`, + ) + } +} + /** * Replace template variables in text using {{variable}} syntax */