candidate ai ranking

This commit is contained in:
Faheed 2025-10-06 15:13:59 +03:00
parent 2a9121e7d0
commit c2f68a2be9
23 changed files with 237 additions and 341 deletions

View File

@ -0,0 +1,10 @@
# to make sure that the celery loads whenever in run my project
#Celery app is loaded and configured as soon as Django starts.
from .celery import app as celery_app
# so that the @shared_task decorator will use this app in all the tasks.py files
__all__ = ('celery_app',)

Binary file not shown.

23
NorahUniversity/celery.py Normal file
View File

@ -0,0 +1,23 @@
import os
from celery import Celery
# to tell the celery program which is seperate from where to find our Django projects settings
os.environ.setdefault('DJANGO_SETTINGS_MODULE','NorahUniversity.settings')
# create a Celery app instance
app=Celery('NorahUniversity')
# load the celery app connfiguration from the projects settings:
app.config_from_object('django.conf:settings',namespace='CELERY')
# Auto discover the tasks from the django apps:
app.autodiscover_tasks()

View File

@ -58,6 +58,7 @@ INSTALLED_APPS = [
'crispy_bootstrap5',
'django_extensions',
'template_partials',
'django_countries'
]
SITE_ID = 1
@ -216,4 +217,9 @@ SECRET_TOKEN = '6KdTGyF0SSCSL_V4Xa34aw'
DATA_UPLOAD_MAX_MEMORY_SIZE = 10485760 # 10MB
FILE_UPLOAD_MAX_MEMORY_SIZE = 10485760 # 10MB
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_CREDENTIALS = True
# Celery + Redis for long running background i will be using it
CELERY_BROKER_URL = 'redis://127.0.0.1:6379/0'
CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/0'

Binary file not shown.

View File

@ -248,77 +248,7 @@ class LinkedInService:
'status_code': getattr(e.response, 'status_code', 500) if hasattr(e, 'response') else 500
}
# def create_job_post_with_image(self, job_posting, image_file, person_urn, asset_urn):
# """Step 3: Create post with uploaded image"""
# url = "https://api.linkedin.com/v2/ugcPosts"
# headers = {
# 'Authorization': f'Bearer {self.access_token}',
# 'Content-Type': 'application/json',
# 'X-Restli-Protocol-Version': '2.0.0'
# }
# # Build the same message as before
# message_parts = [f"🚀 **We're Hiring: {job_posting.title}**"]
# if job_posting.department:
# message_parts.append(f"**Department:** {job_posting.department}")
# if job_posting.description:
# message_parts.append(f"\n{job_posting.description}")
# details = []
# if job_posting.job_type:
# details.append(f"💼 {job_posting.get_job_type_display()}")
# if job_posting.get_location_display() != 'Not specified':
# details.append(f"📍 {job_posting.get_location_display()}")
# if job_posting.workplace_type:
# details.append(f"🏠 {job_posting.get_workplace_type_display()}")
# if job_posting.salary_range:
# details.append(f"💰 {job_posting.salary_range}")
# if details:
# message_parts.append("\n" + " | ".join(details))
# if job_posting.application_url:
# message_parts.append(f"\n🔗 **Apply now:** {job_posting.application_url}")
# hashtags = self.hashtags_list(job_posting.hash_tags)
# if job_posting.department:
# dept_hashtag = f"#{job_posting.department.replace(' ', '')}"
# hashtags.insert(0, dept_hashtag)
# message_parts.append("\n\n" + " ".join(hashtags))
# message = "\n".join(message_parts)
# # Create image post payload
# payload = {
# "author": f"urn:li:person:{person_urn}",
# "lifecycleState": "PUBLISHED",
# "specificContent": {
# "com.linkedin.ugc.ShareContent": {
# "shareCommentary": {"text": message},
# "shareMediaCategory": "IMAGE",
# "media": [{
# "status": "READY",
# "media": asset_urn
# }]
# }
# },
# "visibility": {
# "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
# }
# }
# response = requests.post(url, headers=headers, json=payload, timeout=30)
# response.raise_for_status()
# post_id = response.headers.get('x-restli-id', '')
# post_url = f"https://www.linkedin.com/feed/update/{quote(post_id)}/" if post_id else ""
# return {
# 'success': True,
# 'post_id': post_id,
# 'post_url': post_url,
# 'status_code': response.status_code
# }
def hashtags_list(self,hash_tags_str):
"""Convert comma-separated hashtags string to list"""
@ -331,222 +261,4 @@ class LinkedInService:
return tags
# def create_job_post(self, job_posting):
# """Create a job announcement post on LinkedIn (with image support)"""
# if not self.access_token:
# raise Exception("Not authenticated with LinkedIn")
# try:
# # Get user profile for person URN
# profile = self.get_user_profile()
# person_urn = profile.get('sub')
# if not person_urn:
# raise Exception("Could not retrieve LinkedIn user ID")
# # Check if job has an image
# try:
# image_upload = job_posting.files.first()
# has_image = image_upload and image_upload.linkedinpost_image
# except Exception:
# has_image = False
# if has_image:
# # === POST WITH IMAGE ===
# upload_info = self.register_image_upload(person_urn)
# self.upload_image_to_linkedin(
# upload_info['upload_url'],
# image_upload.linkedinpost_image
# )
# return self.create_job_post_with_image(
# job_posting,
# image_upload.linkedinpost_image,
# person_urn,
# upload_info['asset']
# )
# else:
# # === FALLBACK TO URL/ARTICLE POST ===
# # 🔥 ADD UNIQUE TIMESTAMP TO PREVENT DUPLICATES 🔥
# from django.utils import timezone
# import random
# unique_suffix = f"\n\nPosted: {timezone.now().strftime('%b %d, %Y at %I:%M %p')} (ID: {random.randint(1000, 9999)})"
# message_parts = [f"🚀 **We're Hiring: {job_posting.title}**"]
# if job_posting.department:
# message_parts.append(f"**Department:** {job_posting.department}")
# if job_posting.description:
# message_parts.append(f"\n{job_posting.description}")
# details = []
# if job_posting.job_type:
# details.append(f"💼 {job_posting.get_job_type_display()}")
# if job_posting.get_location_display() != 'Not specified':
# details.append(f"📍 {job_posting.get_location_display()}")
# if job_posting.workplace_type:
# details.append(f"🏠 {job_posting.get_workplace_type_display()}")
# if job_posting.salary_range:
# details.append(f"💰 {job_posting.salary_range}")
# if details:
# message_parts.append("\n" + " | ".join(details))
# if job_posting.application_url:
# message_parts.append(f"\n🔗 **Apply now:** {job_posting.application_url}")
# hashtags = self.hashtags_list(job_posting.hash_tags)
# if job_posting.department:
# dept_hashtag = f"#{job_posting.department.replace(' ', '')}"
# hashtags.insert(0, dept_hashtag)
# message_parts.append("\n\n" + " ".join(hashtags))
# message_parts.append(unique_suffix) # 🔥 Add unique suffix
# message = "\n".join(message_parts)
# # 🔥 FIX URL - REMOVE TRAILING SPACES 🔥
# url = "https://api.linkedin.com/v2/ugcPosts"
# headers = {
# 'Authorization': f'Bearer {self.access_token}',
# 'Content-Type': 'application/json',
# 'X-Restli-Protocol-Version': '2.0.0'
# }
# payload = {
# "author": f"urn:li:person:{person_urn}",
# "lifecycleState": "PUBLISHED",
# "specificContent": {
# "com.linkedin.ugc.ShareContent": {
# "shareCommentary": {"text": message},
# "shareMediaCategory": "ARTICLE",
# "media": [{
# "status": "READY",
# "description": {"text": f"Apply for {job_posting.title} at our university!"},
# "originalUrl": job_posting.application_url,
# "title": {"text": job_posting.title}
# }]
# }
# },
# "visibility": {
# "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
# }
# }
# response = requests.post(url, headers=headers, json=payload, timeout=60)
# response.raise_for_status()
# post_id = response.headers.get('x-restli-id', '')
# # 🔥 FIX POST URL - REMOVE TRAILING SPACES 🔥
# post_url = f"https://www.linkedin.com/feed/update/{quote(post_id)}/" if post_id else ""
# return {
# 'success': True,
# 'post_id': post_id,
# 'post_url': post_url,
# 'status_code': response.status_code
# }
# except Exception as e:
# logger.error(f"Error creating LinkedIn post: {e}")
# return {
# 'success': False,
# 'error': str(e),
# 'status_code': getattr(e.response, 'status_code', 500) if hasattr(e, 'response') else 500
# }
# def create_job_post(self, job_posting):
# """Create a job announcement post on LinkedIn"""
# if not self.access_token:
# raise Exception("Not authenticated with LinkedIn")
# try:
# # Get user profile for person URN
# profile = self.get_user_profile()
# person_urn = profile.get('sub')
# if not person_urn: # uniform resource name used to uniquely identify linked-id for internal systems and apis
# raise Exception("Could not retrieve LinkedIn user ID")
# # Build professional job post message
# message_parts = [f"🚀 **We're Hiring: {job_posting.title}**"]
# if job_posting.department:
# message_parts.append(f"**Department:** {job_posting.department}")
# if job_posting.description:
# message_parts.append(f"\n{job_posting.description}")
# # Add job details
# details = []
# if job_posting.job_type:
# details.append(f"💼 {job_posting.get_job_type_display()}")
# if job_posting.get_location_display() != 'Not specified':
# details.append(f"📍 {job_posting.get_location_display()}")
# if job_posting.workplace_type:
# details.append(f"🏠 {job_posting.get_workplace_type_display()}")
# if job_posting.salary_range:
# details.append(f"💰 {job_posting.salary_range}")
# if details:
# message_parts.append("\n" + " | ".join(details))
# # Add application link
# if job_posting.application_url:
# message_parts.append(f"\n🔗 **Apply now:** {job_posting.application_url}")
# # Add hashtags
# hashtags = ["#HigherEd", "#Hiring", "#FacultyJobs", "#UniversityJobs"]
# if job_posting.department:
# dept_hashtag = f"#{job_posting.department.replace(' ', '')}"
# hashtags.insert(0, dept_hashtag)
# message_parts.append("\n\n" + " ".join(hashtags))
# message = "\n".join(message_parts)
# # Create LinkedIn post
# url = "https://api.linkedin.com/v2/ugcPosts"
# headers = {
# 'Authorization': f'Bearer {self.access_token}',
# 'Content-Type': 'application/json',
# 'X-Restli-Protocol-Version': '2.0.0'
# }
# payload = {
# "author": f"urn:li:person:{person_urn}",
# "lifecycleState": "PUBLISHED",
# "specificContent": {
# "com.linkedin.ugc.ShareContent": {
# "shareCommentary": {"text": message},
# "shareMediaCategory": "ARTICLE",
# "media": [{
# "status": "READY",
# "description": {"text": f"Apply for {job_posting.title} at our university!"},
# "originalUrl": job_posting.application_url,
# "title": {"text": job_posting.title}
# }]
# }
# },
# "visibility": {
# "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
# }
# }
# response = requests.post(url, headers=headers, json=payload, timeout=60)
# response.raise_for_status()
# # Extract post ID from response
# post_id = response.headers.get('x-restli-id', '')
# post_url = f"https://www.linkedin.com/feed/update/{quote(post_id)}/" if post_id else ""
# return {
# 'success': True,
# 'post_id': post_id,
# 'post_url': post_url,
# 'status_code': response.status_code
# }
# except Exception as e:
# logger.error(f"Error creating LinkedIn post: {e}")
# return {
# 'success': False,
# 'error': str(e),
# 'status_code': getattr(e.response, 'status_code', 500) if hasattr(e, 'response') else 500
# }

View File

@ -0,0 +1,58 @@
# Generated by Django 5.2.7 on 2025-10-06 10:48
import django_countries.fields
import django_extensions.db.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0015_hiringagency_candidate_submitted_by_agency_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='source',
options={'ordering': ['name'], 'verbose_name': 'Source', 'verbose_name_plural': 'Sources'},
),
migrations.AddField(
model_name='hiringagency',
name='address',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='hiringagency',
name='country',
field=django_countries.fields.CountryField(blank=True, max_length=2, null=True),
),
migrations.AddField(
model_name='hiringagency',
name='slug',
field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'),
),
migrations.AlterField(
model_name='hiringagency',
name='created_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'),
),
migrations.AlterField(
model_name='hiringagency',
name='updated_at',
field=models.DateTimeField(auto_now=True, verbose_name='Updated at'),
),
migrations.RemoveField(
model_name='jobposting',
name='hiring_agency',
),
migrations.AlterField(
model_name='source',
name='name',
field=models.CharField(help_text='e.g., ATS, ERP ', max_length=100, unique=True, verbose_name='Source Name'),
),
migrations.AddField(
model_name='jobposting',
name='hiring_agency',
field=models.ManyToManyField(blank=True, help_text='External agency responsible for sourcing candidates for this role', null=True, related_name='jobs', to='recruitment.hiringagency', verbose_name='Hiring Agency'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-10-06 10:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0016_alter_source_options_hiringagency_address_and_more'),
]
operations = [
migrations.AlterField(
model_name='jobposting',
name='hiring_agency',
field=models.ManyToManyField(help_text='External agency responsible for sourcing candidates for this role', related_name='jobs', to='recruitment.hiringagency', verbose_name='Hiring Agency'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-10-06 11:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0017_alter_jobposting_hiring_agency'),
]
operations = [
migrations.AlterField(
model_name='jobposting',
name='hiring_agency',
field=models.ManyToManyField(blank=True, help_text='External agency responsible for sourcing candidates for this role', related_name='jobs', to='recruitment.hiringagency', verbose_name='Hiring Agency'),
),
]

View File

@ -6,6 +6,7 @@ from django.core.validators import URLValidator
from django.utils.translation import gettext_lazy as _
from django_extensions.db.fields import RandomCharField
from django.core.exceptions import ValidationError
from django_countries.fields import CountryField
class Base(models.Model):
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created at'))
@ -112,10 +113,8 @@ class JobPosting(Base):
help_text="The system or channel from which this job posting originated or was first published."
)
hiring_agency = models.ForeignKey(
hiring_agency = models.ManyToManyField(
'HiringAgency',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='jobs',
verbose_name=_('Hiring Agency'),
@ -355,37 +354,31 @@ class UploadedFile(models.Model):
class Source(models.Model):
class SourceType(models.TextChoices):
ATS = 'ATS', _('Applicant Tracking System')
CRM = 'ERP', _('ERP system')
name = models.CharField(
max_length=100,
choices=SourceType.choices,
verbose_name=_('Source Type')
unique=True,
verbose_name=_('Source Name'),
help_text=_("e.g., ATS, ERP ")
)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.get_name_display()}"
return self.name
class Meta:
verbose_name = _('Source')
verbose_name_plural = _('Sources')
ordering = ['name']
class HiringAgency(models.Model):
class HiringAgency(Base):
name = models.CharField(max_length=200, unique=True, verbose_name=_('Agency Name'))
contact_person = models.CharField(max_length=150, blank=True, verbose_name=_('Contact Person'))
email = models.EmailField(blank=True)
phone = models.CharField(max_length=20, blank=True)
website = models.URLField(blank=True)
notes = models.TextField(blank=True, help_text=_("Internal notes about the agency"))
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
country=CountryField(blank=True, null=True,blank_label=_('Select country'))
address=models.TextField(blank=True,null=True)
def __str__(self):
return self.name

View File

@ -17,6 +17,7 @@ import logging
logger = logging.getLogger(__name__)
import os
from .utils import extract_text_from_pdf,score_resume_with_openrouter
import asyncio
@ -43,21 +44,84 @@ def score_candidate_resume(sender, instance, created, **kwargs):
# instance.scoring_error = "Could not extract text from resume."
# instance.save(update_fields=['scoring_error'])
# return
job_detail=str(instance.job.description)+str(instance.job.qualifications)
prompt1 = f"""
You are an expert resume parser and summarizer. Given a resume in plain text format, extract and organize the following key-value information into a clean, valid JSON object:
result = score_resume_with_openrouter(resume_text)
full_name: Full name of the candidate
current_title: Most recent or current job title
location: City and state (or country if outside the U.S.)
contact: Phone number and email (as a single string or separate fields)
linkedin: LinkedIn profile URL (if present)
github: GitHub or portfolio URL (if present)
summary: Brief professional profile or summary (12 sentences)
education: List of degrees, each with:
institution
degree
year
gpa (if provided)
relevant_courses (as a list, if mentioned)
skills: Grouped by category if possible (e.g., programming, big data, visualization), otherwise as a flat list of technologies/tools
experience: List of roles, each with:
company
job_title
location
start_date and end_date (or "Present" if applicable)
key_achievements (as a list of concise bullet points)
projects: List of notable projects (if clearly labeled), each with:
name
year
technologies_used
brief_description
Instructions:
Be concise but preserve key details.
Normalize formatting (e.g., Jun. 2014 2014-06).
Omit redundant or promotional language.
If a section is missing, omit the key or set it to null/empty list as appropriate.
Output only valid JSONno markdown, no extra text.
Now, process the following resume text:
{resume_text}
"""
result = score_resume_with_openrouter(prompt1)
prompt = f"""
You are an expert technical recruiter. Your task is to score the following candidate for the role of a Senior Data Analyst based on the provided job criteria.
**Job Criteria:**
{job_detail}
**Candidate's Extracted Resume Json:**
\"\"\"
{result}
\"\"\"
**Your Task:**
Provide a response in strict JSON format with the following keys:
1. 'match_score': A score from 0 to 100 representing how well the candidate fits the role.
2. 'strengths': A brief summary of why the candidate is a strong fit, referencing specific criteria.
3. 'weaknesses': A brief summary of where the candidate falls short or what criteria are missing.
4. 'criteria_checklist': An object where you rate the candidate's match for each specific criterion (e.g., {{'Python': 'Met', 'AWS': 'Not Mentioned'}}).
Only output valid JSON. Do not include any other text.
"""
result1 = score_resume_with_openrouter(prompt)
instance.parsed_summary = str(result)
# Update candidate with scoring results
instance.match_score = result.get('match_score')
instance.strengths = result.get('strengths', '')
instance.weaknesses = result.get('weaknesses', '')
instance.criteria_checklist = result.get('criteria_checklist', {})
instance.match_score = result1.get('match_score')
instance.strengths = result1.get('strengths', '')
instance.weaknesses = result1.get('weaknesses', '')
instance.criteria_checklist = result1.get('criteria_checklist', {})
# Save only scoring-related fields to avoid recursion
instance.save(update_fields=[
'match_score', 'strengths', 'weaknesses',
'criteria_checklist'
'criteria_checklist','parsed_summary'
])
logger.info(f"Successfully scored resume for candidate {instance.id}")
@ -68,4 +132,9 @@ def score_candidate_resume(sender, instance, created, **kwargs):
# instance.save(update_fields=['scoring_error'])
logger.error(f"Failed to score resume for candidate {instance.id}: {e}")
# @receiver(post_save,sender=models.Candidate)
# def trigger_scoring(sender,intance,created,**kwargs):

View File

@ -34,7 +34,7 @@ import json
import logging
logger = logging.getLogger(__name__)
OPENROUTER_API_KEY ='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'
OPENROUTER_API_KEY ='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'
OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct:free'
if not OPENROUTER_API_KEY:
@ -53,29 +53,7 @@ def extract_text_from_pdf(file_path):
raise
return text.strip()
def score_resume_with_openrouter(resume_text):
prompt = f"""
You are an expert technical recruiter. Your task is to score the following candidate for the role of a Senior Data Analyst based on the provided job criteria.
**Job Criteria:**
- Must-Have Skills: Python, SQL, 5+ years of experience.
- Nice-to-Have Skills: Tableau, AWS.
- Experience: Must have led at least one project.
**Candidate's Extracted Resume Text:**
\"\"\"
{resume_text}
\"\"\"
**Your Task:**
Provide a response in strict JSON format with the following keys:
1. 'match_score': A score from 0 to 100 representing how well the candidate fits the role.
2. 'strengths': A brief summary of why the candidate is a strong fit, referencing specific criteria.
3. 'weaknesses': A brief summary of where the candidate falls short or what criteria are missing.
4. 'criteria_checklist': An object where you rate the candidate's match for each specific criterion (e.g., {{'Python': 'Met', 'AWS': 'Not Mentioned'}}).
Only output valid JSON. Do not include any other text.
"""
def score_resume_with_openrouter(prompt):
print("model call")
response = requests.post(
url="https://openrouter.ai/api/v1/chat/completions",
@ -97,11 +75,11 @@ Only output valid JSON. Do not include any other text.
res = response.json()
content = res["choices"][0]['message']['content']
try:
print(content)
content = content.replace("```json","").replace("```","")
print(content)
res = json.loads(content)
print(res)
except Exception as e:
print(e)

View File

@ -132,3 +132,14 @@ wrapt
wurst
xlrd
XlsxWriter
celery[redis]
redis
sentence-transformers
torch
pdfplumber
python-docx
PyMuPDF
pytesseract
Pillow
python-dotenv
django-countries

Binary file not shown.