Files
interviewstreet--hiring-agent/github.py
T
2026-07-13 12:29:44 +08:00

497 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")