chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:29:44 +08:00
commit 3bbf895f5e
29 changed files with 5360 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# LLM Provider Configuration
# Options: "ollama" or "gemini"
LLM_PROVIDER=ollama
# Default model to use
# For Ollama: "gemma3:4b", "qwen3:4b", "mistral:7b", etc.
# For Gemini: "gemini-2.5-pro", "gemini-2.5-flash", etc.
DEFAULT_MODEL=gemma3:4b
# Google Gemini API Key (required if using Gemini provider)
GEMINI_API_KEY=your_gemini_api_key_here
+226
View File
@@ -0,0 +1,226 @@
# Project Specific Files
**/*.pyc
resume/*.pdf
run/*.pdf
test_*.py
cache/
resume_evaluations.csv
greenhouse_resumes/*
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
# Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
# poetry.lock
# poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
# pdm.lock
# pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
# pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# Redis
*.rdb
*.aof
*.pid
# RabbitMQ
mnesia/
rabbitmq/
rabbitmq-data/
# ActiveMQ
activemq-data/
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
# .idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
# Streamlit
.streamlit/secrets.toml
+1
View File
@@ -0,0 +1 @@
3.11.13
+69
View File
@@ -0,0 +1,69 @@
# Contributing Guidelines
Thanks for your interest in improving this project. Contributions are welcome, including documentation updates, bug reports, feature requests, and code changes.
## Reporting Bugs
1. Check that the bug has not already been reported: https://github.com/interviewstreet/hiring-agent/issues
2. Open a new bug report: https://github.com/interviewstreet/hiring-agent/issues/new
3. Please include:
- Clear description of the issue and expected behavior
- Environment info: OS, Python version, hiring-agent commit or version
- Steps to reproduce
- Relevant logs or stack traces
> [!TIP]
> If the bug involves PDF parsing or model output, attach a minimal PDF and the exact model you used.
## Feature Requests
1. Check for existing requests: https://github.com/interviewstreet/hiring-agent/issues
2. Open a new feature request: https://github.com/interviewstreet/hiring-agent/issues/new
3. Describe the problem, the proposed solution, and any alternatives you considered.
## Contributing Code
1. Pick an issue from https://github.com/interviewstreet/hiring-agent/issues or open one first.
2. Comment that you are working on it to avoid duplicate efforts.
3. Fork the repo, then create a feature branch for your change.
### Development
1. Fork and clone your fork.
2. Create a fresh branch per change. Avoid pushing changes to the default branch of your fork.
3. Set up the environment and run the pipeline locally to validate changes
4. Run the CLI on a small sample to verify behavior:
### Coding Style
* Use [Black](https://black.readthedocs.io/en/stable/) for formatting.
* Keep functions short and focused. Prefer pure helpers for prompt assembly and transformations.
### Prompts and Providers
* Keep prompts declarative and provider agnostic.
* Avoid model specific tokens or formatting that only one provider supports.
* Changes to prompts should include short before and after examples in the pull request description.
### Tests and Smoke Checks
* Validate changes with a couple of real resumes under different providers when possible:
* One run with Ollama using the default local model.
* One run with Gemini if you have an API key.
* Add or adjust small smoke tests that exercise each stage with minimal inputs:
* PDF to Markdown
* Section extraction to JSON Resume
* GitHub enrichment on a known username
* Evaluation to JSON with the required fields
### Commit Messages
* Use clear, imperative subjects, for example: `fix: handle en dash date ranges in work parser`.
* Reference the issue number when applicable.
## Code of Conduct
Be respectful and collaborative. If you see unacceptable behavior, report it through an issue or contact the maintainers.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 HackerRank
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+284
View File
@@ -0,0 +1,284 @@
# Hiring Agent
<p align="center"><strong>Resume-to-Score pipeline</strong> that extracts structured data from PDFs, enriches with GitHub signals, and outputs a fair, explainable evaluation.</p>
<p align="center">
<a href="https://www.python.org/downloads/release/python-3110/">
<img alt="Python" src="https://img.shields.io/badge/python-3.11%2B-blue.svg">
</a>
<a href="https://github.com/interviewstreet/hiring-agent/blob/master/LICENSE">
<img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-yellow.svg">
</a>
<a href="https://github.com/psf/black">
<img alt="Code style: Black" src="https://img.shields.io/badge/code%20style-Black-000000.svg">
</a>
</p>
---
## Contents
- [Overview](#overview)
- [Architecture](#architecture)
- [Installation and Setup](#installation-and-setup)
- [Prerequisites](#prerequisites)
- [Quick setup with pip](#quick-setup-with-pip)
- [Ollama models](#ollama-models)
- [Configuration](#configuration)
- [How it works](#how-it-works)
- [CLI usage](#cli-usage)
- [Directory layout](#directory-layout)
- [Provider details](#provider-details)
- [Contributing](#contributing)
- [License](#license)
---
## 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.
---
## Architecture
<table>
<tr>
<td>
**Flow**
1. `pymupdf_rag.py` converts PDF pages to Markdown-like text.
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.
</td>
<td>
**Key modules**
- `models.py`
Pydantic schemas and LLM provider interfaces.
- `llm_utils.py`
Provider initialization and response cleanup.
- `transform.py`
Normalization from loose LLM JSON to JSON Resume style.
- `prompts/`
All Jinja templates for extraction and scoring.
</td>
</tr>
</table>
---
## Installation and Setup
### Prerequisites
- **Python 3.11+**
The repository pins `.python-version` to 3.11.13.
- **One LLM backend** (either of them)
- **Ollama** for local models
Install from the [official site](https://ollama.com/), then run `ollama serve`.
- **Google Gemini** if you have an API key, get it from [here](https://aistudio.google.com/api-keys).
### Quick setup with pip
```bash
$ git clone https://github.com/interviewstreet/hiring-agent
$ cd hiring-agent
$ python -m venv .venv
# Linux or macOS
$ source .venv/bin/activate
# Windows
# .venv\Scripts\activate
$ pip install -r requirements.txt
```
### Ollama Models
Pull the model you want to use. For example:
```bash
$ ollama pull gemma3:4b
```
If you want different results, you can pull other models such as:
```bash
# For higher system configuration
$ ollama pull gemma3:12b
# For lower system configuration
$ ollama pull gemma3:1b
```
---
## Configuration
Copy the template and set your environment variables.
```bash
$ cp .env.example .env
```
**Environment variables**
| Variable | Values | Description |
| ---------------- | ------------------------------------------- | ---------------------------------------------------------------------- |
| `LLM_PROVIDER` | `ollama` or `gemini` | Chooses provider. Defaults to Ollama. |
| `DEFAULT_MODEL` | for example `gemma3:4b` or `gemini-2.5-pro` | Model name passed to the provider. |
| `GEMINI_API_KEY` | string | Required when `LLM_PROVIDER=gemini`. |
| `GITHUB_TOKEN` | optional | Inherits from your shell environment, improves GitHub API rate limits. |
Provider mapping lives in `prompt.py` and `models.py`. The `config.py` file has a single flag:
```python
# config.py
DEVELOPMENT_MODE = True # enables caching and CSV export
```
You can leave it on during iteration. See the next section for details.
---
## How it works
<details>
<summary><b>1) PDF extraction</b></summary>
- `pymupdf_rag.py` and `pdf.py` read the PDF using PyMuPDF and convert pages to Markdown-like text.
- The `to_markdown` routine handles headings, links, tables, and basic formatting.
</details>
<details>
<summary><b>2) Section parsing with templates</b></summary>
- `prompts/templates/*.jinja` define strict instructions for each section
Basics, Work, Education, Skills, Projects, Awards.
- `pdf.PDFHandler` calls the LLM per section and assembles a `JSONResume` object (see `models.py`).
</details>
<details>
<summary><b>3) GitHub enrichment</b></summary>
- `github.py` extracts a username from the resume profiles, fetches profile and repos, and classifies each project.
- It asks the LLM to select exactly 7 unique projects with a minimum author commit threshold, favoring meaningful contributions.
</details>
<details>
<summary><b>4) Evaluation</b></summary>
- `evaluator.py` uses templates that encode fairness and scoring rules.
- Scores include `open_source`, `self_projects`, `production`, and `technical_skills`, plus bonus and deductions, then an explanation for evidence.
</details>
<details>
<summary><b>5) Output and CSV export</b></summary>
- `score.py` prints a readable summary to stdout.
- When `DEVELOPMENT_MODE=True` it creates or appends a `resume_evaluations.csv` with key fields, and caches intermediate JSON under `cache/`.
</details>
---
## CLI usage
### End to end scoring
Provide a path to a resume PDF.
```bash
$ python score.py /path/to/resume.pdf
```
What happens:
1. If development mode is on, the PDF extraction result is cached to `cache/resumecache_<basename>.json`.
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`.
---
## Directory layout
```text
.
├── .env.example
├── .python-version
├── config.py
├── evaluator.py
├── github.py
├── llm_utils.py
├── models.py
├── pdf.py
├── prompt.py
├── prompts/
│ ├── template_manager.py
│ └── templates/
│ ├── awards.jinja
│ ├── basics.jinja
│ ├── education.jinja
│ ├── github_project_selection.jinja
│ ├── projects.jinja
│ ├── resume_evaluation_criteria.jinja
│ ├── resume_evaluation_system_message.jinja
│ ├── skills.jinja
│ ├── system_message.jinja
│ └── work.jinja
├── pymupdf_rag.py
├── requirements.txt
├── score.py
└── transform.py
```
---
## Provider details
### Ollama
- Set `LLM_PROVIDER=ollama`
- Set `DEFAULT_MODEL` to any pulled model, for example `gemma3:4b`
- The provider wrapper in `models.OllamaProvider` calls `ollama.chat`
### Gemini
- Set `LLM_PROVIDER=gemini`
- Set `DEFAULT_MODEL` to a supported Gemini model, for example `gemini-2.0-flash`
- Provide `GEMINI_API_KEY`
- The wrapper in `models.GeminiProvider` adapts responses to a unified format
---
## Contributing
Please read the [CONTRIBUTING.md](./CONTRIBUTING.md) for detailed guidelines on filing issues, proposing changes, and submitting pull requests. Key principles include:
- Keep prompts declarative and provider-agnostic.
- Validate changes with a couple of real resumes under different providers.
- Add or adjust unit-free smoke tests that call each stage with minimal inputs.
---
## License
[MIT](https://github.com/interviewstreet/hiring-agent/blob/master/LICENSE) © HackerRank
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`interviewstreet/hiring-agent`
- 原始仓库:https://github.com/interviewstreet/hiring-agent
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+6
View File
@@ -0,0 +1,6 @@
"""
Configuration settings for the hiring agent application.
"""
# Global development mode flag
DEVELOPMENT_MODE = True
+91
View File
@@ -0,0 +1,91 @@
from typing import Dict, List, Optional, Tuple, Any
from pydantic import BaseModel, Field, field_validator
from models import JSONResume, EvaluationData
from llm_utils import initialize_llm_provider, extract_json_from_response
import logging
import json
import re
MAX_BONUS_POINTS = 20
MIN_FINAL_SCORE = -20
MAX_FINAL_SCORE = 120
from prompt import (
DEFAULT_MODEL,
MODEL_PARAMETERS,
MODEL_PROVIDER_MAPPING,
GEMINI_API_KEY,
)
from prompts.template_manager import TemplateManager
logger = logging.getLogger(__name__)
class ResumeEvaluator:
def __init__(self, model_name: str = DEFAULT_MODEL, model_params: dict = None):
if not model_name:
raise ValueError("Model name cannot be empty")
self.model_name = model_name
self.model_params = model_params or MODEL_PARAMETERS.get(
model_name, {"temperature": 0.5, "top_p": 0.9}
)
self.template_manager = TemplateManager()
self._initialize_llm_provider()
def _initialize_llm_provider(self):
"""Initialize the appropriate LLM provider based on the model."""
self.provider = initialize_llm_provider(self.model_name)
def _load_evaluation_prompt(self, resume_text: str) -> str:
criteria_template = self.template_manager.render_template(
"resume_evaluation_criteria", text_content=resume_text
)
if criteria_template is None:
raise ValueError("Failed to load resume evaluation criteria template")
return criteria_template
def evaluate_resume(self, resume_text: str) -> EvaluationData:
self._last_resume_text = resume_text
full_prompt = self._load_evaluation_prompt(resume_text)
# logger.info(f"🔤 Evaluation prompt being sent: {full_prompt}")
try:
system_message = self.template_manager.render_template(
"resume_evaluation_system_message"
)
if system_message is None:
raise ValueError(
"Failed to load resume evaluation system message template"
)
# Prepare chat parameters
chat_params = {
"model": self.model_name,
"messages": [
{"role": "system", "content": system_message},
{"role": "user", "content": full_prompt},
],
"options": {
"stream": False,
"temperature": self.model_params.get("temperature", 0.5),
"top_p": self.model_params.get("top_p", 0.9),
},
}
# Add format parameter for structured output
kwargs = {"format": EvaluationData.model_json_schema()}
# Use the appropriate provider to make the API call
response = self.provider.chat(**chat_params, **kwargs)
response_text = response["message"]["content"]
response_text = extract_json_from_response(response_text)
logger.error(f"🔤 Prompt response: {response_text}")
evaluation_dict = json.loads(response_text)
evaluation_data = EvaluationData(**evaluation_dict)
return evaluation_data
except Exception as e:
logger.error(f"Error evaluating resume: {str(e)}")
raise
+496
View File
@@ -0,0 +1,496 @@
import os
import re
import json
import requests
import datetime
import time
from pathlib import Path
from typing import Dict, List, Optional, Any
from models import GitHubProfile
from pdf import logger
from prompts.template_manager import TemplateManager
from prompt import DEFAULT_MODEL, MODEL_PARAMETERS
from llm_utils import initialize_llm_provider, extract_json_from_response
from config import DEVELOPMENT_MODE
def _create_cache_filename(api_url: str, params: dict = None) -> str:
url_parts = api_url.replace("https://api.github.com/", "").replace("/", "_")
if params:
param_str = "_".join([f"{k}_{v}" for k, v in sorted(params.items())])
filename = f"cache/gh_githubcache_{url_parts}_{param_str}.json"
else:
filename = f"cache/gh_githubcache_{url_parts}.json"
return filename
def _fetch_github_api(api_url, params=None):
headers = {}
github_token = os.environ.get("GITHUB_TOKEN")
if github_token:
headers["Authorization"] = f"token {github_token}"
cache_filename = _create_cache_filename(api_url, params)
if DEVELOPMENT_MODE and os.path.exists(cache_filename):
print(f"Loading cached GitHub data from {cache_filename}")
try:
cached_data = json.loads(Path(cache_filename).read_text(encoding="utf-8"))
if not cached_data:
raise ValueError("Cached data is empty")
return 200, cached_data
except Exception as e:
print(f"⚠️ Warning: Error reading cache file {cache_filename}: {e}")
try:
os.remove(cache_filename)
except Exception as delete_err:
print(
f"Failed to delete invalid cache file {cache_filename}: {delete_err}"
)
response = requests.get(api_url, params, timeout=10, headers=headers)
status_code = response.status_code
# Check GitHub rate limit headers
rate_limit_remaining = response.headers.get("X-RateLimit-Remaining")
rate_limit_limit = response.headers.get("X-RateLimit-Limit")
rate_limit_reset = response.headers.get("X-RateLimit-Reset")
logger.info(
f"{rate_limit_remaining}/{rate_limit_limit}. Reset at {rate_limit_reset}"
)
if rate_limit_remaining is not None and rate_limit_limit is not None:
remaining = int(rate_limit_remaining)
limit = int(rate_limit_limit)
# Log rate limit information and handle proactively
if remaining < 10 and rate_limit_reset:
reset_timestamp = int(rate_limit_reset)
current_timestamp = int(time.time())
wait_seconds = (
max(0, reset_timestamp - current_timestamp) + 5
) # Add 5 second buffer
reset_time = datetime.datetime.fromtimestamp(reset_timestamp)
# Cap maximum wait time at 1 hour
max_wait = 3600
if wait_seconds > max_wait:
print(
f"⚠️ Rate limit reset time is too far in the future ({wait_seconds}s). Capping wait to {max_wait}s"
)
wait_seconds = max_wait
logger.error(
f"⚠️ GitHub API rate limit low: {remaining}/{limit} requests remaining. Resets at {reset_time}"
)
print(
f"💡 Tip: Set GITHUB_TOKEN environment variable to increase rate limits (60/hour → 5000/hour)"
)
if wait_seconds > 0:
logger.info(
f"⏳ Proactively sleeping for {wait_seconds} seconds until rate limit resets..."
)
time.sleep(wait_seconds)
print(f"✅ Rate limit should be reset now. Continuing...")
elif remaining < 100:
logger.info(
f"️ GitHub API rate limit: {remaining}/{limit} requests remaining"
)
data = response.json() if response.status_code == 200 else {}
if DEVELOPMENT_MODE and status_code == 200:
try:
os.makedirs("cache", exist_ok=True)
Path(cache_filename).write_text(
json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8"
)
except Exception as e:
logger.error(f"Error caching GitHub data to {cache_filename}: {e}")
return status_code, data
def extract_github_username(github_url: str) -> Optional[str]:
if not github_url:
return None
github_url = github_url.replace(" ", "")
github_url = github_url.strip()
patterns = [
r"https?://github\.com/([^/]+)",
r"github\.com/([^/]+)",
r"@([^/]+)",
r"^([a-zA-Z0-9-]+)$",
]
for pattern in patterns:
match = re.search(pattern, github_url)
if match:
username = match.group(1)
# Remove query parameters if present (e.g., "?tab=repositories")
if "?" in username:
username = username.split("?", 1)[0]
return username
return None
def fetch_github_profile(github_url: str) -> Optional[GitHubProfile]:
try:
username = extract_github_username(github_url)
logger.info(f"{username}")
if not username:
print(f"Could not extract username from: {github_url}")
return None
api_url = f"https://api.github.com/users/{username}"
status_code, data = _fetch_github_api(api_url)
if status_code == 200:
profile = GitHubProfile(
username=username,
name=data.get("name"),
bio=data.get("bio"),
location=data.get("location"),
company=data.get("company"),
public_repos=data.get("public_repos"),
followers=data.get("followers"),
following=data.get("following"),
created_at=data.get("created_at"),
updated_at=data.get("updated_at"),
avatar_url=data.get("avatar_url"),
blog=data.get("blog"),
twitter_username=data.get("twitter_username"),
hireable=data.get("hireable"),
)
return profile
elif status_code == 404:
print(f"GitHub user not found: {username}")
return None
else:
print(f"GitHub API error: {status_code} - {data}")
return None
except requests.exceptions.RequestException as e:
print(f"Error fetching GitHub profile: {e}")
return None
except Exception as e:
print(f"Unexpected error fetching GitHub profile: {e}")
return None
def fetch_contributions_count(owner: str, contributors_data):
user_contributions = 0
total_contributions = 0
for contributor in contributors_data:
if isinstance(contributor, dict):
contributions = contributor.get("contributions", 0)
total_contributions += contributions
if contributor.get("login", "").lower() == owner.lower():
user_contributions = contributions
return user_contributions, total_contributions
def fetch_repo_contributors(owner: str, repo_name: str) -> list[dict]:
try:
api_url = f"https://api.github.com/repos/{owner}/{repo_name}/contributors"
status_code, contributors_data = _fetch_github_api(api_url)
if status_code == 200:
return contributors_data
else:
return []
except Exception as e:
logger.error(f"Error fetching contributors for {owner}/{repo_name}: {e}")
return []
def fetch_all_github_repos(github_url: str, max_repos: int = 100) -> List[Dict]:
try:
username = extract_github_username(github_url)
if not username:
print(f"Could not extract username from: {github_url}")
return []
api_url = f"https://api.github.com/users/{username}/repos"
params = {"sort": "updated", "per_page": min(max_repos, 100), "type": "all"}
status_code, repos_data = _fetch_github_api(api_url, params=params)
if status_code == 200:
projects = []
for repo in repos_data:
if repo.get("fork") and repo.get("forks_count", 0) < 5:
continue
repo_name = repo.get("name")
contributors_data = fetch_repo_contributors(username, repo_name)
contributor_count = len(contributors_data)
user_contributions, total_contributions = fetch_contributions_count(
username, contributors_data
)
project_type = (
"open_source" if contributor_count > 1 else "self_project"
)
project = {
"name": repo.get("name"),
"description": repo.get("description"),
"github_url": repo.get("html_url"),
"live_url": repo.get("homepage") if repo.get("homepage") else None,
"technologies": (
[repo.get("language")] if repo.get("language") else []
),
"project_type": project_type,
"contributor_count": contributor_count,
"author_commit_count": user_contributions,
"total_commit_count": total_contributions,
"github_details": {
"stars": repo.get("stargazers_count", 0),
"forks": repo.get("forks_count", 0),
"language": repo.get("language"),
"description": repo.get("description"),
"created_at": repo.get("created_at"),
"updated_at": repo.get("updated_at"),
"topics": repo.get("topics", []),
"open_issues": repo.get("open_issues_count", 0),
"size": repo.get("size", 0),
"fork": repo.get("fork", False),
"archived": repo.get("archived", False),
"default_branch": repo.get("default_branch"),
"contributors": contributor_count,
},
}
projects.append(project)
projects.sort(key=lambda x: x["github_details"]["stars"], reverse=True)
open_source_count = sum(
1 for p in projects if p["project_type"] == "open_source"
)
self_project_count = sum(
1 for p in projects if p["project_type"] == "self_project"
)
print(f"✅ Found {len(projects)} repositories")
print(
f"📊 Project classification: {open_source_count} open source, {self_project_count} self projects"
)
return projects
elif status_code == 404:
print(f"GitHub user not found: {username}")
return []
else:
print(f"GitHub API error: {status_code} - {repos_data}")
return []
except requests.exceptions.RequestException as e:
print(f"Error fetching GitHub repositories: {e}")
return []
except Exception as e:
print(f"Unexpected error fetching GitHub repositories: {e}")
return []
def generate_profile_json(profile: GitHubProfile) -> Dict:
if not profile:
return {}
profile_data = {
"username": profile.username,
"name": profile.name,
"bio": profile.bio,
"location": profile.location,
"company": profile.company,
"public_repos": profile.public_repos,
"followers": profile.followers,
"following": profile.following,
"created_at": profile.created_at,
"updated_at": profile.updated_at,
"avatar_url": profile.avatar_url,
"blog": profile.blog,
"twitter_username": profile.twitter_username,
"hireable": profile.hireable,
}
return profile_data
def generate_projects_json(projects: List[Dict]) -> List[Dict]:
if not projects:
return []
try:
projects_data = []
for project in projects:
if project.get("author_commit_count") == 0:
continue
project_data = {
"name": project.get("name"),
"description": project.get("description"),
"github_url": project.get("github_url"),
"live_url": project.get("live_url"),
"technologies": project.get("technologies", []),
"project_type": project.get("project_type", "self_project"),
"contributor_count": project.get("contributor_count", 1),
"author_commit_count": project.get("author_commit_count", 0),
"total_commit_count": project.get("total_commit_count", 0),
"github_details": project.get("github_details", {}),
}
projects_data.append(project_data)
projects_json = json.dumps(projects_data, indent=2)
template_manager = TemplateManager()
prompt = template_manager.render_template(
"github_project_selection", projects_data=projects_json
)
print(
f"🤖 Using LLM to select top 5 projects from {len(projects)} repositories..."
)
# Initialize the LLM provider
provider = initialize_llm_provider(DEFAULT_MODEL)
# Get model parameters
model_params = MODEL_PARAMETERS.get(
DEFAULT_MODEL, {"temperature": 0.1, "top_p": 0.9}
)
# Prepare chat parameters
chat_params = {
"model": DEFAULT_MODEL,
"messages": [
{
"role": "system",
"content": "You are an expert technical recruiter analyzing GitHub repositories to identify the most impressive projects. CRITICAL: You must select exactly 7 UNIQUE projects - no duplicates allowed. Each project must be different from the others.",
},
{"role": "user", "content": prompt},
],
"options": model_params,
}
# Call the LLM provider
response = provider.chat(**chat_params)
response_text = response["message"]["content"]
try:
response_text = response_text.strip()
response_text = extract_json_from_response(response_text)
selected_projects = json.loads(response_text)
unique_projects = []
seen_names = set()
for project in selected_projects:
project_name = project.get("name", "")
if project_name and project_name not in seen_names:
unique_projects.append(project)
seen_names.add(project_name)
if len(unique_projects) < 7:
print(
f"⚠️ LLM selected {len(selected_projects)} projects but {len(unique_projects)} are unique"
)
for project in projects_data:
if len(unique_projects) >= 7:
break
project_name = project.get("name", "")
if project_name and project_name not in seen_names:
unique_projects.append(project)
seen_names.add(project_name)
project_names = ", ".join(
[proj.get("name", "N/A") for proj in unique_projects]
)
print(
f"✅ LLM selected {len(unique_projects)} unique top projects: {project_names}"
)
return unique_projects
except json.JSONDecodeError as e:
print(f"ERROR: Error parsing LLM response: {e}")
print(f"ERROR: Raw response: {response_text}")
print("🔄 Falling back to first 7 projects")
return projects_data[:7]
except Exception as e:
print(f"Error using LLM for project selection: {e}")
print("🔄 Falling back to first 7 projects")
projects_data = []
for project in projects[:7]:
project_data = {
"name": project.get("name"),
"description": project.get("description"),
"github_url": project.get("github_url"),
"live_url": project.get("live_url"),
"technologies": project.get("technologies", []),
"project_type": project.get("project_type", "self_project"),
"contributor_count": project.get("contributor_count", 1),
"github_details": project.get("github_details", {}),
}
projects_data.append(project_data)
return projects_data
def fetch_and_display_github_info(github_url: str) -> Dict:
logger.info(f"{github_url}")
github_profile = fetch_github_profile(github_url)
if not github_profile:
print("\n❌ Failed to fetch GitHub profile details.")
return {}
print("🔍 Fetching all repository details...")
projects = fetch_all_github_repos(github_url)
if not projects:
print("\n❌ No repositories found or failed to fetch repository details.")
profile_json = generate_profile_json(github_profile)
projects_json = generate_projects_json(projects)
result = {
"profile": profile_json,
"projects": projects_json,
"total_projects": len(projects_json),
}
return result
def main(github_url):
result = fetch_and_display_github_info(github_url)
print("\n" + "=" * 60)
print("JSON DATA OUTPUT")
print("=" * 60)
print(json.dumps(result, indent=2, ensure_ascii=False))
print("=" * 60)
return result
if __name__ == "__main__":
main("https://github.com/PavitKaur05")
+62
View File
@@ -0,0 +1,62 @@
"""
Utility functions for LLM providers.
"""
import logging
from typing import Any, Dict, Optional
from models import ModelProvider, OllamaProvider, GeminiProvider
from prompt import MODEL_PROVIDER_MAPPING, GEMINI_API_KEY
logger = logging.getLogger(__name__)
def extract_json_from_response(response_text: str) -> str:
"""
Extract JSON content from markdown code blocks.
Args:
response_text: Text that may contain JSON wrapped in markdown code blocks
Returns:
Text with markdown code block syntax removed
"""
response_text = response_text.strip()
if "<think>" in response_text:
think_start = response_text.find("<think>")
think_end = response_text.find("</think>")
if think_start != -1 and think_end != -1:
response_text = response_text[:think_start] + response_text[think_end + 8 :]
# Remove leading ```json if present
if response_text.startswith("```json"):
response_text = response_text[7:]
# Remove trailing ``` if present
if response_text.endswith("```"):
response_text = response_text[:-3]
return response_text
def initialize_llm_provider(model_name: str) -> Any:
"""
Initialize the appropriate LLM provider based on the model name.
Args:
model_name: The name of the model to use
Returns:
An initialized LLM provider (either OllamaProvider or GeminiProvider)
"""
# Default to Ollama provider
provider = OllamaProvider()
# If using Gemini and API key is available, use Gemini provider
model_provider = MODEL_PROVIDER_MAPPING.get(model_name, ModelProvider.OLLAMA)
if model_provider == ModelProvider.GEMINI:
if not GEMINI_API_KEY:
logger.warning("⚠️ Gemini API key not found. Falling back to Ollama.")
else:
logger.info(f"🔄 Using Google Gemini API provider with model {model_name}")
provider = GeminiProvider(api_key=GEMINI_API_KEY)
else:
logger.info(f"🔄 Using Ollama provider with model {model_name}")
return provider
+391
View File
@@ -0,0 +1,391 @@
from typing import List, Optional, Dict, Tuple, Any, Protocol, runtime_checkable
from pydantic import BaseModel, Field, field_validator
from enum import Enum
class ModelProvider(Enum):
"""Enum for supported model providers."""
OLLAMA = "ollama"
GEMINI = "gemini"
@runtime_checkable
class LLMProvider(Protocol):
"""Protocol for LLM providers."""
def chat(
self,
model: str,
messages: List[Dict[str, str]],
options: Dict[str, Any] = None,
**kwargs
) -> Dict[str, Any]:
"""Send a chat request to the LLM provider."""
...
class Location(BaseModel):
"""Location information for JSON Resume format."""
address: Optional[str] = None
postalCode: Optional[str] = None
city: Optional[str] = None
countryCode: Optional[str] = None
region: Optional[str] = None
class Profile(BaseModel):
"""Social profile information for JSON Resume format."""
network: Optional[str] = None
username: Optional[str] = None
url: str
class Basics(BaseModel):
"""Basic information for JSON Resume format."""
name: str
email: Optional[str] = None
phone: Optional[str] = None
url: Optional[str] = None
summary: Optional[str] = None
location: Optional[Location] = None
profiles: Optional[List[Profile]] = None
class Work(BaseModel):
"""Work experience for JSON Resume format."""
name: Optional[str] = None
position: Optional[str] = None
url: Optional[str] = None
startDate: Optional[str] = None
endDate: Optional[str] = None
summary: Optional[str] = None
highlights: Optional[List[str]] = None
class Volunteer(BaseModel):
"""Volunteer experience for JSON Resume format."""
organization: Optional[str] = None
position: Optional[str] = None
url: Optional[str] = None
startDate: Optional[str] = None
endDate: Optional[str] = None
summary: Optional[str] = None
highlights: Optional[List[str]] = None
class Education(BaseModel):
"""Education information for JSON Resume format."""
institution: Optional[str] = None
url: Optional[str] = None
area: Optional[str] = None
studyType: Optional[str] = None
startDate: Optional[str] = None
endDate: Optional[str] = None
score: Optional[str] = None
courses: Optional[List[str]] = None
class Award(BaseModel):
"""Award information for JSON Resume format."""
title: Optional[str] = None
date: Optional[str] = None
awarder: Optional[str] = None
summary: Optional[str] = None
class Certificate(BaseModel):
"""Certificate information for JSON Resume format."""
name: Optional[str] = None
date: Optional[str] = None
issuer: Optional[str] = None
url: Optional[str] = None
class Publication(BaseModel):
"""Publication information for JSON Resume format."""
name: Optional[str] = None
publisher: Optional[str] = None
releaseDate: Optional[str] = None
url: Optional[str] = None
summary: Optional[str] = None
class Skill(BaseModel):
"""Skill information for JSON Resume format."""
name: Optional[str] = None
level: Optional[str] = None
keywords: Optional[List[str]] = None
class Language(BaseModel):
"""Language information for JSON Resume format."""
language: Optional[str] = None
fluency: Optional[str] = None
class Interest(BaseModel):
"""Interest information for JSON Resume format."""
name: Optional[str] = None
keywords: Optional[List[str]] = None
class Reference(BaseModel):
"""Reference information for JSON Resume format."""
name: Optional[str] = None
reference: Optional[str] = None
class Project(BaseModel):
"""Project information for JSON Resume format."""
name: Optional[str] = None
startDate: Optional[str] = None
endDate: Optional[str] = None
description: Optional[str] = None
highlights: Optional[List[str]] = None
url: Optional[str] = None
technologies: Optional[List[str]] = None
skills: Optional[List[str]] = None
class BasicsSection(BaseModel):
"""Basics section containing basic information."""
basics: Optional[Basics] = None
class WorkSection(BaseModel):
"""Work section containing a list of work experiences."""
work: Optional[List[Work]] = None
class EducationSection(BaseModel):
"""Education section containing a list of education entries."""
education: Optional[List[Education]] = None
class SkillsSection(BaseModel):
"""Skills section containing a list of skill categories."""
skills: Optional[List[Skill]] = None
class ProjectsSection(BaseModel):
"""Projects section containing a list of projects."""
projects: Optional[List[Project]] = None
class AwardsSection(BaseModel):
"""Awards section containing a list of awards."""
awards: Optional[List[Award]] = None
class JSONResume(BaseModel):
"""Complete JSON Resume format model."""
basics: Optional[Basics] = None
work: Optional[List[Work]] = None
volunteer: Optional[List[Volunteer]] = None
education: Optional[List[Education]] = None
awards: Optional[List[Award]] = None
certificates: Optional[List[Certificate]] = None
publications: Optional[List[Publication]] = None
skills: Optional[List[Skill]] = None
languages: Optional[List[Language]] = None
interests: Optional[List[Interest]] = None
references: Optional[List[Reference]] = None
projects: Optional[List[Project]] = None
class CategoryScore(BaseModel):
score: float = Field(ge=0, description="Score achieved in this category")
max: int = Field(gt=0, description="Maximum possible score")
evidence: str = Field(min_length=1, description="Evidence supporting the score")
class Scores(BaseModel):
open_source: CategoryScore
self_projects: CategoryScore
production: CategoryScore
technical_skills: CategoryScore
class BonusPoints(BaseModel):
total: float = Field(ge=0, le=20, description="Total bonus points")
breakdown: str = Field(description="Breakdown of bonus points")
class Deductions(BaseModel):
total: float = Field(
ge=0,
description="Total deduction points (stored as positive, applied as negative)",
)
reasons: str = Field(description="Reasons for deductions")
class EvaluationData(BaseModel):
scores: Scores
bonus_points: BonusPoints
deductions: Deductions
key_strengths: List[str] = Field(min_items=1, max_items=5)
areas_for_improvement: List[str] = Field(min_items=1, max_items=5)
class GitHubProfile(BaseModel):
"""Pydantic model for GitHub profile data."""
username: str
name: Optional[str] = None
bio: Optional[str] = None
location: Optional[str] = None
company: Optional[str] = None
public_repos: Optional[int] = None
followers: Optional[int] = None
following: Optional[int] = None
created_at: Optional[str] = None
updated_at: Optional[str] = None
avatar_url: Optional[str] = None
blog: Optional[str] = None
twitter_username: Optional[str] = None
hireable: Optional[bool] = None
class OllamaProvider:
"""Ollama LLM provider implementation."""
def __init__(self):
import ollama
self.client = ollama
def chat(
self,
model: str,
messages: List[Dict[str, str]],
options: Dict[str, Any] = None,
**kwargs
) -> Dict[str, Any]:
"""Send a chat request to Ollama."""
ollama_options = options.copy() if options else {}
# remove steam from ollama options
ollama_options.pop("stream", None)
# Add num_ctx 32K context window to options
ollama_options["num_ctx"] = 32768
# convert to chat params
chat_params = {
"model": model,
"messages": messages,
"options": ollama_options,
}
# add it to top level
if "stream" in kwargs:
chat_params["stream"] = kwargs["stream"]
if "format" in kwargs:
chat_params["format"] = kwargs["format"]
return self.client.chat(**chat_params)
class GeminiProvider:
"""Google Gemini API provider implementation."""
def __init__(self, api_key: str):
import google.generativeai as genai
genai.configure(api_key=api_key)
self.client = genai
def chat(
self,
model: str,
messages: List[Dict[str, str]],
options: Dict[str, Any] = None,
**kwargs
) -> Dict[str, Any]:
"""Send a chat request to Google Gemini API."""
import re
import time
import random
from google.api_core.exceptions import ResourceExhausted
MAX_RETRIES = 5
BASE_DELAY = 10.0 # seconds — base for exponential backoff
MAX_DELAY = 120.0 # cap so we never wait more than 2 minutes
# Map options to Gemini parameters
generation_config = {}
if options:
if "temperature" in options:
generation_config["temperature"] = options["temperature"]
if "top_p" in options:
generation_config["top_p"] = options["top_p"]
# Create a Gemini model
gemini_model = self.client.GenerativeModel(
model_name=model, generation_config=generation_config
)
# Convert messages to Gemini format
gemini_messages = []
for msg in messages:
role = "user" if msg["role"] == "user" else "model"
gemini_messages.append({"role": role, "parts": [msg["content"]]})
for attempt in range(MAX_RETRIES):
try:
# Send the chat request
response = gemini_model.generate_content(gemini_messages)
# Convert Gemini response to Ollama-like format for compatibility
return {"message": {"role": "assistant", "content": response.text}}
except ResourceExhausted as e:
if attempt == MAX_RETRIES - 1:
# All retries exhausted — re-raise the original exception.
# This surfaces unrecoverable quota errors (RPD, TPM, etc.)
# instead of silently failing or returning bad data.
raise
# Parse the API-suggested retry delay from the error message
match = re.search(r"retry[_ ]in\s+([\d.]+)s", str(e), re.IGNORECASE)
api_hint = float(match.group(1)) if match else None
# Exponential backoff: BASE_DELAY * 2^attempt, capped at MAX_DELAY
exp_delay = min(BASE_DELAY * (2 ** attempt), MAX_DELAY)
# Prefer the API hint when it is shorter than our computed delay
delay = api_hint if (api_hint and api_hint < exp_delay) else exp_delay
# Add ±20% randomized jitter to avoid thundering herd
sleep_time = round(delay * random.uniform(0.8, 1.2), 2)
print(
f"[GeminiProvider] Rate limit hit "
f"(attempt {attempt + 1}/{MAX_RETRIES}). "
f"Retrying in {sleep_time}s..."
)
time.sleep(sleep_time)
+323
View File
@@ -0,0 +1,323 @@
import os
import sys
import json
import time
import logging
import pymupdf
from models import (
JSONResume,
Basics,
Work,
Education,
Skill,
Project,
Award,
BasicsSection,
WorkSection,
EducationSection,
SkillsSection,
ProjectsSection,
AwardsSection,
)
from llm_utils import initialize_llm_provider, extract_json_from_response
from pymupdf_rag import to_markdown
from typing import List, Optional, Dict, Any
from prompt import (
DEFAULT_MODEL,
MODEL_PARAMETERS,
MODEL_PROVIDER_MAPPING,
GEMINI_API_KEY,
)
from prompts.template_manager import TemplateManager
from transform import transform_parsed_data
logger = logging.getLogger(__name__)
class PDFHandler:
def __init__(self):
self.template_manager = TemplateManager()
self._initialize_llm_provider()
def _initialize_llm_provider(self):
"""Initialize the appropriate LLM provider based on the model."""
self.provider = initialize_llm_provider(DEFAULT_MODEL)
def extract_text_from_pdf(self, pdf_path: str) -> Optional[str]:
try:
if not os.path.exists(pdf_path):
raise FileNotFoundError(f"PDF file not found: {pdf_path}")
with pymupdf.open(pdf_path) as doc:
pages = range(doc.page_count)
resume_text = to_markdown(
doc,
pages=pages,
)
logger.debug(
f"Extracted text from PDF: {len(resume_text) if resume_text else 0} characters"
)
return resume_text
except Exception as e:
logger.error(f"An error occurred while reading the PDF: {e}")
return None
def _call_llm_for_section(
self, section_name: str, text_content: str, prompt: str, return_model=None
) -> Optional[Dict]:
try:
start_time = time.time()
logger.debug(
f"🔄 Extracting {section_name} section using {DEFAULT_MODEL}..."
)
model_params = MODEL_PARAMETERS.get(
DEFAULT_MODEL, {"temperature": 0.1, "top_p": 0.9}
)
section_system_message = self.template_manager.render_template(
"system_message", section_name_param=section_name
)
if not section_system_message:
logger.error(
f"❌ Failed to render system message template for {section_name}"
)
return None
chat_params = {
"model": DEFAULT_MODEL,
"messages": [
{"role": "system", "content": section_system_message},
{"role": "user", "content": prompt},
],
"options": {
"stream": False,
"temperature": model_params["temperature"],
"top_p": model_params["top_p"],
},
}
kwargs = {}
if return_model:
kwargs["format"] = return_model.model_json_schema()
# Use the appropriate provider to make the API call
response = self.provider.chat(**chat_params, **kwargs)
response_text = response["message"]["content"]
try:
response_text = extract_json_from_response(response_text)
json_start = response_text.find("{")
json_end = response_text.rfind("}")
if json_start != -1 and json_end != -1:
response_text = response_text[json_start : json_end + 1]
parsed_data = json.loads(response_text)
logger.debug(f"✅ Successfully extracted {section_name} section")
transformed_data = transform_parsed_data(parsed_data)
end_time = time.time()
total_time = end_time - start_time
logger.debug(
f"⏱️ Total time for separate section extraction: {total_time:.2f} seconds"
)
return transformed_data
except json.JSONDecodeError as e:
logger.error(f"❌ Error parsing JSON for {section_name} section: {e}")
logger.error(f"Raw response: {response_text}")
return None
except Exception as e:
logger.error(f"❌ Error calling LLM for {section_name} section: {e}")
return None
def extract_basics_section(self, resume_text: str) -> Optional[Dict]:
prompt = self.template_manager.render_template(
"basics", text_content=resume_text
)
if not prompt:
logger.error("❌ Failed to render basics template")
return None
return self._call_llm_for_section("basics", resume_text, prompt, BasicsSection)
def extract_work_section(self, resume_text: str) -> Optional[Dict]:
prompt = self.template_manager.render_template("work", text_content=resume_text)
if not prompt:
logger.error("❌ Failed to render work template")
return None
return self._call_llm_for_section("work", resume_text, prompt, WorkSection)
def extract_education_section(self, resume_text: str) -> Optional[Dict]:
prompt = self.template_manager.render_template(
"education", text_content=resume_text
)
if not prompt:
logger.error("❌ Failed to render education template")
return None
return self._call_llm_for_section(
"education", resume_text, prompt, EducationSection
)
def extract_skills_section(self, resume_text: str) -> Optional[Dict]:
prompt = self.template_manager.render_template(
"skills", text_content=resume_text
)
if not prompt:
logger.error("❌ Failed to render skills template")
return None
return self._call_llm_for_section("skills", resume_text, prompt, SkillsSection)
def extract_projects_section(self, resume_text: str) -> Optional[Dict]:
prompt = self.template_manager.render_template(
"projects", text_content=resume_text
)
if not prompt:
logger.error("❌ Failed to render projects template")
return None
return self._call_llm_for_section(
"projects", resume_text, prompt, ProjectsSection
)
def extract_awards_section(self, resume_text: str) -> Optional[Dict]:
prompt = self.template_manager.render_template(
"awards", text_content=resume_text
)
if not prompt:
logger.error("❌ Failed to render awards template")
return None
return self._call_llm_for_section("awards", resume_text, prompt, AwardsSection)
def extract_json_from_text(self, resume_text: str) -> Optional[JSONResume]:
try:
return self._extract_all_sections_separately(resume_text)
except Exception as e:
logger.error(f"Error calling Ollama: {e}")
return None
def extract_json_from_pdf(self, pdf_path: str) -> Optional[JSONResume]:
try:
logger.debug(f"📄 Extracting text from PDF: {pdf_path}")
text_content = self.extract_text_from_pdf(pdf_path)
if not text_content:
logger.error("❌ Failed to extract text from PDF")
return None
logger.debug(
f"✅ Successfully extracted {len(text_content)} characters from PDF"
)
logger.debug("🔄 Extracting all sections separately...")
return self._extract_all_sections_separately(text_content)
except Exception as e:
logger.error(f"❌ Error during PDF to JSON extraction: {e}")
return None
def _extract_section_data(
self, text_content: str, section_name: str, return_model=None
) -> Optional[Dict]:
section_extractors = {
"basics": self.extract_basics_section,
"work": self.extract_work_section,
"education": self.extract_education_section,
"skills": self.extract_skills_section,
"projects": self.extract_projects_section,
"awards": self.extract_awards_section,
}
if section_name not in section_extractors:
logger.error(f"❌ Invalid section name: {section_name}")
logger.error(f"Valid sections: {list(section_extractors.keys())}")
return None
return section_extractors[section_name](text_content)
def _extract_single_section(
self, text_content: str, section_name: str, return_model=None
) -> Optional[Dict]:
section_data = self._extract_section_data(
text_content, section_name, return_model
)
if section_data:
complete_resume = {
"basics": None,
"work": None,
"volunteer": None,
"education": None,
"awards": None,
"certificates": None,
"publications": None,
"skills": None,
"languages": None,
"interests": None,
"references": None,
"projects": None,
"meta": None,
}
complete_resume.update(section_data)
return complete_resume
return None
def _extract_all_sections_separately(
self, text_content: str
) -> Optional[JSONResume]:
start_time = time.time()
sections = ["basics", "work", "education", "skills", "projects", "awards"]
complete_resume = {
"basics": None,
"work": None,
"volunteer": None,
"education": None,
"awards": None,
"certificates": None,
"publications": None,
"skills": None,
"languages": None,
"interests": None,
"references": None,
"projects": None,
"meta": None,
}
for section_name in sections:
section_data = self._extract_section_data(text_content, section_name)
if section_data:
complete_resume.update(section_data)
logger.debug(f"✅ Successfully extracted {section_name} section")
else:
logger.error(
f"⚠️ Failed to extract {section_name} section. Aborting extraction to prevent partial/invalid resume data."
)
return None
try:
if complete_resume.get("basics") and isinstance(
complete_resume["basics"], dict
):
try:
complete_resume["basics"] = Basics(**complete_resume["basics"])
except Exception as e:
logger.error(f"❌ Error creating Basics object: {e}")
complete_resume["basics"] = None
json_resume = JSONResume(**complete_resume)
end_time = time.time()
total_time = end_time - start_time
logger.info(
f"⏱️ Total time for separate section extraction: {total_time:.2f} seconds"
)
return json_resume
except Exception as e:
logger.error(f"❌ Error creating JSONResume object: {e}")
return None
+67
View File
@@ -0,0 +1,67 @@
"""
Prompts for Resume Evaluation System
This module contains all the prompts used by the resume evaluation system.
Centralizing prompts here makes them easier to maintain and update.
"""
import os
from dotenv import load_dotenv
from models import ModelProvider
# Load environment variables
load_dotenv()
# Constants
DEFAULT_MODEL_NAME = "gemma3:4b"
DEFAULT_PROVIDER = ModelProvider.OLLAMA
# Get model and provider from environment or use defaults
DEFAULT_MODEL = os.getenv("DEFAULT_MODEL", DEFAULT_MODEL_NAME)
PROVIDER = os.getenv("LLM_PROVIDER", DEFAULT_PROVIDER.value)
# Validate provider
if PROVIDER not in [p.value for p in ModelProvider]:
PROVIDER = DEFAULT_PROVIDER.value
# Model-specific parameters
MODEL_PARAMETERS = {
# Ollama models
"qwen3:1.7b": {"temperature": 0.0, "top_p": 0.9},
"gemma3:1b": {"temperature": 0.0, "top_p": 0.9},
"qwen3:4b": {"temperature": 0.1, "top_p": 0.4},
"gemma3:4b": {"temperature": 0.1, "top_p": 0.9},
"gemma3:12b": {"temperature": 0.1, "top_p": 0.9},
"mistral:7b": {"temperature": 0.1, "top_p": 0.9},
# Google Gemini models
"gemini-2.0-flash": {"temperature": 0.1, "top_p": 0.9},
"gemini-2.0-flash-lite": {"temperature": 0.1, "top_p": 0.9},
"gemini-2.5-pro": {"temperature": 0.1, "top_p": 0.9},
"gemini-2.5-flash": {"temperature": 0.1, "top_p": 0.9},
"gemini-2.5-flash-lite": {"temperature": 0.1, "top_p": 0.9},
"gemini-3.5-flash": {"temperature": 0.1, "top_p": 0.9},
"gemini-3.1-flash-lite": {"temperature": 0.1, "top_p": 0.9},
}
# Model provider mapping
# Maps model names to their provider
MODEL_PROVIDER_MAPPING = {
# Ollama models
"qwen3:1.7b": ModelProvider.OLLAMA,
"gemma3:1b": ModelProvider.OLLAMA,
"qwen3:4b": ModelProvider.OLLAMA,
"gemma3:4b": ModelProvider.OLLAMA,
"gemma3:12b": ModelProvider.OLLAMA,
"mistral:7b": ModelProvider.OLLAMA,
# Google Gemini models
"gemini-2.0-flash": ModelProvider.GEMINI,
"gemini-2.0-flash-lite": ModelProvider.GEMINI,
"gemini-2.5-flash": ModelProvider.GEMINI,
"gemini-2.5-flash-lite": ModelProvider.GEMINI,
"gemini-2.5-pro": ModelProvider.GEMINI,
"gemini-3.5-flash": ModelProvider.GEMINI,
"gemini-3.1-flash-lite": ModelProvider.GEMINI,
}
# Get API keys from environment
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")
+90
View File
@@ -0,0 +1,90 @@
"""
Template Manager for Section Extraction
This module provides functionality to load and render Jinja templates for
section-specific resume extraction prompts.
"""
import os
from typing import Dict, Optional
from jinja2 import Environment, FileSystemLoader, Template
class TemplateManager:
"""
Manages Jinja templates for section-specific resume extraction.
This class provides functionality to load and render templates for
different resume sections (basics, work, education, skills, projects, awards).
"""
def __init__(self, template_dir: str = "prompts/templates"):
"""
Initialize the template manager.
Args:
template_dir (str): Directory containing Jinja templates
"""
self.template_dir = template_dir
self.env = Environment(
loader=FileSystemLoader(template_dir), trim_blocks=True, lstrip_blocks=True
)
self._templates: Dict[str, Template] = {}
self._load_templates()
def _load_templates(self):
"""Load all available templates."""
template_files = {
"basics": "basics.jinja",
"work": "work.jinja",
"education": "education.jinja",
"skills": "skills.jinja",
"projects": "projects.jinja",
"awards": "awards.jinja",
"system_message": "system_message.jinja",
"github_project_selection": "github_project_selection.jinja",
"resume_evaluation_criteria": "resume_evaluation_criteria.jinja",
"resume_evaluation_system_message": "resume_evaluation_system_message.jinja",
}
for section_name, filename in template_files.items():
try:
template_path = os.path.join(self.template_dir, filename)
if os.path.exists(template_path):
self._templates[section_name] = self.env.get_template(filename)
else:
print(f"⚠️ Template file not found: {template_path}")
except Exception as e:
print(f"❌ Error loading template {filename}: {e}")
def get_available_sections(self) -> list:
"""
Get list of available section names.
Returns:
list: List of available section names
"""
return list(self._templates.keys())
def render_template(self, section_name: str, **kwargs) -> Optional[str]:
"""
Render a template for a specific section.
Args:
section_name (str): Name of the section (basics, work, education, etc.)
**kwargs: Template variables (e.g., text_content)
Returns:
Optional[str]: Rendered template string, or None if template not found
"""
if section_name not in self._templates:
print(f"❌ Template not found for section: {section_name}")
print(f"Available sections: {self.get_available_sections()}")
return None
try:
template = self._templates[section_name]
return template.render(**kwargs)
except Exception as e:
print(f"❌ Error rendering template for {section_name}: {e}")
return None
+20
View File
@@ -0,0 +1,20 @@
Extract ONLY the awards and honors information from this resume.
--- The input markdown starts here ---
{{ text_content }}
--- The input markdown ends here ---
Return ONLY a JSON object with this structure:
{
"awards": [
{
"title": "Award name",
"date": "Award date (YYYY-MM)",
"awarder": "Awarding organization"
}
]
}
**IMPORTANT**: Return ONLY valid JSON. Do not include any explanatory text.
+55
View File
@@ -0,0 +1,55 @@
Extract ONLY the basic information (name, email, phone, location, profiles) from this resume.
--- The input resume markdown starts here ---
{{ text_content }}
--- The input resume markdown ends here ---
Return ONLY a JSON object with this structure:
{
"basics": {
"name": "Full name",
"email": "Email address",
"phone": "Phone number",
"url": null,
"summary": null,
"location": {
"city": "City",
"countryCode": "Country code"
},
"profiles": [
{
"network": "Platform name",
"url": "Full URL",
"username": "Username from URL"
}
]
}
}
**IMPORTANT**: If there is any About Me or Summary section, add that to the summary section of the basics
**CRITICAL**: For profiles section:
- ONLY extract URLs that are EXPLICITLY present in the resume markdown
- Look for URLs in markdown format [text](url) or plain URLs
- DO NOT create any URLs that are not in the original resume
- DO NOT add generic platform URLs like "https://github.com" or "https://linkedin.com" unless they are actually present in the resume
- If no URLs are found in the resume, return an empty profiles array: "profiles": []
- If a URL is present but you cannot determine the network or username, still include it with the available information
- If the URL is of the format github.io or personal domain, mark the network as "Portfolio"
- If there is any link at the header of the resume or Portfolio link, add that as url in the basics
**EXAMPLES OF WHAT NOT TO DO:**
- Do NOT add "https://github.com" if GitHub is not mentioned in the resume
- Do NOT add "https://linkedin.com" if LinkedIn is not mentioned in the resume
- Do NOT add "https://leetcode.com" if LeetCode is not mentioned in the resume
- Do NOT add "https://stackoverflow.com" if Stack Overflow is not mentioned in the resume
**EXAMPLES OF WHAT TO DO:**
- If resume contains "GitHub: https://github.com/username", extract it
- If resume contains "[My Portfolio](https://example.com)", extract it
- If resume contains "LinkedIn: linkedin.com/in/username", extract it
**IMPORTANT**: Return ONLY valid JSON. Do not include any explanatory text.
+23
View File
@@ -0,0 +1,23 @@
Extract ONLY the education information from this resume.
--- The input markdown starts here ---
{{ text_content }}
--- The input markdown ends here ---
Return ONLY a JSON object with this structure:
{
"education": [
{
"institution": "School/University name",
"area": "Field of study",
"studyType": "Degree type",
"startDate": "Start date (YYYY-MM)",
"endDate": "End date (YYYY-MM)",
"score": "GPA/Percentage"
}
]
}
**IMPORTANT**: Return ONLY valid JSON. Do not include any explanatory text.
@@ -0,0 +1,100 @@
You are an expert technical recruiter analyzing GitHub repositories to identify the most impressive and relevant projects for a software engineering position.
**ABSOLUTE REQUIREMENT**: You must ONLY select projects where the author_commit_count is 4 or higher. Projects with 1, 2, or 3 commits indicate minimal involvement and should NEVER be selected.
Given a list of GitHub repositories, select the TOP 7 most impressive projects that would be most relevant for evaluating a candidate's technical skills and experience.
**IMPORTANT: Contributions to Popular Open Source Projects**
- **HIGH PRIORITY**: Contributions to well-known, popular open source projects (1000+ stars) are extremely valuable, even if the contribution is small
- Popular projects include: React, Vue, Angular, Node.js, Express, Django, Flask, TensorFlow, PyTorch, Kubernetes, Docker, VS Code, etc.
- A small contribution to a popular project (bug fix, documentation, feature) is often more impressive than a complete personal project
- Look for repositories that are forks of popular projects where the candidate has made meaningful contributions
- Consider the impact and reach of the project, not just the size of the contribution
**Selection Criteria (in order of importance):**
1. **Author Contribution Level**: Projects where the candidate has made significant contributions (high author_commit_count) - HIGHEST PRIORITY
2. **Popular Open Source Contributions**: Contributions to well-known projects (1000+ stars) - HIGH PRIORITY
3. **Technical Complexity**: Projects that demonstrate advanced programming concepts, architecture, or problem-solving
4. **Real-world Impact**: Projects with actual users, deployments, or practical applications
5. **Code Quality**: Well-documented, maintained, and professional code
6. **Community Engagement**: Projects with stars, forks, or community contributions
7. **Technology Stack**: Projects using modern, relevant technologies
8. **Originality**: Unique projects rather than tutorial-based or classroom assignments
**Projects to PRIORITIZE:**
- Projects with high author_commit_count (15+ commits) - indicates substantial involvement and deep engagement
- Projects with moderate author_commit_count (5-14 commits) - shows meaningful contribution
- Contributions to popular open source projects (React, Vue, Angular, Node.js, Express, Django, Flask, TensorFlow, PyTorch, Kubernetes, Docker, VS Code, etc.)
- Forks of popular projects with meaningful contributions
- Projects with significant community adoption (100+ stars)
- Projects that solve real-world problems
- Well-documented and maintained projects
**Projects to AVOID:**
- Projects with very low author_commit_count (1-3 commits) - these indicate minimal involvement and should be avoided
- Simple tutorial projects (e.g., "Hello World", "HELLO-WORLD", basic calculators) - unless they're contributions to popular projects
- Classroom assignments with generic names
- Projects with very low stars/forks and no meaningful activity
- Very old projects with no recent activity
- Personal projects with no real-world impact (unless they demonstrate exceptional technical complexity)
**Repository Data:**
{{ projects_data }}
**FILTERING STEP:**
1. First, sort all projects by author_commit_count in descending order (highest to lowest)
2. Filter out all projects with author_commit_count less than 4
3. Only consider projects where the candidate has made at least 4 commits
4. Start selection from the top of the sorted list (highest commit counts first)
**CRITICAL REQUIREMENTS:**
- Select exactly 7 UNIQUE projects (no duplicates) if 7 or more qualifying projects exist
- If fewer than 7 qualifying projects exist, select ALL of them (do not pad with additional projects)
- Do not select the same repository multiple times
- Ensure all selected projects are distinct and represent different aspects of the candidate's skills
- **HARD REQUIREMENT**: Only select projects where author_commit_count is 4 or higher
- **HARD REQUIREMENT**: Do NOT select any project with author_commit_count of 1, 2, or 3
- **HARD REQUIREMENT**: Start selection from projects with the highest author_commit_count
Select exactly 7 unique projects that best represent the candidate's technical abilities.
**CRITICAL: Minimum Contribution Threshold**
- Only select projects where author_commit_count is 4 or higher
- Projects with 1-3 commits indicate minimal involvement and should be excluded
- Prioritize projects with 15+ commits (substantial involvement)
- Then prioritize projects with 5-14 commits (meaningful contribution)
**Available High-Contribution Projects:**
Look for projects with author_commit_count of 15 or higher first, then projects with 5-14 commits. Start your selection from projects with the highest commit counts.
Prioritize contributions to popular open source projects over personal projects. Respond with a JSON array containing only the selected project objects:
[
{
"name": "Project name",
"description": "Project description",
"github_url": "GitHub URL",
"live_url": "Live URL if available",
"technologies": ["tech1", "tech2"],
"reason_for_project_selection": "Reason why this project is selected",
"author_commit_count": 0,
"total_commit_count": 0,
"github_details": {
"stars": 0,
"forks": 0,
"language": "Primary language",
"description": "Description",
"created_at": "Creation date",
"updated_at": "Last updated date",
"topics": ["topic1", "topic2"],
"open_issues": 0,
"size": 0,
"fork": false,
"archived": false,
"default_branch": "main"
}
}
]
Respond only with valid JSON, no additional text.
+21
View File
@@ -0,0 +1,21 @@
Extract ONLY the projects information from this resume.
--- The input markdown starts here ---
{{ text_content }}
--- The input markdown ends here ---
Return ONLY a JSON object with this structure:
{
"projects": [
{
"name": "Project name",
"description": "Project description",
"url": "Project URL",
"technologies": ["Tech 1", "Tech 2"]
}
]
}
**IMPORTANT**: Return ONLY valid JSON. Do not include any explanatory text.
@@ -0,0 +1,185 @@
You are evaluating a resume for a Software Intern position at HackerRank. Analyze the resume data and provide scores based on these criteria:
**MANDATORY: You MUST always fill ALL FOUR categories: open_source, self_projects, production, technical_skills.**
## CRITICAL FAIRNESS REQUIREMENTS
**SCORES MUST NEVER DEPEND ON:**
- Candidate's name, gender, or personal demographic information
- College, university, or educational institution name
- CGPA, GPA, or academic grades
- City, location, or geographical information
- Any personal characteristics unrelated to technical skills and experience
**EVALUATION MUST BE BASED ONLY ON:**
- Technical skills and programming languages
- Project complexity and real-world impact
- Open source contributions and community involvement
- Work experience and production-level contributions
- Technical communication and documentation abilities
- Problem-solving and algorithmic thinking demonstrated in projects
## PROGRAM DISTINCTIONS
- "Google Summer of Code (GSoC)" and "Girl Script Summer of Code" are COMPLETELY DIFFERENT programs
- NEVER use "GSoC" as shorthand for "Girl Script Summer of Code"
- When you see "Girl Script Summer of Code" in the resume, refer to it as "Girl Script Summer of Code"
- When you see "Google Summer of Code" in the resume, refer to it as "Google Summer of Code (GSoC)"
## ANALYSIS INSTRUCTIONS
- Analyze the structured resume data (basics, work, volunteer, projects, skills, etc.)
- Use GitHub data (if provided in === GITHUB DATA === section) as additional context
- Use blog data (if provided in === BLOG DATA === section) for technical communication assessment
## SCORING CRITERIA
### Open Source (0-35 points)
**HIGH SCORES (25-35 points):**
- Contributions to popular open source projects (1000+ stars)
- Significant contributions to well-known projects
- Google Summer of Code (GSoC) participation
- Substantial community involvement
**MEDIUM SCORES (15-24 points):**
- Contributions to smaller open source projects
- Active GitHub presence with meaningful contributions to other repositories
- Participation in open source programs
**LOW SCORES (5-10 points):**
- Only personal GitHub repositories with no contributions to other projects
- Minimal open source activity
- Basic GitHub presence
- **CRITICAL**: Hacktoberfest participation alone (without evidence of contributions to significant projects) should receive 3-5 points maximum
**VERY LOW SCORES (0-4 points):**
- No GitHub presence
- Only very basic personal repositories
- Repositories that are clearly tutorial-based with no community involvement
**CRITICAL RULES:**
- Having personal GitHub repositories does NOT constitute open source contribution
- True open source contribution means contributing to OTHER people's projects
- When GitHub data shows all projects are 'self_project' type, open source score MUST be 10 points or less
### Self Projects (0-30 points)
**HIGH SCORES (20-30 points):**
- Complex projects with real-world impact
- Advanced architecture, multiple technologies
- User adoption or contributions to popular open source projects
**MEDIUM SCORES (10-19 points):**
- Projects with some complexity, good documentation
- Multiple features or moderate technical challenge
**LOW SCORES (1-9 points):**
- Simple tutorial projects (todo lists, calculators, basic CRUD apps, weather apps, note-taking apps, recipe apps, exercise apps)
- Basic sentiment analysis using standard libraries (NLTK, scikit-learn)
- Classroom assignments or projects with minimal technical complexity
**ZERO SCORES (0 points):**
- No projects or only extremely basic projects that demonstrate no technical skills
**PROJECT LINK REQUIREMENTS:**
- **NO LINKS**: Projects without URLs, GitHub links, or live demos should receive 30-50% lower scores
- **INACTIVE LINKS**: Projects with broken links should receive 20-30% lower scores
- **LIVE DEMO BONUS**: Projects with working live demos should receive 10-20% higher scores
### Production (0-25 points)
- Analyze the 'work' and 'volunteer' sections for real-world, internship, or production experience
- **SPECIAL CONSIDERATION**: Give extra points for founder roles, co-founder positions, or early-stage engineer roles (first 10-20 employees) at startups
### Technical Skills (0-10 points)
- Analyze the 'skills', 'languages', and evidence of technical breadth or problem-solving in projects, work, or competitions
## PROJECT COMPLEXITY ASSESSMENT
**Simple/Basic Projects (Low Impact):**
- Todo list applications, calculators, basic CRUD applications
- Weather apps using public APIs, note-taking applications
- Simple portfolio websites, basic form applications
- "Hello World" applications, classroom assignment projects
- Tutorial-based projects, recipe sharing applications
- Exercise/health apps using public APIs
- Basic sentiment analysis using standard libraries
- Simple e-commerce applications, basic social media clones
**Complex/Advanced Projects (High Impact):**
- Full-stack applications with multiple features
- Projects with user authentication and databases
- Machine learning or AI applications
- Real-time applications (chat, streaming, etc.)
- Mobile applications with native features
- Projects with microservices architecture
- Contributions to popular open source projects
- Projects with significant user adoption
- Projects solving real-world problems
- Projects demonstrating advanced algorithms or data structures
## BONUS POINTS (Maximum total: 20 points)
- +5 points for Google Summer of Code (GSoC) participation
- +3 points for Girl Script Summer of Code participation
- +3-5 points for startup founder/co-founder experience
- +2-3 points for early-stage engineer experience (first 10-20 employees at a startup)
- +2 points for portfolio website (GitHub URL in basics.url)
- +1 point for LinkedIn profile
- +1-3 points for high-quality technical blogs (if blog data provided)
**CRITICAL**: The total bonus points cannot exceed 20 points under any circumstances.
## DEDUCTIONS
**For Simple Projects:**
- -2 to -5 points if resume contains only simple tutorial projects
- -1 to -3 points for each simple project beyond the first one
- -1 point for projects with generic names like "Calculator", "Todo App", "Weather App"
- -2 points if all projects are classroom assignments or tutorial-based
**For Projects Without Links:**
- -3 to -5 points for each project without any GitHub link, live demo, or active URL
- -2 to -3 points for each project with only GitHub link but no live demo
- -1 to -2 points for each project with broken or inactive links
**CRITICAL ENFORCEMENT:**
- When GitHub data shows all projects are 'self_project' type, apply 3-5 point deductions for lack of true open source contributions
- For candidates with only personal GitHub repositories, open source score should NEVER exceed 10 points
- For candidates with only tutorial-based projects, self_projects score should NEVER exceed 15 points
## CRITICAL REQUIREMENTS
1. You MUST respond with ONLY the JSON structure below - no summary, no other fields
2. You MUST fill ALL FOUR score categories: open_source, self_projects, production, technical_skills
3. You MUST provide evidence for each score
4. You MUST NOT add any other fields like "summary", "skills", "experience", etc.
5. You MUST NOT change the field names or structure
**IMPORTANT LIST CONSTRAINTS:**
- key_strengths: Provide 1-5 items (maximum 5 key strengths)
- areas_for_improvement: Provide 1-3 items (maximum 3 areas for improvement)
**IMPORTANT SCORE CONSTRAINTS:**
- Evidence fields cannot be empty string
- All category scores must be >= 0 (cannot be negative)
- **CATEGORY SCORE LIMITS** (CANNOT be exceeded under any circumstances):
- open_source: 0-35 points (maximum 35)
- self_projects: 0-30 points (maximum 30)
- production: 0-25 points (maximum 25)
- technical_skills: 0-10 points (maximum 10)
- Bonus points total must be <= 20 (maximum 20 points)
- **OVERALL SCORE LIMIT**: The total score (categories + bonus - deductions) cannot exceed 120 points
**DO NOT RETURN A RESUME SUMMARY. RETURN ONLY THE SCORING EVALUATION IN THE SPECIFIED JSON FORMAT.**
Analyze the following resume and provide a JSON response with this EXACT structure (all fields are required):
{
"scores": {
"open_source": {"score": 0, "max": 35, "evidence": "string"},
"self_projects": {"score": 0, "max": 30, "evidence": "string"},
"production": {"score": 0, "max": 25, "evidence": "string"},
"technical_skills": {"score": 0, "max": 10, "evidence": "string"}
},
"bonus_points": {"total": 0, "breakdown": "string"},
"deductions": {"total": 0, "reasons": "string"},
"key_strengths": ["strength1", "strength2", "strength3", "strength4", "strength5"],
"areas_for_improvement": ["improvement1", "improvement2", "improvement3"]
}
Resume to evaluate:
{{ text_content }}
@@ -0,0 +1,49 @@
You are an expert technical recruiter evaluating resumes. Provide accurate, objective evaluations based on the given criteria.
**CRITICAL: You are NOT writing a resume summary. You are SCORING a resume for a job application.**
**CRITICAL FAIRNESS REQUIREMENTS:**
**SCORES MUST NEVER DEPEND ON THE FOLLOWING FACTORS:**
- Candidate's name, gender, or any personal demographic information
- College, university, or educational institution name
- CGPA, GPA, or academic grades
- City, location, or geographical information
- Any personal characteristics unrelated to technical skills and experience
**EVALUATION MUST BE BASED ONLY ON:**
- Technical skills and programming languages
- Project complexity and real-world impact
- Open source contributions and community involvement
- Work experience and production-level contributions
- Technical communication and documentation abilities
- Problem-solving and algorithmic thinking demonstrated in projects
**MANDATORY: You MUST always fill ALL FOUR categories: open_source, self_projects, production, technical_skills.**
- For open_source: Analyze all open source contributions, GitHub/GitLab activity, and community involvement. Look for Google Summer of Code (GSoC) and Girl Script Summer of Code participation. **CRITICAL**: Having personal GitHub repositories does NOT constitute open source contribution. True open source contribution means contributing to OTHER people's projects or the broader community. Personal repositories should receive low scores (5-10 points) unless they demonstrate exceptional complexity or community impact. **CRITICAL**: Hacktoberfest participation alone (without evidence of contributions to significant projects) should receive 5-8 points maximum. **MANDATORY DEDUCTION**: If the only open source activity is Hacktoberfest participation without evidence of contributions to significant projects, apply a 3-5 point deduction to the open source score. **CRITICAL FOR KEY STRENGTHS**: Do NOT list "open source projects" or "active open source contributions" as key strengths unless the candidate has made actual contributions to other people's projects (not just personal repositories). **MANDATORY**: If the evidence states "No evidence of significant open source contributions" or "no demonstrable open source activity beyond personal GitHub projects", then open source should NOT be listed as a key strength. **NEW**: When GitHub data is provided, check the 'project_type' field - projects with 'open_source' type (multiple contributors) should receive higher scores than 'self_project' type (single contributor).
- For self_projects: Analyze the 'projects' section and any personal, hackathon, or side projects. **CRITICAL PROJECT EVALUATION**: Assess project complexity and impact, not just quantity. Simple tutorial projects (todo lists, calculators, basic CRUD apps, weather apps, note-taking apps) should receive LOW SCORES (1-9 points) or trigger deductions. **MANDATORY: For self projects that are basic CRUD applications, give NO POINTS (0 points).** Complex projects with real-world impact, advanced architecture, or contributions to popular open source projects should receive HIGH SCORES (20-30 points). Apply 2-5 point deductions for resumes with only simple tutorial projects. **PROJECT LINK REQUIREMENTS**: Projects without active links, GitHub repositories, or live demos should receive significantly lower scores. Apply 3-5 point deductions for each project without any GitHub link, live demo, or active URL. Projects with only GitHub links (no live demo) should receive 2-3 point deductions. Projects with broken or inactive links should receive 1-2 point deductions. Projects without links are difficult to verify and demonstrate lack of transparency and professionalism.
- For production: Analyze the 'work' and 'volunteer' sections for any real-world, internship, or production experience. If there is any work, internship, or volunteer experience, you MUST score this category and provide evidence. **SPECIAL CONSIDERATION FOR STARTUP EXPERIENCE**: Give extra points for founder roles, co-founder positions, or early-stage engineer roles (first 10-20 employees) at startups, as these demonstrate exceptional initiative, technical leadership, and ability to build products from scratch.
- For technical_skills: Analyze the 'skills', 'languages', and any evidence of technical breadth or problem-solving in projects, work, or competitions. You MUST score this category and provide evidence.
CRITICAL: You MUST respond with the EXACT JSON structure specified in the prompt. Do not change category names, add extra fields, or modify the structure. The response must include ALL required fields: candidate_name, scores (with open_source, self_projects, production, technical_skills), bonus_points, deductions, key_strengths, areas_for_improvement.
**IMPORTANT LIST CONSTRAINTS:**
- key_strengths: Provide 1-5 items (maximum 5 key strengths)
- areas_for_improvement: Provide 1-3 items (maximum 3 areas for improvement)
**IMPORTANT SCORE CONSTRAINTS:**
- Evidence fields cannot be empty string
- All category scores must be >= 0 (cannot be negative)
- **CATEGORY SCORE LIMITS** (CANNOT be exceeded under any circumstances):
- open_source: 0-35 points (maximum 35)
- self_projects: 0-30 points (maximum 30)
- production: 0-25 points (maximum 25)
- technical_skills: 0-10 points (maximum 10)
- Bonus points total must be <= 20 (maximum 20 points)
- **CRITICAL**: The total bonus points cannot exceed 20 points under any circumstances
- **OVERALL SCORE LIMIT**: The total score (categories + bonus - deductions) cannot exceed 120 points
IMPORTANT: Always check the structured 'profiles' section in the resume data before applying deductions for missing GitHub/portfolio. Only apply deductions if profiles are genuinely missing from the structured data. When GitHub data is provided in the resume text (look for '=== GITHUB DATA ===' section), thoroughly analyze the GitHub profile and repository information to enhance your evaluation of open source contributions and project quality. **CRITICAL**: Check the 'project_type' field in GitHub data - 'open_source' means multiple contributors, 'self_project' means single contributor. Self projects should receive low open source scores. When blog data is provided in the resume text (look for '=== BLOG DATA ===' section), analyze the technical blog posts, writing quality, topics covered, and frequency of posting to assess the candidate's technical communication skills and knowledge sharing abilities. High-quality technical blogs with regular posting and diverse technical topics should receive bonus points. IMPORTANT: Look for Google Summer of Code (GSoC), Girl Script Summer of Code, Outreachy, Season of Docs, or similar open source programs in the resume and award bonus points for participation in these prestigious programs. **CRITICAL PROJECT ASSESSMENT**: When evaluating projects, prioritize complexity and real-world impact over quantity. Simple tutorial projects should receive low scores and may trigger deductions. A single complex project is worth more than multiple simple ones. **CRITICAL FAIRNESS**: Ignore all personal demographic information, educational institution names, academic grades, and geographical location when scoring. Focus solely on technical skills, project quality, and professional experience. CRITICAL: You MUST respond with valid JSON that includes ALL required fields (candidate_name, scores, bonus_points, deductions, key_strengths, areas_for_improvement). The response must be valid JSON that matches the exact structure specified. Do not omit any fields or add extra fields. **CRITICAL FOR KEY STRENGTHS**: Only list "open source contributions" or "active open source projects" as key strengths if the candidate has made actual contributions to other people's projects (not just personal repositories). Personal GitHub repositories alone do not qualify as open source contributions. **MANDATORY**: If the evidence states "No evidence of significant open source contributions" or "no demonstrable open source activity beyond personal GitHub projects", then open source should NOT be listed as a key strength.
+20
View File
@@ -0,0 +1,20 @@
Extract ONLY the skills information from this resume.
--- The input markdown starts here ---
{{ text_content }}
--- The input markdown ends here ---
Return ONLY a JSON object with this structure:
{
"skills": [
{
"name": "Skill category",
"level": null,
"keywords": ["Skill 1", "Skill 2"]
}
]
}
**IMPORTANT**: Return ONLY valid JSON. Do not include any explanatory text.
+5
View File
@@ -0,0 +1,5 @@
You are an expert resume parser. Extract ONLY the {{ section_name_param }} section from resumes and format it according to the JSON Resume specification.
**CRITICAL: You must respond with ONLY valid JSON. Do not include any explanatory text, thinking process, markdown formatting, or <think> tags. Return ONLY the JSON object.**
Return ONLY the {{ section_name_param }} section in JSON format.
+36
View File
@@ -0,0 +1,36 @@
Extract ONLY the work experience from this resume.
--- The input markdown starts here ---
{{ text_content }}
--- The input markdown ends here ---
Return ONLY a JSON object with this structure:
{
"work": [
{
"name": "Company name",
"position": "Job title",
"startDate": "Start date (YYYY-MM)",
"endDate": "End date (YYYY-MM) or 'Present'",
"summary": "Job description",
"highlights": ["Achievement 1", "Achievement 2"]
}
]
}
**IMPORTANT**:
For date extraction, follow these rules carefully:
1. Look for date ranges in the work experience section. These often appear as "Start End" or "Start - End" format.
2. Date ranges can be separated by any of these characters: hyphen "-", en dash "", em dash "—", or the word "to".
3. Carefully extract both the start and end dates from these ranges.
4. If a position is current/ongoing, the end date may be indicated by words like "Present", "Current", "Now", "Ongoing", etc.
5. Look for dates near position titles or company names
6. Dates might be formatted as "Month Year" or "MM/YYYY" or other variations
7. If dates appear as "Month Year - Month Year", extract both parts
8. If only years are provided (e.g., "2019-2021"), use the year information only
**IMPORTANT**: Return ONLY valid JSON. Do not include any explanatory text.
+1377
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -0,0 +1,9 @@
PyMuPDF==1.26.3
ollama==0.5.1
pydantic==2.11.7
requests==2.32.4
pymupdf4llm==0.0.27
Jinja2==3.1.6
google-generativeai==0.4.0
python-dotenv==1.0.1
black==25.9.0
+377
View File
@@ -0,0 +1,377 @@
import os
import sys
import json
import logging
import csv
from pdf import PDFHandler
from github import fetch_and_display_github_info
from models import JSONResume, EvaluationData
from typing import List, Optional, Dict
from evaluator import ResumeEvaluator
from pathlib import Path
from prompt import DEFAULT_MODEL, MODEL_PARAMETERS
from transform import (
transform_evaluation_response,
convert_json_resume_to_text,
convert_github_data_to_text,
convert_blog_data_to_text,
)
from config import DEVELOPMENT_MODE
logger = logging.getLogger(__name__)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)5s - %(lineno)5d - %(funcName)33s - %(levelname)5s - %(message)s",
)
def print_evaluation_results(
evaluation: EvaluationData, candidate_name: str = "Candidate"
):
"""Print evaluation results in a readable format."""
print("\n" + "=" * 80)
print(f"📊 RESUME EVALUATION RESULTS FOR: {candidate_name}")
print("=" * 80)
if not evaluation:
print("❌ No evaluation data available")
return
# Calculate overall score
total_score = 0
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"]
# Log warning if score was capped
if category_score < category_data["score"]:
print(
f"⚠️ Warning: {category_name} score capped from {category_data['score']} to {category_score} (max: {category_data['max']})"
)
# Add bonus points
if hasattr(evaluation, "bonus_points") and evaluation.bonus_points:
total_score += evaluation.bonus_points.total
# Subtract deductions
if hasattr(evaluation, "deductions") and evaluation.deductions:
total_score -= evaluation.deductions.total
# Ensure total score doesn't exceed maximum possible score
max_possible_score = max_score + 20 # 120 (100 categories + 20 bonus)
if total_score > max_possible_score:
total_score = max_possible_score
print(f"⚠️ Warning: Total score capped at maximum possible value")
# Overall Score
print(f"\n🎯 OVERALL SCORE: {total_score:.1f}/{max_score}")
# Detailed Scores
print("\n📈 DETAILED SCORES:")
print("-" * 60)
if hasattr(evaluation, "scores") and evaluation.scores:
# Define category maximums
category_maxes = {
"open_source": 35,
"self_projects": 30,
"production": 25,
"technical_skills": 10,
}
# Open Source
if hasattr(evaluation.scores, "open_source") and evaluation.scores.open_source:
os_score = evaluation.scores.open_source
capped_score = min(os_score.score, category_maxes["open_source"])
print(f"🌐 Open Source: {capped_score}/{os_score.max}")
print(f" Evidence: {os_score.evidence}")
print()
# Self Projects
if (
hasattr(evaluation.scores, "self_projects")
and evaluation.scores.self_projects
):
sp_score = evaluation.scores.self_projects
capped_score = min(sp_score.score, category_maxes["self_projects"])
print(f"🚀 Self Projects: {capped_score}/{sp_score.max}")
print(f" Evidence: {sp_score.evidence}")
print()
# Production Experience
if hasattr(evaluation.scores, "production") and evaluation.scores.production:
prod_score = evaluation.scores.production
capped_score = min(prod_score.score, category_maxes["production"])
print(f"🏢 Production Experience: {capped_score}/{prod_score.max}")
print(f" Evidence: {prod_score.evidence}")
print()
# Technical Skills
if (
hasattr(evaluation.scores, "technical_skills")
and evaluation.scores.technical_skills
):
tech_score = evaluation.scores.technical_skills
capped_score = min(tech_score.score, category_maxes["technical_skills"])
print(f"💻 Technical Skills: {capped_score}/{tech_score.max}")
print(f" Evidence: {tech_score.evidence}")
print()
# Bonus Points
if hasattr(evaluation, "bonus_points") and evaluation.bonus_points:
print(f"\n⭐ BONUS POINTS: {evaluation.bonus_points.total}")
print("-" * 30)
print(f" {evaluation.bonus_points.breakdown}")
# Deductions
if (
hasattr(evaluation, "deductions")
and evaluation.deductions
and evaluation.deductions.total > 0
):
print(f"\n⚠️ DEDUCTIONS: -{evaluation.deductions.total}")
print("-" * 30)
if evaluation.deductions.reasons:
print(f" {evaluation.deductions.reasons}")
# Key Strengths
if hasattr(evaluation, "key_strengths") and evaluation.key_strengths:
print(f"\n✅ KEY STRENGTHS:")
print("-" * 30)
for i, strength in enumerate(evaluation.key_strengths, 1):
print(f" {i}. {strength}")
# Areas for Improvement
if (
hasattr(evaluation, "areas_for_improvement")
and evaluation.areas_for_improvement
):
print(f"\n🔧 AREAS FOR IMPROVEMENT:")
print("-" * 30)
for i, area in enumerate(evaluation.areas_for_improvement, 1):
print(f" {i}. {area}")
print("\n" + "=" * 80)
def _evaluate_resume(
resume_data: JSONResume, github_data: dict = None, blog_data: dict = 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)
# Convert JSON resume data to text
resume_text = convert_json_resume_to_text(resume_data)
# Add GitHub data if available
if github_data:
github_text = convert_github_data_to_text(github_data)
resume_text += github_text
# Add blog data if available
if blog_data:
blog_text = convert_blog_data_to_text(blog_data)
resume_text += blog_text
# Evaluate the enhanced resume
evaluation_result = evaluator.evaluate_resume(resume_text)
# print(evaluation_result)
return evaluation_result
def is_valid_resume_data(resume_data: JSONResume) -> bool:
"""Check if the resume data has at least some extracted core content."""
if not resume_data:
return False
core_sections = [
resume_data.basics,
resume_data.work,
resume_data.education,
resume_data.skills,
resume_data.projects,
]
return any(section is not None for section in core_sections)
def find_profile(profiles, network):
if not profiles:
return None
return next(
(p for p in profiles if p.network and p.network.lower() == network.lower()),
None,
)
def main(pdf_path):
# Create cache filename based on PDF path
cache_filename = (
f"cache/resumecache_{os.path.basename(pdf_path).replace('.pdf', '')}.json"
)
github_cache_filename = (
f"cache/githubcache_{os.path.basename(pdf_path).replace('.pdf', '')}.json"
)
resume_data = None
cache_loaded = False
# Check if cache exists and we're in development mode
if DEVELOPMENT_MODE and os.path.exists(cache_filename):
print(f"Loading cached data from {cache_filename}")
try:
cached_data = json.loads(Path(cache_filename).read_text(encoding="utf-8"))
loaded_resume = JSONResume(**cached_data)
if not is_valid_resume_data(loaded_resume):
raise ValueError("Cached resume data contains no core content")
resume_data = loaded_resume
cache_loaded = True
except Exception as e:
print(f"⚠️ Warning: Invalid cache file {cache_filename}: {e}")
print("Ignoring cache and reprocessing PDF...")
try:
os.remove(cache_filename)
except Exception as delete_err:
print(
f"Failed to delete invalid cache file {cache_filename}: {delete_err}"
)
if not cache_loaded:
logger.debug(
f"Extracting data from PDF"
+ (" and caching to " + cache_filename if DEVELOPMENT_MODE else "")
)
pdf_handler = PDFHandler()
resume_data = pdf_handler.extract_json_from_pdf(pdf_path)
if resume_data == None:
return None
if DEVELOPMENT_MODE:
if is_valid_resume_data(resume_data):
os.makedirs(os.path.dirname(cache_filename), exist_ok=True)
Path(cache_filename).write_text(
json.dumps(resume_data.model_dump(), indent=2, ensure_ascii=False),
encoding="utf-8",
)
else:
logger.warning(
"Newly extracted resume data is empty/invalid. Skipping cache write."
)
# Check if cache exists and we're in development mode
github_data = {}
github_cache_loaded = False
if DEVELOPMENT_MODE and os.path.exists(github_cache_filename):
print(f"Loading cached data from {github_cache_filename}")
try:
loaded_github = json.loads(
Path(github_cache_filename).read_text(encoding="utf-8")
)
if (
not isinstance(loaded_github, dict)
or not loaded_github
or "profile" not in loaded_github
):
raise ValueError("Cached GitHub data is invalid or empty")
github_data = loaded_github
github_cache_loaded = True
except Exception as e:
print(f"⚠️ Warning: Invalid GitHub cache file {github_cache_filename}: {e}")
print("Ignoring GitHub cache and refetching...")
try:
os.remove(github_cache_filename)
except Exception as delete_err:
print(
f"Failed to delete invalid GitHub cache file {github_cache_filename}: {delete_err}"
)
if not github_cache_loaded:
# Add validation to handle None values
profiles = []
if resume_data and hasattr(resume_data, "basics") and resume_data.basics:
profiles = resume_data.basics.profiles or []
github_profile = find_profile(profiles, "Github")
if github_profile:
print(
f"Fetching GitHub data"
+ (
" and caching to " + github_cache_filename
if DEVELOPMENT_MODE
else ""
)
)
github_data = fetch_and_display_github_info(github_profile.url)
if (
DEVELOPMENT_MODE
and github_data
and isinstance(github_data, dict)
and "profile" in github_data
):
os.makedirs(os.path.dirname(github_cache_filename), exist_ok=True)
Path(github_cache_filename).write_text(
json.dumps(github_data, indent=2, ensure_ascii=False),
encoding="utf-8",
)
score = _evaluate_resume(resume_data, github_data)
# Get candidate name for display
candidate_name = os.path.basename(pdf_path).replace(".pdf", "")
if (
resume_data
and hasattr(resume_data, "basics")
and resume_data.basics
and resume_data.basics.name
):
candidate_name = resume_data.basics.name
# Print evaluation results in readable format
print_evaluation_results(score, candidate_name)
if DEVELOPMENT_MODE:
csv_row = transform_evaluation_response(
file_name=os.path.basename(pdf_path),
evaluation=score,
resume_data=resume_data,
github_data=github_data,
)
# Write CSV row to file
csv_path = "resume_evaluations.csv"
file_exists = os.path.exists(csv_path)
with open(csv_path, "a", newline="", encoding="utf-8") as csvfile:
fieldnames = list(csv_row.keys())
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
# Write headers if file doesn't exist
if not file_exists:
writer.writeheader()
# Write the row
writer.writerow(csv_row)
return score
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python score.py <pdf_path>")
exit(1)
pdf_path = sys.argv[1]
if not os.path.exists(pdf_path):
print(f"Error: File '{pdf_path}' does not exist.")
exit(1)
main(pdf_path)
+938
View File
@@ -0,0 +1,938 @@
from typing import Dict, List, Optional
import pdb
from models import JSONResume
def transform_parsed_data(parsed_data: Dict) -> Dict:
try:
if isinstance(parsed_data, dict):
if "basics" in parsed_data and len(parsed_data) > 1:
transformed = {
"basics": transform_basics(parsed_data.get("basics", {})),
"work": transform_work_experience(
parsed_data.get(
"work_experience",
parsed_data.get("work", parsed_data.get("experience", [])),
)
),
"volunteer": transform_organizations(
parsed_data.get("organizations", [])
),
"education": transform_education(parsed_data.get("education", [])),
"awards": transform_achievements(
parsed_data.get(
"achievements",
parsed_data.get(
"awards", parsed_data.get("honors_and_awards", [])
),
)
),
"certificates": parsed_data.get("certificates", []),
"publications": parsed_data.get("publications", []),
"skills": transform_skills_comprehensive(parsed_data),
"languages": parsed_data.get("languages", []),
"interests": parsed_data.get("interests", []),
"references": parsed_data.get("references", []),
"projects": transform_projects_comprehensive(parsed_data),
"meta": parsed_data.get("meta", {}),
}
else:
if "basics" in parsed_data:
basics_data = parsed_data.get("basics", parsed_data)
transformed = {"basics": transform_basics(basics_data)}
elif (
"work" in parsed_data
or "work_experience" in parsed_data
or "experience" in parsed_data
):
work_data = parsed_data.get(
"work",
parsed_data.get(
"work_experience", parsed_data.get("experience", [])
),
)
transformed = {"work": transform_work_experience(work_data)}
elif "education" in parsed_data:
transformed = {
"education": transform_education(
parsed_data.get("education", [])
)
}
elif (
"skills" in parsed_data
or "librariesFrameworks" in parsed_data
or "toolsPlatforms" in parsed_data
or "databases" in parsed_data
):
transformed = {
"skills": transform_skills_comprehensive(parsed_data)
}
elif "projects" in parsed_data or "projectsOpenSource" in parsed_data:
transformed = {
"projects": transform_projects_comprehensive(parsed_data)
}
elif (
"awards" in parsed_data
or "achievements" in parsed_data
or "honors_and_awards" in parsed_data
):
awards_data = parsed_data.get(
"awards",
parsed_data.get(
"achievements", parsed_data.get("honors_and_awards", [])
),
)
transformed = {"awards": transform_achievements(awards_data)}
else:
transformed = parsed_data
return transformed
else:
return parsed_data
except Exception as e:
print(f"Error transforming parsed data: {e}")
return parsed_data
def extract_domain_from_url(url: str) -> str:
try:
if "://" in url:
url = url.split("://")[1]
domain = url.split("/")[0]
if domain.startswith("www."):
domain = domain[4:]
return domain
except Exception:
return ""
def get_network_name(domain: str) -> str:
domain_mapping = {
"github.com": "GitHub",
"linkedin.com": "LinkedIn",
"leetcode.com": "LeetCode",
"stackoverflow.com": "Stack Overflow",
"hackerrank.com": "HackerRank",
"behance.net": "Behance",
"dev.to": "DEV Community",
"twitter.com": "X",
"x.com": "X",
}
return domain_mapping.get(domain, "")
def transform_basics(basics_data: Dict) -> Dict:
if not isinstance(basics_data, dict):
return basics_data
profiles = basics_data.get("profiles", [])
transformed_profiles = []
if isinstance(profiles, list):
for i, profile in enumerate(profiles):
if isinstance(profile, dict):
transformed_profile = profile.copy()
url = transformed_profile.get("url", "")
network = transformed_profile.get("network")
if url and network is None:
domain = extract_domain_from_url(url)
network_name = get_network_name(domain)
if network_name:
transformed_profile["network"] = network_name
username = extract_username_from_url(url, domain)
if username:
transformed_profile["username"] = username
transformed_profiles.append(transformed_profile)
basics_data["profiles"] = transformed_profiles
return basics_data
def extract_username_from_url(url: str, domain: str) -> str:
try:
path = url.split(domain)[1] if domain in url else ""
if not path:
return ""
path = path.lstrip("/")
parts = [part for part in path.split("/") if part]
if parts:
if domain == "linkedin.com":
return parts[1]
elif domain == "stackoverflow.com":
return parts[2]
else:
return parts[0]
return ""
except Exception:
return ""
def transform_work_experience(work_list: List) -> List[Dict]:
transformed = []
for item in work_list:
if isinstance(item, dict):
description = item.get("description", "")
if isinstance(description, list):
description = " ".join(description)
# Try to parse from 'startDate' if it contains a date range
start_date_input = item.get("startDate", "")
if start_date_input and any(
month in start_date_input
for month in [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
]
):
start_date, end_date = parse_date_range(start_date_input)
else:
# Use existing startDate and endDate values
start_date = item.get("startDate")
end_date = item.get("endDate")
transformed.append(
{
"name": item.get("name", ""),
"position": item.get(
"position", item.get("type", item.get("title", ""))
),
"url": item.get("url", None),
"startDate": start_date,
"endDate": end_date,
"summary": item.get("summary", description),
"highlights": item.get("highlights", []),
}
)
return transformed
def transform_organizations(org_list: List) -> List[Dict]:
transformed = []
for item in org_list:
if isinstance(item, dict):
transformed.append(
{
"organization": item.get("name", ""),
"position": item.get("role", ""),
"url": item.get("url", None),
"startDate": None,
"endDate": "Present",
"summary": None,
"highlights": [],
}
)
return transformed
def transform_education(edu_list: List) -> List[Dict]:
transformed = []
for item in edu_list:
if isinstance(item, dict):
if "degree" in item:
score = item.get("gpa", item.get("percentage", None))
if score is not None:
score = str(score)
start_date, end_date = parse_date_range(item.get("years", ""))
transformed.append(
{
"institution": item.get("institution", ""),
"url": item.get("url", None),
"area": (
item.get("degree", "").split(", ")[-1]
if "," in item.get("degree", "")
else None
),
"studyType": (
item.get("degree", "").split(", ")[0]
if "," in item.get("degree", "")
else item.get("degree", "")
),
"startDate": start_date,
"endDate": end_date,
"score": score,
"courses": [],
}
)
else:
transformed.append(item)
return transformed
def transform_achievements(achievements_list: List) -> List[Dict]:
transformed = []
for item in achievements_list:
if isinstance(item, dict):
title = item.get("title", item.get("name", ""))
awarder = item.get("awarder", item.get("organization", ""))
summary = item.get("summary", item.get("description", None))
transformed.append(
{
"title": title,
"date": f"{item.get('year', '')}-01" if item.get("year") else None,
"awarder": awarder,
"summary": summary,
}
)
return transformed
def transform_skills(skills_list: List) -> List[Dict]:
transformed = []
for item in skills_list:
if isinstance(item, dict):
if "category" in item:
transformed.append(
{
"name": item.get("category", ""),
"level": None,
"keywords": item.get("keywords", []),
}
)
else:
transformed.append(item)
return transformed
def transform_projects(projects_list: List) -> List[Dict]:
transformed = []
for item in projects_list:
if isinstance(item, dict):
skills = []
project_name = item.get("name", "")
if "|" in project_name:
name_parts = project_name.split("|")
if len(name_parts) > 1:
skills_part = name_parts[1].strip()
skills = [skill.strip() for skill in skills_part.split(",")]
item["name"] = name_parts[0].strip()
technologies = item.get("technologies", [])
if isinstance(technologies, str):
technologies = [tech.strip() for tech in technologies.split(",")]
if not skills and technologies:
skills = technologies
transformed.append(
{
"name": item.get("name", ""),
"startDate": None,
"endDate": None,
"description": item.get("description", ""),
"highlights": [item.get("type", "")] if item.get("type") else [],
"url": item.get("url", None),
"technologies": technologies,
"skills": skills,
}
)
return transformed
def transform_skills_comprehensive(parsed_data: Dict) -> List[Dict]:
skills = []
if "skills" in parsed_data and isinstance(parsed_data["skills"], list):
if parsed_data["skills"] and isinstance(parsed_data["skills"][0], str):
skills.append(
{
"name": "Programming Languages",
"level": None,
"keywords": parsed_data["skills"],
}
)
else:
skills.extend(transform_skills(parsed_data["skills"]))
skill_categories = {
"librariesFrameworks": "Libraries/Frameworks",
"toolsPlatforms": "Tools/Platforms",
"databases": "Databases",
}
for field, category_name in skill_categories.items():
if field in parsed_data and isinstance(parsed_data[field], list):
skills.append(
{"name": category_name, "level": None, "keywords": parsed_data[field]}
)
return skills
def transform_projects_comprehensive(parsed_data: Dict) -> List[Dict]:
projects = []
if "projects" in parsed_data:
projects.extend(transform_projects(parsed_data["projects"]))
if "projectsOpenSource" in parsed_data:
for item in parsed_data["projectsOpenSource"]:
if isinstance(item, dict):
skills = []
project_name = item.get("name", "")
if "|" in project_name:
name_parts = project_name.split("|")
if len(name_parts) > 1:
skills_part = name_parts[1].strip()
skills = [skill.strip() for skill in skills_part.split(",")]
item["name"] = name_parts[0].strip()
projects.append(
{
"name": item.get("name", ""),
"startDate": None,
"endDate": None,
"description": item.get("summary", ""),
"highlights": [],
"url": item.get("url", None),
"technologies": item.get("technologies", []),
"skills": skills,
}
)
return projects
def parse_date_range(date_range: str) -> tuple:
"""
Parse date range and return both start and end dates.
For format like "Jan-Mar 2021", returns ("Jan 2021", "Mar 2021")
"""
if not date_range:
return None, None
# Handle "onwards" case
if "onwards" in date_range:
# Extract the start date from "onwards" format
start_part = date_range.replace("onwards", "").strip()
if start_part:
return start_part, "Present"
return None, "Present"
# Handle format like "Jan-Mar 2021"
if " " in date_range and any(
month in date_range
for month in [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
]
):
parts = date_range.split(" ")
if len(parts) >= 2:
year = parts[-1]
month_map = {
"Jan": "Jan",
"Feb": "Feb",
"Mar": "Mar",
"Apr": "Apr",
"May": "May",
"Jun": "Jun",
"Jul": "Jul",
"Aug": "Aug",
"Sep": "Sep",
"Oct": "Oct",
"Nov": "Nov",
"Dec": "Dec",
}
# Check if it's a range like "Jan-Mar 2021"
if "-" in parts[0] and len(parts[0].split("-")) == 2:
start_month, end_month = parts[0].split("-")
start_date = f"{month_map.get(start_month, start_month)} {year}"
end_date = f"{month_map.get(end_month, end_month)} {year}"
return start_date, end_date
else:
# Single month format like "Jan 2021"
month = month_map.get(parts[0], parts[0])
start_date = f"{month} {year}"
return start_date, None
# Handle year range like "2020-2021"
if "-" in date_range and len(date_range.split("-")) == 2:
start_year, end_year = date_range.split("-")
start_date = f"{start_year}-01"
end_date = f"{end_year}-12"
return start_date, end_date
return None, None
def fetch_profile(profiles, network_names, prefix):
"""Helper function to extract profile information for a given network."""
for network in network_names:
profile = next(
(p for p in profiles if p.network and p.network.lower() == network.lower()),
None,
)
if profile:
return profile
def transform_evaluation_response(
file_name=None, resume_data=None, github_data=None, evaluation=None
):
"""
Transform the three inputs (resume_data, github_data, evaluation) into the most important columns as a CSV row.
Args:
resume_data: JSONResume object containing parsed resume data
github_data: dict containing GitHub profile data
evaluation: EvaluationData object containing evaluation results
Returns:
dict: Dictionary with the most important columns for CSV output
"""
csv_row = {}
csv_row["file_name"] = file_name
# Extract basic information from resume_data
if resume_data and hasattr(resume_data, "basics") and resume_data.basics:
basics = resume_data.basics
csv_row["name"] = basics.name if basics.name else ""
csv_row["email"] = basics.email if basics.email else ""
csv_row["phone"] = basics.phone if basics.phone else ""
csv_row["location"] = (
f"{basics.location.city}, {basics.location.region}"
if basics.location
else ""
)
csv_row["summary"] = basics.summary if basics.summary else ""
# Extract all profile information
if basics.profiles:
# Extract profiles for each platform
github_profile = fetch_profile(basics.profiles, ["github"], "github")
linkedin_profile = fetch_profile(basics.profiles, ["linkedin"], "linkedin")
twitter_profile = fetch_profile(
basics.profiles, ["twitter", "x"], "twitter"
)
dev_profile = fetch_profile(
basics.profiles, ["dev community", "dev"], "dev"
)
behance_profile = fetch_profile(basics.profiles, ["behance"], "behance")
# Add GitHub profile columns
if github_profile:
csv_row["github_url"] = github_profile.url
csv_row["github_username"] = (
github_profile.username if github_profile.username else ""
)
else:
csv_row["github_url"] = ""
csv_row["github_username"] = ""
# Add LinkedIn profile columns
if linkedin_profile:
csv_row["linkedin_url"] = linkedin_profile.url
csv_row["linkedin_username"] = (
linkedin_profile.username if linkedin_profile.username else ""
)
else:
csv_row["linkedin_url"] = ""
csv_row["linkedin_username"] = ""
# Add Twitter/X profile columns
if twitter_profile:
csv_row["twitter_url"] = twitter_profile.url
csv_row["twitter_username"] = (
twitter_profile.username if twitter_profile.username else ""
)
else:
csv_row["twitter_url"] = ""
csv_row["twitter_username"] = ""
# Add DEV Community profile columns
if dev_profile:
csv_row["dev_url"] = dev_profile.url
csv_row["dev_username"] = (
dev_profile.username if dev_profile.username else ""
)
else:
csv_row["dev_url"] = ""
csv_row["dev_username"] = ""
# Add Behance profile columns
if behance_profile:
csv_row["behance_url"] = behance_profile.url
csv_row["behance_username"] = (
behance_profile.username if behance_profile.username else ""
)
else:
csv_row["behance_url"] = ""
csv_row["behance_username"] = ""
else:
# Initialize empty profile columns
for prefix in ["github", "linkedin", "twitter", "dev", "behance"]:
csv_row[f"{prefix}_url"] = ""
csv_row[f"{prefix}_username"] = ""
# Extract work experience summary
if resume_data and hasattr(resume_data, "work") and resume_data.work:
work_experience = resume_data.work
csv_row["total_work_experience"] = len(work_experience)
# Get most recent position
if work_experience:
latest_work = work_experience[0] # Assuming sorted by date
csv_row["current_position"] = (
latest_work.position if latest_work.position else ""
)
csv_row["current_company"] = latest_work.name if latest_work.name else ""
else:
csv_row["current_position"] = ""
csv_row["current_company"] = ""
else:
csv_row["total_work_experience"] = 0
csv_row["current_position"] = ""
csv_row["current_company"] = ""
# Extract education summary
if resume_data and hasattr(resume_data, "education") and resume_data.education:
education = resume_data.education
csv_row["total_education"] = len(education)
# Get highest education level
if education:
highest_edu = education[0] # Assuming sorted by date
csv_row["highest_degree"] = (
highest_edu.studyType if highest_edu.studyType else ""
)
csv_row["institution"] = (
highest_edu.institution if highest_edu.institution else ""
)
else:
csv_row["highest_degree"] = ""
csv_row["institution"] = ""
else:
csv_row["total_education"] = 0
csv_row["highest_degree"] = ""
csv_row["institution"] = ""
# Extract skills summary
if resume_data and hasattr(resume_data, "skills") and resume_data.skills:
skills = resume_data.skills
all_skills = []
for skill_category in skills:
if skill_category.keywords:
all_skills.extend(skill_category.keywords)
csv_row["total_skills"] = len(all_skills)
csv_row["skills_list"] = ", ".join(all_skills[:10]) # Top 10 skills
else:
csv_row["total_skills"] = 0
csv_row["skills_list"] = ""
# Extract projects summary
if resume_data and hasattr(resume_data, "projects") and resume_data.projects:
projects = resume_data.projects
csv_row["total_projects"] = len(projects)
else:
csv_row["total_projects"] = 0
# Extract GitHub data
if github_data:
csv_row["github_repos"] = github_data.get("public_repos", 0)
csv_row["github_followers"] = github_data.get("followers", 0)
csv_row["github_following"] = github_data.get("following", 0)
csv_row["github_created_at"] = github_data.get("created_at", "")
csv_row["github_bio"] = github_data.get("bio", "")
else:
csv_row["github_repos"] = 0
csv_row["github_followers"] = 0
csv_row["github_following"] = 0
csv_row["github_created_at"] = ""
csv_row["github_bio"] = ""
# Extract evaluation scores
if evaluation and hasattr(evaluation, "scores"):
scores = evaluation.scores
csv_row["open_source_score"] = scores.open_source.score
csv_row["open_source_max"] = scores.open_source.max
csv_row["self_projects_score"] = scores.self_projects.score
csv_row["self_projects_max"] = scores.self_projects.max
csv_row["production_score"] = scores.production.score
csv_row["production_max"] = scores.production.max
csv_row["technical_skills_score"] = scores.technical_skills.score
csv_row["technical_skills_max"] = scores.technical_skills.max
total_score = (
scores.open_source.score
+ scores.self_projects.score
+ scores.production.score
+ scores.technical_skills.score
)
total_max = (
scores.open_source.max
+ scores.self_projects.max
+ scores.production.max
+ scores.technical_skills.max
)
csv_row["total_score"] = total_score
csv_row["total_max"] = total_max
else:
csv_row["open_source_score"] = "N/A"
csv_row["open_source_max"] = "N/A"
csv_row["self_projects_score"] = "N/A"
csv_row["self_projects_max"] = "N/A"
csv_row["production_score"] = "N/A"
csv_row["production_max"] = "N/A"
csv_row["technical_skills_score"] = "N/A"
csv_row["technical_skills_max"] = "N/A"
csv_row["total_score"] = "N/A"
csv_row["total_max"] = "N/A"
# Extract bonus points and deductions
if evaluation and hasattr(evaluation, "bonus_points"):
csv_row["bonus_points"] = evaluation.bonus_points.total
csv_row["bonus_breakdown"] = evaluation.bonus_points.breakdown
else:
csv_row["bonus_points"] = 0
csv_row["bonus_breakdown"] = ""
if evaluation and hasattr(evaluation, "deductions"):
csv_row["deductions"] = evaluation.deductions.total
csv_row["deduction_reasons"] = evaluation.deductions.reasons
else:
csv_row["deductions"] = 0
csv_row["deduction_reasons"] = ""
# Extract key strengths and areas for improvement
if evaluation and hasattr(evaluation, "key_strengths"):
csv_row["key_strengths"] = "; ".join(evaluation.key_strengths)
else:
csv_row["key_strengths"] = ""
if evaluation and hasattr(evaluation, "areas_for_improvement"):
csv_row["areas_for_improvement"] = "; ".join(evaluation.areas_for_improvement)
else:
csv_row["areas_for_improvement"] = ""
return csv_row
def convert_json_resume_to_text(resume_data: JSONResume) -> str:
text_parts = []
if resume_data.basics:
basics = resume_data.basics
text_parts.append("=== BASIC INFORMATION ===")
text_parts.append(f"Name: {basics.name or 'Not provided'}")
text_parts.append(f"Email: {basics.email or 'Not provided'}")
text_parts.append(f"Phone: {basics.phone or 'Not provided'}")
text_parts.append(f"Website: {basics.url or 'Not provided'}")
if basics.summary:
text_parts.append(f"Summary: {basics.summary}")
if basics.location:
loc = basics.location
location_parts = []
if loc.address:
location_parts.append(loc.address)
if loc.city:
location_parts.append(loc.city)
if loc.region:
location_parts.append(loc.region)
if loc.postalCode:
location_parts.append(loc.postalCode)
if loc.countryCode:
location_parts.append(loc.countryCode)
if location_parts:
text_parts.append(f"Location: {', '.join(location_parts)}")
if basics.profiles:
text_parts.append("Profiles:")
for profile in basics.profiles:
text_parts.append(
f" - {profile.network}: {profile.username} ({profile.url})"
)
if resume_data.work:
text_parts.append("\n=== WORK EXPERIENCE ===")
for i, work in enumerate(resume_data.work, 1):
text_parts.append(f"{i}. {work.position} at {work.name}")
text_parts.append(f" Period: {work.startDate} - {work.endDate}")
if work.url:
text_parts.append(f" Website: {work.url}")
if work.summary:
text_parts.append(f" Description: {work.summary}")
if work.highlights:
text_parts.append(" Key Achievements:")
for highlight in work.highlights:
text_parts.append(f"{highlight}")
if resume_data.education:
text_parts.append("\n=== EDUCATION ===")
for i, edu in enumerate(resume_data.education, 1):
text_parts.append(f"{i}. {edu.studyType} in {edu.area}")
text_parts.append(f" Institution: {edu.institution}")
text_parts.append(f" Period: {edu.startDate} - {edu.endDate}")
if edu.score:
text_parts.append(f" Score: {edu.score}")
if edu.url:
text_parts.append(f" Website: {edu.url}")
if edu.courses:
text_parts.append(f" Courses: {', '.join(edu.courses)}")
if resume_data.skills:
text_parts.append("\n=== SKILLS ===")
for skill in resume_data.skills:
text_parts.append(f"{skill.name}")
if skill.level:
text_parts.append(f" Level: {skill.level}")
if skill.keywords:
text_parts.append(f" Keywords: {', '.join(skill.keywords)}")
if resume_data.projects:
text_parts.append("\n=== PROJECTS ===")
for i, project in enumerate(resume_data.projects, 1):
text_parts.append(f"{i}. {project.name}")
if project.startDate and project.endDate:
text_parts.append(f" Period: {project.startDate} - {project.endDate}")
if project.description:
text_parts.append(f" Description: {project.description}")
if project.url:
text_parts.append(f" URL: {project.url}")
if project.highlights:
text_parts.append(" Highlights:")
for highlight in project.highlights:
text_parts.append(f"{highlight}")
if resume_data.awards:
text_parts.append("\n=== AWARDS ===")
for award in resume_data.awards:
text_parts.append(f"{award.title} - {award.awarder} ({award.date})")
if award.summary:
text_parts.append(f" {award.summary}")
if resume_data.certificates:
text_parts.append("\n=== CERTIFICATES ===")
for cert in resume_data.certificates:
text_parts.append(f"{cert.name} - {cert.issuer} ({cert.date})")
if cert.url:
text_parts.append(f" URL: {cert.url}")
if resume_data.publications:
text_parts.append("\n=== PUBLICATIONS ===")
for pub in resume_data.publications:
text_parts.append(f"{pub.name} - {pub.publisher} ({pub.releaseDate})")
if pub.url:
text_parts.append(f" URL: {pub.url}")
if pub.summary:
text_parts.append(f" {pub.summary}")
if resume_data.languages:
text_parts.append("\n=== LANGUAGES ===")
for lang in resume_data.languages:
text_parts.append(f"{lang.language} - {lang.fluency}")
if resume_data.interests:
text_parts.append("\n=== INTERESTS ===")
for interest in resume_data.interests:
text_parts.append(f"{interest.name}")
if interest.keywords:
text_parts.append(f" Keywords: {', '.join(interest.keywords)}")
if resume_data.references:
text_parts.append("\n=== REFERENCES ===")
for ref in resume_data.references:
text_parts.append(f"{ref.name}")
if ref.reference:
text_parts.append(f" {ref.reference}")
if resume_data.volunteer:
text_parts.append("\n=== VOLUNTEER EXPERIENCE ===")
for volunteer in resume_data.volunteer:
text_parts.append(f"{volunteer.position} at {volunteer.organization}")
text_parts.append(f" Period: {volunteer.startDate} - {volunteer.endDate}")
if volunteer.url:
text_parts.append(f" Website: {volunteer.url}")
if volunteer.summary:
text_parts.append(f" Description: {volunteer.summary}")
if volunteer.highlights:
text_parts.append(" Highlights:")
for highlight in volunteer.highlights:
text_parts.append(f"{highlight}")
return "\n".join(text_parts)
def convert_github_data_to_text(github_data: dict) -> str:
github_text = "\n\n=== GITHUB DATA ===\n"
if "profile" in github_data:
profile = github_data["profile"]
github_text += f"GitHub Profile:\n"
github_text += f"- Username: {profile.get('username', 'N/A')}\n"
github_text += f"- Name: {profile.get('name', 'N/A')}\n"
github_text += f"- Bio: {profile.get('bio', 'N/A')}\n"
github_text += f"- Public Repositories: {profile.get('public_repos', 'N/A')}\n"
github_text += f"- Followers: {profile.get('followers', 'N/A')}\n"
github_text += f"- Following: {profile.get('following', 'N/A')}\n"
github_text += f"- Account Created: {profile.get('created_at', 'N/A')}\n"
github_text += f"- Last Updated: {profile.get('updated_at', 'N/A')}\n"
if "projects" in github_data:
projects = github_data["projects"]
github_text += f"\nGitHub Projects ({len(projects)} total):\n"
for i, project in enumerate(projects[:10], 1):
github_text += f"{i}. {project.get('name', 'N/A')}\n"
github_text += f" Description: {project.get('description', 'N/A')}\n"
github_text += f" URL: {project.get('github_url', 'N/A')}\n"
if "github_details" in project:
details = project["github_details"]
github_text += f" Stars: {details.get('stars', 'N/A')}\n"
github_text += f" Forks: {details.get('forks', 'N/A')}\n"
github_text += f" Language: {details.get('language', 'N/A')}\n"
github_text += "\n"
return github_text
def convert_blog_data_to_text(blog_data: dict) -> str:
blog_text = "\n\n=== BLOG DATA ===\n"
blog_text += f"Total Blogs Found: {blog_data.get('total_blogs', 'N/A')}\n"
blog_text += f"Blog Score: {blog_data.get('blog_score', 'N/A')}/10.0\n"
blog_text += f"Blog Details: {blog_data.get('blog_details', 'N/A')}\n"
if "blogs" in blog_data:
blog_text += "\nBlog URLs Found:\n"
for i, blog in enumerate(blog_data["blogs"][:5], 1):
blog_text += f"{i}. {blog.get('url', 'N/A')}\n"
blog_text += f" Score: {blog.get('score', 'N/A')}/10.0\n"
blog_text += f" Details: {blog.get('details', 'N/A')}\n"
blog_text += "\n"
return blog_text