Merge branch 'main' into sgoedecke/use-openai-sdk
This commit is contained in:
@@ -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` |
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
+40
-2
@@ -51633,6 +51633,41 @@ function parseTemplateVariables(input) {
|
||||
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
|
||||
*/
|
||||
@@ -51692,14 +51727,17 @@ async function run() {
|
||||
try {
|
||||
const promptFilePath = coreExports.getInput('prompt-file');
|
||||
const inputVariables = coreExports.getInput('input');
|
||||
const fileInputVariables = coreExports.getInput('file_input');
|
||||
let promptConfig = undefined;
|
||||
let systemPrompt = undefined;
|
||||
let prompt = undefined;
|
||||
// Check if we're using a prompt YAML file
|
||||
if (promptFilePath && isPromptYamlFile(promptFilePath)) {
|
||||
coreExports.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);
|
||||
}
|
||||
|
||||
+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 {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)
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user