candidate ai ranking
This commit is contained in:
parent
2a9121e7d0
commit
c2f68a2be9
@ -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.
BIN
NorahUniversity/__pycache__/celery.cpython-312.pyc
Normal file
BIN
NorahUniversity/__pycache__/celery.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
23
NorahUniversity/celery.py
Normal file
23
NorahUniversity/celery.py
Normal 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()
|
||||
|
||||
@ -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'
|
||||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
||||
# }
|
||||
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
||||
|
||||
@ -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 (1–2 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 JSON—no 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):
|
||||
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -132,3 +132,14 @@ wrapt
|
||||
wurst
|
||||
xlrd
|
||||
XlsxWriter
|
||||
celery[redis]
|
||||
redis
|
||||
sentence-transformers
|
||||
torch
|
||||
pdfplumber
|
||||
python-docx
|
||||
PyMuPDF
|
||||
pytesseract
|
||||
Pillow
|
||||
python-dotenv
|
||||
django-countries
|
||||
BIN
static/media/resumes/jitendra.pdf
Normal file
BIN
static/media/resumes/jitendra.pdf
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user