Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

## Overview

Hiring 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.
Hiring 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. Optionally, you can include a Target Role Description to get a Role Fit score.

---

Expand All @@ -52,7 +52,7 @@ Hiring Agent parses a resume PDF to Markdown, extracts sectioned JSON using a lo
2. `pdf.py` calls the LLM per section using Jinja templates under `prompts/templates`.
3. `github.py` fetches profile and repos, classifies projects, and asks the LLM to select the top 7.
4. `evaluator.py` runs a strict-scored evaluation with fairness constraints.
5. `score.py` orchestrates everything end to end and writes CSV when development mode is on.
5. `score.py` orchestrates everything end to end and writes CSV when development mode is on. If a Target Role Description is provided, Role Fit is computed and included in outputs.

</td>
<td>
Expand All @@ -70,6 +70,7 @@ Hiring Agent parses a resume PDF to Markdown, extracts sectioned JSON using a lo

- `prompts/`
All Jinja templates for extraction and scoring.
Includes an optional `role_fit.jinja` used when Role Fit scoring is requested.

</td>
</tr>
Expand Down Expand Up @@ -215,6 +216,18 @@ What happens:
2. If a GitHub profile is found in the resume, repositories are fetched and cached to `cache/githubcache_<basename>.json`.
3. The evaluator prints a report and, in development mode, appends a CSV row to `resume_evaluations.csv`.

### Role Fit (optional)

Provide a plain-text file describing the target role. When supplied, a Role Fit score (0–20) will be computed and displayed, and two new CSV columns will be added.

```bash
$ python score.py /path/to/resume.pdf /path/to/role.txt
```

Outputs affected:
- Console: an additional "Role Fit" category with score and evidence.
- CSV: new columns `role_fit_score`, `role_fit_max`.

---

## Directory layout
Expand All @@ -240,6 +253,7 @@ What happens:
│ ├── projects.jinja
│ ├── resume_evaluation_criteria.jinja
│ ├── resume_evaluation_system_message.jinja
│ ├── role_fit.jinja
│ ├── skills.jinja
│ ├── system_message.jinja
│ └── work.jinja
Expand Down
31 changes: 29 additions & 2 deletions evaluator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Dict, List, Optional, Tuple, Any
from pydantic import BaseModel, Field, field_validator
from models import JSONResume, EvaluationData
from models import JSONResume, EvaluationData, CategoryScore
from llm_utils import initialize_llm_provider, extract_json_from_response
import logging
import json
Expand All @@ -22,7 +22,7 @@


class ResumeEvaluator:
def __init__(self, model_name: str = DEFAULT_MODEL, model_params: dict = None):
def __init__(self, model_name: str = DEFAULT_MODEL, model_params: dict = None, role_description: Optional[str] = None):
if not model_name:
raise ValueError("Model name cannot be empty")

Expand All @@ -31,6 +31,7 @@ def __init__(self, model_name: str = DEFAULT_MODEL, model_params: dict = None):
model_name, {"temperature": 0.5, "top_p": 0.9}
)
self.template_manager = TemplateManager()
self.role_description = role_description
self._initialize_llm_provider()

def _initialize_llm_provider(self):
Expand Down Expand Up @@ -84,6 +85,32 @@ def evaluate_resume(self, resume_text: str) -> EvaluationData:
evaluation_dict = json.loads(response_text)
evaluation_data = EvaluationData(**evaluation_dict)

# If role_description provided, perform a separate role-fit scoring call
if self.role_description:
role_fit_prompt = self.template_manager.render_template(
"role_fit", text_content=resume_text, role_description=self.role_description
)
if role_fit_prompt:
role_chat_params = {
"model": self.model_name,
"messages": [
{"role": "system", "content": "You score role fit strictly as JSON."},
{"role": "user", "content": role_fit_prompt},
],
"options": {
"stream": False,
"temperature": self.model_params.get("temperature", 0.5),
"top_p": self.model_params.get("top_p", 0.9),
},
}
role_kwargs = {"format": CategoryScore.model_json_schema()}
role_resp = self.provider.chat(**role_chat_params, **role_kwargs)
role_text = extract_json_from_response(role_resp["message"]["content"])
role_data = CategoryScore(**json.loads(role_text))
# attach to scores if possible
if hasattr(evaluation_data, "scores") and evaluation_data.scores:
evaluation_data.scores.role_fit = role_data

return evaluation_data

except Exception as e:
Expand Down
1 change: 1 addition & 0 deletions models.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ class Scores(BaseModel):
self_projects: CategoryScore
production: CategoryScore
technical_skills: CategoryScore
role_fit: Optional[CategoryScore] = None


class BonusPoints(BaseModel):
Expand Down
1 change: 1 addition & 0 deletions prompts/template_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def _load_templates(self):
"github_project_selection": "github_project_selection.jinja",
"resume_evaluation_criteria": "resume_evaluation_criteria.jinja",
"resume_evaluation_system_message": "resume_evaluation_system_message.jinja",
"role_fit": "role_fit.jinja",
}

for section_name, filename in template_files.items():
Expand Down
23 changes: 23 additions & 0 deletions prompts/templates/role_fit.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
You are an assistant scoring a candidate's role fit.

Context:
{{ text_content }}

Target Role Description:
{{ role_description }}

Task:
Assess how well the candidate fits the target role based on skills, projects, and evidence from GitHub/blog/portfolio.
Provide a JSON object with fields:
{
"score": number, # 0-20
"max": 20,
"evidence": string # concise reasoning with cited evidence snippets
}

Scoring rubric (0-20):
- 0-5: Weak alignment; core required skills missing or shallow evidence
- 6-10: Partial alignment; some requirements met; limited production evidence
- 11-15: Good alignment; strong skills; relevant projects; moderate production signals
- 16-20: Excellent alignment; deep skills; multiple highly relevant projects; strong OSS/production evidence

16 changes: 16 additions & 0 deletions role.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
What You’ll Do:

Ship production-level code weekly across Python- and Go-based services, as well as a React/Next.js frontend screening millions of candidates.
Design and implement new microservices - including real-time ranking, interview analytics, and multi-million-dollar payout systems.
Collaborate with senior engineers to profile, test, and harden LLM-powered agents for accuracy, speed, and reliability.
Partner with end users to translate feedback into actionable technical specifications.
Own features end-to-end - from architecture and design to deployment and monitoring.


What We’re Looking For:

Strong Computer Science fundamentals with hands-on experience in Python, Go, or React.
Proven ability to iterate quickly — prototype, test, and ship production code in short cycles.
Passion for system performance and product quality, not just functionality.
Curiosity or familiarity with Large Language Models (LLMs), retrieval systems, or ranking algorithms.
Portfolio, GitHub, or side projects that demonstrate real-world problem-solving.
41 changes: 31 additions & 10 deletions score.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,14 @@ def print_evaluation_results(
max_score = 0

if hasattr(evaluation, "scores") and evaluation.scores:
for category_name, category_data in evaluation.scores.model_dump().items():
category_score = min(category_data["score"], category_data["max"])
total_score += category_score
max_score += category_data["max"]
for category_name, category_data in evaluation.scores.model_dump(exclude_none=True).items():
# Guard against malformed entries
if not isinstance(category_data, dict):
continue
if "score" in category_data and "max" in category_data:
category_score = min(category_data["score"], category_data["max"])
total_score += category_score
max_score += category_data["max"]

# Log warning if score was capped
if category_score < category_data["score"]:
Expand Down Expand Up @@ -82,6 +86,7 @@ def print_evaluation_results(
"self_projects": 30,
"production": 25,
"technical_skills": 10,
"role_fit": 20,
}

# Open Source
Expand Down Expand Up @@ -122,6 +127,14 @@ def print_evaluation_results(
print(f" Evidence: {tech_score.evidence}")
print()

# Role Fit
if hasattr(evaluation.scores, "role_fit") and evaluation.scores.role_fit:
rf_score = evaluation.scores.role_fit
capped_score = min(rf_score.score, category_maxes["role_fit"])
print(f"🎯 Role Fit: {capped_score}/{rf_score.max}")
print(f" Evidence: {rf_score.evidence}")
print()

# Bonus Points
if hasattr(evaluation, "bonus_points") and evaluation.bonus_points:
print(f"\n⭐ BONUS POINTS: {evaluation.bonus_points.total}")
Expand Down Expand Up @@ -160,12 +173,12 @@ def print_evaluation_results(


def _evaluate_resume(
resume_data: JSONResume, github_data: dict = None, blog_data: dict = None
resume_data: JSONResume, github_data: dict = None, blog_data: dict = None, role_description: str = None
) -> Optional[EvaluationData]:
"""Evaluate the resume using AI and display results."""

model_params = MODEL_PARAMETERS.get(DEFAULT_MODEL)
evaluator = ResumeEvaluator(model_name=DEFAULT_MODEL, model_params=model_params)
evaluator = ResumeEvaluator(model_name=DEFAULT_MODEL, model_params=model_params, role_description=role_description)

# Convert JSON resume data to text
resume_text = convert_json_resume_to_text(resume_data)
Expand Down Expand Up @@ -197,7 +210,7 @@ def find_profile(profiles, network):
)


def main(pdf_path):
def main(pdf_path, role_description: str = None):
# Create cache filename based on PDF path
cache_filename = (
f"cache/resumecache_{os.path.basename(pdf_path).replace('.pdf', '')}.json"
Expand Down Expand Up @@ -255,7 +268,7 @@ def main(pdf_path):
encoding='utf-8'
)

score = _evaluate_resume(resume_data, github_data)
score = _evaluate_resume(resume_data, github_data, role_description=role_description)

# Get candidate name for display
candidate_name = os.path.basename(pdf_path).replace(".pdf", "")
Expand Down Expand Up @@ -298,12 +311,20 @@ def main(pdf_path):

if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python score.py <pdf_path>")
print("Usage: python score.py <pdf_path> [role_description_file]")
exit(1)
pdf_path = sys.argv[1]
role_desc = None
if len(sys.argv) >= 3:
role_file = sys.argv[2]
if os.path.exists(role_file):
try:
role_desc = Path(role_file).read_text(encoding='utf-8')
except Exception:
role_desc = None

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

main(pdf_path)
main(pdf_path, role_description=role_desc)
10 changes: 10 additions & 0 deletions transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,14 @@ def transform_evaluation_response(
csv_row["technical_skills_score"] = scores.technical_skills.score
csv_row["technical_skills_max"] = scores.technical_skills.max

# Role fit
if hasattr(scores, "role_fit") and scores.role_fit:
csv_row["role_fit_score"] = scores.role_fit.score
csv_row["role_fit_max"] = scores.role_fit.max
else:
csv_row["role_fit_score"] = "N/A"
csv_row["role_fit_max"] = "N/A"

total_score = (
scores.open_source.score
+ scores.self_projects.score
Expand All @@ -709,6 +717,8 @@ def transform_evaluation_response(
csv_row["production_max"] = "N/A"
csv_row["technical_skills_score"] = "N/A"
csv_row["technical_skills_max"] = "N/A"
csv_row["role_fit_score"] = "N/A"
csv_row["role_fit_max"] = "N/A"
csv_row["total_score"] = "N/A"
csv_row["total_max"] = "N/A"

Expand Down