Job analysis is working

This commit is contained in:
James Ketr 2025-06-03 22:30:09 -07:00
parent d9a0267cfa
commit a912e4d24c
5 changed files with 76 additions and 45 deletions

View File

@ -49,7 +49,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
const { apiClient } = useAuth(); const { apiClient } = useAuth();
const theme = useTheme(); const theme = useTheme();
const [jobRequirements, setJobRequirements] = useState<JobRequirements | null>(null); const [jobRequirements, setJobRequirements] = useState<JobRequirements | null>(null);
const [requirements, setRequirements] = useState<string[]>([]); const [requirements, setRequirements] = useState<{ requirement: string, domain: string }[]>([]);
const [skillMatches, setSkillMatches] = useState<SkillMatch[]>([]); const [skillMatches, setSkillMatches] = useState<SkillMatch[]>([]);
const [creatingSession, setCreatingSession] = useState<boolean>(false); const [creatingSession, setCreatingSession] = useState<boolean>(false);
const [loadingRequirements, setLoadingRequirements] = useState<boolean>(false); const [loadingRequirements, setLoadingRequirements] = useState<boolean>(false);
@ -102,23 +102,24 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
console.log(`onMessage: ${msg.type}`, msg); console.log(`onMessage: ${msg.type}`, msg);
if (msg.type === "response") { if (msg.type === "response") {
const incoming: any = toCamelCase<JobRequirements>(JSON.parse(msg.content || '')); const incoming: any = toCamelCase<JobRequirements>(JSON.parse(msg.content || ''));
const requirements: string[] = ['technicalSkills', 'experienceRequirements'].flatMap((type) => { const requirements: { requirement: string, domain: string }[] = ['technicalSkills', 'experienceRequirements'].flatMap((domain) => {
return ['required', 'preferred'].flatMap((level) => { return ['required', 'preferred'].flatMap((level) => {
return incoming[type][level].map((s: string) => s); return incoming[domain][level].map((s: string) => { return { requirement: s, domain: domain }; });
}) })
}); });
['softSkills', 'experience', 'education', 'certifications', 'preferredAttributes'].forEach(l => { ['softSkills', 'experience', 'education', 'certifications', 'preferredAttributes'].forEach(domain => {
if (incoming[l]) { if (incoming[domain]) {
incoming[l].forEach((s: string) => requirements.push(s)); incoming[domain].forEach((s: string) => requirements.push({ requirement: s, domain: domain }));
} }
}); });
// Initialize skill matches with pending status
const initialSkillMatches = requirements.map(req => ({ const initialSkillMatches = requirements.map(req => ({
requirement: req, requirement: req.requirement,
domain: req.domain,
status: 'pending' as const, status: 'pending' as const,
matchScore: 0, matchScore: 0,
assessment: '', assessment: '',
description: '',
citations: [] citations: []
})); }));
@ -167,8 +168,27 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
// Process requirements one by one // Process requirements one by one
for (let i = 0; i < requirements.length; i++) { for (let i = 0; i < requirements.length; i++) {
try { try {
const match: SkillMatch = await apiClient.candidateMatchForRequirement(candidate.id || '', requirements[i]); const result: any = await apiClient.candidateMatchForRequirement(candidate.id || '', requirements[i].requirement);
console.log(match); const skillMatch = result.skillMatch;
let matchScore: number = 0;
switch (skillMatch.evidenceStrength) {
case "STRONG": matchScore = 100; break;
case "MODERATE": matchScore = 75; break;
case "WEAK": matchScore = 50; break;
case "NONE": matchScore = 0; break;
}
if (skillMatch.evidenceStrength == "NONE" && skillMatch.citations.length > 3) {
matchScore = Math.min(skillMatch.citations.length * 8, 40);
}
const match: SkillMatch = {
status: "complete",
matchScore,
domain: requirements[i].domain,
requirement: skillMatch.skill,
assessment: skillMatch.assessment,
citations: skillMatch.evidenceDetails.map((evidence: any) => { return { source: evidence.source, text: evidence.quote, context: evidence.context } }),
description: skillMatch.description
};
setSkillMatches(prev => { setSkillMatches(prev => {
const updated = [...prev]; const updated = [...prev];
updated[i] = match; updated[i] = match;
@ -341,9 +361,14 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
}}> }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}> <Box sx={{ display: 'flex', alignItems: 'center' }}>
{getStatusIcon(match.status, match.matchScore)} {getStatusIcon(match.status, match.matchScore)}
<Typography sx={{ ml: 1, fontWeight: 'medium' }}> <Box sx={{ display: "flex", flexDirection: "column", gap: 0, p: 0, m: 0 }}>
<Typography sx={{ ml: 1, mb: 0, fontWeight: 'medium', marginBottom: "0px !important" }}>
{match.requirement} {match.requirement}
</Typography> </Typography>
<Typography variant="caption" sx={{ ml: 1, fontWeight: 'light' }}>
{match.domain}
</Typography>
</Box>
</Box> </Box>
{match.status === 'complete' ? ( {match.status === 'complete' ? (
@ -356,6 +381,12 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
minWidth: 90 minWidth: 90
}} }}
/> />
) : match.status === 'waiting' ? (
<Chip
label="Waiting..."
size="small"
sx={{ bgcolor: "rgb(189, 173, 85)", color: 'white', minWidth: 90 }}
/>
) : match.status === 'pending' ? ( ) : match.status === 'pending' ? (
<Chip <Chip
label="Analyzing..." label="Analyzing..."
@ -387,7 +418,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
) : ( ) : (
<Box> <Box>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
Assessment: Assessment
</Typography> </Typography>
<Typography paragraph sx={{ mb: 3 }}> <Typography paragraph sx={{ mb: 3 }}>
@ -395,9 +426,8 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
</Typography> </Typography>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
Supporting Evidence: Supporting Evidence
</Typography> </Typography>
{match.citations && match.citations.length > 0 ? ( {match.citations && match.citations.length > 0 ? (
match.citations.map((citation, citIndex) => ( match.citations.map((citation, citIndex) => (
<Card <Card
@ -413,17 +443,20 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
<Typography variant="body1" component="div" sx={{ mb: 1, fontStyle: 'italic' }}> <Typography variant="body1" component="div" sx={{ mb: 1, fontStyle: 'italic' }}>
"{citation.text}" "{citation.text}"
</Typography> </Typography>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', flexDirection: "column" }}>
<Typography variant="body2" color="text.secondary">
Relevance: {citation.context}
</Typography>
<Typography variant="caption" color="text.secondary"> <Typography variant="caption" color="text.secondary">
Source: {citation.source} Source: {citation.source}
</Typography> </Typography>
<Chip {/* <Chip
size="small" size="small"
label={`Relevance: ${citation.relevance}%`} label={`Relevance: ${citation.relevance}%`}
sx={{ sx={{
bgcolor: theme.palette.grey[200], bgcolor: theme.palette.grey[200],
}} }}
/> /> */}
</Box> </Box>
</CardContent> </CardContent>
</Card> </Card>
@ -433,6 +466,13 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
No specific evidence found in candidate's profile. No specific evidence found in candidate's profile.
</Typography> </Typography>
)} )}
<Typography variant="h6" gutterBottom>
Skill description
</Typography>
<Typography paragraph>
{match.description}
</Typography>
</Box> </Box>
)} )}
</AccordionDetails> </AccordionDetails>

View File

@ -1,6 +1,6 @@
// Generated TypeScript types from Pydantic models // Generated TypeScript types from Pydantic models
// Source: src/backend/models.py // Source: src/backend/models.py
// Generated on: 2025-06-04T03:59:11.250216 // Generated on: 2025-06-04T05:16:43.020718
// DO NOT EDIT MANUALLY - This file is auto-generated // DO NOT EDIT MANUALLY - This file is auto-generated
// ============================ // ============================
@ -49,7 +49,7 @@ export type SearchType = "similarity" | "mmr" | "hybrid" | "keyword";
export type SkillLevel = "beginner" | "intermediate" | "advanced" | "expert"; export type SkillLevel = "beginner" | "intermediate" | "advanced" | "expert";
export type SkillStatus = "pending" | "complete" | "error"; export type SkillStatus = "pending" | "complete" | "waiting" | "error";
export type SocialPlatform = "linkedin" | "twitter" | "github" | "dribbble" | "behance" | "website" | "other"; export type SocialPlatform = "linkedin" | "twitter" | "github" | "dribbble" | "behance" | "website" | "other";
@ -391,6 +391,7 @@ export interface ChromaDBGetResponse {
export interface Citation { export interface Citation {
text: string; text: string;
source: string; source: string;
context: string;
relevance: number; relevance: number;
} }
@ -835,10 +836,12 @@ export interface SkillAssessment {
export interface SkillMatch { export interface SkillMatch {
requirement: string; requirement: string;
status: "pending" | "complete" | "error"; domain: string;
status: "pending" | "complete" | "waiting" | "error";
matchScore: number; matchScore: number;
assessment: string; assessment: string;
citations?: Array<Citation>; citations?: Array<Citation>;
description: string;
} }
export interface SocialLink { export interface SocialLink {

View File

@ -73,7 +73,8 @@ a SPECIFIC skill based solely on their resume and supporting evidence.
"skill": "{skill}", "skill": "{skill}",
"evidence_found": true/false, "evidence_found": true/false,
"evidence_strength": "STRONG/MODERATE/WEAK/NONE", "evidence_strength": "STRONG/MODERATE/WEAK/NONE",
"description": "short (two to three sentence) description of what {skill} means with a concise example of what you're looking for", "assessment": "short (one to two sentence) assessment of the candidate's proficiency with {skill}",
"description": "short (two to three sentence) description of what the {skill} is, independent of whether the candidate has that skill or not",
"evidence_details": [ "evidence_details": [
{{ {{
"source": "resume section/position/project", "source": "resume section/position/project",
@ -182,20 +183,9 @@ JSON RESPONSE:"""
return return
json_str = self.extract_json_from_text(skill_assessment.content) json_str = self.extract_json_from_text(skill_assessment.content)
skill_match = json_str#: SkillMatch | None = None
skill_assessment_data = "" skill_assessment_data = ""
try: try:
skill_assessment_data = json.loads(json_str) skill_assessment_data = json.loads(json_str).get("skill_assessment", {})
match_level = (
skill_assessment_data
.get("skill_assessment", {})
.get("evidence_strength", "UNKNOWN")
)
skill_description = (
skill_assessment_data
.get("skill_assessment", {})
.get("description", "")
)
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
status_message.status = ChatStatusType.ERROR status_message.status = ChatStatusType.ERROR
status_message.content = f"Failed to parse Skill assessment JSON: {str(e)}\n\n{skill_assessment_data}" status_message.content = f"Failed to parse Skill assessment JSON: {str(e)}\n\n{skill_assessment_data}"
@ -215,15 +205,9 @@ JSON RESPONSE:"""
logger.error(f"⚠️ {status_message.content}") logger.error(f"⚠️ {status_message.content}")
yield status_message yield status_message
return return
if skill_match is None:
status_message.status = ChatStatusType.ERROR
status_message.content = "Skill assessment analysis failed to produce valid data."
logger.error(f"⚠️ {status_message.content}")
yield status_message
return
status_message.status = ChatStatusType.DONE status_message.status = ChatStatusType.DONE
status_message.type = ChatMessageType.RESPONSE status_message.type = ChatMessageType.RESPONSE
status_message.content = skill_match status_message.content = json.dumps(skill_assessment_data)
yield status_message yield status_message
logger.info(f"✅ Skill assessment completed successfully.") logger.info(f"✅ Skill assessment completed successfully.")

View File

@ -3395,8 +3395,8 @@ async def get_candidate_skill_match(
status_code=500, status_code=500,
content=create_error_response("NO_MATCH", "No skill match found for the given requirement") content=create_error_response("NO_MATCH", "No skill match found for the given requirement")
) )
skill_match = skill_match.content.strip() skill_match = json.loads(skill_match.content)
logger.info(f"✅ Skill match found for candidate {candidate.id}: {skill_match}") logger.info(f"✅ Skill match found for candidate {candidate.id}: {skill_match['evidence_strength']}")
return create_success_response({ return create_success_response({
"candidateId": candidate.id, "candidateId": candidate.id,

View File

@ -87,19 +87,23 @@ class Requirements(BaseModel):
class Citation(BaseModel): class Citation(BaseModel):
text: str text: str
source: str source: str
context: str
relevance: int # 0-100 scale relevance: int # 0-100 scale
class SkillStatus(str, Enum): class SkillStatus(str, Enum):
PENDING = "pending" PENDING = "pending"
COMPLETE = "complete" COMPLETE = "complete"
WAITING = "waiting"
ERROR = "error" ERROR = "error"
class SkillMatch(BaseModel): class SkillMatch(BaseModel):
requirement: str requirement: str
domain: str
status: SkillStatus status: SkillStatus
match_score: int = Field(..., alias='matchScore') match_score: int = Field(..., alias='matchScore')
assessment: str assessment: str
citations: List[Citation] = Field(default_factory=list) citations: List[Citation] = Field(default_factory=list)
description: str
model_config = { model_config = {
"populate_by_name": True # Allow both field names and aliases "populate_by_name": True # Allow both field names and aliases
} }