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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 237 additions & 0 deletions codeforces.py
Original file line number Diff line number Diff line change
@@ -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,
}
2 changes: 1 addition & 1 deletion evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 5 additions & 4 deletions models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
...
Expand Down Expand Up @@ -226,6 +226,7 @@ class Scores(BaseModel):
self_projects: CategoryScore
production: CategoryScore
technical_skills: CategoryScore
competitive_programming: CategoryScore


class BonusPoints(BaseModel):
Expand Down Expand Up @@ -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."""

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
40 changes: 36 additions & 4 deletions prompts/templates/resume_evaluation_criteria.jinja
Original file line number Diff line number Diff line change
@@ -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:**
Expand Down Expand Up @@ -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):**
Expand Down Expand Up @@ -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
Expand All @@ -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.**

Expand All @@ -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"},
Expand Down
Loading