Refresh maintains selected entities
This commit is contained in:
parent
a912e4d24c
commit
4f7b2f3e6a
@ -125,7 +125,6 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
target: ollama
|
target: ollama
|
||||||
#image: ollama
|
|
||||||
container_name: ollama
|
container_name: ollama
|
||||||
restart: "always"
|
restart: "always"
|
||||||
env_file:
|
env_file:
|
||||||
|
@ -24,12 +24,7 @@ import { Candidate, ChatMessage, ChatMessageBase, ChatMessageUser, ChatSession,
|
|||||||
import { useAuth } from 'hooks/AuthContext';
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
import { BackstoryPageProps } from './BackstoryTab';
|
import { BackstoryPageProps } from './BackstoryTab';
|
||||||
import { toCamelCase } from 'types/conversion';
|
import { toCamelCase } from 'types/conversion';
|
||||||
|
import { Job } from 'types/types';
|
||||||
|
|
||||||
interface Job {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface JobAnalysisProps extends BackstoryPageProps {
|
interface JobAnalysisProps extends BackstoryPageProps {
|
||||||
job: Job;
|
job: Job;
|
||||||
@ -48,7 +43,6 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
|||||||
} = props
|
} = props
|
||||||
const { apiClient } = useAuth();
|
const { apiClient } = useAuth();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [jobRequirements, setJobRequirements] = useState<JobRequirements | null>(null);
|
|
||||||
const [requirements, setRequirements] = useState<{ requirement: string, domain: 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);
|
||||||
@ -89,7 +83,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
|||||||
|
|
||||||
// Fetch initial requirements
|
// Fetch initial requirements
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!job.description || !requirementsSession || loadingRequirements || jobRequirements) {
|
if (!job.description || !requirementsSession || loadingRequirements) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,22 +95,36 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
|||||||
onMessage: (msg: ChatMessage) => {
|
onMessage: (msg: ChatMessage) => {
|
||||||
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 job: Job = toCamelCase<Job>(JSON.parse(msg.content || ''));
|
||||||
const requirements: { requirement: string, domain: string }[] = ['technicalSkills', 'experienceRequirements'].flatMap((domain) => {
|
const requirements: { requirement: string, domain: string }[] = [];
|
||||||
return ['required', 'preferred'].flatMap((level) => {
|
if (job.requirements?.technicalSkills) {
|
||||||
return incoming[domain][level].map((s: string) => { return { requirement: s, domain: domain }; });
|
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)' }));
|
||||||
});
|
}
|
||||||
['softSkills', 'experience', 'education', 'certifications', 'preferredAttributes'].forEach(domain => {
|
if (job.requirements?.experienceRequirements) {
|
||||||
if (incoming[domain]) {
|
job.requirements.experienceRequirements.required?.forEach(req => requirements.push({ requirement: req, domain: 'Experience (required)' }));
|
||||||
incoming[domain].forEach((s: string) => requirements.push({ requirement: s, domain: domain }));
|
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 => ({
|
const initialSkillMatches = requirements.map(req => ({
|
||||||
requirement: req.requirement,
|
requirement: req.requirement,
|
||||||
domain: req.domain,
|
domain: req.domain,
|
||||||
status: 'pending' as const,
|
status: 'waiting' as const,
|
||||||
matchScore: 0,
|
matchScore: 0,
|
||||||
assessment: '',
|
assessment: '',
|
||||||
description: '',
|
description: '',
|
||||||
@ -168,6 +176,12 @@ 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 {
|
||||||
|
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 result: any = await apiClient.candidateMatchForRequirement(candidate.id || '', requirements[i].requirement);
|
||||||
const skillMatch = result.skillMatch;
|
const skillMatch = result.skillMatch;
|
||||||
let matchScore: number = 0;
|
let matchScore: number = 0;
|
||||||
@ -177,7 +191,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
|||||||
case "WEAK": matchScore = 50; break;
|
case "WEAK": matchScore = 50; break;
|
||||||
case "NONE": matchScore = 0; 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);
|
matchScore = Math.min(skillMatch.citations.length * 8, 40);
|
||||||
}
|
}
|
||||||
const match: SkillMatch = {
|
const match: SkillMatch = {
|
||||||
@ -234,7 +248,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
|||||||
|
|
||||||
// Get icon based on status
|
// Get icon based on status
|
||||||
const getStatusIcon = (status: string, score: number) => {
|
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 (status === 'error') return <ErrorIcon color="error" />;
|
||||||
if (score >= 70) return <CheckCircleIcon color="success" />;
|
if (score >= 70) return <CheckCircleIcon color="success" />;
|
||||||
if (score >= 40) return <WarningIcon color="warning" />;
|
if (score >= 40) return <WarningIcon color="warning" />;
|
||||||
|
@ -1,5 +1,41 @@
|
|||||||
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
|
||||||
import * as Types from 'types/types';
|
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
|
// App State Interface
|
||||||
@ -9,6 +45,7 @@ export interface AppState {
|
|||||||
selectedCandidate: Types.Candidate | null;
|
selectedCandidate: Types.Candidate | null;
|
||||||
selectedJob: Types.Job | null;
|
selectedJob: Types.Job | null;
|
||||||
selectedEmployer: Types.Employer | null;
|
selectedEmployer: Types.Employer | null;
|
||||||
|
isInitializing: boolean;
|
||||||
// Add more global state as needed:
|
// Add more global state as needed:
|
||||||
// currentView: string;
|
// currentView: string;
|
||||||
// filters: Record<string, any>;
|
// filters: Record<string, any>;
|
||||||
@ -30,13 +67,118 @@ export type AppStateContextType = AppState & AppStateActions;
|
|||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
export function useAppStateLogic(): AppStateContextType {
|
export function useAppStateLogic(): AppStateContextType {
|
||||||
|
const { apiClient } = useAuth();
|
||||||
const [selectedCandidate, setSelectedCandidateState] = useState<Types.Candidate | null>(null);
|
const [selectedCandidate, setSelectedCandidateState] = useState<Types.Candidate | null>(null);
|
||||||
const [selectedJob, setSelectedJobState] = useState<Types.Job | null>(null);
|
const [selectedJob, setSelectedJobState] = useState<Types.Job | null>(null);
|
||||||
const [selectedEmployer, setSelectedEmployerState] = useState<Types.Employer | 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) => {
|
const setSelectedCandidate = useCallback((candidate: Types.Candidate | null) => {
|
||||||
setSelectedCandidateState(candidate);
|
setSelectedCandidateState(candidate);
|
||||||
|
|
||||||
|
// Persist ID to localStorage
|
||||||
|
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, candidate?.id || null);
|
||||||
|
|
||||||
if (candidate) {
|
if (candidate) {
|
||||||
console.log('Selected candidate:', candidate);
|
console.log('Selected candidate:', candidate);
|
||||||
} else {
|
} else {
|
||||||
@ -47,6 +189,9 @@ export function useAppStateLogic(): AppStateContextType {
|
|||||||
const setSelectedJob = useCallback((job: Types.Job | null) => {
|
const setSelectedJob = useCallback((job: Types.Job | null) => {
|
||||||
setSelectedJobState(job);
|
setSelectedJobState(job);
|
||||||
|
|
||||||
|
// Persist ID to localStorage
|
||||||
|
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, job?.id || null);
|
||||||
|
|
||||||
if (job) {
|
if (job) {
|
||||||
console.log('Selected job:', job);
|
console.log('Selected job:', job);
|
||||||
} else {
|
} else {
|
||||||
@ -57,6 +202,9 @@ export function useAppStateLogic(): AppStateContextType {
|
|||||||
const setSelectedEmployer = useCallback((employer: Types.Employer | null) => {
|
const setSelectedEmployer = useCallback((employer: Types.Employer | null) => {
|
||||||
setSelectedEmployerState(employer);
|
setSelectedEmployerState(employer);
|
||||||
|
|
||||||
|
// Persist ID to localStorage
|
||||||
|
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, employer?.id || null);
|
||||||
|
|
||||||
if (employer) {
|
if (employer) {
|
||||||
console.log('Selected employer:', employer);
|
console.log('Selected employer:', employer);
|
||||||
} else {
|
} else {
|
||||||
@ -68,6 +216,12 @@ export function useAppStateLogic(): AppStateContextType {
|
|||||||
setSelectedCandidateState(null);
|
setSelectedCandidateState(null);
|
||||||
setSelectedJobState(null);
|
setSelectedJobState(null);
|
||||||
setSelectedEmployerState(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');
|
console.log('Cleared all selections');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -75,6 +229,7 @@ export function useAppStateLogic(): AppStateContextType {
|
|||||||
selectedCandidate,
|
selectedCandidate,
|
||||||
selectedJob,
|
selectedJob,
|
||||||
selectedEmployer,
|
selectedEmployer,
|
||||||
|
isInitializing,
|
||||||
setSelectedCandidate,
|
setSelectedCandidate,
|
||||||
setSelectedJob,
|
setSelectedJob,
|
||||||
setSelectedEmployer,
|
setSelectedEmployer,
|
||||||
@ -135,6 +290,14 @@ export function useSelectedEmployer() {
|
|||||||
return { selectedEmployer, setSelectedEmployer };
|
return { selectedEmployer, setSelectedEmployer };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to check if the app is still initializing
|
||||||
|
*/
|
||||||
|
export function useAppInitializing() {
|
||||||
|
const { isInitializing } = useAppState();
|
||||||
|
return isInitializing;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// Development Utilities
|
// Development Utilities
|
||||||
// ============================
|
// ============================
|
||||||
@ -148,12 +311,23 @@ export function useAppStateDebug() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
console.group('🔍 App State Debug');
|
console.group('🔍 App State Debug');
|
||||||
|
console.log('Is Initializing:', appState.isInitializing);
|
||||||
console.log('Selected Candidate:', appState.selectedCandidate);
|
console.log('Selected Candidate:', appState.selectedCandidate);
|
||||||
console.log('Selected Job:', appState.selectedJob);
|
console.log('Selected Job:', appState.selectedJob);
|
||||||
console.log('Selected Employer:', appState.selectedEmployer);
|
console.log('Selected Employer:', appState.selectedEmployer);
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
}
|
}
|
||||||
}, [appState.selectedCandidate, appState.selectedJob, appState.selectedEmployer]);
|
}, [appState.selectedCandidate, appState.selectedJob, appState.selectedEmployer, appState.isInitializing]);
|
||||||
|
|
||||||
return appState;
|
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 JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate()
|
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate()
|
||||||
const { setSnack, submitQuery } = props;
|
const { setSnack, submitQuery } = props;
|
||||||
const backstoryProps = { setSnack, submitQuery };
|
const backstoryProps = { setSnack, submitQuery };
|
||||||
@ -53,6 +54,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
|||||||
const [activeStep, setActiveStep] = useState(0);
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
const [jobDescription, setJobDescription] = useState('');
|
const [jobDescription, setJobDescription] = useState('');
|
||||||
const [jobTitle, setJobTitle] = useState('');
|
const [jobTitle, setJobTitle] = useState('');
|
||||||
|
const [company, setCompany] = useState('');
|
||||||
const [jobLocation, setJobLocation] = useState('');
|
const [jobLocation, setJobLocation] = useState('');
|
||||||
const [analysisStarted, setAnalysisStarted] = useState(false);
|
const [analysisStarted, setAnalysisStarted] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@ -60,6 +62,9 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
|||||||
const { apiClient } = useAuth();
|
const { apiClient } = useAuth();
|
||||||
const [candidates, setCandidates] = useState<Candidate[] | null>(null);
|
const [candidates, setCandidates] = useState<Candidate[] | null>(null);
|
||||||
|
|
||||||
|
const user_type = user?.userType || 'guest';
|
||||||
|
const user_id = user?.id || '';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (candidates !== null || selectedCandidate) {
|
if (candidates !== null || selectedCandidate) {
|
||||||
return;
|
return;
|
||||||
@ -96,7 +101,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
|||||||
|
|
||||||
// Steps in our process
|
// Steps in our process
|
||||||
const steps = [
|
const steps = [
|
||||||
{ index: 1, label: 'Job Description', icon: <WorkIcon /> },
|
{ index: 1, label: 'Job Selection', icon: <WorkIcon /> },
|
||||||
{ index: 2, label: 'AI Analysis', icon: <WorkIcon /> },
|
{ index: 2, label: 'AI Analysis', icon: <WorkIcon /> },
|
||||||
{ index: 3, label: 'Generated Resume', icon: <AssessmentIcon /> }
|
{ 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 /> })
|
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
|
// Navigation handlers
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
if (activeStep === 0 && !selectedCandidate) {
|
if (activeStep === 0 && !selectedCandidate) {
|
||||||
@ -230,7 +117,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (activeStep === 1) {
|
if (activeStep === 1) {
|
||||||
if ((/*(extraInfo && !jobTitle) || */!jobDescription)) {
|
if (!jobDescription) {
|
||||||
setError('Please provide job description before continuing.');
|
setError('Please provide job description before continuing.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -338,12 +225,9 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
|||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
|
||||||
const extraInfo = false;
|
|
||||||
|
|
||||||
// Render function for the job description step
|
// Render function for the job description step
|
||||||
const renderJobDescription = () => (
|
const renderJobDescription = () => (
|
||||||
<Paper elevation={3} sx={{ p: 3, mt: 3, mb: 4, borderRadius: 2 }}>
|
<Paper elevation={3} sx={{ p: 3, mt: 3, mb: 4, borderRadius: 2 }}>
|
||||||
{extraInfo && <>
|
|
||||||
<Typography variant="h5" gutterBottom>
|
<Typography variant="h5" gutterBottom>
|
||||||
Enter Job Details
|
Enter Job Details
|
||||||
</Typography>
|
</Typography>
|
||||||
@ -361,6 +245,18 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</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 }}>
|
<Grid size={{ xs: 12, md: 6 }}>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
@ -372,22 +268,20 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Grid size={{ xs: 12 }}>
|
<Grid size={{ xs: 12 }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', mt: 2, mb: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', mt: 2, mb: 1 }}>
|
||||||
<Typography variant="subtitle1" sx={{ mr: 2 }}>
|
<Typography variant="subtitle1" sx={{ mr: 2 }}>
|
||||||
Job Description
|
Job Selection
|
||||||
</Typography>
|
</Typography>
|
||||||
{extraInfo && <Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
startIcon={<FileUploadIcon />}
|
startIcon={<FileUploadIcon />}
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => setOpenUploadDialog(true)}
|
onClick={() => setOpenUploadDialog(true)}
|
||||||
>
|
>
|
||||||
Upload
|
Upload
|
||||||
</Button>}
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
@ -419,7 +313,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
|||||||
<Box sx={{ mt: 3 }}>
|
<Box sx={{ mt: 3 }}>
|
||||||
{selectedCandidate && (
|
{selectedCandidate && (
|
||||||
<JobMatchAnalysis
|
<JobMatchAnalysis
|
||||||
job={{ title: jobTitle, description: jobDescription }}
|
job={{ title: jobTitle, description: jobDescription, company: company, ownerId: user_id, ownerType: user_type }}
|
||||||
candidate={selectedCandidate}
|
candidate={selectedCandidate}
|
||||||
{...backstoryProps}
|
{...backstoryProps}
|
||||||
/>
|
/>
|
||||||
@ -434,14 +328,14 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
|||||||
);
|
);
|
||||||
|
|
||||||
// If no user is logged in, show message
|
// If no user is logged in, show message
|
||||||
if (!user) {
|
if (!user?.id) {
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="md">
|
<Container maxWidth="md">
|
||||||
<Paper elevation={3} sx={{ p: 4, mt: 4, textAlign: 'center' }}>
|
<Paper elevation={3} sx={{ p: 4, mt: 4, textAlign: 'center' }}>
|
||||||
<Typography variant="h5" gutterBottom>
|
<Typography variant="h5" gutterBottom>
|
||||||
Please log in to access candidate analysis
|
Please log in to access candidate analysis
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button variant="contained" color="primary" sx={{ mt: 2 }}>
|
<Button variant="contained" onClick={() => { navigate('/login'); }} color="primary" sx={{ mt: 2 }}>
|
||||||
Log In
|
Log In
|
||||||
</Button>
|
</Button>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
@ -24,9 +24,9 @@ const CandidateRoute: React.FC<CandidateRouteProps> = (props: CandidateRouteProp
|
|||||||
if (candidate?.username === username || !username) {
|
if (candidate?.username === username || !username) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const getCandidate = async (username: string) => {
|
const getCandidate = async (reference: string) => {
|
||||||
try {
|
try {
|
||||||
const result : Candidate = await apiClient.getCandidate(username);
|
const result: Candidate = await apiClient.getCandidate(reference);
|
||||||
setCandidate(result);
|
setCandidate(result);
|
||||||
navigate('/chat');
|
navigate('/chat');
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -506,8 +506,9 @@ class ApiClient {
|
|||||||
// Candidate Methods with Date Conversion
|
// Candidate Methods with Date Conversion
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
async getCandidate(username: string): Promise<Types.Candidate> {
|
// reference can be candidateId, username, or email
|
||||||
const response = await fetch(`${this.baseUrl}/candidates/${username}`, {
|
async getCandidate(reference: string): Promise<Types.Candidate> {
|
||||||
|
const response = await fetch(`${this.baseUrl}/candidates/${reference}`, {
|
||||||
headers: this.defaultHeaders
|
headers: this.defaultHeaders
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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-04T05:16:43.020718
|
// Generated on: 2025-06-04T17:02:08.242818
|
||||||
// DO NOT EDIT MANUALLY - This file is auto-generated
|
// DO NOT EDIT MANUALLY - This file is auto-generated
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
@ -577,26 +577,13 @@ export interface InterviewSchedule {
|
|||||||
|
|
||||||
export interface Job {
|
export interface Job {
|
||||||
id?: string;
|
id?: string;
|
||||||
title: string;
|
ownerId: string;
|
||||||
|
ownerType: "candidate" | "employer" | "guest";
|
||||||
|
title?: string;
|
||||||
|
summary?: string;
|
||||||
|
company?: string;
|
||||||
description: string;
|
description: string;
|
||||||
responsibilities: Array<string>;
|
requirements?: JobRequirements;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JobApplication {
|
export interface JobApplication {
|
||||||
@ -615,6 +602,31 @@ export interface JobApplication {
|
|||||||
decision?: ApplicationDecision;
|
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 {
|
export interface JobListResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data?: Array<Job>;
|
data?: Array<Job>;
|
||||||
@ -1224,23 +1236,6 @@ export function convertInterviewScheduleFromApi(data: any): InterviewSchedule {
|
|||||||
endDate: new Date(data.endDate),
|
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
|
* Convert JobApplication from API response, parsing date fields
|
||||||
* Date fields: appliedDate, updatedDate
|
* Date fields: appliedDate, updatedDate
|
||||||
@ -1256,6 +1251,23 @@ export function convertJobApplicationFromApi(data: any): JobApplication {
|
|||||||
updatedDate: new Date(data.updatedDate),
|
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
|
* Convert MessageReaction from API response, parsing date fields
|
||||||
* Date fields: timestamp
|
* Date fields: timestamp
|
||||||
@ -1378,10 +1390,10 @@ export function convertFromApi<T>(data: any, modelType: string): T {
|
|||||||
return convertInterviewFeedbackFromApi(data) as T;
|
return convertInterviewFeedbackFromApi(data) as T;
|
||||||
case 'InterviewSchedule':
|
case 'InterviewSchedule':
|
||||||
return convertInterviewScheduleFromApi(data) as T;
|
return convertInterviewScheduleFromApi(data) as T;
|
||||||
case 'Job':
|
|
||||||
return convertJobFromApi(data) as T;
|
|
||||||
case 'JobApplication':
|
case 'JobApplication':
|
||||||
return convertJobApplicationFromApi(data) as T;
|
return convertJobApplicationFromApi(data) as T;
|
||||||
|
case 'JobFull':
|
||||||
|
return convertJobFullFromApi(data) as T;
|
||||||
case 'MessageReaction':
|
case 'MessageReaction':
|
||||||
return convertMessageReactionFromApi(data) as T;
|
return convertMessageReactionFromApi(data) as T;
|
||||||
case 'RAGConfiguration':
|
case 'RAGConfiguration':
|
||||||
|
@ -25,7 +25,7 @@ from models import Candidate
|
|||||||
|
|
||||||
_agents: List[Agent] = []
|
_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.
|
Get or create and append a new agent of the specified type, ensuring only one agent per type exists.
|
||||||
|
|
||||||
@ -39,7 +39,8 @@ def get_or_create_agent(agent_type: str, prometheus_collector: CollectorRegistry
|
|||||||
Raises:
|
Raises:
|
||||||
ValueError: If no matching agent type is found or if a agent of this type already exists.
|
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
|
# Check if a global (non-user) agent with the given agent_type already exists
|
||||||
|
if not user:
|
||||||
for agent in _agents:
|
for agent in _agents:
|
||||||
if agent.agent_type == agent_type:
|
if agent.agent_type == agent_type:
|
||||||
return agent
|
return agent
|
||||||
@ -48,7 +49,7 @@ def get_or_create_agent(agent_type: str, prometheus_collector: CollectorRegistry
|
|||||||
for agent_cls in Agent.__subclasses__():
|
for agent_cls in Agent.__subclasses__():
|
||||||
if agent_cls.model_fields["agent_type"].default == agent_type:
|
if agent_cls.model_fields["agent_type"].default == agent_type:
|
||||||
# Create the agent instance with provided kwargs
|
# 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
|
# if agent.agent_persist: # If an agent is not set to persist, do not add it to the list
|
||||||
_agents.append(agent)
|
_agents.append(agent)
|
||||||
return agent
|
return agent
|
||||||
|
@ -40,13 +40,17 @@ class JobRequirementsAgent(Agent):
|
|||||||
## INSTRUCTIONS:
|
## INSTRUCTIONS:
|
||||||
|
|
||||||
1. Analyze ONLY the job description provided.
|
1. Analyze ONLY the job description provided.
|
||||||
2. Extract and categorize all requirements and preferences.
|
2. Extract company information, job title, and all requirements.
|
||||||
3. DO NOT consider any candidate information - this is a pure job analysis task.
|
3. Extract and categorize all requirements and preferences.
|
||||||
|
4. DO NOT consider any candidate information - this is a pure job analysis task.
|
||||||
|
|
||||||
## OUTPUT FORMAT:
|
## OUTPUT FORMAT:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"company_name": "Company Name",
|
||||||
|
"job_title": "Job Title",
|
||||||
|
"job_summary": "Brief summary of the job",
|
||||||
"job_requirements": {
|
"job_requirements": {
|
||||||
"technical_skills": {
|
"technical_skills": {
|
||||||
"required": ["skill1", "skill2"],
|
"required": ["skill1", "skill2"],
|
||||||
@ -133,9 +137,15 @@ class JobRequirementsAgent(Agent):
|
|||||||
json_str = self.extract_json_from_text(generated_message.content)
|
json_str = self.extract_json_from_text(generated_message.content)
|
||||||
job_requirements : JobRequirements | None = None
|
job_requirements : JobRequirements | None = None
|
||||||
job_requirements_data = ""
|
job_requirements_data = ""
|
||||||
|
company_name = ""
|
||||||
|
job_summary = ""
|
||||||
|
job_title = ""
|
||||||
try:
|
try:
|
||||||
job_requirements_data = json.loads(json_str)
|
job_requirements_data = json.loads(json_str)
|
||||||
job_requirements_data = job_requirements_data.get("job_requirements", None)
|
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)
|
job_requirements = JobRequirements.model_validate(job_requirements_data)
|
||||||
if not job_requirements:
|
if not job_requirements:
|
||||||
raise ValueError("Job requirements data is empty or invalid.")
|
raise ValueError("Job requirements data is empty or invalid.")
|
||||||
@ -160,7 +170,13 @@ class JobRequirementsAgent(Agent):
|
|||||||
return
|
return
|
||||||
status_message.status = ChatStatusType.DONE
|
status_message.status = ChatStatusType.DONE
|
||||||
status_message.type = ChatMessageType.RESPONSE
|
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
|
yield status_message
|
||||||
|
|
||||||
logger.info(f"✅ Job requirements analysis completed successfully.")
|
logger.info(f"✅ Job requirements analysis completed successfully.")
|
||||||
|
@ -168,7 +168,7 @@ JSON RESPONSE:"""
|
|||||||
|
|
||||||
user_message.content = prompt
|
user_message.content = prompt
|
||||||
skill_assessment = None
|
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:
|
if skill_assessment.status == ChatStatusType.ERROR:
|
||||||
status_message.status = ChatStatusType.ERROR
|
status_message.status = ChatStatusType.ERROR
|
||||||
status_message.content = skill_assessment.content
|
status_message.content = skill_assessment.content
|
||||||
|
@ -20,11 +20,13 @@ from logger import logger
|
|||||||
import agents as agents
|
import agents as agents
|
||||||
from models import (Tunables, CandidateQuestion, ChatMessageUser, ChatMessage, RagEntry, ChatMessageType, ChatMessageMetaData, ChatStatusType, Candidate, ChatContextType)
|
from models import (Tunables, CandidateQuestion, ChatMessageUser, ChatMessage, RagEntry, ChatMessageType, ChatMessageMetaData, ChatStatusType, Candidate, ChatContextType)
|
||||||
from llm_manager import llm_manager
|
from llm_manager import llm_manager
|
||||||
|
from agents.base import Agent
|
||||||
|
|
||||||
class CandidateEntity(Candidate):
|
class CandidateEntity(Candidate):
|
||||||
model_config = {"arbitrary_types_allowed": True} # Allow ChromaDBFileWatcher, etc
|
model_config = {"arbitrary_types_allowed": True} # Allow ChromaDBFileWatcher, etc
|
||||||
|
|
||||||
# Internal instance members
|
# Internal instance members
|
||||||
|
CandidateEntity__agents: List[Agent] = []
|
||||||
CandidateEntity__observer: Optional[Any] = Field(default=None, exclude=True)
|
CandidateEntity__observer: Optional[Any] = Field(default=None, exclude=True)
|
||||||
CandidateEntity__file_watcher: Optional[ChromaDBFileWatcher] = Field(default=None, exclude=True)
|
CandidateEntity__file_watcher: Optional[ChromaDBFileWatcher] = Field(default=None, exclude=True)
|
||||||
CandidateEntity__prometheus_collector: Optional[CollectorRegistry] = Field(
|
CandidateEntity__prometheus_collector: Optional[CollectorRegistry] = Field(
|
||||||
@ -63,7 +65,7 @@ class CandidateEntity(Candidate):
|
|||||||
# Check if file exists
|
# Check if file exists
|
||||||
return user_info_path.is_file()
|
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.
|
Get or create an agent of the specified type for this candidate.
|
||||||
|
|
||||||
@ -74,11 +76,17 @@ class CandidateEntity(Candidate):
|
|||||||
Returns:
|
Returns:
|
||||||
The created agent instance.
|
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(
|
return agents.get_or_create_agent(
|
||||||
agent_type=agent_type,
|
agent_type=agent_type,
|
||||||
user=self,
|
user=self,
|
||||||
prometheus_collector=self.prometheus_collector,
|
prometheus_collector=self.prometheus_collector
|
||||||
**kwargs)
|
)
|
||||||
|
|
||||||
# Wrapper properties that map into file_watcher
|
# Wrapper properties that map into file_watcher
|
||||||
@property
|
@property
|
||||||
|
@ -64,13 +64,13 @@ import agents
|
|||||||
# =============================
|
# =============================
|
||||||
from models import (
|
from models import (
|
||||||
# API
|
# API
|
||||||
LoginRequest, CreateCandidateRequest, CreateEmployerRequest,
|
Job, LoginRequest, CreateCandidateRequest, CreateEmployerRequest,
|
||||||
|
|
||||||
# User models
|
# User models
|
||||||
Candidate, Employer, BaseUserWithType, BaseUser, Guest, Authentication, AuthResponse, CandidateAI,
|
Candidate, Employer, BaseUserWithType, BaseUser, Guest, Authentication, AuthResponse, CandidateAI,
|
||||||
|
|
||||||
# Job models
|
# Job models
|
||||||
Job, JobApplication, ApplicationStatus,
|
JobFull, JobApplication, ApplicationStatus,
|
||||||
|
|
||||||
# Chat models
|
# Chat models
|
||||||
ChatSession, ChatMessage, ChatContext, ChatQuery, ChatStatusType, ChatMessageBase, ChatMessageUser, ChatSenderType, ChatMessageType, ChatContextType,
|
ChatSession, ChatMessage, ChatContext, ChatQuery, ChatStatusType, ChatMessageBase, ChatMessageUser, ChatSenderType, ChatMessageType, ChatContextType,
|
||||||
@ -2609,28 +2609,24 @@ async def confirm_password_reset(
|
|||||||
# ============================
|
# ============================
|
||||||
|
|
||||||
@api_router.post("/jobs")
|
@api_router.post("/jobs")
|
||||||
async def create_job(
|
async def create_candidate_job(
|
||||||
job_data: Dict[str, Any] = Body(...),
|
job_data: Dict[str, Any] = Body(...),
|
||||||
current_user = Depends(get_current_user),
|
current_user = Depends(get_current_user),
|
||||||
database: RedisDatabase = Depends(get_database)
|
database: RedisDatabase = Depends(get_database)
|
||||||
):
|
):
|
||||||
"""Create a new job"""
|
"""Create a new job"""
|
||||||
|
is_employer = isinstance(current_user, Employer)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Verify user is an employer
|
if is_employer:
|
||||||
if not isinstance(current_user, Employer):
|
job = JobFull.model_validate(job_data)
|
||||||
return JSONResponse(
|
else:
|
||||||
status_code=403,
|
job = Job.model_validate(job_data)
|
||||||
content=create_error_response("FORBIDDEN", "Only employers can create jobs")
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add required fields
|
# Add required fields
|
||||||
job_data["id"] = str(uuid.uuid4())
|
job.id = str(uuid.uuid4())
|
||||||
job_data["datePosted"] = datetime.now(UTC).isoformat()
|
job.owner_id = current_user.id
|
||||||
job_data["views"] = 0
|
|
||||||
job_data["applicationCount"] = 0
|
|
||||||
job_data["employerId"] = current_user.id
|
|
||||||
|
|
||||||
job = Job.model_validate(job_data)
|
|
||||||
await database.set_job(job.id, job.model_dump())
|
await database.set_job(job.id, job.model_dump())
|
||||||
|
|
||||||
return create_success_response(job.model_dump(by_alias=True, exclude_unset=True))
|
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
|
# Get all jobs from Redis
|
||||||
all_jobs_data = await database.get_all_jobs()
|
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(
|
paginated_jobs, total = filter_and_paginate(
|
||||||
jobs_list, page, limit, sortBy, sortOrder, filter_dict
|
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))
|
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(
|
async def get_candidate(
|
||||||
username: str = Path(...),
|
reference: str = Path(...),
|
||||||
database: RedisDatabase = Depends(get_database)
|
database: RedisDatabase = Depends(get_database)
|
||||||
):
|
):
|
||||||
"""Get a candidate by username"""
|
"""Get a candidate by username"""
|
||||||
try:
|
try:
|
||||||
|
# Normalize reference to lowercase for case-insensitive search
|
||||||
|
query_lower = reference.lower()
|
||||||
|
|
||||||
all_candidates_data = await database.get_all_candidates()
|
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")
|
||||||
|
)
|
||||||
|
|
||||||
# Normalize username to lowercase for case-insensitive search
|
candidate_data = None
|
||||||
query_lower = username.lower()
|
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
|
||||||
|
|
||||||
# Filter by search query
|
if not candidate_data:
|
||||||
candidates_list = [
|
logger.warning(f"⚠️ Candidate not found for reference: {reference}")
|
||||||
c for c in candidates_list
|
|
||||||
if (query_lower == c.email.lower() or
|
|
||||||
query_lower == c.username.lower())
|
|
||||||
]
|
|
||||||
|
|
||||||
if not len(candidates_list):
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=404,
|
status_code=404,
|
||||||
content=create_error_response("NOT_FOUND", "Candidate not found")
|
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)
|
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))
|
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)
|
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:
|
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)
|
agent = candidate_entity.get_or_create_agent(agent_type=ChatContextType.SKILL_MATCH)
|
||||||
if not agent:
|
if not agent:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
|
@ -108,17 +108,6 @@ class SkillMatch(BaseModel):
|
|||||||
"populate_by_name": True # Allow both field names and aliases
|
"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):
|
class ChatMessageType(str, Enum):
|
||||||
ERROR = "error"
|
ERROR = "error"
|
||||||
@ -650,14 +639,32 @@ class AuthResponse(BaseModel):
|
|||||||
"populate_by_name": True # Allow both field names and aliases
|
"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):
|
class Job(BaseModel):
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
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
|
description: str
|
||||||
responsibilities: List[str]
|
requirements: Optional[JobRequirements]
|
||||||
requirements: List[str]
|
model_config = {
|
||||||
preferred_skills: Optional[List[str]] = Field(None, alias="preferredSkills")
|
"populate_by_name": True # Allow both field names and aliases
|
||||||
employer_id: str = Field(..., alias="employerId")
|
}
|
||||||
|
|
||||||
|
class JobFull(Job):
|
||||||
location: Location
|
location: Location
|
||||||
salary_range: Optional[SalaryRange] = Field(None, alias="salaryRange")
|
salary_range: Optional[SalaryRange] = Field(None, alias="salaryRange")
|
||||||
employment_type: EmploymentType = Field(..., alias="employmentType")
|
employment_type: EmploymentType = Field(..., alias="employmentType")
|
||||||
@ -672,9 +679,6 @@ class Job(BaseModel):
|
|||||||
featured_until: Optional[datetime] = Field(None, alias="featuredUntil")
|
featured_until: Optional[datetime] = Field(None, alias="featuredUntil")
|
||||||
views: int = 0
|
views: int = 0
|
||||||
application_count: int = Field(0, alias="applicationCount")
|
application_count: int = Field(0, alias="applicationCount")
|
||||||
model_config = {
|
|
||||||
"populate_by_name": True # Allow both field names and aliases
|
|
||||||
}
|
|
||||||
|
|
||||||
class InterviewFeedback(BaseModel):
|
class InterviewFeedback(BaseModel):
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
||||||
@ -1061,4 +1065,4 @@ Candidate.update_forward_refs()
|
|||||||
Employer.update_forward_refs()
|
Employer.update_forward_refs()
|
||||||
ChatSession.update_forward_refs()
|
ChatSession.update_forward_refs()
|
||||||
JobApplication.update_forward_refs()
|
JobApplication.update_forward_refs()
|
||||||
Job.update_forward_refs()
|
JobFull.update_forward_refs()
|
Loading…
x
Reference in New Issue
Block a user