Support .prompt.yml files

This commit is contained in:
Sean Goedecke
2025-07-21 00:11:26 +00:00
parent 0479ac822e
commit 1780121e3b
17 changed files with 3889 additions and 129 deletions
+76
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,111 @@
import * as core from '@actions/core'
import * as fs from 'fs'
import * as yaml from 'js-yaml'
export interface PromptMessage {
role: 'system' | 'user' | 'assistant'
content: string
}
export interface PromptConfig {
messages: PromptMessage[]
model?: string
responseFormat?: 'text' | 'json_schema'
jsonSchema?: string
}
export interface TemplateVariables {
[key: string]: string
}
/**
* Parse template variables from YAML input string
*/
export function parseTemplateVariables(input: string): TemplateVariables {
if (!input.trim()) {
return {}
}
try {
const parsed = yaml.load(input) as TemplateVariables
if (typeof parsed !== 'object' || parsed === null) {
throw new Error('Template variables must be a YAML object')
}
return parsed
} catch (error) {
throw new Error(
`Failed to parse template variables: ${error instanceof Error ? error.message : 'Unknown error'}`
)
}
}
/**
* Replace template variables in text using {{variable}} syntax
*/
export function replaceTemplateVariables(
text: string,
variables: TemplateVariables
): string {
return text.replace(/\{\{(\w+)\}\}/g, (match, variableName) => {
if (variableName in variables) {
return variables[variableName]
}
core.warning(
`Template variable '${variableName}' not found in input variables`
)
return match // Return the original placeholder if variable not found
})
}
/**
* Load and parse a prompt YAML file with template variable substitution
*/
export function loadPromptFile(
filePath: string,
templateVariables: TemplateVariables = {}
): PromptConfig {
if (!fs.existsSync(filePath)) {
throw new Error(`Prompt file not found: ${filePath}`)
}
const fileContent = fs.readFileSync(filePath, 'utf-8')
// Apply template variable substitution
const processedContent = replaceTemplateVariables(
fileContent,
templateVariables
)
try {
const config = yaml.load(processedContent) as PromptConfig
if (!config.messages || !Array.isArray(config.messages)) {
throw new Error('Prompt file must contain a "messages" array')
}
// Validate messages
for (const message of config.messages) {
if (!message.role || !message.content) {
throw new Error(
'Each message must have "role" and "content" properties'
)
}
if (!['system', 'user', 'assistant'].includes(message.role)) {
throw new Error(`Invalid message role: ${message.role}`)
}
}
return config
} catch (error) {
throw new Error(
`Failed to parse prompt file: ${error instanceof Error ? error.message : 'Unknown error'}`
)
}
}
/**
* Check if a file is a prompt YAML file based on extension
*/
export function isPromptYamlFile(filePath: string): boolean {
return filePath.endsWith('.prompt.yml') || filePath.endsWith('.prompt.yaml')
}