diff --git a/frontend/src/components/DocumentManager.tsx b/frontend/src/components/DocumentManager.tsx index ad59844..c6c1e17 100644 --- a/frontend/src/components/DocumentManager.tsx +++ b/frontend/src/components/DocumentManager.tsx @@ -112,9 +112,9 @@ const DocumentManager = (props: BackstoryElementProps) => { try { // Upload file (replace with actual API call) const controller = apiClient.uploadCandidateDocument(file, { includeInRAG: true, isJobDocument: false }); - const newDocument = await controller.promise; + const result = await controller.promise; - setDocuments(prev => [...prev, newDocument]); + setDocuments(prev => [...prev, result.document]); setSnack(`Document uploaded: ${file.name}`, 'success'); // Reset file input diff --git a/frontend/src/components/JobManagement.tsx b/frontend/src/components/JobManagement.tsx index e3d3784..41f5055 100644 --- a/frontend/src/components/JobManagement.tsx +++ b/frontend/src/components/JobManagement.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, JSX } from 'react'; +import React, { useState, useEffect, useRef, JSX } from 'react'; import { Box, Button, @@ -6,7 +6,6 @@ import { Paper, TextField, Grid, - InputAdornment, Dialog, DialogTitle, DialogContent, @@ -14,7 +13,15 @@ import { DialogActions, IconButton, useTheme, - useMediaQuery + useMediaQuery, + Chip, + Divider, + Card, + CardContent, + CardHeader, + LinearProgress, + Stack, + Alert } from '@mui/material'; import { SyncAlt, @@ -25,7 +32,14 @@ import { AutoFixHigh, Image, Psychology, - Build + Build, + CloudUpload, + Description, + Business, + LocationOn, + Work, + CheckCircle, + Star } from '@mui/icons-material'; import { styled } from '@mui/material/styles'; import DescriptionIcon from '@mui/icons-material/Description'; @@ -37,7 +51,6 @@ import { BackstoryElementProps } from './BackstoryTab'; import { LoginRequired } from 'components/ui/LoginRequired'; import * as Types from 'types/types'; -import { StreamingResponse } from 'services/api-client'; const VisuallyHiddenInput = styled('input')({ clip: 'rect(0 0 0 0)', @@ -51,52 +64,85 @@ const VisuallyHiddenInput = styled('input')({ width: 1, }); +const UploadBox = styled(Box)(({ theme }) => ({ + border: `2px dashed ${theme.palette.primary.main}`, + borderRadius: theme.shape.borderRadius * 2, + padding: theme.spacing(4), + textAlign: 'center', + backgroundColor: theme.palette.action.hover, + transition: 'all 0.3s ease', + cursor: 'pointer', + '&:hover': { + backgroundColor: theme.palette.action.selected, + borderColor: theme.palette.primary.dark, + }, +})); + +const StatusBox = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + padding: theme.spacing(1, 2), + backgroundColor: theme.palette.background.paper, + borderRadius: theme.shape.borderRadius, + border: `1px solid ${theme.palette.divider}`, + minHeight: 48, +})); + const getIcon = (type: Types.ApiActivityType) => { switch (type) { case 'converting': - return ; + return ; case 'heartbeat': - return ; + return ; case 'system': - return ; + return ; case 'info': - return ; + return ; case 'searching': - return ; + return ; case 'generating': - return ; + return ; case 'generating_image': - return ; + return ; case 'thinking': - return ; + return ; case 'tooling': - return ; + return ; default: - return ; // fallback icon + return ; } -} +}; + const JobManagement = (props: BackstoryElementProps) => { const { user, apiClient } = useAuth(); - const { selectedCandidate } = useSelectedCandidate() - const { selectedJob, setSelectedJob } = useSelectedJob() + const { selectedCandidate } = useSelectedCandidate(); + const { selectedJob, setSelectedJob } = useSelectedJob(); const { setSnack, submitQuery } = props; const backstoryProps = { setSnack, submitQuery }; const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + const isTablet = useMediaQuery(theme.breakpoints.down('md')); const [openUploadDialog, setOpenUploadDialog] = useState(false); const [jobDescription, setJobDescription] = useState(''); + const [jobRequirements, setJobRequirements] = useState(null); const [jobTitle, setJobTitle] = useState(''); const [company, setCompany] = useState(''); + const [summary, setSummary] = useState(''); const [jobLocation, setJobLocation] = useState(''); const [jobId, setJobId] = useState(''); const [jobStatus, setJobStatus] = useState(''); const [jobStatusIcon, setJobStatusIcon] = useState(<>); + const [isProcessing, setIsProcessing] = useState(false); useEffect(() => { }, [jobTitle, jobDescription, company]); + const fileInputRef = useRef(null); + + if (!user?.id) { return ( @@ -105,188 +151,400 @@ const JobManagement = (props: BackstoryElementProps) => { const jobStatusHandlers = { onStatus: (status: Types.ChatMessageStatus) => { + console.log('status:', status.content); setJobStatusIcon(getIcon(status.activity)); setJobStatus(status.content); }, onMessage: (job: Types.Job) => { console.log('onMessage - job', job); + setCompany(job.company || ''); setJobDescription(job.description); + setSummary(job.summary || ''); setJobTitle(job.title || ''); + setJobRequirements(job.requirements || null); + setJobStatusIcon(<>); + setJobStatus(''); }, onError: (error: Types.ChatMessageError) => { console.log('onError', error); setSnack(error.content, "error"); + setIsProcessing(false); }, onComplete: () => { setJobStatusIcon(<>); setJobStatus(''); + setIsProcessing(false); } }; const documentStatusHandlers = { ...jobStatusHandlers, - onMessage: (document: Types.Document) => { - console.log('onMessage - document', document); - const job: Types.Job = document as any; - setJobDescription(job.description); - setJobTitle(job.title || ''); + onMessage: (document: Types.DocumentMessage) => { + if ('document' in document) { + console.log('onMessage - document', document); + setJobDescription(document.content || ''); + } else if ('requirements' in document) { + console.log('onMessage - document (as job)', document); + jobStatusHandlers.onMessage(document); + } + setJobStatusIcon(<>); + setJobStatus(''); } - } + }; const handleJobUpload = async (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { - const file = e.target.files[0]; - const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase(); - let docType : Types.DocumentType | null = null; - switch (fileExtension.substring(1)) { - case "pdf": - docType = "pdf"; - break; - case "docx": - docType = "docx"; - break; - case "md": - docType = "markdown"; - break; - case "txt": - docType = "txt"; - break; - } + const file = e.target.files[0]; + const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase(); + let docType: Types.DocumentType | null = null; + switch (fileExtension.substring(1)) { + case "pdf": + docType = "pdf"; + break; + case "docx": + docType = "docx"; + break; + case "md": + docType = "markdown"; + break; + case "txt": + docType = "txt"; + break; + } - if (!docType) { + if (!docType) { setSnack('Invalid file type. Please upload .txt, .md, .docx, or .pdf files only.', 'error'); return; - } + } try { - // Upload file (replace with actual API call) - const controller : StreamingResponse = apiClient.uploadCandidateDocument(file, { isJobDocument: true}, documentStatusHandlers); - const document : Types.Document | null = await controller.promise; + setIsProcessing(true); + setJobDescription(''); + setJobTitle(''); + setJobRequirements(null); + setSummary(''); + const controller = apiClient.uploadCandidateDocument(file, { isJobDocument: true, overwrite: true }, documentStatusHandlers); + const document = await controller.promise; if (!document) { return; } - console.log(`Document id: ${document.id}`) + console.log(`Document id: ${document.id}`); e.target.value = ''; } catch (error) { console.error(error); setSnack('Failed to upload document', 'error'); + setIsProcessing(false); } } }; + const handleUploadClick = () => { + fileInputRef.current?.click(); + }; + + const renderRequirementSection = (title: string, items: string[] | undefined, icon: JSX.Element, required = false) => { + if (!items || items.length === 0) return null; + + return ( + + + {icon} + + {title} + + {required && } + + + {items.map((item, index) => ( + + ))} + + + ); + }; + + const renderJobRequirements = () => { + if (!jobRequirements) return null; + + return ( + + } + sx={{ pb: 1 }} + /> + + {renderRequirementSection( + "Technical Skills (Required)", + jobRequirements.technicalSkills.required, + , + true + )} + {renderRequirementSection( + "Technical Skills (Preferred)", + jobRequirements.technicalSkills.preferred, + + )} + {renderRequirementSection( + "Experience Requirements (Required)", + jobRequirements.experienceRequirements.required, + , + true + )} + {renderRequirementSection( + "Experience Requirements (Preferred)", + jobRequirements.experienceRequirements.preferred, + + )} + {renderRequirementSection( + "Soft Skills", + jobRequirements.softSkills, + + )} + {renderRequirementSection( + "Experience", + jobRequirements.experience, + + )} + {renderRequirementSection( + "Education", + jobRequirements.education, + + )} + {renderRequirementSection( + "Certifications", + jobRequirements.certifications, + + )} + {renderRequirementSection( + "Preferred Attributes", + jobRequirements.preferredAttributes, + + )} + + + ); + }; + const handleSave = async () => { - const job : Types.Job = { + const newJob: Types.Job = { ownerId: user?.id || '', ownerType: 'candidate', description: jobDescription, + company: company, + summary: summary, title: jobTitle, - } - apiClient.createJob(job, jobStatusHandlers); - } + requirements: jobRequirements || undefined + }; + setIsProcessing(true); + const job = await apiClient.createJob(newJob); + setIsProcessing(false); + setSelectedJob(job); + }; + + const handleExtractRequirements = () => { + // Implement requirements extraction logic here + setIsProcessing(true); + // This would call your API to extract requirements from the job description + }; const renderJobCreation = () => { if (!user) { - return You must + return You must be logged in; } - return (<> - - - - - Job Selection - - - + + + + + + + + Or Enter Manually + + setJobDescription(e.target.value)} + disabled={isProcessing} + sx={{ mb: 2 }} + /> + {jobRequirements === null && jobDescription && ( + + )} + + + + {(jobStatus || isProcessing) && ( + + + {jobStatusIcon} + + {jobStatus || 'Processing...'} + + + {isProcessing && } + + )} + + + + {/* Job Details Section */} + + } + /> + + + + setJobTitle(e.target.value)} + required + disabled={isProcessing} + InputProps={{ + startAdornment: + }} + /> + + + + setCompany(e.target.value)} + required + disabled={isProcessing} + InputProps={{ + startAdornment: + }} + /> + + + {/* + setJobLocation(e.target.value)} + disabled={isProcessing} + InputProps={{ + startAdornment: + }} + /> + */} + + + + + + + + + + + {/* Job Summary */} + {summary !== '' && + + } + sx={{ pb: 1 }} /> - - Accepted document formats: .pdf, .docx, .txt, or .md - - {jobStatusIcon} {jobStatus} + + {summary} + + + } + + {/* Requirements Display */} + {renderJobRequirements()} + - - setJobDescription(e.target.value)} - required - InputProps={{ - startAdornment: ( - - - - ), - }} - /> - - The job description will be used to extract requirements for candidate matching. - - - - Enter Job Details - - - - - setJobTitle(e.target.value)} - required - margin="normal" - /> - - - - setCompany(e.target.value)} - required - margin="normal" - /> - - - - setJobLocation(e.target.value)} - margin="normal" - /> - - - - - - - ); + ); }; return ( - - { selectedJob === null && renderJobCreation() } - {/* { selectedJob !== null && renderJob() } */} - + + {selectedJob === null && renderJobCreation()} + ); -} +}; export { JobManagement }; \ No newline at end of file diff --git a/frontend/src/components/JobMatchAnalysis.tsx b/frontend/src/components/JobMatchAnalysis.tsx index e7a1391..40d4cb3 100644 --- a/frontend/src/components/JobMatchAnalysis.tsx +++ b/frontend/src/components/JobMatchAnalysis.tsx @@ -57,6 +57,52 @@ const JobMatchAnalysis: React.FC = (props: JobAnalysisProps) = setExpanded(isExpanded ? panel : false); }; + useEffect(() => { + if (!job || !job.requirements) { + return; + } + const requirements: { requirement: string, domain: string }[] = []; + if (job.requirements?.technicalSkills) { + job.requirements.technicalSkills.required?.forEach(req => requirements.push({ requirement: req, domain: 'Technical Skills (required)' })); + job.requirements.technicalSkills.preferred?.forEach(req => requirements.push({ requirement: req, domain: 'Technical Skills (preferred)' })); + } + if (job.requirements?.experienceRequirements) { + job.requirements.experienceRequirements.required?.forEach(req => requirements.push({ requirement: req, domain: 'Experience (required)' })); + job.requirements.experienceRequirements.preferred?.forEach(req => requirements.push({ requirement: req, domain: 'Experience (preferred)' })); + } + if (job.requirements?.softSkills) { + job.requirements.softSkills.forEach(req => requirements.push({ requirement: req, domain: 'Soft Skills' })); + } + if (job.requirements?.experience) { + job.requirements.experience.forEach(req => requirements.push({ requirement: req, domain: 'Experience' })); + } + if (job.requirements?.education) { + job.requirements.education.forEach(req => requirements.push({ requirement: req, domain: 'Education' })); + } + if (job.requirements?.certifications) { + job.requirements.certifications.forEach(req => requirements.push({ requirement: req, domain: 'Certifications' })); + } + if (job.requirements?.preferredAttributes) { + job.requirements.preferredAttributes.forEach(req => requirements.push({ requirement: req, domain: 'Preferred Attributes' })); + } + + const initialSkillMatches = requirements.map(req => ({ + requirement: req.requirement, + domain: req.domain, + status: 'waiting' as const, + matchScore: 0, + assessment: '', + description: '', + citations: [] + })); + + setRequirements(requirements); + setSkillMatches(initialSkillMatches); + setStatusMessage(null); + setLoadingRequirements(false); + + }, [job, setRequirements]); + useEffect(() => { if (requirementsSession || creatingSession) { return; diff --git a/frontend/src/pages/JobAnalysisPage.tsx b/frontend/src/pages/JobAnalysisPage.tsx index 97cd89b..1615b16 100644 --- a/frontend/src/pages/JobAnalysisPage.tsx +++ b/frontend/src/pages/JobAnalysisPage.tsx @@ -89,6 +89,12 @@ const JobAnalysisPage: React.FC = (props: BackstoryPageProps } }, [selectedCandidate, activeStep]); + useEffect(() => { + if (selectedJob && activeStep === 1) { + setActiveStep(2); + } + }, [selectedJob, activeStep]); + // Steps in our process const steps = [ { index: 1, label: 'Job Selection', icon: }, diff --git a/frontend/src/services/api-client.ts b/frontend/src/services/api-client.ts index b4bdadb..1aef0fe 100644 --- a/frontend/src/services/api-client.ts +++ b/frontend/src/services/api-client.ts @@ -622,9 +622,15 @@ class ApiClient { // Job Methods with Date Conversion // ============================ - createJob(job: Omit, streamingOptions?: StreamingOptions): StreamingResponse { + async createJob(job: Omit): Promise { const body = JSON.stringify(formatApiRequest(job)); - return this.streamify(`/jobs`, body, streamingOptions); + const response = await fetch(`${this.baseUrl}/jobs`, { + method: 'POST', + headers: this.defaultHeaders, + body: body + }); + + return this.handleApiResponseWithConversion(response, 'Job'); } async getJob(id: string): Promise { @@ -824,7 +830,7 @@ class ApiClient { const document : Types.Document = await controller.promise; console.log(`Document id: ${document.id}`) */ - uploadCandidateDocument(file: File, options: Types.DocumentOptions, streamingOptions?: StreamingOptions): StreamingResponse { + uploadCandidateDocument(file: File, options: Types.DocumentOptions, streamingOptions?: StreamingOptions): StreamingResponse { const convertedOptions = toSnakeCase(options); const formData = new FormData() formData.append('file', file); @@ -837,7 +843,7 @@ class ApiClient { 'Authorization': this.defaultHeaders['Authorization'] } }; - return this.streamify('/candidates/documents/upload', formData, streamingOptions); + return this.streamify('/candidates/documents/upload', formData, streamingOptions); // { // method: 'POST', // headers: { @@ -1003,12 +1009,6 @@ class ApiClient { let messageId = ''; let finalMessage : T | null = null; - console.log('streamify: ', { - api, - method, - headers, - body: data - }); const promise = new Promise(async (resolve, reject) => { try { const response = await fetch(`${this.baseUrl}${api}`, { diff --git a/frontend/src/types/types.ts b/frontend/src/types/types.ts index aa0d5bd..dfc452e 100644 --- a/frontend/src/types/types.ts +++ b/frontend/src/types/types.ts @@ -1,6 +1,6 @@ // Generated TypeScript types from Pydantic models // Source: src/backend/models.py -// Generated on: 2025-06-05T20:17:00.575243 +// Generated on: 2025-06-05T22:02:22.004513 // DO NOT EDIT MANUALLY - This file is auto-generated // ============================ @@ -502,11 +502,14 @@ export interface DocumentMessage { type: "binary" | "text" | "json"; timestamp?: Date; document: Document; + content?: string; + converted: boolean; } export interface DocumentOptions { includeInRAG?: boolean; isJobDocument?: boolean; + overwrite?: boolean; } export interface DocumentUpdateRequest { diff --git a/src/backend/agents/job_requirements.py b/src/backend/agents/job_requirements.py index 11f16fc..1eb79ea 100644 --- a/src/backend/agents/job_requirements.py +++ b/src/backend/agents/job_requirements.py @@ -132,21 +132,20 @@ class JobRequirementsAgent(Agent): yield error_message return - job_requirements : JobRequirements | None = None + requirements = None job_requirements_data = "" - company_name = "" - job_summary = "" - job_title = "" + company = "" + summary = "" + title = "" try: json_str = self.extract_json_from_text(generated_message.content) - job_requirements_data = json.loads(json_str) - job_requirements_data = job_requirements_data.get("job_requirements", None) - job_title = job_requirements_data.get("job_title", "") - company_name = job_requirements_data.get("company_name", "") - job_summary = job_requirements_data.get("job_summary", "") - job_requirements = JobRequirements.model_validate(job_requirements_data) - if not job_requirements: - raise ValueError("Job requirements data is empty or invalid.") + requirements_json = json.loads(json_str) + + company = requirements_json.get("company_name", "") + title = requirements_json.get("job_title", "") + summary = requirements_json.get("job_summary", "") + job_requirements_data = requirements_json.get("job_requirements", None) + requirements = JobRequirements.model_validate(job_requirements_data) except json.JSONDecodeError as e: status_message.status = ApiStatusType.ERROR status_message.content = f"Failed to parse job requirements JSON: {str(e)}\n\n{job_requirements_data}" @@ -157,6 +156,7 @@ class JobRequirementsAgent(Agent): status_message.status = ApiStatusType.ERROR status_message.content = f"Job requirements validation error: {str(e)}\n\n{job_requirements_data}" logger.error(f"⚠️ {status_message.content}") + logger.error(f"Content: {prompt}") yield status_message return except Exception as e: @@ -169,14 +169,13 @@ class JobRequirementsAgent(Agent): job_requirements_message = JobRequirementsMessage( session_id=session_id, status=ApiStatusType.DONE, - requirements=job_requirements, - company=company_name, - title=job_title, - summary=job_summary, + requirements=requirements, + company=company, + title=title, + summary=summary, description=prompt, ) yield job_requirements_message - logger.info(f"✅ Job requirements analysis completed successfully.") return diff --git a/src/backend/main.py b/src/backend/main.py index 9c6d0da..e6b1ce4 100644 --- a/src/backend/main.py +++ b/src/backend/main.py @@ -1730,7 +1730,7 @@ async def upload_candidate_document( ) """Upload a document for the current candidate""" - async def upload_stream_generator(): + async def upload_stream_generator(file_content): # Verify user is a candidate if current_user.user_type != "candidate": logger.warning(f"⚠️ Unauthorized upload attempt by user type: {current_user.user_type}") @@ -1763,13 +1763,22 @@ async def upload_candidate_document( os.makedirs(dir_path, exist_ok=True) file_path = os.path.join(dir_path, file.filename) if os.path.exists(file_path): - logger.warning(f"⚠️ File already exists: {file_path}") - error_message = ChatMessageError( - session_id=MOCK_UUID, # No session ID for document uploads - content=f"File with this name already exists in the '{directory}' directory" - ) - yield error_message - return + if not options.overwrite: + logger.warning(f"⚠️ File already exists: {file_path}") + error_message = ChatMessageError( + session_id=MOCK_UUID, # No session ID for document uploads + content=f"File with this name already exists in the '{directory}' directory" + ) + yield error_message + return + else: + logger.info(f"🔄 Overwriting existing file: {file_path}") + status_message = ChatMessageStatus( + session_id=MOCK_UUID, # No session ID for document uploads + content=f"Overwriting existing file: {file.filename}", + activity=ApiActivityType.INFO + ) + yield status_message # Validate file type allowed_types = ['.txt', '.md', '.docx', '.pdf', '.png', '.jpg', '.jpeg', '.gif'] @@ -1818,6 +1827,7 @@ async def upload_candidate_document( yield error_message return + converted = False; if document_type != DocumentType.MARKDOWN and document_type != DocumentType.TXT: p = pathlib.Path(file_path) p_as_md = p.with_suffix(".md") @@ -1828,7 +1838,7 @@ async def upload_candidate_document( ): status_message = ChatMessageStatus( session_id=MOCK_UUID, # No session ID for document uploads - content=f"Converting {file.filename} to Markdown format for better processing...", + content=f"Converting content from {document_type}...", activity=ApiActivityType.CONVERTING ) yield status_message @@ -1837,6 +1847,9 @@ async def upload_candidate_document( md = MarkItDown(enable_plugins=False) # Set to True to enable plugins result = md.convert(file_path, output_format="markdown") p_as_md.write_text(result.text_content) + file_content = result.text_content + converted = True + logger.info(f"✅ Converted {file.filename} to Markdown format: {p_as_md}") file_path = p_as_md except Exception as e: error_message = ChatMessageError( @@ -1856,47 +1869,56 @@ async def upload_candidate_document( type=ApiMessageType.JSON, status=ApiStatusType.DONE, document=document_data, + converted=converted, + content=file_content, ) yield chat_message # If this is a job description, process it with the job requirements agent - if options.is_job_document: - content = None - with open(file_path, "r") as f: - content = f.read() - if not content or len(content) == 0: + if not options.is_job_document: + return + + status_message = ChatMessageStatus( + session_id=MOCK_UUID, # No session ID for document uploads + content=f"Initiating connection with {candidate.first_name}'s AI agent...", + activity=ApiActivityType.INFO + ) + yield status_message + await asyncio.sleep(0) + + async with entities.get_candidate_entity(candidate=candidate) as candidate_entity: + 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="Job description file is empty" + content="No agent found for job requirements chat type" ) yield error_message return - async with entities.get_candidate_entity(candidate=candidate) as candidate_entity: - 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 - message = None - async for message in chat_agent.generate( - llm=llm_manager.get_llm(), - model=defines.model, - session_id=MOCK_UUID, - prompt=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="Failed to process job description file" - ) - yield error_message - return - yield message + message = None + status_message = ChatMessageStatus( + session_id=MOCK_UUID, # No session ID for document uploads + content=f"Analyzing document for company and requirement details...", + activity=ApiActivityType.SEARCHING + ) + yield status_message + await asyncio.sleep(0) + + async for message in chat_agent.generate( + llm=llm_manager.get_llm(), + model=defines.model, + session_id=MOCK_UUID, + prompt=file_content + ): + pass + if not message or not isinstance(message, JobRequirementsMessage): + error_message = ChatMessageError( + session_id=MOCK_UUID, # No session ID for document uploads + content="Failed to process job description file" + ) + yield error_message + return + yield message try: async def to_json(method): @@ -1912,7 +1934,7 @@ async def upload_candidate_document( # return DebugStreamingResponse( return StreamingResponse( - to_json(upload_stream_generator()), + to_json(upload_stream_generator(file_content)), media_type="text/event-stream", headers={ "Cache-Control": "no-cache, no-store, must-revalidate", diff --git a/src/backend/models.py b/src/backend/models.py index da7e987..d5010eb 100644 --- a/src/backend/models.py +++ b/src/backend/models.py @@ -521,6 +521,7 @@ class DocumentType(str, Enum): class DocumentOptions(BaseModel): include_in_RAG: Optional[bool] = Field(True, alias="includeInRAG") is_job_document: Optional[bool] = Field(False, alias="isJobDocument") + overwrite: Optional[bool] = Field(False, alias="overwrite") model_config = { "populate_by_name": True # Allow both field names and aliases } @@ -829,6 +830,8 @@ class JobRequirementsMessage(ApiMessage): class DocumentMessage(ApiMessage): type: ApiMessageType = ApiMessageType.JSON document: Document = Field(..., alias="document") + content: Optional[str] = "" + converted: bool = Field(False, alias="converted") model_config = { "populate_by_name": True # Allow both field names and aliases }