From 6a1fe0925abcfc01b9a0d473338d4ce9f3598305 Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 18 Jul 2024 21:09:39 +1000 Subject: [PATCH] feat: added a candidates api that matches resumes to jobs (#131) --- apps/registry/app/[username]/jobs/JobList.js | 10 +++ apps/registry/app/jobs/[uuid]/layout.js | 5 ++ apps/registry/app/jobs/[uuid]/page.js | 7 ++ apps/registry/pages/api/candidates.js | 27 +++++++ apps/registry/scripts/jobs/jobs-embeddings.js | 74 +++++++++++++++++++ 5 files changed, 123 insertions(+) create mode 100644 apps/registry/app/jobs/[uuid]/layout.js create mode 100644 apps/registry/app/jobs/[uuid]/page.js create mode 100644 apps/registry/pages/api/candidates.js create mode 100644 apps/registry/scripts/jobs/jobs-embeddings.js diff --git a/apps/registry/app/[username]/jobs/JobList.js b/apps/registry/app/[username]/jobs/JobList.js index cff97bc..16305db 100644 --- a/apps/registry/app/[username]/jobs/JobList.js +++ b/apps/registry/app/[username]/jobs/JobList.js @@ -9,6 +9,7 @@ import { CheckCircle, Star, } from 'lucide-react'; +import Link from 'next/link'; const JobDescription = ({ job, makeCoverletter }) => { const [expanded, setExpanded] = useState(false); @@ -155,6 +156,15 @@ const JobDescription = ({ job, makeCoverletter }) => { > View Original Job + + + View Job Candiates + )} diff --git a/apps/registry/app/jobs/[uuid]/layout.js b/apps/registry/app/jobs/[uuid]/layout.js new file mode 100644 index 0000000..4b7c43b --- /dev/null +++ b/apps/registry/app/jobs/[uuid]/layout.js @@ -0,0 +1,5 @@ +'use client'; + +export default function Home({ children }) { + return <>{children}; +} diff --git a/apps/registry/app/jobs/[uuid]/page.js b/apps/registry/app/jobs/[uuid]/page.js new file mode 100644 index 0000000..4db98f2 --- /dev/null +++ b/apps/registry/app/jobs/[uuid]/page.js @@ -0,0 +1,7 @@ +import React from 'react'; + +const JobList = () => { + return
List of jobs
; +}; + +export default JobList; diff --git a/apps/registry/pages/api/candidates.js b/apps/registry/pages/api/candidates.js new file mode 100644 index 0000000..7501496 --- /dev/null +++ b/apps/registry/pages/api/candidates.js @@ -0,0 +1,27 @@ +const { createClient } = require('@supabase/supabase-js'); + +const supabaseUrl = 'https://itxuhvvwryeuzuyihpkp.supabase.co'; +const supabaseKey = process.env.SUPABASE_KEY; +const supabase = createClient(supabaseUrl, supabaseKey); + +if (!process.env.OPENAI_API_KEY) { + throw new Error('Missing env var from OpenAI'); +} + +// This API route is used to match candidates to a job + +export default async function handler(req, res) { + const jobId = 40224903; + + const { data } = await supabase.from('jobs').select().eq('uuid', jobId); + + const jdEmbedding = data[0].embedding_v5; + + const { data: documents } = await supabase.rpc('match_resumes_v5', { + query_embedding: jdEmbedding, + match_threshold: 0.14, // Choose an appropriate threshold for your data + match_count: 40, // Choose the number of matches + }); + + return res.status(200).send(documents); +} diff --git a/apps/registry/scripts/jobs/jobs-embeddings.js b/apps/registry/scripts/jobs/jobs-embeddings.js new file mode 100644 index 0000000..857f64a --- /dev/null +++ b/apps/registry/scripts/jobs/jobs-embeddings.js @@ -0,0 +1,74 @@ +require('dotenv').config({ path: __dirname + '/./../../.env' }); + +const { createClient } = require('@supabase/supabase-js'); +const OpenAI = require('openai'); + +const supabaseUrl = 'https://itxuhvvwryeuzuyihpkp.supabase.co'; +const supabaseKey = process.env.SUPABASE_KEY; +const supabase = createClient(supabaseUrl, supabaseKey); +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +async function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function main() { + const { data, error } = await supabase + .from('resumes') + .select('id::text, username, resume, embedding') + .is('embedding', null); + // .limit(300); + console.log({ data, error }); + + for (let i = 0; i < data.length; i++) { + const resume = data[i]; + if (!resume.embedding) { + console.log('Create embedding for'); + console.log('Username:', resume.username); + console.log('ID:', resume.id); + + const completion1 = await openai.embeddings.create({ + model: 'text-embedding-3-large', + input: resume.resume.substr(0, 8192), + }); + console.log('Embedding done'); + + const desiredLength = 3072; + + let embedding = completion1.data[0].embedding; + + if (embedding.length < desiredLength) { + embedding = embedding.concat( + Array(desiredLength - embedding.length).fill(0) + ); + } + + console.log('Embedding to be saved:', embedding); + + try { + console.log('Saving embedding for ID:', resume.id); + + const updateResponse = await supabase + .from('resumes') + .update({ embedding }) + .eq('id', resume.id); + + console.log('Update response:', updateResponse); + + if (updateResponse.error) { + console.log('Error updating embedding:', updateResponse.error); + } else { + console.log('Embedding updated successfully:', updateResponse.data); + } + + await sleep(2000); // Sleep for 2 seconds + } catch (e) { + console.error(e); + } + } + } +} + +main();