hiring-agent

CLI Tool

interviewstreet/hiring-agent

AI agent to evaluate and score resumes from PDFs.

Overview

Hiring Agent parses resume PDFs, extracts structured data using LLMs, enriches with GitHub signals, and outputs fair, explainable scores. Runs fully local with Ollama or uses Google Gemini.

README Preview

# Hiring Agent\n\nResume-to-Score pipeline that extracts structured data from PDFs, enriches with GitHub signals, and outputs a fair, explainable evaluation.\n\n\n  \n    \n  \n  \n    \n  \n  \n    \n  \n\n\n---\n\n## Contents\n\n- [Overview](#overview)\n- [Architecture](#architecture)\n- [Installation and Setup](#installation-and-setup)\n  - [Prerequisites](#prerequisites)\n  - [Quick setup with pip](#quick-setup-with-pip)\n  - [Ollama models](#ollama-models)\n- [Configuration](#configuration)\n- [How it works](#how-it-works)\n- [CLI usage](#cli-usage)\n- [Directory layout](#directory-layout)\n- [Provider details](#provider-details)\n- [Contributing](#contributing)\n- [License](#license)\n\n---\n\n## Overview\n\nHiring Agent parses a resume PDF to Markdown, extracts sectioned JSON using a local or hosted LLM, augments the data with GitHub profile and repository signals, then produces an objective evaluation with category scores, evidence, bonus points, and deductions. You can run fully local with Ollama or use Google Gemini.\n\n---\n\n## Architecture\n\n\n\n\n\n**Flow**\n\n1. `pymupdf_rag.py` converts PDF pages to Markdown-like text.\n2. `pdf.py` calls the LLM per section using Jinja templates under `prompts/templates`.\n3. `github.py` fetches profile and repos, classifies projects, and asks the LLM to select the top 7.\n4. `evaluator.py` runs a strict-scored evaluation with fairness constraints.\n5. `score.py` orchestrates everything end to end and writes CSV when development mode is on.\n\n\n\n\n**Key modules**\n\n- `models.py`\n  Pydantic schemas and LLM provider interfaces.\n\n- `llm_utils.py`\n  Provider initialization and response cleanup.\n\n- `transform.py`\n  Normalization from loose LLM JSON to JSON Resume style.\n\n- `prompts/`\n  All Jinja templates for extraction and scoring.\n\n\n\n\n\n---\n\n## Installation and Setup\n\n### Prerequisites\n\n- **Python 3.11+**\n\n  The repository pins `.python-version` to 3.11.13.\n\n- **One LLM backend** (either of 

FAQ (5)

Troubleshooting
How do I fix "NameError: name 'response' is not defined" in GitHub API error handling?

In fetch_all_github_repos in github.py, _fetch_github_api() returns (status_code, data), so do not access response.status_code or response.text. Replace the error-handling block with:

elif status_code == 404:
    print(f"GitHub user not found: {username}")
    return []
else:
    print(f"GitHub API error: {status_code} - {repos_data}")
    return []

This prevents the application from crashing with NameError when GitHub returns 404, rate-limit, network, or other API errors.

GitHub Issue #106
Troubleshooting
How do I fix a PyMuPDF memory leak when processing multiple PDF files in Python?

Close each PyMuPDF Document explicitly. In pdf.py, update extract_text_from_pdf() to use a context manager:

import fitz

def extract_text_from_pdf(path):
    with fitz.open(path) as doc:
        return "".join(page.get_text() for page in doc)

The with statement automatically closes the PDF after extraction, releasing memory. If you cannot use a context manager, call doc.close() in a finally block. This prevents memory from growing continuously when processing batches of 50+ PDFs with PyMuPDF 1.24.x on Python 3.11.

GitHub Issue #132
Troubleshooting
How do I fix "UnicodeEncodeError: 'charmap' codec can't encode" when writing JSON cache files on Windows with Python?

Explicitly set UTF-8 encoding in every Path.write_text() call that writes cache files. On Windows, the default encoding may be cp1252, which cannot encode emojis, accented names, or other Unicode characters.

Change:

Path(github_cache_filename).write_text(
    json.dumps(github_data, indent=2, ensure_ascii=False)
)

To:

Path(github_cache_filename).write_text(
    json.dumps(github_data, indent=2, ensure_ascii=False),
    encoding="utf-8"
)

Apply the same encoding="utf-8" argument to all cache-writing calls in github.py and score.py, including the GitHub API cache around github.py line 49. For additional robustness, cache reads can handle empty or corrupted JSON by rebuilding the cache. The fix was verified on Windows 11 with Python 3.11.

GitHub Issue #128
Troubleshooting
How do I fix Windows console UnicodeEncodeError and Python 3.14 Protobuf TypeError when running score.py?

Add the following before importing Protobuf or LLM libraries:

import os
import sys

# Use UTF-8 for Windows console output
if hasattr(sys.stdout, "reconfigure"):
    sys.stdout.reconfigure(encoding="utf-8", errors="replace")
if hasattr(sys.stderr, "reconfigure"):
    sys.stderr.reconfigure(encoding="utf-8", errors="replace")

# Force the pure-Python Protobuf implementation
os.environ.setdefault("PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION", "python")

The environment variable must be set before protobuf is imported. Alternatively, run:

$env:PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION="python"
python score.py

You can also enable UTF-8 mode with set PYTHONUTF8=1 or run chcp 65001 before starting the script. A fix is provided in PR #340.

GitHub Issue #339
Troubleshooting
Why does resume scoring fail with 'List should have at least 1 item' after a Gemini 429 error or Ollama timeout?

A failed LLM extraction can write incomplete data, such as empty lists or null fields, to cache/resumecache_<resume>.json. On later runs, the corrupted cache is loaded instead of reprocessing the PDF, causing the Pydantic ValidationError.

Immediate workaround:

rm cache/resumecache_resume.json
python score.py data/resume.pdf

Delete the cache file matching the affected PDF if its name differs. You can also temporarily set DEVELOPMENT_MODE = False to bypass cache loading.

Permanent fix: validate the extracted JSONResume object before writing it to disk, never cache failed extractions, and validate cached data on load. If cache validation fails, delete or ignore the file and run a fresh PDF extraction. This behavior is addressed in PR #198; update to a release containing that PR if available.

GitHub Issue #192