feat: CV Batches system with AI extraction, matching, and slide-in chat
This commit is contained in:
159
ai_service.py
159
ai_service.py
@@ -455,4 +455,161 @@ def chat(user_message: str, conversation_history: list = None, context: str = ""
|
||||
|
||||
messages.append({"role": "user", "content": user_message})
|
||||
|
||||
return llm_chat(messages, temperature=0.5, max_tokens=2000)
|
||||
return llm_chat(messages, temperature=0.5, max_tokens=2000)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# BATCH FUNCTIONS — extract requirements, match candidates, batch chat
|
||||
# ============================================================
|
||||
|
||||
EXTRACT_REQUIREMENTS_PROMPT = """You are a requirements analyst. Analyze the following document and extract the job positions and their requirements.
|
||||
|
||||
Return ONLY a valid JSON object with this structure:
|
||||
{
|
||||
"batch_name": "Short name for this batch (from the document title or first heading)",
|
||||
"description": "Brief description of what this batch is for (1-2 sentences)",
|
||||
"positions": [
|
||||
{
|
||||
"job_title": "Position title",
|
||||
"num_positions": 1,
|
||||
"required_skills": ["skill1", "skill2"],
|
||||
"required_years": 5,
|
||||
"required_certs": ["cert1"],
|
||||
"nice_to_have": ["skill3"],
|
||||
"disqualifiers": ["something that would disqualify a candidate"],
|
||||
"description": "Brief description of the role"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Rules:
|
||||
- Extract ALL positions mentioned in the document.
|
||||
- If a field is not specified, use empty string "" or empty array [].
|
||||
- Be precise about required skills — distinguish must-haves from nice-to-haves.
|
||||
- If no positions are found, return an empty positions array.
|
||||
- Return ONLY the JSON, no other text.
|
||||
|
||||
Document text:
|
||||
---
|
||||
__DOC_TEXT__
|
||||
---"""
|
||||
|
||||
def extract_requirements(doc_text: str) -> dict:
|
||||
"""Extract job requirements from a document using AI."""
|
||||
max_chars = 15000
|
||||
if len(doc_text) > max_chars:
|
||||
doc_text = doc_text[:max_chars] + "\n[...truncated...]"
|
||||
|
||||
prompt = EXTRACT_REQUIREMENTS_PROMPT.replace("__DOC_TEXT__", doc_text)
|
||||
messages = [
|
||||
{"role": "system", "content": "You are a requirements analyst that outputs only valid JSON. Make sure all string values are properly escaped."},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
for max_tokens in [4000, 6000, 8000]:
|
||||
try:
|
||||
resp = llm_chat(messages, temperature=0.1, max_tokens=max_tokens)
|
||||
return extract_json(resp)
|
||||
except (ValueError, json.JSONDecodeError):
|
||||
if max_tokens == 8000:
|
||||
raise
|
||||
continue
|
||||
|
||||
|
||||
MATCH_PROMPT = """You are a CV matching specialist. You are given a job position's requirements and a list of candidates with their skills, experience, education, and certifications.
|
||||
|
||||
For each candidate, determine:
|
||||
1. A match score (0-100) based on how well they meet the requirements
|
||||
2. Whether they are a good fit (score >= 60)
|
||||
3. A brief reasoning for the match or mismatch
|
||||
4. If they are a close match (score 50-79), suggest how their CV could be realigned (reworded) to better fit — without fabricating skills they don't have
|
||||
|
||||
Return ONLY a valid JSON array of matching candidates:
|
||||
[
|
||||
{
|
||||
"candidate_id": "the UUID",
|
||||
"candidate_name": "First Last",
|
||||
"match_score": 85,
|
||||
"fit": true,
|
||||
"reasoning": "Strong match: has 7 years Python, AWS cert, Kubernetes experience",
|
||||
"realignment_suggestion": "Emphasize microservices architecture experience over generic 'distributed systems' wording; reorder skills to put AWS and Kubernetes first"
|
||||
}
|
||||
]
|
||||
|
||||
Only include candidates with match_score >= 40. Sort by match_score descending.
|
||||
Return ONLY the JSON array, no other text.
|
||||
|
||||
Position Requirements:
|
||||
__POSITION__
|
||||
|
||||
Candidates:
|
||||
__CANDIDATES__"""
|
||||
|
||||
def match_candidates_for_position(position: dict, candidates: list) -> list:
|
||||
"""Match candidates against a single position's requirements using AI."""
|
||||
# Build a compact summary of each candidate for the LLM
|
||||
cand_summaries = []
|
||||
for c in candidates:
|
||||
skills = ", ".join([s.get("skill_name", "") for s in c.get("skills", [])[:15]])
|
||||
exp = "; ".join([f"{e.get('position','')} at {e.get('company','')}" for e in c.get("experience", [])[:5]])
|
||||
certs = ", ".join([cert.get("name", "") for cert in c.get("certifications", [])[:5]])
|
||||
edu = "; ".join([f"{e.get('degree','')} {e.get('field_of_study','')} at {e.get('institution','')}" for e in c.get("education", [])[:3]])
|
||||
|
||||
cand_summaries.append(f"""Candidate ID: {c.get('id', 'N/A')}
|
||||
Name: {c.get('first_name', '')} {c.get('last_name', '')}
|
||||
Skills: {skills}
|
||||
Experience: {exp}
|
||||
Certifications: {certs}
|
||||
Education: {edu}
|
||||
Summary: {c.get('summary', '')[:200]}""")
|
||||
|
||||
position_json = json.dumps(position, indent=2)
|
||||
candidates_text = "\n\n---\n\n".join(cand_summaries)
|
||||
|
||||
prompt = MATCH_PROMPT.replace("__POSITION__", position_json).replace("__CANDIDATES__", candidates_text)
|
||||
messages = [
|
||||
{"role": "system", "content": "You are a CV matching specialist that outputs only valid JSON. Make sure all string values are properly escaped."},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
for max_tokens in [4000, 6000, 8000]:
|
||||
try:
|
||||
resp = llm_chat(messages, temperature=0.1, max_tokens=max_tokens)
|
||||
result = extract_json(resp)
|
||||
if isinstance(result, list):
|
||||
return result
|
||||
elif isinstance(result, dict) and 'candidates' in result:
|
||||
return result['candidates']
|
||||
return [result]
|
||||
except (ValueError, json.JSONDecodeError):
|
||||
if max_tokens == 8000:
|
||||
raise
|
||||
continue
|
||||
return []
|
||||
|
||||
|
||||
BATCH_CHAT_SYSTEM = """You are an AI assistant helping a user manage a CV batch. You have context about the batch (name, description, positions, and matched candidates).
|
||||
|
||||
You can help the user with:
|
||||
1. Discussing which candidates are best for specific positions
|
||||
2. Suggesting CV realignment — rewording a candidate's experience to better match a position (without fabricating)
|
||||
3. Answering questions about candidate qualifications
|
||||
4. Recommending which candidates to approve or remove
|
||||
|
||||
Be concise and direct. When suggesting changes, be specific about what to change and why.
|
||||
|
||||
Batch context:
|
||||
__BATCH_CONTEXT__"""
|
||||
|
||||
def batch_chat(user_message: str, batch_context: str, conversation_history: list = None) -> str:
|
||||
"""Context-aware chat for a specific batch."""
|
||||
system_content = BATCH_CHAT_SYSTEM.replace("__BATCH_CONTEXT__", batch_context)
|
||||
messages = [{"role": "system", "content": system_content}]
|
||||
|
||||
if conversation_history:
|
||||
for msg in conversation_history[-10:]:
|
||||
messages.append({"role": msg["role"], "content": msg["content"]})
|
||||
|
||||
messages.append({"role": "user", "content": user_message})
|
||||
|
||||
return llm_chat(messages, temperature=0.5, max_tokens=3000)
|
||||
Reference in New Issue
Block a user