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
-
-
- }
- size={isMobile ? "small" : "medium"}>
- Upload
-
+ {/* Upload Section */}
+
+ }
+ />
+
+
+
+
+
+ Upload Job Description
+
+
+
+
+ Drop your job description here
+
+
+ Supported formats: PDF, DOCX, TXT, MD
+
+ }
+ disabled={isProcessing}
+ // onClick={handleUploadClick}
+ >
+ Choose File
+
+
+
+
+
+
+
+
+ Or Enter Manually
+
+ setJobDescription(e.target.value)}
+ disabled={isProcessing}
+ sx={{ mb: 2 }}
+ />
+ {jobRequirements === null && jobDescription && (
+ }
+ disabled={isProcessing}
+ fullWidth={isMobile}
+ >
+ Extract Requirements
+
+ )}
+
+
+
+ {(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:
+ }}
+ />
+ */}
+
+
+
+ }
+ >
+ Save Job
+
+
+
+
+
+
+
+ {/* 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
}