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 {
|
||||
Box,
|
||||
Button,
|
||||
Typography,
|
||||
Paper,
|
||||
TextField,
|
||||
Grid,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogActions,
|
||||
IconButton,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
Chip,
|
||||
Divider,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
LinearProgress,
|
||||
Stack,
|
||||
Alert
|
||||
Paper,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
SyncAlt,
|
||||
@ -36,21 +28,22 @@ import {
|
||||
CloudUpload,
|
||||
Description,
|
||||
Business,
|
||||
LocationOn,
|
||||
Work,
|
||||
CheckCircle,
|
||||
Star
|
||||
} from '@mui/icons-material';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import DescriptionIcon from '@mui/icons-material/Description';
|
||||
import FileUploadIcon from '@mui/icons-material/FileUpload';
|
||||
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
import { useAppState, useSelectedCandidate, useSelectedJob } from 'hooks/GlobalContext';
|
||||
import { useAppState, useSelectedJob } from 'hooks/GlobalContext';
|
||||
import { BackstoryElementProps } from './BackstoryTab';
|
||||
import { LoginRequired } from 'components/ui/LoginRequired';
|
||||
|
||||
import * as Types from 'types/types';
|
||||
import { StyledMarkdown } from './StyledMarkdown';
|
||||
import { JobInfo } from './ui/JobInfo';
|
||||
import { Scrollable } from './Scrollable';
|
||||
|
||||
const VisuallyHiddenInput = styled('input')({
|
||||
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;
|
||||
}
|
||||
const JobCreator = (props: JobCreator) => {
|
||||
const JobCreator = (props: JobCreatorProps) => {
|
||||
const { user, apiClient } = useAuth();
|
||||
const { onSave } = props;
|
||||
const { selectedCandidate } = useSelectedCandidate();
|
||||
const { onSave } = props;
|
||||
const { selectedJob, setSelectedJob } = useSelectedJob();
|
||||
const { setSnack } = useAppState();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
const [openUploadDialog, setOpenUploadDialog] = useState<boolean>(false);
|
||||
const [jobDescription, setJobDescription] = useState<string>('');
|
||||
const [jobRequirements, setJobRequirements] = useState<Types.JobRequirements | null>(null);
|
||||
const [jobTitle, setJobTitle] = useState<string>('');
|
||||
const [company, setCompany] = useState<string>('');
|
||||
const [summary, setSummary] = useState<string>('');
|
||||
const [jobLocation, setJobLocation] = useState<string>('');
|
||||
const [jobId, setJobId] = useState<string>('');
|
||||
const [job, setJob] = useState<Types.Job | null>(null);
|
||||
const [jobStatus, setJobStatus] = useState<string>('');
|
||||
const [jobStatusIcon, setJobStatusIcon] = useState<JSX.Element>(<></>);
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
}, [jobTitle, jobDescription, company]);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
|
||||
@ -158,8 +143,10 @@ const JobCreator = (props: JobCreator) => {
|
||||
setJobStatusIcon(getIcon(status.activity));
|
||||
setJobStatus(status.content);
|
||||
},
|
||||
onMessage: (job: Types.Job) => {
|
||||
onMessage: (jobMessage: Types.JobRequirementsMessage) => {
|
||||
const job: Types.Job = jobMessage.job
|
||||
console.log('onMessage - job', job);
|
||||
setJob(job);
|
||||
setCompany(job.company || '');
|
||||
setJobDescription(job.description);
|
||||
setSummary(job.summary || '');
|
||||
@ -333,8 +320,9 @@ const JobCreator = (props: JobCreator) => {
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
setIsProcessing(true);
|
||||
const job = await apiClient.createJob(newJob);
|
||||
const jobMessage = await apiClient.createJob(newJob);
|
||||
setIsProcessing(false);
|
||||
const job: Types.Job = jobMessage.job;
|
||||
onSave ? onSave(job) : setSelectedJob(job);
|
||||
};
|
||||
|
||||
@ -357,10 +345,6 @@ const JobCreator = (props: JobCreator) => {
|
||||
};
|
||||
|
||||
const renderJobCreation = () => {
|
||||
if (!user) {
|
||||
return <Box>You must be logged in</Box>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
mx: 'auto', p: { xs: 2, sm: 3 },
|
||||
@ -500,21 +484,6 @@ const JobCreator = (props: JobCreator) => {
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -548,7 +517,27 @@ const JobCreator = (props: JobCreator) => {
|
||||
width: "100%",
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
} from '@mui/material';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import { useMediaQuery } from '@mui/material';
|
||||
import { JobFull } from 'types/types';
|
||||
import { Job, JobFull } from 'types/types';
|
||||
import { CopyBubble } from "components/CopyBubble";
|
||||
import { rest } from 'lodash';
|
||||
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';
|
||||
|
||||
interface JobInfoProps {
|
||||
job: JobFull;
|
||||
job: Job | JobFull;
|
||||
sx?: SxProps;
|
||||
action?: string;
|
||||
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" }}>
|
||||
{variant !== "small" && <>
|
||||
{job.location &&
|
||||
{'location' in job &&
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
<strong>Location:</strong> {job.location.city}, {job.location.state || job.location.country}
|
||||
</Typography>
|
||||
|
@ -83,7 +83,7 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
||||
Session ID: {guest.sessionId}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Created: {guest.createdAt.toLocaleString()}
|
||||
Created: {guest.createdAt?.toLocaleString()}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
@ -39,6 +39,7 @@ const TOKEN_STORAGE = {
|
||||
PENDING_VERIFICATION_EMAIL: 'pendingVerificationEmail'
|
||||
} as const;
|
||||
|
||||
|
||||
// ============================
|
||||
// Streaming Types and Interfaces
|
||||
// ============================
|
||||
@ -647,12 +648,12 @@ class ApiClient {
|
||||
// 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);
|
||||
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 response = await fetch(`${this.baseUrl}/jobs`, {
|
||||
method: 'POST',
|
||||
@ -660,7 +661,7 @@ class ApiClient {
|
||||
body: body
|
||||
});
|
||||
|
||||
return this.handleApiResponseWithConversion<Types.Job>(response, 'Job');
|
||||
return this.handleApiResponseWithConversion<Types.JobRequirementsMessage>(response, 'JobRequirementsMessage');
|
||||
}
|
||||
|
||||
async getJob(id: string): Promise<Types.Job> {
|
||||
@ -884,10 +885,10 @@ class ApiClient {
|
||||
'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()
|
||||
formData.append('file', file);
|
||||
formData.append('filename', file.name);
|
||||
@ -898,7 +899,7 @@ class ApiClient {
|
||||
'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> {
|
||||
@ -906,7 +907,7 @@ class ApiClient {
|
||||
...streamingOptions,
|
||||
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> {
|
||||
@ -915,7 +916,7 @@ class ApiClient {
|
||||
...streamingOptions,
|
||||
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,
|
||||
streamingOptions?: StreamingOptions<Types.ChatMessageSkillAssessment>)
|
||||
@ -925,7 +926,7 @@ class ApiClient {
|
||||
...streamingOptions,
|
||||
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> {
|
||||
@ -1226,7 +1227,7 @@ class ApiClient {
|
||||
* @param options callbacks, headers, and method
|
||||
* @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 signal = options.signal || abortController.signal;
|
||||
const headers = options.headers || null;
|
||||
@ -1308,8 +1309,8 @@ class ApiClient {
|
||||
break;
|
||||
|
||||
case 'done':
|
||||
const message = Types.convertApiMessageFromApi(incoming) as T;
|
||||
finalMessage = message as any;
|
||||
const message = (modelType ? convertFromApi<T>(incoming, modelType) : incoming) as T;
|
||||
finalMessage = message;
|
||||
try {
|
||||
options.onMessage?.(message);
|
||||
} catch (error) {
|
||||
@ -1361,7 +1362,7 @@ class ApiClient {
|
||||
options: StreamingOptions = {}
|
||||
): StreamingResponse {
|
||||
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
|
||||
// ============================
|
||||
|
||||
async handleRequest<T>(requestFn: () => Promise<Response>, modelType?: string): Promise<T> {
|
||||
try {
|
||||
const response = await requestFn();
|
||||
|
@ -823,5 +823,16 @@ Content: {content}
|
||||
|
||||
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
|
||||
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")
|
||||
|
||||
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
|
||||
agent_registry.register(GeneratePersona._agent_type, GeneratePersona)
|
||||
|
@ -19,7 +19,7 @@ import asyncio
|
||||
import numpy as np # type: ignore
|
||||
|
||||
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
|
||||
from logger import logger
|
||||
import defines
|
||||
@ -107,6 +107,15 @@ class JobRequirementsAgent(Agent):
|
||||
async def generate(
|
||||
self, llm: Any, model: str, session_id: str, prompt: str, tunables: Optional[Tunables] = None, temperature=0.7
|
||||
) -> 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
|
||||
status_message = ChatMessageStatus(
|
||||
session_id=session_id,
|
||||
@ -166,15 +175,21 @@ class JobRequirementsAgent(Agent):
|
||||
logger.error(f"⚠️ {status_message.content}")
|
||||
yield status_message
|
||||
return
|
||||
job_requirements_message = JobRequirementsMessage(
|
||||
session_id=session_id,
|
||||
status=ApiStatusType.DONE,
|
||||
requirements=requirements,
|
||||
job = Job(
|
||||
owner_id=self.user.id,
|
||||
owner_type=self.user.user_type,
|
||||
company=company,
|
||||
title=title,
|
||||
summary=summary,
|
||||
requirements=requirements,
|
||||
session_id=session_id,
|
||||
description=prompt,
|
||||
)
|
||||
job_requirements_message = JobRequirementsMessage(
|
||||
session_id=session_id,
|
||||
status=ApiStatusType.DONE,
|
||||
job=job,
|
||||
)
|
||||
yield job_requirements_message
|
||||
logger.info(f"✅ Job requirements analysis completed successfully.")
|
||||
return
|
||||
|
@ -2357,7 +2357,6 @@ async def create_job_from_content(database: RedisDatabase, current_user: Candida
|
||||
activity=ApiActivityType.SEARCHING
|
||||
)
|
||||
yield status_message
|
||||
await asyncio.sleep(0)
|
||||
|
||||
async for message in chat_agent.generate(
|
||||
llm=llm_manager.get_llm(),
|
||||
@ -2366,16 +2365,52 @@ async def create_job_from_content(database: RedisDatabase, current_user: Candida
|
||||
prompt=content
|
||||
):
|
||||
pass
|
||||
|
||||
if not message or not isinstance(message, JobRequirementsMessage):
|
||||
error_message = ChatMessageError(
|
||||
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
|
||||
return
|
||||
|
||||
logger.info(f"✅ Successfully saved job requirements job {message.id}")
|
||||
yield message
|
||||
status_message = ChatMessageStatus(
|
||||
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
|
||||
|
||||
@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'")
|
||||
|
||||
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
|
||||
return
|
||||
|
||||
@ -3403,9 +3439,10 @@ async def create_job_from_file(
|
||||
yield error_message
|
||||
logger.error(f"❌ Error converting {file.filename} to Markdown: {e}")
|
||||
return
|
||||
async for message in create_job_from_content(database=database, current_user=current_user, content=file_content):
|
||||
yield message
|
||||
return
|
||||
|
||||
async for message in create_job_from_content(database=database, current_user=current_user, content=file_content):
|
||||
yield message
|
||||
return
|
||||
|
||||
try:
|
||||
async def to_json(method):
|
||||
|
@ -480,8 +480,8 @@ class BaseUser(BaseUserWithType):
|
||||
full_name: str = Field(..., alias="fullName")
|
||||
phone: Optional[str] = None
|
||||
location: Optional[Location] = None
|
||||
created_at: datetime = Field(..., alias="createdAt")
|
||||
updated_at: datetime = Field(..., alias="updatedAt")
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt")
|
||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="updatedAt")
|
||||
last_login: Optional[datetime] = Field(None, alias="lastLogin")
|
||||
profile_image: Optional[str] = Field(None, alias="profileImage")
|
||||
status: UserStatus
|
||||
@ -613,7 +613,7 @@ class Guest(BaseUser):
|
||||
username: str # Add username for consistency with other user types
|
||||
converted_to_user_id: Optional[str] = Field(None, alias="convertedToUserId")
|
||||
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")
|
||||
rag_content_size: int = 0
|
||||
model_config = {
|
||||
@ -690,8 +690,8 @@ class Job(BaseModel):
|
||||
company: Optional[str]
|
||||
description: str
|
||||
requirements: Optional[JobRequirements]
|
||||
created_at: datetime = Field(..., alias="createdAt")
|
||||
updated_at: datetime = Field(..., alias="updatedAt")
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt")
|
||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="updatedAt")
|
||||
model_config = {
|
||||
"populate_by_name": True # Allow both field names and aliases
|
||||
}
|
||||
@ -700,7 +700,7 @@ class JobFull(Job):
|
||||
location: Location
|
||||
salary_range: Optional[SalaryRange] = Field(None, alias="salaryRange")
|
||||
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")
|
||||
is_active: bool = Field(..., alias="isActive")
|
||||
applicants: Optional[List["JobApplication"]] = None
|
||||
@ -723,8 +723,8 @@ class InterviewFeedback(BaseModel):
|
||||
weaknesses: List[str]
|
||||
recommendation: InterviewRecommendation
|
||||
comments: str
|
||||
created_at: datetime = Field(..., alias="createdAt")
|
||||
updated_at: datetime = Field(..., alias="updatedAt")
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt")
|
||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="updatedAt")
|
||||
is_visible: bool = Field(..., alias="isVisible")
|
||||
skill_assessments: Optional[List[SkillAssessment]] = Field(None, alias="skillAssessments")
|
||||
model_config = {
|
||||
@ -954,11 +954,7 @@ class ChatMessageRagSearch(ApiMessage):
|
||||
|
||||
class JobRequirementsMessage(ApiMessage):
|
||||
type: ApiMessageType = ApiMessageType.JSON
|
||||
title: Optional[str]
|
||||
summary: Optional[str]
|
||||
company: Optional[str]
|
||||
description: str
|
||||
requirements: Optional[JobRequirements]
|
||||
job: Job = Field(..., alias="job")
|
||||
|
||||
class DocumentMessage(ApiMessage):
|
||||
type: ApiMessageType = ApiMessageType.JSON
|
||||
@ -1079,8 +1075,8 @@ class RAGConfiguration(BaseModel):
|
||||
embedding_model: str = Field(..., alias="embeddingModel")
|
||||
vector_store_type: VectorStoreType = Field(..., alias="vectorStoreType")
|
||||
retrieval_parameters: RetrievalParameters = Field(..., alias="retrievalParameters")
|
||||
created_at: datetime = Field(..., alias="createdAt")
|
||||
updated_at: datetime = Field(..., alias="updatedAt")
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt")
|
||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="updatedAt")
|
||||
version: int
|
||||
is_active: bool = Field(..., alias="isActive")
|
||||
model_config = {
|
||||
@ -1251,7 +1247,7 @@ class EmployerResponse(BaseModel):
|
||||
|
||||
class JobResponse(BaseModel):
|
||||
success: bool
|
||||
data: Optional["Job"] = None
|
||||
data: Optional[Job] = None
|
||||
error: Optional[ErrorDetail] = None
|
||||
meta: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user