220 lines
8.1 KiB
Python
220 lines
8.1 KiB
Python
from __future__ import annotations
|
|
from pydantic import model_validator, Field # type: ignore
|
|
from typing import (
|
|
Dict,
|
|
Literal,
|
|
ClassVar,
|
|
Any,
|
|
AsyncGenerator,
|
|
List,
|
|
Optional
|
|
# override
|
|
) # NOTE: You must import Optional for late binding to work
|
|
import inspect
|
|
import re
|
|
import json
|
|
import traceback
|
|
import asyncio
|
|
import time
|
|
import asyncio
|
|
import numpy as np # type: ignore
|
|
|
|
from logger import logger
|
|
from .base import Agent, agent_registry
|
|
from models import (ApiActivityType, ApiStatusType, Candidate, ChatMessage, ChatMessageError, ChatMessageResume, ChatMessageStatus, JobRequirements, JobRequirementsMessage, SkillAssessment, SkillStrength, Tunables)
|
|
|
|
class GenerateResume(Agent):
|
|
agent_type: Literal["generate_resume"] = "generate_resume" # type: ignore
|
|
_agent_type: ClassVar[str] = agent_type # Add this for registration
|
|
|
|
def generate_resume_prompt(
|
|
self,
|
|
skills: List[SkillAssessment]
|
|
):
|
|
"""
|
|
Generate a professional resume based on skill assessment results
|
|
|
|
Parameters:
|
|
- candidate_name (str): The candidate's full name
|
|
- candidate_contact_info (dict): Contact details like email, phone, location
|
|
- skill_assessment_results (list): List of individual skill assessment results from LLM queries
|
|
- original_resume (str): Original resume text for reference
|
|
|
|
Returns:
|
|
- str: System prompt for generating a professional resume
|
|
"""
|
|
if not self.user:
|
|
raise ValueError("User must be bound to agent")
|
|
|
|
# Extract and organize skill assessment data
|
|
skills_by_strength = {
|
|
SkillStrength.STRONG: [],
|
|
SkillStrength.MODERATE: [],
|
|
SkillStrength.WEAK: [],
|
|
SkillStrength.NONE: []
|
|
}
|
|
|
|
experience_evidence = {}
|
|
|
|
# Process each skill assessment
|
|
for assessment in skills:
|
|
skill = assessment.skill
|
|
strength = assessment.evidence_strength
|
|
|
|
# Add to appropriate strength category
|
|
if skill and strength in skills_by_strength:
|
|
skills_by_strength[strength].append(skill)
|
|
|
|
# Collect experience evidence
|
|
for evidence in assessment.evidence_details:
|
|
source = evidence.source
|
|
if source:
|
|
if source not in experience_evidence:
|
|
experience_evidence[source] = []
|
|
|
|
experience_evidence[source].append(
|
|
{
|
|
"skill": skill,
|
|
"quote": evidence.quote,
|
|
"context": evidence.context
|
|
}
|
|
)
|
|
|
|
# Build the system prompt
|
|
system_prompt = f"""You are a professional resume writer with expertise in highlighting candidate strengths and experiences.
|
|
Create a polished, concise, and ATS-friendly resume for the candidate based on the assessment data provided.
|
|
|
|
## CANDIDATE INFORMATION:
|
|
Name: {self.user.full_name}
|
|
Email: {self.user.email or 'N/A'}
|
|
Phone: {self.user.phone or 'N/A'}
|
|
{f'Location: {json.dumps(self.user.location.model_dump())}' if self.user.location else ''}
|
|
|
|
## SKILL ASSESSMENT RESULTS:
|
|
"""
|
|
|
|
if len(skills_by_strength[SkillStrength.STRONG]):
|
|
system_prompt += f"""\
|
|
|
|
### Strong Skills (prominent in resume):
|
|
{", ".join(skills_by_strength[SkillStrength.STRONG])}
|
|
"""
|
|
|
|
if len(skills_by_strength[SkillStrength.MODERATE]):
|
|
system_prompt += f"""\
|
|
|
|
### Moderate Skills (demonstrated in resume):
|
|
{", ".join(skills_by_strength[SkillStrength.MODERATE])}
|
|
"""
|
|
|
|
if len(skills_by_strength[SkillStrength.WEAK]):
|
|
system_prompt += f"""\
|
|
|
|
### Weaker Skills (mentioned or implied):
|
|
{", ".join(skills_by_strength[SkillStrength.WEAK])}
|
|
"""
|
|
|
|
system_prompt += """\
|
|
|
|
## EXPERIENCE EVIDENCE:
|
|
"""
|
|
|
|
# Add experience evidence by source/position
|
|
for source, evidences in experience_evidence.items():
|
|
system_prompt += f"\n### {source}:\n"
|
|
for evidence in evidences:
|
|
system_prompt += f"- {evidence['skill']}: {evidence['context']}\n"
|
|
|
|
# Add instructions for the resume creation
|
|
system_prompt += """\
|
|
|
|
## INSTRUCTIONS:
|
|
|
|
CRITICAL: Do NOT invent, fabricate, or assume any information not explicitly provided.
|
|
If information is missing, state what is missing rather than creating fictional details.
|
|
When sections lack data, output "Information not provided" or use placeholder text.
|
|
|
|
1. Create a professional resume that emphasizes the candidate's strongest skills and most relevant experiences.
|
|
2. Format the resume in a clean, concise, and modern style that will pass ATS systems.
|
|
3. Include these sections:
|
|
- Professional Summary (highlight strongest skills and experience level)
|
|
- Skills (organized by strength, under a single section). When listing skills, rephrase them so they are not identical to the original assessment.
|
|
- Professional Experience (focus on achievements and evidence of the skill)
|
|
4. Optional sections, to include only if evidence is present:
|
|
- Education section
|
|
Certifications section
|
|
- Additional sections as appropriate
|
|
5. Use action verbs and quantifiable achievements where possible.
|
|
6. Maintain a professional tone throughout.
|
|
7. Be concise and impactful - the resume should be 1-2 pages MAXIMUM.
|
|
8. Ensure all information is accurate to the original resume - do not embellish or fabricate experiences.
|
|
|
|
If SKILL ASSESSMENT RESULTS or EXPERIENCE EVIDENCE sections are empty:
|
|
- Do not create fictional work history
|
|
- Do not invent specific companies, dates, or achievements
|
|
- Instead, create a template with placeholders like [Company Name], [Start Date - End Date]
|
|
- Include a note: "Template requires candidate input for: [missing sections]"
|
|
|
|
IF sufficient experience data exists:
|
|
Create full professional experience section
|
|
ELSE:
|
|
Output: "Professional Experience section requires candidate input"
|
|
Provide template format only
|
|
|
|
## OUTPUT FORMAT:
|
|
Provide the resume in clean markdown format, ready for the candidate to use.
|
|
|
|
"""
|
|
prompt = """\
|
|
Create a tailored professional resume that highlights candidate's skills and experience most relevant to the job requirements.
|
|
Format it in clean, ATS-friendly markdown. Provide ONLY the resume with no commentary before or after.
|
|
"""
|
|
|
|
return system_prompt, prompt
|
|
|
|
async def generate_resume(
|
|
self, llm: Any, model: str, session_id: str, skills: List[SkillAssessment]
|
|
) -> AsyncGenerator[ChatMessage | ChatMessageError, None]:
|
|
# Stage 1A: Analyze job requirements
|
|
status_message = ChatMessageStatus(
|
|
session_id=session_id,
|
|
content = f"Analyzing job requirements",
|
|
activity=ApiActivityType.THINKING
|
|
)
|
|
yield status_message
|
|
|
|
system_prompt, prompt = self.generate_resume_prompt(skills=skills)
|
|
|
|
generated_message = None
|
|
async for generated_message in self.llm_one_shot(llm=llm, model=model, session_id=session_id, prompt=prompt, system_prompt=system_prompt):
|
|
if generated_message.status == ApiStatusType.ERROR:
|
|
yield generated_message
|
|
return
|
|
if generated_message.status != ApiStatusType.DONE:
|
|
yield generated_message
|
|
|
|
if not generated_message:
|
|
error_message = ChatMessageError(
|
|
session_id=session_id,
|
|
content="Job requirements analysis failed to generate a response."
|
|
)
|
|
logger.error(f"⚠️ {error_message.content}")
|
|
yield error_message
|
|
return
|
|
|
|
resume_message = ChatMessageResume(
|
|
session_id=session_id,
|
|
status=ApiStatusType.DONE,
|
|
content="Resume generation completed successfully.",
|
|
metadata=generated_message.metadata,
|
|
resume=generated_message.content,
|
|
prompt=prompt,
|
|
system_prompt=system_prompt,
|
|
)
|
|
yield resume_message
|
|
logger.info(f"✅ Resume generation completed successfully.")
|
|
return
|
|
|
|
# Register the base agent
|
|
agent_registry.register(GenerateResume._agent_type, GenerateResume)
|