read prompt from file and print output to file

This commit is contained in:
Aiqiao Yan
2025-04-17 17:41:35 +00:00
parent c7105a4c1e
commit 43f6a3831f
8 changed files with 123 additions and 34 deletions

View File

@@ -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

View File

@@ -34,6 +34,30 @@ 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: Checkout repository
uses: actions/checkout@v4
- name: Run AI Inference with Prompt File
id: inference
uses: actions/ai-inference@v1
with:
prompt-file: './path/to/prompt.txt'
- name: Use Response File
run: |
echo "Response saved to: ${{ steps.inference.outputs.response-path }}"
cat "${{ steps.inference.outputs.response-path }}"
```
This is particularly useful for longer prompts or when you need to use the file
path where the response is stored.
## Inputs
Various inputs are defined in [`action.yml`](action.yml) to let you configure
@@ -43,6 +67,7 @@ the action:
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `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 | `""` |
| `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` |
@@ -52,9 +77,10 @@ the action:
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-path` | The file path where the response is saved (useful for larger responses) |
## Required Permissions

View File

@@ -7,7 +7,6 @@
*/
import { jest } from '@jest/globals'
import * as core from '../__fixtures__/core.js'
const mockPost = jest.fn().mockImplementation(() => ({
body: {
choices: [
@@ -58,11 +57,21 @@ describe('main.ts', () => {
'response',
'Hello, user!'
)
expect(core.setOutput).toHaveBeenNthCalledWith(
2,
'response-path',
expect.stringContaining('modelResponse.txt')
)
})
it('Sets a failed status', async () => {
// Clear the getInput mock and return an empty prompt
core.getInput.mockClear().mockReturnValueOnce('')
// Clear the getInput mock and simulate no prompt or prompt-file input
core.getInput.mockImplementation((name) => {
if (name === 'prompt') return ''
if (name === 'prompt_file') return ''
return ''
})
await run()

View File

@@ -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
@@ -38,6 +42,8 @@ inputs:
outputs:
response:
description: The response from the model
response-path:
description: The file path where the response is saved
runs:
using: node20

65
dist/index.js generated vendored
View File

@@ -1,7 +1,10 @@
import require$$0 from 'os';
import * as require$$0 from 'os';
import require$$0__default from 'os';
import require$$0$1, { randomUUID as randomUUID$1 } from 'crypto';
import require$$1 from 'fs';
import require$$1$5 from 'path';
import * as fs from 'fs';
import fs__default from 'fs';
import * as require$$1 from 'path';
import require$$1__default from 'path';
import require$$2 from 'http';
import require$$1$1 from 'https';
import require$$0$4 from 'net';
@@ -30,7 +33,7 @@ import require$$6$1 from 'timers';
import * as os from 'node:os';
import { EOL } from 'node:os';
import * as process$1 from 'node:process';
import require$$1$6 from 'tty';
import require$$1$5 from 'tty';
import * as http from 'node:http';
import * as https from 'node:https';
import * as zlib from 'node:zlib';
@@ -120,7 +123,7 @@ function requireCommand () {
};
Object.defineProperty(command, "__esModule", { value: true });
command.issue = command.issueCommand = void 0;
const os = __importStar(require$$0);
const os = __importStar(require$$0__default);
const utils_1 = requireUtils$1();
/**
* Commands
@@ -229,8 +232,8 @@ function requireFileCommand () {
// We use any as a valid input type
/* eslint-disable @typescript-eslint/no-explicit-any */
const crypto = __importStar(require$$0$1);
const fs = __importStar(require$$1);
const os = __importStar(require$$0);
const fs = __importStar(fs__default);
const os = __importStar(require$$0__default);
const utils_1 = requireUtils$1();
function issueFileCommand(command, message) {
const filePath = process.env[`GITHUB_${command}`];
@@ -25206,8 +25209,8 @@ function requireSummary () {
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0;
const os_1 = require$$0;
const fs_1 = require$$1;
const os_1 = require$$0__default;
const fs_1 = fs__default;
const { access, appendFile, writeFile } = fs_1.promises;
exports.SUMMARY_ENV_VAR = 'GITHUB_STEP_SUMMARY';
exports.SUMMARY_DOCS_URL = 'https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary';
@@ -25513,7 +25516,7 @@ function requirePathUtils () {
};
Object.defineProperty(pathUtils, "__esModule", { value: true });
pathUtils.toPlatformPath = pathUtils.toWin32Path = pathUtils.toPosixPath = void 0;
const path = __importStar(require$$1$5);
const path = __importStar(require$$1__default);
/**
* toPosixPath converts the given path to the posix form. On Windows, \\ will be
* replaced with /.
@@ -25599,8 +25602,8 @@ function requireIoUtil () {
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getCmdPath = exports.tryGetExecutablePath = exports.isRooted = exports.isDirectory = exports.exists = exports.READONLY = exports.UV_FS_O_EXLOCK = exports.IS_WINDOWS = exports.unlink = exports.symlink = exports.stat = exports.rmdir = exports.rm = exports.rename = exports.readlink = exports.readdir = exports.open = exports.mkdir = exports.lstat = exports.copyFile = exports.chmod = void 0;
const fs = __importStar(require$$1);
const path = __importStar(require$$1$5);
const fs = __importStar(fs__default);
const path = __importStar(require$$1__default);
_a = fs.promises
// export const {open} = 'fs'
, exports.chmod = _a.chmod, exports.copyFile = _a.copyFile, exports.lstat = _a.lstat, exports.mkdir = _a.mkdir, exports.open = _a.open, exports.readdir = _a.readdir, exports.readlink = _a.readlink, exports.rename = _a.rename, exports.rm = _a.rm, exports.rmdir = _a.rmdir, exports.stat = _a.stat, exports.symlink = _a.symlink, exports.unlink = _a.unlink;
@@ -25790,7 +25793,7 @@ function requireIo () {
Object.defineProperty(io, "__esModule", { value: true });
io.findInPath = io.which = io.mkdirP = io.rmRF = io.mv = io.cp = void 0;
const assert_1 = require$$0$3;
const path = __importStar(require$$1$5);
const path = __importStar(require$$1__default);
const ioUtil = __importStar(requireIoUtil());
/**
* Copies a file or folder.
@@ -26095,10 +26098,10 @@ function requireToolrunner () {
};
Object.defineProperty(toolrunner, "__esModule", { value: true });
toolrunner.argStringToArray = toolrunner.ToolRunner = void 0;
const os = __importStar(require$$0);
const os = __importStar(require$$0__default);
const events = __importStar(require$$4);
const child = __importStar(require$$2$2);
const path = __importStar(require$$1$5);
const path = __importStar(require$$1__default);
const io = __importStar(requireIo());
const ioUtil = __importStar(requireIoUtil());
const timers_1 = require$$6$1;
@@ -26838,7 +26841,7 @@ function requirePlatform () {
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getDetails = exports.isLinux = exports.isMacOS = exports.isWindows = exports.arch = exports.platform = void 0;
const os_1 = __importDefault(require$$0);
const os_1 = __importDefault(require$$0__default);
const exec = __importStar(requireExec());
const getWindowsInfo = () => __awaiter(void 0, void 0, void 0, function* () {
const { stdout: version } = yield exec.getExecOutput('powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Version"', undefined, {
@@ -26941,8 +26944,8 @@ function requireCore () {
const command_1 = requireCommand();
const file_command_1 = requireFileCommand();
const utils_1 = requireUtils$1();
const os = __importStar(require$$0);
const path = __importStar(require$$1$5);
const os = __importStar(require$$0__default);
const path = __importStar(require$$1__default);
const oidc_utils_1 = requireOidcUtils();
/**
* The code to exit an action
@@ -29841,8 +29844,8 @@ var hasRequiredSupportsColor;
function requireSupportsColor () {
if (hasRequiredSupportsColor) return supportsColor_1;
hasRequiredSupportsColor = 1;
const os = require$$0;
const tty = require$$1$6;
const os = require$$0__default;
const tty = require$$1$5;
const hasFlag = requireHasFlag();
const {env} = process;
@@ -29988,7 +29991,7 @@ function requireNode () {
if (hasRequiredNode) return node.exports;
hasRequiredNode = 1;
(function (module, exports) {
const tty = require$$1$6;
const tty = require$$1$5;
const util = require$$0$2;
/**
@@ -33548,6 +33551,7 @@ function getPathFromMapKey(mapKey) {
return mapKey.slice(pathStart);
}
const RESPONSE_FILE = 'modelResponse.txt';
/**
* The main function for the action.
*
@@ -33555,7 +33559,14 @@ function getPathFromMapKey(mapKey) {
*/
async function run() {
try {
const prompt = coreExports.getInput('prompt');
const promptFile = coreExports.getInput('prompt-file');
let prompt = coreExports.getInput('prompt');
if (promptFile !== undefined && promptFile !== '') {
if (!fs.existsSync(promptFile)) {
throw new Error(`Prompt file not found: ${promptFile}`);
}
prompt = fs.readFileSync(promptFile, 'utf-8');
}
if (prompt === undefined || prompt === '') {
throw new Error('prompt is not set');
}
@@ -33595,6 +33606,12 @@ async function run() {
const modelResponse = response.body.choices[0].message.content;
// Set outputs for other workflow steps to use
coreExports.setOutput('response', modelResponse || '');
// Save the response to a file in case the response overflow the output limit
const responseFilePath = require$$1.join(tempDir(), RESPONSE_FILE);
coreExports.setOutput('response-path', responseFilePath);
if (modelResponse && modelResponse !== '') {
fs.writeFileSync(responseFilePath, modelResponse, 'utf-8');
}
}
catch (error) {
// Fail the workflow run if an error occurs
@@ -33606,6 +33623,10 @@ async function run() {
}
}
}
function tempDir() {
const tempDirectory = process.env['RUNNER_TEMP'] || require$$0.tmpdir();
return tempDirectory;
}
/**
* The entrypoint for the action. This file simply imports and runs the action's

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "typescript-action",
"version": "0.0.0",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "typescript-action",
"version": "0.0.0",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.11.1"

View File

@@ -1,6 +1,11 @@
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'
/**
* The main function for the action.
@@ -9,7 +14,16 @@ import { AzureKeyCredential } from '@azure/core-auth'
*/
export async function run(): Promise<void> {
try {
const prompt: string = core.getInput('prompt')
const promptFile: string = core.getInput('prompt-file')
let prompt: string = core.getInput('prompt')
if (promptFile !== undefined && promptFile !== '') {
if (!fs.existsSync(promptFile)) {
throw new Error(`Prompt file not found: ${promptFile}`)
}
prompt = fs.readFileSync(promptFile, 'utf-8')
}
if (prompt === undefined || prompt === '') {
throw new Error('prompt is not set')
}
@@ -60,6 +74,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-path', 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 +91,8 @@ export async function run(): Promise<void> {
}
}
}
function tempDir(): string {
const tempDirectory = process.env['RUNNER_TEMP'] || os.tmpdir()
return tempDirectory
}