"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.writeResultsToDatabase = writeResultsToDatabase;
exports.readResult = readResult;
exports.fetchEvalResultsFileFromDb = fetchEvalResultsFileFromDb;
exports.updateResult = updateResult;
exports.getLatestEval = getLatestEval;
exports.getPromptsWithPredicate = getPromptsWithPredicate;
exports.getPromptsForTestCasesHash = getPromptsForTestCasesHash;
exports.getTestCasesWithPredicate = getTestCasesWithPredicate;
exports.getPrompts = getPrompts;
exports.getTestCases = getTestCases;
exports.getPromptFromHash = getPromptFromHash;
exports.getDatasetFromHash = getDatasetFromHash;
exports.getEvalsWithPredicate = getEvalsWithPredicate;
exports.getEvals = getEvals;
exports.getEvalFromId = getEvalFromId;
exports.deleteEval = deleteEval;
exports.deleteAllEvals = deleteAllEvals;
exports.getStandaloneEvals = getStandaloneEvals;
const crypto_1 = require("crypto");
const drizzle_orm_1 = require("drizzle-orm");
const node_cache_1 = __importDefault(require("node-cache"));
const database_1 = require("../database");
const tables_1 = require("../database/tables");
const accounts_1 = require("../globalConfig/accounts");
const logger_1 = __importDefault(require("../logger"));
const eval_1 = __importStar(require("../models/eval"));
const evalResult_1 = __importDefault(require("../models/evalResult"));
const prompt_1 = require("../models/prompt");
const invariant_1 = __importDefault(require("../util/invariant"));
const createHash_1 = require("./createHash");
const DEFAULT_QUERY_LIMIT = 100;
async function writeResultsToDatabase(results, config, createdAt = new Date()) {
    createdAt = createdAt || (results.timestamp ? new Date(results.timestamp) : new Date());
    const evalId = (0, eval_1.createEvalId)(createdAt);
    const db = (0, database_1.getDb)();
    const promises = [];
    promises.push(db
        .insert(tables_1.evalsTable)
        .values({
        id: evalId,
        createdAt,
        author: (0, accounts_1.getAuthor)(),
        description: config.description,
        config,
        results,
    })
        .onConflictDoNothing()
        .execute());
    logger_1.default.debug(`Inserting eval ${evalId}`);
    // Record prompt relation
    (0, invariant_1.default)(results.table, 'Table is required');
    for (const prompt of results.table.head.prompts) {
        const label = prompt.label || prompt.display || prompt.raw;
        const promptId = (0, prompt_1.generateIdFromPrompt)(prompt);
        promises.push(db
            .insert(tables_1.promptsTable)
            .values({
            id: promptId,
            prompt: label,
            projectId: config.projectId ?? 'default',
            workspaceId: config.workspaceId ?? 'default',
        })
            .onConflictDoNothing()
            .execute());
        promises.push(db
            .insert(tables_1.evalsToPromptsTable)
            .values({
            evalId,
            promptId,
        })
            .onConflictDoNothing()
            .execute());
        logger_1.default.debug(`Inserting prompt ${promptId}`);
    }
    // Record dataset relation
    const datasetId = (0, createHash_1.sha256)(JSON.stringify(config.tests || []));
    const testsForStorage = Array.isArray(config.tests) ? config.tests : [];
    // Log when non-array tests are converted to empty array for database storage
    if (config.tests && !Array.isArray(config.tests)) {
        const testsType = typeof config.tests;
        const hasPath = typeof config.tests === 'object' && config.tests !== null && 'path' in config.tests;
        logger_1.default.debug(`Converting non-array test configuration to empty array for database storage. Type: ${testsType}, hasPath: ${hasPath}`);
    }
    promises.push(db
        .insert(tables_1.datasetsTable)
        .values({
        id: datasetId,
        tests: testsForStorage,
    })
        .onConflictDoNothing()
        .execute());
    promises.push(db
        .insert(tables_1.evalsToDatasetsTable)
        .values({
        evalId,
        datasetId,
    })
        .onConflictDoNothing()
        .execute());
    logger_1.default.debug(`Inserting dataset ${datasetId}`);
    // Record tags
    if (config.tags) {
        const tags = config.tags;
        for (const [tagKey, tagValue] of Object.entries(tags)) {
            const tagId = (0, createHash_1.sha256)(`${tagKey}:${tagValue}`);
            promises.push(db
                .insert(tables_1.tagsTable)
                .values({
                id: tagId,
                name: tagKey,
                value: tagValue,
            })
                .onConflictDoNothing()
                .execute());
            promises.push(db
                .insert(tables_1.evalsToTagsTable)
                .values({
                evalId,
                tagId,
            })
                .onConflictDoNothing()
                .execute());
            logger_1.default.debug(`Inserting tag ${tagId}`);
        }
    }
    logger_1.default.debug(`Awaiting ${promises.length} promises to database...`);
    await Promise.all(promises);
    return evalId;
}
async function readResult(id, options) {
    try {
        const eval_ = await eval_1.default.findById(id);
        (0, invariant_1.default)(eval_, `Eval with ID ${id} not found.`);
        return {
            id,
            result: await eval_.toResultsFile(options),
            createdAt: new Date(eval_.createdAt),
        };
    }
    catch (err) {
        logger_1.default.error(`Failed to read result with ID ${id} from database:\n${err}`);
    }
}
/**
 * Same shape as {@link readResult}, but loads normalized rows from `eval_results`
 * via {@link EvalResult.findManyByEvalId} instead of {@link Eval.findById}.
 * Legacy evals (embedded `results.table` on the `evals` row) still use that JSON.
 */
async function fetchEvalResultsFileFromDb(evalId, options) {
    try {
        const db = (0, database_1.getDb)();
        const evalRows = await db
            .select({
            id: tables_1.evalsTable.id,
            createdAt: tables_1.evalsTable.createdAt,
            author: tables_1.evalsTable.author,
            config: (0, drizzle_orm_1.sql) `${tables_1.evalsTable.config} -> 'redteam'`,
            prompts: tables_1.evalsTable.prompts,
            vars: tables_1.evalsTable.vars,
        })
            .from(tables_1.evalsTable)
            .where((0, drizzle_orm_1.eq)(tables_1.evalsTable.id, evalId))
            .limit(1);
        if (evalRows.length === 0) {
            return undefined;
        }
        const [datasetRow] = await db
            .select({ datasetId: tables_1.evalsToDatasetsTable.datasetId })
            .from(tables_1.evalsToDatasetsTable)
            .where((0, drizzle_orm_1.eq)(tables_1.evalsToDatasetsTable.evalId, evalId))
            .limit(1);
        const row = evalRows[0];
        const evalInstance = new eval_1.default(row.config, {
            id: row.id,
            createdAt: new Date(row.createdAt),
            author: row.author || undefined,
            prompts: row.prompts || [],
            datasetId: datasetRow?.datasetId,
            persisted: true,
            vars: row.vars || [],
        });
        if (!evalInstance.useOldResults()) {
            evalInstance.results = await evalResult_1.default.findManyByEvalId(evalId, {
                excludePromptResponse: options?.excludePromptResponse,
            });
            evalInstance._resultsLoaded = true;
        }
        const result = await evalInstance.toResultsFile(options);
        return {
            id: evalId,
            result,
            createdAt: new Date(row.createdAt),
        };
    }
    catch (err) {
        logger_1.default.error(`Failed to fetch eval results file from DB for ${evalId}:\n${err}`);
    }
}
async function updateResult(id, newConfig, newTable) {
    try {
        // Fetch the existing eval data from the database
        const existingEval = await eval_1.default.findById(id);
        if (!existingEval) {
            logger_1.default.error(`Eval with ID ${id} not found.`);
            return;
        }
        if (newConfig) {
            existingEval.config = newConfig;
        }
        if (newTable) {
            existingEval.setTable(newTable);
        }
        await existingEval.save();
        logger_1.default.info(`Updated eval with ID ${id}`);
    }
    catch (err) {
        logger_1.default.error(`Failed to update eval with ID ${id}:\n${err}`);
    }
}
async function getLatestEval(filterDescription) {
    const eval_ = await eval_1.default.latest();
    return await eval_?.toResultsFile();
}
async function getPromptsWithPredicate(predicate, limit) {
    // TODO(ian): Make this use a proper database query
    const evals_ = await eval_1.default.getMany(limit);
    const groupedPrompts = {};
    for (const eval_ of evals_) {
        const createdAt = new Date(eval_.createdAt).toISOString();
        const resultWrapper = await eval_.toResultsFile();
        if (predicate(resultWrapper)) {
            for (const prompt of eval_.getPrompts()) {
                const promptId = (0, createHash_1.sha256)(prompt.raw);
                const datasetId = resultWrapper.config.tests
                    ? (0, createHash_1.sha256)(JSON.stringify(resultWrapper.config.tests))
                    : '-';
                if (promptId in groupedPrompts) {
                    groupedPrompts[promptId].recentEvalDate = new Date(Math.max(groupedPrompts[promptId].recentEvalDate.getTime(), new Date(createdAt).getTime()));
                    groupedPrompts[promptId].count += 1;
                    groupedPrompts[promptId].evals.push({
                        id: eval_.id,
                        datasetId,
                        metrics: prompt.metrics,
                    });
                }
                else {
                    groupedPrompts[promptId] = {
                        count: 1,
                        id: promptId,
                        prompt,
                        recentEvalDate: new Date(createdAt),
                        recentEvalId: eval_.id,
                        evals: [
                            {
                                id: eval_.id,
                                datasetId,
                                metrics: prompt.metrics,
                            },
                        ],
                    };
                }
            }
        }
    }
    return Object.values(groupedPrompts);
}
function getPromptsForTestCasesHash(testCasesSha256, limit = DEFAULT_QUERY_LIMIT) {
    return getPromptsWithPredicate((result) => {
        const testsJson = JSON.stringify(result.config.tests);
        const hash = (0, createHash_1.sha256)(testsJson);
        return hash === testCasesSha256;
    }, limit);
}
async function getTestCasesWithPredicate(predicate, limit) {
    const evals_ = await eval_1.default.getMany(limit);
    const groupedTestCases = {};
    for (const eval_ of evals_) {
        const createdAt = new Date(eval_.createdAt).toISOString();
        const resultWrapper = await eval_.toResultsFile();
        const testCases = resultWrapper.config.tests;
        if (testCases && predicate(resultWrapper)) {
            const evalId = eval_.id;
            // For database storage, we need to handle the union type properly
            // Only store actual test case arrays, not generator configs
            let storableTestCases;
            if (typeof testCases === 'string') {
                storableTestCases = testCases;
            }
            else if (Array.isArray(testCases)) {
                storableTestCases = testCases;
            }
            else {
                // If it's a TestGeneratorConfig object, we can't store it directly
                // This case should be rare as the database typically stores resolved tests
                logger_1.default.warn('Skipping TestGeneratorConfig object in database storage');
                continue;
            }
            const datasetId = (0, createHash_1.sha256)(JSON.stringify(storableTestCases));
            if (datasetId in groupedTestCases) {
                groupedTestCases[datasetId].recentEvalDate = new Date(Math.max(groupedTestCases[datasetId].recentEvalDate.getTime(), eval_.createdAt));
                groupedTestCases[datasetId].count += 1;
                const newPrompts = eval_.getPrompts().map((prompt) => ({
                    id: (0, createHash_1.sha256)(prompt.raw),
                    prompt,
                    evalId,
                }));
                const promptsById = {};
                for (const prompt of groupedTestCases[datasetId].prompts.concat(newPrompts)) {
                    if (!(prompt.id in promptsById)) {
                        promptsById[prompt.id] = prompt;
                    }
                }
                groupedTestCases[datasetId].prompts = Object.values(promptsById);
            }
            else {
                const newPrompts = eval_.getPrompts().map((prompt) => ({
                    id: (0, createHash_1.sha256)(prompt.raw),
                    prompt,
                    evalId,
                }));
                const promptsById = {};
                for (const prompt of newPrompts) {
                    if (!(prompt.id in promptsById)) {
                        promptsById[prompt.id] = prompt;
                    }
                }
                groupedTestCases[datasetId] = {
                    id: datasetId,
                    count: 1,
                    testCases: storableTestCases,
                    recentEvalDate: new Date(createdAt),
                    recentEvalId: evalId,
                    prompts: Object.values(promptsById),
                };
            }
        }
    }
    return Object.values(groupedTestCases);
}
function getPrompts(limit = DEFAULT_QUERY_LIMIT) {
    return getPromptsWithPredicate(() => true, limit);
}
async function getTestCases(limit = DEFAULT_QUERY_LIMIT) {
    return getTestCasesWithPredicate(() => true, limit);
}
async function getPromptFromHash(hash) {
    const prompts = await getPrompts();
    for (const prompt of prompts) {
        if (prompt.id.startsWith(hash)) {
            return prompt;
        }
    }
    return undefined;
}
async function getDatasetFromHash(hash) {
    const datasets = await getTestCases();
    for (const dataset of datasets) {
        if (dataset.id.startsWith(hash)) {
            return dataset;
        }
    }
    return undefined;
}
async function getEvalsWithPredicate(predicate, limit) {
    const db = (0, database_1.getDb)();
    const evals_ = await db
        .select({
        id: tables_1.evalsTable.id,
        createdAt: tables_1.evalsTable.createdAt,
        author: tables_1.evalsTable.author,
        results: tables_1.evalsTable.results,
        config: tables_1.evalsTable.config,
        description: tables_1.evalsTable.description,
    })
        .from(tables_1.evalsTable)
        .orderBy((0, drizzle_orm_1.desc)(tables_1.evalsTable.createdAt))
        .limit(limit)
        .execute();
    const ret = [];
    for (const eval_ of evals_) {
        const createdAt = new Date(eval_.createdAt).toISOString();
        const resultWrapper = {
            version: 3,
            createdAt,
            author: eval_.author,
            // @ts-ignore
            results: eval_.results,
            config: eval_.config,
        };
        if (predicate(resultWrapper)) {
            const evalId = eval_.id;
            ret.push({
                id: evalId,
                date: new Date(eval_.createdAt),
                config: eval_.config,
                // @ts-ignore
                results: eval_.results,
                description: eval_.description || undefined,
            });
        }
    }
    return ret;
}
async function getEvals(limit = DEFAULT_QUERY_LIMIT) {
    return getEvalsWithPredicate(() => true, limit);
}
async function getEvalFromId(hash) {
    const evals_ = await getEvals();
    for (const eval_ of evals_) {
        if (eval_.id.startsWith(hash)) {
            return eval_;
        }
    }
    return undefined;
}
async function deleteEval(evalId) {
    const db = (0, database_1.getDb)();
    await db.transaction(async (tx) => {
        await tx.delete(tables_1.evalsToPromptsTable).where((0, drizzle_orm_1.eq)(tables_1.evalsToPromptsTable.evalId, evalId)).execute();
        await tx.delete(tables_1.evalsToDatasetsTable).where((0, drizzle_orm_1.eq)(tables_1.evalsToDatasetsTable.evalId, evalId)).execute();
        await tx.delete(tables_1.evalsToTagsTable).where((0, drizzle_orm_1.eq)(tables_1.evalsToTagsTable.evalId, evalId)).execute();
        await tx.delete(tables_1.evalResultsTable).where((0, drizzle_orm_1.eq)(tables_1.evalResultsTable.evalId, evalId)).execute();
        // Finally, delete the eval record
        const deletedIds = await tx.delete(tables_1.evalsTable).where((0, drizzle_orm_1.eq)(tables_1.evalsTable.id, evalId)).execute();
        if (!deletedIds || (Array.isArray(deletedIds) && deletedIds.length === 0)) {
            throw new Error(`Eval with ID ${evalId} not found`);
        }
    });
}
/**
 * Deletes all evaluations and related records with foreign keys from the database.
 * @async
 * @returns {Promise<void>}
 */
async function deleteAllEvals() {
    const db = (0, database_1.getDb)();
    await db.transaction(async (tx) => {
        await tx.delete(tables_1.evalResultsTable).execute();
        await tx.delete(tables_1.evalsToPromptsTable).execute();
        await tx.delete(tables_1.evalsToDatasetsTable).execute();
        await tx.delete(tables_1.evalsToTagsTable).execute();
        await tx.delete(tables_1.evalsTable).execute();
    });
}
const standaloneEvalCache = new node_cache_1.default({ stdTTL: 60 * 60 * 2 }); // Cache for 2 hours
async function getStandaloneEvals({ limit = DEFAULT_QUERY_LIMIT, tag, description, } = {}) {
    const cacheKey = `standalone_evals_${limit}_${tag?.key}_${tag?.value}`;
    const cachedResult = standaloneEvalCache.get(cacheKey);
    if (cachedResult) {
        return cachedResult;
    }
    const db = (0, database_1.getDb)();
    const results = db
        .select({
        evalId: tables_1.evalsTable.id,
        description: tables_1.evalsTable.description,
        results: tables_1.evalsTable.results,
        createdAt: tables_1.evalsTable.createdAt,
        promptId: tables_1.evalsToPromptsTable.promptId,
        datasetId: tables_1.evalsToDatasetsTable.datasetId,
        tagName: tables_1.tagsTable.name,
        tagValue: tables_1.tagsTable.value,
        isRedteam: (0, drizzle_orm_1.sql) `json_extract(evals.config, '$.redteam') IS NOT NULL`.as('isRedteam'),
    })
        .from(tables_1.evalsTable)
        .leftJoin(tables_1.evalsToPromptsTable, (0, drizzle_orm_1.eq)(tables_1.evalsTable.id, tables_1.evalsToPromptsTable.evalId))
        .leftJoin(tables_1.evalsToDatasetsTable, (0, drizzle_orm_1.eq)(tables_1.evalsTable.id, tables_1.evalsToDatasetsTable.evalId))
        .leftJoin(tables_1.evalsToTagsTable, (0, drizzle_orm_1.eq)(tables_1.evalsTable.id, tables_1.evalsToTagsTable.evalId))
        .leftJoin(tables_1.tagsTable, (0, drizzle_orm_1.eq)(tables_1.evalsToTagsTable.tagId, tables_1.tagsTable.id))
        .where((0, drizzle_orm_1.and)(tag ? (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(tables_1.tagsTable.name, tag.key), (0, drizzle_orm_1.eq)(tables_1.tagsTable.value, tag.value)) : undefined, description ? (0, drizzle_orm_1.eq)(tables_1.evalsTable.description, description) : undefined))
        .orderBy((0, drizzle_orm_1.desc)(tables_1.evalsTable.createdAt))
        .limit(limit)
        .execute();
    // TODO(Performance): Load all necessary data in one go rather than re-requesting each eval!
    const standaloneEvals = (await Promise.all(results?.map(async (result) => {
        const { description, createdAt, evalId, promptId, datasetId, 
        // @ts-ignore
        isRedteam, } = result;
        const eval_ = await eval_1.default.findById(evalId);
        (0, invariant_1.default)(eval_, `Eval with ID ${evalId} not found`);
        const table = (await eval_.getTable()) || { body: [] };
        // @ts-ignore
        return eval_.getPrompts().map((col, index) => {
            // Compute some stats
            const pluginCounts = table.body.reduce(
            // @ts-ignore
            (acc, row) => {
                const pluginId = row.test.metadata?.pluginId;
                if (pluginId) {
                    const isPass = row.outputs[index].pass;
                    acc.pluginPassCount[pluginId] =
                        (acc.pluginPassCount[pluginId] || 0) + (isPass ? 1 : 0);
                    acc.pluginFailCount[pluginId] =
                        (acc.pluginFailCount[pluginId] || 0) + (isPass ? 0 : 1);
                }
                return acc;
            }, { pluginPassCount: {}, pluginFailCount: {} });
            return {
                evalId,
                description,
                promptId,
                datasetId,
                createdAt,
                isRedteam: isRedteam,
                ...pluginCounts,
                ...col,
            };
        });
    }))).flat();
    // Ensure each row has a UUID as the `id` and `evalId` properties are not unique!
    const withUUIDs = standaloneEvals.map((eval_) => ({
        ...eval_,
        uuid: (0, crypto_1.randomUUID)(),
    }));
    standaloneEvalCache.set(cacheKey, withUUIDs);
    return withUUIDs;
}
//# sourceMappingURL=database.js.map