diff --git a/NorahUniversity/__init__.py b/NorahUniversity/__init__.py index e69de29..a02b1a7 100644 --- a/NorahUniversity/__init__.py +++ b/NorahUniversity/__init__.py @@ -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',) + + diff --git a/NorahUniversity/__pycache__/__init__.cpython-312.pyc b/NorahUniversity/__pycache__/__init__.cpython-312.pyc index df1bddc..2c0dca1 100644 Binary files a/NorahUniversity/__pycache__/__init__.cpython-312.pyc and b/NorahUniversity/__pycache__/__init__.cpython-312.pyc differ diff --git a/NorahUniversity/__pycache__/celery.cpython-312.pyc b/NorahUniversity/__pycache__/celery.cpython-312.pyc new file mode 100644 index 0000000..82f0ca8 Binary files /dev/null and b/NorahUniversity/__pycache__/celery.cpython-312.pyc differ diff --git a/NorahUniversity/__pycache__/settings.cpython-312.pyc b/NorahUniversity/__pycache__/settings.cpython-312.pyc index 0df818e..c5c0a1a 100644 Binary files a/NorahUniversity/__pycache__/settings.cpython-312.pyc and b/NorahUniversity/__pycache__/settings.cpython-312.pyc differ diff --git a/NorahUniversity/celery.py b/NorahUniversity/celery.py new file mode 100644 index 0000000..ffefeb6 --- /dev/null +++ b/NorahUniversity/celery.py @@ -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() + diff --git a/NorahUniversity/settings.py b/NorahUniversity/settings.py index 479cfc4..987d498 100644 --- a/NorahUniversity/settings.py +++ b/NorahUniversity/settings.py @@ -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 \ No newline at end of file +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' \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 index d2c0aab..c37b46d 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/recruitment/__pycache__/linkedin_service.cpython-312.pyc b/recruitment/__pycache__/linkedin_service.cpython-312.pyc index ce66250..8350c01 100644 Binary files a/recruitment/__pycache__/linkedin_service.cpython-312.pyc and b/recruitment/__pycache__/linkedin_service.cpython-312.pyc differ diff --git a/recruitment/__pycache__/models.cpython-312.pyc b/recruitment/__pycache__/models.cpython-312.pyc index 5e2cde2..56ce80a 100644 Binary files a/recruitment/__pycache__/models.cpython-312.pyc and b/recruitment/__pycache__/models.cpython-312.pyc differ diff --git a/recruitment/__pycache__/signals.cpython-312.pyc b/recruitment/__pycache__/signals.cpython-312.pyc index 3062301..c8f2950 100644 Binary files a/recruitment/__pycache__/signals.cpython-312.pyc and b/recruitment/__pycache__/signals.cpython-312.pyc differ diff --git a/recruitment/__pycache__/utils.cpython-312.pyc b/recruitment/__pycache__/utils.cpython-312.pyc index 52712b1..fc89d14 100644 Binary files a/recruitment/__pycache__/utils.cpython-312.pyc and b/recruitment/__pycache__/utils.cpython-312.pyc differ diff --git a/recruitment/linkedin_service.py b/recruitment/linkedin_service.py index 49877a1..d1fb4f8 100644 --- a/recruitment/linkedin_service.py +++ b/recruitment/linkedin_service.py @@ -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 - # } \ No newline at end of file + \ No newline at end of file diff --git a/recruitment/migrations/0016_alter_source_options_hiringagency_address_and_more.py b/recruitment/migrations/0016_alter_source_options_hiringagency_address_and_more.py new file mode 100644 index 0000000..7354ad0 --- /dev/null +++ b/recruitment/migrations/0016_alter_source_options_hiringagency_address_and_more.py @@ -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'), + ), + ] diff --git a/recruitment/migrations/0017_alter_jobposting_hiring_agency.py b/recruitment/migrations/0017_alter_jobposting_hiring_agency.py new file mode 100644 index 0000000..da4e051 --- /dev/null +++ b/recruitment/migrations/0017_alter_jobposting_hiring_agency.py @@ -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'), + ), + ] diff --git a/recruitment/migrations/0018_alter_jobposting_hiring_agency.py b/recruitment/migrations/0018_alter_jobposting_hiring_agency.py new file mode 100644 index 0000000..07c349f --- /dev/null +++ b/recruitment/migrations/0018_alter_jobposting_hiring_agency.py @@ -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'), + ), + ] diff --git a/recruitment/migrations/__pycache__/0016_alter_source_options_hiringagency_address_and_more.cpython-312.pyc b/recruitment/migrations/__pycache__/0016_alter_source_options_hiringagency_address_and_more.cpython-312.pyc new file mode 100644 index 0000000..a5f4837 Binary files /dev/null and b/recruitment/migrations/__pycache__/0016_alter_source_options_hiringagency_address_and_more.cpython-312.pyc differ diff --git a/recruitment/migrations/__pycache__/0017_alter_jobposting_hiring_agency.cpython-312.pyc b/recruitment/migrations/__pycache__/0017_alter_jobposting_hiring_agency.cpython-312.pyc new file mode 100644 index 0000000..250d210 Binary files /dev/null and b/recruitment/migrations/__pycache__/0017_alter_jobposting_hiring_agency.cpython-312.pyc differ diff --git a/recruitment/migrations/__pycache__/0018_alter_jobposting_hiring_agency.cpython-312.pyc b/recruitment/migrations/__pycache__/0018_alter_jobposting_hiring_agency.cpython-312.pyc new file mode 100644 index 0000000..a339fa6 Binary files /dev/null and b/recruitment/migrations/__pycache__/0018_alter_jobposting_hiring_agency.cpython-312.pyc differ diff --git a/recruitment/models.py b/recruitment/models.py index a7aab57..250680c 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -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 diff --git a/recruitment/signals.py b/recruitment/signals.py index 5984f2b..073a12c 100644 --- a/recruitment/signals.py +++ b/recruitment/signals.py @@ -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): + + \ No newline at end of file diff --git a/recruitment/utils.py b/recruitment/utils.py index 988880e..3524a1a 100644 --- a/recruitment/utils.py +++ b/recruitment/utils.py @@ -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) diff --git a/requirements.txt b/requirements.txt index 38e0665..98ed8b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -132,3 +132,14 @@ wrapt wurst xlrd XlsxWriter +celery[redis] +redis +sentence-transformers +torch +pdfplumber +python-docx +PyMuPDF +pytesseract +Pillow +python-dotenv +django-countries \ No newline at end of file diff --git a/static/media/resumes/jitendra.pdf b/static/media/resumes/jitendra.pdf new file mode 100644 index 0000000..fae2754 Binary files /dev/null and b/static/media/resumes/jitendra.pdf differ