Working on job creation flow
This commit is contained in:
parent
9edf5a5b23
commit
4f4187eba4
@ -1,27 +1,19 @@
|
|||||||
import React, { useState, useEffect, useRef, JSX } from 'react';
|
import React, { useState, useRef, JSX } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Typography,
|
Typography,
|
||||||
Paper,
|
|
||||||
TextField,
|
TextField,
|
||||||
Grid,
|
Grid,
|
||||||
Dialog,
|
|
||||||
DialogTitle,
|
|
||||||
DialogContent,
|
|
||||||
DialogContentText,
|
|
||||||
DialogActions,
|
|
||||||
IconButton,
|
|
||||||
useTheme,
|
useTheme,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
Chip,
|
Chip,
|
||||||
Divider,
|
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
Stack,
|
Stack,
|
||||||
Alert
|
Paper,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
SyncAlt,
|
SyncAlt,
|
||||||
@ -36,21 +28,22 @@ import {
|
|||||||
CloudUpload,
|
CloudUpload,
|
||||||
Description,
|
Description,
|
||||||
Business,
|
Business,
|
||||||
LocationOn,
|
|
||||||
Work,
|
Work,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
Star
|
Star
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import DescriptionIcon from '@mui/icons-material/Description';
|
|
||||||
import FileUploadIcon from '@mui/icons-material/FileUpload';
|
import FileUploadIcon from '@mui/icons-material/FileUpload';
|
||||||
|
|
||||||
import { useAuth } from 'hooks/AuthContext';
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
import { useAppState, useSelectedCandidate, useSelectedJob } from 'hooks/GlobalContext';
|
import { useAppState, useSelectedJob } from 'hooks/GlobalContext';
|
||||||
import { BackstoryElementProps } from './BackstoryTab';
|
import { BackstoryElementProps } from './BackstoryTab';
|
||||||
import { LoginRequired } from 'components/ui/LoginRequired';
|
import { LoginRequired } from 'components/ui/LoginRequired';
|
||||||
|
|
||||||
import * as Types from 'types/types';
|
import * as Types from 'types/types';
|
||||||
|
import { StyledMarkdown } from './StyledMarkdown';
|
||||||
|
import { JobInfo } from './ui/JobInfo';
|
||||||
|
import { Scrollable } from './Scrollable';
|
||||||
|
|
||||||
const VisuallyHiddenInput = styled('input')({
|
const VisuallyHiddenInput = styled('input')({
|
||||||
clip: 'rect(0 0 0 0)',
|
clip: 'rect(0 0 0 0)',
|
||||||
@ -114,35 +107,27 @@ const getIcon = (type: Types.ApiActivityType) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
interface JobCreator extends BackstoryElementProps {
|
interface JobCreatorProps extends BackstoryElementProps {
|
||||||
onSave?: (job: Types.Job) => void;
|
onSave?: (job: Types.Job) => void;
|
||||||
}
|
}
|
||||||
const JobCreator = (props: JobCreator) => {
|
const JobCreator = (props: JobCreatorProps) => {
|
||||||
const { user, apiClient } = useAuth();
|
const { user, apiClient } = useAuth();
|
||||||
const { onSave } = props;
|
const { onSave } = props;
|
||||||
const { selectedCandidate } = useSelectedCandidate();
|
|
||||||
const { selectedJob, setSelectedJob } = useSelectedJob();
|
const { selectedJob, setSelectedJob } = useSelectedJob();
|
||||||
const { setSnack } = useAppState();
|
const { setSnack } = useAppState();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
const isTablet = useMediaQuery(theme.breakpoints.down('md'));
|
|
||||||
|
|
||||||
const [openUploadDialog, setOpenUploadDialog] = useState<boolean>(false);
|
|
||||||
const [jobDescription, setJobDescription] = useState<string>('');
|
const [jobDescription, setJobDescription] = useState<string>('');
|
||||||
const [jobRequirements, setJobRequirements] = useState<Types.JobRequirements | null>(null);
|
const [jobRequirements, setJobRequirements] = useState<Types.JobRequirements | null>(null);
|
||||||
const [jobTitle, setJobTitle] = useState<string>('');
|
const [jobTitle, setJobTitle] = useState<string>('');
|
||||||
const [company, setCompany] = useState<string>('');
|
const [company, setCompany] = useState<string>('');
|
||||||
const [summary, setSummary] = useState<string>('');
|
const [summary, setSummary] = useState<string>('');
|
||||||
const [jobLocation, setJobLocation] = useState<string>('');
|
const [job, setJob] = useState<Types.Job | null>(null);
|
||||||
const [jobId, setJobId] = useState<string>('');
|
|
||||||
const [jobStatus, setJobStatus] = useState<string>('');
|
const [jobStatus, setJobStatus] = useState<string>('');
|
||||||
const [jobStatusIcon, setJobStatusIcon] = useState<JSX.Element>(<></>);
|
const [jobStatusIcon, setJobStatusIcon] = useState<JSX.Element>(<></>);
|
||||||
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
|
|
||||||
}, [jobTitle, jobDescription, company]);
|
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
|
||||||
@ -158,8 +143,10 @@ const JobCreator = (props: JobCreator) => {
|
|||||||
setJobStatusIcon(getIcon(status.activity));
|
setJobStatusIcon(getIcon(status.activity));
|
||||||
setJobStatus(status.content);
|
setJobStatus(status.content);
|
||||||
},
|
},
|
||||||
onMessage: (job: Types.Job) => {
|
onMessage: (jobMessage: Types.JobRequirementsMessage) => {
|
||||||
|
const job: Types.Job = jobMessage.job
|
||||||
console.log('onMessage - job', job);
|
console.log('onMessage - job', job);
|
||||||
|
setJob(job);
|
||||||
setCompany(job.company || '');
|
setCompany(job.company || '');
|
||||||
setJobDescription(job.description);
|
setJobDescription(job.description);
|
||||||
setSummary(job.summary || '');
|
setSummary(job.summary || '');
|
||||||
@ -333,8 +320,9 @@ const JobCreator = (props: JobCreator) => {
|
|||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
const job = await apiClient.createJob(newJob);
|
const jobMessage = await apiClient.createJob(newJob);
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
|
const job: Types.Job = jobMessage.job;
|
||||||
onSave ? onSave(job) : setSelectedJob(job);
|
onSave ? onSave(job) : setSelectedJob(job);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -357,10 +345,6 @@ const JobCreator = (props: JobCreator) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderJobCreation = () => {
|
const renderJobCreation = () => {
|
||||||
if (!user) {
|
|
||||||
return <Box>You must be logged in</Box>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
mx: 'auto', p: { xs: 2, sm: 3 },
|
mx: 'auto', p: { xs: 2, sm: 3 },
|
||||||
@ -500,21 +484,6 @@ const JobCreator = (props: JobCreator) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid> */}
|
</Grid> */}
|
||||||
|
|
||||||
<Grid size={{ xs: 12, md: 6 }}>
|
|
||||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-end', height: '100%' }}>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={!jobTitle || !company || !jobDescription || isProcessing}
|
|
||||||
fullWidth={isMobile}
|
|
||||||
size="large"
|
|
||||||
startIcon={<CheckCircle />}
|
|
||||||
>
|
|
||||||
Save Job
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@ -548,7 +517,27 @@ const JobCreator = (props: JobCreator) => {
|
|||||||
width: "100%",
|
width: "100%",
|
||||||
display: "flex", flexDirection: "column"
|
display: "flex", flexDirection: "column"
|
||||||
}}>
|
}}>
|
||||||
{selectedJob === null && renderJobCreation()}
|
{job === null && renderJobCreation()}
|
||||||
|
{job &&
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-end', height: '100%' }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={!jobTitle || !company || !jobDescription || isProcessing}
|
||||||
|
fullWidth={isMobile}
|
||||||
|
size="large"
|
||||||
|
startIcon={<CheckCircle />}
|
||||||
|
>
|
||||||
|
Save Job
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "row", flexShrink: 1, gap: 1 }}>
|
||||||
|
<Paper elevation={1} sx={{ p: 1, m: 1, flexGrow: 1 }}><Scrollable><JobInfo job={job} /></Scrollable></Paper>
|
||||||
|
<Paper elevation={1} sx={{ p: 1, m: 1, flexGrow: 1 }}><Scrollable><StyledMarkdown content={job.description} /></Scrollable></Paper>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
import { useMediaQuery } from '@mui/material';
|
import { useMediaQuery } from '@mui/material';
|
||||||
import { JobFull } from 'types/types';
|
import { Job, JobFull } from 'types/types';
|
||||||
import { CopyBubble } from "components/CopyBubble";
|
import { CopyBubble } from "components/CopyBubble";
|
||||||
import { rest } from 'lodash';
|
import { rest } from 'lodash';
|
||||||
import { AIBanner } from 'components/ui/AIBanner';
|
import { AIBanner } from 'components/ui/AIBanner';
|
||||||
@ -17,7 +17,7 @@ import { DeleteConfirmation } from '../DeleteConfirmation';
|
|||||||
import { Build, CheckCircle, Description, Psychology, Star, Work } from '@mui/icons-material';
|
import { Build, CheckCircle, Description, Psychology, Star, Work } from '@mui/icons-material';
|
||||||
|
|
||||||
interface JobInfoProps {
|
interface JobInfoProps {
|
||||||
job: JobFull;
|
job: Job | JobFull;
|
||||||
sx?: SxProps;
|
sx?: SxProps;
|
||||||
action?: string;
|
action?: string;
|
||||||
elevation?: number;
|
elevation?: number;
|
||||||
@ -153,7 +153,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
|||||||
>
|
>
|
||||||
<CardContent sx={{ display: "flex", flexGrow: 1, p: 3, height: '100%', flexDirection: 'column', alignItems: 'stretch', position: "relative" }}>
|
<CardContent sx={{ display: "flex", flexGrow: 1, p: 3, height: '100%', flexDirection: 'column', alignItems: 'stretch', position: "relative" }}>
|
||||||
{variant !== "small" && <>
|
{variant !== "small" && <>
|
||||||
{job.location &&
|
{'location' in job &&
|
||||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||||
<strong>Location:</strong> {job.location.city}, {job.location.state || job.location.country}
|
<strong>Location:</strong> {job.location.city}, {job.location.state || job.location.country}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
@ -83,7 +83,7 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
|||||||
Session ID: {guest.sessionId}
|
Session ID: {guest.sessionId}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
Created: {guest.createdAt.toLocaleString()}
|
Created: {guest.createdAt?.toLocaleString()}
|
||||||
</Typography>
|
</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -39,6 +39,7 @@ const TOKEN_STORAGE = {
|
|||||||
PENDING_VERIFICATION_EMAIL: 'pendingVerificationEmail'
|
PENDING_VERIFICATION_EMAIL: 'pendingVerificationEmail'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// Streaming Types and Interfaces
|
// Streaming Types and Interfaces
|
||||||
// ============================
|
// ============================
|
||||||
@ -647,12 +648,12 @@ class ApiClient {
|
|||||||
// Job Methods with Date Conversion
|
// Job Methods with Date Conversion
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
createJobFromDescription(job_description: string, streamingOptions?: StreamingOptions<Types.Job>): StreamingResponse<Types.Job> {
|
createJobFromDescription(job_description: string, streamingOptions?: StreamingOptions<Types.JobRequirementsMessage>): StreamingResponse<Types.JobRequirementsMessage> {
|
||||||
const body = JSON.stringify(job_description);
|
const body = JSON.stringify(job_description);
|
||||||
return this.streamify<Types.Job>('/jobs/from-content', body, streamingOptions);
|
return this.streamify<Types.JobRequirementsMessage>('/jobs/from-content', body, streamingOptions, "JobRequirementsMessage");
|
||||||
}
|
}
|
||||||
|
|
||||||
async createJob(job: Omit<Types.Job, 'id' | 'datePosted' | 'views' | 'applicationCount'>): Promise<Types.Job> {
|
async createJob(job: Omit<Types.Job, 'id' | 'datePosted' | 'views' | 'applicationCount'>): Promise<Types.JobRequirementsMessage> {
|
||||||
const body = JSON.stringify(formatApiRequest(job));
|
const body = JSON.stringify(formatApiRequest(job));
|
||||||
const response = await fetch(`${this.baseUrl}/jobs`, {
|
const response = await fetch(`${this.baseUrl}/jobs`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -660,7 +661,7 @@ class ApiClient {
|
|||||||
body: body
|
body: body
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.handleApiResponseWithConversion<Types.Job>(response, 'Job');
|
return this.handleApiResponseWithConversion<Types.JobRequirementsMessage>(response, 'JobRequirementsMessage');
|
||||||
}
|
}
|
||||||
|
|
||||||
async getJob(id: string): Promise<Types.Job> {
|
async getJob(id: string): Promise<Types.Job> {
|
||||||
@ -884,10 +885,10 @@ class ApiClient {
|
|||||||
'Authorization': this.defaultHeaders['Authorization']
|
'Authorization': this.defaultHeaders['Authorization']
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return this.streamify<Types.DocumentMessage>('/candidates/documents/upload', formData, streamingOptions);
|
return this.streamify<Types.DocumentMessage>('/candidates/documents/upload', formData, streamingOptions, "DocumentMessage");
|
||||||
}
|
}
|
||||||
|
|
||||||
createJobFromFile(file: File, streamingOptions?: StreamingOptions<Types.Job>): StreamingResponse<Types.Job> {
|
createJobFromFile(file: File, streamingOptions?: StreamingOptions<Types.JobRequirementsMessage>): StreamingResponse<Types.JobRequirementsMessage> {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('filename', file.name);
|
formData.append('filename', file.name);
|
||||||
@ -898,7 +899,7 @@ class ApiClient {
|
|||||||
'Authorization': this.defaultHeaders['Authorization']
|
'Authorization': this.defaultHeaders['Authorization']
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return this.streamify<Types.Job>('/jobs/upload', formData, streamingOptions);
|
return this.streamify<Types.JobRequirementsMessage>('/jobs/upload', formData, streamingOptions, "JobRequirementsMessage");
|
||||||
}
|
}
|
||||||
|
|
||||||
getJobRequirements(jobId: string, streamingOptions?: StreamingOptions<Types.DocumentMessage>): StreamingResponse<Types.DocumentMessage> {
|
getJobRequirements(jobId: string, streamingOptions?: StreamingOptions<Types.DocumentMessage>): StreamingResponse<Types.DocumentMessage> {
|
||||||
@ -906,7 +907,7 @@ class ApiClient {
|
|||||||
...streamingOptions,
|
...streamingOptions,
|
||||||
headers: this.defaultHeaders,
|
headers: this.defaultHeaders,
|
||||||
};
|
};
|
||||||
return this.streamify<Types.DocumentMessage>(`/jobs/requirements/${jobId}`, null, streamingOptions);
|
return this.streamify<Types.DocumentMessage>(`/jobs/requirements/${jobId}`, null, streamingOptions, "DocumentMessage");
|
||||||
}
|
}
|
||||||
|
|
||||||
generateResume(candidateId: string, skills: Types.SkillAssessment[], streamingOptions?: StreamingOptions<Types.ChatMessageResume>): StreamingResponse<Types.ChatMessageResume> {
|
generateResume(candidateId: string, skills: Types.SkillAssessment[], streamingOptions?: StreamingOptions<Types.ChatMessageResume>): StreamingResponse<Types.ChatMessageResume> {
|
||||||
@ -915,7 +916,7 @@ class ApiClient {
|
|||||||
...streamingOptions,
|
...streamingOptions,
|
||||||
headers: this.defaultHeaders,
|
headers: this.defaultHeaders,
|
||||||
};
|
};
|
||||||
return this.streamify<Types.ChatMessageResume>(`/candidates/${candidateId}/generate-resume`, body, streamingOptions);
|
return this.streamify<Types.ChatMessageResume>(`/candidates/${candidateId}/generate-resume`, body, streamingOptions, "ChatMessageResume");
|
||||||
}
|
}
|
||||||
candidateMatchForRequirement(candidate_id: string, requirement: string,
|
candidateMatchForRequirement(candidate_id: string, requirement: string,
|
||||||
streamingOptions?: StreamingOptions<Types.ChatMessageSkillAssessment>)
|
streamingOptions?: StreamingOptions<Types.ChatMessageSkillAssessment>)
|
||||||
@ -925,7 +926,7 @@ class ApiClient {
|
|||||||
...streamingOptions,
|
...streamingOptions,
|
||||||
headers: this.defaultHeaders,
|
headers: this.defaultHeaders,
|
||||||
};
|
};
|
||||||
return this.streamify<Types.ChatMessageSkillAssessment>(`/candidates/${candidate_id}/skill-match`, body, streamingOptions);
|
return this.streamify<Types.ChatMessageSkillAssessment>(`/candidates/${candidate_id}/skill-match`, body, streamingOptions, "ChatMessageSkillAssessment");
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateCandidateDocument(document: Types.Document) : Promise<Types.Document> {
|
async updateCandidateDocument(document: Types.Document) : Promise<Types.Document> {
|
||||||
@ -1226,7 +1227,7 @@ class ApiClient {
|
|||||||
* @param options callbacks, headers, and method
|
* @param options callbacks, headers, and method
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
streamify<T = Types.ChatMessage[]>(api: string, data: BodyInit | null, options: StreamingOptions<T> = {}) : StreamingResponse<T> {
|
streamify<T = Types.ChatMessage[]>(api: string, data: BodyInit | null, options: StreamingOptions<T> = {}, modelType?: string) : StreamingResponse<T> {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
const signal = options.signal || abortController.signal;
|
const signal = options.signal || abortController.signal;
|
||||||
const headers = options.headers || null;
|
const headers = options.headers || null;
|
||||||
@ -1308,8 +1309,8 @@ class ApiClient {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'done':
|
case 'done':
|
||||||
const message = Types.convertApiMessageFromApi(incoming) as T;
|
const message = (modelType ? convertFromApi<T>(incoming, modelType) : incoming) as T;
|
||||||
finalMessage = message as any;
|
finalMessage = message;
|
||||||
try {
|
try {
|
||||||
options.onMessage?.(message);
|
options.onMessage?.(message);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -1361,7 +1362,7 @@ class ApiClient {
|
|||||||
options: StreamingOptions = {}
|
options: StreamingOptions = {}
|
||||||
): StreamingResponse {
|
): StreamingResponse {
|
||||||
const body = JSON.stringify(formatApiRequest(chatMessage));
|
const body = JSON.stringify(formatApiRequest(chatMessage));
|
||||||
return this.streamify(`/chat/sessions/${chatMessage.sessionId}/messages/stream`, body, options)
|
return this.streamify(`/chat/sessions/${chatMessage.sessionId}/messages/stream`, body, options, "ChatMessage")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1470,7 +1471,6 @@ class ApiClient {
|
|||||||
// ============================
|
// ============================
|
||||||
// Error Handling Helper
|
// Error Handling Helper
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
async handleRequest<T>(requestFn: () => Promise<Response>, modelType?: string): Promise<T> {
|
async handleRequest<T>(requestFn: () => Promise<Response>, modelType?: string): Promise<T> {
|
||||||
try {
|
try {
|
||||||
const response = await requestFn();
|
const response = await requestFn();
|
||||||
|
@ -823,5 +823,16 @@ Content: {content}
|
|||||||
|
|
||||||
raise ValueError("No JSON found in the response")
|
raise ValueError("No JSON found in the response")
|
||||||
|
|
||||||
|
def extract_markdown_from_text(self, text: str) -> str:
|
||||||
|
"""Extract Markdown string from text that may contain other content."""
|
||||||
|
markdown_pattern = r"```(md|markdown)\s*([\s\S]*?)\s*```"
|
||||||
|
match = re.search(markdown_pattern, text)
|
||||||
|
if match:
|
||||||
|
return match.group(2).strip()
|
||||||
|
|
||||||
|
raise ValueError("No Markdown found in the response")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Register the base agent
|
# Register the base agent
|
||||||
agent_registry.register(Agent._agent_type, Agent)
|
agent_registry.register(Agent._agent_type, Agent)
|
||||||
|
@ -511,14 +511,5 @@ Make sure at least one of the candidate's job descriptions take into account the
|
|||||||
|
|
||||||
raise ValueError("No JSON found in the response")
|
raise ValueError("No JSON found in the response")
|
||||||
|
|
||||||
def extract_markdown_from_text(self, text: str) -> str:
|
|
||||||
"""Extract Markdown string from text that may contain other content."""
|
|
||||||
markdown_pattern = r"```(md|markdown)\s*([\s\S]*?)\s*```"
|
|
||||||
match = re.search(markdown_pattern, text)
|
|
||||||
if match:
|
|
||||||
return match.group(2).strip()
|
|
||||||
|
|
||||||
raise ValueError("No Markdown found in the response")
|
|
||||||
|
|
||||||
# Register the base agent
|
# Register the base agent
|
||||||
agent_registry.register(GeneratePersona._agent_type, GeneratePersona)
|
agent_registry.register(GeneratePersona._agent_type, GeneratePersona)
|
||||||
|
@ -19,7 +19,7 @@ import asyncio
|
|||||||
import numpy as np # type: ignore
|
import numpy as np # type: ignore
|
||||||
|
|
||||||
from .base import Agent, agent_registry, LLMMessage
|
from .base import Agent, agent_registry, LLMMessage
|
||||||
from models import ApiActivityType, Candidate, ChatMessage, ChatMessageError, ChatMessageMetaData, ApiMessageType, ChatMessageStatus, ChatMessageUser, ChatOptions, ChatSenderType, ApiStatusType, JobRequirements, JobRequirementsMessage, Tunables
|
from models import ApiActivityType, Candidate, ChatMessage, ChatMessageError, ChatMessageMetaData, ApiMessageType, ChatMessageStatus, ChatMessageUser, ChatOptions, ChatSenderType, ApiStatusType, Job, JobRequirements, JobRequirementsMessage, Tunables
|
||||||
import model_cast
|
import model_cast
|
||||||
from logger import logger
|
from logger import logger
|
||||||
import defines
|
import defines
|
||||||
@ -107,6 +107,15 @@ class JobRequirementsAgent(Agent):
|
|||||||
async def generate(
|
async def generate(
|
||||||
self, llm: Any, model: str, session_id: str, prompt: str, tunables: Optional[Tunables] = None, temperature=0.7
|
self, llm: Any, model: str, session_id: str, prompt: str, tunables: Optional[Tunables] = None, temperature=0.7
|
||||||
) -> AsyncGenerator[ChatMessage, None]:
|
) -> AsyncGenerator[ChatMessage, None]:
|
||||||
|
if not self.user:
|
||||||
|
error_message = ChatMessageError(
|
||||||
|
session_id=session_id,
|
||||||
|
content="User is not set for this agent."
|
||||||
|
)
|
||||||
|
logger.error(f"⚠️ {error_message.content}")
|
||||||
|
yield error_message
|
||||||
|
return
|
||||||
|
|
||||||
# Stage 1A: Analyze job requirements
|
# Stage 1A: Analyze job requirements
|
||||||
status_message = ChatMessageStatus(
|
status_message = ChatMessageStatus(
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
@ -166,15 +175,21 @@ class JobRequirementsAgent(Agent):
|
|||||||
logger.error(f"⚠️ {status_message.content}")
|
logger.error(f"⚠️ {status_message.content}")
|
||||||
yield status_message
|
yield status_message
|
||||||
return
|
return
|
||||||
job_requirements_message = JobRequirementsMessage(
|
job = Job(
|
||||||
session_id=session_id,
|
owner_id=self.user.id,
|
||||||
status=ApiStatusType.DONE,
|
owner_type=self.user.user_type,
|
||||||
requirements=requirements,
|
|
||||||
company=company,
|
company=company,
|
||||||
title=title,
|
title=title,
|
||||||
summary=summary,
|
summary=summary,
|
||||||
|
requirements=requirements,
|
||||||
|
session_id=session_id,
|
||||||
description=prompt,
|
description=prompt,
|
||||||
)
|
)
|
||||||
|
job_requirements_message = JobRequirementsMessage(
|
||||||
|
session_id=session_id,
|
||||||
|
status=ApiStatusType.DONE,
|
||||||
|
job=job,
|
||||||
|
)
|
||||||
yield job_requirements_message
|
yield job_requirements_message
|
||||||
logger.info(f"✅ Job requirements analysis completed successfully.")
|
logger.info(f"✅ Job requirements analysis completed successfully.")
|
||||||
return
|
return
|
||||||
|
@ -2357,7 +2357,6 @@ async def create_job_from_content(database: RedisDatabase, current_user: Candida
|
|||||||
activity=ApiActivityType.SEARCHING
|
activity=ApiActivityType.SEARCHING
|
||||||
)
|
)
|
||||||
yield status_message
|
yield status_message
|
||||||
await asyncio.sleep(0)
|
|
||||||
|
|
||||||
async for message in chat_agent.generate(
|
async for message in chat_agent.generate(
|
||||||
llm=llm_manager.get_llm(),
|
llm=llm_manager.get_llm(),
|
||||||
@ -2366,16 +2365,52 @@ async def create_job_from_content(database: RedisDatabase, current_user: Candida
|
|||||||
prompt=content
|
prompt=content
|
||||||
):
|
):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if not message or not isinstance(message, JobRequirementsMessage):
|
if not message or not isinstance(message, JobRequirementsMessage):
|
||||||
error_message = ChatMessageError(
|
error_message = ChatMessageError(
|
||||||
sessionId=MOCK_UUID, # No session ID for document uploads
|
sessionId=MOCK_UUID, # No session ID for document uploads
|
||||||
content="Failed to process job description file"
|
content="Job extraction did not convert successfully"
|
||||||
)
|
)
|
||||||
yield error_message
|
yield error_message
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info(f"✅ Successfully saved job requirements job {message.id}")
|
status_message = ChatMessageStatus(
|
||||||
yield message
|
sessionId=MOCK_UUID, # No session ID for document uploads
|
||||||
|
content=f"Reformatting job description as markdown...",
|
||||||
|
activity=ApiActivityType.CONVERTING
|
||||||
|
)
|
||||||
|
yield status_message
|
||||||
|
|
||||||
|
job_requirements : JobRequirementsMessage = message
|
||||||
|
async for message in chat_agent.llm_one_shot(
|
||||||
|
llm=llm_manager.get_llm(),
|
||||||
|
model=defines.model,
|
||||||
|
session_id=MOCK_UUID,
|
||||||
|
prompt=content,
|
||||||
|
system_prompt="""
|
||||||
|
You are a document editor. Take the provided job description and reformat as legible markdown.
|
||||||
|
Return only the markdown content, no other text. Make sure all content is included.
|
||||||
|
"""
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not message or not isinstance(message, ChatMessage):
|
||||||
|
logger.error("❌ Failed to reformat job description to markdown")
|
||||||
|
error_message = ChatMessageError(
|
||||||
|
sessionId=MOCK_UUID, # No session ID for document uploads
|
||||||
|
content="Failed to reformat job description"
|
||||||
|
)
|
||||||
|
yield error_message
|
||||||
|
return
|
||||||
|
chat_message : ChatMessage = message
|
||||||
|
markdown = chat_message.content
|
||||||
|
try:
|
||||||
|
markdown = chat_agent.extract_markdown_from_text(chat_message.content)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
job_requirements.job.description = markdown
|
||||||
|
logger.info(f"✅ Successfully saved job requirements job {job_requirements.id}")
|
||||||
|
yield job_requirements
|
||||||
return
|
return
|
||||||
|
|
||||||
@api_router.post("/candidates/profile/upload")
|
@api_router.post("/candidates/profile/upload")
|
||||||
@ -3272,6 +3307,7 @@ async def create_job_from_description(
|
|||||||
logger.info(f"📁 Received file content: size='{len(content)} bytes'")
|
logger.info(f"📁 Received file content: size='{len(content)} bytes'")
|
||||||
|
|
||||||
async for message in create_job_from_content(database=database, current_user=current_user, content=content):
|
async for message in create_job_from_content(database=database, current_user=current_user, content=content):
|
||||||
|
logger.info(f"📄 Yielding job creation message status: {message.status}")
|
||||||
yield message
|
yield message
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -3403,6 +3439,7 @@ async def create_job_from_file(
|
|||||||
yield error_message
|
yield error_message
|
||||||
logger.error(f"❌ Error converting {file.filename} to Markdown: {e}")
|
logger.error(f"❌ Error converting {file.filename} to Markdown: {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
async for message in create_job_from_content(database=database, current_user=current_user, content=file_content):
|
async for message in create_job_from_content(database=database, current_user=current_user, content=file_content):
|
||||||
yield message
|
yield message
|
||||||
return
|
return
|
||||||
|
@ -480,8 +480,8 @@ class BaseUser(BaseUserWithType):
|
|||||||
full_name: str = Field(..., alias="fullName")
|
full_name: str = Field(..., alias="fullName")
|
||||||
phone: Optional[str] = None
|
phone: Optional[str] = None
|
||||||
location: Optional[Location] = None
|
location: Optional[Location] = None
|
||||||
created_at: datetime = Field(..., alias="createdAt")
|
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt")
|
||||||
updated_at: datetime = Field(..., alias="updatedAt")
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="updatedAt")
|
||||||
last_login: Optional[datetime] = Field(None, alias="lastLogin")
|
last_login: Optional[datetime] = Field(None, alias="lastLogin")
|
||||||
profile_image: Optional[str] = Field(None, alias="profileImage")
|
profile_image: Optional[str] = Field(None, alias="profileImage")
|
||||||
status: UserStatus
|
status: UserStatus
|
||||||
@ -613,7 +613,7 @@ class Guest(BaseUser):
|
|||||||
username: str # Add username for consistency with other user types
|
username: str # Add username for consistency with other user types
|
||||||
converted_to_user_id: Optional[str] = Field(None, alias="convertedToUserId")
|
converted_to_user_id: Optional[str] = Field(None, alias="convertedToUserId")
|
||||||
ip_address: Optional[str] = Field(None, alias="ipAddress")
|
ip_address: Optional[str] = Field(None, alias="ipAddress")
|
||||||
created_at: datetime = Field(..., alias="createdAt")
|
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt")
|
||||||
user_agent: Optional[str] = Field(None, alias="userAgent")
|
user_agent: Optional[str] = Field(None, alias="userAgent")
|
||||||
rag_content_size: int = 0
|
rag_content_size: int = 0
|
||||||
model_config = {
|
model_config = {
|
||||||
@ -690,8 +690,8 @@ class Job(BaseModel):
|
|||||||
company: Optional[str]
|
company: Optional[str]
|
||||||
description: str
|
description: str
|
||||||
requirements: Optional[JobRequirements]
|
requirements: Optional[JobRequirements]
|
||||||
created_at: datetime = Field(..., alias="createdAt")
|
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt")
|
||||||
updated_at: datetime = Field(..., alias="updatedAt")
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="updatedAt")
|
||||||
model_config = {
|
model_config = {
|
||||||
"populate_by_name": True # Allow both field names and aliases
|
"populate_by_name": True # Allow both field names and aliases
|
||||||
}
|
}
|
||||||
@ -700,7 +700,7 @@ 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")
|
||||||
date_posted: datetime = Field(..., alias="datePosted")
|
date_posted: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="datePosted")
|
||||||
application_deadline: Optional[datetime] = Field(None, alias="applicationDeadline")
|
application_deadline: Optional[datetime] = Field(None, alias="applicationDeadline")
|
||||||
is_active: bool = Field(..., alias="isActive")
|
is_active: bool = Field(..., alias="isActive")
|
||||||
applicants: Optional[List["JobApplication"]] = None
|
applicants: Optional[List["JobApplication"]] = None
|
||||||
@ -723,8 +723,8 @@ class InterviewFeedback(BaseModel):
|
|||||||
weaknesses: List[str]
|
weaknesses: List[str]
|
||||||
recommendation: InterviewRecommendation
|
recommendation: InterviewRecommendation
|
||||||
comments: str
|
comments: str
|
||||||
created_at: datetime = Field(..., alias="createdAt")
|
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt")
|
||||||
updated_at: datetime = Field(..., alias="updatedAt")
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="updatedAt")
|
||||||
is_visible: bool = Field(..., alias="isVisible")
|
is_visible: bool = Field(..., alias="isVisible")
|
||||||
skill_assessments: Optional[List[SkillAssessment]] = Field(None, alias="skillAssessments")
|
skill_assessments: Optional[List[SkillAssessment]] = Field(None, alias="skillAssessments")
|
||||||
model_config = {
|
model_config = {
|
||||||
@ -954,11 +954,7 @@ class ChatMessageRagSearch(ApiMessage):
|
|||||||
|
|
||||||
class JobRequirementsMessage(ApiMessage):
|
class JobRequirementsMessage(ApiMessage):
|
||||||
type: ApiMessageType = ApiMessageType.JSON
|
type: ApiMessageType = ApiMessageType.JSON
|
||||||
title: Optional[str]
|
job: Job = Field(..., alias="job")
|
||||||
summary: Optional[str]
|
|
||||||
company: Optional[str]
|
|
||||||
description: str
|
|
||||||
requirements: Optional[JobRequirements]
|
|
||||||
|
|
||||||
class DocumentMessage(ApiMessage):
|
class DocumentMessage(ApiMessage):
|
||||||
type: ApiMessageType = ApiMessageType.JSON
|
type: ApiMessageType = ApiMessageType.JSON
|
||||||
@ -1079,8 +1075,8 @@ class RAGConfiguration(BaseModel):
|
|||||||
embedding_model: str = Field(..., alias="embeddingModel")
|
embedding_model: str = Field(..., alias="embeddingModel")
|
||||||
vector_store_type: VectorStoreType = Field(..., alias="vectorStoreType")
|
vector_store_type: VectorStoreType = Field(..., alias="vectorStoreType")
|
||||||
retrieval_parameters: RetrievalParameters = Field(..., alias="retrievalParameters")
|
retrieval_parameters: RetrievalParameters = Field(..., alias="retrievalParameters")
|
||||||
created_at: datetime = Field(..., alias="createdAt")
|
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt")
|
||||||
updated_at: datetime = Field(..., alias="updatedAt")
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="updatedAt")
|
||||||
version: int
|
version: int
|
||||||
is_active: bool = Field(..., alias="isActive")
|
is_active: bool = Field(..., alias="isActive")
|
||||||
model_config = {
|
model_config = {
|
||||||
@ -1251,7 +1247,7 @@ class EmployerResponse(BaseModel):
|
|||||||
|
|
||||||
class JobResponse(BaseModel):
|
class JobResponse(BaseModel):
|
||||||
success: bool
|
success: bool
|
||||||
data: Optional["Job"] = None
|
data: Optional[Job] = None
|
||||||
error: Optional[ErrorDetail] = None
|
error: Optional[ErrorDetail] = None
|
||||||
meta: Optional[Dict[str, Any]] = None
|
meta: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user