-
-
Notifications
You must be signed in to change notification settings - Fork 4.9k
ATS Screening Feature, Chrome Extension Integration, PDF Layout Fix & Dashboard UX #778
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 47 commits
e634ebf
a19e96e
85e0908
8480ec5
a8f34d1
d57bd88
fc79bde
ced11de
6b33186
7e3036c
9b05f8f
7f2cbb3
77f88fd
81e811f
afe71ee
3b5ae86
2db2720
91d82cd
344d68d
8a5af6e
f83b4f5
5827b4d
76e37b7
2daf8aa
86828a9
0f9f4f3
7fc412a
d3b9391
63734b4
ebd7361
37b09b7
03aca01
7f84d93
26c92f1
e0592fe
47a9bca
9c0a958
1d8f1d8
951f708
b7a9f7c
43ff227
dadb127
c62024b
05cca6a
36bca68
8c245e8
c19023c
0a9f326
254e701
fc080d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| """Prompt templates for the ATS two-pass pipeline.""" | ||
|
|
||
| ATS_SCORE_PROMPT = """You are an ATS (Applicant Tracking System) engine. Analyze the job description and resume below. | ||
|
|
||
| Score using these weighted categories. Do NOT exceed the listed maximum for each: | ||
| - skills_match: 0-30 points (explicit hard skills: programming languages, domain skills, certifications) | ||
| - experience_match: 0-25 points (years of experience, seniority level, role progression match) | ||
| - domain_match: 0-20 points (industry knowledge, domain terminology, sector-specific language) | ||
| - tools_match: 0-15 points (specific tools, platforms, software, technologies named in JD) | ||
| - achievement_match: 0-10 points (quantified results, measurable impacts, specific accomplishments) | ||
|
|
||
| Decision rules (apply AFTER scoring): | ||
| - total_score >= 75 → decision = "PASS" | ||
| - 60 <= total_score < 75 → decision = "BORDERLINE" | ||
| - total_score < 60 → decision = "REJECT" — you MUST provide at least 10 distinct warning_flags | ||
|
|
||
| For keyword_table: extract the 20-30 most important keywords/phrases from the JD. For each, state whether it appears in the resume and in which section. Use section names: "summary", "workExperience", "education", "additional", or null if not found. | ||
|
|
||
| For warning_flags: be concrete and specific. Write "Missing 3+ years of product management experience — resume shows 1 year" not "lacks experience". Every flag must name a specific gap. | ||
|
|
||
| Output ONLY the following JSON object. No explanation, no markdown: | ||
| {{ | ||
| "score_breakdown": {{ | ||
| "skills_match": <integer 0-30>, | ||
| "experience_match": <integer 0-25>, | ||
| "domain_match": <integer 0-20>, | ||
| "tools_match": <integer 0-15>, | ||
| "achievement_match": <integer 0-10> | ||
| }}, | ||
| "total_score": <integer 0-100>, | ||
| "decision": "PASS" | "BORDERLINE" | "REJECT", | ||
| "keyword_table": [ | ||
| {{"keyword": "...", "found_in_resume": true, "section": "workExperience"}}, | ||
| {{"keyword": "...", "found_in_resume": false, "section": null}} | ||
| ], | ||
| "missing_keywords": ["keyword1", "keyword2"], | ||
| "warning_flags": ["Specific flag 1", "Specific flag 2"] | ||
| }} | ||
|
|
||
| Job Description: | ||
| {job_description} | ||
|
|
||
| Resume: | ||
| {resume_text}""" | ||
|
|
||
|
|
||
| ATS_OPTIMIZE_PROMPT = """You are an ATS resume optimizer. Improve the resume below to better match the job description, guided by the gap analysis. | ||
|
|
||
| {critical_truthfulness_rules} | ||
|
|
||
| Gap Analysis: | ||
| Missing Keywords: {missing_keywords} | ||
|
|
||
| Warning Flags: | ||
| {warning_flags} | ||
|
|
||
| Score Breakdown: {score_breakdown} | ||
|
|
||
| Optimization rules: | ||
| - Weave missing keywords into existing bullets ONLY where the candidate's actual experience supports them | ||
| - If the resume contains any of these exact phrases — "product judgment", "operating in ambiguity", "structured thinking", "data-driven decision making" — preserve them verbatim in the output | ||
| - Do NOT add those PM phrases if they do not appear anywhere in the original resume text | ||
| - Strengthen vague action verbs ("worked on" → "led", "helped with" → "drove") | ||
| - Improve the summary to lead with the most JD-relevant experience | ||
| - Provide 5-10 specific, actionable optimization_suggestions explaining what changed and why | ||
|
|
||
| Job Description: | ||
| {job_description} | ||
|
|
||
| Original Resume (JSON): | ||
| {resume_json} | ||
|
|
||
| Output ONLY this JSON. The optimized_resume field must match the schema exactly: | ||
| {{ | ||
| "optimized_resume": {schema}, | ||
| "optimization_suggestions": ["suggestion1", "suggestion2"] | ||
| }}""" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| """ATS screening endpoint.""" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P0: Custom agent: Flag Security Vulnerabilities ATS endpoints create an IDOR/authorization gap by trusting client-supplied resume/job/parent IDs for reads and writes without ownership scoping. Prompt for AI agents |
||
|
|
||
| import json | ||
| import logging | ||
|
|
||
| from fastapi import APIRouter, HTTPException | ||
|
|
||
| from app.database import db | ||
| from app.schemas.ats import ATSScreenRequest, ATSScreeningResult, ATSSaveResumeRequest, ATSSaveResumeResponse | ||
| from app.services.ats_optimizer import run_pass2 | ||
| from app.services.ats_scorer import run_pass1 | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| router = APIRouter(prefix="/ats", tags=["ATS"]) | ||
|
|
||
|
|
||
| @router.post("/screen", response_model=ATSScreeningResult) | ||
| async def screen_resume(request: ATSScreenRequest) -> ATSScreeningResult: | ||
| """Run ATS screening: score + optimize resume vs job description.""" | ||
| # Validate at least one resume source and one job source | ||
| if not request.resume_id and not request.resume_text: | ||
| raise HTTPException( | ||
| status_code=422, | ||
| detail="Either resume_id or resume_text is required.", | ||
| ) | ||
| if not request.job_id and not request.job_description: | ||
| raise HTTPException( | ||
| status_code=422, | ||
| detail="Either job_id or job_description is required.", | ||
| ) | ||
|
|
||
| # Resolve resume | ||
| resume_text = request.resume_text or "" | ||
| resume_json: dict = {} | ||
| from_db = False | ||
|
|
||
| if request.resume_id: | ||
| resume = db.get_resume(request.resume_id) | ||
| if not resume: | ||
| raise HTTPException(status_code=404, detail="Resume not found.") | ||
| resume_json = resume.get("processed_data") or {} | ||
| resume_text = resume.get("content", resume_text) | ||
| from_db = True | ||
|
|
||
| # Only enforce the minimum length check on directly-supplied resume_text, | ||
| # not on content fetched from the database (which may be a stored reference). | ||
| if not from_db and len(resume_text.strip()) < 100: | ||
| raise HTTPException( | ||
| status_code=400, | ||
| detail="Resume text too short to analyze (minimum 100 characters).", | ||
| ) | ||
|
|
||
| # Resolve job description | ||
| job_text = request.job_description or "" | ||
|
|
||
| if request.job_id: | ||
| job = db.get_job(request.job_id) | ||
| if not job: | ||
| raise HTTPException(status_code=404, detail="Job not found.") | ||
| job_text = job.get("content", job_text) | ||
|
|
||
| # Pass 1: Score | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Job description lacks minimum length validation before ATS scoring, allowing whitespace-only or empty content to reach the LLM and trigger wasteful calls. Prompt for AI agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Job description lacks minimum length validation before ATS scoring, allowing whitespace-only or empty content to reach the LLM and trigger wasteful calls. Prompt for AI agents |
||
| try: | ||
| pass1 = await run_pass1(resume_text, job_text) | ||
| except Exception as exc: | ||
| logger.error("ATS Pass 1 failed: %s", exc) | ||
| raise HTTPException( | ||
| status_code=500, | ||
| detail="ATS scoring failed. Please try again.", | ||
| ) from exc | ||
|
|
||
| # Pass 2: Optimize (non-fatal — partial result returned on failure) | ||
| # Skip if no structured resume JSON available (paste mode only) | ||
| optimized_resume = None | ||
| optimization_suggestions: list[str] = [] | ||
| if resume_json: # Only run if we have structured resume data | ||
| try: | ||
| pass2 = await run_pass2( | ||
| resume_json=resume_json, | ||
| job_text=job_text, | ||
| score_data=pass1, | ||
| ) | ||
| optimized_resume = pass2["optimized_resume"] | ||
| optimization_suggestions = pass2["optimization_suggestions"] | ||
| except Exception as exc: | ||
| logger.warning("ATS Pass 2 failed (non-fatal): %s", exc) | ||
|
|
||
| # Optionally persist the optimized resume | ||
| saved_resume_id: str | None = None | ||
| if request.save_optimized: | ||
| if optimized_resume is None: | ||
| raise HTTPException( | ||
| status_code=409, | ||
| detail="Optimization unavailable — cannot save.", | ||
| ) | ||
| try: | ||
| optimized_dict = optimized_resume.model_dump() | ||
| new_resume = db.create_resume( | ||
| content=json.dumps(optimized_dict), | ||
| content_type="json", | ||
| processed_data=optimized_dict, | ||
| processing_status="ready", | ||
| parent_id=request.resume_id, | ||
| title="ATS Optimized Resume", | ||
| ) | ||
| saved_resume_id = new_resume["resume_id"] | ||
| except Exception as exc: | ||
| logger.error("Failed to save optimized resume: %s", exc) | ||
| raise HTTPException( | ||
| status_code=500, | ||
| detail="Failed to save optimized resume.", | ||
| ) from exc | ||
|
|
||
| return ATSScreeningResult( | ||
| score=pass1["score"], | ||
| decision=pass1["decision"], | ||
| keyword_table=pass1["keyword_table"], | ||
| missing_keywords=pass1["missing_keywords"], | ||
| warning_flags=pass1["warning_flags"], | ||
| optimization_suggestions=optimization_suggestions, | ||
| optimized_resume=optimized_resume, | ||
| saved_resume_id=saved_resume_id, | ||
| ) | ||
|
|
||
|
|
||
| @router.post("/save-resume", response_model=ATSSaveResumeResponse) | ||
| async def save_ats_resume(request: ATSSaveResumeRequest) -> ATSSaveResumeResponse: | ||
| """Save an ATS-optimized resume directly without re-running the LLM.""" | ||
| import json as _json | ||
| try: | ||
| optimized_dict = request.resume_data.model_dump() | ||
| new_resume = db.create_resume( | ||
| content=_json.dumps(optimized_dict), | ||
| content_type="json", | ||
| processed_data=optimized_dict, | ||
| processing_status="ready", | ||
| parent_id=request.parent_id, | ||
| title=request.title, | ||
| ) | ||
| return ATSSaveResumeResponse(resume_id=new_resume["resume_id"]) | ||
| except Exception as exc: | ||
| logger.error("Failed to save ATS resume: %s", exc) | ||
| raise HTTPException(status_code=500, detail="Failed to save resume.") from exc | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,11 +11,15 @@ | |
|
|
||
| @router.get("/health", response_model=HealthResponse) | ||
| async def health_check() -> HealthResponse: | ||
| """Lightweight liveness check for Docker HEALTHCHECK. | ||
| """Liveness and readiness check. | ||
|
|
||
| Does NOT call the LLM provider. Use GET /status for full LLM health. | ||
| Calls the LLM provider to verify connectivity. Returns 'degraded' if the | ||
| LLM is unreachable or misconfigured, 'healthy' otherwise. | ||
| """ | ||
| return HealthResponse(status="healthy") | ||
| config = get_llm_config() | ||
| llm_status = await check_llm_health(config) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Custom agent: Flag Security Vulnerabilities Unauthenticated Prompt for AI agents
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
|
||
| status = "healthy" if llm_status["healthy"] else "degraded" | ||
| return HealthResponse(status=status) | ||
|
|
||
|
|
||
| @router.get("/status", response_model=StatusResponse) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| """Pydantic schemas for ATS screening.""" | ||
|
|
||
| from typing import Literal | ||
|
|
||
| from pydantic import BaseModel | ||
|
|
||
| from app.schemas.models import ResumeData | ||
|
|
||
|
|
||
| class ATSScreenRequest(BaseModel): | ||
| """Request body for POST /api/v1/ats/screen.""" | ||
|
|
||
| resume_id: str | None = None | ||
| resume_text: str | None = None | ||
| job_id: str | None = None | ||
| job_description: str | None = None | ||
| save_optimized: bool = False | ||
|
|
||
|
|
||
| class ScoreBreakdown(BaseModel): | ||
| """Weighted ATS score per dimension.""" | ||
|
|
||
| skills_match: float # max 30 | ||
| experience_match: float # max 25 | ||
| domain_match: float # max 20 | ||
| tools_match: float # max 15 | ||
| achievement_match: float # max 10 | ||
| total: float # 0-100 | ||
|
|
||
|
|
||
| class KeywordRow(BaseModel): | ||
| """One row in the keyword match table.""" | ||
|
|
||
| keyword: str | ||
| found_in_resume: bool | ||
| section: str | None = None # e.g. "workExperience", "summary", null if not found | ||
|
|
||
|
|
||
| class ATSScreeningResult(BaseModel): | ||
| """Full ATS screening report returned by the endpoint.""" | ||
|
|
||
| score: ScoreBreakdown | ||
| decision: Literal["PASS", "BORDERLINE", "REJECT"] | ||
| keyword_table: list[KeywordRow] | ||
| missing_keywords: list[str] | ||
| warning_flags: list[str] # >= 10 items when decision == "REJECT" | ||
| optimization_suggestions: list[str] | ||
| optimized_resume: ResumeData | None = None | ||
| saved_resume_id: str | None = None # populated when save_optimized=True | ||
|
|
||
|
|
||
| class ATSSaveResumeRequest(BaseModel): | ||
| """Request body for POST /api/v1/ats/save-resume.""" | ||
|
|
||
| resume_data: ResumeData | ||
| parent_id: str | None = None | ||
| title: str = "ATS Optimized Resume" | ||
|
|
||
|
|
||
| class ATSSaveResumeResponse(BaseModel): | ||
| """Response body for POST /api/v1/ats/save-resume.""" | ||
|
|
||
| resume_id: str |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P0: Custom agent: Flag Security Vulnerabilities
ATS endpoints create an IDOR/authorization gap by trusting client-supplied resume/job/parent IDs for reads and writes without ownership scoping.
Prompt for AI agents