Added job regenerate
This commit is contained in:
parent
80b63fe0e1
commit
a46172d696
@ -12,10 +12,14 @@ services:
|
||||
environment:
|
||||
- PRODUCTION=0
|
||||
- FRONTEND_URL=https://backstory-beta.ketrenos.com
|
||||
- MODEL_NAME=${MODEL_NAME:-qwen2.5:7b}
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- REDIS_DB=0
|
||||
- SSL_ENABLED=true
|
||||
# - DEFAULT_LLM_PROVIDER=anthropic
|
||||
# - MODEL_NAME=claude-3-5-haiku-latest
|
||||
- DEFAULT_LLM_PROVIDER=openai
|
||||
- MODEL_NAME=${MODEL_NAME:-qwen2.5:7b}
|
||||
- OPENAI_URL=http://ollama:11434
|
||||
devices:
|
||||
- /dev/dri:/dev/dri
|
||||
depends_on:
|
||||
@ -52,10 +56,12 @@ services:
|
||||
environment:
|
||||
- PRODUCTION=1
|
||||
- FRONTEND_URL=https://backstory.ketrenos.com
|
||||
- MODEL_NAME=${MODEL_NAME:-qwen2.5:7b}
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- REDIS_DB=1
|
||||
- SSL_ENABLED=false
|
||||
- DEFAULT_LLM_PROVIDER=openai
|
||||
- MODEL_NAME=${MODEL_NAME:-qwen2.5:7b}
|
||||
- OPENAI_URL=http://ollama:11434
|
||||
devices:
|
||||
- /dev/dri:/dev/dri
|
||||
depends_on:
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
} from '@mui/material';
|
||||
import { Card, CardContent, Divider, useTheme } from '@mui/material';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { useMediaQuery } from '@mui/material';
|
||||
import { Job } from 'types/types';
|
||||
import { rest } from 'lodash';
|
||||
@ -32,12 +33,14 @@ interface JobInfoProps {
|
||||
action?: string;
|
||||
elevation?: number;
|
||||
variant?: 'minimal' | 'small' | 'normal' | 'all' | null;
|
||||
onClose?: () => void;
|
||||
inDialog?: boolean; // Whether this is rendered in a dialog
|
||||
}
|
||||
|
||||
const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
||||
const { setSnack } = useAppState();
|
||||
const { user, apiClient } = useAuth();
|
||||
const { sx, variant = 'normal', job } = props;
|
||||
const { sx, variant = 'normal', job, onClose, inDialog = false } = props;
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md')) || variant === 'minimal';
|
||||
const isAdmin = user?.isAdmin;
|
||||
@ -99,13 +102,9 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
||||
setAdminStatusType(status.activity);
|
||||
setAdminStatus(status.content);
|
||||
},
|
||||
onMessage: async (jobMessage: Types.JobRequirementsMessage): Promise<void> => {
|
||||
const newJob: Types.Job = jobMessage.job;
|
||||
console.log('onMessage - job', newJob);
|
||||
newJob.id = job.id;
|
||||
newJob.createdAt = job.createdAt;
|
||||
const updatedJob: Types.Job = await apiClient.updateJob(job.id || '', newJob);
|
||||
setActiveJob(updatedJob);
|
||||
onMessage: async (jobRequirementsMessage: Types.JobRequirementsMessage): Promise<void> => {
|
||||
console.log('onMessage - job', jobRequirementsMessage);
|
||||
setActiveJob(jobRequirementsMessage.job);
|
||||
},
|
||||
onError: (error: Types.ChatMessageError): void => {
|
||||
console.log('onError', error);
|
||||
@ -117,7 +116,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
||||
setAdminStatus(null);
|
||||
},
|
||||
};
|
||||
apiClient.createJobFromDescription(activeJob.description, jobStatusHandlers);
|
||||
apiClient.regenerateJob(activeJob, jobStatusHandlers);
|
||||
};
|
||||
|
||||
const renderRequirementSection = (
|
||||
@ -257,6 +256,14 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{onClose && !inDialog && (
|
||||
<Box sx={{ position: 'absolute', top: 0, right: 0, zIndex: 1 }}>
|
||||
<IconButton onClick={onClose} size="small">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@ -349,9 +356,26 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
||||
|
||||
{variant !== 'small' && variant !== 'minimal' && (
|
||||
<>
|
||||
{activeJob.details && (
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
<strong>Location:</strong> {activeJob.details.location.city},{' '}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Chip
|
||||
label={job.details?.isActive ? 'Active' : 'Inactive'}
|
||||
color={job.details?.isActive ? 'success' : 'default'}
|
||||
size="small"
|
||||
sx={{ mr: 1 }}
|
||||
/>
|
||||
{job.details?.employmentType && (
|
||||
<Chip
|
||||
label={job.details.employmentType}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{ mr: 1 }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{activeJob.details?.location && (
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
📍 {activeJob.details.location.city},{' '}
|
||||
{activeJob.details.location.state || activeJob.details.location.country}
|
||||
</Typography>
|
||||
)}
|
||||
|
@ -43,6 +43,7 @@ import {
|
||||
Edit as EditIcon,
|
||||
Delete as DeleteIcon,
|
||||
Close as CloseIcon,
|
||||
ModelTraining,
|
||||
} from '@mui/icons-material';
|
||||
import { TransitionProps } from '@mui/material/transitions';
|
||||
import * as Types from 'types/types'; // Adjust the import path as necessary
|
||||
@ -50,6 +51,7 @@ import { useAuth } from 'hooks/AuthContext';
|
||||
import { StyledMarkdown } from 'components/StyledMarkdown';
|
||||
import { Scrollable } from 'components/Scrollable';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { JobInfo } from './JobInfo';
|
||||
|
||||
// async searchJobs(query: string): Promise<Types.PaginatedResponse> {
|
||||
// const results = await this.getJobs();
|
||||
@ -73,6 +75,7 @@ interface JobsViewProps {
|
||||
onJobSelect?: (selectedJobs: Types.Job[]) => void;
|
||||
onJobView?: (job: Types.Job) => void;
|
||||
onJobEdit?: (job: Types.Job) => void;
|
||||
onJobRegenerate?: (job: Types.Job) => Promise<Types.Job>;
|
||||
onJobDelete?: (job: Types.Job) => Promise<void>;
|
||||
selectable?: boolean;
|
||||
showActions?: boolean;
|
||||
@ -90,88 +93,11 @@ const Transition = React.forwardRef(function Transition(
|
||||
return <Slide direction="up" ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
const JobInfoPanel: React.FC<{ job: Types.Job; onClose?: () => void; inDialog?: boolean }> = ({
|
||||
job,
|
||||
onClose,
|
||||
inDialog = false,
|
||||
}) => (
|
||||
<Box
|
||||
sx={{
|
||||
p: inDialog ? 2 : 1.5,
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
|
||||
<Typography variant="h5" component="h1" gutterBottom>
|
||||
{job.title}
|
||||
</Typography>
|
||||
{onClose && !inDialog && (
|
||||
<IconButton onClick={onClose} size="small">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Typography variant="h6" color="primary" gutterBottom>
|
||||
{job.company}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Chip
|
||||
label={job.details?.isActive ? 'Active' : 'Inactive'}
|
||||
color={job.details?.isActive ? 'success' : 'default'}
|
||||
size="small"
|
||||
sx={{ mr: 1 }}
|
||||
/>
|
||||
{job.details?.employmentType && (
|
||||
<Chip label={job.details.employmentType} variant="outlined" size="small" sx={{ mr: 1 }} />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{job.details?.location && (
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
📍 {job.details.location.city}, {job.details.location.state || job.details.location.country}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<StyledMarkdown content={job.description} />
|
||||
|
||||
{job.requirements &&
|
||||
job.requirements.technicalSkills &&
|
||||
job.requirements.technicalSkills.required &&
|
||||
job.requirements.technicalSkills.required.length > 0 && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Required Skills
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
||||
{job.requirements.technicalSkills.required.map(skill => (
|
||||
<Chip key={skill} label={skill} size="small" variant="outlined" />
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ mt: 3, pt: 2, borderTop: 1, borderColor: 'divider' }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Posted: {job.createdAt?.toLocaleDateString()}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Updated: {job.updatedAt?.toLocaleDateString()}
|
||||
</Typography>
|
||||
{/* {job.views && (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Views: {job.views}
|
||||
</Typography>
|
||||
)} */}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const JobsView: React.FC<JobsViewProps> = ({
|
||||
onJobSelect,
|
||||
onJobView,
|
||||
onJobEdit,
|
||||
onJobRegenerate,
|
||||
onJobDelete,
|
||||
selectable = true,
|
||||
showActions = true,
|
||||
@ -526,7 +452,7 @@ const JobsView: React.FC<JobsViewProps> = ({
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
jobs.map(job => {
|
||||
jobs.map((job, index) => {
|
||||
const isItemSelected = isSelected(job.id || '');
|
||||
return (
|
||||
<TableRow
|
||||
@ -607,6 +533,20 @@ const JobsView: React.FC<JobsViewProps> = ({
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{onJobRegenerate && (
|
||||
<Tooltip title="Regenerate Requirements">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={async (): Promise<void> => {
|
||||
const generatedJob = await onJobRegenerate(job);
|
||||
jobs[index] = generatedJob;
|
||||
setJobs([...jobs]);
|
||||
}}
|
||||
>
|
||||
<ModelTraining fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{onJobDelete && (
|
||||
<Tooltip title="Delete Job">
|
||||
<IconButton
|
||||
@ -644,20 +584,50 @@ const JobsView: React.FC<JobsViewProps> = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'row', position: 'relative', ...sx }}>
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
position: 'relative',
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<Scrollable
|
||||
sx={{ display: 'flex', flex: 1, flexDirection: 'column', height: '100%', width: '100%' }}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Paper sx={{ flex: 1, display: 'flex', flexDirection: 'column' }}>{tableContent}</Paper>
|
||||
<Paper sx={{ flex: 1, display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
|
||||
{tableContent}
|
||||
</Paper>
|
||||
</Scrollable>
|
||||
|
||||
{detailsPanelOpen && !isMobile && (
|
||||
<Scrollable
|
||||
sx={{ display: 'flex', flex: 1, flexDirection: 'row', height: '100%', width: '100%' }}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Paper sx={{ flex: 1, ml: 1 }}>
|
||||
<Paper
|
||||
sx={{
|
||||
flex: 1,
|
||||
ml: 1,
|
||||
flexGrow: 1,
|
||||
height: 'max-content',
|
||||
}}
|
||||
>
|
||||
{selectedJob ? (
|
||||
<JobInfoPanel
|
||||
<JobInfo
|
||||
variant="all"
|
||||
job={selectedJob}
|
||||
onClose={(): void => {
|
||||
console.log('Closing JobInfoPanel');
|
||||
@ -709,7 +679,7 @@ const JobsView: React.FC<JobsViewProps> = ({
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
{selectedJob && <JobInfoPanel job={selectedJob} inDialog />}
|
||||
{selectedJob && <JobInfo variant="all" job={selectedJob} inDialog />}
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
|
@ -55,7 +55,7 @@ import { parsePhoneNumberFromString } from 'libphonenumber-js';
|
||||
import { useReactToPrint } from 'react-to-print';
|
||||
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
import { useAppState, useSelectedJob } from 'hooks/GlobalContext';
|
||||
import { useAppState } from 'hooks/GlobalContext';
|
||||
import { StyledMarkdown } from 'components/StyledMarkdown';
|
||||
import { Resume } from 'types/types';
|
||||
import { BackstoryTextField } from 'components/BackstoryTextField';
|
||||
|
@ -1,29 +1,106 @@
|
||||
import React from 'react';
|
||||
import { SxProps } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
SxProps,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
Typography,
|
||||
LinearProgress,
|
||||
} from '@mui/material';
|
||||
import * as Types from 'types/types'; // Adjust the import path as necessary
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
import { JobsView } from 'components/ui/JobsView';
|
||||
import { StatusBox, StatusIcon } from 'components/ui/StatusIcon';
|
||||
|
||||
interface JobsViewPageProps {
|
||||
sx?: SxProps;
|
||||
}
|
||||
|
||||
interface ProgressDialogProps {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
status: string;
|
||||
statusType: Types.ApiActivityType;
|
||||
}
|
||||
|
||||
const ProgressDialog: React.FC<ProgressDialogProps> = (props: ProgressDialogProps) => {
|
||||
const { isOpen, title, status, statusType } = props;
|
||||
const theme = useTheme();
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||
return (
|
||||
<Dialog fullScreen={fullScreen} open={isOpen} aria-labelledby="responsive-dialog-title">
|
||||
<DialogTitle id="responsive-dialog-title">{title}</DialogTitle>
|
||||
<DialogContent>
|
||||
<StatusBox>
|
||||
{statusType && <StatusIcon type={statusType} />}
|
||||
<Typography variant="body2" sx={{ ml: 1, minWidth: '200px', maxWidth: '200px' }}>
|
||||
{status || 'Processing...'}
|
||||
</Typography>
|
||||
</StatusBox>
|
||||
{status && <LinearProgress sx={{ mt: 1 }} />}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const JobsViewPage: React.FC<JobsViewPageProps> = (props: JobsViewPageProps) => {
|
||||
const { sx } = props;
|
||||
const { apiClient } = useAuth();
|
||||
const [status, setStatus] = useState<string | null>(null);
|
||||
const [statusType, setStatusType] = useState<Types.ApiActivityType | null>(null);
|
||||
|
||||
return (
|
||||
<JobsView
|
||||
onJobSelect={(selectedJobs: Types.Job[]): void => console.log('Selected:', selectedJobs)}
|
||||
onJobView={(job: Types.Job): void => console.log('View job:', job)}
|
||||
onJobEdit={(job: Types.Job): void => console.log('Edit job:', job)}
|
||||
onJobDelete={async (job: Types.Job): Promise<void> => {
|
||||
await apiClient.deleteJob(job.id || '');
|
||||
}}
|
||||
selectable={true}
|
||||
showActions={true}
|
||||
sx={sx}
|
||||
/>
|
||||
<>
|
||||
<JobsView
|
||||
onJobSelect={(selectedJobs: Types.Job[]): void => console.log('Selected:', selectedJobs)}
|
||||
onJobView={(job: Types.Job): void => console.log('View job:', job)}
|
||||
onJobEdit={(job: Types.Job): void => console.log('Edit job:', job)}
|
||||
onJobRegenerate={async (job: Types.Job): Promise<Types.Job> => {
|
||||
setStatus('Re-extracting Job information...');
|
||||
const jobStatusHandlers = {
|
||||
onStatus: (status: Types.ChatMessageStatus): void => {
|
||||
console.log('status:', status.content);
|
||||
setStatusType(status.activity);
|
||||
setStatus(status.content);
|
||||
},
|
||||
onMessage: async (
|
||||
jobRequirementsMessage: Types.JobRequirementsMessage
|
||||
): Promise<void> => {
|
||||
console.log('onMessage - job', jobRequirementsMessage);
|
||||
setStatusType(null);
|
||||
setStatus(null);
|
||||
},
|
||||
onError: (error: Types.ChatMessageError): void => {
|
||||
console.log('onError', error);
|
||||
setStatusType(null);
|
||||
setStatus(null);
|
||||
},
|
||||
onComplete: (): void => {
|
||||
setStatusType(null);
|
||||
setStatus(null);
|
||||
},
|
||||
};
|
||||
const result = apiClient.regenerateJob(job, jobStatusHandlers);
|
||||
return (await result.promise).job;
|
||||
}}
|
||||
onJobDelete={async (job: Types.Job): Promise<void> => {
|
||||
await apiClient.deleteJob(job.id || '');
|
||||
}}
|
||||
selectable={true}
|
||||
showActions={true}
|
||||
sx={{ maxHeight: '100%', ...sx }}
|
||||
/>
|
||||
<ProgressDialog
|
||||
isOpen={!!status}
|
||||
title="Job Processing"
|
||||
status={status || ''}
|
||||
statusType={statusType || 'info'}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -592,6 +592,19 @@ class ApiClient {
|
||||
return handleApiResponse<DeleteResponse>(response);
|
||||
}
|
||||
|
||||
regenerateJob(
|
||||
job: Types.Job,
|
||||
streamingOptions?: StreamingOptions<Types.JobRequirementsMessage>
|
||||
): StreamingResponse<Types.JobRequirementsMessage> {
|
||||
const body = JSON.stringify(formatApiRequest(job));
|
||||
return this.streamify<Types.JobRequirementsMessage>(
|
||||
'/jobs/regenerate',
|
||||
body,
|
||||
streamingOptions,
|
||||
'Job'
|
||||
);
|
||||
}
|
||||
|
||||
async uploadCandidateProfile(file: File): Promise<boolean> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
@ -585,11 +585,6 @@ Content: {content}
|
||||
LLMMessage(role="user", content=prompt),
|
||||
]
|
||||
|
||||
status_message = ChatMessageStatus(
|
||||
session_id=session_id, activity=ApiActivityType.GENERATING, content="Generating response..."
|
||||
)
|
||||
yield status_message
|
||||
|
||||
logger.info(f"Message options: {options.model_dump(exclude_unset=True)}")
|
||||
response = None
|
||||
content = ""
|
||||
|
@ -74,7 +74,10 @@ class GenerateResume(Agent):
|
||||
|
||||
# Build the system prompt
|
||||
system_prompt = f"""You are a professional resume writer with expertise in highlighting candidate strengths and experiences.
|
||||
Create a polished, concise, and ATS-friendly resume for the candidate based on the assessment data provided. Rephrase skills to avoid
|
||||
Create a polished, concise, and ATS-friendly resume for the candidate based on the assessment data provided.
|
||||
|
||||
Rephrase skills in the SKILL ASSESSMENT RESULTS section to avoid direct duplication from the assessment.
|
||||
Do not use the exact phrases or wording from the assessment, but rather integrate the skills naturally into the resume without any
|
||||
direct duplication from the assessment.
|
||||
|
||||
Do not provide header information like name, email, or phone number in the resume, as that information will be added later.
|
||||
|
@ -90,6 +90,45 @@ content is already in markdown format, return it as is.
|
||||
yield chat_message
|
||||
return
|
||||
|
||||
async def generate_job_requirements(database: RedisDatabase, candidate_entity: CandidateEntity, markdown: str):
|
||||
chat_agent = candidate_entity.get_or_create_agent(agent_type=ChatContextType.JOB_REQUIREMENTS)
|
||||
if not chat_agent:
|
||||
error_message = ChatMessageError(
|
||||
session_id=MOCK_UUID, # No session ID for document uploads
|
||||
content="No agent found for job requirements chat type",
|
||||
)
|
||||
yield error_message
|
||||
return
|
||||
status_message = ChatMessageStatus(
|
||||
session_id=MOCK_UUID, # No session ID for document uploads
|
||||
content="Analyzing document for company and requirement details...",
|
||||
activity=ApiActivityType.SEARCHING,
|
||||
)
|
||||
yield status_message
|
||||
|
||||
message = None
|
||||
async for message in chat_agent.generate(
|
||||
llm=llm_manager.get_llm(database.redis),
|
||||
model=defines.model,
|
||||
session_id=MOCK_UUID,
|
||||
prompt=markdown,
|
||||
database=database,
|
||||
):
|
||||
if message.status != ApiStatusType.DONE:
|
||||
yield message
|
||||
|
||||
if not message or not isinstance(message, JobRequirementsMessage):
|
||||
error_message = ChatMessageError(
|
||||
session_id=MOCK_UUID, # No session ID for document uploads
|
||||
content="Job extraction did not convert successfully",
|
||||
)
|
||||
yield error_message
|
||||
return
|
||||
|
||||
job_requirements: JobRequirementsMessage = message
|
||||
logger.info(f"✅ Successfully generated job requirements for job {job_requirements.id}")
|
||||
yield job_requirements
|
||||
|
||||
|
||||
async def create_job_from_content(database: RedisDatabase, current_user: Candidate, content: str):
|
||||
status_message = ChatMessageStatus(
|
||||
@ -115,43 +154,10 @@ async def create_job_from_content(database: RedisDatabase, current_user: Candida
|
||||
return
|
||||
markdown_message = message
|
||||
|
||||
chat_agent = candidate_entity.get_or_create_agent(agent_type=ChatContextType.JOB_REQUIREMENTS)
|
||||
if not chat_agent:
|
||||
error_message = ChatMessageError(
|
||||
session_id=MOCK_UUID, # No session ID for document uploads
|
||||
content="No agent found for job requirements chat type",
|
||||
)
|
||||
yield error_message
|
||||
return
|
||||
status_message = ChatMessageStatus(
|
||||
session_id=MOCK_UUID, # No session ID for document uploads
|
||||
content="Analyzing document for company and requirement details...",
|
||||
activity=ApiActivityType.SEARCHING,
|
||||
)
|
||||
yield status_message
|
||||
|
||||
message = None
|
||||
async for message in chat_agent.generate(
|
||||
llm=llm_manager.get_llm(database.redis),
|
||||
model=defines.model,
|
||||
session_id=MOCK_UUID,
|
||||
prompt=markdown_message.content,
|
||||
database=database,
|
||||
async for message in generate_job_requirements(
|
||||
database=database, candidate_entity=candidate_entity, markdown=markdown_message.content
|
||||
):
|
||||
if message.status != ApiStatusType.DONE:
|
||||
yield message
|
||||
|
||||
if not message or not isinstance(message, JobRequirementsMessage):
|
||||
error_message = ChatMessageError(
|
||||
session_id=MOCK_UUID, # No session ID for document uploads
|
||||
content="Job extraction did not convert successfully",
|
||||
)
|
||||
yield error_message
|
||||
return
|
||||
|
||||
job_requirements: JobRequirementsMessage = message
|
||||
logger.info(f"✅ Successfully generated job requirements for job {job_requirements.id}")
|
||||
yield job_requirements
|
||||
yield message
|
||||
return
|
||||
|
||||
|
||||
@ -242,6 +248,108 @@ async def update_job(
|
||||
logger.error(f"❌ Update job error: {e}")
|
||||
return JSONResponse(status_code=400, content=create_error_response("UPDATE_FAILED", str(e)))
|
||||
|
||||
@router.post("/regenerate")
|
||||
async def regenerate_job(
|
||||
job: Job = Body(...), current_user=Depends(get_current_user), database: RedisDatabase = Depends(get_database)
|
||||
):
|
||||
"""Regenerate requirements for a job (used if prompts are changed or to test different models)"""
|
||||
|
||||
async def content_stream_generator():
|
||||
# Verify user is a candidate
|
||||
if current_user.user_type != "candidate":
|
||||
logger.warning(f"⚠️ Unauthorized upload attempt by user type: {current_user.user_type}")
|
||||
error_message = ChatMessageError(
|
||||
session_id=MOCK_UUID, # No session ID for document uploads
|
||||
content="Only candidates can upload documents",
|
||||
)
|
||||
yield error_message
|
||||
return
|
||||
|
||||
if job.owner_id != current_user.id:
|
||||
logger.warning(f"⚠️ Unauthorized job regeneration attempt by user {current_user.id} on job {job.id}")
|
||||
error_message = ChatMessageError(
|
||||
session_id=MOCK_UUID, # No session ID for document uploads
|
||||
content="Cannot regenerate another user's job",
|
||||
)
|
||||
yield error_message
|
||||
return
|
||||
|
||||
last_yield_was_streaming = False
|
||||
async with entities.get_candidate_entity(candidate=current_user) as candidate_entity:
|
||||
message = None
|
||||
|
||||
async for message in generate_job_requirements(
|
||||
database=database, candidate_entity=candidate_entity, markdown=job.description
|
||||
):
|
||||
if message.status != ApiStatusType.STREAMING:
|
||||
last_yield_was_streaming = False
|
||||
else:
|
||||
if last_yield_was_streaming:
|
||||
continue
|
||||
last_yield_was_streaming = True
|
||||
logger.info(f"📄 Yielding job regeneration message status: {message.status}")
|
||||
if message.status != ApiStatusType.DONE:
|
||||
yield message
|
||||
|
||||
if isinstance(message, JobRequirementsMessage):
|
||||
job_requirements_message: JobRequirementsMessage = message
|
||||
job_requirements_message.job.id = job.id
|
||||
job_requirements_message.job.owner = job.owner
|
||||
job_requirements_message.job.updated_at = datetime.now(UTC)
|
||||
logger.info(f"🔄 Saving updated job {job.id}")
|
||||
await database.set_job(job.id, job_requirements_message.job.model_dump())
|
||||
yield job_requirements_message
|
||||
else:
|
||||
logger.error("❌ Failed to regenerate job requirements")
|
||||
error_message = ChatMessageError(
|
||||
session_id=MOCK_UUID, # No session ID for document uploads
|
||||
content="Failed to regenerate job requirements",
|
||||
)
|
||||
yield error_message
|
||||
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
async def to_json(method):
|
||||
try:
|
||||
async for message in method:
|
||||
json_data = message.model_dump(mode="json", by_alias=True)
|
||||
json_str = json.dumps(json_data)
|
||||
yield f"data: {json_str}\n\n".encode("utf-8")
|
||||
except Exception as e:
|
||||
logger.error(backstory_traceback.format_exc())
|
||||
logger.error(f"Error in to_json conversion: {e}")
|
||||
return
|
||||
|
||||
return StreamingResponse(
|
||||
to_json(content_stream_generator()),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no", # Nginx
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"Access-Control-Allow-Origin": "*", # Adjust for your CORS needs
|
||||
"Transfer-Encoding": "chunked",
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(backstory_traceback.format_exc())
|
||||
logger.error(f"❌ Job regeneration error: {e}")
|
||||
return StreamingResponse(
|
||||
iter(
|
||||
[
|
||||
json.dumps(
|
||||
ChatMessageError(
|
||||
session_id=MOCK_UUID, # No session ID for document uploads
|
||||
content="Failed to regenerate job requirements",
|
||||
).model_dump(by_alias=True)
|
||||
).encode("utf-8")
|
||||
]
|
||||
),
|
||||
media_type="text/event-stream",
|
||||
)
|
||||
|
||||
@router.post("/from-content")
|
||||
async def create_job_from_description(
|
||||
|
Loading…
x
Reference in New Issue
Block a user