Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5022b33bc1 | ||
|
|
c9e14713bc | ||
|
|
39308142df | ||
|
|
48f0edec4d | ||
|
|
36ea1371dc | ||
|
|
de16a30c20 | ||
|
|
dd3dff10ba | ||
|
|
4bb01ee5ee | ||
|
|
af1c1c29a3 | ||
|
|
83bb5ca3e8 | ||
|
|
4d2337d006 | ||
|
|
7ba7530ad4 | ||
|
|
4d7d83c494 | ||
|
|
a1c1182922 | ||
|
|
dfaa426c29 | ||
|
|
7fa0024f13 | ||
|
|
fc6f9a0800 | ||
|
|
a1d07305b7 | ||
|
|
6e0d8949d8 |
2
.github/workflows/check-dist.yml
vendored
2
.github/workflows/check-dist.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
id: checkout
|
id: checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
id: setup-node
|
id: setup-node
|
||||||
|
|||||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
id: checkout
|
id: checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
id: setup-node
|
id: setup-node
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
id: checkout
|
id: checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Test Local Action
|
- name: Test Local Action
|
||||||
id: test-action
|
id: test-action
|
||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
id: checkout
|
id: checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Create Prompt File
|
- name: Create Prompt File
|
||||||
run: echo "hello" > prompt.txt
|
run: echo "hello" > prompt.txt
|
||||||
|
|||||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
id: checkout
|
id: checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
id: initialize
|
id: initialize
|
||||||
|
|||||||
2
.github/workflows/licensed.yml
vendored
2
.github/workflows/licensed.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
id: checkout
|
id: checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
id: setup-node
|
id: setup-node
|
||||||
|
|||||||
2
.github/workflows/linter.yml
vendored
2
.github/workflows/linter.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
id: checkout
|
id: checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
@@ -162,6 +162,9 @@ This action now supports **read-only** integration with the GitHub-hosted Model
|
|||||||
Context Protocol (MCP) server, which provides access to GitHub tools like
|
Context Protocol (MCP) server, which provides access to GitHub tools like
|
||||||
repository management, issue tracking, and pull request operations.
|
repository management, issue tracking, and pull request operations.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> The GitHub MCP integration requires a Personal Access Token (PAT) and cannot use the built-in `GITHUB_TOKEN`.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
steps:
|
steps:
|
||||||
- name: AI Inference with GitHub Tools
|
- name: AI Inference with GitHub Tools
|
||||||
@@ -209,7 +212,7 @@ the action:
|
|||||||
| `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` |
|
||||||
| `github-mcp-token` | Token to use for GitHub MCP server (defaults to the main token if not specified). Use a separate PAT for tighter security | `""` |
|
| `github-mcp-token` | Token to use for GitHub MCP server (defaults to the main token if not specified). This must be a PAT in order for MCP to work | `""` |
|
||||||
|
|
||||||
## Outputs
|
## Outputs
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ describe('helpers.ts - inference request building', () => {
|
|||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
'gpt-4',
|
'gpt-4',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
100,
|
100,
|
||||||
'https://api.test.com',
|
'https://api.test.com',
|
||||||
'test-token',
|
'test-token',
|
||||||
@@ -117,6 +119,8 @@ describe('helpers.ts - inference request building', () => {
|
|||||||
{role: 'user', content: 'User message'},
|
{role: 'user', content: 'User message'},
|
||||||
],
|
],
|
||||||
modelName: 'gpt-4',
|
modelName: 'gpt-4',
|
||||||
|
temperature: undefined,
|
||||||
|
topP: undefined,
|
||||||
maxTokens: 100,
|
maxTokens: 100,
|
||||||
endpoint: 'https://api.test.com',
|
endpoint: 'https://api.test.com',
|
||||||
token: 'test-token',
|
token: 'test-token',
|
||||||
@@ -136,6 +140,8 @@ describe('helpers.ts - inference request building', () => {
|
|||||||
'System prompt',
|
'System prompt',
|
||||||
'User prompt',
|
'User prompt',
|
||||||
'gpt-4',
|
'gpt-4',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
100,
|
100,
|
||||||
'https://api.test.com',
|
'https://api.test.com',
|
||||||
'test-token',
|
'test-token',
|
||||||
@@ -147,6 +153,8 @@ describe('helpers.ts - inference request building', () => {
|
|||||||
{role: 'user', content: 'User prompt'},
|
{role: 'user', content: 'User prompt'},
|
||||||
],
|
],
|
||||||
modelName: 'gpt-4',
|
modelName: 'gpt-4',
|
||||||
|
temperature: undefined,
|
||||||
|
topP: undefined,
|
||||||
maxTokens: 100,
|
maxTokens: 100,
|
||||||
endpoint: 'https://api.test.com',
|
endpoint: 'https://api.test.com',
|
||||||
token: 'test-token',
|
token: 'test-token',
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ inputs:
|
|||||||
required: false
|
required: false
|
||||||
default: 'false'
|
default: 'false'
|
||||||
github-mcp-token:
|
github-mcp-token:
|
||||||
description: The token to use for GitHub MCP server (defaults to GITHUB_TOKEN if not specified)
|
description: The token to use for GitHub MCP server (defaults to the main token if not specified). This must be a PAT for MCP to work.
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
|
|
||||||
|
|||||||
89
dist/index.js
generated
vendored
89
dist/index.js
generated
vendored
@@ -49496,28 +49496,18 @@ async function simpleInference(request) {
|
|||||||
messages: request.messages,
|
messages: request.messages,
|
||||||
max_tokens: request.maxTokens,
|
max_tokens: request.maxTokens,
|
||||||
model: request.modelName,
|
model: request.modelName,
|
||||||
|
temperature: request.temperature,
|
||||||
|
top_p: request.topP,
|
||||||
};
|
};
|
||||||
// Add response format if specified
|
// Add response format if specified
|
||||||
if (request.responseFormat) {
|
if (request.responseFormat) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
chatCompletionRequest.response_format = request.responseFormat;
|
chatCompletionRequest.response_format = request.responseFormat;
|
||||||
}
|
}
|
||||||
try {
|
const response = await chatCompletion(client, chatCompletionRequest, 'simpleInference');
|
||||||
const response = await client.chat.completions.create(chatCompletionRequest);
|
const modelResponse = response.choices[0]?.message?.content;
|
||||||
if ('choices' in response) {
|
coreExports.info(`Model response: ${modelResponse || 'No response content'}`);
|
||||||
const modelResponse = response.choices[0]?.message?.content;
|
return modelResponse || null;
|
||||||
coreExports.info(`Model response: ${modelResponse || 'No response content'}`);
|
|
||||||
return modelResponse || null;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
coreExports.error(`Unexpected response format from API: ${JSON.stringify(response)}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
coreExports.error(`API error: ${error}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* GitHub MCP-enabled inference with tool execution loop
|
* GitHub MCP-enabled inference with tool execution loop
|
||||||
@@ -49542,6 +49532,8 @@ async function mcpInference(request, githubMcpClient) {
|
|||||||
messages: messages,
|
messages: messages,
|
||||||
max_tokens: request.maxTokens,
|
max_tokens: request.maxTokens,
|
||||||
model: request.modelName,
|
model: request.modelName,
|
||||||
|
temperature: request.temperature,
|
||||||
|
top_p: request.topP,
|
||||||
};
|
};
|
||||||
// Add response format if specified (only on final iteration to avoid conflicts with tool calls)
|
// Add response format if specified (only on final iteration to avoid conflicts with tool calls)
|
||||||
if (finalMessage && request.responseFormat) {
|
if (finalMessage && request.responseFormat) {
|
||||||
@@ -49552,10 +49544,7 @@ async function mcpInference(request, githubMcpClient) {
|
|||||||
chatCompletionRequest.tools = githubMcpClient.tools;
|
chatCompletionRequest.tools = githubMcpClient.tools;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await client.chat.completions.create(chatCompletionRequest);
|
const response = await chatCompletion(client, chatCompletionRequest, `mcpInference iteration ${iterationCount}`);
|
||||||
if (!('choices' in response)) {
|
|
||||||
throw new Error(`Unexpected response format from API: ${JSON.stringify(response)}`);
|
|
||||||
}
|
|
||||||
const assistantMessage = response.choices[0]?.message;
|
const assistantMessage = response.choices[0]?.message;
|
||||||
const modelResponse = assistantMessage?.content;
|
const modelResponse = assistantMessage?.content;
|
||||||
const toolCalls = assistantMessage?.tool_calls;
|
const toolCalls = assistantMessage?.tool_calls;
|
||||||
@@ -49567,17 +49556,13 @@ async function mcpInference(request, githubMcpClient) {
|
|||||||
});
|
});
|
||||||
if (!toolCalls || toolCalls.length === 0) {
|
if (!toolCalls || toolCalls.length === 0) {
|
||||||
coreExports.info('No tool calls requested, ending GitHub MCP inference loop');
|
coreExports.info('No tool calls requested, ending GitHub MCP inference loop');
|
||||||
// If we have a response format set and we haven't explicitly run one final message iteration,
|
|
||||||
// do another loop with the response format set
|
|
||||||
if (request.responseFormat && !finalMessage) {
|
if (request.responseFormat && !finalMessage) {
|
||||||
coreExports.info('Making one more MCP loop with the requested response format...');
|
coreExports.info('Making one more MCP loop with the requested response format...');
|
||||||
// Add a user message requesting JSON format and try again
|
|
||||||
messages.push({
|
messages.push({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: `Please provide your response in the exact ${request.responseFormat.type} format specified.`,
|
content: `Please provide your response in the exact ${request.responseFormat.type} format specified.`,
|
||||||
});
|
});
|
||||||
finalMessage = true;
|
finalMessage = true;
|
||||||
// Continue the loop to get a properly formatted response
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -49585,9 +49570,7 @@ async function mcpInference(request, githubMcpClient) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
coreExports.info(`Model requested ${toolCalls.length} tool calls`);
|
coreExports.info(`Model requested ${toolCalls.length} tool calls`);
|
||||||
// Execute all tool calls via GitHub MCP
|
|
||||||
const toolResults = await executeToolCalls(githubMcpClient.client, toolCalls);
|
const toolResults = await executeToolCalls(githubMcpClient.client, toolCalls);
|
||||||
// Add tool results to the conversation
|
|
||||||
messages.push(...toolResults);
|
messages.push(...toolResults);
|
||||||
coreExports.info('Tool results added, continuing conversation...');
|
coreExports.info('Tool results added, continuing conversation...');
|
||||||
}
|
}
|
||||||
@@ -49604,6 +49587,38 @@ async function mcpInference(request, githubMcpClient) {
|
|||||||
.find(msg => msg.role === 'assistant');
|
.find(msg => msg.role === 'assistant');
|
||||||
return lastAssistantMessage?.content || null;
|
return lastAssistantMessage?.content || null;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Wrapper around OpenAI chat.completions.create with defensive handling for cases where
|
||||||
|
* the SDK returns a raw string (e.g., unexpected content-type or streaming body) instead of
|
||||||
|
* a parsed object. Ensures an object with a 'choices' array is returned or throws a descriptive error.
|
||||||
|
*/
|
||||||
|
async function chatCompletion(client, params, context) {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
let response = await client.chat.completions.create(params);
|
||||||
|
coreExports.debug(`${context}: raw response typeof=${typeof response}`);
|
||||||
|
if (typeof response === 'string') {
|
||||||
|
// Attempt to parse if we unexpectedly received a string
|
||||||
|
try {
|
||||||
|
response = JSON.parse(response);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
const preview = response.slice(0, 400);
|
||||||
|
throw new Error(`${context}: Chat completion response was a string and not valid JSON (${e.message}). Preview: ${preview}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!response || typeof response !== 'object' || !('choices' in response)) {
|
||||||
|
const preview = JSON.stringify(response)?.slice(0, 800);
|
||||||
|
throw new Error(`${context}: Unexpected response shape (no choices). Preview: ${preview}`);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
// Re-throw after logging for upstream handling
|
||||||
|
coreExports.error(`${context}: chatCompletion failed: ${err}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to load content from a file or use fallback input
|
* Helper function to load content from a file or use fallback input
|
||||||
@@ -49674,12 +49689,14 @@ function buildResponseFormat(promptConfig) {
|
|||||||
/**
|
/**
|
||||||
* Build complete InferenceRequest from prompt config and inputs
|
* Build complete InferenceRequest from prompt config and inputs
|
||||||
*/
|
*/
|
||||||
function buildInferenceRequest(promptConfig, systemPrompt, prompt, modelName, maxTokens, endpoint, token) {
|
function buildInferenceRequest(promptConfig, systemPrompt, prompt, modelName, temperature, topP, maxTokens, endpoint, token) {
|
||||||
const messages = buildMessages(promptConfig, systemPrompt, prompt);
|
const messages = buildMessages(promptConfig, systemPrompt, prompt);
|
||||||
const responseFormat = buildResponseFormat(promptConfig);
|
const responseFormat = buildResponseFormat(promptConfig);
|
||||||
return {
|
return {
|
||||||
messages,
|
messages,
|
||||||
modelName,
|
modelName,
|
||||||
|
temperature,
|
||||||
|
topP,
|
||||||
maxTokens,
|
maxTokens,
|
||||||
endpoint,
|
endpoint,
|
||||||
token,
|
token,
|
||||||
@@ -52561,10 +52578,8 @@ function loadPromptFile(filePath, templateVariables = {}) {
|
|||||||
throw new Error(`Prompt file not found: ${filePath}`);
|
throw new Error(`Prompt file not found: ${filePath}`);
|
||||||
}
|
}
|
||||||
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
||||||
// Apply template variable substitution
|
|
||||||
const processedContent = replaceTemplateVariables(fileContent, templateVariables);
|
|
||||||
try {
|
try {
|
||||||
const config = load(processedContent);
|
const config = load(fileContent);
|
||||||
if (!config.messages || !Array.isArray(config.messages)) {
|
if (!config.messages || !Array.isArray(config.messages)) {
|
||||||
throw new Error('Prompt file must contain a "messages" array');
|
throw new Error('Prompt file must contain a "messages" array');
|
||||||
}
|
}
|
||||||
@@ -52577,6 +52592,13 @@ function loadPromptFile(filePath, templateVariables = {}) {
|
|||||||
throw new Error(`Invalid message role: ${message.role}`);
|
throw new Error(`Invalid message role: ${message.role}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Prepare messages by replacing template variables with actual content
|
||||||
|
config.messages = config.messages.map(msg => {
|
||||||
|
return {
|
||||||
|
...msg,
|
||||||
|
content: replaceTemplateVariables(msg.content, templateVariables),
|
||||||
|
};
|
||||||
|
});
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
@@ -52624,7 +52646,10 @@ async function run() {
|
|||||||
}
|
}
|
||||||
// Get common parameters
|
// Get common parameters
|
||||||
const modelName = promptConfig?.model || coreExports.getInput('model');
|
const modelName = promptConfig?.model || coreExports.getInput('model');
|
||||||
const maxTokens = parseInt(coreExports.getInput('max-tokens'), 10);
|
let maxTokens = promptConfig?.modelParameters?.maxTokens ?? coreExports.getInput('max-tokens');
|
||||||
|
if (typeof maxTokens === 'string') {
|
||||||
|
maxTokens = parseInt(maxTokens, 10);
|
||||||
|
}
|
||||||
const token = process.env['GITHUB_TOKEN'] || coreExports.getInput('token');
|
const token = process.env['GITHUB_TOKEN'] || coreExports.getInput('token');
|
||||||
if (token === undefined) {
|
if (token === undefined) {
|
||||||
throw new Error('GITHUB_TOKEN is not set');
|
throw new Error('GITHUB_TOKEN is not set');
|
||||||
@@ -52633,7 +52658,7 @@ async function run() {
|
|||||||
const githubMcpToken = coreExports.getInput('github-mcp-token') || token;
|
const githubMcpToken = coreExports.getInput('github-mcp-token') || token;
|
||||||
const endpoint = coreExports.getInput('endpoint');
|
const endpoint = coreExports.getInput('endpoint');
|
||||||
// Build the inference request with pre-processed messages and response format
|
// Build the inference request with pre-processed messages and response format
|
||||||
const inferenceRequest = buildInferenceRequest(promptConfig, systemPrompt, prompt, modelName, maxTokens, endpoint, token);
|
const inferenceRequest = buildInferenceRequest(promptConfig, systemPrompt, prompt, modelName, promptConfig?.modelParameters?.temperature, promptConfig?.modelParameters?.topP, maxTokens, endpoint, token);
|
||||||
const enableMcp = coreExports.getBooleanInput('enable-github-mcp') || false;
|
const enableMcp = coreExports.getBooleanInput('enable-github-mcp') || false;
|
||||||
let modelResponse = null;
|
let modelResponse = null;
|
||||||
if (enableMcp) {
|
if (enableMcp) {
|
||||||
|
|||||||
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
@@ -82,6 +82,8 @@ export function buildInferenceRequest(
|
|||||||
systemPrompt: string | undefined,
|
systemPrompt: string | undefined,
|
||||||
prompt: string | undefined,
|
prompt: string | undefined,
|
||||||
modelName: string,
|
modelName: string,
|
||||||
|
temperature: number | undefined,
|
||||||
|
topP: number | undefined,
|
||||||
maxTokens: number,
|
maxTokens: number,
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
token: string,
|
token: string,
|
||||||
@@ -92,6 +94,8 @@ export function buildInferenceRequest(
|
|||||||
return {
|
return {
|
||||||
messages,
|
messages,
|
||||||
modelName,
|
modelName,
|
||||||
|
temperature,
|
||||||
|
topP,
|
||||||
maxTokens,
|
maxTokens,
|
||||||
endpoint,
|
endpoint,
|
||||||
token,
|
token,
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export interface InferenceRequest {
|
|||||||
maxTokens: number
|
maxTokens: number
|
||||||
endpoint: string
|
endpoint: string
|
||||||
token: string
|
token: string
|
||||||
|
temperature?: number
|
||||||
|
topP?: number
|
||||||
responseFormat?: {type: 'json_schema'; json_schema: unknown} // Processed response format for the API
|
responseFormat?: {type: 'json_schema'; json_schema: unknown} // Processed response format for the API
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +47,8 @@ export async function simpleInference(request: InferenceRequest): Promise<string
|
|||||||
messages: request.messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[],
|
messages: request.messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[],
|
||||||
max_tokens: request.maxTokens,
|
max_tokens: request.maxTokens,
|
||||||
model: request.modelName,
|
model: request.modelName,
|
||||||
|
temperature: request.temperature,
|
||||||
|
top_p: request.topP,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add response format if specified
|
// Add response format if specified
|
||||||
@@ -53,21 +57,10 @@ export async function simpleInference(request: InferenceRequest): Promise<string
|
|||||||
chatCompletionRequest.response_format = request.responseFormat as any
|
chatCompletionRequest.response_format = request.responseFormat as any
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const response = await chatCompletion(client, chatCompletionRequest, 'simpleInference')
|
||||||
const response = await client.chat.completions.create(chatCompletionRequest)
|
const modelResponse = response.choices[0]?.message?.content
|
||||||
|
core.info(`Model response: ${modelResponse || 'No response content'}`)
|
||||||
if ('choices' in response) {
|
return modelResponse || null
|
||||||
const modelResponse = response.choices[0]?.message?.content
|
|
||||||
core.info(`Model response: ${modelResponse || 'No response content'}`)
|
|
||||||
return modelResponse || null
|
|
||||||
} else {
|
|
||||||
core.error(`Unexpected response format from API: ${JSON.stringify(response)}`)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
core.error(`API error: ${error}`)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -101,6 +94,8 @@ export async function mcpInference(
|
|||||||
messages: messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[],
|
messages: messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[],
|
||||||
max_tokens: request.maxTokens,
|
max_tokens: request.maxTokens,
|
||||||
model: request.modelName,
|
model: request.modelName,
|
||||||
|
temperature: request.temperature,
|
||||||
|
top_p: request.topP,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add response format if specified (only on final iteration to avoid conflicts with tool calls)
|
// Add response format if specified (only on final iteration to avoid conflicts with tool calls)
|
||||||
@@ -112,11 +107,7 @@ export async function mcpInference(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await client.chat.completions.create(chatCompletionRequest)
|
const response = await chatCompletion(client, chatCompletionRequest, `mcpInference iteration ${iterationCount}`)
|
||||||
|
|
||||||
if (!('choices' in response)) {
|
|
||||||
throw new Error(`Unexpected response format from API: ${JSON.stringify(response)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const assistantMessage = response.choices[0]?.message
|
const assistantMessage = response.choices[0]?.message
|
||||||
const modelResponse = assistantMessage?.content
|
const modelResponse = assistantMessage?.content
|
||||||
@@ -133,20 +124,13 @@ export async function mcpInference(
|
|||||||
if (!toolCalls || toolCalls.length === 0) {
|
if (!toolCalls || toolCalls.length === 0) {
|
||||||
core.info('No tool calls requested, ending GitHub MCP inference loop')
|
core.info('No tool calls requested, ending GitHub MCP inference loop')
|
||||||
|
|
||||||
// If we have a response format set and we haven't explicitly run one final message iteration,
|
|
||||||
// do another loop with the response format set
|
|
||||||
if (request.responseFormat && !finalMessage) {
|
if (request.responseFormat && !finalMessage) {
|
||||||
core.info('Making one more MCP loop with the requested response format...')
|
core.info('Making one more MCP loop with the requested response format...')
|
||||||
|
|
||||||
// Add a user message requesting JSON format and try again
|
|
||||||
messages.push({
|
messages.push({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: `Please provide your response in the exact ${request.responseFormat.type} format specified.`,
|
content: `Please provide your response in the exact ${request.responseFormat.type} format specified.`,
|
||||||
})
|
})
|
||||||
|
|
||||||
finalMessage = true
|
finalMessage = true
|
||||||
|
|
||||||
// Continue the loop to get a properly formatted response
|
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
return modelResponse || null
|
return modelResponse || null
|
||||||
@@ -154,13 +138,8 @@ export async function mcpInference(
|
|||||||
}
|
}
|
||||||
|
|
||||||
core.info(`Model requested ${toolCalls.length} tool calls`)
|
core.info(`Model requested ${toolCalls.length} tool calls`)
|
||||||
|
|
||||||
// Execute all tool calls via GitHub MCP
|
|
||||||
const toolResults = await executeToolCalls(githubMcpClient.client, toolCalls as ToolCall[])
|
const toolResults = await executeToolCalls(githubMcpClient.client, toolCalls as ToolCall[])
|
||||||
|
|
||||||
// Add tool results to the conversation
|
|
||||||
messages.push(...toolResults)
|
messages.push(...toolResults)
|
||||||
|
|
||||||
core.info('Tool results added, continuing conversation...')
|
core.info('Tool results added, continuing conversation...')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
core.error(`OpenAI API error: ${error}`)
|
core.error(`OpenAI API error: ${error}`)
|
||||||
@@ -178,3 +157,43 @@ export async function mcpInference(
|
|||||||
|
|
||||||
return lastAssistantMessage?.content || null
|
return lastAssistantMessage?.content || null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around OpenAI chat.completions.create with defensive handling for cases where
|
||||||
|
* the SDK returns a raw string (e.g., unexpected content-type or streaming body) instead of
|
||||||
|
* a parsed object. Ensures an object with a 'choices' array is returned or throws a descriptive error.
|
||||||
|
*/
|
||||||
|
async function chatCompletion(
|
||||||
|
client: OpenAI,
|
||||||
|
params: OpenAI.Chat.Completions.ChatCompletionCreateParams,
|
||||||
|
context: string,
|
||||||
|
): Promise<OpenAI.Chat.Completions.ChatCompletion> {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
let response: any = await client.chat.completions.create(params)
|
||||||
|
core.debug(`${context}: raw response typeof=${typeof response}`)
|
||||||
|
|
||||||
|
if (typeof response === 'string') {
|
||||||
|
// Attempt to parse if we unexpectedly received a string
|
||||||
|
try {
|
||||||
|
response = JSON.parse(response)
|
||||||
|
} catch (e) {
|
||||||
|
const preview = response.slice(0, 400)
|
||||||
|
throw new Error(
|
||||||
|
`${context}: Chat completion response was a string and not valid JSON (${(e as Error).message}). Preview: ${preview}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response || typeof response !== 'object' || !('choices' in response)) {
|
||||||
|
const preview = JSON.stringify(response)?.slice(0, 800)
|
||||||
|
throw new Error(`${context}: Unexpected response shape (no choices). Preview: ${preview}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response as OpenAI.Chat.Completions.ChatCompletion
|
||||||
|
} catch (err) {
|
||||||
|
// Re-throw after logging for upstream handling
|
||||||
|
core.error(`${context}: chatCompletion failed: ${err}`)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,7 +53,11 @@ export async function run(): Promise<void> {
|
|||||||
|
|
||||||
// Get common parameters
|
// Get common parameters
|
||||||
const modelName = promptConfig?.model || core.getInput('model')
|
const modelName = promptConfig?.model || core.getInput('model')
|
||||||
const maxTokens = parseInt(core.getInput('max-tokens'), 10)
|
let maxTokens = promptConfig?.modelParameters?.maxTokens ?? core.getInput('max-tokens')
|
||||||
|
|
||||||
|
if (typeof maxTokens === 'string') {
|
||||||
|
maxTokens = parseInt(maxTokens, 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) {
|
||||||
@@ -71,6 +75,8 @@ export async function run(): Promise<void> {
|
|||||||
systemPrompt,
|
systemPrompt,
|
||||||
prompt,
|
prompt,
|
||||||
modelName,
|
modelName,
|
||||||
|
promptConfig?.modelParameters?.temperature,
|
||||||
|
promptConfig?.modelParameters?.topP,
|
||||||
maxTokens,
|
maxTokens,
|
||||||
endpoint,
|
endpoint,
|
||||||
token,
|
token,
|
||||||
|
|||||||
@@ -7,9 +7,16 @@ export interface PromptMessage {
|
|||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ModelParameters {
|
||||||
|
maxTokens?: number
|
||||||
|
temperature?: number
|
||||||
|
topP?: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface PromptConfig {
|
export interface PromptConfig {
|
||||||
messages: PromptMessage[]
|
messages: PromptMessage[]
|
||||||
model?: string
|
model?: string
|
||||||
|
modelParameters?: ModelParameters
|
||||||
responseFormat?: 'text' | 'json_schema'
|
responseFormat?: 'text' | 'json_schema'
|
||||||
jsonSchema?: string
|
jsonSchema?: string
|
||||||
}
|
}
|
||||||
@@ -101,11 +108,8 @@ export function loadPromptFile(filePath: string, templateVariables: TemplateVari
|
|||||||
|
|
||||||
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
||||||
|
|
||||||
// Apply template variable substitution
|
|
||||||
const processedContent = replaceTemplateVariables(fileContent, templateVariables)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const config = yaml.load(processedContent) as PromptConfig
|
const config = yaml.load(fileContent) as PromptConfig
|
||||||
|
|
||||||
if (!config.messages || !Array.isArray(config.messages)) {
|
if (!config.messages || !Array.isArray(config.messages)) {
|
||||||
throw new Error('Prompt file must contain a "messages" array')
|
throw new Error('Prompt file must contain a "messages" array')
|
||||||
@@ -121,6 +125,14 @@ export function loadPromptFile(filePath: string, templateVariables: TemplateVari
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepare messages by replacing template variables with actual content
|
||||||
|
config.messages = config.messages.map(msg => {
|
||||||
|
return {
|
||||||
|
...msg,
|
||||||
|
content: replaceTemplateVariables(msg.content, templateVariables),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return config
|
return config
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to parse prompt file: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
throw new Error(`Failed to parse prompt file: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||||
|
|||||||
Reference in New Issue
Block a user