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 { 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>
|
||||||
|
@ -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 {
|
||||||
|
@ -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.")
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user