feat: CV Batches system with AI extraction, matching, and slide-in chat

This commit is contained in:
root
2026-07-25 09:41:26 +00:00
parent 692b383aa5
commit ecb9ebb507
10 changed files with 910 additions and 18 deletions

View File

@@ -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)