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