Refresh maintains selected entities
This commit is contained in:
parent
a912e4d24c
commit
4f7b2f3e6a
@ -125,7 +125,6 @@ services:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: ollama
|
||||
#image: ollama
|
||||
container_name: ollama
|
||||
restart: "always"
|
||||
env_file:
|
||||
|
@ -24,12 +24,7 @@ import { Candidate, ChatMessage, ChatMessageBase, ChatMessageUser, ChatSession,
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
import { BackstoryPageProps } from './BackstoryTab';
|
||||
import { toCamelCase } from 'types/conversion';
|
||||
|
||||
|
||||
interface Job {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
import { Job } from 'types/types';
|
||||
|
||||
interface JobAnalysisProps extends BackstoryPageProps {
|
||||
job: Job;
|
||||
@ -48,7 +43,6 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
||||
} = props
|
||||
const { apiClient } = useAuth();
|
||||
const theme = useTheme();
|
||||
const [jobRequirements, setJobRequirements] = useState<JobRequirements | null>(null);
|
||||
const [requirements, setRequirements] = useState<{ requirement: string, domain: string }[]>([]);
|
||||
const [skillMatches, setSkillMatches] = useState<SkillMatch[]>([]);
|
||||
const [creatingSession, setCreatingSession] = useState<boolean>(false);
|
||||
@ -89,7 +83,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
||||
|
||||
// Fetch initial requirements
|
||||
useEffect(() => {
|
||||
if (!job.description || !requirementsSession || loadingRequirements || jobRequirements) {
|
||||
if (!job.description || !requirementsSession || loadingRequirements) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -101,22 +95,36 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
||||
onMessage: (msg: ChatMessage) => {
|
||||
console.log(`onMessage: ${msg.type}`, msg);
|
||||
if (msg.type === "response") {
|
||||
const incoming: any = toCamelCase<JobRequirements>(JSON.parse(msg.content || ''));
|
||||
const requirements: { requirement: string, domain: string }[] = ['technicalSkills', 'experienceRequirements'].flatMap((domain) => {
|
||||
return ['required', 'preferred'].flatMap((level) => {
|
||||
return incoming[domain][level].map((s: string) => { return { requirement: s, domain: domain }; });
|
||||
})
|
||||
});
|
||||
['softSkills', 'experience', 'education', 'certifications', 'preferredAttributes'].forEach(domain => {
|
||||
if (incoming[domain]) {
|
||||
incoming[domain].forEach((s: string) => requirements.push({ requirement: s, domain: domain }));
|
||||
}
|
||||
});
|
||||
const job: Job = toCamelCase<Job>(JSON.parse(msg.content || ''));
|
||||
const requirements: { requirement: string, domain: string }[] = [];
|
||||
if (job.requirements?.technicalSkills) {
|
||||
job.requirements.technicalSkills.required?.forEach(req => requirements.push({ requirement: req, domain: 'Technical Skills (required)' }));
|
||||
job.requirements.technicalSkills.preferred?.forEach(req => requirements.push({ requirement: req, domain: 'Technical Skills (preferred)' }));
|
||||
}
|
||||
if (job.requirements?.experienceRequirements) {
|
||||
job.requirements.experienceRequirements.required?.forEach(req => requirements.push({ requirement: req, domain: 'Experience (required)' }));
|
||||
job.requirements.experienceRequirements.preferred?.forEach(req => requirements.push({ requirement: req, domain: 'Experience (preferred)' }));
|
||||
}
|
||||
if (job.requirements?.softSkills) {
|
||||
job.requirements.softSkills.forEach(req => requirements.push({ requirement: req, domain: 'Soft Skills' }));
|
||||
}
|
||||
if (job.requirements?.experience) {
|
||||
job.requirements.experience.forEach(req => requirements.push({ requirement: req, domain: 'Experience' }));
|
||||
}
|
||||
if (job.requirements?.education) {
|
||||
job.requirements.education.forEach(req => requirements.push({ requirement: req, domain: 'Education' }));
|
||||
}
|
||||
if (job.requirements?.certifications) {
|
||||
job.requirements.certifications.forEach(req => requirements.push({ requirement: req, domain: 'Certifications' }));
|
||||
}
|
||||
if (job.requirements?.preferredAttributes) {
|
||||
job.requirements.preferredAttributes.forEach(req => requirements.push({ requirement: req, domain: 'Preferred Attributes' }));
|
||||
}
|
||||
|
||||
const initialSkillMatches = requirements.map(req => ({
|
||||
requirement: req.requirement,
|
||||
domain: req.domain,
|
||||
status: 'pending' as const,
|
||||
status: 'waiting' as const,
|
||||
matchScore: 0,
|
||||
assessment: '',
|
||||
description: '',
|
||||
@ -168,6 +176,12 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
||||
// Process requirements one by one
|
||||
for (let i = 0; i < requirements.length; i++) {
|
||||
try {
|
||||
setSkillMatches(prev => {
|
||||
const updated = [...prev];
|
||||
updated[i] = { ...updated[i], status: 'pending' };
|
||||
return updated;
|
||||
});
|
||||
|
||||
const result: any = await apiClient.candidateMatchForRequirement(candidate.id || '', requirements[i].requirement);
|
||||
const skillMatch = result.skillMatch;
|
||||
let matchScore: number = 0;
|
||||
@ -177,7 +191,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
||||
case "WEAK": matchScore = 50; break;
|
||||
case "NONE": matchScore = 0; break;
|
||||
}
|
||||
if (skillMatch.evidenceStrength == "NONE" && skillMatch.citations.length > 3) {
|
||||
if (skillMatch.evidenceStrength == "NONE" && skillMatch.citations && skillMatch.citations.length > 3) {
|
||||
matchScore = Math.min(skillMatch.citations.length * 8, 40);
|
||||
}
|
||||
const match: SkillMatch = {
|
||||
@ -234,7 +248,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
||||
|
||||
// Get icon based on status
|
||||
const getStatusIcon = (status: string, score: number) => {
|
||||
if (status === 'pending') return <PendingIcon />;
|
||||
if (status === 'pending' || status === 'waiting') return <PendingIcon />;
|
||||
if (status === 'error') return <ErrorIcon color="error" />;
|
||||
if (score >= 70) return <CheckCircleIcon color="success" />;
|
||||
if (score >= 40) return <WarningIcon color="warning" />;
|
||||
|
@ -1,5 +1,41 @@
|
||||
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
|
||||
import * as Types from 'types/types';
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
|
||||
// ============================
|
||||
// Local Storage Keys
|
||||
// ============================
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
SELECTED_CANDIDATE_ID: 'selectedCandidateId',
|
||||
SELECTED_JOB_ID: 'selectedJobId',
|
||||
SELECTED_EMPLOYER_ID: 'selectedEmployerId'
|
||||
} as const;
|
||||
|
||||
// ============================
|
||||
// Local Storage Utilities
|
||||
// ============================
|
||||
|
||||
function getStoredId(key: string): string | null {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
} catch (error) {
|
||||
console.warn('Failed to read from localStorage:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function setStoredId(key: string, id: string | null): void {
|
||||
try {
|
||||
if (id) {
|
||||
localStorage.setItem(key, id);
|
||||
} else {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to write to localStorage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================
|
||||
// App State Interface
|
||||
@ -9,6 +45,7 @@ export interface AppState {
|
||||
selectedCandidate: Types.Candidate | null;
|
||||
selectedJob: Types.Job | null;
|
||||
selectedEmployer: Types.Employer | null;
|
||||
isInitializing: boolean;
|
||||
// Add more global state as needed:
|
||||
// currentView: string;
|
||||
// filters: Record<string, any>;
|
||||
@ -30,13 +67,118 @@ export type AppStateContextType = AppState & AppStateActions;
|
||||
// ============================
|
||||
|
||||
export function useAppStateLogic(): AppStateContextType {
|
||||
const { apiClient } = useAuth();
|
||||
const [selectedCandidate, setSelectedCandidateState] = useState<Types.Candidate | null>(null);
|
||||
const [selectedJob, setSelectedJobState] = useState<Types.Job | null>(null);
|
||||
const [selectedEmployer, setSelectedEmployerState] = useState<Types.Employer | null>(null);
|
||||
const [isInitializing, setIsInitializing] = useState<boolean>(true);
|
||||
|
||||
// ============================
|
||||
// Initialization Effect
|
||||
// ============================
|
||||
|
||||
useEffect(() => {
|
||||
const initializeFromStorage = async () => {
|
||||
setIsInitializing(true);
|
||||
|
||||
try {
|
||||
// Get stored IDs
|
||||
const candidateId = getStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID);
|
||||
const jobId = getStoredId(STORAGE_KEYS.SELECTED_JOB_ID);
|
||||
const employerId = getStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID);
|
||||
|
||||
// Restore entities in parallel if IDs exist
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
if (candidateId) {
|
||||
promises.push(
|
||||
(async () => {
|
||||
try {
|
||||
// Assuming apiClient.getCandidate exists
|
||||
const candidate = await apiClient.getCandidate(candidateId);
|
||||
if (candidate) {
|
||||
setSelectedCandidateState(candidate);
|
||||
console.log('Restored candidate from storage:', candidate);
|
||||
} else {
|
||||
// Data not available, clear stored ID
|
||||
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, null);
|
||||
console.log('Candidate not found, cleared from storage');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to restore candidate:', error);
|
||||
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, null);
|
||||
}
|
||||
})()
|
||||
);
|
||||
}
|
||||
|
||||
if (jobId) {
|
||||
promises.push(
|
||||
(async () => {
|
||||
try {
|
||||
// Assuming apiClient.getJob exists
|
||||
const job = await apiClient.getJob(jobId);
|
||||
if (job) {
|
||||
setSelectedJobState(job);
|
||||
console.log('Restored job from storage:', job);
|
||||
} else {
|
||||
// Data not available, clear stored ID
|
||||
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null);
|
||||
console.log('Job not found, cleared from storage');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to restore job:', error);
|
||||
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null);
|
||||
}
|
||||
})()
|
||||
);
|
||||
}
|
||||
|
||||
if (employerId) {
|
||||
promises.push(
|
||||
(async () => {
|
||||
try {
|
||||
// Assuming apiClient.getEmployer exists
|
||||
const employer = await apiClient.getEmployer(employerId);
|
||||
if (employer) {
|
||||
setSelectedEmployerState(employer);
|
||||
console.log('Restored employer from storage:', employer);
|
||||
} else {
|
||||
// Data not available, clear stored ID
|
||||
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null);
|
||||
console.log('Employer not found, cleared from storage');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to restore employer:', error);
|
||||
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null);
|
||||
}
|
||||
})()
|
||||
);
|
||||
}
|
||||
|
||||
// Wait for all restoration attempts to complete
|
||||
await Promise.all(promises);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during app state initialization:', error);
|
||||
} finally {
|
||||
setIsInitializing(false);
|
||||
}
|
||||
};
|
||||
|
||||
initializeFromStorage();
|
||||
}, []);
|
||||
|
||||
// ============================
|
||||
// State Setters with Persistence
|
||||
// ============================
|
||||
|
||||
const setSelectedCandidate = useCallback((candidate: Types.Candidate | null) => {
|
||||
setSelectedCandidateState(candidate);
|
||||
|
||||
// Persist ID to localStorage
|
||||
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, candidate?.id || null);
|
||||
|
||||
if (candidate) {
|
||||
console.log('Selected candidate:', candidate);
|
||||
} else {
|
||||
@ -47,6 +189,9 @@ export function useAppStateLogic(): AppStateContextType {
|
||||
const setSelectedJob = useCallback((job: Types.Job | null) => {
|
||||
setSelectedJobState(job);
|
||||
|
||||
// Persist ID to localStorage
|
||||
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, job?.id || null);
|
||||
|
||||
if (job) {
|
||||
console.log('Selected job:', job);
|
||||
} else {
|
||||
@ -57,6 +202,9 @@ export function useAppStateLogic(): AppStateContextType {
|
||||
const setSelectedEmployer = useCallback((employer: Types.Employer | null) => {
|
||||
setSelectedEmployerState(employer);
|
||||
|
||||
// Persist ID to localStorage
|
||||
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, employer?.id || null);
|
||||
|
||||
if (employer) {
|
||||
console.log('Selected employer:', employer);
|
||||
} else {
|
||||
@ -68,6 +216,12 @@ export function useAppStateLogic(): AppStateContextType {
|
||||
setSelectedCandidateState(null);
|
||||
setSelectedJobState(null);
|
||||
setSelectedEmployerState(null);
|
||||
|
||||
// Clear all from localStorage
|
||||
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, null);
|
||||
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null);
|
||||
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null);
|
||||
|
||||
console.log('Cleared all selections');
|
||||
}, []);
|
||||
|
||||
@ -75,6 +229,7 @@ export function useAppStateLogic(): AppStateContextType {
|
||||
selectedCandidate,
|
||||
selectedJob,
|
||||
selectedEmployer,
|
||||
isInitializing,
|
||||
setSelectedCandidate,
|
||||
setSelectedJob,
|
||||
setSelectedEmployer,
|
||||
@ -135,6 +290,14 @@ export function useSelectedEmployer() {
|
||||
return { selectedEmployer, setSelectedEmployer };
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to check if the app is still initializing
|
||||
*/
|
||||
export function useAppInitializing() {
|
||||
const { isInitializing } = useAppState();
|
||||
return isInitializing;
|
||||
}
|
||||
|
||||
// ============================
|
||||
// Development Utilities
|
||||
// ============================
|
||||
@ -148,12 +311,23 @@ export function useAppStateDebug() {
|
||||
useEffect(() => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.group('🔍 App State Debug');
|
||||
console.log('Is Initializing:', appState.isInitializing);
|
||||
console.log('Selected Candidate:', appState.selectedCandidate);
|
||||
console.log('Selected Job:', appState.selectedJob);
|
||||
console.log('Selected Employer:', appState.selectedEmployer);
|
||||
console.groupEnd();
|
||||
}
|
||||
}, [appState.selectedCandidate, appState.selectedJob, appState.selectedEmployer]);
|
||||
}, [appState.selectedCandidate, appState.selectedJob, appState.selectedEmployer, appState.isInitializing]);
|
||||
|
||||
return appState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to manually clear all localStorage for this app (development/debugging)
|
||||
*/
|
||||
export function clearAppLocalStorage() {
|
||||
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, null);
|
||||
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null);
|
||||
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null);
|
||||
console.log('Cleared all app localStorage');
|
||||
}
|
@ -46,6 +46,7 @@ import { ComingSoon } from 'components/ui/ComingSoon';
|
||||
const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
||||
const theme = useTheme();
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate()
|
||||
const { setSnack, submitQuery } = props;
|
||||
const backstoryProps = { setSnack, submitQuery };
|
||||
@ -53,6 +54,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [jobDescription, setJobDescription] = useState('');
|
||||
const [jobTitle, setJobTitle] = useState('');
|
||||
const [company, setCompany] = useState('');
|
||||
const [jobLocation, setJobLocation] = useState('');
|
||||
const [analysisStarted, setAnalysisStarted] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@ -60,6 +62,9 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
const { apiClient } = useAuth();
|
||||
const [candidates, setCandidates] = useState<Candidate[] | null>(null);
|
||||
|
||||
const user_type = user?.userType || 'guest';
|
||||
const user_id = user?.id || '';
|
||||
|
||||
useEffect(() => {
|
||||
if (candidates !== null || selectedCandidate) {
|
||||
return;
|
||||
@ -96,7 +101,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
|
||||
// Steps in our process
|
||||
const steps = [
|
||||
{ index: 1, label: 'Job Description', icon: <WorkIcon /> },
|
||||
{ index: 1, label: 'Job Selection', icon: <WorkIcon /> },
|
||||
{ index: 2, label: 'AI Analysis', icon: <WorkIcon /> },
|
||||
{ index: 3, label: 'Generated Resume', icon: <AssessmentIcon /> }
|
||||
];
|
||||
@ -104,124 +109,6 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
steps.unshift({ index: 0, label: 'Select Candidate', icon: <PersonIcon /> })
|
||||
}
|
||||
|
||||
const fetchMatchForRequirement = async (requirement: string): Promise<any> => {
|
||||
// Create different mock responses based on the requirement
|
||||
const mockResponses: Record<string, any> = {
|
||||
"5+ years of React development experience": {
|
||||
requirement: "5+ years of React development experience",
|
||||
status: "complete",
|
||||
matchScore: 85,
|
||||
assessment: "The candidate demonstrates extensive React experience spanning over 6 years, with a strong portfolio of complex applications and deep understanding of React's component lifecycle and hooks.",
|
||||
citations: [
|
||||
{
|
||||
text: "Led frontend development team of 5 engineers to rebuild our customer portal using React and TypeScript, resulting in 40% improved performance and 30% reduction in bugs.",
|
||||
source: "Resume, Work Experience",
|
||||
relevance: 95
|
||||
},
|
||||
{
|
||||
text: "Developed and maintained reusable React component library used across 12 different products within the organization.",
|
||||
source: "Resume, Work Experience",
|
||||
relevance: 90
|
||||
},
|
||||
{
|
||||
text: "I've been working with React since 2017, building everything from small widgets to enterprise applications.",
|
||||
source: "Cover Letter",
|
||||
relevance: 85
|
||||
}
|
||||
]
|
||||
},
|
||||
"Strong TypeScript skills": {
|
||||
requirement: "Strong TypeScript skills",
|
||||
status: "complete",
|
||||
matchScore: 90,
|
||||
assessment: "The candidate shows excellent TypeScript proficiency through their work history and personal projects. They have implemented complex type systems and demonstrate an understanding of advanced TypeScript features.",
|
||||
citations: [
|
||||
{
|
||||
text: "Converted a legacy JavaScript codebase of 100,000+ lines to TypeScript, implementing strict type checking and reducing runtime errors by 70%.",
|
||||
source: "Resume, Projects",
|
||||
relevance: 98
|
||||
},
|
||||
{
|
||||
text: "Created comprehensive TypeScript interfaces for our GraphQL API, ensuring type safety across the entire application stack.",
|
||||
source: "Resume, Technical Skills",
|
||||
relevance: 95
|
||||
}
|
||||
]
|
||||
},
|
||||
"Experience with RESTful APIs": {
|
||||
requirement: "Experience with RESTful APIs",
|
||||
status: "complete",
|
||||
matchScore: 75,
|
||||
assessment: "The candidate has good experience with RESTful APIs, having both consumed and designed them. They understand REST principles but have less documented experience with API versioning and caching strategies.",
|
||||
citations: [
|
||||
{
|
||||
text: "Designed and implemented a RESTful API serving over 1M requests daily with a focus on performance and scalability.",
|
||||
source: "Resume, Technical Projects",
|
||||
relevance: 85
|
||||
},
|
||||
{
|
||||
text: "Worked extensively with third-party APIs including Stripe, Twilio, and Salesforce to integrate payment processing and communication features.",
|
||||
source: "Resume, Work Experience",
|
||||
relevance: 70
|
||||
}
|
||||
]
|
||||
},
|
||||
"Knowledge of state management solutions (Redux, Context API)": {
|
||||
requirement: "Knowledge of state management solutions (Redux, Context API)",
|
||||
status: "complete",
|
||||
matchScore: 65,
|
||||
assessment: "The candidate has moderate experience with state management, primarily using Redux. There is less evidence of Context API usage, which could indicate a knowledge gap in more modern React state management approaches.",
|
||||
citations: [
|
||||
{
|
||||
text: "Implemented Redux for global state management in an e-commerce application, handling complex state logic for cart, user preferences, and product filtering.",
|
||||
source: "Resume, Skills",
|
||||
relevance: 80
|
||||
},
|
||||
{
|
||||
text: "My experience includes working with state management libraries like Redux and MobX.",
|
||||
source: "Cover Letter",
|
||||
relevance: 60
|
||||
}
|
||||
]
|
||||
},
|
||||
"Experience with CI/CD pipelines": {
|
||||
requirement: "Experience with CI/CD pipelines",
|
||||
status: "complete",
|
||||
matchScore: 40,
|
||||
assessment: "The candidate shows limited experience with CI/CD pipelines. While they mention some exposure to Jenkins and GitLab CI, there is insufficient evidence of setting up or maintaining comprehensive CI/CD workflows.",
|
||||
citations: [
|
||||
{
|
||||
text: "Familiar with CI/CD tools including Jenkins and GitLab CI.",
|
||||
source: "Resume, Skills",
|
||||
relevance: 40
|
||||
}
|
||||
]
|
||||
},
|
||||
"Cloud platform experience (AWS, Azure, GCP)": {
|
||||
requirement: "Cloud platform experience (AWS, Azure, GCP)",
|
||||
status: "complete",
|
||||
matchScore: 30,
|
||||
assessment: "The candidate demonstrates minimal experience with cloud platforms. There is a brief mention of AWS S3 and Lambda, but no substantial evidence of deeper cloud architecture knowledge or experience with Azure or GCP.",
|
||||
citations: [
|
||||
{
|
||||
text: "Used AWS S3 for file storage and Lambda for image processing in a photo sharing application.",
|
||||
source: "Resume, Projects",
|
||||
relevance: 35
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// Return a promise that resolves with the mock data after a delay
|
||||
return new Promise((resolve) => {
|
||||
// Different requirements resolve at different speeds to simulate real-world analysis
|
||||
const delay = Math.random() * 5000 + 2000; // 2-7 seconds
|
||||
setTimeout(() => {
|
||||
resolve(mockResponses[requirement]);
|
||||
}, delay);
|
||||
});
|
||||
};
|
||||
|
||||
// Navigation handlers
|
||||
const handleNext = () => {
|
||||
if (activeStep === 0 && !selectedCandidate) {
|
||||
@ -230,7 +117,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
}
|
||||
|
||||
if (activeStep === 1) {
|
||||
if ((/*(extraInfo && !jobTitle) || */!jobDescription)) {
|
||||
if (!jobDescription) {
|
||||
setError('Please provide job description before continuing.');
|
||||
return;
|
||||
}
|
||||
@ -338,12 +225,9 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
</Paper>
|
||||
);
|
||||
|
||||
const extraInfo = false;
|
||||
|
||||
// Render function for the job description step
|
||||
const renderJobDescription = () => (
|
||||
<Paper elevation={3} sx={{ p: 3, mt: 3, mb: 4, borderRadius: 2 }}>
|
||||
{extraInfo && <>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Enter Job Details
|
||||
</Typography>
|
||||
@ -361,6 +245,18 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Company"
|
||||
variant="outlined"
|
||||
value={company}
|
||||
onChange={(e) => setCompany(e.target.value)}
|
||||
required
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
@ -371,23 +267,21 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
}
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mt: 2, mb: 1 }}>
|
||||
<Typography variant="subtitle1" sx={{ mr: 2 }}>
|
||||
Job Description
|
||||
Job Selection
|
||||
</Typography>
|
||||
{extraInfo && <Button
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<FileUploadIcon />}
|
||||
size="small"
|
||||
onClick={() => setOpenUploadDialog(true)}
|
||||
>
|
||||
Upload
|
||||
</Button>}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<TextField
|
||||
@ -419,7 +313,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
<Box sx={{ mt: 3 }}>
|
||||
{selectedCandidate && (
|
||||
<JobMatchAnalysis
|
||||
job={{ title: jobTitle, description: jobDescription }}
|
||||
job={{ title: jobTitle, description: jobDescription, company: company, ownerId: user_id, ownerType: user_type }}
|
||||
candidate={selectedCandidate}
|
||||
{...backstoryProps}
|
||||
/>
|
||||
@ -434,14 +328,14 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
);
|
||||
|
||||
// If no user is logged in, show message
|
||||
if (!user) {
|
||||
if (!user?.id) {
|
||||
return (
|
||||
<Container maxWidth="md">
|
||||
<Paper elevation={3} sx={{ p: 4, mt: 4, textAlign: 'center' }}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Please log in to access candidate analysis
|
||||
</Typography>
|
||||
<Button variant="contained" color="primary" sx={{ mt: 2 }}>
|
||||
<Button variant="contained" onClick={() => { navigate('/login'); }} color="primary" sx={{ mt: 2 }}>
|
||||
Log In
|
||||
</Button>
|
||||
</Paper>
|
||||
|
@ -24,9 +24,9 @@ const CandidateRoute: React.FC<CandidateRouteProps> = (props: CandidateRouteProp
|
||||
if (candidate?.username === username || !username) {
|
||||
return;
|
||||
}
|
||||
const getCandidate = async (username: string) => {
|
||||
const getCandidate = async (reference: string) => {
|
||||
try {
|
||||
const result : Candidate = await apiClient.getCandidate(username);
|
||||
const result: Candidate = await apiClient.getCandidate(reference);
|
||||
setCandidate(result);
|
||||
navigate('/chat');
|
||||
} catch {
|
||||
|
@ -506,8 +506,9 @@ class ApiClient {
|
||||
// Candidate Methods with Date Conversion
|
||||
// ============================
|
||||
|
||||
async getCandidate(username: string): Promise<Types.Candidate> {
|
||||
const response = await fetch(`${this.baseUrl}/candidates/${username}`, {
|
||||
// reference can be candidateId, username, or email
|
||||
async getCandidate(reference: string): Promise<Types.Candidate> {
|
||||
const response = await fetch(`${this.baseUrl}/candidates/${reference}`, {
|
||||
headers: this.defaultHeaders
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Generated TypeScript types from Pydantic models
|
||||
// Source: src/backend/models.py
|
||||
// Generated on: 2025-06-04T05:16:43.020718
|
||||
// Generated on: 2025-06-04T17:02:08.242818
|
||||
// DO NOT EDIT MANUALLY - This file is auto-generated
|
||||
|
||||
// ============================
|
||||
@ -577,26 +577,13 @@ export interface InterviewSchedule {
|
||||
|
||||
export interface Job {
|
||||
id?: string;
|
||||
title: string;
|
||||
ownerId: string;
|
||||
ownerType: "candidate" | "employer" | "guest";
|
||||
title?: string;
|
||||
summary?: string;
|
||||
company?: string;
|
||||
description: string;
|
||||
responsibilities: Array<string>;
|
||||
requirements: Array<string>;
|
||||
preferredSkills?: Array<string>;
|
||||
employerId: string;
|
||||
location: Location;
|
||||
salaryRange?: SalaryRange;
|
||||
employmentType: "full-time" | "part-time" | "contract" | "internship" | "freelance";
|
||||
datePosted: Date;
|
||||
applicationDeadline?: Date;
|
||||
isActive: boolean;
|
||||
applicants?: Array<JobApplication>;
|
||||
department?: string;
|
||||
reportsTo?: string;
|
||||
benefits?: Array<string>;
|
||||
visaSponsorship?: boolean;
|
||||
featuredUntil?: Date;
|
||||
views: number;
|
||||
applicationCount: number;
|
||||
requirements?: JobRequirements;
|
||||
}
|
||||
|
||||
export interface JobApplication {
|
||||
@ -615,6 +602,31 @@ export interface JobApplication {
|
||||
decision?: ApplicationDecision;
|
||||
}
|
||||
|
||||
export interface JobFull {
|
||||
id?: string;
|
||||
ownerId: string;
|
||||
ownerType: "candidate" | "employer" | "guest";
|
||||
title?: string;
|
||||
summary?: string;
|
||||
company?: string;
|
||||
description: string;
|
||||
requirements?: JobRequirements;
|
||||
location: Location;
|
||||
salaryRange?: SalaryRange;
|
||||
employmentType: "full-time" | "part-time" | "contract" | "internship" | "freelance";
|
||||
datePosted: Date;
|
||||
applicationDeadline?: Date;
|
||||
isActive: boolean;
|
||||
applicants?: Array<JobApplication>;
|
||||
department?: string;
|
||||
reportsTo?: string;
|
||||
benefits?: Array<string>;
|
||||
visaSponsorship?: boolean;
|
||||
featuredUntil?: Date;
|
||||
views: number;
|
||||
applicationCount: number;
|
||||
}
|
||||
|
||||
export interface JobListResponse {
|
||||
success: boolean;
|
||||
data?: Array<Job>;
|
||||
@ -1224,23 +1236,6 @@ export function convertInterviewScheduleFromApi(data: any): InterviewSchedule {
|
||||
endDate: new Date(data.endDate),
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Convert Job from API response, parsing date fields
|
||||
* Date fields: datePosted, applicationDeadline, featuredUntil
|
||||
*/
|
||||
export function convertJobFromApi(data: any): Job {
|
||||
if (!data) return data;
|
||||
|
||||
return {
|
||||
...data,
|
||||
// Convert datePosted from ISO string to Date
|
||||
datePosted: new Date(data.datePosted),
|
||||
// Convert applicationDeadline from ISO string to Date
|
||||
applicationDeadline: data.applicationDeadline ? new Date(data.applicationDeadline) : undefined,
|
||||
// Convert featuredUntil from ISO string to Date
|
||||
featuredUntil: data.featuredUntil ? new Date(data.featuredUntil) : undefined,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Convert JobApplication from API response, parsing date fields
|
||||
* Date fields: appliedDate, updatedDate
|
||||
@ -1256,6 +1251,23 @@ export function convertJobApplicationFromApi(data: any): JobApplication {
|
||||
updatedDate: new Date(data.updatedDate),
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Convert JobFull from API response, parsing date fields
|
||||
* Date fields: datePosted, applicationDeadline, featuredUntil
|
||||
*/
|
||||
export function convertJobFullFromApi(data: any): JobFull {
|
||||
if (!data) return data;
|
||||
|
||||
return {
|
||||
...data,
|
||||
// Convert datePosted from ISO string to Date
|
||||
datePosted: new Date(data.datePosted),
|
||||
// Convert applicationDeadline from ISO string to Date
|
||||
applicationDeadline: data.applicationDeadline ? new Date(data.applicationDeadline) : undefined,
|
||||
// Convert featuredUntil from ISO string to Date
|
||||
featuredUntil: data.featuredUntil ? new Date(data.featuredUntil) : undefined,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Convert MessageReaction from API response, parsing date fields
|
||||
* Date fields: timestamp
|
||||
@ -1378,10 +1390,10 @@ export function convertFromApi<T>(data: any, modelType: string): T {
|
||||
return convertInterviewFeedbackFromApi(data) as T;
|
||||
case 'InterviewSchedule':
|
||||
return convertInterviewScheduleFromApi(data) as T;
|
||||
case 'Job':
|
||||
return convertJobFromApi(data) as T;
|
||||
case 'JobApplication':
|
||||
return convertJobApplicationFromApi(data) as T;
|
||||
case 'JobFull':
|
||||
return convertJobFullFromApi(data) as T;
|
||||
case 'MessageReaction':
|
||||
return convertMessageReactionFromApi(data) as T;
|
||||
case 'RAGConfiguration':
|
||||
|
@ -25,7 +25,7 @@ from models import Candidate
|
||||
|
||||
_agents: List[Agent] = []
|
||||
|
||||
def get_or_create_agent(agent_type: str, prometheus_collector: CollectorRegistry, user: Optional[Candidate]=None, **kwargs) -> Agent:
|
||||
def get_or_create_agent(agent_type: str, prometheus_collector: CollectorRegistry, user: Optional[Candidate]=None) -> Agent:
|
||||
"""
|
||||
Get or create and append a new agent of the specified type, ensuring only one agent per type exists.
|
||||
|
||||
@ -39,16 +39,17 @@ def get_or_create_agent(agent_type: str, prometheus_collector: CollectorRegistry
|
||||
Raises:
|
||||
ValueError: If no matching agent type is found or if a agent of this type already exists.
|
||||
"""
|
||||
# Check if a agent with the given agent_type already exists
|
||||
for agent in _agents:
|
||||
if agent.agent_type == agent_type:
|
||||
return agent
|
||||
# Check if a global (non-user) agent with the given agent_type already exists
|
||||
if not user:
|
||||
for agent in _agents:
|
||||
if agent.agent_type == agent_type:
|
||||
return agent
|
||||
|
||||
# Find the matching subclass
|
||||
for agent_cls in Agent.__subclasses__():
|
||||
if agent_cls.model_fields["agent_type"].default == agent_type:
|
||||
# Create the agent instance with provided kwargs
|
||||
agent = agent_cls(agent_type=agent_type, user=user, prometheus_collector=prometheus_collector, **kwargs)
|
||||
agent = agent_cls(agent_type=agent_type, user=user, prometheus_collector=prometheus_collector)
|
||||
# if agent.agent_persist: # If an agent is not set to persist, do not add it to the list
|
||||
_agents.append(agent)
|
||||
return agent
|
||||
|
@ -40,13 +40,17 @@ class JobRequirementsAgent(Agent):
|
||||
## INSTRUCTIONS:
|
||||
|
||||
1. Analyze ONLY the job description provided.
|
||||
2. Extract and categorize all requirements and preferences.
|
||||
3. DO NOT consider any candidate information - this is a pure job analysis task.
|
||||
2. Extract company information, job title, and all requirements.
|
||||
3. Extract and categorize all requirements and preferences.
|
||||
4. DO NOT consider any candidate information - this is a pure job analysis task.
|
||||
|
||||
## OUTPUT FORMAT:
|
||||
|
||||
```json
|
||||
{
|
||||
{
|
||||
"company_name": "Company Name",
|
||||
"job_title": "Job Title",
|
||||
"job_summary": "Brief summary of the job",
|
||||
"job_requirements": {
|
||||
"technical_skills": {
|
||||
"required": ["skill1", "skill2"],
|
||||
@ -133,9 +137,15 @@ class JobRequirementsAgent(Agent):
|
||||
json_str = self.extract_json_from_text(generated_message.content)
|
||||
job_requirements : JobRequirements | None = None
|
||||
job_requirements_data = ""
|
||||
company_name = ""
|
||||
job_summary = ""
|
||||
job_title = ""
|
||||
try:
|
||||
job_requirements_data = json.loads(json_str)
|
||||
job_requirements_data = job_requirements_data.get("job_requirements", None)
|
||||
job_title = job_requirements_data.get("job_title", "")
|
||||
company_name = job_requirements_data.get("company_name", "")
|
||||
job_summary = job_requirements_data.get("job_summary", "")
|
||||
job_requirements = JobRequirements.model_validate(job_requirements_data)
|
||||
if not job_requirements:
|
||||
raise ValueError("Job requirements data is empty or invalid.")
|
||||
@ -160,7 +170,13 @@ class JobRequirementsAgent(Agent):
|
||||
return
|
||||
status_message.status = ChatStatusType.DONE
|
||||
status_message.type = ChatMessageType.RESPONSE
|
||||
status_message.content = json.dumps(job_requirements.model_dump(mode="json", exclude_unset=True))
|
||||
job_data = {
|
||||
"company": company_name,
|
||||
"title": job_title,
|
||||
"summary": job_summary,
|
||||
"requirements": job_requirements.model_dump(mode="json", exclude_unset=True)
|
||||
}
|
||||
status_message.content = json.dumps(job_data)
|
||||
yield status_message
|
||||
|
||||
logger.info(f"✅ Job requirements analysis completed successfully.")
|
||||
|
@ -168,7 +168,7 @@ JSON RESPONSE:"""
|
||||
|
||||
user_message.content = prompt
|
||||
skill_assessment = None
|
||||
async for skill_assessment in self.llm_one_shot(llm=llm, model=model, user_message=user_message, system_prompt=system_prompt, temperature=0.1):
|
||||
async for skill_assessment in self.llm_one_shot(llm=llm, model=model, user_message=user_message, system_prompt=system_prompt, temperature=0.7):
|
||||
if skill_assessment.status == ChatStatusType.ERROR:
|
||||
status_message.status = ChatStatusType.ERROR
|
||||
status_message.content = skill_assessment.content
|
||||
|
@ -20,11 +20,13 @@ from logger import logger
|
||||
import agents as agents
|
||||
from models import (Tunables, CandidateQuestion, ChatMessageUser, ChatMessage, RagEntry, ChatMessageType, ChatMessageMetaData, ChatStatusType, Candidate, ChatContextType)
|
||||
from llm_manager import llm_manager
|
||||
from agents.base import Agent
|
||||
|
||||
class CandidateEntity(Candidate):
|
||||
model_config = {"arbitrary_types_allowed": True} # Allow ChromaDBFileWatcher, etc
|
||||
|
||||
# Internal instance members
|
||||
CandidateEntity__agents: List[Agent] = []
|
||||
CandidateEntity__observer: Optional[Any] = Field(default=None, exclude=True)
|
||||
CandidateEntity__file_watcher: Optional[ChromaDBFileWatcher] = Field(default=None, exclude=True)
|
||||
CandidateEntity__prometheus_collector: Optional[CollectorRegistry] = Field(
|
||||
@ -63,7 +65,7 @@ class CandidateEntity(Candidate):
|
||||
# Check if file exists
|
||||
return user_info_path.is_file()
|
||||
|
||||
def get_or_create_agent(self, agent_type: ChatContextType, **kwargs) -> agents.Agent:
|
||||
def get_or_create_agent(self, agent_type: ChatContextType) -> agents.Agent:
|
||||
"""
|
||||
Get or create an agent of the specified type for this candidate.
|
||||
|
||||
@ -74,11 +76,17 @@ class CandidateEntity(Candidate):
|
||||
Returns:
|
||||
The created agent instance.
|
||||
"""
|
||||
|
||||
# Only instantiate one agent of each type per user
|
||||
for agent in self.CandidateEntity__agents:
|
||||
if agent.agent_type == agent_type:
|
||||
return agent
|
||||
|
||||
return agents.get_or_create_agent(
|
||||
agent_type=agent_type,
|
||||
user=self,
|
||||
prometheus_collector=self.prometheus_collector,
|
||||
**kwargs)
|
||||
prometheus_collector=self.prometheus_collector
|
||||
)
|
||||
|
||||
# Wrapper properties that map into file_watcher
|
||||
@property
|
||||
|
@ -64,13 +64,13 @@ import agents
|
||||
# =============================
|
||||
from models import (
|
||||
# API
|
||||
LoginRequest, CreateCandidateRequest, CreateEmployerRequest,
|
||||
Job, LoginRequest, CreateCandidateRequest, CreateEmployerRequest,
|
||||
|
||||
# User models
|
||||
Candidate, Employer, BaseUserWithType, BaseUser, Guest, Authentication, AuthResponse, CandidateAI,
|
||||
|
||||
# Job models
|
||||
Job, JobApplication, ApplicationStatus,
|
||||
JobFull, JobApplication, ApplicationStatus,
|
||||
|
||||
# Chat models
|
||||
ChatSession, ChatMessage, ChatContext, ChatQuery, ChatStatusType, ChatMessageBase, ChatMessageUser, ChatSenderType, ChatMessageType, ChatContextType,
|
||||
@ -2609,28 +2609,24 @@ async def confirm_password_reset(
|
||||
# ============================
|
||||
|
||||
@api_router.post("/jobs")
|
||||
async def create_job(
|
||||
async def create_candidate_job(
|
||||
job_data: Dict[str, Any] = Body(...),
|
||||
current_user = Depends(get_current_user),
|
||||
database: RedisDatabase = Depends(get_database)
|
||||
):
|
||||
"""Create a new job"""
|
||||
is_employer = isinstance(current_user, Employer)
|
||||
|
||||
try:
|
||||
# Verify user is an employer
|
||||
if not isinstance(current_user, Employer):
|
||||
return JSONResponse(
|
||||
status_code=403,
|
||||
content=create_error_response("FORBIDDEN", "Only employers can create jobs")
|
||||
)
|
||||
|
||||
if is_employer:
|
||||
job = JobFull.model_validate(job_data)
|
||||
else:
|
||||
job = Job.model_validate(job_data)
|
||||
|
||||
# Add required fields
|
||||
job_data["id"] = str(uuid.uuid4())
|
||||
job_data["datePosted"] = datetime.now(UTC).isoformat()
|
||||
job_data["views"] = 0
|
||||
job_data["applicationCount"] = 0
|
||||
job_data["employerId"] = current_user.id
|
||||
job.id = str(uuid.uuid4())
|
||||
job.owner_id = current_user.id
|
||||
|
||||
job = Job.model_validate(job_data)
|
||||
await database.set_job(job.id, job.model_dump())
|
||||
|
||||
return create_success_response(job.model_dump(by_alias=True, exclude_unset=True))
|
||||
@ -2687,7 +2683,12 @@ async def get_jobs(
|
||||
|
||||
# Get all jobs from Redis
|
||||
all_jobs_data = await database.get_all_jobs()
|
||||
jobs_list = [Job.model_validate(data) for data in all_jobs_data.values() if data.get("is_active", True)]
|
||||
jobs_list = []
|
||||
for job in all_jobs_data.values():
|
||||
if job.get("user_type") == "employer":
|
||||
jobs_list.append(JobFull.model_validate(job))
|
||||
else:
|
||||
jobs_list.append(Job.model_validate(job))
|
||||
|
||||
paginated_jobs, total = filter_and_paginate(
|
||||
jobs_list, page, limit, sortBy, sortOrder, filter_dict
|
||||
@ -2822,33 +2823,40 @@ async def post_candidate_rag_search(
|
||||
content=create_error_response("SUMMARY_ERROR", str(e))
|
||||
)
|
||||
|
||||
@api_router.get("/candidates/{username}")
|
||||
# reference can be candidateId, username, or email
|
||||
@api_router.get("/candidates/{reference}")
|
||||
async def get_candidate(
|
||||
username: str = Path(...),
|
||||
reference: str = Path(...),
|
||||
database: RedisDatabase = Depends(get_database)
|
||||
):
|
||||
"""Get a candidate by username"""
|
||||
try:
|
||||
# Normalize reference to lowercase for case-insensitive search
|
||||
query_lower = reference.lower()
|
||||
|
||||
all_candidates_data = await database.get_all_candidates()
|
||||
candidates_list = [Candidate.model_validate(data) for data in all_candidates_data.values()]
|
||||
if not all_candidates_data:
|
||||
logger.warning(f"⚠️ No candidates found in database")
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content=create_error_response("NOT_FOUND", "No candidates found")
|
||||
)
|
||||
|
||||
candidate_data = None
|
||||
for candidate in all_candidates_data.values():
|
||||
if (candidate.get("id", "").lower() == query_lower or
|
||||
candidate.get("username", "").lower() == query_lower or
|
||||
candidate.get("email", "").lower() == query_lower):
|
||||
candidate_data = candidate
|
||||
break
|
||||
|
||||
# Normalize username to lowercase for case-insensitive search
|
||||
query_lower = username.lower()
|
||||
|
||||
# Filter by search query
|
||||
candidates_list = [
|
||||
c for c in candidates_list
|
||||
if (query_lower == c.email.lower() or
|
||||
query_lower == c.username.lower())
|
||||
]
|
||||
|
||||
if not len(candidates_list):
|
||||
if not candidate_data:
|
||||
logger.warning(f"⚠️ Candidate not found for reference: {reference}")
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content=create_error_response("NOT_FOUND", "Candidate not found")
|
||||
)
|
||||
|
||||
candidate_data = candidates_list[0]
|
||||
candidate = Candidate.model_validate(candidate_data) if not candidate_data.get("is_AI") else CandidateAI.model_validate(candidate_data)
|
||||
|
||||
return create_success_response(candidate.model_dump(by_alias=True, exclude_unset=True))
|
||||
@ -3368,8 +3376,8 @@ async def get_candidate_skill_match(
|
||||
|
||||
candidate = Candidate.model_validate(candidate_data)
|
||||
|
||||
logger.info(f"🔍 Running skill match for candidate {candidate.id} against requirement: {requirement}")
|
||||
async with entities.get_candidate_entity(candidate=candidate) as candidate_entity:
|
||||
logger.info(f"🔍 Running skill match for candidate {candidate_entity.username} against requirement: {requirement}")
|
||||
agent = candidate_entity.get_or_create_agent(agent_type=ChatContextType.SKILL_MATCH)
|
||||
if not agent:
|
||||
return JSONResponse(
|
||||
|
@ -108,18 +108,7 @@ class SkillMatch(BaseModel):
|
||||
"populate_by_name": True # Allow both field names and aliases
|
||||
}
|
||||
|
||||
class JobRequirements(BaseModel):
|
||||
technical_skills: Requirements = Field(..., alias="technicalSkills")
|
||||
experience_requirements: Requirements = Field(..., alias="experienceRequirements")
|
||||
soft_skills: Optional[List[str]] = Field(default_factory=list, alias="softSkills")
|
||||
experience: Optional[List[str]] = []
|
||||
education: Optional[List[str]] = []
|
||||
certifications: Optional[List[str]] = []
|
||||
preferred_attributes: Optional[List[str]] = Field(None, alias="preferredAttributes")
|
||||
model_config = {
|
||||
"populate_by_name": True # Allow both field names and aliases
|
||||
}
|
||||
|
||||
|
||||
class ChatMessageType(str, Enum):
|
||||
ERROR = "error"
|
||||
GENERATING = "generating"
|
||||
@ -650,14 +639,32 @@ class AuthResponse(BaseModel):
|
||||
"populate_by_name": True # Allow both field names and aliases
|
||||
}
|
||||
|
||||
class JobRequirements(BaseModel):
|
||||
technical_skills: Requirements = Field(..., alias="technicalSkills")
|
||||
experience_requirements: Requirements = Field(..., alias="experienceRequirements")
|
||||
soft_skills: Optional[List[str]] = Field(default_factory=list, alias="softSkills")
|
||||
experience: Optional[List[str]] = []
|
||||
education: Optional[List[str]] = []
|
||||
certifications: Optional[List[str]] = []
|
||||
preferred_attributes: Optional[List[str]] = Field(None, alias="preferredAttributes")
|
||||
model_config = {
|
||||
"populate_by_name": True # Allow both field names and aliases
|
||||
}
|
||||
|
||||
class Job(BaseModel):
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
||||
title: str
|
||||
owner_id: str = Field(..., alias="ownerId")
|
||||
owner_type: UserType = Field(..., alias="ownerType")
|
||||
title: Optional[str]
|
||||
summary: Optional[str]
|
||||
company: Optional[str]
|
||||
description: str
|
||||
responsibilities: List[str]
|
||||
requirements: List[str]
|
||||
preferred_skills: Optional[List[str]] = Field(None, alias="preferredSkills")
|
||||
employer_id: str = Field(..., alias="employerId")
|
||||
requirements: Optional[JobRequirements]
|
||||
model_config = {
|
||||
"populate_by_name": True # Allow both field names and aliases
|
||||
}
|
||||
|
||||
class JobFull(Job):
|
||||
location: Location
|
||||
salary_range: Optional[SalaryRange] = Field(None, alias="salaryRange")
|
||||
employment_type: EmploymentType = Field(..., alias="employmentType")
|
||||
@ -672,9 +679,6 @@ class Job(BaseModel):
|
||||
featured_until: Optional[datetime] = Field(None, alias="featuredUntil")
|
||||
views: int = 0
|
||||
application_count: int = Field(0, alias="applicationCount")
|
||||
model_config = {
|
||||
"populate_by_name": True # Allow both field names and aliases
|
||||
}
|
||||
|
||||
class InterviewFeedback(BaseModel):
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
||||
@ -1061,4 +1065,4 @@ Candidate.update_forward_refs()
|
||||
Employer.update_forward_refs()
|
||||
ChatSession.update_forward_refs()
|
||||
JobApplication.update_forward_refs()
|
||||
Job.update_forward_refs()
|
||||
JobFull.update_forward_refs()
|
Loading…
x
Reference in New Issue
Block a user