#!/usr/bin/env node
import { L as state, M as getEnvString, c as logger, k as getEnvBool } from "./logger-4ntIRWmf.js";
import { r as fetchWithProxy } from "./fetch-gOBR1GF3.js";
import { t as invariant } from "./invariant-BtWWVVhl.js";
import { n as telemetry } from "./telemetry-Do_evhAG.js";
import { a as isVideoFile, i as isJavascriptFile, n as isAudioFile, r as isImageFile } from "./fileExtensions-2M1yL3B0.js";
import { r as importModule } from "./esm-BOYGx5J1.js";
import { Y as extractVariablesFromTemplate, Z as getNunjucksEngine, q as renderVarsInObject } from "./util-DAS7mIn_.js";
import { r as runPython } from "./pythonUtils-x98q_VG7.js";
import { n as loadFromPackage, t as isPackagePath } from "./packageParser-DVFGAxzK.js";
import { o as transform } from "./transform-BgLZyIl1.js";
import * as path$1 from "path";
import yaml from "js-yaml";
import fsPromises from "fs/promises";
//#region src/integrations/helicone.ts
const heliconeApiKey = getEnvString("HELICONE_API_KEY");
const buildFilter = (majorVersion, minorVersion) => {
	const filter = {};
	if (majorVersion === void 0 && minorVersion === void 0) return filter;
	if (majorVersion !== void 0) filter.left = { prompts_versions: { major_version: { equals: majorVersion } } };
	if (minorVersion === void 0) {
		filter.operator = "and";
		filter.right = "all";
	} else {
		if (!filter.left) {
			filter.left = { prompts_versions: { minor_version: { equals: minorVersion } } };
			filter.operator = "and";
			filter.right = "all";
			return filter;
		}
		filter.operator = "and";
		filter.right = { prompts_versions: { minor_version: { equals: minorVersion } } };
	}
	return filter;
};
async function getPrompt$2(id, variables, majorVersion, minorVersion) {
	const getHeliconePrompt = async (id, majorVersion, minorVersion, variables) => {
		return await (await fetchWithProxy(`https://api.helicone.ai/v1/prompt/${id}/compile`, {
			headers: {
				Authorization: `Bearer ${heliconeApiKey}`,
				"Content-Type": "application/json"
			},
			method: "POST",
			body: JSON.stringify({
				filter: buildFilter(majorVersion, minorVersion),
				inputs: variables
			})
		})).json();
	};
	const heliconePrompt = await getHeliconePrompt(id, majorVersion, minorVersion, variables);
	if (heliconePrompt.error) throw new Error(heliconePrompt.error);
	return heliconePrompt.data?.prompt_compiled;
}
//#endregion
//#region src/integrations/langfuse.ts
const langfuseParams = {
	publicKey: getEnvString("LANGFUSE_PUBLIC_KEY"),
	secretKey: getEnvString("LANGFUSE_SECRET_KEY"),
	baseUrl: getEnvString("LANGFUSE_HOST")
};
let langfuse;
async function getPrompt$1(id, vars, type, version, label) {
	let prompt;
	if (!langfuse) try {
		const { Langfuse } = await import("langfuse");
		langfuse = new Langfuse(langfuseParams);
	} catch (_err) {
		throw new Error("The langfuse package is required for Langfuse integration. Please install it with: npm install langfuse");
	}
	const options = label ? { label } : {};
	try {
		if (type === "text" || type === void 0) prompt = await langfuse.getPrompt(id, version, {
			...options,
			type: "text"
		});
		else prompt = await langfuse.getPrompt(id, version, {
			...options,
			type: "chat"
		});
	} catch (err) {
		const error = err;
		if (label) throw new Error(`Failed to fetch Langfuse prompt "${id}" with label "${label}": ${error.message || error}`);
		else if (version === void 0) throw new Error(`Failed to fetch Langfuse prompt "${id}": ${error.message || error}`);
		else throw new Error(`Failed to fetch Langfuse prompt "${id}" version ${version}: ${error.message || error}`);
	}
	const stringVars = {};
	for (const [key, value] of Object.entries(vars)) stringVars[key] = typeof value === "string" ? value : JSON.stringify(value);
	const compiledPrompt = prompt.compile(stringVars);
	if (typeof compiledPrompt !== "string") return JSON.stringify(compiledPrompt);
	return compiledPrompt;
}
//#endregion
//#region src/integrations/portkey.ts
async function getPrompt(id, variables) {
	const apiKey = getEnvString("PORTKEY_API_KEY");
	invariant(apiKey, "PORTKEY_API_KEY is required");
	const response = await fetchWithProxy(`https://api.portkey.ai/v1/prompts/${id}/render`, {
		method: "POST",
		headers: {
			"Content-Type": "application/json",
			"x-portkey-api-key": apiKey
		},
		body: JSON.stringify({ variables })
	});
	if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
	const result = await response.json();
	if (!result.success) throw new Error(`Portkey error! ${JSON.stringify(result)}`);
	return result.data;
}
//#endregion
//#region src/util/numeric.ts
/**
* Filters a record to only include finite numeric values, dropping strings,
* null, NaN, Infinity, arrays, objects, etc. Used to sanitize namedScores
* from untrusted sources (extension hooks) before metrics aggregation.
*/
function filterFiniteScores(scores) {
	const filtered = {};
	for (const [key, value] of Object.entries(scores)) if (typeof value === "number" && Number.isFinite(value)) filtered[key] = value;
	return filtered;
}
//#endregion
//#region src/evaluatorHelpers.ts
async function extractTextFromPDF(pdfPath) {
	logger.debug(`Extracting text from PDF: ${pdfPath}`);
	try {
		const { PDFParse } = await import("pdf-parse");
		const parser = new PDFParse({ data: await fsPromises.readFile(pdfPath) });
		const result = await parser.getText();
		await parser.destroy();
		return result.text.trim();
	} catch (error) {
		if (error instanceof Error && error.message.includes("Cannot find module 'pdf-parse'")) throw new Error("pdf-parse is not installed. Please install it with: npm install pdf-parse");
		throw new Error(`Failed to extract text from PDF ${pdfPath}: ${error instanceof Error ? error.message : String(error)}`);
	}
}
function resolveVariables(variables, skipResolveVars, varsResolvedFromSkipped) {
	let resolved;
	const regex = /\{\{\s*(\w+)\s*\}\}/;
	let iterations = 0;
	do {
		resolved = true;
		for (const key of Object.keys(variables)) {
			if (skipResolveVars?.includes(key) || varsResolvedFromSkipped?.has(key) || typeof variables[key] !== "string") continue;
			const value = variables[key];
			const match = regex.exec(value);
			if (match) {
				const [placeholder, varName] = match;
				if (variables[varName] === void 0) {} else {
					variables[key] = value.replace(placeholder, variables[varName]);
					if (skipResolveVars?.includes(varName) || varsResolvedFromSkipped?.has(varName)) varsResolvedFromSkipped?.add(key);
					resolved = false;
				}
			}
		}
		iterations++;
	} while (!resolved && iterations < 5);
	return variables;
}
function autoWrapRawIfPartialNunjucks(prompt) {
	const hasPartialTag = /({%[^%]*$|{{[^}]*$|{#[^#]*$)/m.test(prompt);
	const alreadyWrapped = /{\%\s*raw\s*\%}/.test(prompt) && /{\%\s*endraw\s*\%}/.test(prompt);
	if (hasPartialTag && !alreadyWrapped) return `{% raw %}${prompt}{% endraw %}`;
	return prompt;
}
function referencesUndefinedVariables(template, vars) {
	return extractVariablesFromTemplate(template).some((variableName) => {
		const rootVariableName = /^([A-Za-z_]\w*)/.exec(variableName)?.[1];
		return Boolean(rootVariableName && rootVariableName !== "env" && vars[rootVariableName] === void 0);
	});
}
/**
* Collects metadata about file variables in the vars object.
* @param vars The variables object containing potential file references
* @returns An object mapping variable names to their file metadata
*/
function collectFileMetadata(vars) {
	const fileMetadata = {};
	for (const [varName, value] of Object.entries(vars)) if (typeof value === "string" && value.startsWith("file://")) {
		const filePath = path$1.resolve(state.basePath || "", value.slice(7));
		const fileExtension = filePath.split(".").pop() || "";
		if (isImageFile(filePath)) fileMetadata[varName] = {
			path: value,
			type: "image",
			format: fileExtension
		};
		else if (isVideoFile(filePath)) fileMetadata[varName] = {
			path: value,
			type: "video",
			format: fileExtension
		};
		else if (isAudioFile(filePath)) fileMetadata[varName] = {
			path: value,
			type: "audio",
			format: fileExtension
		};
	}
	return fileMetadata;
}
/**
* Gets MIME type from file extension
*
* Supported formats:
* - JPEG/JPG (image/jpeg)
* - PNG (image/png)
* - GIF (image/gif)
* - WebP (image/webp)
* - BMP (image/bmp)
* - SVG (image/svg+xml)
* - TIFF (image/tiff)
* - ICO (image/x-icon)
* - AVIF (image/avif)
* - HEIC/HEIF (image/heic)
*
* @param extension File extension (with or without dot)
* @returns MIME type string (defaults to image/jpeg for unknown formats)
*/
function getMimeTypeFromExtension(extension) {
	return {
		jpg: "image/jpeg",
		jpeg: "image/jpeg",
		png: "image/png",
		gif: "image/gif",
		bmp: "image/bmp",
		webp: "image/webp",
		svg: "image/svg+xml",
		tif: "image/tiff",
		tiff: "image/tiff",
		ico: "image/x-icon",
		avif: "image/avif",
		heic: "image/heic",
		heif: "image/heif"
	}[extension.toLowerCase().replace(/^\./, "")] || "image/jpeg";
}
/**
* Detects MIME type from base64 magic numbers for additional accuracy
*
* Magic numbers (base64-encoded file signatures):
* - JPEG: /9j/ (0xFFD8FF)
* - PNG: iVBORw0KGgo (0x89504E47)
* - GIF: R0lGODlh or R0lGODdh (GIF87a/GIF89a)
* - WebP: UklGR (RIFF)
* - BMP: Qk0 or Qk1 (BM)
* - TIFF: SUkq or TU0A (II* or MM*)
* - ICO: AAABAA (0x00000100)
*
* @param base64Data Base64 encoded image data
* @returns MIME type string or null if format cannot be detected
*/
function detectMimeFromBase64(base64Data) {
	if (base64Data.startsWith("/9j/")) return "image/jpeg";
	else if (base64Data.startsWith("iVBORw0KGgo")) return "image/png";
	else if (base64Data.startsWith("R0lGODlh") || base64Data.startsWith("R0lGODdh")) return "image/gif";
	else if (base64Data.startsWith("UklGR")) return "image/webp";
	else if (base64Data.startsWith("Qk0") || base64Data.startsWith("Qk1")) return "image/bmp";
	else if (base64Data.startsWith("SUkq") || base64Data.startsWith("TU0A")) return "image/tiff";
	else if (base64Data.startsWith("AAABAA")) return "image/x-icon";
	return null;
}
/**
* Renders a prompt template with variable substitution using Nunjucks.
*
* @param prompt - The prompt template to render
* @param vars - Variables to substitute into the template
* @param nunjucksFilters - Optional custom Nunjucks filters
* @param provider - Optional API provider for context
* @param skipRenderVars - Optional array of variable names to skip special loading and template
*                         rendering for. This is critical for red team testing where injection
*                         variables contain attack payloads (e.g., SSTI, XSS) that should NOT be
*                         evaluated by Promptfoo before reaching the target.
* @returns The rendered prompt string
*/
async function renderPrompt(prompt, vars, nunjucksFilters, provider, skipRenderVars) {
	const nunjucks = getNunjucksEngine(nunjucksFilters);
	let basePrompt = prompt.raw;
	for (const [varName, value] of Object.entries(vars)) {
		if (skipRenderVars?.includes(varName)) continue;
		if (typeof value === "string" && value.startsWith("file://")) {
			const basePath = state.basePath || "";
			const filePath = path$1.resolve(process.cwd(), basePath, value.slice(7));
			const fileExtension = filePath.split(".").pop();
			logger.debug(`Loading var ${varName} from file: ${filePath}`);
			if (isJavascriptFile(filePath)) {
				const javascriptOutput = await (await importModule(filePath))(varName, basePrompt, vars, provider);
				if (javascriptOutput.error) throw new Error(`Error running ${filePath}: ${javascriptOutput.error}`);
				if (!javascriptOutput.output) throw new Error(`Expected ${filePath} to return { output: string } but got ${javascriptOutput}`);
				vars[varName] = javascriptOutput.output;
			} else if (fileExtension === "py") {
				const pythonScriptOutput = await runPython(filePath, "get_var", [
					varName,
					basePrompt,
					vars
				]);
				if (pythonScriptOutput.error) throw new Error(`Error running Python script ${filePath}: ${pythonScriptOutput.error}`);
				if (!pythonScriptOutput.output) throw new Error(`Python script ${filePath} did not return any output`);
				invariant(typeof pythonScriptOutput.output === "string", `pythonScriptOutput.output must be a string. Received: ${typeof pythonScriptOutput.output}`);
				vars[varName] = pythonScriptOutput.output.trim();
			} else if (fileExtension === "yaml" || fileExtension === "yml") vars[varName] = JSON.stringify(yaml.load(await fsPromises.readFile(filePath, "utf8")));
			else if (fileExtension === "pdf" && !getEnvBool("PROMPTFOO_DISABLE_PDF_AS_TEXT")) {
				telemetry.record("feature_used", { feature: "extract_text_from_pdf" });
				vars[varName] = await extractTextFromPDF(filePath);
			} else if ((isImageFile(filePath) || isVideoFile(filePath) || isAudioFile(filePath)) && !getEnvBool("PROMPTFOO_DISABLE_MULTIMEDIA_AS_BASE64")) {
				const fileType = isImageFile(filePath) ? "image" : isVideoFile(filePath) ? "video" : "audio";
				telemetry.record("feature_used", { feature: `load_${fileType}_as_base64` });
				logger.debug(`Loading ${fileType} as base64: ${filePath}`);
				try {
					const base64Data = (await fsPromises.readFile(filePath)).toString("base64");
					if (fileType === "image") {
						let mimeType = getMimeTypeFromExtension(path$1.extname(filePath));
						const extensionWasUnknown = !path$1.extname(filePath) || mimeType === "image/jpeg";
						const detectedType = detectMimeFromBase64(base64Data);
						if (detectedType) {
							if (detectedType !== mimeType) {
								logger.debug(`Magic number detection overriding extension-based MIME type: ${detectedType} (was ${mimeType}) for ${filePath}`);
								mimeType = detectedType;
							}
						} else if (extensionWasUnknown) logger.warn(`Could not detect image format for ${filePath}, defaulting to image/jpeg. Supported formats: JPEG, PNG, GIF, WebP, BMP, TIFF, ICO, AVIF, HEIC, SVG`);
						vars[varName] = `data:${mimeType};base64,${base64Data}`;
					} else vars[varName] = base64Data;
				} catch (error) {
					throw new Error(`Failed to load ${fileType} ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
				}
			} else vars[varName] = (await fsPromises.readFile(filePath, "utf8")).trim();
		} else if (isPackagePath(value)) {
			const requiredModule = await loadFromPackage(value, state.basePath || "");
			if (typeof requiredModule !== "function") throw new Error(`Variable source malformed: ${value} must export a function. Received: ${typeof requiredModule}`);
			const javascriptOutput = await requiredModule(varName, basePrompt, vars, provider);
			if (javascriptOutput.error) throw new Error(`Error running ${value}: ${javascriptOutput.error}`);
			if (!javascriptOutput.output) throw new Error(`Expected ${value} to return { output: string } but got ${javascriptOutput}`);
			vars[varName] = javascriptOutput.output;
		}
	}
	if (prompt.function) {
		const result = await prompt.function({
			vars,
			provider
		});
		if (typeof result === "string") basePrompt = result;
		else if (typeof result === "object") if ("prompt" in result) {
			basePrompt = typeof result.prompt === "string" ? result.prompt : JSON.stringify(result.prompt);
			if (result.config) prompt.config = {
				...prompt.config || {},
				...result.config
			};
		} else basePrompt = JSON.stringify(result);
		else throw new Error(`Prompt function must return a string or object, got ${typeof result}`);
	}
	for (const key of Object.keys(vars)) if (typeof vars[key] === "string" && !skipRenderVars?.includes(key)) vars[key] = vars[key].replace(/\n$/, "");
	const varsResolvedFromSkipped = /* @__PURE__ */ new Set();
	resolveVariables(vars, skipRenderVars, varsResolvedFromSkipped);
	if (prompt.raw.startsWith("portkey://")) {
		const portKeyResult = await getPrompt(prompt.raw.slice(10), vars);
		return JSON.stringify(portKeyResult.messages);
	} else if (prompt.raw.startsWith("langfuse://")) {
		const langfusePrompt = prompt.raw.slice(11);
		let helper;
		let version;
		let label;
		let promptType = "text";
		const labelMatch = langfusePrompt.match(/^(.+)@([^:@]+)(?::(.+))?$/);
		const versionMatch = langfusePrompt.match(/^([^:]+):([^:]+)(?::(.+))?$/);
		if (labelMatch) {
			helper = labelMatch[1];
			label = labelMatch[2];
			if (labelMatch[3]) promptType = labelMatch[3];
		} else if (versionMatch) {
			helper = versionMatch[1];
			const versionOrLabel = versionMatch[2];
			if (/^\d+$/.test(versionOrLabel)) version = versionOrLabel;
			else {
				label = versionOrLabel;
				if (label === "latest") version = void 0;
			}
			if (versionMatch[3]) promptType = versionMatch[3];
		} else helper = langfusePrompt;
		if (promptType !== "text" && promptType !== "chat") throw new Error(`Invalid Langfuse prompt type: ${promptType}. Must be 'text' or 'chat'.`);
		return await getPrompt$1(helper, vars, promptType, version === void 0 || version === "latest" ? void 0 : Number(version), label);
	} else if (prompt.raw.startsWith("helicone://")) {
		const [id, version] = prompt.raw.slice(11).split(":");
		const [majorVersion, minorVersion] = version ? version.split(".") : [void 0, void 0];
		return await getPrompt$2(id, vars, majorVersion === void 0 ? void 0 : Number(majorVersion), minorVersion === void 0 ? void 0 : Number(minorVersion));
	}
	try {
		if (getEnvBool("PROMPTFOO_DISABLE_JSON_AUTOESCAPE")) {
			basePrompt = autoWrapRawIfPartialNunjucks(basePrompt);
			return nunjucks.renderString(basePrompt, vars);
		}
		const parsed = JSON.parse(basePrompt);
		return JSON.stringify(renderVarsInObject(parsed, vars), null, 2);
	} catch {
		const renderedVars = Object.fromEntries(Object.entries(vars).map(([key, value]) => {
			if (typeof value !== "string" || skipRenderVars?.includes(key) || varsResolvedFromSkipped.has(key)) return [key, value];
			if (referencesUndefinedVariables(value, vars)) return [key, value];
			return [key, nunjucks.renderString(autoWrapRawIfPartialNunjucks(value), vars)];
		}));
		basePrompt = autoWrapRawIfPartialNunjucks(basePrompt);
		return nunjucks.renderString(basePrompt, renderedVars);
	}
}
/**
* Runs extension hooks for the given hook name and context. The hook will be called with the context object,
* and can update the context object to persist data into provider calls.
* @param extensions - An array of extension paths, or null.
* @param hookName - The name of the hook to run.
* @param context - The context object to pass to the hook. T depends on the type of the hook.
* @returns A Promise that resolves with one of the following:
*  - The original context object, if no extensions are provided OR if the returned context is not valid.
*  - The updated context object, if the extension hook returns a valid context object. The updated context,
*    if defined, must conform to the type T; otherwise, a validation error is thrown.
*/
/**
* Valid hook names that can be used to filter which hooks an extension runs for.
* If an extension specifies one of these as its function name (e.g., file://path:beforeAll),
* it will only run for that specific hook and use the NEW calling convention: (context, { hookName }).
* If an extension specifies a custom function name (e.g., file://path:myHandler),
* it will run for ALL hooks and use the LEGACY calling convention: (hookName, context).
*/
const EXTENSION_HOOK_NAMES = new Set([
	"beforeAll",
	"beforeEach",
	"afterEach",
	"afterAll"
]);
/**
* Extracts the hook name from an extension path.
* Format: file://path/to/file.js:hookName or file://path/to/file.py:hook_name
* @returns The hook name or undefined if not specified
*/
function getExtensionHookName(extension) {
	if (!extension.startsWith("file://")) return;
	const lastColonIndex = extension.lastIndexOf(":");
	if (lastColonIndex > 8) return extension.slice(lastColonIndex + 1) || void 0;
}
async function runExtensionHook(extensions, hookName, context) {
	if (!extensions || !Array.isArray(extensions) || extensions.length === 0) return context;
	telemetry.record("feature_used", { feature: "extension_hook" });
	logger.debug(`Running ${hookName} hook with ${extensions.length} extension(s)`);
	let updatedContext = { ...context };
	for (const extension of extensions) {
		invariant(typeof extension === "string", "extension must be a string");
		const extensionHookName = getExtensionHookName(extension);
		if (extensionHookName && EXTENSION_HOOK_NAMES.has(extensionHookName) && extensionHookName !== hookName) {
			logger.debug(`Skipping extension ${extension} for hook ${hookName} (extension targets ${extensionHookName} only)`);
			continue;
		}
		const useNewCallingConvention = extensionHookName && EXTENSION_HOOK_NAMES.has(extensionHookName);
		logger.debug(`Running extension ${extension} for hook ${hookName} (${useNewCallingConvention ? "new" : "legacy"} convention)`);
		let extensionReturnValue;
		try {
			if (useNewCallingConvention) extensionReturnValue = await transform(extension, updatedContext, { hookName }, false);
			else extensionReturnValue = await transform(extension, hookName, updatedContext, false);
		} catch (error) {
			const errorMessage = error instanceof Error ? error.message : String(error);
			const wrappedError = /* @__PURE__ */ new Error(`Extension hook "${hookName}" failed for ${extension}: ${errorMessage}`);
			wrappedError.cause = error;
			throw wrappedError;
		}
		if (extensionReturnValue) switch (hookName) {
			case "beforeAll":
				updatedContext = { suite: {
					...updatedContext.suite,
					prompts: extensionReturnValue.suite.prompts,
					providerPromptMap: extensionReturnValue.suite.providerPromptMap,
					tests: extensionReturnValue.suite.tests,
					scenarios: extensionReturnValue.suite.scenarios,
					defaultTest: extensionReturnValue.suite.defaultTest,
					nunjucksFilters: extensionReturnValue.suite.nunjucksFilters,
					derivedMetrics: extensionReturnValue.suite.derivedMetrics,
					redteam: extensionReturnValue.suite.redteam
				} };
				break;
			case "beforeEach":
				updatedContext = { test: extensionReturnValue.test };
				break;
			case "afterEach":
				if (extensionReturnValue.result) {
					const currentResult = updatedContext.result;
					const mergedResponse = currentResult.response && extensionReturnValue.result.response?.metadata ? {
						...currentResult.response,
						metadata: {
							...currentResult.response.metadata,
							...extensionReturnValue.result.response.metadata
						}
					} : currentResult.response;
					const validScores = filterFiniteScores(extensionReturnValue.result.namedScores || {});
					updatedContext = {
						test: updatedContext.test,
						result: {
							...currentResult,
							namedScores: {
								...currentResult.namedScores,
								...validScores
							},
							metadata: {
								...currentResult.metadata,
								...extensionReturnValue.result.metadata || {}
							},
							response: mergedResponse
						}
					};
				}
				break;
		}
	}
	return updatedContext;
}
//#endregion
export { filterFiniteScores as i, renderPrompt as n, runExtensionHook as r, collectFileMetadata as t };

//# sourceMappingURL=evaluatorHelpers-Jx0MTlXG.js.map