Support .prompt.yml files
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import * as core from '@actions/core'
|
||||
import { GetChatCompletionsDefaultResponse } from '@azure-rest/ai-inference'
|
||||
import * as fs from 'fs'
|
||||
import { PromptConfig } from './prompt.js'
|
||||
import { InferenceRequest } from './inference.js'
|
||||
|
||||
/**
|
||||
* Helper function to load content from a file or use fallback input
|
||||
@@ -64,3 +66,77 @@ export function handleUnexpectedResponse(
|
||||
: JSON.stringify(response.body))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build messages array from either prompt config or legacy format
|
||||
*/
|
||||
export function buildMessages(
|
||||
promptConfig?: PromptConfig,
|
||||
systemPrompt?: string,
|
||||
prompt?: string
|
||||
): Array<{ role: string; content: string }> {
|
||||
if (promptConfig?.messages && promptConfig.messages.length > 0) {
|
||||
// Use new message format
|
||||
return promptConfig.messages.map((msg) => ({
|
||||
role: msg.role,
|
||||
content: msg.content
|
||||
}))
|
||||
} else {
|
||||
// Use legacy format
|
||||
return [
|
||||
{
|
||||
role: 'system',
|
||||
content: systemPrompt || 'You are a helpful assistant'
|
||||
},
|
||||
{ role: 'user', content: prompt || '' }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build response format object for API from prompt config
|
||||
*/
|
||||
export function buildResponseFormat(promptConfig?: PromptConfig): any {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
+17
-19
@@ -5,12 +5,12 @@ import { GitHubMCPClient, executeToolCalls } from './mcp.js'
|
||||
import { handleUnexpectedResponse } from './helpers.js'
|
||||
|
||||
export interface InferenceRequest {
|
||||
systemPrompt: string
|
||||
prompt: string
|
||||
messages: Array<{ role: string; content: string }>
|
||||
modelName: string
|
||||
maxTokens: number
|
||||
endpoint: string
|
||||
token: string
|
||||
responseFormat?: any // Will contain the processed response format for the API
|
||||
}
|
||||
|
||||
export interface InferenceResponse {
|
||||
@@ -41,18 +41,17 @@ export async function simpleInference(
|
||||
}
|
||||
)
|
||||
|
||||
const requestBody = {
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: request.systemPrompt
|
||||
},
|
||||
{ role: 'user', content: request.prompt }
|
||||
],
|
||||
const requestBody: any = {
|
||||
messages: request.messages,
|
||||
max_tokens: request.maxTokens,
|
||||
model: request.modelName
|
||||
}
|
||||
|
||||
// Add response format if specified
|
||||
if (request.responseFormat) {
|
||||
requestBody.response_format = request.responseFormat
|
||||
}
|
||||
|
||||
const response = await client.path('/chat/completions').post({
|
||||
body: requestBody
|
||||
})
|
||||
@@ -84,14 +83,8 @@ export async function mcpInference(
|
||||
}
|
||||
)
|
||||
|
||||
// Start with the initial conversation
|
||||
const messages = [
|
||||
{
|
||||
role: 'system',
|
||||
content: request.systemPrompt
|
||||
},
|
||||
{ role: 'user', content: request.prompt }
|
||||
]
|
||||
// Start with the pre-processed messages
|
||||
const messages: Array<any> = [...request.messages]
|
||||
|
||||
let iterationCount = 0
|
||||
const maxIterations = 5 // Prevent infinite loops
|
||||
@@ -100,13 +93,18 @@ export async function mcpInference(
|
||||
iterationCount++
|
||||
core.info(`MCP inference iteration ${iterationCount}`)
|
||||
|
||||
const requestBody = {
|
||||
const requestBody: any = {
|
||||
messages: messages,
|
||||
max_tokens: request.maxTokens,
|
||||
model: request.modelName,
|
||||
tools: githubMcpClient.tools
|
||||
}
|
||||
|
||||
// Add response format if specified (only on first iteration to avoid conflicts)
|
||||
if (iterationCount === 1 && request.responseFormat) {
|
||||
requestBody.response_format = request.responseFormat
|
||||
}
|
||||
|
||||
const response = await client.path('/chat/completions').post({
|
||||
body: requestBody
|
||||
})
|
||||
|
||||
+43
-14
@@ -3,8 +3,13 @@ import * as fs from 'fs'
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
import { connectToGitHubMCP } from './mcp.js'
|
||||
import { simpleInference, mcpInference, InferenceRequest } from './inference.js'
|
||||
import { loadContentFromFileOrInput } from './helpers.js'
|
||||
import { simpleInference, mcpInference } from './inference.js'
|
||||
import { loadContentFromFileOrInput, buildInferenceRequest } from './helpers.js'
|
||||
import {
|
||||
loadPromptFile,
|
||||
parseTemplateVariables,
|
||||
isPromptYamlFile
|
||||
} from './prompt.js'
|
||||
|
||||
const RESPONSE_FILE = 'modelResponse.txt'
|
||||
|
||||
@@ -15,16 +20,37 @@ const RESPONSE_FILE = 'modelResponse.txt'
|
||||
*/
|
||||
export async function run(): Promise<void> {
|
||||
try {
|
||||
const prompt = loadContentFromFileOrInput('prompt-file', 'prompt')
|
||||
const promptFilePath = core.getInput('prompt-file')
|
||||
const inputVariables = core.getInput('input')
|
||||
|
||||
const systemPrompt = loadContentFromFileOrInput(
|
||||
'system-prompt-file',
|
||||
'system-prompt',
|
||||
'You are a helpful assistant'
|
||||
)
|
||||
let promptConfig: any = undefined
|
||||
let systemPrompt: string | undefined = undefined
|
||||
let prompt: string | undefined = undefined
|
||||
|
||||
const modelName: string = core.getInput('model')
|
||||
const maxTokens: number = parseInt(core.getInput('max-tokens'), 10)
|
||||
// Check if we're using a prompt YAML file
|
||||
if (promptFilePath && isPromptYamlFile(promptFilePath)) {
|
||||
core.info('Using prompt YAML file format')
|
||||
|
||||
// Parse template variables
|
||||
const templateVariables = parseTemplateVariables(inputVariables)
|
||||
|
||||
// Load and process prompt file
|
||||
promptConfig = loadPromptFile(promptFilePath, templateVariables)
|
||||
} else {
|
||||
// Use legacy format
|
||||
core.info('Using legacy prompt format')
|
||||
|
||||
prompt = loadContentFromFileOrInput('prompt-file', 'prompt')
|
||||
systemPrompt = loadContentFromFileOrInput(
|
||||
'system-prompt-file',
|
||||
'system-prompt',
|
||||
'You are a helpful assistant'
|
||||
)
|
||||
}
|
||||
|
||||
// Get common parameters
|
||||
const modelName = promptConfig?.model || core.getInput('model')
|
||||
const maxTokens = parseInt(core.getInput('max-tokens'), 10)
|
||||
|
||||
const token = process.env['GITHUB_TOKEN'] || core.getInput('token')
|
||||
if (token === undefined) {
|
||||
@@ -32,21 +58,24 @@ export async function run(): Promise<void> {
|
||||
}
|
||||
|
||||
const endpoint = core.getInput('endpoint')
|
||||
const enableMcp = core.getBooleanInput('enable-github-mcp') || false
|
||||
|
||||
const inferenceRequest: InferenceRequest = {
|
||||
// Build the inference request with pre-processed messages and response format
|
||||
const inferenceRequest = buildInferenceRequest(
|
||||
promptConfig,
|
||||
systemPrompt,
|
||||
prompt,
|
||||
modelName,
|
||||
maxTokens,
|
||||
endpoint,
|
||||
token
|
||||
}
|
||||
)
|
||||
|
||||
const enableMcp = core.getBooleanInput('enable-github-mcp') || false
|
||||
|
||||
let modelResponse: string | null = null
|
||||
|
||||
if (enableMcp) {
|
||||
const mcpClient = await connectToGitHubMCP(token)
|
||||
const mcpClient = await connectToGitHubMCP(inferenceRequest.token)
|
||||
|
||||
if (mcpClient) {
|
||||
modelResponse = await mcpInference(inferenceRequest, mcpClient)
|
||||
|
||||
+111
@@ -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