Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d645f067d8 | ||
|
|
9c57490bf1 | ||
|
|
aa31275bdc | ||
|
|
cacab0de8c | ||
|
|
8562e77a99 | ||
|
|
9aac9c75b3 | ||
|
|
eb37c9a493 | ||
|
|
7ee5d2347b | ||
|
|
c9a9379c71 | ||
|
|
ad31e754e3 | ||
|
|
c4ce17bc84 | ||
|
|
c0259b3c7d | ||
|
|
989a68a941 | ||
|
|
3e924fe06b | ||
|
|
96e0fda3bb | ||
|
|
91ba53d8b4 | ||
|
|
f8ee4c952b | ||
|
|
d0b41e9e29 | ||
|
|
8eaf9b3bbc | ||
|
|
d9d6269e33 | ||
|
|
d043c3eaa1 | ||
|
|
5b3308935f | ||
|
|
2d37c90e93 | ||
|
|
b72d8483c7 | ||
|
|
2aea1d4fc8 | ||
|
|
1c557cdc25 | ||
|
|
f1591cfa68 | ||
|
|
43f6a3831f | ||
|
|
a2fd55fb87 | ||
|
|
c7105a4c1e | ||
|
|
b75b177af3 | ||
|
|
a3d57cc6dc | ||
|
|
9239ab5d65 | ||
|
|
72f855ca83 |
30
.github/workflows/ci.yml
vendored
30
.github/workflows/ci.yml
vendored
@@ -67,3 +67,33 @@ jobs:
|
||||
- name: Print Output
|
||||
id: output
|
||||
run: echo "${{ steps.test-action.outputs.response }}"
|
||||
|
||||
test-action-prompt-file:
|
||||
name: GitHub Actions Test with Prompt File
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
id: checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create Prompt File
|
||||
run: echo "hello" > prompt.txt
|
||||
|
||||
- name: Create System Prompt File
|
||||
run:
|
||||
echo "You are a helpful AI assistant for testing." > system-prompt.txt
|
||||
|
||||
- name: Test Local Action with Prompt File
|
||||
id: test-action-prompt-file
|
||||
uses: ./
|
||||
with:
|
||||
prompt-file: prompt.txt
|
||||
system-prompt-file: system-prompt.txt
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Print Output
|
||||
run: |
|
||||
echo "Response saved to: ${{ steps.test-action-prompt-file.outputs.response-file }}"
|
||||
cat "${{ steps.test-action-prompt-file.outputs.response-file }}"
|
||||
|
||||
2
.github/workflows/linter.yml
vendored
2
.github/workflows/linter.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
- name: Lint Codebase
|
||||
id: super-linter
|
||||
uses: super-linter/super-linter/slim@4e8a7c2bf106c4c766c816b35ec612638dc9b6b2
|
||||
uses: super-linter/super-linter/slim@12150456a73e248bdc94d0794898f94e23127c88
|
||||
env:
|
||||
DEFAULT_BRANCH: main
|
||||
FILTER_REGEX_EXCLUDE: dist/**/*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: undici
|
||||
version: 5.28.5
|
||||
version: 5.29.0
|
||||
type: npm
|
||||
summary: An HTTP/1.1 client, written from scratch for Node.js
|
||||
homepage: https://undici.nodejs.org
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
############################################################################
|
||||
|
||||
# Default owners, unless a later match takes precedence.
|
||||
* @actions/actions-oss-maintainers
|
||||
* @actions/models
|
||||
|
||||
@@ -46,7 +46,7 @@ avoid having to include the `node_modules/` directory in the repository.
|
||||
1. Make your change, add tests, and make sure the tests still pass:
|
||||
`npm run test`
|
||||
1. Make sure your code is correctly formatted: `npm run format`
|
||||
1. Update `dist/index.js` using `npm run build`. This creates a single
|
||||
1. Update `dist/index.js` using `npm run bundle`. This creates a single
|
||||
JavaScript file that is used as an entrypoint for the action
|
||||
1. Push to your fork and [submit a pull request][pr]
|
||||
1. Pat yourself on the back and wait for your pull request to be reviewed and
|
||||
|
||||
78
README.md
78
README.md
@@ -7,7 +7,7 @@
|
||||
[](./badges/coverage.svg)
|
||||
|
||||
Use AI models from [GitHub Models](https://github.com/marketplace/models) in
|
||||
your actions.
|
||||
your workflows.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -34,27 +34,76 @@ jobs:
|
||||
run: echo "${{ steps.inference.outputs.response }}"
|
||||
```
|
||||
|
||||
### Using a prompt file
|
||||
|
||||
You can also provide a prompt file instead of an inline prompt:
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- name: Run AI Inference with Prompt File
|
||||
id: inference
|
||||
uses: actions/ai-inference@v1
|
||||
with:
|
||||
prompt-file: './path/to/prompt.txt'
|
||||
```
|
||||
|
||||
### Using a system prompt file
|
||||
|
||||
In addition to the regular prompt, you can provide a system prompt file instead
|
||||
of an inline system prompt:
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- name: Run AI Inference with System Prompt File
|
||||
id: inference
|
||||
uses: actions/ai-inference@v1
|
||||
with:
|
||||
prompt: 'Hello!'
|
||||
system-prompt-file: './path/to/system-prompt.txt'
|
||||
```
|
||||
|
||||
### Read output from file instead of output
|
||||
|
||||
This can be useful when model response exceeds actions output limit
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- name: Test Local Action
|
||||
id: inference
|
||||
uses: actions/ai-inference@v1
|
||||
with:
|
||||
prompt: 'Hello!'
|
||||
|
||||
- name: Use Response File
|
||||
run: |
|
||||
echo "Response saved to: ${{ steps.inference.outputs.response-file }}"
|
||||
cat "${{ steps.inference.outputs.response-file }}"
|
||||
```
|
||||
|
||||
## Inputs
|
||||
|
||||
Various inputs are defined in [`action.yml`](action.yml) to let you configure
|
||||
the action:
|
||||
|
||||
| Name | Description | Default |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
|
||||
| `token` | Token to use for inference. Typically the GITHUB_TOKEN secret | `github.token` |
|
||||
| `prompt` | The prompt to send to the model | N/A |
|
||||
| `system-prompt` | The system prompt to send to the model | `""` |
|
||||
| `model` | The model to use for inference. Must be available in the [GitHub Models](https://github.com/marketplace?type=models) catalog | `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` |
|
||||
| `max-tokens` | The max number of tokens to generate | 200 |
|
||||
| Name | Description | Default |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
|
||||
| `token` | Token to use for inference. Typically the GITHUB_TOKEN secret | `github.token` |
|
||||
| `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 | `""` |
|
||||
| `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 | `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` |
|
||||
| `max-tokens` | The max number of tokens to generate | 200 |
|
||||
|
||||
## Outputs
|
||||
|
||||
The AI inference action provides the following outputs:
|
||||
|
||||
| Name | Description |
|
||||
| ---------- | --------------------------- |
|
||||
| `response` | The response from the model |
|
||||
| Name | Description |
|
||||
| --------------- | ----------------------------------------------------------------------- |
|
||||
| `response` | The response from the model |
|
||||
| `response-file` | The file path where the response is saved (useful for larger responses) |
|
||||
|
||||
## Required Permissions
|
||||
|
||||
@@ -96,9 +145,10 @@ following steps:
|
||||
to create a new release in GitHub so users can easily reference the new tags
|
||||
in their workflows.
|
||||
|
||||
## License
|
||||
## License
|
||||
|
||||
This project is licensed under the terms of the MIT open source license. Please refer to [MIT](./LICENSE.txt) for the full terms.
|
||||
This project is licensed under the terms of the MIT open source license. Please
|
||||
refer to [MIT](./LICENSE.txt) for the full terms.
|
||||
|
||||
## Contributions
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
*/
|
||||
import { jest } from '@jest/globals'
|
||||
import * as core from '../__fixtures__/core.js'
|
||||
|
||||
const mockPost = jest.fn().mockImplementation(() => ({
|
||||
body: {
|
||||
choices: [
|
||||
@@ -29,6 +28,81 @@ jest.unstable_mockModule('@azure-rest/ai-inference', () => ({
|
||||
isUnexpected: jest.fn(() => false)
|
||||
}))
|
||||
|
||||
// Default to throwing errors to catch unexpected calls
|
||||
const mockExistsSync = jest.fn().mockImplementation(() => {
|
||||
throw new Error(
|
||||
'Unexpected call to existsSync - test should override this implementation'
|
||||
)
|
||||
})
|
||||
const mockReadFileSync = jest.fn().mockImplementation(() => {
|
||||
throw new Error(
|
||||
'Unexpected call to readFileSync - test should override this implementation'
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Helper function to mock file system operations for one or more files
|
||||
* @param fileContents - Object mapping file paths to their contents
|
||||
* @param nonExistentFiles - Array of file paths that should be treated as non-existent
|
||||
*/
|
||||
function mockFileContent(
|
||||
fileContents: Record<string, string> = {},
|
||||
nonExistentFiles: string[] = []
|
||||
): void {
|
||||
// Mock existsSync to return true for files that exist, false for those that don't
|
||||
mockExistsSync.mockImplementation((...args: unknown[]): boolean => {
|
||||
const [path] = args as [string]
|
||||
if (nonExistentFiles.includes(path)) {
|
||||
return false
|
||||
}
|
||||
return path in fileContents || true
|
||||
})
|
||||
|
||||
// Mock readFileSync to return the content for known files
|
||||
mockReadFileSync.mockImplementation((...args: unknown[]): string => {
|
||||
const [path, options] = args as [string, BufferEncoding]
|
||||
if (options === 'utf-8' && path in fileContents) {
|
||||
return fileContents[path]
|
||||
}
|
||||
throw new Error(`Unexpected file read: ${path}`)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to mock action inputs
|
||||
* @param inputs - Object mapping input names to their values
|
||||
*/
|
||||
function mockInputs(inputs: Record<string, string> = {}): void {
|
||||
// Default values that are applied unless overridden
|
||||
const defaultInputs: Record<string, string> = {
|
||||
token: 'fake-token'
|
||||
}
|
||||
|
||||
// Combine defaults with user-provided inputs
|
||||
const allInputs: Record<string, string> = { ...defaultInputs, ...inputs }
|
||||
|
||||
core.getInput.mockImplementation((name: string) => {
|
||||
return allInputs[name] || ''
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to verify common response assertions
|
||||
*/
|
||||
function verifyStandardResponse(): void {
|
||||
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'response', 'Hello, user!')
|
||||
expect(core.setOutput).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'response-file',
|
||||
expect.stringContaining('modelResponse.txt')
|
||||
)
|
||||
}
|
||||
|
||||
jest.unstable_mockModule('fs', () => ({
|
||||
existsSync: mockExistsSync,
|
||||
readFileSync: mockReadFileSync
|
||||
}))
|
||||
|
||||
jest.unstable_mockModule('@actions/core', () => core)
|
||||
|
||||
// The module being tested should be imported dynamically. This ensures that the
|
||||
@@ -36,37 +110,270 @@ jest.unstable_mockModule('@actions/core', () => core)
|
||||
const { run } = await import('../src/main.js')
|
||||
|
||||
describe('main.ts', () => {
|
||||
// Reset all mocks before each test
|
||||
beforeEach(() => {
|
||||
// Set the action's inputs as return values from core.getInput().
|
||||
core.getInput.mockImplementation((name) => {
|
||||
if (name === 'prompt') return 'Hello, AI!'
|
||||
if (name === 'system_prompt') return 'You are a test assistant.'
|
||||
if (name === 'model_name') return 'gpt-4o'
|
||||
return ''
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('Sets the response output', async () => {
|
||||
// Set the action's inputs as return values from core.getInput().
|
||||
mockInputs({
|
||||
prompt: 'Hello, AI!',
|
||||
'system-prompt': 'You are a test assistant.'
|
||||
})
|
||||
|
||||
await run()
|
||||
|
||||
expect(core.setOutput).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'response',
|
||||
'Hello, user!'
|
||||
)
|
||||
expect(core.setOutput).toHaveBeenCalled()
|
||||
verifyStandardResponse()
|
||||
})
|
||||
|
||||
it('Sets a failed status', async () => {
|
||||
// Clear the getInput mock and return an empty prompt
|
||||
core.getInput.mockClear().mockReturnValueOnce('')
|
||||
it('Sets a failed status when no prompt is set', async () => {
|
||||
// Clear the getInput mock and simulate no prompt or prompt-file input
|
||||
mockInputs({
|
||||
prompt: '',
|
||||
'prompt-file': ''
|
||||
})
|
||||
|
||||
await run()
|
||||
|
||||
// Verify that the action was marked as failed.
|
||||
expect(core.setFailed).toHaveBeenNthCalledWith(1, 'prompt is not set')
|
||||
expect(core.setFailed).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'Neither prompt-file nor prompt was set'
|
||||
)
|
||||
})
|
||||
|
||||
it('uses prompt-file', async () => {
|
||||
const promptFile = 'prompt.txt'
|
||||
const promptContent = 'This is a prompt from a file'
|
||||
|
||||
// Set up mock to return specific content for the prompt file
|
||||
mockFileContent({
|
||||
[promptFile]: promptContent
|
||||
})
|
||||
|
||||
// Set up input mocks
|
||||
mockInputs({
|
||||
'prompt-file': promptFile,
|
||||
'system-prompt': 'You are a test assistant.'
|
||||
})
|
||||
|
||||
await run()
|
||||
|
||||
expect(mockExistsSync).toHaveBeenCalledWith(promptFile)
|
||||
expect(mockReadFileSync).toHaveBeenCalledWith(promptFile, 'utf-8')
|
||||
verifyStandardResponse()
|
||||
})
|
||||
|
||||
it('handles non-existent prompt-file with an error', async () => {
|
||||
const promptFile = 'non-existent-prompt.txt'
|
||||
|
||||
// Mock the file not existing
|
||||
mockFileContent({}, [promptFile])
|
||||
|
||||
// Set up input mocks
|
||||
mockInputs({
|
||||
'prompt-file': promptFile
|
||||
})
|
||||
|
||||
await run()
|
||||
|
||||
// Verify that the error was correctly reported
|
||||
expect(core.setFailed).toHaveBeenCalledWith(
|
||||
`File for prompt-file was not found: ${promptFile}`
|
||||
)
|
||||
})
|
||||
|
||||
it('prefers prompt-file over prompt when both are provided', async () => {
|
||||
const promptFile = 'prompt.txt'
|
||||
const promptFileContent = 'This is a prompt from a file that should be used'
|
||||
const promptString = 'This is a direct prompt that should be ignored'
|
||||
|
||||
// Set up mock to return specific content for the prompt file
|
||||
mockFileContent({
|
||||
[promptFile]: promptFileContent
|
||||
})
|
||||
|
||||
// Set up input mocks
|
||||
mockInputs({
|
||||
prompt: promptString,
|
||||
'prompt-file': promptFile,
|
||||
'system-prompt': 'You are a test assistant.'
|
||||
})
|
||||
|
||||
await run()
|
||||
|
||||
expect(mockExistsSync).toHaveBeenCalledWith(promptFile)
|
||||
expect(mockReadFileSync).toHaveBeenCalledWith(promptFile, 'utf-8')
|
||||
|
||||
// Check that the post call was made with the prompt from the file, not the input parameter
|
||||
expect(mockPost).toHaveBeenCalledWith({
|
||||
body: {
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: expect.any(String)
|
||||
},
|
||||
{ role: 'user', content: promptFileContent } // Should use the file content, not the string input
|
||||
],
|
||||
max_tokens: expect.any(Number),
|
||||
model: expect.any(String)
|
||||
}
|
||||
})
|
||||
|
||||
verifyStandardResponse()
|
||||
})
|
||||
|
||||
it('uses system-prompt-file', async () => {
|
||||
const systemPromptFile = 'system-prompt.txt'
|
||||
const systemPromptContent =
|
||||
'You are a specialized system assistant for testing'
|
||||
|
||||
// Set up mock to return specific content for the system prompt file
|
||||
mockFileContent({
|
||||
[systemPromptFile]: systemPromptContent
|
||||
})
|
||||
|
||||
// Set up input mocks
|
||||
mockInputs({
|
||||
prompt: 'Hello, AI!',
|
||||
'system-prompt-file': systemPromptFile
|
||||
})
|
||||
|
||||
await run()
|
||||
|
||||
expect(mockExistsSync).toHaveBeenCalledWith(systemPromptFile)
|
||||
expect(mockReadFileSync).toHaveBeenCalledWith(systemPromptFile, 'utf-8')
|
||||
verifyStandardResponse()
|
||||
})
|
||||
|
||||
it('handles non-existent system-prompt-file with an error', async () => {
|
||||
const systemPromptFile = 'non-existent-system-prompt.txt'
|
||||
|
||||
// Mock the file not existing
|
||||
mockFileContent({}, [systemPromptFile])
|
||||
|
||||
// Set up input mocks
|
||||
mockInputs({
|
||||
prompt: 'Hello, AI!',
|
||||
'system-prompt-file': systemPromptFile
|
||||
})
|
||||
|
||||
await run()
|
||||
|
||||
// Verify that the error was correctly reported
|
||||
expect(core.setFailed).toHaveBeenCalledWith(
|
||||
`File for system-prompt-file was not found: ${systemPromptFile}`
|
||||
)
|
||||
})
|
||||
|
||||
it('prefers system-prompt-file over system-prompt when both are provided', async () => {
|
||||
const systemPromptFile = 'system-prompt.txt'
|
||||
const systemPromptFileContent =
|
||||
'You are a specialized system assistant from file'
|
||||
const systemPromptString =
|
||||
'You are a basic system assistant from input parameter'
|
||||
|
||||
// Set up mock to return specific content for the system prompt file
|
||||
mockFileContent({
|
||||
[systemPromptFile]: systemPromptFileContent
|
||||
})
|
||||
|
||||
// Set up input mocks
|
||||
mockInputs({
|
||||
prompt: 'Hello, AI!',
|
||||
'system-prompt-file': systemPromptFile,
|
||||
'system-prompt': systemPromptString
|
||||
})
|
||||
|
||||
await run()
|
||||
|
||||
expect(mockExistsSync).toHaveBeenCalledWith(systemPromptFile)
|
||||
expect(mockReadFileSync).toHaveBeenCalledWith(systemPromptFile, 'utf-8')
|
||||
|
||||
// Check that the post call was made with the system prompt from the file, not the input parameter
|
||||
expect(mockPost).toHaveBeenCalledWith({
|
||||
body: {
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: systemPromptFileContent // Should use the file content, not the string input
|
||||
},
|
||||
{ role: 'user', content: 'Hello, AI!' }
|
||||
],
|
||||
max_tokens: expect.any(Number),
|
||||
model: expect.any(String)
|
||||
}
|
||||
})
|
||||
|
||||
verifyStandardResponse()
|
||||
})
|
||||
|
||||
it('uses both prompt-file and system-prompt-file together', async () => {
|
||||
const promptFile = 'prompt.txt'
|
||||
const promptContent = 'This is a prompt from a file'
|
||||
const systemPromptFile = 'system-prompt.txt'
|
||||
const systemPromptContent =
|
||||
'You are a specialized system assistant from file'
|
||||
|
||||
// Set up mock to return specific content for both files
|
||||
mockFileContent({
|
||||
[promptFile]: promptContent,
|
||||
[systemPromptFile]: systemPromptContent
|
||||
})
|
||||
|
||||
// Set up input mocks
|
||||
mockInputs({
|
||||
'prompt-file': promptFile,
|
||||
'system-prompt-file': systemPromptFile
|
||||
})
|
||||
|
||||
await run()
|
||||
|
||||
expect(mockExistsSync).toHaveBeenCalledWith(promptFile)
|
||||
expect(mockExistsSync).toHaveBeenCalledWith(systemPromptFile)
|
||||
expect(mockReadFileSync).toHaveBeenCalledWith(promptFile, 'utf-8')
|
||||
expect(mockReadFileSync).toHaveBeenCalledWith(systemPromptFile, 'utf-8')
|
||||
|
||||
// Check that the post call was made with both the prompt and system prompt from files
|
||||
expect(mockPost).toHaveBeenCalledWith({
|
||||
body: {
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: systemPromptContent
|
||||
},
|
||||
{ role: 'user', content: promptContent }
|
||||
],
|
||||
max_tokens: expect.any(Number),
|
||||
model: expect.any(String)
|
||||
}
|
||||
})
|
||||
|
||||
verifyStandardResponse()
|
||||
})
|
||||
|
||||
it('passes custom max-tokens parameter to the model', async () => {
|
||||
const customMaxTokens = 500
|
||||
|
||||
mockInputs({
|
||||
prompt: 'Hello, AI!',
|
||||
'system-prompt': 'You are a test assistant.',
|
||||
'max-tokens': customMaxTokens.toString()
|
||||
})
|
||||
|
||||
await run()
|
||||
|
||||
// Check that the post call was made with the correct max_tokens parameter
|
||||
expect(mockPost).toHaveBeenCalledWith({
|
||||
body: {
|
||||
messages: expect.any(Array),
|
||||
max_tokens: customMaxTokens,
|
||||
model: expect.any(String)
|
||||
}
|
||||
})
|
||||
|
||||
verifyStandardResponse()
|
||||
})
|
||||
})
|
||||
|
||||
12
action.yml
12
action.yml
@@ -11,7 +11,11 @@ branding:
|
||||
inputs:
|
||||
prompt:
|
||||
description: The prompt for the model
|
||||
required: true
|
||||
required: false
|
||||
default: ''
|
||||
prompt-file:
|
||||
description: Path to a file containing the prompt
|
||||
required: false
|
||||
default: ''
|
||||
model:
|
||||
description: The model to use
|
||||
@@ -25,6 +29,10 @@ inputs:
|
||||
description: The system prompt for the model
|
||||
required: false
|
||||
default: 'You are a helpful assistant'
|
||||
system-prompt-file:
|
||||
description: Path to a file containing the system prompt
|
||||
required: false
|
||||
default: ''
|
||||
max-tokens:
|
||||
description: The maximum number of tokens to generate
|
||||
required: false
|
||||
@@ -38,6 +46,8 @@ inputs:
|
||||
outputs:
|
||||
response:
|
||||
description: The response from the model
|
||||
response-file:
|
||||
description: The file path where the response is saved
|
||||
|
||||
runs:
|
||||
using: node20
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="116" height="20" role="img" aria-label="Coverage: 77.27%"><title>Coverage: 77.27%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#e05d44"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">77.27%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">77.27%</text></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="116" height="20" role="img" aria-label="Coverage: 84.21%"><title>Coverage: 84.21%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#dfb317"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">84.21%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">84.21%</text></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
4328
dist/index.js
generated
vendored
4328
dist/index.js
generated
vendored
File diff suppressed because it is too large
Load Diff
2
dist/index.js.map
generated
vendored
2
dist/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
5556
package-lock.json
generated
5556
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "typescript-action",
|
||||
"description": "GitHub Actions TypeScript template",
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"author": "",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
@@ -32,7 +32,7 @@
|
||||
"local-action": "npx @github/local-action . src/main.ts .env",
|
||||
"package": "npx rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript",
|
||||
"package:watch": "npm run package -- --watch",
|
||||
"test": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest",
|
||||
"test": "npx cross-env NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest",
|
||||
"all": "npm run format:write && npm run lint && npm run test && npm run coverage && npm run package"
|
||||
},
|
||||
"license": "MIT",
|
||||
@@ -43,30 +43,30 @@
|
||||
"@azure-rest/ai-inference": "latest",
|
||||
"@azure/core-auth": "latest",
|
||||
"@azure/core-sse": "latest",
|
||||
"@eslint/compat": "^1.2.7",
|
||||
"@github/local-action": "^3.1.3",
|
||||
"@eslint/compat": "^1.2.9",
|
||||
"@github/local-action": "^3.2.1",
|
||||
"@jest/globals": "^29.7.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-commonjs": "^28.0.3",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^20.17.28",
|
||||
"@typescript-eslint/eslint-plugin": "^8.28.0",
|
||||
"@typescript-eslint/parser": "^8.28.0",
|
||||
"eslint": "^9.23.0",
|
||||
"eslint-config-prettier": "^10.0.2",
|
||||
"eslint-import-resolver-typescript": "^4.3.1",
|
||||
"@types/node": "^22.15.21",
|
||||
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
||||
"@typescript-eslint/parser": "^8.32.1",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-import-resolver-typescript": "^4.4.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jest": "^28.11.0",
|
||||
"eslint-plugin-prettier": "^5.2.5",
|
||||
"eslint-plugin-prettier": "^5.4.0",
|
||||
"jest": "^29.7.0",
|
||||
"make-coverage-badge": "^1.2.0",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-eslint": "^16.3.0",
|
||||
"rollup": "^4.38.0",
|
||||
"ts-jest": "^29.3.0",
|
||||
"prettier-eslint": "^16.4.2",
|
||||
"rollup": "^4.41.1",
|
||||
"ts-jest": "^29.3.4",
|
||||
"ts-jest-resolver": "^2.0.1",
|
||||
"typescript": "^5.8.2"
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-linux-x64-gnu": "*"
|
||||
|
||||
61
src/main.ts
61
src/main.ts
@@ -1,6 +1,40 @@
|
||||
import * as core from '@actions/core'
|
||||
import ModelClient, { isUnexpected } from '@azure-rest/ai-inference'
|
||||
import { AzureKeyCredential } from '@azure/core-auth'
|
||||
import * as fs from 'fs'
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
|
||||
const RESPONSE_FILE = 'modelResponse.txt'
|
||||
|
||||
/**
|
||||
* Helper function to load content from a file or use fallback input
|
||||
* @param filePathInput - Input name for the file path
|
||||
* @param contentInput - Input name for the direct content
|
||||
* @param defaultValue - Default value to use if neither file nor content is provided
|
||||
* @returns The loaded content
|
||||
*/
|
||||
function loadContentFromFileOrInput(
|
||||
filePathInput: string,
|
||||
contentInput: string,
|
||||
defaultValue?: string
|
||||
): string {
|
||||
const filePath = core.getInput(filePathInput)
|
||||
const contentString = core.getInput(contentInput)
|
||||
|
||||
if (filePath !== undefined && filePath !== '') {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`File for ${filePathInput} was not found: ${filePath}`)
|
||||
}
|
||||
return fs.readFileSync(filePath, 'utf-8')
|
||||
} else if (contentString !== undefined && contentString !== '') {
|
||||
return contentString
|
||||
} else if (defaultValue !== undefined) {
|
||||
return defaultValue
|
||||
} else {
|
||||
throw new Error(`Neither ${filePathInput} nor ${contentInput} was set`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The main function for the action.
|
||||
@@ -9,12 +43,16 @@ import { AzureKeyCredential } from '@azure/core-auth'
|
||||
*/
|
||||
export async function run(): Promise<void> {
|
||||
try {
|
||||
const prompt: string = core.getInput('prompt')
|
||||
if (prompt === undefined || prompt === '') {
|
||||
throw new Error('prompt is not set')
|
||||
}
|
||||
// Load prompt content - required
|
||||
const prompt = loadContentFromFileOrInput('prompt-file', 'prompt')
|
||||
|
||||
// Load system prompt with default value
|
||||
const systemPrompt = loadContentFromFileOrInput(
|
||||
'system-prompt-file',
|
||||
'system-prompt',
|
||||
'You are a helpful assistant'
|
||||
)
|
||||
|
||||
const systemPrompt: string = core.getInput('system-prompt')
|
||||
const modelName: string = core.getInput('model')
|
||||
const maxTokens: number = parseInt(core.getInput('max-tokens'), 10)
|
||||
|
||||
@@ -60,6 +98,14 @@ export async function run(): Promise<void> {
|
||||
|
||||
// Set outputs for other workflow steps to use
|
||||
core.setOutput('response', modelResponse || '')
|
||||
|
||||
// Save the response to a file in case the response overflow the output limit
|
||||
const responseFilePath = path.join(tempDir(), RESPONSE_FILE)
|
||||
core.setOutput('response-file', responseFilePath)
|
||||
|
||||
if (modelResponse && modelResponse !== '') {
|
||||
fs.writeFileSync(responseFilePath, modelResponse, 'utf-8')
|
||||
}
|
||||
} catch (error) {
|
||||
// Fail the workflow run if an error occurs
|
||||
if (error instanceof Error) {
|
||||
@@ -69,3 +115,8 @@ export async function run(): Promise<void> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function tempDir(): string {
|
||||
const tempDirectory = process.env['RUNNER_TEMP'] || os.tmpdir()
|
||||
return tempDirectory
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user