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)