Job analysis is working
This commit is contained in:
parent
d9a0267cfa
commit
a912e4d24c
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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.")
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user