Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9693b137b6 | ||
|
|
d0b2f23c43 | ||
|
|
0df96479bc | ||
|
|
446f075e3b | ||
|
|
ce58b26ac7 | ||
|
|
1cf96b0212 | ||
|
|
f79e4e11cb | ||
|
|
72102e50bf | ||
|
|
2bc30a525a | ||
|
|
8f64ac1284 | ||
|
|
1f89e942aa | ||
|
|
77a7cbe11b | ||
|
|
fcc8550115 | ||
|
|
29b5f08d0f | ||
|
|
6f7bd88d1d | ||
|
|
8ae5306787 | ||
|
|
9445295106 | ||
|
|
e385879671 | ||
|
|
1780121e3b | ||
|
|
b002da2928 | ||
|
|
ba509f9275 |
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@12150456a73e248bdc94d0794898f94e23127c88
|
||||
uses: super-linter/super-linter/slim@5119dcd8011e92182ce8219d9e9efc82f16fddb6
|
||||
env:
|
||||
DEFAULT_BRANCH: main
|
||||
FILTER_REGEX_EXCLUDE: dist/**/*
|
||||
|
||||
22
.github/workflows/release-new-action-version.yml
vendored
Normal file
22
.github/workflows/release-new-action-version.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: Release new action version
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
|
||||
env:
|
||||
TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update_tag:
|
||||
name:
|
||||
Update the major tag to include the ${{ github.event.release.tag_name }}
|
||||
changes
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Update the ${{ env.TAG_NAME }} tag
|
||||
uses: actions/publish-action@v0.2.2
|
||||
with:
|
||||
source-tag: ${{ env.TAG_NAME }}
|
||||
32
.licenses/npm/@types/js-yaml.dep.yml
Normal file
32
.licenses/npm/@types/js-yaml.dep.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: "@types/js-yaml"
|
||||
version: 4.0.9
|
||||
type: npm
|
||||
summary: TypeScript definitions for js-yaml
|
||||
homepage: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/js-yaml
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |2
|
||||
MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE
|
||||
notices: []
|
||||
265
.licenses/npm/argparse.dep.yml
Normal file
265
.licenses/npm/argparse.dep.yml
Normal file
@@ -0,0 +1,265 @@
|
||||
---
|
||||
name: argparse
|
||||
version: 2.0.1
|
||||
type: npm
|
||||
summary: CLI arguments parser. Native port of python's argparse.
|
||||
homepage:
|
||||
license: other
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
A. HISTORY OF THE SOFTWARE
|
||||
==========================
|
||||
|
||||
Python was created in the early 1990s by Guido van Rossum at Stichting
|
||||
Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands
|
||||
as a successor of a language called ABC. Guido remains Python's
|
||||
principal author, although it includes many contributions from others.
|
||||
|
||||
In 1995, Guido continued his work on Python at the Corporation for
|
||||
National Research Initiatives (CNRI, see http://www.cnri.reston.va.us)
|
||||
in Reston, Virginia where he released several versions of the
|
||||
software.
|
||||
|
||||
In May 2000, Guido and the Python core development team moved to
|
||||
BeOpen.com to form the BeOpen PythonLabs team. In October of the same
|
||||
year, the PythonLabs team moved to Digital Creations, which became
|
||||
Zope Corporation. In 2001, the Python Software Foundation (PSF, see
|
||||
https://www.python.org/psf/) was formed, a non-profit organization
|
||||
created specifically to own Python-related Intellectual Property.
|
||||
Zope Corporation was a sponsoring member of the PSF.
|
||||
|
||||
All Python releases are Open Source (see http://www.opensource.org for
|
||||
the Open Source Definition). Historically, most, but not all, Python
|
||||
releases have also been GPL-compatible; the table below summarizes
|
||||
the various releases.
|
||||
|
||||
Release Derived Year Owner GPL-
|
||||
from compatible? (1)
|
||||
|
||||
0.9.0 thru 1.2 1991-1995 CWI yes
|
||||
1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
|
||||
1.6 1.5.2 2000 CNRI no
|
||||
2.0 1.6 2000 BeOpen.com no
|
||||
1.6.1 1.6 2001 CNRI yes (2)
|
||||
2.1 2.0+1.6.1 2001 PSF no
|
||||
2.0.1 2.0+1.6.1 2001 PSF yes
|
||||
2.1.1 2.1+2.0.1 2001 PSF yes
|
||||
2.1.2 2.1.1 2002 PSF yes
|
||||
2.1.3 2.1.2 2002 PSF yes
|
||||
2.2 and above 2.1.1 2001-now PSF yes
|
||||
|
||||
Footnotes:
|
||||
|
||||
(1) GPL-compatible doesn't mean that we're distributing Python under
|
||||
the GPL. All Python licenses, unlike the GPL, let you distribute
|
||||
a modified version without making your changes open source. The
|
||||
GPL-compatible licenses make it possible to combine Python with
|
||||
other software that is released under the GPL; the others don't.
|
||||
|
||||
(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
|
||||
because its license has a choice of law clause. According to
|
||||
CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
|
||||
is "not incompatible" with the GPL.
|
||||
|
||||
Thanks to the many outside volunteers who have worked under Guido's
|
||||
direction to make these releases possible.
|
||||
|
||||
|
||||
B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
|
||||
===============================================================
|
||||
|
||||
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
||||
--------------------------------------------
|
||||
|
||||
1. This LICENSE AGREEMENT is between the Python Software Foundation
|
||||
("PSF"), and the Individual or Organization ("Licensee") accessing and
|
||||
otherwise using this software ("Python") in source or binary form and
|
||||
its associated documentation.
|
||||
|
||||
2. Subject to the terms and conditions of this License Agreement, PSF hereby
|
||||
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
|
||||
analyze, test, perform and/or display publicly, prepare derivative works,
|
||||
distribute, and otherwise use Python alone or in any derivative version,
|
||||
provided, however, that PSF's License Agreement and PSF's notice of copyright,
|
||||
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
|
||||
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation;
|
||||
All Rights Reserved" are retained in Python alone or in any derivative version
|
||||
prepared by Licensee.
|
||||
|
||||
3. In the event Licensee prepares a derivative work that is based on
|
||||
or incorporates Python or any part thereof, and wants to make
|
||||
the derivative work available to others as provided herein, then
|
||||
Licensee hereby agrees to include in any such work a brief summary of
|
||||
the changes made to Python.
|
||||
|
||||
4. PSF is making Python available to Licensee on an "AS IS"
|
||||
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
|
||||
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
6. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
7. Nothing in this License Agreement shall be deemed to create any
|
||||
relationship of agency, partnership, or joint venture between PSF and
|
||||
Licensee. This License Agreement does not grant permission to use PSF
|
||||
trademarks or trade name in a trademark sense to endorse or promote
|
||||
products or services of Licensee, or any third party.
|
||||
|
||||
8. By copying, installing or otherwise using Python, Licensee
|
||||
agrees to be bound by the terms and conditions of this License
|
||||
Agreement.
|
||||
|
||||
|
||||
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
|
||||
-------------------------------------------
|
||||
|
||||
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
|
||||
|
||||
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
|
||||
office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
|
||||
Individual or Organization ("Licensee") accessing and otherwise using
|
||||
this software in source or binary form and its associated
|
||||
documentation ("the Software").
|
||||
|
||||
2. Subject to the terms and conditions of this BeOpen Python License
|
||||
Agreement, BeOpen hereby grants Licensee a non-exclusive,
|
||||
royalty-free, world-wide license to reproduce, analyze, test, perform
|
||||
and/or display publicly, prepare derivative works, distribute, and
|
||||
otherwise use the Software alone or in any derivative version,
|
||||
provided, however, that the BeOpen Python License is retained in the
|
||||
Software, alone or in any derivative version prepared by Licensee.
|
||||
|
||||
3. BeOpen is making the Software available to Licensee on an "AS IS"
|
||||
basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
|
||||
SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
|
||||
AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
|
||||
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
5. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
6. This License Agreement shall be governed by and interpreted in all
|
||||
respects by the law of the State of California, excluding conflict of
|
||||
law provisions. Nothing in this License Agreement shall be deemed to
|
||||
create any relationship of agency, partnership, or joint venture
|
||||
between BeOpen and Licensee. This License Agreement does not grant
|
||||
permission to use BeOpen trademarks or trade names in a trademark
|
||||
sense to endorse or promote products or services of Licensee, or any
|
||||
third party. As an exception, the "BeOpen Python" logos available at
|
||||
http://www.pythonlabs.com/logos.html may be used according to the
|
||||
permissions granted on that web page.
|
||||
|
||||
7. By copying, installing or otherwise using the software, Licensee
|
||||
agrees to be bound by the terms and conditions of this License
|
||||
Agreement.
|
||||
|
||||
|
||||
CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
|
||||
---------------------------------------
|
||||
|
||||
1. This LICENSE AGREEMENT is between the Corporation for National
|
||||
Research Initiatives, having an office at 1895 Preston White Drive,
|
||||
Reston, VA 20191 ("CNRI"), and the Individual or Organization
|
||||
("Licensee") accessing and otherwise using Python 1.6.1 software in
|
||||
source or binary form and its associated documentation.
|
||||
|
||||
2. Subject to the terms and conditions of this License Agreement, CNRI
|
||||
hereby grants Licensee a nonexclusive, royalty-free, world-wide
|
||||
license to reproduce, analyze, test, perform and/or display publicly,
|
||||
prepare derivative works, distribute, and otherwise use Python 1.6.1
|
||||
alone or in any derivative version, provided, however, that CNRI's
|
||||
License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
|
||||
1995-2001 Corporation for National Research Initiatives; All Rights
|
||||
Reserved" are retained in Python 1.6.1 alone or in any derivative
|
||||
version prepared by Licensee. Alternately, in lieu of CNRI's License
|
||||
Agreement, Licensee may substitute the following text (omitting the
|
||||
quotes): "Python 1.6.1 is made available subject to the terms and
|
||||
conditions in CNRI's License Agreement. This Agreement together with
|
||||
Python 1.6.1 may be located on the Internet using the following
|
||||
unique, persistent identifier (known as a handle): 1895.22/1013. This
|
||||
Agreement may also be obtained from a proxy server on the Internet
|
||||
using the following URL: http://hdl.handle.net/1895.22/1013".
|
||||
|
||||
3. In the event Licensee prepares a derivative work that is based on
|
||||
or incorporates Python 1.6.1 or any part thereof, and wants to make
|
||||
the derivative work available to others as provided herein, then
|
||||
Licensee hereby agrees to include in any such work a brief summary of
|
||||
the changes made to Python 1.6.1.
|
||||
|
||||
4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
|
||||
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||
1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
|
||||
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
6. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
7. This License Agreement shall be governed by the federal
|
||||
intellectual property law of the United States, including without
|
||||
limitation the federal copyright law, and, to the extent such
|
||||
U.S. federal law does not apply, by the law of the Commonwealth of
|
||||
Virginia, excluding Virginia's conflict of law provisions.
|
||||
Notwithstanding the foregoing, with regard to derivative works based
|
||||
on Python 1.6.1 that incorporate non-separable material that was
|
||||
previously distributed under the GNU General Public License (GPL), the
|
||||
law of the Commonwealth of Virginia shall govern this License
|
||||
Agreement only as to issues arising under or with respect to
|
||||
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
|
||||
License Agreement shall be deemed to create any relationship of
|
||||
agency, partnership, or joint venture between CNRI and Licensee. This
|
||||
License Agreement does not grant permission to use CNRI trademarks or
|
||||
trade name in a trademark sense to endorse or promote products or
|
||||
services of Licensee, or any third party.
|
||||
|
||||
8. By clicking on the "ACCEPT" button where indicated, or by copying,
|
||||
installing or otherwise using Python 1.6.1, Licensee agrees to be
|
||||
bound by the terms and conditions of this License Agreement.
|
||||
|
||||
ACCEPT
|
||||
|
||||
|
||||
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
|
||||
--------------------------------------------------
|
||||
|
||||
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
|
||||
The Netherlands. All rights reserved.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software and its
|
||||
documentation for any purpose and without fee is hereby granted,
|
||||
provided that the above copyright notice appear in all copies and that
|
||||
both that copyright notice and this permission notice appear in
|
||||
supporting documentation, and that the name of Stichting Mathematisch
|
||||
Centrum or CWI not be used in advertising or publicity pertaining to
|
||||
distribution of the software without specific, written prior
|
||||
permission.
|
||||
|
||||
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
|
||||
FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
notices: []
|
||||
32
.licenses/npm/js-yaml.dep.yml
Normal file
32
.licenses/npm/js-yaml.dep.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: js-yaml
|
||||
version: 4.1.0
|
||||
type: npm
|
||||
summary: YAML 1.2 parser and serializer
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
(The MIT License)
|
||||
|
||||
Copyright (C) 2011-2015 by Vitaly Puzrin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
notices: []
|
||||
107
README.md
107
README.md
@@ -36,17 +36,90 @@ jobs:
|
||||
|
||||
### 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
|
||||
steps:
|
||||
- name: Run AI Inference with Prompt File
|
||||
- name: Run AI Inference with Text File
|
||||
id: inference
|
||||
uses: actions/ai-inference@v1
|
||||
with:
|
||||
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
|
||||
|
||||
In addition to the regular prompt, you can provide a system prompt file instead
|
||||
@@ -90,34 +163,36 @@ repository management, issue tracking, and pull request operations.
|
||||
steps:
|
||||
- name: AI Inference with GitHub Tools
|
||||
id: inference
|
||||
uses: actions/ai-inference@v1
|
||||
uses: actions/ai-inference@v1.2
|
||||
with:
|
||||
prompt: 'List my open pull requests and create a summary'
|
||||
enable-github-mcp: true
|
||||
token: ${{ secrets.USER_PAT }}
|
||||
```
|
||||
|
||||
When MCP is enabled, the AI model will have access to GitHub tools and can
|
||||
perform actions like searching issues and PRs.
|
||||
|
||||
**Note:** MCP integration requires your workflow token to have appropriate
|
||||
GitHub permissions for the operations the AI will perform.
|
||||
**Note:** For now, MCP integration cannot be used with the built-in token. You
|
||||
must pass a GitHub PAT into `token:` instead.
|
||||
|
||||
## 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 |
|
||||
| `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 | `openai/gpt-4.1` |
|
||||
| `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 |
|
||||
| `enable-github-mcp` | Enable Model Context Protocol integration with GitHub tools | `false` |
|
||||
| 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 (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-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` |
|
||||
| `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 |
|
||||
| `enable-github-mcp` | Enable Model Context Protocol integration with GitHub tools | `false` |
|
||||
|
||||
## Outputs
|
||||
|
||||
|
||||
41
__fixtures__/prompts/json-schema.prompt.yml
Normal file
41
__fixtures__/prompts/json-schema.prompt.yml
Normal file
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
6
__fixtures__/prompts/simple.prompt.yml
Normal file
6
__fixtures__/prompts/simple.prompt.yml
Normal file
@@ -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
|
||||
163
__tests__/helpers-inference.test.ts
Normal file
163
__tests__/helpers-inference.test.ts
Normal file
@@ -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', () => {
|
||||
const mockRequest = {
|
||||
systemPrompt: 'You are a test assistant',
|
||||
prompt: 'Hello, AI!',
|
||||
messages: [
|
||||
{ role: 'system', content: 'You are a test assistant' },
|
||||
{ role: 'user', content: 'Hello, AI!' }
|
||||
],
|
||||
modelName: 'gpt-4',
|
||||
maxTokens: 100,
|
||||
endpoint: 'https://api.test.com',
|
||||
|
||||
182
__tests__/main-prompt-integration.test.ts
Normal file
182
__tests__/main-prompt-integration.test.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals'
|
||||
import * as core from '../__fixtures__/core.js'
|
||||
|
||||
// Create fs mocks
|
||||
const mockExistsSync = jest.fn()
|
||||
const mockReadFileSync = jest.fn()
|
||||
const mockWriteFileSync = jest.fn()
|
||||
|
||||
// Create inference mocks
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const mockSimpleInference = jest.fn() as jest.MockedFunction<any>
|
||||
const mockMcpInference = jest.fn()
|
||||
|
||||
// Create MCP mocks
|
||||
const mockConnectToGitHubMCP = jest.fn()
|
||||
|
||||
// Mock fs module
|
||||
jest.unstable_mockModule('fs', () => ({
|
||||
existsSync: mockExistsSync,
|
||||
readFileSync: mockReadFileSync,
|
||||
writeFileSync: mockWriteFileSync
|
||||
}))
|
||||
|
||||
// Mock the inference functions
|
||||
jest.unstable_mockModule('../src/inference.js', () => ({
|
||||
simpleInference: mockSimpleInference,
|
||||
mcpInference: mockMcpInference
|
||||
}))
|
||||
|
||||
// Mock the MCP connection
|
||||
jest.unstable_mockModule('../src/mcp.js', () => ({
|
||||
connectToGitHubMCP: mockConnectToGitHubMCP
|
||||
}))
|
||||
|
||||
jest.unstable_mockModule('@actions/core', () => core)
|
||||
|
||||
// The module being tested should be imported dynamically. This ensures that the
|
||||
// mocks are used in place of any actual dependencies.
|
||||
const { run } = await import('../src/main.js')
|
||||
|
||||
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
|
||||
core.getInput.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.readFileSync for prompt file
|
||||
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
|
||||
mockWriteFileSync.mockImplementation(() => {})
|
||||
|
||||
// Mock simpleInference
|
||||
mockSimpleInference.mockResolvedValue('Mocked AI response')
|
||||
})
|
||||
|
||||
it('should handle prompt YAML files with template variables', async () => {
|
||||
mockExistsSync.mockReturnValue(true)
|
||||
core.getInput.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
|
||||
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 () => {
|
||||
mockExistsSync.mockReturnValue(false)
|
||||
core.getInput.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 converted message format
|
||||
expect(mockSimpleInference).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'You are helpful'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello, world!'
|
||||
}
|
||||
],
|
||||
modelName: 'openai/gpt-4o',
|
||||
maxTokens: 200,
|
||||
endpoint: 'https://models.github.ai/inference',
|
||||
token: 'test-token'
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -161,12 +161,15 @@ describe('main.ts', () => {
|
||||
await run()
|
||||
|
||||
expect(mockSimpleInference).toHaveBeenCalledWith({
|
||||
systemPrompt: 'You are a test assistant.',
|
||||
prompt: 'Hello, AI!',
|
||||
messages: [
|
||||
{ role: 'system', content: 'You are a test assistant.' },
|
||||
{ role: 'user', content: 'Hello, AI!' }
|
||||
],
|
||||
modelName: 'gpt-4',
|
||||
maxTokens: 100,
|
||||
endpoint: 'https://api.test.com',
|
||||
token: 'fake-token'
|
||||
token: 'fake-token',
|
||||
responseFormat: undefined
|
||||
})
|
||||
expect(mockConnectToGitHubMCP).not.toHaveBeenCalled()
|
||||
expect(mockMcpInference).not.toHaveBeenCalled()
|
||||
@@ -193,8 +196,10 @@ describe('main.ts', () => {
|
||||
expect(mockConnectToGitHubMCP).toHaveBeenCalledWith('fake-token')
|
||||
expect(mockMcpInference).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
systemPrompt: 'You are a test assistant.',
|
||||
prompt: 'Hello, AI!',
|
||||
messages: [
|
||||
{ role: 'system', content: 'You are a test assistant.' },
|
||||
{ role: 'user', content: 'Hello, AI!' }
|
||||
],
|
||||
token: 'fake-token'
|
||||
}),
|
||||
mockMcpClient
|
||||
@@ -243,12 +248,15 @@ describe('main.ts', () => {
|
||||
await run()
|
||||
|
||||
expect(mockSimpleInference).toHaveBeenCalledWith({
|
||||
systemPrompt: systemPromptContent,
|
||||
prompt: promptContent,
|
||||
messages: [
|
||||
{ role: 'system', content: systemPromptContent },
|
||||
{ role: 'user', content: promptContent }
|
||||
],
|
||||
modelName: 'gpt-4',
|
||||
maxTokens: 100,
|
||||
endpoint: 'https://api.test.com',
|
||||
token: 'fake-token'
|
||||
token: 'fake-token',
|
||||
responseFormat: undefined
|
||||
})
|
||||
verifyStandardResponse()
|
||||
})
|
||||
|
||||
133
__tests__/prompt.test.ts
Normal file
133
__tests__/prompt.test.ts
Normal file
@@ -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'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -14,7 +14,13 @@ inputs:
|
||||
required: false
|
||||
default: ''
|
||||
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
|
||||
default: ''
|
||||
model:
|
||||
|
||||
3028
dist/index.js
generated
vendored
3028
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
10
package-lock.json
generated
10
package-lock.json
generated
@@ -12,6 +12,8 @@
|
||||
"@actions/core": "^1.11.1",
|
||||
"@modelcontextprotocol/sdk": "^1.15.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"js-yaml": "^4.1.0",
|
||||
"pkce-challenge": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -4407,6 +4409,12 @@
|
||||
"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": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
@@ -5173,7 +5181,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/array-buffer-byte-length": {
|
||||
@@ -10254,7 +10261,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
"@actions/core": "^1.11.1",
|
||||
"@modelcontextprotocol/sdk": "^1.15.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"js-yaml": "^4.1.0",
|
||||
"pkce-challenge": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as core from '@actions/core'
|
||||
import { GetChatCompletionsDefaultResponse } from '@azure-rest/ai-inference'
|
||||
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
|
||||
@@ -64,3 +66,79 @@ export function handleUnexpectedResponse(
|
||||
: 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
|
||||
): { type: 'json_schema'; json_schema: unknown } | undefined {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
import * as core from '@actions/core'
|
||||
import ModelClient, { isUnexpected } from '@azure-rest/ai-inference'
|
||||
import { AzureKeyCredential } from '@azure/core-auth'
|
||||
import { GitHubMCPClient, executeToolCalls } from './mcp.js'
|
||||
import { GitHubMCPClient, executeToolCalls, MCPTool, ToolCall } from './mcp.js'
|
||||
import { handleUnexpectedResponse } from './helpers.js'
|
||||
|
||||
interface ChatMessage {
|
||||
role: string
|
||||
content: string | null
|
||||
tool_calls?: ToolCall[]
|
||||
}
|
||||
|
||||
interface ChatCompletionsRequestBody {
|
||||
messages: ChatMessage[]
|
||||
max_tokens: number
|
||||
model: string
|
||||
response_format?: { type: 'json_schema'; json_schema: unknown }
|
||||
tools?: MCPTool[]
|
||||
}
|
||||
|
||||
export interface InferenceRequest {
|
||||
systemPrompt: string
|
||||
prompt: string
|
||||
messages: Array<{ role: string; content: string }>
|
||||
modelName: string
|
||||
maxTokens: number
|
||||
endpoint: string
|
||||
token: string
|
||||
responseFormat?: { type: 'json_schema'; json_schema: unknown } // Processed response format for the API
|
||||
}
|
||||
|
||||
export interface InferenceResponse {
|
||||
@@ -41,18 +55,17 @@ export async function simpleInference(
|
||||
}
|
||||
)
|
||||
|
||||
const requestBody = {
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: request.systemPrompt
|
||||
},
|
||||
{ role: 'user', content: request.prompt }
|
||||
],
|
||||
const requestBody: ChatCompletionsRequestBody = {
|
||||
messages: request.messages,
|
||||
max_tokens: request.maxTokens,
|
||||
model: request.modelName
|
||||
}
|
||||
|
||||
// Add response format if specified
|
||||
if (request.responseFormat) {
|
||||
requestBody.response_format = request.responseFormat
|
||||
}
|
||||
|
||||
const response = await client.path('/chat/completions').post({
|
||||
body: requestBody
|
||||
})
|
||||
@@ -84,14 +97,8 @@ export async function mcpInference(
|
||||
}
|
||||
)
|
||||
|
||||
// Start with the initial conversation
|
||||
const messages = [
|
||||
{
|
||||
role: 'system',
|
||||
content: request.systemPrompt
|
||||
},
|
||||
{ role: 'user', content: request.prompt }
|
||||
]
|
||||
// Start with the pre-processed messages
|
||||
const messages: ChatMessage[] = [...request.messages]
|
||||
|
||||
let iterationCount = 0
|
||||
const maxIterations = 5 // Prevent infinite loops
|
||||
@@ -100,13 +107,18 @@ export async function mcpInference(
|
||||
iterationCount++
|
||||
core.info(`MCP inference iteration ${iterationCount}`)
|
||||
|
||||
const requestBody = {
|
||||
const requestBody: ChatCompletionsRequestBody = {
|
||||
messages: messages,
|
||||
max_tokens: request.maxTokens,
|
||||
model: request.modelName,
|
||||
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({
|
||||
body: requestBody
|
||||
})
|
||||
|
||||
58
src/main.ts
58
src/main.ts
@@ -3,8 +3,14 @@ import * as fs from 'fs'
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
import { connectToGitHubMCP } from './mcp.js'
|
||||
import { simpleInference, mcpInference, InferenceRequest } from './inference.js'
|
||||
import { loadContentFromFileOrInput } from './helpers.js'
|
||||
import { simpleInference, mcpInference } from './inference.js'
|
||||
import { loadContentFromFileOrInput, buildInferenceRequest } from './helpers.js'
|
||||
import {
|
||||
loadPromptFile,
|
||||
parseTemplateVariables,
|
||||
isPromptYamlFile,
|
||||
PromptConfig
|
||||
} from './prompt.js'
|
||||
|
||||
const RESPONSE_FILE = 'modelResponse.txt'
|
||||
|
||||
@@ -15,16 +21,37 @@ const RESPONSE_FILE = 'modelResponse.txt'
|
||||
*/
|
||||
export async function run(): Promise<void> {
|
||||
try {
|
||||
const prompt = loadContentFromFileOrInput('prompt-file', 'prompt')
|
||||
const promptFilePath = core.getInput('prompt-file')
|
||||
const inputVariables = core.getInput('input')
|
||||
|
||||
const systemPrompt = loadContentFromFileOrInput(
|
||||
'system-prompt-file',
|
||||
'system-prompt',
|
||||
'You are a helpful assistant'
|
||||
)
|
||||
let promptConfig: PromptConfig | undefined = undefined
|
||||
let systemPrompt: string | undefined = undefined
|
||||
let prompt: string | undefined = undefined
|
||||
|
||||
const modelName: string = core.getInput('model')
|
||||
const maxTokens: number = parseInt(core.getInput('max-tokens'), 10)
|
||||
// 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',
|
||||
'You are a helpful assistant'
|
||||
)
|
||||
}
|
||||
|
||||
// Get common parameters
|
||||
const modelName = promptConfig?.model || core.getInput('model')
|
||||
const maxTokens = parseInt(core.getInput('max-tokens'), 10)
|
||||
|
||||
const token = process.env['GITHUB_TOKEN'] || core.getInput('token')
|
||||
if (token === undefined) {
|
||||
@@ -32,21 +59,24 @@ export async function run(): Promise<void> {
|
||||
}
|
||||
|
||||
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,
|
||||
prompt,
|
||||
modelName,
|
||||
maxTokens,
|
||||
endpoint,
|
||||
token
|
||||
}
|
||||
)
|
||||
|
||||
const enableMcp = core.getBooleanInput('enable-github-mcp') || false
|
||||
|
||||
let modelResponse: string | null = null
|
||||
|
||||
if (enableMcp) {
|
||||
const mcpClient = await connectToGitHubMCP(token)
|
||||
const mcpClient = await connectToGitHubMCP(inferenceRequest.token)
|
||||
|
||||
if (mcpClient) {
|
||||
modelResponse = await mcpInference(inferenceRequest, mcpClient)
|
||||
|
||||
111
src/prompt.ts
Normal file
111
src/prompt.ts
Normal file
@@ -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