diff --git a/app/core/config.py b/app/core/config.py index 62533cd..9f97c18 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -9,8 +9,8 @@ class Settings(BaseSettings): ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 ALGORITHM: str = "HS256" - # OpenAI Configuration - OPENAI_API_KEY: str = os.getenv("OPENAI_API_KEY", "") + # Cohere Configuration + COHERE_API_KEY: str = os.getenv("COHERE_API_KEY", "") # CORS settings CORS_ORIGINS: list = ["*"] diff --git a/app/services/ai_service.py b/app/services/ai_service.py index 655072c..f8be204 100644 --- a/app/services/ai_service.py +++ b/app/services/ai_service.py @@ -1,6 +1,6 @@ import asyncio import hashlib -from openai import AsyncOpenAI +import cohere from typing import Dict, List, Any from app.core.config import settings from app.core.cache import ai_cache, cache_response @@ -9,7 +9,7 @@ import json class AIService: def __init__(self): - self.client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY) + self.client = cohere.AsyncClient(api_key=settings.COHERE_API_KEY) self._semaphore = asyncio.Semaphore(5) # Limit concurrent AI calls def _create_cache_key(self, text: str, operation: str) -> str: @@ -18,7 +18,7 @@ class AIService: return f"{operation}:{text_hash}" async def analyze_resume(self, resume_text: str) -> Dict[str, Any]: - """Extract structured data from resume text using AI with caching""" + """Extract structured data from resume text using Cohere AI with caching""" # Check cache first cache_key = self._create_cache_key(resume_text, "analyze_resume") cached_result = ai_cache.get(cache_key) @@ -27,54 +27,63 @@ class AIService: # Rate limiting with semaphore async with self._semaphore: - prompt = f""" - Analyze the following resume text and extract structured information: - - {resume_text[:4000]} # Limit text length for faster processing - - Please return a JSON object with the following structure: - {{ - "skills": ["skill1", "skill2", ...], - "experience_years": number, - "education_level": "string", - "work_experience": [ - {{ - "company": "string", - "position": "string", - "duration": "string", - "description": "string" - }} - ], - "education": [ - {{ - "institution": "string", - "degree": "string", - "field": "string", - "year": "string" - }} - ], - "contact_info": {{ - "email": "string", - "phone": "string", - "location": "string" - }} - }} - """ + prompt = f"""Analyze this resume and extract structured information. Return only valid JSON. + +Resume text: +{resume_text[:4000]} + +Extract the following information in JSON format: +{{ + "skills": ["skill1", "skill2", ...], + "experience_years": number, + "education_level": "Bachelor's/Master's/PhD/High School/etc", + "work_experience": [ + {{ + "company": "company name", + "position": "job title", + "duration": "time period", + "description": "brief description" + }} + ], + "education": [ + {{ + "institution": "school name", + "degree": "degree type", + "field": "field of study", + "year": "graduation year" + }} + ], + "contact_info": {{ + "email": "email address", + "phone": "phone number", + "location": "location" + }} +}} + +JSON:""" try: - response = await self.client.chat.completions.create( - model="gpt-3.5-turbo", - messages=[ - {"role": "system", "content": "You are an expert resume analyzer. Return only valid JSON."}, - {"role": "user", "content": prompt} - ], + response = await self.client.chat( + model="command-r", + message=prompt, temperature=0.1, - max_tokens=1500, # Limit response length - timeout=30 # 30 second timeout + max_tokens=1500, + connectors=[] ) - result = response.choices[0].message.content - parsed_result = json.loads(result) + result = response.text.strip() + + # Try to extract JSON from the response + if result.startswith('{') and result.endswith('}'): + parsed_result = json.loads(result) + else: + # Try to find JSON in the response + import re + json_match = re.search(r'\{.*\}', result, re.DOTALL) + if json_match: + parsed_result = json.loads(json_match.group()) + else: + raise ValueError("No valid JSON found in response") # Cache the result for 1 hour ai_cache.set(cache_key, parsed_result, ttl=3600) @@ -88,7 +97,7 @@ class AIService: return empty_result async def analyze_job_description(self, job_description: str) -> Dict[str, Any]: - """Extract structured data from job description using AI with caching""" + """Extract structured data from job description using Cohere AI with caching""" # Check cache first cache_key = self._create_cache_key(job_description, "analyze_job") cached_result = ai_cache.get(cache_key) @@ -96,38 +105,47 @@ class AIService: return cached_result async with self._semaphore: - prompt = f""" - Analyze the following job description and extract structured information: - - {job_description[:3000]} # Limit text length - - Please return a JSON object with the following structure: - {{ - "required_skills": ["skill1", "skill2", ...], - "preferred_skills": ["skill1", "skill2", ...], - "experience_level": "entry/mid/senior", - "education_requirement": "string", - "key_responsibilities": ["resp1", "resp2", ...], - "company_benefits": ["benefit1", "benefit2", ...], - "job_type": "full-time/part-time/contract", - "remote_option": "yes/no/hybrid" - }} - """ + prompt = f"""Analyze this job description and extract structured information. Return only valid JSON. + +Job description: +{job_description[:3000]} + +Extract the following information in JSON format: +{{ + "required_skills": ["skill1", "skill2", ...], + "preferred_skills": ["skill1", "skill2", ...], + "experience_level": "entry/mid/senior", + "education_requirement": "minimum education required", + "key_responsibilities": ["responsibility1", "responsibility2", ...], + "company_benefits": ["benefit1", "benefit2", ...], + "job_type": "full-time/part-time/contract", + "remote_option": "yes/no/hybrid" +}} + +JSON:""" try: - response = await self.client.chat.completions.create( - model="gpt-3.5-turbo", - messages=[ - {"role": "system", "content": "You are an expert job description analyzer. Return only valid JSON."}, - {"role": "user", "content": prompt} - ], + response = await self.client.chat( + model="command-r", + message=prompt, temperature=0.1, max_tokens=1000, - timeout=30 + connectors=[] ) - result = response.choices[0].message.content - parsed_result = json.loads(result) + result = response.text.strip() + + # Try to extract JSON from the response + if result.startswith('{') and result.endswith('}'): + parsed_result = json.loads(result) + else: + # Try to find JSON in the response + import re + json_match = re.search(r'\{.*\}', result, re.DOTALL) + if json_match: + parsed_result = json.loads(json_match.group()) + else: + raise ValueError("No valid JSON found in response") # Cache for 1 hour ai_cache.set(cache_key, parsed_result, ttl=3600) @@ -142,7 +160,7 @@ class AIService: async def calculate_match_score( self, resume_data: Dict[str, Any], job_data: Dict[str, Any] ) -> Dict[str, Any]: - """Calculate match score between resume and job description with caching""" + """Calculate match score between resume and job description using Cohere AI with caching""" # Create cache key from both resume and job data combined_data = f"{json.dumps(resume_data, sort_keys=True)}{json.dumps(job_data, sort_keys=True)}" cache_key = self._create_cache_key(combined_data, "match_score") @@ -155,41 +173,52 @@ class AIService: limited_resume = {k: v for k, v in resume_data.items() if k in ["skills", "experience_years", "education_level"]} limited_job = {k: v for k, v in job_data.items() if k in ["required_skills", "preferred_skills", "experience_level", "education_requirement"]} - prompt = f""" - Calculate a match score between this resume and job description: - - RESUME: {json.dumps(limited_resume)} - JOB: {json.dumps(limited_job)} - - Return JSON: - {{ - "overall_score": number (0-100), - "skill_match_score": number (0-100), - "experience_match_score": number (0-100), - "education_match_score": number (0-100), - "missing_skills": [ - {{"skill": "string", "importance": "required/preferred", "suggestion": "string"}} - ], - "strengths": ["strength1", "strength2"], - "weaknesses": ["weakness1", "weakness2"], - "overall_feedback": "brief feedback" - }} - """ + prompt = f"""Calculate a match score between this resume and job requirements. Return only valid JSON. + +RESUME DATA: +{json.dumps(limited_resume)} + +JOB REQUIREMENTS: +{json.dumps(limited_job)} + +Analyze and return a match score in this JSON format: +{{ + "overall_score": number_0_to_100, + "skill_match_score": number_0_to_100, + "experience_match_score": number_0_to_100, + "education_match_score": number_0_to_100, + "missing_skills": [ + {{"skill": "skill_name", "importance": "required/preferred", "suggestion": "how_to_acquire"}} + ], + "strengths": ["strength1", "strength2"], + "weaknesses": ["weakness1", "weakness2"], + "overall_feedback": "brief_summary" +}} + +JSON:""" try: - response = await self.client.chat.completions.create( - model="gpt-3.5-turbo", - messages=[ - {"role": "system", "content": "You are an expert HR analyst. Provide accurate match scoring. Be concise."}, - {"role": "user", "content": prompt} - ], + response = await self.client.chat( + model="command-r", + message=prompt, temperature=0.2, max_tokens=1500, - timeout=30 + connectors=[] ) - result = response.choices[0].message.content - parsed_result = json.loads(result) + result = response.text.strip() + + # Try to extract JSON from the response + if result.startswith('{') and result.endswith('}'): + parsed_result = json.loads(result) + else: + # Try to find JSON in the response + import re + json_match = re.search(r'\{.*\}', result, re.DOTALL) + if json_match: + parsed_result = json.loads(json_match.group()) + else: + raise ValueError("No valid JSON found in response") # Cache for 30 minutes ai_cache.set(cache_key, parsed_result, ttl=1800) @@ -204,7 +233,7 @@ class AIService: async def generate_resume_suggestions( self, resume_data: Dict[str, Any], job_data: Dict[str, Any], match_analysis: Dict[str, Any] ) -> List[Dict[str, str]]: - """Generate suggestions for improving resume based on job requirements with caching""" + """Generate suggestions for improving resume using Cohere AI with caching""" # Create cache key from all input data combined_data = f"{json.dumps(resume_data, sort_keys=True)}{json.dumps(job_data, sort_keys=True)}{json.dumps(match_analysis, sort_keys=True)}" cache_key = self._create_cache_key(combined_data, "resume_suggestions") @@ -215,41 +244,50 @@ class AIService: async with self._semaphore: # Use only essential data for faster processing limited_data = { - "skills": resume_data.get("skills", []), + "current_skills": resume_data.get("skills", []), "missing_skills": match_analysis.get("missing_skills", []), "weaknesses": match_analysis.get("weaknesses", []) } - prompt = f""" - Provide 3-5 specific resume improvement suggestions based on this analysis: - - DATA: {json.dumps(limited_data)} - - Return JSON array: - [ - {{ - "section": "skills/experience/education/summary", - "suggestion": "specific actionable suggestion", - "priority": "high/medium/low", - "impact": "brief explanation" - }} - ] - """ + prompt = f"""Provide 3-5 specific resume improvement suggestions. Return only valid JSON. + +Analysis data: +{json.dumps(limited_data)} + +Return suggestions in this JSON array format: +[ + {{ + "section": "skills/experience/education/summary", + "suggestion": "specific_actionable_suggestion", + "priority": "high/medium/low", + "impact": "brief_explanation" + }} +] + +JSON:""" try: - response = await self.client.chat.completions.create( - model="gpt-3.5-turbo", - messages=[ - {"role": "system", "content": "You are an expert resume coach. Be concise and actionable."}, - {"role": "user", "content": prompt} - ], + response = await self.client.chat( + model="command-r", + message=prompt, temperature=0.3, max_tokens=800, - timeout=30 + connectors=[] ) - result = response.choices[0].message.content - parsed_result = json.loads(result) + result = response.text.strip() + + # Try to extract JSON from the response + if result.startswith('[') and result.endswith(']'): + parsed_result = json.loads(result) + else: + # Try to find JSON array in the response + import re + json_match = re.search(r'\[.*\]', result, re.DOTALL) + if json_match: + parsed_result = json.loads(json_match.group()) + else: + raise ValueError("No valid JSON array found in response") # Cache for 1 hour ai_cache.set(cache_key, parsed_result, ttl=3600) @@ -264,7 +302,7 @@ class AIService: async def generate_cover_letter( self, resume_data: Dict[str, Any], job_data: Dict[str, Any], user_name: str ) -> str: - """Generate a personalized cover letter with caching""" + """Generate a personalized cover letter using Cohere AI with caching""" # Create cache key from resume, job, and user name combined_data = f"{json.dumps(resume_data, sort_keys=True)}{json.dumps(job_data, sort_keys=True)}{user_name}" cache_key = self._create_cache_key(combined_data, "cover_letter") @@ -275,7 +313,7 @@ class AIService: async with self._semaphore: # Use essential data only essential_resume = { - "skills": resume_data.get("skills", []), + "skills": resume_data.get("skills", [])[:8], # Top 8 skills "work_experience": resume_data.get("work_experience", [])[:2] # Only first 2 jobs } essential_job = { @@ -284,32 +322,31 @@ class AIService: "required_skills": job_data.get("required_skills", [])[:5] # Top 5 skills } - prompt = f""" - Write a professional cover letter for {user_name}: - - RESUME: {json.dumps(essential_resume)} - JOB: {json.dumps(essential_job)} - - Requirements: - - 3 paragraphs - - Professional tone - - Highlight relevant skills - - Show enthusiasm - """ + prompt = f"""Write a professional cover letter for {user_name} applying to this job. + +APPLICANT BACKGROUND: +{json.dumps(essential_resume)} + +JOB DETAILS: +{json.dumps(essential_job)} + +Write a compelling 3-paragraph cover letter that: +- Opens with enthusiasm for the specific role +- Highlights relevant skills and experience +- Closes with a call to action + +Keep it professional, concise, and engaging. Do not include placeholders or brackets.""" try: - response = await self.client.chat.completions.create( - model="gpt-3.5-turbo", - messages=[ - {"role": "system", "content": "You are an expert cover letter writer. Write compelling, concise cover letters."}, - {"role": "user", "content": prompt} - ], + response = await self.client.chat( + model="command-r", + message=prompt, temperature=0.4, max_tokens=600, - timeout=30 + connectors=[] ) - result = response.choices[0].message.content + result = response.text.strip() # Cache for 30 minutes ai_cache.set(cache_key, result, ttl=1800) diff --git a/requirements.txt b/requirements.txt index 985777d..3d5692a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ python-multipart==0.0.6 python-jose[cryptography]==3.3.0 passlib[bcrypt]==1.7.4 httpx==0.25.2 -openai>=1.6.1 +cohere==4.47 PyPDF2==3.0.1 python-docx==1.1.0 cachetools==5.3.2