Multi-pass is flowing...

This commit is contained in:
James Ketr 2025-05-06 21:47:14 -07:00
parent 963cd7491e
commit 2ee1356189
2 changed files with 851 additions and 33 deletions

View File

@ -287,7 +287,7 @@ class Agent(BaseModel, ABC):
message.metadata["timers"]["llm_with_tools"] = f"{(end_time - start_time):.4f}"
return
async def generate_llm_response(self, llm: Any, model: str, message: Message) -> AsyncGenerator[Message, None]:
async def generate_llm_response(self, llm: Any, model: str, message: Message, temperature = 0.7) -> AsyncGenerator[Message, None]:
logger.info(f"{self.agent_type} - {inspect.stack()[0].function}")
self.metrics.generate_count.labels(agent=self.agent_type).inc()
@ -312,7 +312,7 @@ class Agent(BaseModel, ABC):
message.metadata["options"]={
"seed": 8911,
"num_ctx": self.context_size,
#"temperature": 0.9, # Higher temperature to encourage tool usage
"temperature": temperature # Higher temperature to encourage tool usage
}
# Create a dict for storing various timing stats

View File

@ -1,15 +1,19 @@
from __future__ import annotations
from pydantic import model_validator # type: ignore
from typing import Literal, ClassVar, Optional, Any, AsyncGenerator, List # NOTE: You must import Optional for late binding to work
from pydantic import model_validator, Field # type: ignore
from typing import Dict, Literal, ClassVar, Optional, Any, AsyncGenerator, List # NOTE: You must import Optional for late binding to work
from datetime import datetime
import inspect
import re
import json
import traceback
from . base import Agent, agent_registry
from . base import Agent, agent_registry, LLMMessage
from .. conversation import Conversation
from .. message import Message
from .. setup_logging import setup_logging
logger = setup_logging()
system_generate_resume = """
You are an objective skills analyzer for resume tailoring. Your task is to
analyze a job description and a candidate's background, identifying ONLY
@ -95,33 +99,50 @@ When answering queries, follow these steps:
- Avoid phrases like 'According to the <|context|>' or similar references to the <|context|>, <|job_description|>, <|resume|>, or <|context|> tags.
""".strip()
system_user_qualifications = """
You are an objective job requirements analyzer. Your task is to extract and categorize the specific skills, experiences, and qualifications required in a job description WITHOUT any reference to any candidate.
## INSTRUCTIONS:
1. Analyze ONLY the job description provided.
2. Extract and categorize all requirements and preferences.
3. DO NOT consider any candidate information - this is a pure job analysis task.
## OUTPUT FORMAT:
```json
{
"job_requirements": {
"technical_skills": {
"required": ["skill1", "skill2"...],
"preferred": ["skill1", "skill2"...]
},
"experience_requirements": {
"required": ["exp1", "exp2"...],
"preferred": ["exp1", "exp2"...]
},
"education_requirements": ["req1", "req2"...],
"soft_skills": ["skill1", "skill2"...],
"industry_knowledge": ["knowledge1", "knowledge2"...],
"responsibilities": ["resp1", "resp2"...],
"company_values": ["value1", "value2"...]
}
}
```
Be specific and detailed in your extraction. Break down compound requirements into individual components.
For example, "5+ years experience with React, Node.js and MongoDB" should be separated into:
- Experience: "5+ years software development"
- Technical skills: "React", "Node.js", "MongoDB"
Avoid vague categorizations and be precise about whether skills are explicitly required or just preferred.
</|system|>
<|job_description|>
[INSERT JOB DESCRIPTION HERE]
</|job_description|>
"""
# def run_resume_pipeline(job_description, resume, context):
# # Stage 1: Analysis
# analysis_prompt = create_analysis_prompt(job_description, resume, context)
# job_analysis = call_llm_with_prompt(analysis_prompt)
# # Validate analysis output
# validated_analysis = validate_job_analysis(job_analysis)
# if not validated_analysis:
# return {"error": "Analysis stage failed validation"}
# # Stage 2: Generation
# generation_prompt = create_generation_prompt(validated_analysis, resume)
# tailored_resume = call_llm_with_prompt(generation_prompt)
# # Stage 3: Verification
# verification_prompt = create_verification_prompt(validated_analysis, resume, context, tailored_resume)
# verification_result = call_llm_with_prompt(verification_prompt)
# # Process verification results
# if verification_result.get("verification_results", {}).get("overall_assessment") == "APPROVED":
# return {"status": "success", "resume": tailored_resume}
# else:
# # Optional: Implement correction loop
# corrected_resume = apply_corrections(tailored_resume, verification_result)
# return {"status": "corrected", "resume": corrected_resume}
class JobDescription(Agent):
agent_type: Literal["job_description"] = "job_description" # type: ignore
_agent_type: ClassVar[str] = agent_type # Add this for registration
@ -129,6 +150,9 @@ class JobDescription(Agent):
system_prompt: str = system_generate_resume
job_description: str
llm: Any = Field(default=None, exclude=True)
model: str = Field(default=None, exclude=True)
@model_validator(mode="after")
def validate_job_description(self):
if not self.job_description.strip():
@ -181,6 +205,8 @@ class JobDescription(Agent):
if message.status != "done":
yield message
self.system_prompt = system_user_qualifications
resume_agent = self.context.get_agent(agent_type="resume")
if not resume_agent:
# Switch agent from "Create Resume from Job Desription" mode
@ -190,14 +216,806 @@ class JobDescription(Agent):
# Instantiate the "resume" agent, and seed (or reset) its conversation
# with this message.
resume_agent = self.context.get_or_create_agent(agent_type="resume", resume=message.response)
first_resume_message = message.copy()
first_resume_message = message.model_copy()
first_resume_message.prompt = "Generate a resume for the job description."
resume_agent.conversation.add(first_resume_message)
message.response = "Resume generated."
# Return the final message
yield message
return
# BEGIN
# Helper functions
def extract_json_from_text(self, text: str) -> str:
"""Extract JSON string from text that may contain other content."""
json_pattern = r'```json\s*([\s\S]*?)\s*```'
match = re.search(json_pattern, text)
if match:
return match.group(1).strip()
# Try to find JSON without the markdown code block
json_pattern = r'({[\s\S]*})'
match = re.search(json_pattern, text)
if match:
return match.group(1).strip()
raise ValueError("No JSON found in the response")
def validate_job_requirements(self, job_requirements: Dict) -> None:
"""Validate the structure of job requirements."""
required_keys = ["job_requirements"]
if not all(key in job_requirements for key in required_keys):
missing = [key for key in required_keys if key not in job_requirements]
raise ValueError(f"Missing required keys in job requirements: {missing}")
# Additional validation can be added here
def validate_candidate_qualifications(self, candidate_qualifications: Dict) -> None:
"""Validate the structure of candidate qualifications."""
required_keys = ["candidate_qualifications"]
if not all(key in candidate_qualifications for key in required_keys):
missing = [key for key in required_keys if key not in candidate_qualifications]
raise ValueError(f"Missing required keys in candidate qualifications: {missing}")
# Additional validation can be added here
def validate_skills_mapping(self, skills_mapping: Dict) -> None:
"""Validate the structure of skills mapping."""
required_keys = ["skills_mapping", "resume_recommendations"]
if not all(key in skills_mapping for key in required_keys):
missing = [key for key in required_keys if key not in skills_mapping]
raise ValueError(f"Missing required keys in skills mapping: {missing}")
# Additional validation can be added here
def extract_header_from_resume(self, resume: str) -> str:
"""Extract header information from the original resume."""
# Simple implementation - in practice, you might want a more sophisticated approach
lines = resume.strip().split("\n")
# Take the first few non-empty lines as the header
header_lines = []
for line in lines[:10]: # Arbitrarily choose first 10 lines to search
if line.strip():
header_lines.append(line)
if len(header_lines) >= 4: # Assume header is no more than 4 lines
break
return "\n".join(header_lines)
# Stage 1A: Job Analysis Implementation
def create_job_analysis_prompt(self, job_description: str) -> tuple[str, str]:
"""Create the prompt for job requirements analysis."""
system_prompt = """
You are an objective job requirements analyzer. Your task is to extract and categorize the specific skills,
experiences, and qualifications required in a job description WITHOUT any reference to any candidate.
## INSTRUCTIONS:
1. Analyze ONLY the job description provided.
2. Extract and categorize all requirements and preferences.
3. DO NOT consider any candidate information - this is a pure job analysis task.
## OUTPUT FORMAT:
```json
{
"job_requirements": {
"technical_skills": {
"required": ["skill1", "skill2"],
"preferred": ["skill1", "skill2"]
},
"experience_requirements": {
"required": ["exp1", "exp2"],
"preferred": ["exp1", "exp2"]
},
"education_requirements": ["req1", "req2"],
"soft_skills": ["skill1", "skill2"],
"industry_knowledge": ["knowledge1", "knowledge2"],
"responsibilities": ["resp1", "resp2"],
"company_values": ["value1", "value2"]
}
}
```
Be specific and detailed in your extraction. Break down compound requirements into individual components.
For example, "5+ years experience with React, Node.js and MongoDB" should be separated into:
- Experience: "5+ years software development"
- Technical skills: "React", "Node.js", "MongoDB"
Avoid vague categorizations and be precise about whether skills are explicitly required or just preferred.
"""
prompt = f"Job Description:\n{job_description}"
return system_prompt, prompt
async def analyze_job_requirements(self, message, job_description: str) -> AsyncGenerator[Message, None]:
"""Analyze job requirements from job description."""
try:
system_prompt, prompt = self.create_job_analysis_prompt(job_description)
async for message in self.call_llm(message, system_prompt, prompt):
if message.status != "done":
yield message
if message.status == "error":
return
# Extract JSON from response
json_str = self.extract_json_from_text(message.response)
job_requirements = json.loads(json_str)
self.validate_job_requirements(job_requirements)
message.status = "done"
message.response = json_str
yield message
return
except Exception as e:
message.status = "error"
message.response = f"Error in job requirements analysis: {str(e)}"
logger.error(message.response)
logger.error(traceback.format_exc())
yield message
raise
# Stage 1B: Candidate Analysis Implementation
def create_candidate_analysis_prompt(self, resume: str, context: str) -> tuple[str, str]:
"""Create the prompt for candidate qualifications analysis."""
system_prompt = """
You are an objective resume analyzer. Your task is to catalog ALL skills, experiences, and qualifications
present in a candidate's materials WITHOUT any reference to any job description.
## INSTRUCTIONS:
1. Analyze ONLY the candidate's resume and context provided.
2. Create a comprehensive inventory of the candidate's actual qualifications.
3. DO NOT consider any job requirements - this is a pure candidate analysis task.
4. For each qualification, cite exactly where in the materials it appears.
## OUTPUT FORMAT:
```json
{
"candidate_qualifications": {
"technical_skills": [
{
"skill": "skill name",
"evidence": "exact quote from materials",
"source": "resume or context",
"expertise_level": "explicit level mentioned or 'unspecified'"
}
],
"work_experience": [
{
"role": "job title",
"company": "company name",
"duration": "time period",
"responsibilities": ["resp1", "resp2"],
"technologies_used": ["tech1", "tech2"],
"achievements": ["achievement1", "achievement2"]
}
],
"education": [
{
"degree": "degree name",
"institution": "institution name",
"completed": true/false,
"evidence": "exact quote from materials"
}
],
"projects": [
{
"name": "project name",
"description": "brief description",
"technologies_used": ["tech1", "tech2"],
"evidence": "exact quote from materials"
}
],
"soft_skills": [
{
"skill": "skill name",
"evidence": "exact quote or inference basis",
"source": "resume or context"
}
]
}
}
```
Be thorough and precise. Include ONLY skills and experiences explicitly mentioned in the materials.
For each entry, provide the exact text evidence from the materials that supports its inclusion.
Do not make assumptions about skills based on job titles or project names - only include skills explicitly mentioned.
"""
prompt = f"Resume:\n{resume}\n\nAdditional Context:\n{context}"
return system_prompt, prompt
async def call_llm(self, message: Message, system_prompt, prompt, temperature=0.7):
messages : List[LLMMessage] = [
LLMMessage(role="system", content=system_prompt),
LLMMessage(role="user", content=prompt)
]
message.metadata["options"]={
"seed": 8911,
"num_ctx": self.context_size,
"temperature": temperature # Higher temperature to encourage tool usage
}
message.status = "streaming"
yield message
message.response = ""
for response in self.llm.chat(
model=self.model,
messages=messages,
options={
**message.metadata["options"],
},
stream=True,
):
if not response:
message.status = "error"
message.response = "No response from LLM."
yield message
return
message.status = "streaming"
message.response += response.message.content
if not response.done:
yield message
if response.done:
message.metadata["eval_count"] += response.eval_count
message.metadata["eval_duration"] += response.eval_duration
message.metadata["prompt_eval_count"] += response.prompt_eval_count
message.metadata["prompt_eval_duration"] += response.prompt_eval_duration
self.context_tokens = response.prompt_eval_count + response.eval_count
message.status = "done"
yield message
async def analyze_candidate_qualifications(self, message: Message, resume: str, context: str) -> AsyncGenerator[Message, None]:
"""Analyze candidate qualifications from resume and context."""
try:
system_prompt, prompt = self.create_candidate_analysis_prompt(resume, context)
async for message in self.call_llm(message, system_prompt, prompt):
if message.status != "done":
yield message
if message.status == "error":
return
# Extract JSON from response
json_str = self.extract_json_from_text(message.response)
candidate_qualifications = json.loads(json_str)
# Validate structure
self.validate_candidate_qualifications(candidate_qualifications)
message.status = "done"
message.response = json.dumps(candidate_qualifications)
return
except Exception as e:
message.status = "error"
message.response = f"Error in candidate qualifications analysis: {str(e)}"
logger.error(message.response)
logger.error(traceback.format_exc())
yield message
raise
# Stage 1C: Mapping Analysis Implementation
def create_mapping_analysis_prompt(self ,job_requirements: Dict, candidate_qualifications: Dict) -> tuple[str, str]:
"""Create the prompt for mapping analysis."""
system_prompt = """
You are an objective skills mapper. Your task is to identify legitimate matches between job requirements
and candidate qualifications WITHOUT fabricating or stretching the truth.
## INSTRUCTIONS:
1. Use ONLY the structured job requirements and candidate qualifications provided.
2. Create a mapping that shows where the candidate's actual skills and experiences align with job requirements.
3. Identify gaps where the candidate lacks required qualifications.
4. Suggest legitimate transferable skills ONLY when there is reasonable evidence.
## OUTPUT FORMAT:
```json
{
"skills_mapping": {
"direct_matches": [
{
"job_requirement": "required skill",
"candidate_qualification": "matching skill",
"evidence": "exact quote from candidate materials"
}
],
"transferable_skills": [
{
"job_requirement": "required skill",
"candidate_qualification": "transferable skill",
"reasoning": "explanation of legitimate transferability",
"evidence": "exact quote from candidate materials"
}
],
"gap_analysis": {
"missing_required_skills": ["skill1", "skill2"],
"missing_preferred_skills": ["skill1", "skill2"],
"missing_experience": ["exp1", "exp2"]
}
},
"resume_recommendations": {
"highlight_points": [
{
"qualification": "candidate's qualification",
"relevance": "why this is highly relevant to the job"
}
],
"transferable_narratives": [
{
"from": "candidate's actual experience",
"to": "job requirement",
"suggested_framing": "how to honestly present the transfer"
}
],
"honest_limitations": [
"frank assessment of major qualification gaps"
]
}
}
```
CRITICAL RULES:
1. A "direct match" requires the EXACT SAME skill in both job requirements and candidate qualifications
2. A "transferable skill" must have legitimate, defensible connection - do not stretch credibility
3. All "missing_required_skills" MUST be acknowledged - do not ignore major gaps
4. Every match or transfer claim must cite specific evidence from the candidate materials
5. Be conservative in claiming transferability - when in doubt, list as missing
"""
prompt = f"Job Requirements:\n{json.dumps(job_requirements, indent=2)}\n\n"
prompt += f"Candidate Qualifications:\n{json.dumps(candidate_qualifications, indent=2)}"
return system_prompt, prompt
async def create_skills_mapping(self, message, job_requirements: Dict, candidate_qualifications: Dict) -> AsyncGenerator[Message, None]:
"""Create mapping between job requirements and candidate qualifications."""
try:
system_prompt, prompt = self.create_mapping_analysis_prompt(job_requirements, candidate_qualifications)
async for message in self.call_llm(message, system_prompt, prompt):
if message != "done":
yield message
if message.status == "error":
return
# Extract JSON from response
json_str = self.extract_json_from_text(message.response)
skills_mapping = json.loads(json_str)
# Validate structure
self.validate_skills_mapping(skills_mapping)
message.status = "done"
message.response = json_str
yield message
return
except Exception as e:
message.status = "error"
message.response = f"Error in skills mapping analysis: {str(e)}"
logger.error(message.response)
logger.error(traceback.format_exc())
yield message
raise
# Stage 2: Resume Generation Implementation
def create_resume_generation_prompt(self, skills_mapping: Dict, candidate_qualifications: Dict, original_header: str) -> tuple[str, str]:
"""Create the prompt for resume generation."""
system_prompt = """
You are a professional resume writer whose primary concern is FACTUAL ACCURACY. Your task is to create
a tailored resume that presents the candidate's actual qualifications in the most relevant way for this job,
using ONLY information that has been verified in the skills mapping.
## INSTRUCTIONS:
1. Use ONLY the information provided in the skills mapping JSON
2. Each skill, experience, or achievement you include MUST appear in either "direct_matches" or "transferable_skills"
3. DO NOT include skills listed in "missing_required_skills" or "missing_preferred_skills"
4. Format a professional resume with these sections:
- Header with name and contact information (exactly as provided in original resume)
- Professional Summary (focused on verified matching and transferable skills)
- Skills (ONLY from "direct_matches" and "transferable_skills" sections)
- Professional Experience (highlighting experiences referenced in the mapping)
- Education (exactly as listed in the candidate qualifications)
5. Follow these principles:
- Use the exact wording from "highlight_points" and "transferable_narratives" when describing experiences
- Maintain original job titles, companies, and dates exactly as provided
- Use achievement-oriented language that emphasizes results and impact
- Prioritize experiences that directly relate to the job requirements
## EVIDENCE REQUIREMENT:
For each skill or qualification you include in the resume, you MUST be able to trace it to:
1. A specific entry in "direct_matches" or "transferable_skills", AND
2. The original evidence citation in the candidate qualifications
If you cannot meet both these requirements for any content, DO NOT include it.
## FORMAT REQUIREMENTS:
- Create a clean, professional resume format
- Use consistent formatting for similar elements
- Ensure readability with appropriate white space
- Use bullet points for skills and achievements
- Include a final note: "Note: Initial draft of the resume was generated using the Backstory application written by James Ketrenos."
## FINAL VERIFICATION:
Before completing the resume:
1. Check that EVERY skill listed appears in either "direct_matches" or "transferable_skills"
2. Verify that no skills from "missing_required_skills" are included
3. Ensure all experience descriptions can be traced to evidence in candidate qualifications
4. Confirm that transferable skills are presented honestly without exaggeration
"""
prompt = f"Skills Mapping:\n{json.dumps(skills_mapping, indent=2)}\n\n"
prompt += f"Candidate Qualifications:\n{json.dumps(candidate_qualifications, indent=2)}\n\n"
prompt += f"Original Resume Header:\n{original_header}"
return system_prompt, prompt
async def generate_tailored_resume(self, message, skills_mapping: Dict, candidate_qualifications: Dict, original_header: str) -> AsyncGenerator[Message, None]:
"""Generate a tailored resume based on skills mapping."""
try:
system_prompt, prompt = self.create_resume_generation_prompt(skills_mapping, candidate_qualifications, original_header)
async for message in self.call_llm(message, system_prompt, prompt, temperature=0.4): # Slightly higher temperature for better writing
if message.status != "done":
yield message
if message.status == "error":
return
message.status = "done"
yield message
return
except Exception as e:
message.status = "error"
message.response = f"Error in resume generation: {str(e)}"
logger.error(message.response)
logger.error(traceback.format_exc())
yield message
raise
# Stage 3: Verification Implementation
def create_verification_prompt(self, generated_resume: str, skills_mapping: Dict, candidate_qualifications: Dict) -> tuple[str, str]:
"""Create the prompt for resume verification."""
system_prompt = """
You are a critical resume fact-checker responsible for verifying the accuracy of a tailored resume.
Your task is to identify and flag any fabricated or embellished information that does not appear in
the candidate's original materials.
## INSTRUCTIONS:
1. Compare the tailored resume against:
- The structured skills mapping
- The candidate's original qualifications
2. Perform a line-by-line verification focusing on:
- Skills claimed vs. skills verified in original materials
- Experience descriptions vs. actual documented experience
- Projects and achievements vs. documented accomplishments
- Technical knowledge claims vs. verified technical background
3. Create a verification report with these sections:
## OUTPUT FORMAT:
```json
{
"verification_results": {
"factual_accuracy": {
"status": "PASS/FAIL",
"issues": [
{
"claim": "The specific claim in the resume",
"issue": "Why this is problematic",
"source_check": "Result of checking against source materials",
"suggested_correction": "How to fix this issue"
}
]
},
"skill_verification": {
"status": "PASS/FAIL",
"unverified_skills": ["skill1", "skill2"]
},
"experience_verification": {
"status": "PASS/FAIL",
"problematic_statements": [
{
"statement": "The problematic experience statement",
"issue": "Why this is problematic",
"suggested_correction": "How to fix this issue"
}
]
},
"overall_assessment": "APPROVED/NEEDS REVISION",
"correction_instructions": "Specific instructions for correcting the resume"
}
}
```
## CRITICAL VERIFICATION CRITERIA:
1. Any skill mentioned in the resume MUST appear verbatim in the skills mapping
2. Any technology experience claimed MUST be explicitly documented in original materials
3. Role descriptions must not imply expertise with technologies not listed in original materials
4. "Transferable skills" must be reasonably transferable, not stretches or fabrications
5. Job titles, dates, and companies must match exactly with original materials
6. Professional summary must not imply experience with technologies from the job description that aren't in the candidate's background
## SPECIAL ATTENTION:
Pay particular attention to subtle fabrications such as:
- Vague wording that implies experience ("worked with", "familiar with", "utilized") with technologies not in original materials
- Reframing unrelated experience to falsely imply relevance to the job requirements
- Adding technologies to project descriptions that weren't mentioned in the original materials
- Exaggerating level of involvement or responsibility in projects or roles
- Creating achievements that weren't documented in the original materials
"""
prompt = f"Tailored Resume:\n{generated_resume}\n\n"
prompt += f"Skills Mapping:\n{json.dumps(skills_mapping, indent=2)}\n\n"
prompt += f"Candidate Qualifications:\n{json.dumps(candidate_qualifications, indent=2)}"
return system_prompt, prompt
async def verify_resume(self, message: Message, generated_resume: str, skills_mapping: Dict, candidate_qualifications: Dict) -> AsyncGenerator[Message, None]:
"""Verify the generated resume for accuracy against original materials."""
try:
system_prompt, prompt = self.create_verification_prompt(generated_resume, skills_mapping, candidate_qualifications)
async for message in self.call_llm(message, system_prompt, prompt):
if message.status != "done":
yield message
# Extract JSON from response
json_str = self.extract_json_from_text(message.response)
verification_results = json.loads(json_str)
message.status = "done"
message.response = json_str
yield message
return
except Exception as e:
message.status = "error"
message.response = f"Error in resume verification: {str(e)}"
logger.error(message.response)
logger.error(traceback.format_exc())
yield message
raise
async def correct_resume_issues(self, message: Message, generated_resume: str, verification_results: Dict, skills_mapping: Dict, candidate_qualifications: Dict, original_header: str) -> AsyncGenerator[Message, None]:
"""Correct issues in the resume based on verification results."""
if verification_results["verification_results"]["overall_assessment"] == "APPROVED":
message.status = "done"
message.status = generated_resume
yield message
return
system_prompt = """
You are a professional resume editor with a focus on factual accuracy. Your task is to correct
the identified issues in a tailored resume according to the verification report.
## INSTRUCTIONS:
1. Make ONLY the changes specified in the verification report
2. Ensure all corrections maintain factual accuracy based on the skills mapping
3. Do not introduce any new claims or skills not present in the verification data
4. Maintain the original format and structure of the resume
## PROCESS:
1. For each issue in the verification report:
- Identify the problematic text in the resume
- Replace it with the suggested correction
- Ensure the correction is consistent with the rest of the resume
2. After making all corrections:
- Review the revised resume for consistency
- Ensure no factual inaccuracies have been introduced
- Check that all formatting remains professional
Return the fully corrected resume.
"""
prompt = f"Original Resume:\n{generated_resume}\n\n"
prompt += f"Verification Results:\n{json.dumps(verification_results, indent=2)}\n\n"
prompt += f"Skills Mapping:\n{json.dumps(skills_mapping, indent=2)}\n\n"
prompt += f"Candidate Qualifications:\n{json.dumps(candidate_qualifications, indent=2)}\n\n"
prompt += f"Original Resume Header:\n{original_header}"
try:
async for message in self.call_llm(message, prompt, system_prompt, temperature=0.3):
if message.status != "done":
yield message
yield message
if message.status == "error":
return
except Exception as e:
message.status = "error"
message.response = f"Error in resume correction: {str(e)}"
logger.error(message.response)
logger.error(traceback.format_exc())
yield message
raise
async def generate_factual_tailored_resume(self, message: Message, job_description: str, resume: str, additional_context: str = "") -> AsyncGenerator[Message, None]:
"""
Main function to generate a factually accurate tailored resume.
Args:
job_description: The job description text
resume: The candidate's original resume text
additional_context: Any additional context about the candidate (optional)
Returns:
Dict containing the generated resume and supporting analysis
"""
try:
message.status = "thinking"
message.response = "Starting multi-stage RAG resume generation process"
logger.info(message.response)
yield message
# Stage 1A: Analyze job requirements
message.response = "Stage 1A: Analyzing job requirements"
logger.info(message.response)
yield message
async for message in self.analyze_job_requirements(message, job_description):
if message.status != "done":
yield message
if message.status == "error":
return
job_requirements = json.loads(message.response)
# Stage 1B: Analyze candidate qualifications
message.status = "thinking"
message.response = "Stage 1B: Analyzing candidate qualifications"
logger.info(message.response)
yield message
async for message in self.analyze_candidate_qualifications(message, resume, additional_context):
if message.status != "done":
yield message
if message.status == "error":
return
candidate_qualifications = json.loads(message.response)
# Stage 1C: Create skills mapping
message.status = "thinking"
message.response = "Stage 1C: Creating skills mapping"
logger.info(message.response)
yield message
async for message in self.create_skills_mapping(message, job_requirements, candidate_qualifications):
if message.status != "done":
yield message
if message.status == "error":
return
skills_mapping = json.loads(message.response)
# Extract header from original resume
original_header = self.extract_header_from_resume(resume)
# Stage 2: Generate tailored resume
message.status = "thinking"
message.response = "Stage 2: Generating tailored resume"
logger.info(message.response)
yield message
async for message in self.generate_tailored_resume(message, skills_mapping, candidate_qualifications, original_header):
if message.status != "done":
yield message
if message.status == "error":
return
generated_resume = message.response
# Stage 3: Verify resume
message.status = "thinking"
message.response = "Stage 3: Verifying resume for accuracy"
logger.info(message.response)
yield message
async for message in self.verify_resume(message, generated_resume, skills_mapping, candidate_qualifications):
if message.status != "done":
yield message
if message.status == "error":
return
verification_results = json.loads(message.response)
# Handle corrections if needed
if verification_results["verification_results"]["overall_assessment"] == "NEEDS REVISION":
message.status = "thinking"
message.response = "Correcting issues found in verification"
logger.info(message.response)
yield message
async for message in self.correct_resume_issues(
message=message,
generated_resume=generated_resume,
verification_results=verification_results,
skills_mapping=skills_mapping,
candidate_qualifications=candidate_qualifications,
original_header=original_header
):
if message.status != "done":
yield message
if message.status == "error":
return
generated_resume = message.response
# Re-verify after corrections
message.status = "thinking"
message.response = "Re-verifying corrected resume"
logger.info(message.response)
async for message in self.verify_resume(
message=message,
generated_resume=generated_resume,
skills_mapping=skills_mapping,
candidate_qualifications=candidate_qualifications):
if message.status != "done":
yield message
if message.status == "error":
return
# Return the final results
message.status = "done"
message.response = json.dumps({
"job_requirements": job_requirements,
"candidate_qualifications": candidate_qualifications,
"skills_mapping": skills_mapping,
"generated_resume": generated_resume,
"verification_results": verification_results
})
yield message
logger.info("Resume generation process completed successfully")
return
except Exception as e:
message.status = "error"
logger.info(message.response)
message.response = f"Error in resume generation process: {str(e)}"
logger.error(message.response)
logger.error(traceback.format_exc())
yield message
raise
# Main orchestration function
async def generate_llm_response(self, llm: Any, model: str, message: Message, temperature=0.7) -> AsyncGenerator[Message, None]:
logger.info(f"{self.agent_type} - {inspect.stack()[0].function}")
self.llm = llm
self.model = model
self.metrics.generate_count.labels(agent=self.agent_type).inc()
with self.metrics.generate_duration.labels(agent=self.agent_type).time():
job_description = "You write C and C++ code for 4 years."
resume = "I have worked on Cobol and QuickBasic for 18 years."
additional_context = ""
async for message in self.generate_factual_tailored_resume(message=message, job_description=job_description, resume=resume, additional_context=additional_context):
if message.status != "done":
yield message
yield message
return
# Register the base agent
agent_registry.register(JobDescription._agent_type, JobDescription)