197 lines
6.9 KiB
TypeScript
197 lines
6.9 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { createServerSupabaseClient } from "@/lib/supabase";
|
|
import { generateEmbedding } from "@/lib/embeddings";
|
|
import fs from "fs/promises";
|
|
import path from "path";
|
|
|
|
interface TestResult {
|
|
fileName: string;
|
|
status: "success" | "failed";
|
|
error?: string;
|
|
candidateId?: string;
|
|
candidateName?: string;
|
|
evaluation?: unknown;
|
|
}
|
|
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
const supabase = createServerSupabaseClient();
|
|
const origin = request.nextUrl.origin;
|
|
const bulkMode = request.nextUrl.searchParams.get("bulk") === "true";
|
|
|
|
// 1. Fetch or create a Job Vacancy for testing
|
|
let job = null;
|
|
const { data: existingJobs, error: jobsFetchError } = await supabase
|
|
.from("jobs")
|
|
.select("*")
|
|
.limit(1);
|
|
|
|
if (jobsFetchError) {
|
|
return NextResponse.json({ error: `Failed to fetch jobs: ${jobsFetchError.message}` }, { status: 500 });
|
|
}
|
|
|
|
if (existingJobs && existingJobs.length > 0) {
|
|
job = existingJobs[0];
|
|
} else {
|
|
// Create a default job if none exist
|
|
const title = "Senior AI Research Engineer";
|
|
const requirementsText = "We are seeking a Senior AI Research Engineer with expert knowledge in Large Language Models, PyTorch, LangChain, and agentic reasoning architectures.";
|
|
const embedding = await generateEmbedding(requirementsText);
|
|
|
|
const { data: newJob, error: jobInsertError } = await supabase
|
|
.from("jobs")
|
|
.insert({
|
|
title,
|
|
requirements: { text: requirementsText },
|
|
embedding,
|
|
})
|
|
.select("*")
|
|
.single();
|
|
|
|
if (jobInsertError || !newJob) {
|
|
return NextResponse.json({ error: `Failed to create mock job: ${jobInsertError?.message}` }, { status: 500 });
|
|
}
|
|
job = newJob;
|
|
}
|
|
|
|
// 2. Identify files to test
|
|
const testAssetsDir = path.join(process.cwd(), "test-assets");
|
|
let filesToTest: string[] = [];
|
|
|
|
try {
|
|
const files = await fs.readdir(testAssetsDir);
|
|
filesToTest = files.filter(f => f.toLowerCase().endsWith(".pdf"));
|
|
} catch (err) {
|
|
return NextResponse.json({ error: `Failed to read test-assets folder: ${err instanceof Error ? err.message : err}` }, { status: 500 });
|
|
}
|
|
|
|
if (filesToTest.length === 0) {
|
|
return NextResponse.json({ error: "No PDF CV assets found in test-assets folder." }, { status: 400 });
|
|
}
|
|
|
|
// If not bulk mode, limit to just the default single file
|
|
if (!bulkMode) {
|
|
const defaultFile = "curriculum-vitae-english.pdf";
|
|
if (filesToTest.includes(defaultFile)) {
|
|
filesToTest = [defaultFile];
|
|
} else {
|
|
filesToTest = [filesToTest[0]]; // Fallback to first available file
|
|
}
|
|
}
|
|
|
|
const results: TestResult[] = [];
|
|
|
|
// 3. Process test files sequentially
|
|
for (const fileName of filesToTest) {
|
|
const cvPath = path.join(testAssetsDir, fileName);
|
|
let fileBuffer: Buffer;
|
|
let candidateId: string | null = null;
|
|
|
|
try {
|
|
fileBuffer = await fs.readFile(cvPath);
|
|
} catch (fsError) {
|
|
results.push({
|
|
fileName,
|
|
status: "failed",
|
|
error: `Could not read file: ${fsError instanceof Error ? fsError.message : fsError}`,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
// Step A: Parse CV (creates candidate record in DB)
|
|
const formData = new FormData();
|
|
const fileBlob = new Blob([new Uint8Array(fileBuffer)], { type: "application/pdf" });
|
|
formData.append("file", fileBlob, fileName);
|
|
formData.append("isTest", "false"); // Must insert in DB so evaluate API can read it
|
|
|
|
const parseCvUrl = `${origin}/candidates/api/parse-cv`;
|
|
const parseResponse = await fetch(parseCvUrl, {
|
|
method: "POST",
|
|
body: formData,
|
|
});
|
|
|
|
if (!parseResponse.ok) {
|
|
const errText = await parseResponse.text();
|
|
throw new Error(`Parse-CV API error: ${parseResponse.status} - ${errText}`);
|
|
}
|
|
|
|
const parseResult = await parseResponse.json();
|
|
candidateId = parseResult.candidateId;
|
|
const candidateName = parseResult.candidateName;
|
|
|
|
if (!candidateId || candidateId === "00000000-0000-0000-0000-000000000000") {
|
|
throw new Error("Parse-CV API returned invalid candidateId");
|
|
}
|
|
|
|
// Step B: Run AI Evaluation (triggers n8n evaluation with isTest: true)
|
|
const evaluateUrl = `${origin}/candidates/api/evaluate`;
|
|
const evaluateResponse = await fetch(evaluateUrl, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
candidateId,
|
|
jobId: job.id,
|
|
isTest: true, // In-memory evaluation to avoid inserting test score in Supabase
|
|
}),
|
|
});
|
|
|
|
if (!evaluateResponse.ok) {
|
|
const errText = await evaluateResponse.text();
|
|
throw new Error(`Evaluate API error: ${evaluateResponse.status} - ${errText}`);
|
|
}
|
|
|
|
const evalResult = await evaluateResponse.json();
|
|
const n8nResponse = evalResult.n8nResponse;
|
|
|
|
// Step C: Verify n8n output structure
|
|
const hasEvaluation = n8nResponse && typeof n8nResponse === "object" && "evaluation" in n8nResponse;
|
|
const hasAiScore = n8nResponse && typeof n8nResponse === "object" && "ai_score" in n8nResponse;
|
|
|
|
if (hasEvaluation && hasAiScore) {
|
|
results.push({
|
|
fileName,
|
|
status: "success",
|
|
candidateId,
|
|
candidateName,
|
|
evaluation: n8nResponse,
|
|
});
|
|
} else {
|
|
throw new Error(`Invalid response structure from n8n: ${JSON.stringify(n8nResponse)}`);
|
|
}
|
|
} catch (err: unknown) {
|
|
results.push({
|
|
fileName,
|
|
status: "failed",
|
|
error: err instanceof Error ? err.message : String(err),
|
|
candidateId: candidateId || undefined,
|
|
});
|
|
} finally {
|
|
// Step D: Cleanup candidate from database (cascades to scores/interviews)
|
|
if (candidateId && candidateId !== "00000000-0000-0000-0000-000000000000") {
|
|
console.log(`Cleaning up test candidate: ${candidateId}`);
|
|
await supabase.from("candidates").delete().eq("id", candidateId);
|
|
}
|
|
}
|
|
}
|
|
|
|
const overallSuccess = results.every(r => r.status === "success");
|
|
|
|
return NextResponse.json({
|
|
status: overallSuccess ? "success" : "partial_or_full_failure",
|
|
bulkMode,
|
|
totalCount: results.length,
|
|
successCount: results.filter(r => r.status === "success").length,
|
|
jobUsed: {
|
|
id: job.id,
|
|
title: job.title,
|
|
},
|
|
results,
|
|
});
|
|
|
|
} catch (error: unknown) {
|
|
console.error("Error in webhook-test API:", error);
|
|
const errorMessage = error instanceof Error ? error.message : "Internal Server Error";
|
|
return NextResponse.json({ error: errorMessage }, { status: 500 });
|
|
}
|
|
}
|