Build a Multi-Persona Resume Reviewer

Use Gemini AI to review any PDF resume from three professional angles.

Introduction

30 Second Summary

Getting feedback on your resume usually means asking friends who say "looks great!" or paying a career coach who only sees it from one angle. Real hiring pipelines run your resume past grammar checkers, applicant tracking systems, and actual hiring managers, each judging it by completely different standards.

In this project, you will build a Python command-line tool that reviews any PDF resume from three distinct AI-powered reviewer personas. Each reviewer scores your resume, identifies gaps, and gives you specific suggestions you can act on immediately.

What You'll Build

You run a single command, point it at any PDF resume and a target role, and within seconds your terminal prints three structured reviews from a Grammar Editor, an ATS Scanner, and a Hiring Manager, complete with numeric scores and line-specific feedback.

By the end of this project, you'll have:

  • A CLI tool that extracts text from any PDF resume and runs it through three AI reviewer personas in one command, printing scored feedback for each.
  • Structured, consistent JSON output from every reviewer showing strengths, gaps, and specific suggestions you can apply to your resume right away.
  • A personal baseline criteria file that you maintain and that every reviewer enforces alongside their specialty evaluation, so your own standards are always checked.
  • Secret Mission: Add a Technical Reviewer persona that dynamically evaluates your resume for a specific tech stack based on the target role.

Are there any prerequisites?

You need Python 3.10 or higher installed on your machine and a Google account to get a free Gemini API key. No prior AI or machine learning experience is required.

Before We Start

Your resume review agent needs three things to work: access to the Gemini API for AI-powered evaluation, a library to read PDF files, and a way to enforce consistent output from the model.

In this step, you will set up a Python project with all the required packages and grab your free Gemini API key so everything is ready for building.

In this step, get ready to:
  • Create your project folder and Python virtual environment.
  • Install the required packages and get your Gemini API key.
  • Verify everything works with a test script.
Create your project folder and virtual environment

A virtual environment keeps your project's packages isolated from your system Python. This prevents version conflicts with other projects on your machine.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir resume-reviewer
cd resume-reviewer
  • Confirm the folder exists by running:
ls

You should see the resume-reviewer folder listed in the output. Since you already moved into it, the ls output will be empty (the folder is new and has no files yet).

  • Create a virtual environment by running:
python3 -m venv venv
  • Activate the virtual environment:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate

Your terminal prompt now shows (venv) at the beginning, confirming the virtual environment is active.

Don't see (venv) in your prompt?

Make sure you ran the activate command from inside the resume-reviewer folder. If you see a "file not found" error, confirm the venv folder was created by running ls and checking for a venv directory.

On Windows, if you see an execution policy error, run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned first, then retry activation.

help me activate my Python virtual environment

  • Install the three packages your project needs by running:
pip install google-genai pymupdf pydantic

You will see download and installation progress in your terminal. Once finished, the last line shows the installed packages.

What are these packages?

  • google-genai is Google's SDK for calling the Gemini API.
  • pymupdf extracts text from PDF files with no extra system dependencies.
  • pydantic defines data models that enforce structured output from the AI.

Seeing a pip error?

If you see "pip not found," try pip3 install google-genai pymupdf pydantic instead. Make sure (venv) is showing in your prompt before running the install.

help me fix a pip install error in my virtual environment

Get your Gemini API key

The Gemini API is free to use on Google's free tier. You need an API key to authenticate your requests.

  • Open your browser and navigate to Google AI Studio.
  • Sign in with your Google account if prompted.
  • Navigate to the API keys page from the left panel.
  • Click Create API key.
  • Copy the generated key. You will only need it once to paste into your project file.

What if I'm a new user?

Google AI Studio automatically creates a default project and API key for new users after you accept the Terms of Service. You may already have a key on the API keys page ready to copy.

  • Open your editor. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Cursor (or VS Code) and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Select the resume-reviewer folder on your Desktop, then click Open.
  • Create a new file called .env in the root of your project folder.
  • Paste the following into .env, replacing your-key-here with the API key you just copied:
GEMINI_API_KEY=your-key-here
  • Save .env.

Keep your key safe

Never commit your .env file to version control. If you use Git later, add .env to your .gitignore file.

Verify everything works

Before building anything, confirm that all three packages import correctly. A quick test script catches missing or broken installs now instead of mid-project.

  • Create a new file called test_setup.py in your resume-reviewer folder by pasting this code:
from google import genai
import pymupdf
import pydantic
print('All packages ready')

What does this script do?

  • from google import genai loads the Gemini SDK. If this fails, google-genai was not installed correctly.
  • import pymupdf loads the PDF extraction library.
  • import pydantic loads the data validation library you will use for structured output.
  • If all three imports succeed, the script prints a confirmation message.
  • Save test_setup.py.

Before you run this, what do you expect to see if all packages installed correctly?

  • Run the test script in your terminal (make sure (venv) is still showing):
python test_setup.py

You should see All packages ready printed in your terminal. That confirms all three packages are installed and importable.

Seeing a ModuleNotFoundError?

This means a package did not install into your virtual environment. Check that (venv) appears in your terminal prompt. If it does not, re-activate the environment and re-run the install command.

If (venv) is showing but imports still fail, try running pip install google-genai pymupdf pydantic again and watch for error messages during installation.

help me fix ModuleNotFoundError when running my test script

✔️ Awesome, I've got everything!

Great. Double check that you saved both .env and test_setup.py.

ⓧ I'd like to double check the full code

Here is what each file should contain:

.env:

GEMINI_API_KEY=your-key-here

test_setup.py:

from google import genai
import pymupdf
import pydantic
print('All packages ready')

Your environment is locked in and every package imports cleanly. Next up, you will build the PDF extraction that turns a resume file into text the AI can read.

Extract Text from a PDF Resume

Your environment is set up and every package imports cleanly. Now you face the core problem: the AI reviewer you are building can only process text, but your resume lives inside a PDF file.

In this step, you will build the extraction layer that reads a PDF and pulls out all the text content. You will also wire up command-line argument handling so you can point the script at any resume from your terminal.

In this step, get ready to:
  • Write a function that extracts text from any PDF file using PyMuPDF.
  • Add CLI argument handling so you can run the script against any resume.
  • Run the tool and see raw resume text printed in your terminal.
Create the main script with PDF extraction

PyMuPDF gives you a simple API for opening PDF files and pulling text from each page. You will wrap this in a reusable function that accepts any file path.

  • Create a new file called review.py in your resume-reviewer folder and add the following code:
import sys
import os
import pymupdf


def extract_text_from_pdf(pdf_path: str) -> str:
    doc = pymupdf.open(pdf_path)
    text = ""
    for page in doc:
        text += page.get_text()
    doc.close()
    return text

What does this code do?

pymupdf.open() opens the PDF file at the given path and returns a document object you can iterate over.

The for page in doc loop visits every page in the PDF. page.get_text() extracts the text content from that page and appends it to a running string.

doc.close() releases the file handle once extraction is done. The function returns all the combined text as a single string.

  • Save review.py.
  • Confirm the file exists by running:
ls review.py

You should see review.py listed in the output.

Don't see the file?

Make sure you saved the file inside the resume-reviewer folder, not a parent directory. Check that your terminal is also inside resume-reviewer by running pwd.

help me find where my file was saved

Add CLI argument handling

The extraction function is ready, but you have no way to call it from the command line yet. You need a main() function that reads the PDF path from terminal arguments and passes it to your extractor.

  • In review.py, add the following code below the extract_text_from_pdf function:
def main():
    if len(sys.argv) < 2:
        print("Usage: python review.py <resume.pdf>")
        sys.exit(1)

    pdf_path = sys.argv[1]

    if not os.path.exists(pdf_path):
        print(f"Error: File '{pdf_path}' not found.")
        sys.exit(1)

    print(f"Extracting text from: {pdf_path}")
    resume_text = extract_text_from_pdf(pdf_path)
    print(resume_text)


if __name__ == "__main__":
    main()

What does this code do?

sys.argv is a list of arguments passed when running a Python script from the terminal. sys.argv[0] is always the script name itself, so sys.argv[1] is the first argument the user provides.

The os.path.exists() check prevents a confusing crash if the learner mistypes the filename. Instead, it prints a clear error message and exits gracefully.

The if __name__ == "__main__" block ensures main() only runs when you execute the file directly (not when importing it as a module).

  • Save review.py.

You need a PDF resume to test with

Grab any PDF resume you have on hand. If you do not have one, export your LinkedIn profile as a PDF or use any PDF document.

Copy your PDF file into the resume-reviewer folder so the script can find it easily.

Before you run this, what do you think will appear in your terminal? Plain text? Garbled characters? Formatted content?

  • Run the script against your PDF by running this command (replace the filename with your actual PDF):
python review.py [[PDF_FILENAME="your-resume.pdf"]]

You should see the raw text content of your resume printed line by line in the terminal. Headers, bullet points, and body text all appear as plain text strings.

Seeing an error instead of text?

  • If you see a "File not found" error, double check that your PDF is inside the resume-reviewer folder and that you spelled the filename exactly right (including the .pdf extension).
  • If you see "No module named pymupdf", make sure your virtual environment is activated. Run source venv/bin/activate (macOS/Linux) or venv\Scripts\activate (Windows) and try again.
  • If the output is empty, your PDF might be image-based (a scanned document). Try a different PDF that was created digitally.

my resume extraction is not working correctly

✔️ Awesome, I've got everything!

Great. Make sure you have saved review.py and that your extracted text is visible in the terminal.

ⓧ I'd like to double check the full code

Your review.py file should contain these sections in order:

  • Imports at the top: sys, os, pymupdf.
  • The extract_text_from_pdf function that opens the PDF, loops through pages, collects text, closes the document, and returns the combined string.
  • The main() function that checks for a CLI argument, verifies the file exists, extracts the text, and prints it.
  • The if __name__ == "__main__" entry point that calls main().

Scroll up and confirm each section is present in your file.

You now have raw resume text flowing from a PDF into your Python script. Next up, you will send this text to the Gemini API and get your first AI-powered review back.

Get Your First AI Review

You have raw resume text extracted from a PDF, but no intelligence applied to it yet. This step connects your script to the Gemini API and gets your first AI-powered review back.

The review will come back as a single unstructured blob of text. By the end of this step, you will feel the limitation firsthand: the output is hard to parse, and it looks different every time you run it.

In this step, get ready to:
  • Send your resume text to the Gemini API with a Hiring Manager persona.
  • See your first AI-generated review printed to the terminal.
  • Experience why unstructured text output is unreliable across runs.
Connect to the Gemini API and send the resume

Your script currently prints raw text and only accepts one argument (the PDF path). You need it to also accept a target role, load your API key, and send the resume to Gemini for evaluation.

  • In review.py, find the import lines at the top of the file:
import sys
import os
import pymupdf
  • Add two new imports below them, so the full import section looks like this:
import sys
import os
import pymupdf
from google import genai
from google.genai import types
  • Next, find the argument check inside the main() function:
    if len(sys.argv) < 2:
        print("Usage: python review.py <resume.pdf>")
        sys.exit(1)

    pdf_path = sys.argv[1]
  • Replace that section with the following, which adds a second required argument for the target role:
    if len(sys.argv) < 3:
        print("Usage: python review.py <resume.pdf> <target_role>")
        sys.exit(1)

    pdf_path = sys.argv[1]
    target_role = sys.argv[2]

What changed here?

You now require two command-line arguments: the PDF file path and the target role (like "Software Engineer"). The script will use both when asking the AI to review your resume.

  • Now find the line that says print(resume_text) and delete it.
  • In its place, add the following code that loads your API key and initializes the Gemini client:
    # Load API key from environment or .env file
    api_key = os.environ.get("GEMINI_API_KEY")
    if not api_key:
        env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")
        if os.path.exists(env_path):
            with open(env_path) as f:
                for line in f:
                    if line.strip().startswith("GEMINI_API_KEY="):
                        api_key = line.strip().split("=", 1)[1]
                        break

    client = genai.Client(api_key=api_key)

What does this code do?

It first checks for a GEMINI_API_KEY environment variable. If that is not set, it reads your .env file line by line to find the key.

This approach means the script works whether you export the key in your shell or just keep it in the .env file.

  • Directly below the client = genai.Client(api_key=api_key) line, add the system instruction, prompt, and API call:
    # Define the Hiring Manager persona
    system_instruction = (
        "You are a hiring manager reviewing a resume for the target role. "
        "Evaluate the resume for: relevance to the target role, "
        "demonstration of impact (not just responsibilities), "
        "career progression and growth signals, "
        "whether this candidate would get an interview, "
        "and any red flags (gaps, job hopping, vague descriptions). "
        "Be direct about whether you would advance this candidate and why."
    )

    # Build the prompt and send to Gemini
    prompt = (
        f"Target Role: {target_role}\n\n"
        f"Resume Content:\n{resume_text}\n\n"
        "Review this resume from your perspective."
    )

    response = client.models.generate_content(
        model="gemini-2.5-flash",
        contents=prompt,
        config=types.GenerateContentConfig(
            system_instruction=system_instruction,
            temperature=0.3,
        ),
    )

    print(response.text)

What does this code do?

  • system_instruction tells Gemini to behave as a Hiring Manager with specific evaluation criteria.
  • prompt combines the target role and resume text into a single message for the model.
  • generate_content sends the request to gemini-2.5-flash with a low temperature (0.3) for more focused output.
  • response.text contains whatever the model wrote back, printed directly to your terminal.
  • Save review.py.

Before you run this, what do you think the output will look like? Will it be a tidy list of scores, or something else?

  • Run your script against a PDF resume by running this command:
python review.py your_resume.pdf "Software Engineer"

You should see a free-text review printed to your terminal. It reads like a paragraph of feedback from a hiring manager, with opinions about your resume's strengths and weaknesses.

Seeing an error?

  • If you see ModuleNotFoundError: No module named 'google', make sure your virtual environment is activated (source venv/bin/activate).
  • If you see an API key error, double-check your .env file has GEMINI_API_KEY= followed by your actual key with no spaces around the = sign.
  • If you see Error: File 'your_resume.pdf' not found, replace your_resume.pdf with the actual filename of a PDF in your resume-reviewer folder.

Still stuck? Help me debug my Gemini API connection in review.py

✔️ Awesome, I've got everything!

Great. Double check you have saved review.py and move on.

ⓧ I'd like to double check the full code

Your review.py should now contain these sections in order:

  • The import block (5 lines: sys, os, pymupdf, genai, types).
  • The extract_text_from_pdf function (unchanged from the previous step).
  • The main() function with: argument check for 3 args, PDF extraction, API key loading, client init, system instruction, prompt, API call, and print(response.text).
  • The if __name__ == "__main__" block at the bottom.

Scroll up and confirm each section is present in your file.

Experience the limitation

You have a working AI review. But is the output reliable enough to build on?

  • Run the exact same command again:
python review.py your_resume.pdf "Software Engineer"
  • Compare the two outputs side by side.

Notice how the format shifts between runs. One run might give you bullet points, the next a wall of paragraphs.

Sometimes the model mentions a score, sometimes it does not. There is no reliable way to extract specific fields like "strengths" or "gaps" from this text.

Why is this a problem?

If you want to compare reviews across multiple personas (a Grammar Editor, an ATS Scanner, a Hiring Manager), you need every reviewer to return the same fields in the same format. Free-text output makes that impossible.

In the next step, you will enforce a strict JSON schema so every review comes back with the exact same structure: scores, strengths, gaps, and suggestions.

You have proven that the AI can review your resume, but the output is a mess to work with. Next up, you will tame that output with structured JSON so every review is consistent and parseable.

Add Structured JSON Output

In the last step, you got your first AI-powered review back. But you probably noticed the problem: every time you ran it, the output looked different.

Sometimes the AI returned bullet points, sometimes paragraphs, sometimes numbered lists. That inconsistency makes the output impossible to compare across runs or personas. Pydantic solves this by letting you define a strict schema that the Gemini API must conform to, so every review returns the exact same fields every time.

In this step, get ready to:
  • Define a Pydantic model that enforces consistent review output.
  • Configure the Gemini API to return JSON matching your schema.
  • Build a formatted display function to print the review cleanly.
Define the review output schema with Pydantic

A Pydantic BaseModel is a Python class that describes the exact shape of data you expect. When you pass this schema to the Gemini API, the model is constrained to return JSON that matches those fields exactly.

  • In review.py, find the import lines at the top of your file. Replace them and add the new schema class by copying this code over the top of your file (above the extract_text_from_pdf function):
import sys
import os
import pymupdf
from pydantic import BaseModel
from google import genai
from google.genai import types


# --- Schema ---

class ResumeReview(BaseModel):
    overall_score: int
    strengths: list[str]
    gaps: list[str]
    suggestions: list[str]
    summary: str


# --- PDF Extraction ---

What does this code do?

  • from pydantic import BaseModel gives you the base class for defining structured data models.
  • ResumeReview defines five fields the AI must fill in every time: overall_score (a number 1-10), strengths, gaps, suggestions (all lists of strings), and summary (a single string).
  • The section comments (# --- Schema --- etc.) organize the file into logical blocks as it grows.
  • Save review.py.
  • Confirm the file still loads without errors by running:
python review.py

You should see the Usage: python review.py <resume.pdf> <target_role> message. This proves your new import and class have no syntax errors.

Seeing an ImportError?

  • Make sure your virtual environment is active (you should see (venv) in your terminal prompt).
  • Confirm Pydantic is installed by running pip install pydantic again.

Still stuck? Help me fix this ImportError for pydantic in my review.py file.

Parse and display the structured response

Now you need a function to display the structured data in a readable terminal format. You also need to update the API call to enforce your schema.

  • In review.py, add the print_review function directly below your extract_text_from_pdf function (and above the def main(): line):
# --- Display ---

def print_review(persona_name: str, review: ResumeReview):
    print(f"\n{'='*60}")
    print(f"  {persona_name} Review | Score: {review.overall_score}/10")
    print(f"{'='*60}")
    print(f"\n  Summary: {review.summary}")
    print(f"\n  Strengths:")
    for s in review.strengths:
        print(f"    + {s}")
    print(f"\n  Gaps:")
    for g in review.gaps:
        print(f"    - {g}")
    print(f"\n  Suggestions:")
    for s in review.suggestions:
        print(f"    > {s}")

What does this function do?

  • It takes a persona name and a ResumeReview object and prints a cleanly formatted terminal report.
  • Strengths are prefixed with +, gaps with -, and suggestions with > so each category is visually distinct.
  • The persona_name parameter prepares the display for when you add multiple reviewers in the next step.

Now you need to update the API call inside main() to enforce structured JSON output and use the new display function.

  • In review.py, find this block inside your main() function (starting at the prompt = line):
    prompt = (
        f"Target Role: {target_role}\n\n"
        f"Resume Content:\n{resume_text}\n\n"
        "Review this resume from your perspective."
    )

    response = client.models.generate_content(
        model="gemini-2.5-flash",
        contents=prompt,
        config=types.GenerateContentConfig(
            system_instruction=system_instruction,
            temperature=0.3,
        ),
    )

    print(response.text)
  • Replace that entire block with the following:
    prompt = (
        f"Target Role: {target_role}\n\n"
        f"Resume Content:\n{resume_text}\n\n"
        "Review this resume from your perspective. "
        "Provide an overall score from 1-10, specific strengths, "
        "specific gaps or issues, actionable suggestions for improvement, "
        "and a brief summary of your assessment."
    )

    response = client.models.generate_content(
        model="gemini-2.5-flash",
        contents=prompt,
        config=types.GenerateContentConfig(
            system_instruction=system_instruction,
            response_mime_type="application/json",
            response_json_schema=ResumeReview.model_json_schema(),
            temperature=0.3,
        ),
    )

    review = ResumeReview.model_validate_json(response.text)
    print_review("Hiring Manager", review)

What changed here?

  • The prompt now explicitly asks for scores, strengths, gaps, and suggestions. This helps the model fill the schema fields accurately.
  • response_mime_type="application/json" tells the Gemini API to return only valid JSON.
  • response_json_schema=ResumeReview.model_json_schema() constrains the JSON to match your exact Pydantic model fields.
  • ResumeReview.model_validate_json(response.text) parses the JSON string into a typed Python object you can access with dot notation.
  • Also add the # --- Main --- comment directly above the def main(): line to keep the file organized.
  • Save review.py.

Before you run this, what do you think the output will look like compared to the free-text wall you saw in the last step?

  • Run the review against your resume:
python review.py your_resume.pdf "Software Engineer"

You should see a neatly formatted report with a numeric score out of 10, a list of strengths prefixed with +, gaps prefixed with -, and actionable suggestions prefixed with >. Every run will now return the same fields in the same structure.

Seeing an error from the API?

  • If you see a message about your API key, confirm your .env file contains GEMINI_API_KEY= followed by your actual key (no quotes around the value).
  • If you see a JSON parsing error, make sure the response_json_schema line uses ResumeReview.model_json_schema() with the parentheses. Without them, you pass the method itself instead of the schema.
  • If the script exits with no output, check that your PDF file path is correct and the PDF contains selectable text (scanned images without OCR will extract as empty).

Still stuck? Help me debug my structured JSON output from the Gemini API in review.py.

✔️ Awesome, I've got everything!

Great. Double check you have saved review.py and move on to the next step.

ⓧ I'd like to double check the full code

Your review.py should now have these sections in order. Scroll up and confirm each one is present:

  • Imports (sys, os, pymupdf, BaseModel, genai, types).
  • # --- Schema --- section with the ResumeReview class (5 fields: overall_score, strengths, gaps, suggestions, summary).
  • # --- PDF Extraction --- section with extract_text_from_pdf (unchanged from before).
  • # --- Display --- section with print_review function (takes persona_name and review, prints formatted output).
  • # --- Main --- section with main() containing the updated prompt, response_mime_type="application/json", response_json_schema=ResumeReview.model_json_schema(), and the model_validate_json + print_review call at the end.

You now have reliable, structured output from your AI reviewer. Every run returns the same five fields, making it easy to compare results across different runs and reviewers. Next up, you will build the full multi-persona system where three distinct reviewers each evaluate the same resume from their own angle.

Build the Multi-Persona Review System

Your structured output from the last step solved the formatting problem. Every review now comes back with consistent scores, lists, and a summary.

But one reviewer perspective still misses critical angles. A Grammar Editor catches what a Hiring Manager ignores. An ATS Scanner evaluates things neither of the others consider. In this step, you will build a multi-persona system where three distinct reviewers each evaluate your resume, and all of them also enforce your personal baseline criteria loaded from a simple text file.

In this step, get ready to:
  • Create a baseline criteria file that all reviewers enforce alongside their specialty.
  • Define three reviewer personas (Grammar Editor, ATS Scanner, Hiring Manager) with distinct evaluation angles.
  • Run all three personas against your resume and print an aggregate summary with scores, top issues, and priority actions.
Create the criteria file and loader

Before adding personas, you need a universal checklist that applies no matter who is reviewing. These are your personal standards for any resume you submit.

  • Create a new file called criteria.txt in your resume-reviewer folder by running:
touch criteria.txt
  • Open criteria.txt and paste the following baseline rules:
- Must include quantified achievements with specific numbers and metrics
- No objective statement; use a professional summary instead
- Contact info must include LinkedIn URL
- All bullet points should start with a strong action verb
- No more than 2 pages for candidates with under 10 years of experience
- Each role must include at least 3 bullet points demonstrating impact
- No personal pronouns (I, me, my) in bullet points
- Skills section must be present and organized by category
- Education section must include graduation year
- No generic phrases like "team player" or "hard worker" without evidence
- Dates must be consistent format throughout (e.g., Mon YYYY or MM/YYYY)
- No typos or grammatical errors anywhere in the document

What is this criteria file for?

Each line is a rule that every reviewer persona will check against, regardless of their specialty. The Grammar Editor checks grammar AND these rules. The ATS Scanner checks keywords AND these rules.

You can edit this file anytime to add or remove rules without touching any Python code.

  • Save criteria.txt.
  • Confirm it exists by running:
cat criteria.txt

You should see all 12 rules printed in your terminal.

Now you need a function that reads this file so Python can inject the criteria into each persona's instructions.

  • In review.py, find this line at the top:
import os
  • Add import json on the next line, so your imports look like this:
import sys
import os
import json
import pymupdf
  • Below the extract_text_from_pdf function, add the criteria loader section:
# --- Criteria Loader ---

def load_criteria(criteria_path: str) -> str:
    if not os.path.exists(criteria_path):
        return ""
    with open(criteria_path, "r") as f:
        return f.read().strip()

What does this code do?

The load_criteria() function takes a file path, checks if the file exists, and returns its contents as a string. If the file is missing, it returns an empty string so the script still runs without crashing.

  • Save review.py and verify the loader works by running:
python -c "from review import load_criteria; print(load_criteria('criteria.txt'))"

You should see all 12 criteria rules printed in your terminal, confirming the function reads the file correctly.

Seeing a ModuleNotFoundError?

Make sure your virtual environment is still activated. You should see (venv) at the start of your terminal prompt.

If you see a syntax error, check that your function is indented correctly and that the # --- Criteria Loader --- comment is at the same level as the other section comments in review.py.

help me debug my load_criteria function

Define the personas and inject criteria

Each persona is a distinct reviewer with its own priorities and evaluation lens. You will store them in a dictionary that maps persona names to their system instructions.

  • In review.py, add the personas section directly below the load_criteria() function:
# --- Personas ---

PERSONAS = {
    "Grammar Editor": (
        "You are a professional grammar editor reviewing a resume. "
        "Evaluate the resume for: spelling errors, grammatical mistakes, "
        "punctuation issues, tense consistency (past tense for previous roles, "
        "present for current), awkward phrasing, and professional tone. "
        "Be specific about where errors occur and how to fix them."
    ),
    "ATS Scanner": (
        "You are an Applicant Tracking System (ATS) scanner analyzing a resume. "
        "Evaluate the resume for: keyword alignment with the target role, "
        "formatting compatibility (simple headers, no tables/columns that break parsing), "
        "presence of quantifiable achievements with metrics, "
        "standard section headers (Experience, Education, Skills), "
        "and appropriate keyword density without stuffing. "
        "Be specific about missing keywords and formatting issues."
    ),
    "Hiring Manager": (
        "You are a hiring manager reviewing a resume for the target role. "
        "Evaluate the resume for: relevance to the target role, "
        "demonstration of impact (not just responsibilities), "
        "career progression and growth signals, "
        "whether this candidate would get an interview, "
        "and any red flags (gaps, job hopping, vague descriptions). "
        "Be direct about whether you would advance this candidate and why."
    ),
}

What does each persona focus on?

The Grammar Editor hunts for spelling, punctuation, and tense errors. The ATS Scanner checks keyword alignment and formatting that automated tracking systems parse. The Hiring Manager evaluates whether you would actually get an interview.

Each persona instruction becomes the system_instruction for a separate Gemini API call. The model adopts that role for its entire evaluation.

  • Directly below the PERSONAS dictionary, add the function that combines a persona's specialty instruction with your baseline criteria:
def build_system_instruction(persona_instruction: str, criteria: str) -> str:
    if not criteria:
        return persona_instruction
    return (
        f"{persona_instruction}\n\n"
        "In addition to your specialty evaluation above, also check the resume "
        "against these universal baseline standards and flag any violations:\n\n"
        f"{criteria}"
    )

Why inject criteria into every persona?

Without this step, the Grammar Editor only checks grammar and the ATS Scanner only checks keywords. Your baseline criteria (like "must include quantified achievements") would be invisible to them.

By appending criteria to each persona's system instruction, every reviewer enforces your universal standards in addition to their specialty.

  • Save review.py and confirm everything compiles by running:
python -c "from review import PERSONAS, build_system_instruction; print(f'{len(PERSONAS)} personas defined')"

You should see 3 personas defined printed. This confirms your dictionary and function have no syntax errors.

Seeing a SyntaxError?

Check that each persona's string ends with a closing parenthesis and a comma. The dictionary must have a closing } after the last entry.

Also verify the build_system_instruction function is outside the dictionary, not nested inside it.

help me fix a syntax error in my PERSONAS dictionary

Loop through personas and print the aggregate report

You now have personas and criteria, but no way to run them in a loop. You need a reusable review function that calls the Gemini API for any persona, and an aggregate function that summarizes all results.

  • In review.py, add the review function between build_system_instruction() and the # --- Display --- comment:
# --- Review Function ---

def review_resume(client: genai.Client, resume_text: str, target_role: str, persona_name: str, system_instruction: str) -> ResumeReview:
    prompt = (
        f"Target Role: {target_role}\n\n"
        f"Resume Content:\n{resume_text}\n\n"
        "Review this resume from your perspective. "
        "Provide an overall score from 1-10, specific strengths, "
        "specific gaps or issues, actionable suggestions for improvement, "
        "and a brief summary of your assessment."
    )

    response = client.models.generate_content(
        model="gemini-2.5-flash",
        contents=prompt,
        config=types.GenerateContentConfig(
            system_instruction=system_instruction,
            response_mime_type="application/json",
            response_json_schema=ResumeReview.model_json_schema(),
            temperature=0.3,
        ),
    )

    return ResumeReview.model_validate_json(response.text)

What does this code do?

This is the same API call logic you had inline in main(), now extracted into a reusable function. It takes any persona's system instruction, builds the prompt, calls gemini-2.5-flash, and returns a validated ResumeReview object.

By making this a standalone function, you can call it once for each persona in a loop instead of duplicating the API call code three times.

  • Directly below the print_review() function, add the aggregate display function:
def print_aggregate(reviews: dict[str, ResumeReview]):
    scores = [r.overall_score for r in reviews.values()]
    avg_score = sum(scores) / len(scores)

    print(f"\n{'='*60}")
    print(f"  AGGREGATE SUMMARY")
    print(f"{'='*60}")
    print(f"\n  Average Score: {avg_score:.1f}/10")

    all_gaps = []
    for name, review in reviews.items():
        for gap in review.gaps:
            all_gaps.append(f"[{name}] {gap}")

    print(f"\n  Top Issues Across All Reviewers:")
    for gap in all_gaps[:5]:
        print(f"    ! {gap}")

    print(f"\n  Priority Actions:")
    all_suggestions = []
    for name, review in reviews.items():
        if review.suggestions:
            all_suggestions.append(review.suggestions[0])

    for i, suggestion in enumerate(all_suggestions, 1):
        print(f"    {i}. {suggestion}")

What does print_aggregate do?

After all three personas finish, this function computes the average score across reviewers, collects the top 5 issues flagged by any reviewer, and pulls the #1 suggestion from each persona into a priority action list.

This gives you a quick at-a-glance summary without scrolling through each individual review.

  • Save review.py and confirm the new functions load without errors:
python -c "from review import review_resume, print_aggregate; print('Review functions ready')"

You should see Review functions ready printed. Now you need to rewire main() to use the persona loop.

  • Delete your entire existing main() function (everything from def main(): down to the last line before if __name__ == "__main__":).
  • Replace it with the new main() function that handles arguments and extraction:
# --- Main ---

def main():
    if len(sys.argv) < 3:
        print("Usage: python review.py <resume.pdf> <target_role>")
        print('Example: python review.py resume.pdf "Senior Backend Engineer"')
        sys.exit(1)

    pdf_path = sys.argv[1]
    target_role = sys.argv[2]

    if not os.path.exists(pdf_path):
        print(f"Error: File '{pdf_path}' not found.")
        sys.exit(1)

    # Extract text
    print(f"Extracting text from: {pdf_path}")
    resume_text = extract_text_from_pdf(pdf_path)

    if not resume_text.strip():
        print("Error: No text could be extracted from the PDF.")
        sys.exit(1)

    print(f"Extracted {len(resume_text)} characters from resume.")
    print(f"Target role: {target_role}")
  • Continue the main() function by adding this code directly below (still inside the function, with the same indentation):
    # Load baseline criteria
    criteria_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "criteria.txt")
    criteria = load_criteria(criteria_path)
    if criteria:
        print(f"Loaded baseline criteria from: criteria.txt")
    else:
        print("No criteria.txt found. Running without baseline criteria.")

    print(f"\nRunning {len(PERSONAS)} reviewer personas...")

    # Initialize client
    api_key = os.environ.get("GEMINI_API_KEY")
    if not api_key:
        # Try loading from .env file
        env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")
        if os.path.exists(env_path):
            with open(env_path) as f:
                for line in f:
                    if line.strip().startswith("GEMINI_API_KEY="):
                        api_key = line.strip().split("=", 1)[1]
                        break
  • Finish the main() function by adding the error check, persona loop, and aggregate call:
    if not api_key:
        print("Error: GEMINI_API_KEY not found. Set it in .env or as an environment variable.")
        sys.exit(1)

    client = genai.Client(api_key=api_key)

    # Run reviews
    reviews = {}
    for persona_name, persona_instruction in PERSONAS.items():
        print(f"\n  Reviewing as: {persona_name}...")
        system_instruction = build_system_instruction(persona_instruction, criteria)
        review = review_resume(client, resume_text, target_role, persona_name, system_instruction)
        reviews[persona_name] = review
        print_review(persona_name, review)

    # Aggregate
    print_aggregate(reviews)
    print(f"\n{'='*60}")
    print("  Review complete!")
    print(f"{'='*60}\n")

What changed in main()?

The hardcoded single-persona API call is gone. Instead, main() loads your criteria file, then loops through every persona in the PERSONAS dictionary.

For each persona, it builds a combined system instruction (specialty + baseline criteria), calls review_resume(), prints that persona's results, and stores them. After the loop, print_aggregate() summarizes everything.

  • Make sure the if __name__ == "__main__": block is still at the bottom of the file after your new main() function.
  • Save review.py.

Before you run this, predict: how many separate review sections will print in your terminal?

  • Run the multi-persona review against your resume:
python review.py your_resume.pdf "Senior Backend Engineer"

You should see three separate review sections printed (Grammar Editor, ATS Scanner, Hiring Manager), each with their own score, strengths, gaps, and suggestions. After all three, an aggregate summary shows the average score and top issues across all reviewers.

Seeing an API error or timeout?

The free tier of the Gemini API limits you to 10 requests per minute. Since this script makes 3 requests in quick succession, you may hit rate limits if you run it multiple times rapidly. Wait 60 seconds and try again.

If you see a NameError for PERSONAS or build_system_instruction, check that these are defined above main() in review.py. Python reads top-to-bottom, so functions must be defined before they are called.

help me debug my multi-persona resume review

✔️ Awesome, I've got everything!

Great. Double check you have saved both criteria.txt and review.py.

ⓧ I'd like to double check the full code

Your review.py should now contain these sections in order. Scroll up and confirm each is present:

  • Imports: sys, os, json, pymupdf, BaseModel from pydantic, genai and types from google.
  • Schema section: ResumeReview Pydantic model with 5 fields.
  • PDF Extraction section: extract_text_from_pdf() function.
  • Criteria Loader section: load_criteria() function.
  • Personas section: PERSONAS dictionary with 3 entries, then build_system_instruction() function.
  • Review Function section: review_resume() function.
  • Display section: print_review() then print_aggregate().
  • Main section: main() with argument handling, criteria loading, persona loop, and aggregate print.
  • Entry point: if __name__ == "__main__": main() at the very bottom.

Your criteria.txt should contain exactly 12 lines, each starting with a - character.

Your resume review agent now evaluates from three professional angles simultaneously, enforcing your personal baseline criteria across all of them. Next up, you will take on the Secret Mission and add a fourth reviewer that adapts to the specific tech stack in your target role.

Secret mission

Add a Technical Reviewer Persona

Your three reviewers cover grammar, ATS compatibility, and hiring fit. But for technical roles, none of them evaluate whether your resume actually demonstrates the right tech stack depth. Can you add a fourth persona that dynamically adapts its evaluation criteria based on the target role's implied technologies?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally with a free-tier API key, so there are no ongoing costs. Decide whether to keep your resources for future resume reviews, or delete them entirely.

Resources you used:

  • The resume-reviewer project folder (contains review.py, criteria.txt, .env, test_setup.py, and the venv virtual environment).
  • A Gemini API key (free tier, no charges while unused).

Keep everything running

No action needed. Choose this if you plan to use the resume reviewer again.

  • Your project folder stays on your Desktop, ready to run anytime with python review.py.
  • The Gemini API key remains active on the free tier. It costs nothing while idle.
  • You can update criteria.txt anytime to refine your personal review standards.

Pause - I'll come back to this later

Free up terminal resources while keeping your files intact.

  • If your virtual environment is still activated, deactivate it by running:
  • Your project files remain on disk and the API key stays active (no cost).
  • When you return, reactivate with source venv/bin/activate (macOS/Linux) or venv\Scripts\activate (Windows).
deactivate

Delete - I don't want to use this again

Remove all project files and revoke your API key for a clean slate.

  • Delete the project folder by running this command from your Desktop:
# macOS/Linux
rm -rf ~/Desktop/resume-reviewer
# Windows
rmdir /s /q %USERPROFILE%\Desktop\resume-reviewer
  • Revoke your Gemini API key by opening the AI Studio API Keys page.
  • Find your key in the list, click the Actions icon, and select Delete API key.
  • Confirm deletion in the modal. It takes a few minutes to propagate.

That's a Wrap!

That's a Wrap!

Nice work. You just built a multi-persona resume review agent that evaluates any PDF resume from three professional angles and prints structured, actionable feedback in your terminal.

Here's what you learned:

  • Built a Python CLI tool that extracts text from PDF resumes using PyMuPDF and sends it to the Google Gemini API for AI-powered review.
  • Implemented structured JSON output with Pydantic schemas to enforce consistent, parseable reviews from every persona.
  • Designed a multi-persona review system with three distinct reviewers (Grammar Editor, ATS Scanner, Hiring Manager) that each enforce your personal baseline criteria alongside their specialty evaluation.
  • Secret Mission: Added a dynamic Technical Reviewer persona that adapts its evaluation criteria based on the target role's tech stack.

Ready to quiz yourself?