From a912e4d24c6168d012c99ed96237fc06f54aee28 Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Tue, 3 Jun 2025 22:30:09 -0700 Subject: [PATCH] Job analysis is working --- frontend/src/components/JobMatchAnalysis.tsx | 80 +++++++++++++++----- frontend/src/types/types.ts | 9 ++- src/backend/agents/skill_match.py | 24 +----- src/backend/main.py | 4 +- src/backend/models.py | 4 + 5 files changed, 76 insertions(+), 45 deletions(-) diff --git a/frontend/src/components/JobMatchAnalysis.tsx b/frontend/src/components/JobMatchAnalysis.tsx index 5c8b6f0..494382f 100644 --- a/frontend/src/components/JobMatchAnalysis.tsx +++ b/frontend/src/components/JobMatchAnalysis.tsx @@ -49,7 +49,7 @@ const JobMatchAnalysis: React.FC = (props: JobAnalysisProps) = const { apiClient } = useAuth(); const theme = useTheme(); const [jobRequirements, setJobRequirements] = useState(null); - const [requirements, setRequirements] = useState([]); + const [requirements, setRequirements] = useState<{ requirement: string, domain: string }[]>([]); const [skillMatches, setSkillMatches] = useState([]); const [creatingSession, setCreatingSession] = useState(false); const [loadingRequirements, setLoadingRequirements] = useState(false); @@ -102,23 +102,24 @@ const JobMatchAnalysis: React.FC = (props: JobAnalysisProps) = console.log(`onMessage: ${msg.type}`, msg); if (msg.type === "response") { const incoming: any = toCamelCase(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 = (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 = (props: JobAnalysisProps) = }}> {getStatusIcon(match.status, match.matchScore)} - + + {match.requirement} + + {match.domain} + + {match.status === 'complete' ? ( @@ -356,6 +381,12 @@ const JobMatchAnalysis: React.FC = (props: JobAnalysisProps) = minWidth: 90 }} /> + ) : match.status === 'waiting' ? ( + ) : match.status === 'pending' ? ( = (props: JobAnalysisProps) = ) : ( - Assessment: + Assessment @@ -395,9 +426,8 @@ const JobMatchAnalysis: React.FC = (props: JobAnalysisProps) = - Supporting Evidence: - - + Supporting Evidence + {match.citations && match.citations.length > 0 ? ( match.citations.map((citation, citIndex) => ( = (props: JobAnalysisProps) = "{citation.text}" - + + + Relevance: {citation.context} + Source: {citation.source} - + /> */} @@ -433,6 +466,13 @@ const JobMatchAnalysis: React.FC = (props: JobAnalysisProps) = No specific evidence found in candidate's profile. )} + + Skill description + + + {match.description} + + )} diff --git a/frontend/src/types/types.ts b/frontend/src/types/types.ts index 0915ac9..1153a37 100644 --- a/frontend/src/types/types.ts +++ b/frontend/src/types/types.ts @@ -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; + description: string; } export interface SocialLink { diff --git a/src/backend/agents/skill_match.py b/src/backend/agents/skill_match.py index 092ac99..05bca65 100644 --- a/src/backend/agents/skill_match.py +++ b/src/backend/agents/skill_match.py @@ -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.") diff --git a/src/backend/main.py b/src/backend/main.py index 87d1be7..00c67a6 100644 --- a/src/backend/main.py +++ b/src/backend/main.py @@ -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, diff --git a/src/backend/models.py b/src/backend/models.py index 90aeaba..bf99e2d 100644 --- a/src/backend/models.py +++ b/src/backend/models.py @@ -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 }