Allow templating variables from files

This commit is contained in:
Sean Goedecke
2025-08-05 01:32:32 +00:00
parent c37f296c98
commit 15868b88f4
6 changed files with 127 additions and 11 deletions
+7 -1
View File
@@ -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` |
+43
View File
@@ -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) => {
+26 -7
View File
@@ -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')
})
})
})
+4
View File
@@ -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
+12 -3
View File
@@ -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<void> {
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<void> {
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)
+35
View File
@@ -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<string, unknown>
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
*/