Working on job creation flow

This commit is contained in:
James Ketr 2025-06-09 19:57:08 -07:00
parent 9edf5a5b23
commit 4f4187eba4
9 changed files with 143 additions and 104 deletions

View File

@ -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>
); );
}; };

View File

@ -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>

View File

@ -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>

View File

@ -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();

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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