Support .prompt.yml files
This commit is contained in:
@@ -36,17 +36,90 @@ jobs:
|
|||||||
|
|
||||||
### Using a prompt file
|
### Using a prompt file
|
||||||
|
|
||||||
You can also provide a prompt file instead of an inline prompt:
|
You can also provide a prompt file instead of an inline prompt. The action
|
||||||
|
supports both plain text files and structured `.prompt.yml` files:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
steps:
|
steps:
|
||||||
- name: Run AI Inference with Prompt File
|
- name: Run AI Inference with Text File
|
||||||
id: inference
|
id: inference
|
||||||
uses: actions/ai-inference@v1
|
uses: actions/ai-inference@v1
|
||||||
with:
|
with:
|
||||||
prompt-file: './path/to/prompt.txt'
|
prompt-file: './path/to/prompt.txt'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Using GitHub prompt.yml files
|
||||||
|
|
||||||
|
For more advanced use cases, you can use structured `.prompt.yml` files that
|
||||||
|
support templating, custom models, and JSON schema responses:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
- name: Run AI Inference with Prompt YAML
|
||||||
|
id: inference
|
||||||
|
uses: actions/ai-inference@v1
|
||||||
|
with:
|
||||||
|
prompt-file: './.github/prompts/sample.prompt.yml'
|
||||||
|
input: |
|
||||||
|
var1: hello
|
||||||
|
var2: ${{ steps.some-step.outputs.output }}
|
||||||
|
var3: |
|
||||||
|
Lorem Ipsum
|
||||||
|
Hello World
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Simple prompt.yml example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
messages:
|
||||||
|
- role: system
|
||||||
|
content: Be as concise as possible
|
||||||
|
- role: user
|
||||||
|
content: 'Compare {{a}} and {{b}}, please'
|
||||||
|
model: openai/gpt-4o
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Prompt.yml with JSON schema support
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
messages:
|
||||||
|
- role: system
|
||||||
|
content:
|
||||||
|
You are a helpful assistant that describes animals using JSON format
|
||||||
|
- role: user
|
||||||
|
content: |-
|
||||||
|
Describe a {{animal}}
|
||||||
|
Use JSON format as specified in the response schema
|
||||||
|
model: openai/gpt-4o
|
||||||
|
responseFormat: json_schema
|
||||||
|
jsonSchema: |-
|
||||||
|
{
|
||||||
|
"name": "describe_animal",
|
||||||
|
"strict": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the animal"
|
||||||
|
},
|
||||||
|
"habitat": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The habitat the animal lives in"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"habitat"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Variables in prompt.yml files are templated using `{{variable}}` format and are
|
||||||
|
supplied via the `input` parameter in YAML format.
|
||||||
|
|
||||||
### Using a system prompt file
|
### Using a system prompt file
|
||||||
|
|
||||||
In addition to the regular prompt, you can provide a system prompt file instead
|
In addition to the regular prompt, you can provide a system prompt file instead
|
||||||
@@ -108,13 +181,14 @@ Various inputs are defined in [`action.yml`](action.yml) to let you configure
|
|||||||
the action:
|
the action:
|
||||||
|
|
||||||
| Name | Description | Default |
|
| Name | Description | Default |
|
||||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
|
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
|
||||||
| `token` | Token to use for inference. Typically the GITHUB_TOKEN secret | `github.token` |
|
| `token` | Token to use for inference. Typically the GITHUB_TOKEN secret | `github.token` |
|
||||||
| `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. 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) | `""` |
|
||||||
| `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-4.1` |
|
| `model` | The model to use for inference. Must be available in the [GitHub Models](https://github.com/marketplace?type=models) catalog | `openai/gpt-4o` |
|
||||||
| `endpoint` | The endpoint to use for inference. If you're running this as part of an org, you should probably use the org-specific Models endpoint | `https://models.github.ai/inference` |
|
| `endpoint` | The endpoint to use for inference. If you're running this as part of an org, you should probably use the org-specific Models endpoint | `https://models.github.ai/inference` |
|
||||||
| `max-tokens` | The max number of tokens to generate | 200 |
|
| `max-tokens` | The max number of tokens to generate | 200 |
|
||||||
| `enable-github-mcp` | Enable Model Context Protocol integration with GitHub tools | `false` |
|
| `enable-github-mcp` | Enable Model Context Protocol integration with GitHub tools | `false` |
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
messages:
|
||||||
|
- role: system
|
||||||
|
content:
|
||||||
|
You are a helpful assistant that describes animals using JSON format
|
||||||
|
- role: user
|
||||||
|
content: |-
|
||||||
|
Describe a {{animal}}
|
||||||
|
Use JSON format as specified in the response schema
|
||||||
|
model: openai/gpt-4o
|
||||||
|
responseFormat: json_schema
|
||||||
|
jsonSchema: |-
|
||||||
|
{
|
||||||
|
"name": "describe_animal",
|
||||||
|
"strict": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the animal"
|
||||||
|
},
|
||||||
|
"habitat": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The habitat the animal lives in"
|
||||||
|
},
|
||||||
|
"characteristics": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Key characteristics of the animal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"habitat",
|
||||||
|
"characteristics"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
messages:
|
||||||
|
- role: system
|
||||||
|
content: Be as concise as possible
|
||||||
|
- role: user
|
||||||
|
content: 'Compare {{a}} and {{b}}, please'
|
||||||
|
model: openai/gpt-4o
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import { describe, it, expect } from '@jest/globals'
|
||||||
|
import {
|
||||||
|
buildMessages,
|
||||||
|
buildResponseFormat,
|
||||||
|
buildInferenceRequest
|
||||||
|
} from '../src/helpers'
|
||||||
|
import { PromptConfig } from '../src/prompt'
|
||||||
|
|
||||||
|
describe('helpers.ts - inference request building', () => {
|
||||||
|
describe('buildMessages', () => {
|
||||||
|
it('should build messages from prompt config', () => {
|
||||||
|
const promptConfig: PromptConfig = {
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: 'System message' },
|
||||||
|
{ role: 'user', content: 'User message' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = buildMessages(promptConfig)
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ role: 'system', content: 'System message' },
|
||||||
|
{ role: 'user', content: 'User message' }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should build messages from legacy format', () => {
|
||||||
|
const result = buildMessages(undefined, 'System prompt', 'User prompt')
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ role: 'system', content: 'System prompt' },
|
||||||
|
{ role: 'user', content: 'User prompt' }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use default system prompt when none provided', () => {
|
||||||
|
const result = buildMessages(undefined, undefined, 'User prompt')
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ role: 'system', content: 'You are a helpful assistant' },
|
||||||
|
{ role: 'user', content: 'User prompt' }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('buildResponseFormat', () => {
|
||||||
|
it('should build JSON schema response format', () => {
|
||||||
|
const promptConfig: PromptConfig = {
|
||||||
|
messages: [],
|
||||||
|
responseFormat: 'json_schema',
|
||||||
|
jsonSchema: JSON.stringify({
|
||||||
|
name: 'test_schema',
|
||||||
|
schema: { type: 'object' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = buildResponseFormat(promptConfig)
|
||||||
|
expect(result).toEqual({
|
||||||
|
type: 'json_schema',
|
||||||
|
json_schema: {
|
||||||
|
name: 'test_schema',
|
||||||
|
schema: { type: 'object' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return undefined for text format', () => {
|
||||||
|
const promptConfig: PromptConfig = {
|
||||||
|
messages: [],
|
||||||
|
responseFormat: 'text'
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = buildResponseFormat(promptConfig)
|
||||||
|
expect(result).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return undefined when no response format specified', () => {
|
||||||
|
const promptConfig: PromptConfig = {
|
||||||
|
messages: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = buildResponseFormat(promptConfig)
|
||||||
|
expect(result).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error for invalid JSON schema', () => {
|
||||||
|
const promptConfig: PromptConfig = {
|
||||||
|
messages: [],
|
||||||
|
responseFormat: 'json_schema',
|
||||||
|
jsonSchema: 'invalid json'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => buildResponseFormat(promptConfig)).toThrow(
|
||||||
|
'Invalid JSON schema'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('buildInferenceRequest', () => {
|
||||||
|
it('should build complete inference request from prompt config', () => {
|
||||||
|
const promptConfig: PromptConfig = {
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: 'System message' },
|
||||||
|
{ role: 'user', content: 'User message' }
|
||||||
|
],
|
||||||
|
responseFormat: 'json_schema',
|
||||||
|
jsonSchema: JSON.stringify({
|
||||||
|
name: 'test_schema',
|
||||||
|
schema: { type: 'object' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = buildInferenceRequest(
|
||||||
|
promptConfig,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
'gpt-4',
|
||||||
|
100,
|
||||||
|
'https://api.test.com',
|
||||||
|
'test-token'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: 'System message' },
|
||||||
|
{ role: 'user', content: 'User message' }
|
||||||
|
],
|
||||||
|
modelName: 'gpt-4',
|
||||||
|
maxTokens: 100,
|
||||||
|
endpoint: 'https://api.test.com',
|
||||||
|
token: 'test-token',
|
||||||
|
responseFormat: {
|
||||||
|
type: 'json_schema',
|
||||||
|
json_schema: {
|
||||||
|
name: 'test_schema',
|
||||||
|
schema: { type: 'object' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should build inference request from legacy format', () => {
|
||||||
|
const result = buildInferenceRequest(
|
||||||
|
undefined,
|
||||||
|
'System prompt',
|
||||||
|
'User prompt',
|
||||||
|
'gpt-4',
|
||||||
|
100,
|
||||||
|
'https://api.test.com',
|
||||||
|
'test-token'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: 'System prompt' },
|
||||||
|
{ role: 'user', content: 'User prompt' }
|
||||||
|
],
|
||||||
|
modelName: 'gpt-4',
|
||||||
|
maxTokens: 100,
|
||||||
|
endpoint: 'https://api.test.com',
|
||||||
|
token: 'test-token',
|
||||||
|
responseFormat: undefined
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -33,8 +33,10 @@ const { simpleInference, mcpInference } = await import('../src/inference.js')
|
|||||||
|
|
||||||
describe('inference.ts', () => {
|
describe('inference.ts', () => {
|
||||||
const mockRequest = {
|
const mockRequest = {
|
||||||
systemPrompt: 'You are a test assistant',
|
messages: [
|
||||||
prompt: 'Hello, AI!',
|
{ role: 'system', content: 'You are a test assistant' },
|
||||||
|
{ role: 'user', content: 'Hello, AI!' }
|
||||||
|
],
|
||||||
modelName: 'gpt-4',
|
modelName: 'gpt-4',
|
||||||
maxTokens: 100,
|
maxTokens: 100,
|
||||||
endpoint: 'https://api.test.com',
|
endpoint: 'https://api.test.com',
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import { describe, it, expect, beforeEach, jest } from '@jest/globals'
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
import { run } from '../src/main'
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
|
const __dirname = path.dirname(__filename)
|
||||||
|
|
||||||
|
// Mock the action toolkit functions
|
||||||
|
jest.mock('@actions/core')
|
||||||
|
|
||||||
|
// Mock fs to handle temporary file creation
|
||||||
|
jest.mock('fs')
|
||||||
|
|
||||||
|
// Mock the inference functions
|
||||||
|
jest.mock('../src/inference', () => ({
|
||||||
|
simpleInference: jest.fn(),
|
||||||
|
mcpInference: jest.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock the MCP connection
|
||||||
|
jest.mock('../src/mcp', () => ({
|
||||||
|
connectToGitHubMCP: jest.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { simpleInference } from '../src/inference'
|
||||||
|
|
||||||
|
describe('main.ts - prompt.yml integration', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
|
||||||
|
// Mock environment variables
|
||||||
|
process.env['GITHUB_TOKEN'] = 'test-token'
|
||||||
|
|
||||||
|
// Mock core.getInput to return appropriate values
|
||||||
|
const mockGetInput = core.getInput as jest.Mock
|
||||||
|
mockGetInput.mockImplementation((name: string) => {
|
||||||
|
switch (name) {
|
||||||
|
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 ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock core.getBooleanInput
|
||||||
|
const mockGetBooleanInput = core.getBooleanInput as jest.Mock
|
||||||
|
mockGetBooleanInput.mockReturnValue(false)
|
||||||
|
|
||||||
|
// Mock fs.existsSync
|
||||||
|
const mockExistsSync = fs.existsSync as jest.Mock
|
||||||
|
mockExistsSync.mockReturnValue(true)
|
||||||
|
|
||||||
|
// Mock fs.readFileSync for prompt file
|
||||||
|
const mockReadFileSync = fs.readFileSync as jest.Mock
|
||||||
|
mockReadFileSync.mockReturnValue(`
|
||||||
|
messages:
|
||||||
|
- role: system
|
||||||
|
content: Be as concise as possible
|
||||||
|
- role: user
|
||||||
|
content: 'Compare {{a}} and {{b}}, please'
|
||||||
|
model: openai/gpt-4o
|
||||||
|
`)
|
||||||
|
|
||||||
|
// Mock fs.writeFileSync
|
||||||
|
const mockWriteFileSync = fs.writeFileSync as jest.Mock
|
||||||
|
mockWriteFileSync.mockImplementation(() => {})
|
||||||
|
|
||||||
|
// Mock simpleInference
|
||||||
|
const mockSimpleInference = simpleInference as jest.Mock
|
||||||
|
mockSimpleInference.mockResolvedValue('Mocked AI response')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle prompt YAML files with template variables', async () => {
|
||||||
|
const mockGetInput = core.getInput as jest.Mock
|
||||||
|
mockGetInput.mockImplementation((name: string) => {
|
||||||
|
switch (name) {
|
||||||
|
case 'prompt-file':
|
||||||
|
return 'test.prompt.yml'
|
||||||
|
case 'input':
|
||||||
|
return 'a: cats\nb: dogs'
|
||||||
|
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()
|
||||||
|
|
||||||
|
// Verify simpleInference was called with the correct message structure
|
||||||
|
const mockSimpleInference = simpleInference as jest.Mock
|
||||||
|
expect(mockSimpleInference).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: 'Be as concise as possible'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: 'Compare cats and dogs, please'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
modelName: 'openai/gpt-4o',
|
||||||
|
maxTokens: 200,
|
||||||
|
endpoint: 'https://models.github.ai/inference',
|
||||||
|
token: 'test-token'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify outputs were set
|
||||||
|
expect(core.setOutput).toHaveBeenCalledWith(
|
||||||
|
'response',
|
||||||
|
'Mocked AI response'
|
||||||
|
)
|
||||||
|
expect(core.setOutput).toHaveBeenCalledWith(
|
||||||
|
'response-file',
|
||||||
|
expect.any(String)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fall back to legacy format when not using prompt YAML', async () => {
|
||||||
|
const mockGetInput = core.getInput as jest.Mock
|
||||||
|
mockGetInput.mockImplementation((name: string) => {
|
||||||
|
switch (name) {
|
||||||
|
case 'prompt':
|
||||||
|
return 'Hello, world!'
|
||||||
|
case 'system-prompt':
|
||||||
|
return 'You are helpful'
|
||||||
|
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()
|
||||||
|
|
||||||
|
// Verify simpleInference was called with legacy format
|
||||||
|
const mockSimpleInference = simpleInference as jest.Mock
|
||||||
|
expect(mockSimpleInference).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
systemPrompt: 'You are helpful',
|
||||||
|
prompt: 'Hello, world!',
|
||||||
|
modelName: 'openai/gpt-4o',
|
||||||
|
maxTokens: 200,
|
||||||
|
endpoint: 'https://models.github.ai/inference',
|
||||||
|
token: 'test-token'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
+16
-8
@@ -161,12 +161,15 @@ describe('main.ts', () => {
|
|||||||
await run()
|
await run()
|
||||||
|
|
||||||
expect(mockSimpleInference).toHaveBeenCalledWith({
|
expect(mockSimpleInference).toHaveBeenCalledWith({
|
||||||
systemPrompt: 'You are a test assistant.',
|
messages: [
|
||||||
prompt: 'Hello, AI!',
|
{ role: 'system', content: 'You are a test assistant.' },
|
||||||
|
{ role: 'user', content: 'Hello, AI!' }
|
||||||
|
],
|
||||||
modelName: 'gpt-4',
|
modelName: 'gpt-4',
|
||||||
maxTokens: 100,
|
maxTokens: 100,
|
||||||
endpoint: 'https://api.test.com',
|
endpoint: 'https://api.test.com',
|
||||||
token: 'fake-token'
|
token: 'fake-token',
|
||||||
|
responseFormat: undefined
|
||||||
})
|
})
|
||||||
expect(mockConnectToGitHubMCP).not.toHaveBeenCalled()
|
expect(mockConnectToGitHubMCP).not.toHaveBeenCalled()
|
||||||
expect(mockMcpInference).not.toHaveBeenCalled()
|
expect(mockMcpInference).not.toHaveBeenCalled()
|
||||||
@@ -193,8 +196,10 @@ describe('main.ts', () => {
|
|||||||
expect(mockConnectToGitHubMCP).toHaveBeenCalledWith('fake-token')
|
expect(mockConnectToGitHubMCP).toHaveBeenCalledWith('fake-token')
|
||||||
expect(mockMcpInference).toHaveBeenCalledWith(
|
expect(mockMcpInference).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
systemPrompt: 'You are a test assistant.',
|
messages: [
|
||||||
prompt: 'Hello, AI!',
|
{ role: 'system', content: 'You are a test assistant.' },
|
||||||
|
{ role: 'user', content: 'Hello, AI!' }
|
||||||
|
],
|
||||||
token: 'fake-token'
|
token: 'fake-token'
|
||||||
}),
|
}),
|
||||||
mockMcpClient
|
mockMcpClient
|
||||||
@@ -243,12 +248,15 @@ describe('main.ts', () => {
|
|||||||
await run()
|
await run()
|
||||||
|
|
||||||
expect(mockSimpleInference).toHaveBeenCalledWith({
|
expect(mockSimpleInference).toHaveBeenCalledWith({
|
||||||
systemPrompt: systemPromptContent,
|
messages: [
|
||||||
prompt: promptContent,
|
{ role: 'system', content: systemPromptContent },
|
||||||
|
{ role: 'user', content: promptContent }
|
||||||
|
],
|
||||||
modelName: 'gpt-4',
|
modelName: 'gpt-4',
|
||||||
maxTokens: 100,
|
maxTokens: 100,
|
||||||
endpoint: 'https://api.test.com',
|
endpoint: 'https://api.test.com',
|
||||||
token: 'fake-token'
|
token: 'fake-token',
|
||||||
|
responseFormat: undefined
|
||||||
})
|
})
|
||||||
verifyStandardResponse()
|
verifyStandardResponse()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { describe, it, expect } from '@jest/globals'
|
||||||
|
import * as path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
import {
|
||||||
|
parseTemplateVariables,
|
||||||
|
replaceTemplateVariables,
|
||||||
|
loadPromptFile,
|
||||||
|
isPromptYamlFile
|
||||||
|
} from '../src/prompt'
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
|
const __dirname = path.dirname(__filename)
|
||||||
|
|
||||||
|
describe('prompt.ts', () => {
|
||||||
|
describe('parseTemplateVariables', () => {
|
||||||
|
it('should parse simple YAML variables', () => {
|
||||||
|
const input = `
|
||||||
|
a: hello
|
||||||
|
b: world
|
||||||
|
`
|
||||||
|
const result = parseTemplateVariables(input)
|
||||||
|
expect(result).toEqual({ a: 'hello', b: 'world' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse multiline variables', () => {
|
||||||
|
const input = `
|
||||||
|
var1: hello
|
||||||
|
var2: |
|
||||||
|
This is a
|
||||||
|
multiline string
|
||||||
|
`
|
||||||
|
const result = parseTemplateVariables(input)
|
||||||
|
expect(result.var1).toBe('hello')
|
||||||
|
expect(result.var2).toContain('This is a')
|
||||||
|
expect(result.var2).toContain('multiline string')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return empty object for empty input', () => {
|
||||||
|
const result = parseTemplateVariables('')
|
||||||
|
expect(result).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error for invalid YAML', () => {
|
||||||
|
const input = 'invalid: yaml: content:'
|
||||||
|
expect(() => parseTemplateVariables(input)).toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('replaceTemplateVariables', () => {
|
||||||
|
it('should replace simple variables', () => {
|
||||||
|
const text = 'Hello {{name}}, welcome to {{place}}!'
|
||||||
|
const variables = { name: 'John', place: 'GitHub' }
|
||||||
|
const result = replaceTemplateVariables(text, variables)
|
||||||
|
expect(result).toBe('Hello John, welcome to GitHub!')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should leave unreplaced variables as is', () => {
|
||||||
|
const text = 'Hello {{name}}, welcome to {{unknown}}!'
|
||||||
|
const variables = { name: 'John' }
|
||||||
|
const result = replaceTemplateVariables(text, variables)
|
||||||
|
expect(result).toBe('Hello John, welcome to {{unknown}}!')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle no variables', () => {
|
||||||
|
const text = 'No variables here'
|
||||||
|
const variables = {}
|
||||||
|
const result = replaceTemplateVariables(text, variables)
|
||||||
|
expect(result).toBe('No variables here')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('isPromptYamlFile', () => {
|
||||||
|
it('should detect .prompt.yml files', () => {
|
||||||
|
expect(isPromptYamlFile('test.prompt.yml')).toBe(true)
|
||||||
|
expect(isPromptYamlFile('path/to/test.prompt.yml')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should detect .prompt.yaml files', () => {
|
||||||
|
expect(isPromptYamlFile('test.prompt.yaml')).toBe(true)
|
||||||
|
expect(isPromptYamlFile('path/to/test.prompt.yaml')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should reject other file types', () => {
|
||||||
|
expect(isPromptYamlFile('test.txt')).toBe(false)
|
||||||
|
expect(isPromptYamlFile('test.yml')).toBe(false)
|
||||||
|
expect(isPromptYamlFile('test.yaml')).toBe(false)
|
||||||
|
expect(isPromptYamlFile('test.prompt')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('loadPromptFile', () => {
|
||||||
|
it('should load simple prompt file', () => {
|
||||||
|
const filePath = path.join(
|
||||||
|
__dirname,
|
||||||
|
'../__fixtures__/prompts/simple.prompt.yml'
|
||||||
|
)
|
||||||
|
const variables = { a: 'cats', b: 'dogs' }
|
||||||
|
const result = loadPromptFile(filePath, variables)
|
||||||
|
|
||||||
|
expect(result.messages).toHaveLength(2)
|
||||||
|
expect(result.messages[0]).toEqual({
|
||||||
|
role: 'system',
|
||||||
|
content: 'Be as concise as possible'
|
||||||
|
})
|
||||||
|
expect(result.messages[1]).toEqual({
|
||||||
|
role: 'user',
|
||||||
|
content: 'Compare cats and dogs, please'
|
||||||
|
})
|
||||||
|
expect(result.model).toBe('openai/gpt-4o')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should load JSON schema prompt file', () => {
|
||||||
|
const filePath = path.join(
|
||||||
|
__dirname,
|
||||||
|
'../__fixtures__/prompts/json-schema.prompt.yml'
|
||||||
|
)
|
||||||
|
const variables = { animal: 'dog' }
|
||||||
|
const result = loadPromptFile(filePath, variables)
|
||||||
|
|
||||||
|
expect(result.messages).toHaveLength(2)
|
||||||
|
expect(result.messages[1].content).toContain('Describe a dog')
|
||||||
|
expect(result.responseFormat).toBe('json_schema')
|
||||||
|
expect(result.jsonSchema).toBeDefined()
|
||||||
|
expect(result.jsonSchema).toContain('describe_animal')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error for non-existent file', () => {
|
||||||
|
expect(() => loadPromptFile('non-existent.prompt.yml')).toThrow(
|
||||||
|
'Prompt file not found'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
+7
-1
@@ -14,7 +14,13 @@ inputs:
|
|||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
prompt-file:
|
prompt-file:
|
||||||
description: Path to a file containing the prompt
|
description:
|
||||||
|
Path to a file containing the prompt (supports .txt and .prompt.yml
|
||||||
|
formats)
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
input:
|
||||||
|
description: Template variables in YAML format for .prompt.yml files
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
model:
|
model:
|
||||||
|
|||||||
+3001
-69
File diff suppressed because it is too large
Load Diff
+1
-1
File diff suppressed because one or more lines are too long
Generated
+8
-2
@@ -12,6 +12,8 @@
|
|||||||
"@actions/core": "^1.11.1",
|
"@actions/core": "^1.11.1",
|
||||||
"@modelcontextprotocol/sdk": "^1.15.1",
|
"@modelcontextprotocol/sdk": "^1.15.1",
|
||||||
"@rollup/plugin-json": "^6.1.0",
|
"@rollup/plugin-json": "^6.1.0",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
"pkce-challenge": "^5.0.0"
|
"pkce-challenge": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -4407,6 +4409,12 @@
|
|||||||
"pretty-format": "^29.0.0"
|
"pretty-format": "^29.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/js-yaml": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
@@ -5173,7 +5181,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "Python-2.0"
|
"license": "Python-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/array-buffer-byte-length": {
|
"node_modules/array-buffer-byte-length": {
|
||||||
@@ -10254,7 +10261,6 @@
|
|||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
|
|||||||
@@ -40,6 +40,8 @@
|
|||||||
"@actions/core": "^1.11.1",
|
"@actions/core": "^1.11.1",
|
||||||
"@modelcontextprotocol/sdk": "^1.15.1",
|
"@modelcontextprotocol/sdk": "^1.15.1",
|
||||||
"@rollup/plugin-json": "^6.1.0",
|
"@rollup/plugin-json": "^6.1.0",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
"pkce-challenge": "^5.0.0"
|
"pkce-challenge": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import { GetChatCompletionsDefaultResponse } from '@azure-rest/ai-inference'
|
import { GetChatCompletionsDefaultResponse } from '@azure-rest/ai-inference'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
|
import { PromptConfig } from './prompt.js'
|
||||||
|
import { InferenceRequest } from './inference.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to load content from a file or use fallback input
|
* Helper function to load content from a file or use fallback input
|
||||||
@@ -64,3 +66,77 @@ export function handleUnexpectedResponse(
|
|||||||
: JSON.stringify(response.body))
|
: JSON.stringify(response.body))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build messages array from either prompt config or legacy format
|
||||||
|
*/
|
||||||
|
export function buildMessages(
|
||||||
|
promptConfig?: PromptConfig,
|
||||||
|
systemPrompt?: string,
|
||||||
|
prompt?: string
|
||||||
|
): Array<{ role: string; content: string }> {
|
||||||
|
if (promptConfig?.messages && promptConfig.messages.length > 0) {
|
||||||
|
// Use new message format
|
||||||
|
return promptConfig.messages.map((msg) => ({
|
||||||
|
role: msg.role,
|
||||||
|
content: msg.content
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
// Use legacy format
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: systemPrompt || 'You are a helpful assistant'
|
||||||
|
},
|
||||||
|
{ role: 'user', content: prompt || '' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build response format object for API from prompt config
|
||||||
|
*/
|
||||||
|
export function buildResponseFormat(promptConfig?: PromptConfig): any {
|
||||||
|
if (
|
||||||
|
promptConfig?.responseFormat === 'json_schema' &&
|
||||||
|
promptConfig.jsonSchema
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const schema = JSON.parse(promptConfig.jsonSchema)
|
||||||
|
return {
|
||||||
|
type: 'json_schema',
|
||||||
|
json_schema: schema
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid JSON schema: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build complete InferenceRequest from prompt config and inputs
|
||||||
|
*/
|
||||||
|
export function buildInferenceRequest(
|
||||||
|
promptConfig: PromptConfig | undefined,
|
||||||
|
systemPrompt: string | undefined,
|
||||||
|
prompt: string | undefined,
|
||||||
|
modelName: string,
|
||||||
|
maxTokens: number,
|
||||||
|
endpoint: string,
|
||||||
|
token: string
|
||||||
|
): InferenceRequest {
|
||||||
|
const messages = buildMessages(promptConfig, systemPrompt, prompt)
|
||||||
|
const responseFormat = buildResponseFormat(promptConfig)
|
||||||
|
|
||||||
|
return {
|
||||||
|
messages,
|
||||||
|
modelName,
|
||||||
|
maxTokens,
|
||||||
|
endpoint,
|
||||||
|
token,
|
||||||
|
responseFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+17
-19
@@ -5,12 +5,12 @@ import { GitHubMCPClient, executeToolCalls } from './mcp.js'
|
|||||||
import { handleUnexpectedResponse } from './helpers.js'
|
import { handleUnexpectedResponse } from './helpers.js'
|
||||||
|
|
||||||
export interface InferenceRequest {
|
export interface InferenceRequest {
|
||||||
systemPrompt: string
|
messages: Array<{ role: string; content: string }>
|
||||||
prompt: string
|
|
||||||
modelName: string
|
modelName: string
|
||||||
maxTokens: number
|
maxTokens: number
|
||||||
endpoint: string
|
endpoint: string
|
||||||
token: string
|
token: string
|
||||||
|
responseFormat?: any // Will contain the processed response format for the API
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InferenceResponse {
|
export interface InferenceResponse {
|
||||||
@@ -41,18 +41,17 @@ export async function simpleInference(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const requestBody = {
|
const requestBody: any = {
|
||||||
messages: [
|
messages: request.messages,
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: request.systemPrompt
|
|
||||||
},
|
|
||||||
{ role: 'user', content: request.prompt }
|
|
||||||
],
|
|
||||||
max_tokens: request.maxTokens,
|
max_tokens: request.maxTokens,
|
||||||
model: request.modelName
|
model: request.modelName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add response format if specified
|
||||||
|
if (request.responseFormat) {
|
||||||
|
requestBody.response_format = request.responseFormat
|
||||||
|
}
|
||||||
|
|
||||||
const response = await client.path('/chat/completions').post({
|
const response = await client.path('/chat/completions').post({
|
||||||
body: requestBody
|
body: requestBody
|
||||||
})
|
})
|
||||||
@@ -84,14 +83,8 @@ export async function mcpInference(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Start with the initial conversation
|
// Start with the pre-processed messages
|
||||||
const messages = [
|
const messages: Array<any> = [...request.messages]
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: request.systemPrompt
|
|
||||||
},
|
|
||||||
{ role: 'user', content: request.prompt }
|
|
||||||
]
|
|
||||||
|
|
||||||
let iterationCount = 0
|
let iterationCount = 0
|
||||||
const maxIterations = 5 // Prevent infinite loops
|
const maxIterations = 5 // Prevent infinite loops
|
||||||
@@ -100,13 +93,18 @@ export async function mcpInference(
|
|||||||
iterationCount++
|
iterationCount++
|
||||||
core.info(`MCP inference iteration ${iterationCount}`)
|
core.info(`MCP inference iteration ${iterationCount}`)
|
||||||
|
|
||||||
const requestBody = {
|
const requestBody: any = {
|
||||||
messages: messages,
|
messages: messages,
|
||||||
max_tokens: request.maxTokens,
|
max_tokens: request.maxTokens,
|
||||||
model: request.modelName,
|
model: request.modelName,
|
||||||
tools: githubMcpClient.tools
|
tools: githubMcpClient.tools
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add response format if specified (only on first iteration to avoid conflicts)
|
||||||
|
if (iterationCount === 1 && request.responseFormat) {
|
||||||
|
requestBody.response_format = request.responseFormat
|
||||||
|
}
|
||||||
|
|
||||||
const response = await client.path('/chat/completions').post({
|
const response = await client.path('/chat/completions').post({
|
||||||
body: requestBody
|
body: requestBody
|
||||||
})
|
})
|
||||||
|
|||||||
+39
-10
@@ -3,8 +3,13 @@ import * as fs from 'fs'
|
|||||||
import * as os from 'os'
|
import * as os from 'os'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import { connectToGitHubMCP } from './mcp.js'
|
import { connectToGitHubMCP } from './mcp.js'
|
||||||
import { simpleInference, mcpInference, InferenceRequest } from './inference.js'
|
import { simpleInference, mcpInference } from './inference.js'
|
||||||
import { loadContentFromFileOrInput } from './helpers.js'
|
import { loadContentFromFileOrInput, buildInferenceRequest } from './helpers.js'
|
||||||
|
import {
|
||||||
|
loadPromptFile,
|
||||||
|
parseTemplateVariables,
|
||||||
|
isPromptYamlFile
|
||||||
|
} from './prompt.js'
|
||||||
|
|
||||||
const RESPONSE_FILE = 'modelResponse.txt'
|
const RESPONSE_FILE = 'modelResponse.txt'
|
||||||
|
|
||||||
@@ -15,16 +20,37 @@ const RESPONSE_FILE = 'modelResponse.txt'
|
|||||||
*/
|
*/
|
||||||
export async function run(): Promise<void> {
|
export async function run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const prompt = loadContentFromFileOrInput('prompt-file', 'prompt')
|
const promptFilePath = core.getInput('prompt-file')
|
||||||
|
const inputVariables = core.getInput('input')
|
||||||
|
|
||||||
const systemPrompt = loadContentFromFileOrInput(
|
let promptConfig: any = undefined
|
||||||
|
let systemPrompt: string | undefined = undefined
|
||||||
|
let prompt: string | undefined = undefined
|
||||||
|
|
||||||
|
// Check if we're using a prompt YAML file
|
||||||
|
if (promptFilePath && isPromptYamlFile(promptFilePath)) {
|
||||||
|
core.info('Using prompt YAML file format')
|
||||||
|
|
||||||
|
// Parse template variables
|
||||||
|
const templateVariables = parseTemplateVariables(inputVariables)
|
||||||
|
|
||||||
|
// Load and process prompt file
|
||||||
|
promptConfig = loadPromptFile(promptFilePath, templateVariables)
|
||||||
|
} else {
|
||||||
|
// Use legacy format
|
||||||
|
core.info('Using legacy prompt format')
|
||||||
|
|
||||||
|
prompt = loadContentFromFileOrInput('prompt-file', 'prompt')
|
||||||
|
systemPrompt = loadContentFromFileOrInput(
|
||||||
'system-prompt-file',
|
'system-prompt-file',
|
||||||
'system-prompt',
|
'system-prompt',
|
||||||
'You are a helpful assistant'
|
'You are a helpful assistant'
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const modelName: string = core.getInput('model')
|
// Get common parameters
|
||||||
const maxTokens: number = parseInt(core.getInput('max-tokens'), 10)
|
const modelName = promptConfig?.model || core.getInput('model')
|
||||||
|
const maxTokens = parseInt(core.getInput('max-tokens'), 10)
|
||||||
|
|
||||||
const token = process.env['GITHUB_TOKEN'] || core.getInput('token')
|
const token = process.env['GITHUB_TOKEN'] || core.getInput('token')
|
||||||
if (token === undefined) {
|
if (token === undefined) {
|
||||||
@@ -32,21 +58,24 @@ export async function run(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = core.getInput('endpoint')
|
const endpoint = core.getInput('endpoint')
|
||||||
const enableMcp = core.getBooleanInput('enable-github-mcp') || false
|
|
||||||
|
|
||||||
const inferenceRequest: InferenceRequest = {
|
// Build the inference request with pre-processed messages and response format
|
||||||
|
const inferenceRequest = buildInferenceRequest(
|
||||||
|
promptConfig,
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
prompt,
|
prompt,
|
||||||
modelName,
|
modelName,
|
||||||
maxTokens,
|
maxTokens,
|
||||||
endpoint,
|
endpoint,
|
||||||
token
|
token
|
||||||
}
|
)
|
||||||
|
|
||||||
|
const enableMcp = core.getBooleanInput('enable-github-mcp') || false
|
||||||
|
|
||||||
let modelResponse: string | null = null
|
let modelResponse: string | null = null
|
||||||
|
|
||||||
if (enableMcp) {
|
if (enableMcp) {
|
||||||
const mcpClient = await connectToGitHubMCP(token)
|
const mcpClient = await connectToGitHubMCP(inferenceRequest.token)
|
||||||
|
|
||||||
if (mcpClient) {
|
if (mcpClient) {
|
||||||
modelResponse = await mcpInference(inferenceRequest, mcpClient)
|
modelResponse = await mcpInference(inferenceRequest, mcpClient)
|
||||||
|
|||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as yaml from 'js-yaml'
|
||||||
|
|
||||||
|
export interface PromptMessage {
|
||||||
|
role: 'system' | 'user' | 'assistant'
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PromptConfig {
|
||||||
|
messages: PromptMessage[]
|
||||||
|
model?: string
|
||||||
|
responseFormat?: 'text' | 'json_schema'
|
||||||
|
jsonSchema?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemplateVariables {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse template variables from YAML input string
|
||||||
|
*/
|
||||||
|
export function parseTemplateVariables(input: string): TemplateVariables {
|
||||||
|
if (!input.trim()) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = yaml.load(input) as TemplateVariables
|
||||||
|
if (typeof parsed !== 'object' || parsed === null) {
|
||||||
|
throw new Error('Template variables must be a YAML object')
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to parse template variables: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace template variables in text using {{variable}} syntax
|
||||||
|
*/
|
||||||
|
export function replaceTemplateVariables(
|
||||||
|
text: string,
|
||||||
|
variables: TemplateVariables
|
||||||
|
): string {
|
||||||
|
return text.replace(/\{\{(\w+)\}\}/g, (match, variableName) => {
|
||||||
|
if (variableName in variables) {
|
||||||
|
return variables[variableName]
|
||||||
|
}
|
||||||
|
core.warning(
|
||||||
|
`Template variable '${variableName}' not found in input variables`
|
||||||
|
)
|
||||||
|
return match // Return the original placeholder if variable not found
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load and parse a prompt YAML file with template variable substitution
|
||||||
|
*/
|
||||||
|
export function loadPromptFile(
|
||||||
|
filePath: string,
|
||||||
|
templateVariables: TemplateVariables = {}
|
||||||
|
): PromptConfig {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
throw new Error(`Prompt file not found: ${filePath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
||||||
|
|
||||||
|
// Apply template variable substitution
|
||||||
|
const processedContent = replaceTemplateVariables(
|
||||||
|
fileContent,
|
||||||
|
templateVariables
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const config = yaml.load(processedContent) as PromptConfig
|
||||||
|
|
||||||
|
if (!config.messages || !Array.isArray(config.messages)) {
|
||||||
|
throw new Error('Prompt file must contain a "messages" array')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate messages
|
||||||
|
for (const message of config.messages) {
|
||||||
|
if (!message.role || !message.content) {
|
||||||
|
throw new Error(
|
||||||
|
'Each message must have "role" and "content" properties'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!['system', 'user', 'assistant'].includes(message.role)) {
|
||||||
|
throw new Error(`Invalid message role: ${message.role}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to parse prompt file: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a file is a prompt YAML file based on extension
|
||||||
|
*/
|
||||||
|
export function isPromptYamlFile(filePath: string): boolean {
|
||||||
|
return filePath.endsWith('.prompt.yml') || filePath.endsWith('.prompt.yaml')
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user