UNPKG

@yuxilabs/gptp-core

Version:

Core validation, formatting and execution logic for the GPTP file format.

103 lines (102 loc) 4.4 kB
// src/engine/execute/executePrompt.ts import { renderPrompt } from '@/engine/render/renderPrompt'; import { validatePrompt } from '@/engine/validate/validatePrompt'; import { resolveSecrets } from '@/runtime/secrets'; import * as fs from 'node:fs/promises'; import * as path from 'node:path'; import { selectProvider, mapParams } from '@/engine/execute/providerRouter'; /** * Executes a GPTP prompt: resolves variables and optionally calls a model. */ export async function executePrompt(prompt, options) { const { input, run = false, signal, timeoutMs, validate = false, retry, lockfilePath } = options; if (!Array.isArray(prompt.messages)) { throw new Error('Invalid prompt: "messages" must be an array.'); } // Resolve provider secrets from env:KEY references; warn on inline secrets resolveSecrets(prompt, { injectStandardEnv: true }); // Optional validation (off by default to avoid network fetch in typical unit tests) if (validate) { const validation = await validatePrompt(prompt); if (!validation.valid) { throw new Error(`Prompt validation failed: ${validation.errors?.[0]?.message || 'Unknown error'}`); } } // Deterministic render with single-pass interpolation and hashes const { renderedMessages: resolvedMessages, renderedPromptHash, variablesHash } = renderPrompt(prompt, input); if (!run) { return { resolvedMessages, modelOutput: '', renderedPromptHash, variablesHash }; } // Optional timeout handling let controller; let timeoutId; let effectiveSignal = signal; if (timeoutMs && !signal) { controller = new AbortController(); effectiveSignal = controller.signal; timeoutId = setTimeout(() => controller?.abort(), timeoutMs); } const retries = Math.max(0, retry?.retries ?? 0); const baseDelay = Math.max(0, retry?.baseDelayMs ?? 250); const maxDelay = Math.max(baseDelay, retry?.maxDelayMs ?? 4000); const useJitter = !!retry?.jitter; let lastError; for (let attempt = 0; attempt <= retries; attempt++) { try { const { call, type, name } = selectProvider(prompt); const mapped = mapParams(prompt.params); const aiResponse = await call({ messages: resolvedMessages, model: mapped.model, temperature: mapped.temperature, top_p: mapped.top_p, max_tokens: mapped.max_tokens, signal: effectiveSignal, }); const modelOutput = aiResponse?.choices?.[0]?.message?.content ?? '[No response from model]'; // Optional lockfile write if (lockfilePath) { await writeLockfile(lockfilePath, { timestamp: new Date().toISOString(), provider: { type, name }, model: mapped.model, params: { temperature: mapped.temperature, top_p: mapped.top_p, max_tokens: mapped.max_tokens, }, hashes: { renderedPromptHash, variablesHash }, output: modelOutput, }); } return { resolvedMessages, modelOutput, renderedPromptHash, variablesHash }; } catch (err) { // If aborted, do not retry if (effectiveSignal?.aborted) { throw err; } lastError = err; if (attempt < retries) { const expBackoff = Math.min(maxDelay, baseDelay * Math.pow(2, attempt)); const delay = useJitter ? Math.floor(Math.random() * expBackoff) : expBackoff; await delayMs(delay); continue; } else { throw err; } } } // should never get here throw lastError instanceof Error ? lastError : new Error('Unknown execution error'); async function writeLockfile(file, payload) { const dir = path.dirname(file); await fs.mkdir(dir, { recursive: true }); const content = JSON.stringify(payload, null, 2); await fs.writeFile(file, content, 'utf-8'); } function delayMs(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } }