Merge pull request #85 from actions/sgoedecke/file-inputs
Allow templating variables from files
This commit is contained in:
@@ -65,6 +65,9 @@ steps:
|
|||||||
var3: |
|
var3: |
|
||||||
Lorem Ipsum
|
Lorem Ipsum
|
||||||
Hello World
|
Hello World
|
||||||
|
file_input: |
|
||||||
|
var4: ./path/to/long-text.txt
|
||||||
|
var5: ./path/to/config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Simple prompt.yml example
|
#### Simple prompt.yml example
|
||||||
@@ -116,7 +119,9 @@ jsonSchema: |-
|
|||||||
```
|
```
|
||||||
|
|
||||||
Variables in prompt.yml files are templated using `{{variable}}` format and are
|
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
|
### Using a system prompt file
|
||||||
|
|
||||||
@@ -197,6 +202,7 @@ the action:
|
|||||||
| `prompt` | The prompt to send to the model | N/A |
|
| `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 | `""` |
|
| `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) | `""` |
|
| `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` | 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 | `""` |
|
| `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` |
|
| `model` | The model to use for inference. Must be available in the [GitHub Models](https://github.com/marketplace?type=models) catalog | `openai/gpt-4o` |
|
||||||
|
|||||||
@@ -130,6 +130,49 @@ model: openai/gpt-4o
|
|||||||
expect(core.setOutput).toHaveBeenCalledWith('response-file', expect.any(String))
|
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 () => {
|
it('should fall back to legacy format when not using prompt YAML', async () => {
|
||||||
mockExistsSync.mockReturnValue(false)
|
mockExistsSync.mockReturnValue(false)
|
||||||
core.getInput.mockImplementation((name: string) => {
|
core.getInput.mockImplementation((name: string) => {
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import {describe, it, expect} from 'vitest'
|
import {describe, it, expect} from 'vitest'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import {fileURLToPath} from 'url'
|
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 __filename = fileURLToPath(import.meta.url)
|
||||||
const __dirname = path.dirname(__filename)
|
const __dirname = path.dirname(__filename)
|
||||||
@@ -10,8 +16,8 @@ describe('prompt.ts', () => {
|
|||||||
describe('parseTemplateVariables', () => {
|
describe('parseTemplateVariables', () => {
|
||||||
it('should parse simple YAML variables', () => {
|
it('should parse simple YAML variables', () => {
|
||||||
const input = `
|
const input = `
|
||||||
a: hello
|
a: hello
|
||||||
b: world
|
b: world
|
||||||
`
|
`
|
||||||
const result = parseTemplateVariables(input)
|
const result = parseTemplateVariables(input)
|
||||||
expect(result).toEqual({a: 'hello', b: 'world'})
|
expect(result).toEqual({a: 'hello', b: 'world'})
|
||||||
@@ -19,10 +25,10 @@ b: world
|
|||||||
|
|
||||||
it('should parse multiline variables', () => {
|
it('should parse multiline variables', () => {
|
||||||
const input = `
|
const input = `
|
||||||
var1: hello
|
var1: hello
|
||||||
var2: |
|
var2: |
|
||||||
This is a
|
This is a
|
||||||
multiline string
|
multiline string
|
||||||
`
|
`
|
||||||
const result = parseTemplateVariables(input)
|
const result = parseTemplateVariables(input)
|
||||||
expect(result.var1).toBe('hello')
|
expect(result.var1).toBe('hello')
|
||||||
@@ -117,4 +123,17 @@ var2: |
|
|||||||
expect(() => loadPromptFile('non-existent.prompt.yml')).toThrow('Prompt file not found')
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ inputs:
|
|||||||
description: Template variables in YAML format for .prompt.yml files
|
description: Template variables in YAML format for .prompt.yml files
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
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:
|
model:
|
||||||
description: The model to use
|
description: The model to use
|
||||||
required: false
|
required: false
|
||||||
|
|||||||
+40
-2
@@ -52047,6 +52047,41 @@ function parseTemplateVariables(input) {
|
|||||||
throw new Error(`Failed to parse template variables: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
throw new Error(`Failed to parse template variables: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
function parseFileTemplateVariables(fileInput) {
|
||||||
|
if (!fileInput.trim()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = load(fileInput);
|
||||||
|
if (typeof parsed !== 'object' || parsed === null) {
|
||||||
|
throw new Error('File template variables must be a YAML object');
|
||||||
|
}
|
||||||
|
const result = {};
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
result[key] = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
throw new Error(`Failed to read file for template variable '${key}' at path '${filePath}': ${err instanceof Error ? err.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
* Replace template variables in text using {{variable}} syntax
|
||||||
*/
|
*/
|
||||||
@@ -52106,14 +52141,17 @@ async function run() {
|
|||||||
try {
|
try {
|
||||||
const promptFilePath = coreExports.getInput('prompt-file');
|
const promptFilePath = coreExports.getInput('prompt-file');
|
||||||
const inputVariables = coreExports.getInput('input');
|
const inputVariables = coreExports.getInput('input');
|
||||||
|
const fileInputVariables = coreExports.getInput('file_input');
|
||||||
let promptConfig = undefined;
|
let promptConfig = undefined;
|
||||||
let systemPrompt = undefined;
|
let systemPrompt = undefined;
|
||||||
let prompt = undefined;
|
let prompt = undefined;
|
||||||
// Check if we're using a prompt YAML file
|
// Check if we're using a prompt YAML file
|
||||||
if (promptFilePath && isPromptYamlFile(promptFilePath)) {
|
if (promptFilePath && isPromptYamlFile(promptFilePath)) {
|
||||||
coreExports.info('Using prompt YAML file format');
|
coreExports.info('Using prompt YAML file format');
|
||||||
// Parse template variables
|
// Parse template variables from both string inputs and file-based inputs
|
||||||
const templateVariables = parseTemplateVariables(inputVariables);
|
const stringVars = parseTemplateVariables(inputVariables);
|
||||||
|
const fileVars = parseFileTemplateVariables(fileInputVariables);
|
||||||
|
const templateVariables = { ...stringVars, ...fileVars };
|
||||||
// Load and process prompt file
|
// Load and process prompt file
|
||||||
promptConfig = loadPromptFile(promptFilePath, templateVariables);
|
promptConfig = loadPromptFile(promptFilePath, templateVariables);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+12
-3
@@ -5,7 +5,13 @@ import * as path from 'path'
|
|||||||
import {connectToGitHubMCP} from './mcp.js'
|
import {connectToGitHubMCP} from './mcp.js'
|
||||||
import {simpleInference, mcpInference} from './inference.js'
|
import {simpleInference, mcpInference} from './inference.js'
|
||||||
import {loadContentFromFileOrInput, buildInferenceRequest} from './helpers.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'
|
const RESPONSE_FILE = 'modelResponse.txt'
|
||||||
|
|
||||||
@@ -18,6 +24,7 @@ export async function run(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
const promptFilePath = core.getInput('prompt-file')
|
const promptFilePath = core.getInput('prompt-file')
|
||||||
const inputVariables = core.getInput('input')
|
const inputVariables = core.getInput('input')
|
||||||
|
const fileInputVariables = core.getInput('file_input')
|
||||||
|
|
||||||
let promptConfig: PromptConfig | undefined = undefined
|
let promptConfig: PromptConfig | undefined = undefined
|
||||||
let systemPrompt: string | undefined = undefined
|
let systemPrompt: string | undefined = undefined
|
||||||
@@ -27,8 +34,10 @@ export async function run(): Promise<void> {
|
|||||||
if (promptFilePath && isPromptYamlFile(promptFilePath)) {
|
if (promptFilePath && isPromptYamlFile(promptFilePath)) {
|
||||||
core.info('Using prompt YAML file format')
|
core.info('Using prompt YAML file format')
|
||||||
|
|
||||||
// Parse template variables
|
// Parse template variables from both string inputs and file-based inputs
|
||||||
const templateVariables = parseTemplateVariables(inputVariables)
|
const stringVars = parseTemplateVariables(inputVariables)
|
||||||
|
const fileVars = parseFileTemplateVariables(fileInputVariables)
|
||||||
|
const templateVariables = {...stringVars, ...fileVars}
|
||||||
|
|
||||||
// Load and process prompt file
|
// Load and process prompt file
|
||||||
promptConfig = loadPromptFile(promptFilePath, templateVariables)
|
promptConfig = loadPromptFile(promptFilePath, templateVariables)
|
||||||
|
|||||||
@@ -37,6 +37,47 @@ 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}`)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
result[key] = fs.readFileSync(filePath, 'utf-8')
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to read file for template variable '${key}' at path '${filePath}': ${err instanceof Error ? err.message : 'Unknown error'}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
* Replace template variables in text using {{variable}} syntax
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user