first commit

This commit is contained in:
Anurag Jangid 2026-10-08 02:50:35 +05:30
commit 7a72438fd1
74 changed files with 7548 additions and 0 deletions

Binary file not shown.

View file

@ -0,0 +1,51 @@
import logging
from fastapi_mail import FastMail, MessageSchema, ConnectionConfig, MessageType
from pydantic import EmailStr
from app.config import get_settings
settings = get_settings()
conf = ConnectionConfig(
MAIL_USERNAME=settings.mail_username,
MAIL_PASSWORD=settings.mail_password,
MAIL_FROM=settings.mail_from,
MAIL_PORT=settings.mail_port,
MAIL_SERVER=settings.mail_server,
MAIL_STARTTLS=settings.mail_tls,
MAIL_SSL_TLS=settings.mail_ssl,
USE_CREDENTIALS=True,
VALIDATE_CERTS=True
)
async def send_interview_email(candidate_email: EmailStr, candidate_name: str, date: str, time: str, meeting_link: str, notes: str):
# Only try to send if credentials are provided
if not settings.mail_username or not settings.mail_password:
logging.warning("Email credentials not configured. Skipping interview email sending.")
return False
html = f"""
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; color: #333;">
<h2 style="color: #2563eb;">Interview Scheduled</h2>
<p>Hi {candidate_name},</p>
<p>Your interview has been scheduled for <strong>{date}</strong> at <strong>{time}</strong>.</p>
<p><strong>Meeting Link:</strong> <a href="{meeting_link}">{meeting_link}</a></p>
{f'<p><strong>Notes:</strong> {notes}</p>' if notes else ''}
<br>
<p>Best regards,<br>HireAI Team</p>
</div>
"""
message = MessageSchema(
subject="Interview Invitation - HireAI",
recipients=[candidate_email],
body=html,
subtype=MessageType.html
)
fm = FastMail(conf)
try:
await fm.send_message(message)
return True
except Exception as e:
logging.error(f"Failed to send email: {e}")
return False

View file

@ -0,0 +1,104 @@
import logging
import json
from google import genai
from google.genai import types
from app.config import get_settings
from app.models.question import QuestionItem
from app.models.pronunciation import PronunciationItem
settings = get_settings()
client = None
if settings.gemini_api_key:
client = genai.Client(api_key=settings.gemini_api_key)
async def generate_interview_questions(resume_text: str, role_name: str, skills: list[str], experience: int, projects: list[dict] = None) -> list[dict]:
if not client:
logging.error("Gemini API key not configured.")
raise ValueError("Gemini API key not configured")
# Format projects for the prompt
projects_section = ""
if projects:
projects_section = "\n Candidate's Projects (PRIORITIZE these in your questions):\n"
for p in projects[:5]:
tech_str = ", ".join(p.get("tech", []))
projects_section += f" - {p['name']}: {p.get('description', '')[:300]} (Tech: {tech_str})\n"
else:
projects_section = " No projects listed."
prompt = f"""
You are an expert technical interviewer.
Job Requirement:
- Role: {role_name}
- Required Experience: {experience} years
- Target Skills: {', '.join(skills)}
{projects_section}
Full Candidate Resume Text:
{resume_text}
Generate exactly 5 interview questions following this distribution:
- Questions 1, 2, 3: MUST be directly about the candidate's specific PROJECTS listed above.
Ask about technical decisions made, challenges faced, architecture, or improvements they would make.
- Questions 4, 5: Should cover general skills from the job requirement.
For each question, provide:
1. question: The question itself.
2. expected_answer: Key points an ideal answer should cover.
3. hint: A short hint if the candidate gets stuck.
4. difficulty: Easy, Medium, or Hard.
5. category: Technical, Behavioral, or System Design.
"""
try:
response = client.models.generate_content(
model='gemini-2.5-flash',
contents=prompt,
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=list[QuestionItem],
temperature=0.7,
),
)
questions = json.loads(response.text)
return questions
except Exception as e:
logging.error(f"Failed to generate questions: {e}")
raise ValueError(f"Failed to generate questions: {e}")
async def get_word_pronunciation(word: str) -> dict:
if not client:
logging.error("Gemini API key not configured.")
raise ValueError("Gemini API key not configured")
prompt = f"""
You are an expert linguist and technical dictionary.
Analyze the technical term: "{word}"
Provide the following structured information:
1. word: The correctly spelled word.
2. phonetic: The exact IPA (International Phonetic Alphabet) pronunciation (e.g., /ˌpɒlɪˈmɔːfɪzəm/).
3. syllables: An array of strings representing the syllable breakdown (e.g., ["pol", "y", "mor", "phism"]).
4. definition: A concise definition of the word, specifically in a computer science or tech context (max 2 sentences).
"""
try:
response = client.models.generate_content(
model='gemini-2.5-flash',
contents=prompt,
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=PronunciationItem,
temperature=0.1,
),
)
# Parse the JSON response
data = json.loads(response.text)
return data
except Exception as e:
logging.error(f"Failed to get pronunciation: {e}")
raise ValueError(f"Failed to get pronunciation: {e}")

View file

@ -0,0 +1,267 @@
"""
Resume parsing service — enhanced extraction.
Supports PDF (via PyMuPDF) and DOCX (via python-docx).
Extracts: name, email, phone, socials, objective, skills,
projects, education, experience, languages, interests.
"""
import re
import io
from typing import Optional
# ── Section header patterns ───────────────────────────────────
SECTION_PATTERNS = {
"objective": r"objective|summary|profile|about me|career goal",
"skills": r"skills|technical skills|competencies|technologies",
"experience": r"experience|work history|employment|professional experience|internship",
"education": r"education|academic|qualification|academics",
"projects": r"projects|portfolio|personal projects|academic projects",
"languages": r"languages|spoken languages",
"interests": r"interests|hobbies|activities|extra.?curricular",
"certifications": r"certifications?|achievements?|awards?",
}
# ── Programming / spoken language keywords ────────────────────
SKILL_KEYWORDS = {
"python", "javascript", "typescript", "java", "c++", "c#", "c", "go", "golang",
"rust", "swift", "kotlin", "scala", "ruby", "php", "r", "matlab", "bash",
"html", "css", "react", "vue", "angular", "next.js", "nuxt", "svelte",
"node.js", "express", "fastapi", "django", "flask", "spring", "laravel",
"machine learning", "deep learning", "tensorflow", "pytorch", "keras",
"scikit-learn", "pandas", "numpy", "matplotlib", "nlp", "computer vision",
"aws", "gcp", "azure", "docker", "kubernetes", "terraform", "jenkins",
"github actions", "ci/cd", "linux",
"mongodb", "postgresql", "mysql", "redis", "elasticsearch", "sqlite",
"dynamodb", "cassandra", "neo4j",
"git", "graphql", "rest api", "microservices", "kafka", "rabbitmq",
"websocket", "grpc", "firebase", "supabase", "prisma", "tailwind",
}
SPOKEN_LANGUAGE_KEYWORDS = {
"english", "hindi", "spanish", "french", "german", "chinese", "mandarin",
"japanese", "korean", "arabic", "portuguese", "italian", "russian",
"telugu", "tamil", "kannada", "bengali", "marathi", "gujarati", "malayalam",
"urdu", "punjabi",
}
# ── Low-level extractors ───────────────────────────────────────
def extract_email(text: str) -> Optional[str]:
m = re.search(r"[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}", text)
return m.group(0) if m else None
def extract_phone(text: str) -> Optional[str]:
m = re.search(r"(\+?[\d\s\-().]{10,15})", text)
return m.group(0).strip() if m else None
def extract_linkedin(text: str) -> Optional[str]:
m = re.search(r"linkedin\.com/in/[\w\-]+", text, re.IGNORECASE)
if m:
return "https://" + m.group(0)
return None
def extract_github(text: str) -> Optional[str]:
m = re.search(r"github\.com/[\w\-]+", text, re.IGNORECASE)
if m:
return "https://" + m.group(0)
return None
def extract_website(text: str) -> Optional[str]:
# Match personal URLs that are NOT linkedin/github
m = re.search(
r"https?://(?!.*(?:linkedin|github))[a-zA-Z0-9.\-/~]+",
text,
re.IGNORECASE,
)
return m.group(0) if m else None
def extract_name(text: str) -> Optional[str]:
lines = [l.strip() for l in text.split("\n") if l.strip()]
name_pattern = re.compile(r"^[A-Z][a-z]+(?:\s[A-Z][a-z]+){1,3}$")
for line in lines[:15]:
if name_pattern.match(line):
return line
return None
def extract_skills(text: str) -> list[str]:
text_lower = text.lower()
found = []
for skill in SKILL_KEYWORDS:
if re.search(r"\b" + re.escape(skill) + r"\b", text_lower):
found.append(skill.title() if len(skill) > 3 else skill.upper())
return sorted(set(found))
def extract_languages(section_text: str, full_text: str) -> list[str]:
"""Extract spoken/natural languages (not programming languages)."""
search_text = (section_text or full_text).lower()
found = []
for lang in SPOKEN_LANGUAGE_KEYWORDS:
if re.search(r"\b" + lang + r"\b", search_text):
found.append(lang.capitalize())
return sorted(set(found))
def extract_interests(section_text: str) -> list[str]:
if not section_text:
return []
items = re.split(r"[,\n•\-|/]", section_text)
interests = [i.strip().strip("•-– ") for i in items if i.strip() and len(i.strip()) > 2]
# Deduplicate & cap
seen = set()
result = []
for item in interests:
if item.lower() not in seen and len(item) < 60:
seen.add(item.lower())
result.append(item.title())
return result[:15]
def extract_projects(section_text: str) -> list[dict]:
"""Parse projects section into list of {name, description, tech}."""
if not section_text:
return []
projects = []
# Split by common project delimiters: blank line, bullet, or ALL-CAPS title line
blocks = re.split(r"\n{2,}|(?=\n[•\-★▸])", section_text)
for block in blocks:
block = block.strip()
if not block:
continue
lines = [l.strip() for l in block.split("\n") if l.strip()]
if not lines:
continue
name = lines[0].lstrip("•-★▸ ").strip()
description = " ".join(lines[1:]).strip() if len(lines) > 1 else ""
# Extract tech from description
tech_found = []
desc_lower = description.lower()
for skill in SKILL_KEYWORDS:
if re.search(r"\b" + re.escape(skill) + r"\b", desc_lower):
tech_found.append(skill.title() if len(skill) > 3 else skill.upper())
if name and len(name) < 120:
projects.append({
"name": name,
"description": description[:600] if description else "",
"tech": sorted(set(tech_found)),
})
return projects[:10]
def extract_education(section_text: str) -> list[dict]:
if not section_text:
return []
institutions = []
lines = [l.strip() for l in section_text.split("\n") if l.strip()]
i = 0
while i < len(lines):
line = lines[i]
# Detect degree lines
degree_pattern = re.search(
r"(b\.?tech|m\.?tech|b\.?e|m\.?e|bsc|msc|b\.?sc|m\.?sc|bachelor|master|phd|doctorate|diploma|high school|hsc|ssc|10th|12th)",
line, re.IGNORECASE
)
if degree_pattern:
entry = {
"degree": line,
"institution": lines[i + 1] if i + 1 < len(lines) else "",
"year": "",
}
# Look for year
for j in range(i, min(i + 4, len(lines))):
year_m = re.search(r"\b(19|20)\d{2}\b", lines[j])
if year_m:
entry["year"] = year_m.group(0)
break
institutions.append(entry)
i += 2
else:
i += 1
return institutions[:6]
# ── Section splitter ───────────────────────────────────────────
def split_sections(text: str) -> dict[str, str]:
sections: dict[str, list[str]] = {"header": []}
current = "header"
lines = text.split("\n")
for line in lines:
stripped = line.strip()
matched_section = None
for sec_key, pattern in SECTION_PATTERNS.items():
if re.match(rf"^({pattern})s?[\s:]*$", stripped, re.IGNORECASE):
matched_section = sec_key
break
if matched_section:
current = matched_section
sections.setdefault(current, [])
else:
sections.setdefault(current, []).append(stripped)
return {k: "\n".join(v) for k, v in sections.items()}
# ── Main parse function ────────────────────────────────────────
def parse_text(raw_text: str) -> dict:
sections = split_sections(raw_text)
objective_text = sections.get("objective", "").strip()
return {
"name": extract_name(raw_text),
"email": extract_email(raw_text),
"phone": extract_phone(raw_text),
"linkedin": extract_linkedin(raw_text),
"github": extract_github(raw_text),
"website": extract_website(raw_text),
"objective": objective_text[:600] if objective_text else None,
"skills": extract_skills(raw_text),
"languages": extract_languages(sections.get("languages", ""), raw_text),
"interests": extract_interests(sections.get("interests", "")),
"projects": extract_projects(sections.get("projects", "")),
"education": extract_education(sections.get("education", "")),
"experience": [], # Structured experience parsing can be added as needed
"raw_text": raw_text[:8000], # increased cap for better AI context
}
def parse_pdf(file_bytes: bytes) -> dict:
try:
import fitz # PyMuPDF
doc = fitz.open(stream=file_bytes, filetype="pdf")
text = "\n".join(page.get_text() for page in doc)
doc.close()
return parse_text(text)
except Exception as e:
return {"error": str(e), "raw_text": "", "skills": [], "projects": []}
def parse_docx(file_bytes: bytes) -> dict:
try:
from docx import Document
doc = Document(io.BytesIO(file_bytes))
text = "\n".join(p.text for p in doc.paragraphs)
return parse_text(text)
except Exception as e:
return {"error": str(e), "raw_text": "", "skills": [], "projects": []}