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