From 17b7e4674cb6e138d5e38a5d696b177a0e272999 Mon Sep 17 00:00:00 2001 From: Hrishikesh Uchake Date: Sat, 24 Jan 2026 18:18:49 -0600 Subject: [PATCH] Implement semantic tech-stack matching for GitHub projects selection --- app.py | 10 ++++++- app_production.py | 22 +++++++++++++- scrapers/github_scraper.py | 60 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/app.py b/app.py index 4c683db..c01f783 100644 --- a/app.py +++ b/app.py @@ -319,7 +319,15 @@ def generate_resume(): # Step 2: Analyze job description job_requirements = job_analyzer.analyze(job_description) - # Step 3: Generate tailored resume content + # Step 3: Refine GitHub projects based on job requirements + if "github" in profile_data and "repositories" in profile_data["github"]: + job_skills = job_requirements.get("skills", {}) + relevant_projects = github_scraper.select_relevant_projects( + profile_data["github"]["repositories"], job_skills + ) + profile_data["github"]["top_projects"] = relevant_projects + + # Step 4: Generate tailored resume content resume_content = resume_generator.generate(profile_data, job_requirements) # Step 4: Compile to PDF diff --git a/app_production.py b/app_production.py index cbfb654..d2d4e39 100644 --- a/app_production.py +++ b/app_production.py @@ -492,7 +492,27 @@ def generate_resume(): logger.error(f"[{request.request_id}] Job analysis failed: {e}") raise ResumeGenerationError("Failed to analyze job description") - # Step 3: Generate tailored resume content + # Step 3: Refine GitHub projects based on job requirements + if "github" in profile_data and "repositories" in profile_data["github"]: + try: + logger.debug( + f"[{request.request_id}] Refining GitHub projects by relevance" + ) + job_skills = job_requirements.get("skills", {}) + relevant_projects = github_scraper.select_relevant_projects( + profile_data["github"]["repositories"], job_skills + ) + profile_data["github"]["top_projects"] = relevant_projects + logger.debug( + f"[{request.request_id}] Refined to {len(relevant_projects)} relevant projects" + ) + except Exception as e: + logger.warning( + f"[{request.request_id}] GitHub project refinement failed: {e}" + ) + # Continue with default projects if refinement fails + + # Step 4: Generate tailored resume content try: logger.debug(f"[{request.request_id}] Generating resume content") resume_content = resume_generator.generate(profile_data, job_requirements) diff --git a/scrapers/github_scraper.py b/scrapers/github_scraper.py index dcf5d15..acda815 100644 --- a/scrapers/github_scraper.py +++ b/scrapers/github_scraper.py @@ -144,3 +144,63 @@ def scrape_profile(self, username: str) -> Dict: exc_info=True, ) raise GitHubAPIError(f"Failed to scrape GitHub profile: {str(e)}") + + def select_relevant_projects( + self, repos: List[Dict], job_skills: Dict[str, List[str]] + ) -> List[Dict]: + """ + Select projects that best match the job requirements + + Args: + repos: List of repositories + job_skills: Dictionary of required skills by category + + Returns: + List of relevant repositories sorted by relevance + """ + if not repos: + return [] + + # Flatten all job skills into a single set for easy matching + all_required_skills = set() + for skills in job_skills.values(): + if isinstance(skills, list): + all_required_skills.update([s.lower() for s in skills]) + + if not all_required_skills: + # Fallback to stars if no skills identified + return sorted(repos, key=lambda x: x.get("stars", 0), reverse=True)[ + : config.GITHUB_MAX_TOP_PROJECTS + ] + + scored_repos = [] + for repo in repos: + score = 0 + repo_name = repo.get("name", "").lower() + repo_desc = (repo.get("description") or "").lower() + repo_lang = (repo.get("language") or "").lower() + repo_topics = [t.lower() for t in repo.get("topics", [])] + + # Match language (High weight) + if repo_lang in all_required_skills: + score += 5 + + # Match topics (Medium weight) + for topic in repo_topics: + if topic in all_required_skills: + score += 3 + + # Match keywords in name/description (Low weight) + for skill in all_required_skills: + if skill in repo_name or skill in repo_desc: + score += 1 + + scored_repos.append((score, repo)) + + # Sort by score (desc), then stars (desc) as tie-breaker + sorted_repos = sorted( + scored_repos, key=lambda x: (x[0], x[1].get("stars", 0)), reverse=True + ) + + # Return only the repository dictionaries + return [repo for score, repo in sorted_repos[: config.GITHUB_MAX_TOP_PROJECTS]]