Job Hunting Is Annoying. Build an AI Assistant to Automate the Application process.

Job hunting can be soul-sucking. Scouring job boards, tailoring resumes, crafting cover letters, and reaching out to recruiters — it’s a full-time job on its own, minus the pay. It’s daunting, boring, and to be honest, a bit of an emotional rollercoaster.

I genuinely feel it’s one of the most monotonous tasks ever. These days, whenever I face a boring task, my brain instantly goes: wait, why not let a language model do it for you?

So, in an effort to answer that question and inject some efficiency into the job application process, I built a quick AI prototype: a Streamlit app powered by AI to ease the pain of job applications.

This article is all about how to make job searching a little less painful and faster, and in general, learn how to build a quick prototype to help out with tasks AI can do for you.

Why This App?

Just applying for a ton of jobs with the same exact resume and not sending a cover letter doesn’t really do much. But tailoring your resume for each application, writing a personalized cover letter, and sending a customized cold email to recruiters are strategies known to increase your chances significantly. But they’re also time-consuming. That’s where this app comes in.

It uses AI to tailor your resume based on the job description, generates a coherent and relevant cover letter, and outputs a personalized cold email ready to send to recruiters. This pretty much embodies what I believe to be a decent strategy for job applications: tailor the resume to the job description, write a cover letter, apply for the job, then find the email for the recruiter and send a personalized cold email. This is time-consuming, which is why we use AI to make the whole thing a little less painful and a lot faster.

What Do You Need?

To use the app, you just need a valid OpenAI or Anthropic API key with sufficient funds in your account. Then, you’re all set to check out the app here and give it a spin by pasting your documents and letting the AI work its magic. ✨

To build the app, some Python know-how and comfort in connecting APIs are essential. A little familiarity with Streamlit will also go a long way.

What does this App do?

The core of the app is built around a simple idea: input your resume, the job description of the position you want to apply for, and a bit of personal bio, and let the AI do its thing. The output? Customized resume, cover letter, and cold email are ready for you to use for your application and send to the recruiter.

What tech are we using?

At its core, the app gives users the flexibility to choose between Anthropic and OpenAI as their chosen AI provider to generate the docs.

This allows for a more tailored experience, as each provider has its own unique strengths (I personally prefer Anthropic these days).

To create an intuitive interface, we leverage the simplicity of Streamlit, a Python library that makes it easy to turn scripts into shareable web apps.

And since I am interested in improving the app based on user feedback, I’ve integrated a Google Sheets backend to collect and store user suggestions.

Breaking down the code

Let’s dive into the app. We are going to go through the code piece by piece to make sure it’s broken down and easy to follow. We’ll walk through each section, explaining its purpose and how it contributes to the overall functionality of the app.

First things first: there are just two Python files: generator.py (responsible for generating the content) and app.py (our streamlit user interface)

Setting Up the Environment (`generator.py`)

import os
import logging
from dotenv import load_dotenv
import anthropic
import openai

# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

The initial setup involves importing necessary libraries and configuring logging to help debug and track the app’s performance. `load_dotenv` is used for loading environment variables securely, which includes our API keys for Anthropic and OpenAI, ensuring our app remains secure and functional.

Generating Documents

Here’s where the real fun begins. We’ve got a function that’s essentially the brain of the operation. You feed it your resume, job description, and a snippet about yourself, and it outputs all the tailored docs you need.

def generate_documents(resume, job_description, bio, api_key, ai_provider, formality_level, additional_info):
try:
if not api_key:
logging.error("API key not provided")
return None
if ai_provider == "Anthropic":
client = anthropic.Anthropic(api_key=api_key)
logging.info("Successfully created Anthropic client")
elif ai_provider == "OpenAI":
openai.api_key = api_key
logging.info("Successfully set OpenAI API key")
else:
logging.error("Invalid AI provider selected")
return None

This snippet checks for the API key and decides which AI provider to use based on what the user’s picked in the frontend.

Crafting the AI Prompt

Next, we add a detailed prompt that guides the AI in generating a tailored resume, cover letter, and cold email. This part is essential for ensuring the outputs are relevant and personalized.

system_prompt = (
"You are an expert in early career professional resume crafting, cover letter writing, and cold email composition. "
"Your task is to generate a tailored resume, cover letter, and cold email for a job seeker based on the provided inputs."
)
prompt = (
"Given the following information for the candidate:\n\n"
"Resume:\n{resume}\n\n"
"Job Description:\n{job_description}\n\n"
"Bio/Brief Intro:\n{bio}\n\n"
"Formality Level for the cover letter: {formality_level}\n\n"
"Additional Information: {additional_info}\n\n"
"Please generate the following outputs:\n\n"
"===TAILORED RESUME===\n"
"Tailor the resume to highlight the candidate's relevant skills and experiences that match the job requirements. "
"Optimize the structure and formatting to make it more visually appealing and ATS-friendly, while maintaining the order of the sections and the exact length as much as possible.\n\n"
"===TAILORED COVER LETTER===\n"
"Craft a personalized cover letter that introduces the candidate, emphasizes their fit for the role, and encourages the recruiter to review the candidate's resume. "
"Include a brief introduction highlighting the candidate's interest and fit for the role, specific examples of how their skills and experiences align with the job requirements, "
"and a call-to-action encouraging the recruiter to consider the candidate for the role. Adjust the formality level based on the provided preference.\n\n"
"===CUSTOMIZED COLD EMAIL===\n"
"Generate a customized cold email that greets the recruiter, introduces the candidate, and invites the recruiter to consider the candidate for the role. "
"Adjust the formality level based on the provided preference.\n\n"
"Remember to keep the outputs concise, professional, and tailored to the specific job and candidate details provided. "
"THIS IS VERY IMPORTANT: Ensure that each output section is clearly delineated with the '===SECTION_TITLE===' format."
).format(resume=resume, job_description=job_description, bio=bio, formality_level=formality_level, additional_info=additional_info)
logging.info(f"Sending the following prompt to the AI: {prompt}")
if ai_provider == "Anthropic":
response = client.messages.create(
model="claude-3-haiku-20240307",
max_tokens=4000,
system=system_prompt,
temperature=0.5,
messages=[
{
"role": "user",
"content": [
{
"type": "text",
"text": prompt
}
]
}
]
)
logging.info("Successfully generated the documents using Anthropic")
logging.info(f"AI Response: {response.content[0].text.strip()}")
ai_response = response.content[0].text.strip()
elif ai_provider == "OpenAI":
response = openai.chat.completions.create(
model="gpt-4-turbo-preview",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt},
],
temperature=0.5,
)
logging.info("Successfully generated the documents using OpenAI")
logging.info(f"AI Response: {response.choices[0].message.content.strip()}")
ai_response = response.choices[0].message.content.strip()
else:
logging.error("Invalid AI provider selected")
return None

The prompt is designed to extract the best possible output from the AI, guiding it through the task with specific instructions and inputs. It also helps structure the output in a way that makes it easy for us to extract each document later.

Handling the AI Response

Once we send the prompt to the chosen AI and receive the output, we need to parse it, splitting it into distinct sections for the resume, cover letter, and email. This parsing is important to then display it properly in the Streamlit UI.

# Split the AI response into the individual outputs
sections = ai_response.split("\n\n===")
outputs = {}
for section in sections:
if section.strip():
section_title = section.strip().split("\n")[0].strip("===")
section_content = "\n".join(section.strip().split("\n")[1:])
outputs[section_title] = section_content
logging.info(f"Outputs: {outputs}")
return outputs
except Exception as e:
logging.error(f"Error generating documents: {e}")
return None

This section demonstrates how we split the output, organizing it into the 3 documents we will then pass to the frontend.

Streamlit Frontend (`app.py`)

Now, moving to `app.py`, this is where we create a quick and intuitive interface for users to interact with our app. Streamlit’s straightforward syntax lets us focus on functionality rather than boilerplate code.

import streamlit as st
from generator import generate_documents
import gspread
from google.oauth2.service_account import Credentials
from google.oauth2 import service_account
import toml

def main():
st.title(" JobBot: your AI Job Application Sidekick")
# User input for API key and provider
st.sidebar.title("API Settings")
ai_provider_toggle = st.sidebar.toggle("Select AI Provider", value=True, key="ai_provider_toggle")
if ai_provider_toggle:
ai_provider = "**Anthropic**"
ai_provider_emoji = ""
else:
ai_provider = "**OpenAI**"
ai_provider_emoji = ""
st.sidebar.write(f"You have selected {ai_provider} {ai_provider_emoji}")
api_key = st.sidebar.text_input("Enter your API key:", type="password")
if not api_key:
st.sidebar.error(" Please enter a valid API key to use the app.")
st.stop()

Here, we define our app’s title and proceed to build a form where users can input their resume, job description, and personal bio. This user-friendly interface is the bridge between the user and our AI-powered document generator.

User Inputs and Document

We guide users through inputting their details and selecting preferences for their application documents. The magic happens when they hit the “Generate Docs” button — our app communicates with the AI, processes the inputs, and generates customized application documents.

# User inputs
st.subheader("Paste your resume here ")
resume = st.text_area("", height=200, placeholder="Your resume goes here...")
st.subheader("Paste the job description here ")
job_description = st.text_area("", height=200, placeholder="The job description goes here...")
st.subheader("Paste your bio/brief intro here ‍♂️")
bio = st.text_area("", height=100, placeholder="Your bio or brief intro goes here. For example, you can add your LinkedIn summary or a short introduction about yourself.")
# Additional user inputs
st.sidebar.subheader("Additional Settings ")
formality_level = st.sidebar.select_slider("Formality Level for cover letter and email ", ["Super Casual", "Casual", "Neutral", "Formal", "Super Formal"], value="Neutral")
additional_info = st.sidebar.text_area("Additional Information ", height=100, placeholder="Anything else you want the AI to consider or focus on?")
# Define the output section titles
output_section_titles = {
"Tailored Resume": "TAILORED RESUME",
"Tailored Cover Letter": "TAILORED COVER LETTER",
"Customized Cold Email": "CUSTOMIZED COLD EMAIL"
}
if st.button("✨Generate Docs"):
# Show loader while generating documents
with st.spinner("AI is cooking up the docs to help with your application, just a few secs! ‍"):
# Generate outputs
outputs = generate_documents(resume, job_description, bio, api_key, ai_provider.replace("**", ""), formality_level, additional_info)
if outputs:
for section_title, section_key in output_section_titles.items():
if section_key in outputs and outputs[section_key].strip():
st.subheader(section_title)
st.code(outputs[section_key], language="text")
# How to use section in the sidebar
st.sidebar.subheader("How to Use ")
with st.sidebar.expander("Click here to see a quick guide!"):
st.markdown("""
- Copy and paste your resume
- Copy and paste the job description you're applying for
- ‍♂️ Provide a brief bio/intro about yourself
- Pick the formality level for the cover letter and email (default works well for most cases)
- Add any additional information you want the AI to consider (optional)
- Click the "Generate Documents" button and let the magic happen!✨
""")

This section of the code captures user inputs and triggers the document generation process, this is the app’s core functionality.

Gathering user feedback (Google Sheets integration)

If you are interested in implementing in your app, this is how we gather feedback and store it in a Google Sheet.

In addition to the core functionality of the app, I included a user feedback form in the sidebar. This allows users to share their thoughts, suggestions, and any features they’d find useful. The feedback is then stored in a Google Sheet, this way we can improve the app based on user input.

The feedback form is built using Streamlit’s form functionality. Users can simply enter their thoughts in the text area and click the “Submit Feedback” button. Behind the scenes, the app sets up the Google Sheets API using the TOML secrets provided in the Streamlit environment. Once the feedback is submitted, the app appends the user’s input to a dedicated worksheet in the Google Sheet, making it easy to review and keep the app evolving based on user’s inputs.

Here’s the code:

# Feedback form in the sidebar
with st.sidebar:
st.subheader("Got Feedback or features you'd find useful? ")
# Start of the form
with st.form(key='feedback_form'):
feedback = st.text_area("Share your thoughts with me here:")
submit_feedback = st.form_submit_button(label='Submit Feedback ')
if submit_feedback:
# Set up the Google Sheets API using TOML secrets
try:
secrets = {
"type": st.secrets["type"],
"project_id": st.secrets["project_id"],
"private_key_id": st.secrets["private_key_id"],
"private_key": st.secrets["private_key"],
"client_email": st.secrets["client_email"],
"client_id": st.secrets["client_id"],
"auth_uri": st.secrets["auth_uri"],
"token_uri": st.secrets["token_uri"],
"auth_provider_x509_cert_url": st.secrets["auth_provider_x509_cert_url"],
"client_x509_cert_url": st.secrets["client_x509_cert_url"],
"universe_domain": st.secrets["universe_domain"]
}
creds = service_account.Credentials.from_service_account_info(
secrets,
scopes=['https://spreadsheets.google.com/feeds', 'https://www.googleapis.com/auth/spreadsheets', 'https://www.googleapis.com/auth/drive']
)
client = gspread.authorize(creds)
# Open the Google Sheet and append the feedback
sh = client.open('prototype_feedback').worksheet('Feedback')
sh.append_row([feedback])
st.sidebar.success("Thanks for your feedback! ")
except Exception as e:
st.sidebar.error(f"Error: {e}")

This is what our user interface looks like:

Image from the author — Made with ❤️ and Python.

Wrapping up and Trying It Out

That’s all for this article! Dive into the code to build your own version by visiting the GitHub repo. You can also give the app a try here. Please remember to make sure you have a valid OpenAI or Anthropic API key!

Excited to hear your feedback or any cool ideas you might have for further enhancements. This is just the starting point, I would love to make the job-hunting process as automated and painless as possible.

If you found this article useful, consider expressing your appreciation with 50 claps — your support means a ton.

Thanks for following along, and happy coding! 🙂

All images are by me, the author, unless otherwise noted. Apart from being a user, I have no affiliation with OpenAI, Streamlit, Anthropic or any other organisation.

Original Post>