145 lines
4.9 KiB
TypeScript
145 lines
4.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";
|
|
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
const supabase = createServerSupabaseClient();
|
|
const origin = request.nextUrl.origin;
|
|
|
|
// 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. Read the Curriculum Vitae test asset
|
|
const cvPath = path.join(process.cwd(), "test-assets", "curriculum-vitae-english.pdf");
|
|
let fileBuffer;
|
|
try {
|
|
fileBuffer = await fs.readFile(cvPath);
|
|
} catch (fsError) {
|
|
return NextResponse.json({
|
|
error: `Could not read test CV asset at ${cvPath}. Please ensure it exists. Detailed error: ${fsError instanceof Error ? fsError.message : fsError}`
|
|
}, { status: 400 });
|
|
}
|
|
|
|
const testMode = request.nextUrl.searchParams.get("testMode") !== "false";
|
|
|
|
// 3. Prepare Form Data for parse-cv endpoint
|
|
const formData = new FormData();
|
|
const fileBlob = new Blob([fileBuffer], { type: "application/pdf" });
|
|
formData.append("file", fileBlob, "curriculum-vitae-english.pdf");
|
|
formData.append("jobId", job.id);
|
|
formData.append("isTest", testMode ? "true" : "false");
|
|
|
|
// 4. Send POST request to local parse-cv API
|
|
const parseCvUrl = `${origin}/candidates/api/parse-cv`;
|
|
let parseResult;
|
|
try {
|
|
const parseResponse = await fetch(parseCvUrl, {
|
|
method: "POST",
|
|
body: formData,
|
|
});
|
|
|
|
if (!parseResponse.ok) {
|
|
const errText = await parseResponse.text();
|
|
return NextResponse.json({
|
|
error: `Parse-CV API returned non-OK status: ${parseResponse.status} - ${errText}`
|
|
}, { status: 500 });
|
|
}
|
|
|
|
parseResult = await parseResponse.json();
|
|
} catch (fetchError) {
|
|
return NextResponse.json({
|
|
error: `Failed to call local parse-cv route: ${fetchError instanceof Error ? fetchError.message : fetchError}`
|
|
}, { status: 500 });
|
|
}
|
|
|
|
const { candidateId, interviewId, n8nResponse } = parseResult;
|
|
|
|
// 5. Verification - Poll the Supabase `scores` table to verify n8n updated the DB
|
|
let scoreRecord = null;
|
|
let verificationAttempts = 0;
|
|
const maxAttempts = 10;
|
|
const delayMs = 1500;
|
|
|
|
for (let i = 0; i < maxAttempts; i++) {
|
|
verificationAttempts++;
|
|
// Wait for n8n execution to finish and write back
|
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
|
|
const { data: score, error: scoreError } = await supabase
|
|
.from("scores")
|
|
.select("*")
|
|
.eq("candidate_id", candidateId)
|
|
.eq("interview_id", interviewId)
|
|
.maybeSingle();
|
|
|
|
if (score && !scoreError) {
|
|
scoreRecord = score;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return NextResponse.json({
|
|
status: scoreRecord ? "success" : "completed_with_pending_evaluation",
|
|
message: scoreRecord
|
|
? "Pipeline test executed and verified successfully!"
|
|
: "Pipeline executed but AI evaluation write-back is pending or failed.",
|
|
testDetails: {
|
|
jobUsed: {
|
|
id: job.id,
|
|
title: job.title,
|
|
status: existingJobs && existingJobs.length > 0 ? "reused" : "created",
|
|
},
|
|
parseCvResponse: {
|
|
candidateId,
|
|
interviewId,
|
|
n8nWebhookResponse: n8nResponse,
|
|
},
|
|
verification: {
|
|
attempts: verificationAttempts,
|
|
verified: !!scoreRecord,
|
|
scoreData: scoreRecord,
|
|
}
|
|
}
|
|
});
|
|
|
|
} 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 });
|
|
}
|
|
}
|