Ok, so for the final instalment, we are going to integrate OpenAI using Langchain.js into our project to give it some more grunt and maybe spark some ideas of your own that you can go and build.
We are going to implement some AI HR feedback in this post using Langchain.js
Feedback from your HR department
Imagine having instant access to valuable feedback from your HR department or an experienced HR professional. With our Langchain / OpenAI magic, you can compare your skills, qualifications, and experiences against the job requirements, and receive personalized recommendations for each job. The chatbot will analyze your profile and provide a recommendation about a likely application. This feedback can then be included in your outgoing email or application to indicate your suitability for the position.
Requirements
You need to get the repo from here https://github.com/BenGardiner123/job-search-tool and follow the directions to spin it up or go back and read your way through Parts 1 and 2.
We need to install Langchain.js. The docs are really nice https://js.langchain.com/docs/ and there are plenty of demos to get you going, the library is in Python and JavaScript with Python getting slightly more advanced features but other than that they are very close. I messaged the CEO recently on the topic
You also need to have an OpenAI API key which you can obtain here, or if you need more help here is a nice article that goes into a lot of depth.
Langchain.js
Ok, so not going to go hardcore into depth on Langchain here because that's a post in itself. But I would say if Lego + OpenAI and Javascript had a baby you'd end up with Lanchain.
Lanchain pretty much strives to be as modular as possible, you can just take all these tools, and functionalities and just clip them together pretty much. It really is amazing and kinda wicked that they had this vision to make it that way.
It's exploding right now and with Vector Databases and the Chat-GPT thing it's just everywhere.
Do this to install the package
npm install -S langchain
Ahhh... there is a small problem in that langchain doesn't like the current version of Puppetier. You can open the project's package.json
file. and change it to this
"dependencies": {
"puppeteer": "^19.7.2",
...
}
then save your file and then in your console
npm install
annnd we should be good to install lanchain again
npm install -S langchain
Adding the new HR department.
We want to store some of our information so that Chat-GPT can incorporate it into its answers. The easiest way is to use this.
npm install hnswlib-node
Cool. Now we want to get the job description, pass it to our "HR dept" get some text output and attach it to the email before it is sent.
There are quite a few ways you could go about this. The simplest would be using a template. We come up with our HR department Prompt and then simply feed the job description into it as a variable.
The basic example from the docs looks like this
import { PromptTemplate } from "langchain/prompts";
const template = "What is a good name for a company that makes {product}?";
const prompt = new PromptTemplate({
template: template,
inputVariables: ["product"],
});
Which we could update to something like this. Prompt engineering is a whole thing so this could be worked up much more but roughly...
import { PromptTemplate } from "langchain/prompts";
const template = "Act as if you are my personal HR expert and look
at this job: {job} given my experience: {myExp} tell me if
you would recommend this job for me?";
const prompt = new PromptTemplate({
template: template,
inputVariables: ["job", "myExp"],
});
But for now, I want to use a bit more of Langchain's more interesting tools.
We are going to create a resume.txt document, then chop it up into little chunks and then store the embeddings into an "in-memory" VectorStore or Vector Database.
Then, we are going to use Langchains VectorDBQAChain to then ask questions using the LargeLanguageModel - but it will know about our document and our potential jobs.
Create a new folder called docs in the root.
then add a text file with this Chat-GPT created fake resume.txt file
Miguel Sanchez
123 Main Street
Anytown, USA 12345
(123) 456-7890
migel@migel.com
Objective:
Highly motivated and enthusiastic web developer with 2 years of practical experience seeking a challenging position in a dynamic organization to further enhance my skills and contribute to the growth and success of the company.
Education:
Bachelor of Science in Computer Science
XYZ University, Anytown, USA
2019
Skills:
Proficient in HTML, CSS, and JavaScript
Experience with front-end frameworks such as React.js and Angular
Knowledge of back-end development using Node.js and Express
Familiarity with database management systems like MySQL and MongoDB
Understanding of version control systems like Git
Strong problem-solving and analytical skills
Excellent communication and teamwork abilities
Adaptability and ability to learn quickly in fast-paced environments
Experience:
Web Developer
ABC Company, Anytown, USA
January 2021 - December 2022
Developed responsive and user-friendly websites using HTML, CSS, and JavaScript.
Collaborated with the design team to implement website layouts and interactive features.
Implemented front-end frameworks, including React.js, to enhance user experience.
Integrated back-end functionalities using Node.js and Express to create dynamic web applications.
Worked with databases like MySQL and MongoDB to store and retrieve data efficiently.
Ensured cross-browser compatibility and optimized website performance.
Debugged and resolved issues, conducted testing, and implemented necessary improvements.
Collaborated with team members on project planning and execution, meeting tight deadlines.
Projects:
E-commerce Website
Created a fully functional e-commerce website using React.js and Node.js.
Integrated a payment gateway for secure online transactions.
Implemented features such as product listing, shopping cart, and user authentication.
Portfolio Website
Designed and developed a responsive portfolio website using HTML, CSS, and JavaScript.
Showcased personal projects and skills in an aesthetically pleasing manner.
Implemented smooth transitions and interactive elements for an engaging user experience.
Certifications:
Web Development Certification, XYZ Certification Authority, 2020
JavaScript Fundamentals, ABC Online Learning Platform, 2021
References:
Available upon request
Update your index.js, we are adding some output arrays to hold our values, and adding the code to get our langchain.js local vector storage going and using an object to store the specific job id and feedback for consumption later in the email function.
import puppeteer from "puppeteer"; // Importing the Puppeteer library for web scraping
import constants, { KEYWORDS } from "./utils/constants.js"; // Importing constants and keywords from the constants module
import mongoose from "mongoose"; // Importing the Mongoose library for MongoDB
import dotenv from "dotenv";
import { Job } from "./models/job.js"; // Importing the Job model from the models module
import { evaluate } from "./evaluate.js";
import { OpenAI } from "langchain/llms/openai";
import { OpenAIEmbeddings } from "langchain/embeddings/openai";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { DocxLoader } from "langchain/document_loaders/fs/docx";
import { TextLoader } from "langchain/document_loaders/fs/text";
import { HNSWLib } from "langchain/vectorstores";
import { DirectoryLoader } from "langchain/document_loaders/fs/directory";
import { PDFLoader } from "langchain/document_loaders/fs/pdf";
import { RetrievalQAChain } from "langchain/chains";
import getSingleJobDetails from "./getSingleJobDetails.js";
import sendEmail from "./email.js";
dotenv.config(); // Configure dotenv to load the .env file
const mongoUrl = process.env.MONGO_URI; // Setting the MongoDB connection URL from the environment variable we set in the .env file
const jobTitle = "junior web developer"; // Setting the job title to search for.
const jobLocation = "Work from home"; // Setting the job location to search for
const searchUrl = constants.SEEK_URL + jobTitle + "-jobs?where=" + jobLocation; // Constructing the search URL
export default async function runJobScrape() {
const browser = await puppeteer.launch({
headless: false, // Launch Puppeteer in non-headless mode (visible browser window)
args: ["--no-sandbox"], // Additional arguments for Puppeteer
});
const page = await browser.newPage(); // Create a new page in the browser
await page.goto(constants.SEEK_URL); // Navigate the page to the SEEK website URL
await page.click(constants.KEYWORDS); // Click on the search input field for keywords
await page.keyboard.type(jobTitle); // Type the job title into the search input field
await page.click(constants.LOCATION); // Click on the search input field for location
await page.keyboard.type(jobLocation); // Type the job location into the search input field
await page.click(constants.SEARCH); // Click the search button
await new Promise((r) => setTimeout(r, 2000)); // Wait for 2 seconds (delay)
// await page.screenshot({ path: "./src/screenshots/search.png" });
// Take a screenshot of the search results page (optional)
let numPages = await getNumPages(page); // Get the total number of pages in the search results
console.log("getNumPages => total: ", numPages);
const jobList = []; // Create an empty array to store job information when we loop through the search results pages
for (let h = 1; h <= numPages; h++) {
let pageUrl = searchUrl + "&page=" + h; // Construct the URL for the current page of search results
await page.goto(pageUrl); // Navigate the page to the current search results page
console.log(`Page ${h}`); // log the current page number to console for visibility
// Find all the job elements on the page
const jobElements = await page.$$(
"div._1wkzzau0.szurmz0.szurmzb div._1wkzzau0.a1msqi7e"
);
for (const element of jobElements) {
const jobTitleElement = await element.$('a[data-automation="jobTitle"]'); // Find the job title element
const jobUrl = await page.evaluate((el) => el.href, jobTitleElement); // Extract the job URL from the job title element
// Extract the job title from the element
const jobTitle = await element.$eval(
'a[data-automation="jobTitle"]',
(el) => el.textContent
);
// Extract the job company from the element
const jobCompany = await element.$eval(
'a[data-automation="jobCompany"]',
(el) => el.textContent
);
// Extract the job details from the element
const jobDetails = await element.$eval(
'span[data-automation="jobShortDescription"]',
(el) => el.textContent
);
// Extract the job category from the element
const jobCategory = await element.$eval(
'a[data-automation="jobSubClassification"]',
(el) => el.textContent
);
// Extract the job location from the element
const jobLocation = await element.$eval(
'a[data-automation="jobLocation"]',
(el) => el.textContent
);
// Extract the job listing date from the element
const jobListingDate = await element.$eval(
'span[data-automation="jobListingDate"]',
(el) => el.textContent
);
// Now we check if the job details contain any of the keywords that we set out in utils/constants.js
// Ive done this as an exmaple to show when you store the jobs in the database, you can use the keywords to filter the jobs
// or use the keywords for other data related uses/analysis.
const jobDetailsHasKeywords = KEYWORDS.filter((keyword) =>
jobDetails.toLowerCase().includes(keyword.toLowerCase())
);
// the job salary is not always available, so we need to check if it exists before we try to extract it
let jobSalary = "";
try {
jobSalary = await element.$eval(
'span[data-automation="jobSalary"]',
(el) => el.textContent
);
} catch (error) {
// return an empty string if no salary is found for the job, we don't want to throw an error
jobSalary = "";
}
const job = {
title: jobTitle || "",
company: jobCompany || "",
details: jobDetails || "",
category: jobCategory || "",
location: jobLocation || "",
listingDate: jobListingDate || "",
salary: jobSalary || "",
dateScraped: new Date(),
url: jobUrl || "",
keywords: jobDetailsHasKeywords || [],
};
// verify the job object has been created correctly inside the loop
// console.log("Job elements loop => Job", job);
jobList.push(job);
}
}
await insertJobs(jobList);
// await browser.close();
}
// borrowed from https://github.com/ongsterr/job-scrape/blob/master/src/job-scrape.js
async function getNumPages(page) {
// Get the selector for the job count element from the constants
const jobCount = constants.JOBS_NUM;
// Use the page's evaluate function to run the following code in the browser context
let pageCount = await page.evaluate((sel) => {
let jobs = parseInt(document.querySelector(sel).innerText); // Get the inner text of the job count element and convert it to an integer
let pages = Math.ceil(jobs / 20); // Calculate the number of pages based on the total job count (assuming 20 jobs per page)
return pages; // Return the number of pages
}, jobCount);
return pageCount; // Return the total number of pages
}
async function insertJobs(jobPosts) {
try {
// Connect to the MongoDB
await mongoose.connect(mongoUrl, {
useNewUrlParser: true,
});
console.log("Successfully connected to MongoDB.");
// Get the list of existing job details in the database
const existingJobDetails = await Job.distinct("details");
// Filter out the existing jobs from the jobPosts array
const newJobs = jobPosts.filter(
(jobPost) => !existingJobDetails.includes(jobPost.details)
);
console.log(`Total jobs: ${jobPosts.length}`);
console.log(`Existing jobs: ${existingJobDetails.length}`);
console.log(`New jobs: ${newJobs.length}`);
// Process the new jobs
for (const jobPost of newJobs) {
const job = new Job({
title: jobPost.title,
company: jobPost.company,
details: jobPost.details,
category: jobPost.category,
location: jobPost.location,
listingDate: jobPost.listingDate,
dateCrawled: jobPost.dateScraped,
salary: jobPost.salary,
url: jobPost.url,
keywords: jobPost.keywords,
});
// Save the job
const savedJob = await job.save();
console.log("Job saved successfully:", savedJob);
}
} catch (error) {
console.log("Could not save jobs:", error);
} finally {
// Close the database connection
mongoose.connection.close();
}
}
function normalizeDocuments(docs) {
return docs.map((doc) => {
if (typeof doc.pageContent === "string") {
return doc.pageContent;
} else if (Array.isArray(doc.pageContent)) {
return doc.pageContent.join("\n");
}
});
}
// await runJobScrape();
const evaluationResults = await evaluate();
// Using the getSingleJobDetails go to the job url and scrape the full job description now that we know its worth our time
let jobDetailsTextResult = [];
// create a list of jobHrFeedbackResults to hold the results of the jobHrFeedback function
const jobHrFeedbackResults = [];
for (const job of evaluationResults) {
const jobObject = {
id: job._id,
details: "",
title: job.title,
};
const jobDetailsText = await getSingleJobDetails(job.url);
jobObject.details = jobDetailsText;
jobDetailsTextResult.push(jobObject);
}
// instantiate the OpenAI LLM that will be used to answer the question and pass your key as the apiKey from the .env file
const model = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
// the directory loader will load all the documents in the docs folder with the correct extension as specified in the second argument
// note this is not the only way to load documents check the docs for more info
const directoryLoader = new DirectoryLoader("docs", {
".pdf": (path) => new PDFLoader(path),
".txt": (path) => new TextLoader(path),
".docx": (path) => new DocxLoader(path),
});
// load the documents into the docs variable
const docs = await directoryLoader.load();
// verify the docs have been loaded correctly
console.log({ docs });
// Split text into chunks with any TextSplitter. You can then use it as context or save it to memory afterwards.
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000,
});
// normalize the documents to make sure they are all strings
const normalizedDocs = normalizeDocuments(docs);
// https://js.langchain.com/docs/modules/schema/document
const splitDocs = await textSplitter.createDocuments(normalizedDocs);
// now create the vector store from the splitDocs and the OpenAIEmbeddings so that we can use it to create the chain
const vectorStore = await HNSWLib.fromDocuments(
splitDocs,
new OpenAIEmbeddings()
);
// https://js.langchain.com/docs/modules/chains/index_related_chains/retrieval_qa
const chain = RetrievalQAChain.fromLLM(model, vectorStore.asRetriever());
// loop through each job in the jobDetailsTextResult and provide the
// prompt to enable our "HR Helper" to provide some feedback
for (const job of jobDetailsTextResult) {
const question = `${job.details} is a job that Migel could apply for.
Act like you a recruiter or HR manager and read each job description step by step.
When matching up requirements with experience treat SQL Server and SQL as the same thing as well as .NET and .NET Core but mention if you are doing this so i can see.
If the job is not suitable for Migel then say so in your recommendations.
then then tell me if Migel should apply for the job or not and give examples like this:
// below is the example of style of answer i want to see
"Migel should apply for this job because he has the required experience with React and Node. Although he does not fit all the experience requirements he has the required experience with React and Node. So it may be worth a try
He has not used Ionic before but he has used Typescript so he should be able to pick it up quickly."
// end of example
`;
const res = await chain.call({
input_documents: docs,
query: question,
});
let outputObject = {
id: job.id.toString(),
jobHrFeedback: res.text,
};
jobHrFeedbackResults.push(outputObject);
}
console.log("jobHrFeedbackResults", jobHrFeedbackResults);
await sendEmail(evaluationResults, jobHrFeedbackResults);
So a couple of things, we want to provide an example of what the output should look like and I will leave it up to you to experiment with what works best for your particular case.
The cool thing about this is that we are expanding the knowledge of "Chat-GPT" to include our particular information. Now yes we could copy and paste that in for this particular case for a single job but the magic here is you can expand this logic out to anything. You could load in your top 100 pdfs on Trading stocks and options and get it to come up with a strategy, or cookbooks whatever really it is limitless.
Let's run it and see how we go - dont forget to comment out the sendEmail function at the bottom because we just want to check the output for now
console.log("jobHrFeedbackResults", jobHrFeedbackResults);
// await sendEmail(evaluationResults);
Ok, now we get back this awesome list of HR feedback!!!! Look, the formatting isn't great and we can improve this for sure but this just blows me away... with just a few lines of code we have our own HR / Recruitment expert on hand providing recommendations. We haven't even loaded in any specialist HR documents that might make this even better.
One thing to look out for is the model hallucinating - when looking at using different strategies I noticed that it tends to conflate the job data with your data and give responses that aren't based on correct information so be careful with your testing and verify when trying different strategies.
jobHrFeedbackResults [
{
id: '6485acdd5b7f606879b46fcc',
jobHrFeedback: " Miguel should not apply for this job. The job requires 3+ years of JavaScript expertise and telecommunications domain experience, which Miguel does not have. He also does not have experience with Ionic, which is another requirement of the job. Although he does have experience with React.js, Node.js, HTML, CSS, RESTful APIs and web services, and version control systems like Git, these do not make up for the requirements he does not meet. It may be worth exploring other positions that better fit Miguel's experience."
},
{
id: '6485acdd5b7f606879b46fc9',
jobHrFeedback: '\n' +
'No, Miguel should not apply for this job. He does not have experience with Ionic or Vue.js, which are two of the primary requirements for this position. He also does not have the 25 years of high end applications development experience required for this role. Miguel may be able to learn Ionic and Vue.js quickly, but without the required experience for the position he would not be a suitable candidate for this role.'
},
{
id: '6485acdd5b7f606879b46fdc',
jobHrFeedback: ' Miguel should apply for this job because he has the required experience with React, Node.js, HTML, CSS, JavaScript, MySQL, MongoDB, Git, and version control systems. He also has strong problem-solving skills, communication and teamwork abilities, and the ability to learn quickly in fast-paced environments. Additionally, he has created a fully functional e-commerce website using React.js and Node.js, and he has experience integrating a payment gateway for secure online transactions. He also has experience developing websites using HTML, CSS, and JavaScript, and collaborating with the design team to implement website layouts and interactive features. Finally, he is certified in web development and JavaScript fundamentals. Miguel meets the majority of the requirements for the job and should apply.'
},
{
id: '6485acdd5b7f606879b46fdd',
jobHrFeedback: '\n' +
'Migel should apply for this job because he has the required experience with React and Node.js, HTML, CSS, JavaScript, version control systems, problem-solving and analytical skills, and communication and teamwork abilities. Although he does not have direct experience with Ionic, .NET Core, .NET Framework, Razor Views, REST/JSON APIs, and relational databases, his understanding of web development and his ability to learn quickly in fast-paced environments makes him a great candidate for this position.'
},
{
id: '648658b9b3c6db3e18e35736',
jobHrFeedback: ' \n' +
'Miguel should apply for this job as he has the essential experience needed for the role, including proficiency in HTML, CSS, JavaScript, React.js, Node.js, and databases such as MySQL and MongoDB. His experience as a Web Developer also demonstrates his ability to develop responsive and user-friendly websites, integrate back-end functionalities, debug and resolve issues, and collaborate with the design team. He also has a Bachelor of Science in Computer Science and certifications in web development and JavaScript fundamentals. He does not have experience with Ionic, but his understanding of other frameworks and his knowledge of version control systems should help him quickly pick up the skills necessary for the role.'
},
{
id: '648658b9b3c6db3e18e35733',
jobHrFeedback: " Based on Miguel's experience and skills, he is not a suitable candidate for this job. While he has experience with HTML, CSS, JavaScript, React.js, Node.js, MySQL, and MongoDB, he does not have experience with Ionic and Vue.js, and he has no experience with SQL Server or .NET Core, both of which are required for the job. Additionally, the job requires 3 contactable references, and Miguel has not listed any."
},
{
id: '648658b9b3c6db3e18e35746',
jobHrFeedback: '\n' +
'Migel should apply for this job because he has the necessary skills and experience. He has a Bachelor of Science in Computer Science, is proficient in HTML, CSS, and JavaScript, and has experience with front-end frameworks such as React.js and Angular. In addition, he has knowledge of back-end development using Node.js and Express, and familiarity with database management systems such as MySQL and MongoDB. He has also created a fully functional e-commerce website using React.js and Node.js, showing his ability to integrate back-end functionalities. He also has certifications in Web Development and JavaScript Fundamentals. Although he does not have experience with Ionic, he is familiar with Typescript and should be able to pick it up quickly. Overall, Miguel is a strong candidate and should apply for this job.'
},
{
id: '648658b9b3c6db3e18e35747',
jobHrFeedback: ' Miguel should apply for this job, as he has the required experience with HTML, CSS, JavaScript, React.js, Node.js, MySQL, MongoDB, and version control systems like Git. He also has strong problem-solving and analytical skills, excellent communication and teamwork abilities, and adaptability and the ability to learn quickly in fast-paced environments. Although he does not have experience with .NET Core / .NET Framework, Razor Views, REST / JSON APIs, and relational databases (SQL Server), he has experience with other similar technologies and should be able to transition to the required technologies quickly. He also has experience with version control systems, which could be beneficial. Overall, Miguel has the skills and experience necessary to be a successful Software Developer (.NET) and should apply for the job.'
},
{
id: '6487b6f4ecec870424ec1f6c',
jobHrFeedback: " Based on Migel's experience, he should definitely apply for this job. He has experience with HTML, CSS, JavaScript, React.js, Node.js, MySQL, MongoDB, version control systems such as Git, and front-end frameworks such as Angular. He also has strong problem-solving and analytical skills, as well as excellent communication and teamwork abilities. He has developed a fully-functional e-commerce website using React.js and Node.js, and designed and developed a portfolio website using HTML, CSS, and JavaScript. He also has certifications in web development and JavaScript fundamentals, which are beneficial for this role. His experience, skills, and certifications make him an ideal candidate for this job."
}
]
Great so now we need to update the email function to accept the new array of feedback objects, then we'll need to do a find inside the html template to map the job.id with the jobHrFeedbackResults.id
await sendEmail(evaluationResults, jobHrFeedbackResults);
and then
import nodemailer from "nodemailer";
export const sendEmail = async (jobs, jobHrFeedbackResults) => {
const jobHrFeedbackResultsMapped = jobHrFeedbackResults.map(
(jobHrFeedbackResult) => {
return {
id: jobHrFeedbackResult.id,
feedback: jobHrFeedbackResult.jobHrFeedback,
};
}
);
// Create a nodemailer transporter
const transporter = nodemailer.createTransport({
host: "smtp.office365.com",
port: 587,
secure: false,
auth: {
user: "someone@someone.com", // Replace with your Outlook email address
pass: "abc123", // Replace with your Outlook password
},
});
try {
// Compose the email message
const message = {
from: "someone@someone.com", // Sender email address
to: "someone@someone.com", // Recipient email address
subject: "New Job Opportunities",
html: `<html>
<head>
<style>
.job-card {
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 20px;
border-radius: 5px;
}
.job-title {
color: #333;
margin-bottom: 10px;
}
.job-details {
margin-bottom: 10px;
}
.job-link {
color: blue;
text-decoration: underline;
}
.job-keywords {
margin-top: 10px;
}
</style>
</head>
<body>
${jobs
.map(
(job) => `
<div class="job-card">
<h2 class="job-title">${job.title}</h2>
<p><strong>Company:</strong> ${job.company}</p>
<p><strong>Location:</strong> ${job.location}</p>
<p class="job-details"><strong>Job Description:</strong></p>
<p>${job.details}</p>
<p class="job-details"><strong>You HR Helper Feedback:</strong></p>
<p>"${
(
jobHrFeedbackResultsMapped.find(
(f) => f.id.toString() === job.id.toString()
) || {}
).feedback || ""
}"</p>
<p><strong>Link:</strong> <a class="job-link" href="${job.url}">${
job.url
}</a></p>
<p class="job-keywords"><strong>Keywords:</strong> ${job.keywords.join(
", "
)}</p>
</div>
`
)
.join("")}
</body>
</html>`,
};
// Send the email
const info = await transporter.sendMail(message);
console.log("Email sent:", info.messageId);
} catch (error) {
console.log("Error sending email:", error);
}
};
export default sendEmail;
Cool! now we run and wait for the magic to happen.....
Aweeesome!!! I love it. I feel like this is way better than what you get from seek.com.au or any other job site for that matter!
The code needs some refactoring and there are a couple more things to add but this is starting to get a bit long so I think ill make one more post in the series to finish.
I will update the repo here https://github.com/BenGardiner123/job-search-tool
I hope you enjoyed it so far - if you did I'd really appreciate a like so i know im on the right track!
Well, that's it! see you in the final instalment where we implement a cover letter creation facility.
Happy Coding