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 theme = useTheme();
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 [creatingSession, setCreatingSession] = 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);
if (msg.type === "response") {
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 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 => {
if (incoming[l]) {
incoming[l].forEach((s: string) => requirements.push(s));
['softSkills', 'experience', 'education', 'certifications', 'preferredAttributes'].forEach(domain => {
if (incoming[domain]) {
incoming[domain].forEach((s: string) => requirements.push({ requirement: s, domain: domain }));
}
});
// Initialize skill matches with pending status
const initialSkillMatches = requirements.map(req => ({
requirement: req,
requirement: req.requirement,
domain: req.domain,
status: 'pending' as const,
matchScore: 0,
assessment: '',
description: '',
citations: []
}));
@ -167,8 +168,27 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
// Process requirements one by one
for (let i = 0; i < requirements.length; i++) {
try {
const match: SkillMatch = await apiClient.candidateMatchForRequirement(candidate.id || '', requirements[i]);
console.log(match);
const result: any = await apiClient.candidateMatchForRequirement(candidate.id || '', requirements[i].requirement);
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 => {
const updated = [...prev];
updated[i] = match;
@ -341,9 +361,14 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
}}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{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}
</Typography>
<Typography variant="caption" sx={{ ml: 1, fontWeight: 'light' }}>
{match.domain}
</Typography>
</Box>
</Box>
{match.status === 'complete' ? (
@ -356,6 +381,12 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
minWidth: 90
}}
/>
) : match.status === 'waiting' ? (
<Chip
label="Waiting..."
size="small"
sx={{ bgcolor: "rgb(189, 173, 85)", color: 'white', minWidth: 90 }}
/>
) : match.status === 'pending' ? (
<Chip
label="Analyzing..."
@ -387,7 +418,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
) : (
<Box>
<Typography variant="h6" gutterBottom>
Assessment:
Assessment
</Typography>
<Typography paragraph sx={{ mb: 3 }}>
@ -395,9 +426,8 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
</Typography>
<Typography variant="h6" gutterBottom>
Supporting Evidence:
</Typography>
Supporting Evidence
</Typography>
{match.citations && match.citations.length > 0 ? (
match.citations.map((citation, citIndex) => (
<Card
@ -413,17 +443,20 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
<Typography variant="body1" component="div" sx={{ mb: 1, fontStyle: 'italic' }}>
"{citation.text}"
</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">
Source: {citation.source}
</Typography>
<Chip
{/* <Chip
size="small"
label={`Relevance: ${citation.relevance}%`}
sx={{
bgcolor: theme.palette.grey[200],
}}
/>
/> */}
</Box>
</CardContent>
</Card>
@ -433,6 +466,13 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
No specific evidence found in candidate's profile.
</Typography>
)}
<Typography variant="h6" gutterBottom>
Skill description
</Typography>
<Typography paragraph>
{match.description}
</Typography>
</Box>
)}
</AccordionDetails>

View File

@ -1,6 +1,6 @@
// Generated TypeScript types from Pydantic models
// 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
// ============================
@ -49,7 +49,7 @@ export type SearchType = "similarity" | "mmr" | "hybrid" | "keyword";
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";
@ -391,6 +391,7 @@ export interface ChromaDBGetResponse {
export interface Citation {
text: string;
source: string;
context: string;
relevance: number;
}
@ -835,10 +836,12 @@ export interface SkillAssessment {
export interface SkillMatch {
requirement: string;
status: "pending" | "complete" | "error";
domain: string;
status: "pending" | "complete" | "waiting" | "error";
matchScore: number;
assessment: string;
citations?: Array<Citation>;
description: string;
}
export interface SocialLink {

View File

@ -73,7 +73,8 @@ a SPECIFIC skill based solely on their resume and supporting evidence.
"skill": "{skill}",
"evidence_found": true/false,
"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": [
{{
"source": "resume section/position/project",
@ -182,20 +183,9 @@ JSON RESPONSE:"""
return
json_str = self.extract_json_from_text(skill_assessment.content)
skill_match = json_str#: SkillMatch | None = None
skill_assessment_data = ""
try:
skill_assessment_data = json.loads(json_str)
match_level = (
skill_assessment_data
.get("skill_assessment", {})
.get("evidence_strength", "UNKNOWN")
)
skill_description = (
skill_assessment_data
.get("skill_assessment", {})
.get("description", "")
)
skill_assessment_data = json.loads(json_str).get("skill_assessment", {})
except json.JSONDecodeError as e:
status_message.status = ChatStatusType.ERROR
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}")
yield status_message
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.type = ChatMessageType.RESPONSE
status_message.content = skill_match
status_message.content = json.dumps(skill_assessment_data)
yield status_message
logger.info(f"✅ Skill assessment completed successfully.")

View File

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

View File

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