backstory/src/backend/agents/generate_resume.py

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)