diff --git a/codeforces.py b/codeforces.py new file mode 100644 index 00000000..a21bfee6 --- /dev/null +++ b/codeforces.py @@ -0,0 +1,237 @@ +""" +Codeforces API Integration + +Fetches competitive programming data from Codeforces public API: +- User profile (rating, rank, solved count) +- Cheating detection via SKIPPED verdict analysis +""" + +import logging +import requests +from typing import Dict, Optional, List, Tuple + +logger = logging.getLogger(__name__) + +CODEFORCES_API_BASE = "https://codeforces.com/api" + +# Cheating thresholds +SKIPPED_PER_CONTEST_THRESHOLD = 4 # 4+ SKIPPED in one contest = cheated +CHEATED_CONTESTS_THRESHOLD = 2 # 2+ contests cheated = nuclear (entire CP section 0) +TOTAL_SKIPPED_THRESHOLD = 6 # 6+ SKIPPED across all contests = also nuclear + + +def _extract_handle_from_url(url: str) -> Optional[str]: + """Extract Codeforces handle from a profile URL. + + Handles formats like: + - https://codeforces.com/profile/tourist + - https://www.codeforces.com/profile/tourist + - codeforces.com/profile/tourist + - Just the handle itself (e.g. 'tourist') + """ + if not url: + return None + + url = url.strip().rstrip("/") + + # If it looks like a URL with /profile/ + if "/profile/" in url: + parts = url.split("/profile/") + if len(parts) == 2 and parts[1]: + return parts[1].strip("/") + + # If it's a codeforces URL without /profile/ (e.g. codeforces.com/tourist) + if "codeforces.com" in url: + parts = url.rstrip("/").split("/") + if parts: + return parts[-1] + + # If it's just a handle (no slashes, no dots) + if "/" not in url and "." not in url: + return url + + return None + + +def fetch_codeforces_profile(handle: str) -> Optional[Dict]: + """Fetch user profile info from Codeforces API. + + Returns: + Dict with keys: handle, rating, max_rating, rank, max_rank, solved_count + or None on failure. + """ + try: + # Fetch user info + resp = requests.get( + f"{CODEFORCES_API_BASE}/user.info", + params={"handles": handle}, + timeout=15, + ) + resp.raise_for_status() + data = resp.json() + + if data.get("status") != "OK" or not data.get("result"): + logger.warning(f"Codeforces API returned non-OK for handle '{handle}'") + return None + + user = data["result"][0] + + profile = { + "handle": user.get("handle", handle), + "rating": user.get("rating", 0), + "max_rating": user.get("maxRating", 0), + "rank": user.get("rank", "unrated"), + "max_rank": user.get("maxRank", "unrated"), + } + + # Fetch solved count from user.status (count unique accepted problems) + solved_count = _fetch_solved_count(handle) + profile["solved_count"] = solved_count + + return profile + + except requests.RequestException as e: + logger.error(f"Failed to fetch Codeforces profile for '{handle}': {e}") + return None + except (KeyError, IndexError, ValueError) as e: + logger.error(f"Failed to parse Codeforces profile for '{handle}': {e}") + return None + + +def _fetch_solved_count(handle: str) -> int: + """Count unique problems solved (ACCEPTED verdicts) by a user.""" + try: + resp = requests.get( + f"{CODEFORCES_API_BASE}/user.status", + params={"handle": handle}, + timeout=30, + ) + resp.raise_for_status() + data = resp.json() + + if data.get("status") != "OK": + return 0 + + solved = set() + for submission in data.get("result", []): + if submission.get("verdict") == "OK": + problem = submission.get("problem", {}) + contest_id = problem.get("contestId", "") + index = problem.get("index", "") + if contest_id and index: + solved.add(f"{contest_id}-{index}") + + return len(solved) + + except Exception as e: + logger.error(f"Failed to fetch solved count for '{handle}': {e}") + return 0 + + +def detect_cheating(handle: str) -> Dict: + """Analyze user submissions for cheating patterns. + + Detects SKIPPED verdicts per contest. A SKIPPED verdict on Codeforces + typically indicates the system detected plagiarism/cheating. + + Returns: + Dict with keys: + - is_cheater: bool + - is_serial_cheater: bool (2+ contests or 6+ total SKIPPED) + - cheated_contests: int (number of contests with 4+ SKIPPED) + - total_skipped: int + - details: str + """ + result = { + "is_cheater": False, + "is_serial_cheater": False, + "cheated_contests": 0, + "total_skipped": 0, + "details": "No cheating detected", + } + + try: + resp = requests.get( + f"{CODEFORCES_API_BASE}/user.status", + params={"handle": handle}, + timeout=30, + ) + resp.raise_for_status() + data = resp.json() + + if data.get("status") != "OK": + return result + + # Group SKIPPED verdicts by contest + skipped_by_contest: Dict[int, int] = {} + for submission in data.get("result", []): + if submission.get("verdict") == "SKIPPED": + contest_id = submission.get("contestId") + if contest_id: + skipped_by_contest[contest_id] = ( + skipped_by_contest.get(contest_id, 0) + 1 + ) + + total_skipped = sum(skipped_by_contest.values()) + cheated_contests = sum( + 1 + for count in skipped_by_contest.values() + if count >= SKIPPED_PER_CONTEST_THRESHOLD + ) + + result["total_skipped"] = total_skipped + result["cheated_contests"] = cheated_contests + + if cheated_contests >= 1: + result["is_cheater"] = True + result["details"] = ( + f"Detected {cheated_contests} contest(s) with " + f"{SKIPPED_PER_CONTEST_THRESHOLD}+ SKIPPED submissions " + f"(total SKIPPED: {total_skipped})" + ) + + # Nuclear: 2+ cheated contests or 6+ total SKIPPED + if ( + cheated_contests >= CHEATED_CONTESTS_THRESHOLD + or total_skipped >= TOTAL_SKIPPED_THRESHOLD + ): + result["is_serial_cheater"] = True + result["details"] = ( + f"SERIAL CHEATING: {cheated_contests} cheated contest(s), " + f"{total_skipped} total SKIPPED submissions. " + f"Entire competitive programming section zeroed." + ) + + except Exception as e: + logger.error(f"Failed to run cheating detection for '{handle}': {e}") + result["details"] = f"Cheating detection failed: {e}" + + return result + + +def fetch_codeforces_data(url_or_handle: str) -> Optional[Dict]: + """Orchestrate Codeforces data fetching: profile + cheating detection. + + Args: + url_or_handle: Codeforces profile URL or raw handle. + + Returns: + Dict with keys: profile, cheating, or None if handle extraction fails. + """ + handle = _extract_handle_from_url(url_or_handle) + if not handle: + logger.warning(f"Could not extract Codeforces handle from: {url_or_handle}") + return None + + logger.info(f"Fetching Codeforces data for handle: {handle}") + + profile = fetch_codeforces_profile(handle) + if not profile: + return None + + cheating = detect_cheating(handle) + + return { + "profile": profile, + "cheating": cheating, + } diff --git a/evaluator.py b/evaluator.py index 1f9e91f5..d28b00b6 100644 --- a/evaluator.py +++ b/evaluator.py @@ -8,7 +8,7 @@ MAX_BONUS_POINTS = 20 MIN_FINAL_SCORE = -20 -MAX_FINAL_SCORE = 120 +MAX_FINAL_SCORE = 140 from prompt import ( DEFAULT_MODEL, diff --git a/models.py b/models.py index e7146005..4e4a6824 100644 --- a/models.py +++ b/models.py @@ -19,7 +19,7 @@ def chat( model: str, messages: List[Dict[str, str]], options: Dict[str, Any] = None, - **kwargs + **kwargs, ) -> Dict[str, Any]: """Send a chat request to the LLM provider.""" ... @@ -226,6 +226,7 @@ class Scores(BaseModel): self_projects: CategoryScore production: CategoryScore technical_skills: CategoryScore + competitive_programming: CategoryScore class BonusPoints(BaseModel): @@ -281,7 +282,7 @@ def chat( model: str, messages: List[Dict[str, str]], options: Dict[str, Any] = None, - **kwargs + **kwargs, ) -> Dict[str, Any]: """Send a chat request to Ollama.""" @@ -324,7 +325,7 @@ def chat( model: str, messages: List[Dict[str, str]], options: Dict[str, Any] = None, - **kwargs + **kwargs, ) -> Dict[str, Any]: """Send a chat request to Google Gemini API.""" import re @@ -375,7 +376,7 @@ def chat( 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) + 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 diff --git a/prompts/templates/resume_evaluation_criteria.jinja b/prompts/templates/resume_evaluation_criteria.jinja index 45c0daf9..60d76f72 100644 --- a/prompts/templates/resume_evaluation_criteria.jinja +++ b/prompts/templates/resume_evaluation_criteria.jinja @@ -1,6 +1,6 @@ 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.** +**MANDATORY: You MUST always fill ALL FIVE categories: open_source, self_projects, production, technical_skills, competitive_programming.** ## CRITICAL FAIRNESS REQUIREMENTS **SCORES MUST NEVER DEPEND ON:** @@ -89,6 +89,31 @@ You are evaluating a resume for a Software Intern position at HackerRank. Analyz ### Technical Skills (0-10 points) - Analyze the 'skills', 'languages', and evidence of technical breadth or problem-solving in projects, work, or competitions +### Competitive Programming (0-20 points) +Analyze the competitive programming data (if provided in === COMPETITIVE PROGRAMMING DATA === section), resume awards, and any mentions of competitive programming platforms. + +**ICPC (up to 10 points):** +- +10 points for ICPC Regionalist (participated at ICPC Regional level or higher) +- +5 points for ICPC participation at preliminary/online rounds only + +**Codeforces Rating (up to 5 points):** +- +5 points for Expert or higher on Codeforces (rating >= 1600) +- +3 points for Specialist on Codeforces (rating >= 1400) +- +1 point for Pupil on Codeforces (rating >= 1200) +- 0 points for Newbie or unrated + +**Problem Solving Count (up to 5 points) — highest applicable tier only:** +- +5 points for 1000+ problems solved (LeetCode + Codeforces combined) +- +4 points for 800+ problems solved +- +3 points for 500+ problems solved +- +1 point for 200+ problems solved +- 0 points for fewer than 200 problems + +**CHEATING PENALTIES (CRITICAL — MUST ENFORCE):** +- If Codeforces cheating data shows `is_cheater: True` (4+ SKIPPED submissions in a single contest): set Codeforces rating points to 0 (ICPC and problem count points remain) +- If Codeforces cheating data shows `is_serial_cheater: True` (2+ cheated contests or 6+ total SKIPPED): set the ENTIRE competitive_programming score to 0 +- Cheating penalties are NON-NEGOTIABLE — if the data says cheating is detected, you MUST apply the penalty regardless of other achievements + ## PROJECT COMPLEXITY ASSESSMENT **Simple/Basic Projects (Low Impact):** @@ -141,9 +166,14 @@ You are evaluating a resume for a Software Intern position at HackerRank. Analyz - 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 +**For Competitive Programming Cheating:** +- If `is_cheater` is True: Codeforces rating-based points within competitive_programming → 0 +- If `is_serial_cheater` is True: entire competitive_programming score → 0 +- Include cheating details in evidence and deduction reasons + ## 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 +2. You MUST fill ALL FIVE score categories: open_source, self_projects, production, technical_skills, competitive_programming 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 @@ -160,8 +190,9 @@ You are evaluating a resume for a Software Intern position at HackerRank. Analyz - self_projects: 0-30 points (maximum 30) - production: 0-25 points (maximum 25) - technical_skills: 0-10 points (maximum 10) + - competitive_programming: 0-20 points (maximum 20) - Bonus points total must be <= 20 (maximum 20 points) -- **OVERALL SCORE LIMIT**: The total score (categories + bonus - deductions) cannot exceed 120 points +- **OVERALL SCORE LIMIT**: The total score (categories + bonus - deductions) cannot exceed 140 points **DO NOT RETURN A RESUME SUMMARY. RETURN ONLY THE SCORING EVALUATION IN THE SPECIFIED JSON FORMAT.** @@ -172,7 +203,8 @@ Analyze the following resume and provide a JSON response with this EXACT structu "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"} + "technical_skills": {"score": 0, "max": 10, "evidence": "string"}, + "competitive_programming": {"score": 0, "max": 20, "evidence": "string"} }, "bonus_points": {"total": 0, "breakdown": "string"}, "deductions": {"total": 0, "reasons": "string"}, diff --git a/prompts/templates/resume_evaluation_system_message.jinja b/prompts/templates/resume_evaluation_system_message.jinja index eb68c0f1..3bef68ed 100644 --- a/prompts/templates/resume_evaluation_system_message.jinja +++ b/prompts/templates/resume_evaluation_system_message.jinja @@ -18,7 +18,7 @@ You are an expert technical recruiter evaluating resumes. Provide accurate, obje - 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.** +**MANDATORY: You MUST always fill ALL FIVE categories: open_source, self_projects, production, technical_skills, competitive_programming.** - 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). @@ -28,7 +28,9 @@ You are an expert technical recruiter evaluating resumes. Provide accurate, obje - 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. +- For competitive_programming: Analyze the competitive programming data (if provided in === COMPETITIVE PROGRAMMING DATA === section), resume awards, mentions of ICPC, Codeforces, LeetCode, or competitive programming contests. Score based on: ICPC participation (up to 10 pts), Codeforces rating (up to 5 pts), and problems solved count on LeetCode + Codeforces combined (up to 5 pts). **CRITICAL CHEATING ENFORCEMENT**: If the competitive programming data indicates `is_cheater: True`, set Codeforces rating-based points to 0 within this category. If `is_serial_cheater: True`, set the ENTIRE competitive_programming score to 0. These cheating penalties are NON-NEGOTIABLE. If no competitive programming data or mentions are found, score this category as 0 with evidence stating no CP activity found. + +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, competitive_programming), bonus_points, deductions, key_strengths, areas_for_improvement. **IMPORTANT LIST CONSTRAINTS:** - key_strengths: Provide 1-5 items (maximum 5 key strengths) @@ -42,8 +44,9 @@ CRITICAL: You MUST respond with the EXACT JSON structure specified in the prompt - self_projects: 0-30 points (maximum 30) - production: 0-25 points (maximum 25) - technical_skills: 0-10 points (maximum 10) + - competitive_programming: 0-20 points (maximum 20) - 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 +- **OVERALL SCORE LIMIT**: The total score (categories + bonus - deductions) cannot exceed 140 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. \ No newline at end of file +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. When competitive programming data is provided (look for '=== COMPETITIVE PROGRAMMING DATA ===' section), use the verified Codeforces rating, rank, solved count, and cheating analysis to accurately score the competitive_programming category. **CRITICAL CHEATING ENFORCEMENT**: The cheating analysis in CP data is based on verified API data — if `is_cheater` or `is_serial_cheater` is True, you MUST apply the specified penalties without exception. 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. \ No newline at end of file diff --git a/score.py b/score.py index 21fd06cd..c2f3284c 100644 --- a/score.py +++ b/score.py @@ -15,7 +15,9 @@ convert_json_resume_to_text, convert_github_data_to_text, convert_blog_data_to_text, + convert_cp_data_to_text, ) +from codeforces import fetch_codeforces_data from config import DEVELOPMENT_MODE logger = logging.getLogger(__name__) @@ -63,7 +65,7 @@ def print_evaluation_results( 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) + max_possible_score = max_score + 20 # 140 (120 categories + 20 bonus) if total_score > max_possible_score: total_score = max_possible_score print(f"⚠️ Warning: Total score capped at maximum possible value") @@ -82,6 +84,7 @@ def print_evaluation_results( "self_projects": 30, "production": 25, "technical_skills": 10, + "competitive_programming": 20, } # Open Source @@ -122,6 +125,19 @@ def print_evaluation_results( print(f" Evidence: {tech_score.evidence}") print() + # Competitive Programming + if ( + hasattr(evaluation.scores, "competitive_programming") + and evaluation.scores.competitive_programming + ): + cp_score = evaluation.scores.competitive_programming + capped_score = min( + cp_score.score, category_maxes["competitive_programming"] + ) + print(f"🏆 Competitive Prog: {capped_score}/{cp_score.max}") + print(f" Evidence: {cp_score.evidence}") + print() + # Bonus Points if hasattr(evaluation, "bonus_points") and evaluation.bonus_points: print(f"\n⭐ BONUS POINTS: {evaluation.bonus_points.total}") @@ -160,7 +176,10 @@ def print_evaluation_results( def _evaluate_resume( - resume_data: JSONResume, github_data: dict = None, blog_data: dict = None + resume_data: JSONResume, + github_data: dict = None, + blog_data: dict = None, + cp_data: dict = None, ) -> Optional[EvaluationData]: """Evaluate the resume using AI and display results.""" @@ -180,6 +199,11 @@ def _evaluate_resume( blog_text = convert_blog_data_to_text(blog_data) resume_text += blog_text + # Add competitive programming data if available + if cp_data: + cp_text = convert_cp_data_to_text(cp_data) + resume_text += cp_text + # Evaluate the enhanced resume evaluation_result = evaluator.evaluate_resume(resume_text) @@ -323,7 +347,32 @@ def main(pdf_path): encoding="utf-8", ) - score = _evaluate_resume(resume_data, github_data) + # --- Competitive Programming Data --- + cp_data = {} + + # Look for Codeforces profile + profiles = [] + if resume_data and hasattr(resume_data, "basics") and resume_data.basics: + profiles = resume_data.basics.profiles or [] + + codeforces_profile = find_profile(profiles, "Codeforces") + if codeforces_profile: + print(f"Fetching Codeforces data...") + cf_data = fetch_codeforces_data(codeforces_profile.url) + if cf_data: + cp_data["codeforces"] = cf_data + + # Look for LeetCode profile (no API, just pass URL for context) + leetcode_profile = find_profile(profiles, "LeetCode") + if not leetcode_profile: + leetcode_profile = find_profile(profiles, "Leetcode") + if leetcode_profile: + cp_data["leetcode"] = { + "username": leetcode_profile.username, + "url": leetcode_profile.url, + } + + score = _evaluate_resume(resume_data, github_data, cp_data=cp_data) # Get candidate name for display candidate_name = os.path.basename(pdf_path).replace(".pdf", "") diff --git a/transform.py b/transform.py index 25eab1d3..eeeded31 100644 --- a/transform.py +++ b/transform.py @@ -685,17 +685,22 @@ def transform_evaluation_response( csv_row["technical_skills_score"] = scores.technical_skills.score csv_row["technical_skills_max"] = scores.technical_skills.max + csv_row["competitive_programming_score"] = scores.competitive_programming.score + csv_row["competitive_programming_max"] = scores.competitive_programming.max + total_score = ( scores.open_source.score + scores.self_projects.score + scores.production.score + scores.technical_skills.score + + scores.competitive_programming.score ) total_max = ( scores.open_source.max + scores.self_projects.max + scores.production.max + scores.technical_skills.max + + scores.competitive_programming.max ) csv_row["total_score"] = total_score @@ -709,6 +714,8 @@ def transform_evaluation_response( csv_row["production_max"] = "N/A" csv_row["technical_skills_score"] = "N/A" csv_row["technical_skills_max"] = "N/A" + csv_row["competitive_programming_score"] = "N/A" + csv_row["competitive_programming_max"] = "N/A" csv_row["total_score"] = "N/A" csv_row["total_max"] = "N/A" @@ -936,3 +943,44 @@ def convert_blog_data_to_text(blog_data: dict) -> str: blog_text += "\n" return blog_text + + +def convert_cp_data_to_text(cp_data: dict) -> str: + """Convert competitive programming data to text for LLM evaluation.""" + cp_text = "\n\n=== COMPETITIVE PROGRAMMING DATA ===\n" + + if "codeforces" in cp_data and cp_data["codeforces"]: + cf = cp_data["codeforces"] + profile = cf.get("profile", {}) + cheating = cf.get("cheating", {}) + + cp_text += "\nCodeforces Profile:\n" + cp_text += f"- Handle: {profile.get('handle', 'N/A')}\n" + cp_text += f"- Current Rating: {profile.get('rating', 0)}\n" + cp_text += f"- Max Rating: {profile.get('max_rating', 0)}\n" + cp_text += f"- Current Rank: {profile.get('rank', 'unrated')}\n" + cp_text += f"- Max Rank: {profile.get('max_rank', 'unrated')}\n" + cp_text += f"- Problems Solved: {profile.get('solved_count', 0)}\n" + + cp_text += "\nCodeforces Cheating Analysis:\n" + cp_text += f"- Cheating Detected: {cheating.get('is_cheater', False)}\n" + cp_text += f"- Serial Cheater: {cheating.get('is_serial_cheater', False)}\n" + cp_text += f"- Cheated Contests: {cheating.get('cheated_contests', 0)}\n" + cp_text += f"- Total Skipped Submissions: {cheating.get('total_skipped', 0)}\n" + cp_text += f"- Details: {cheating.get('details', 'N/A')}\n" + + if "leetcode" in cp_data and cp_data["leetcode"]: + lc = cp_data["leetcode"] + cp_text += "\nLeetCode Profile:\n" + cp_text += f"- Username: {lc.get('username', 'N/A')}\n" + cp_text += f"- URL: {lc.get('url', 'N/A')}\n" + cp_text += ( + " (LeetCode data is self-reported from resume; no API verification)\n" + ) + + if "icpc" in cp_data and cp_data["icpc"]: + cp_text += "\nICPC Data:\n" + cp_text += f"- Participation: {cp_data['icpc'].get('participation', 'N/A')}\n" + cp_text += f"- Details: {cp_data['icpc'].get('details', 'N/A')}\n" + + return cp_text