Compare commits

...

2 Commits

Author SHA1 Message Date
4f4187eba4 Working on job creation flow 2025-06-09 19:57:08 -07:00
9edf5a5b23 Added recursive pydantic model converter 2025-06-09 19:56:55 -07:00
11 changed files with 631 additions and 239 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

@ -1,6 +1,6 @@
// Generated TypeScript types from Pydantic models // Generated TypeScript types from Pydantic models
// Source: src/backend/models.py // Source: src/backend/models.py
// Generated on: 2025-06-09T20:36:06.432367 // Generated on: 2025-06-10T02:48:12.087485
// DO NOT EDIT MANUALLY - This file is auto-generated // DO NOT EDIT MANUALLY - This file is auto-generated
// ============================ // ============================
@ -161,8 +161,8 @@ export interface BaseUser {
fullName: string; fullName: string;
phone?: string; phone?: string;
location?: Location; location?: Location;
createdAt: Date; createdAt?: Date;
updatedAt: Date; updatedAt?: Date;
lastLogin?: Date; lastLogin?: Date;
profileImage?: string; profileImage?: string;
status: "active" | "inactive" | "pending" | "banned"; status: "active" | "inactive" | "pending" | "banned";
@ -185,8 +185,8 @@ export interface Candidate {
fullName: string; fullName: string;
phone?: string; phone?: string;
location?: Location; location?: Location;
createdAt: Date; createdAt?: Date;
updatedAt: Date; updatedAt?: Date;
lastLogin?: Date; lastLogin?: Date;
profileImage?: string; profileImage?: string;
status: "active" | "inactive" | "pending" | "banned"; status: "active" | "inactive" | "pending" | "banned";
@ -219,8 +219,8 @@ export interface CandidateAI {
fullName: string; fullName: string;
phone?: string; phone?: string;
location?: Location; location?: Location;
createdAt: Date; createdAt?: Date;
updatedAt: Date; updatedAt?: Date;
lastLogin?: Date; lastLogin?: Date;
profileImage?: string; profileImage?: string;
status: "active" | "inactive" | "pending" | "banned"; status: "active" | "inactive" | "pending" | "banned";
@ -353,6 +353,8 @@ export interface ChatMessageResume {
tunables?: Tunables; tunables?: Tunables;
metadata: ChatMessageMetaData; metadata: ChatMessageMetaData;
resume: string; resume: string;
systemPrompt?: string;
prompt?: string;
} }
export interface ChatMessageSkillAssessment { export interface ChatMessageSkillAssessment {
@ -567,8 +569,8 @@ export interface Employer {
fullName: string; fullName: string;
phone?: string; phone?: string;
location?: Location; location?: Location;
createdAt: Date; createdAt?: Date;
updatedAt: Date; updatedAt?: Date;
lastLogin?: Date; lastLogin?: Date;
profileImage?: string; profileImage?: string;
status: "active" | "inactive" | "pending" | "banned"; status: "active" | "inactive" | "pending" | "banned";
@ -620,8 +622,8 @@ export interface Guest {
fullName: string; fullName: string;
phone?: string; phone?: string;
location?: Location; location?: Location;
createdAt: Date; createdAt?: Date;
updatedAt: Date; updatedAt?: Date;
lastLogin?: Date; lastLogin?: Date;
profileImage?: string; profileImage?: string;
status: "active" | "inactive" | "pending" | "banned"; status: "active" | "inactive" | "pending" | "banned";
@ -682,8 +684,8 @@ export interface InterviewFeedback {
weaknesses: Array<string>; weaknesses: Array<string>;
recommendation: "strong_hire" | "hire" | "no_hire" | "strong_no_hire"; recommendation: "strong_hire" | "hire" | "no_hire" | "strong_no_hire";
comments: string; comments: string;
createdAt: Date; createdAt?: Date;
updatedAt: Date; updatedAt?: Date;
isVisible: boolean; isVisible: boolean;
skillAssessments?: Array<SkillAssessment>; skillAssessments?: Array<SkillAssessment>;
} }
@ -712,8 +714,8 @@ export interface Job {
company?: string; company?: string;
description: string; description: string;
requirements?: JobRequirements; requirements?: JobRequirements;
createdAt: Date; createdAt?: Date;
updatedAt: Date; updatedAt?: Date;
} }
export interface JobApplication { export interface JobApplication {
@ -742,12 +744,12 @@ export interface JobFull {
company?: string; company?: string;
description: string; description: string;
requirements?: JobRequirements; requirements?: JobRequirements;
createdAt: Date; createdAt?: Date;
updatedAt: Date; updatedAt?: Date;
location: Location; location: Location;
salaryRange?: SalaryRange; salaryRange?: SalaryRange;
employmentType: "full-time" | "part-time" | "contract" | "internship" | "freelance"; employmentType: "full-time" | "part-time" | "contract" | "internship" | "freelance";
datePosted: Date; datePosted?: Date;
applicationDeadline?: Date; applicationDeadline?: Date;
isActive: boolean; isActive: boolean;
applicants?: Array<JobApplication>; applicants?: Array<JobApplication>;
@ -784,11 +786,7 @@ export interface JobRequirementsMessage {
status: "streaming" | "status" | "done" | "error"; status: "streaming" | "status" | "done" | "error";
type: "binary" | "text" | "json"; type: "binary" | "text" | "json";
timestamp?: Date; timestamp?: Date;
title?: string; job: Job;
summary?: string;
company?: string;
description: string;
requirements?: JobRequirements;
} }
export interface JobResponse { export interface JobResponse {
@ -901,8 +899,8 @@ export interface RAGConfiguration {
embeddingModel: string; embeddingModel: string;
vectorStoreType: "chroma"; vectorStoreType: "chroma";
retrievalParameters: RetrievalParameters; retrievalParameters: RetrievalParameters;
createdAt: Date; createdAt?: Date;
updatedAt: Date; updatedAt?: Date;
version: number; version: number;
isActive: boolean; isActive: boolean;
} }
@ -1078,14 +1076,14 @@ export interface WorkExperience {
} }
// ============================ // ============================
// Date Conversion Functions // Date and Nested Model Conversion Functions
// ============================ // ============================
// These functions convert API responses to properly typed objects // These functions convert API responses to properly typed objects
// with Date objects instead of ISO date strings // with Date objects instead of ISO date strings and nested model conversions
/** /**
* Convert Analytics from API response, parsing date fields * Convert Analytics from API response
* Date fields: timestamp * Date fields: timestamp
*/ */
export function convertAnalyticsFromApi(data: any): Analytics { export function convertAnalyticsFromApi(data: any): Analytics {
@ -1098,7 +1096,7 @@ export function convertAnalyticsFromApi(data: any): Analytics {
}; };
} }
/** /**
* Convert ApiMessage from API response, parsing date fields * Convert ApiMessage from API response
* Date fields: timestamp * Date fields: timestamp
*/ */
export function convertApiMessageFromApi(data: any): ApiMessage { export function convertApiMessageFromApi(data: any): ApiMessage {
@ -1111,7 +1109,7 @@ export function convertApiMessageFromApi(data: any): ApiMessage {
}; };
} }
/** /**
* Convert ApplicationDecision from API response, parsing date fields * Convert ApplicationDecision from API response
* Date fields: date * Date fields: date
*/ */
export function convertApplicationDecisionFromApi(data: any): ApplicationDecision { export function convertApplicationDecisionFromApi(data: any): ApplicationDecision {
@ -1124,7 +1122,7 @@ export function convertApplicationDecisionFromApi(data: any): ApplicationDecisio
}; };
} }
/** /**
* Convert Attachment from API response, parsing date fields * Convert Attachment from API response
* Date fields: uploadedAt * Date fields: uploadedAt
*/ */
export function convertAttachmentFromApi(data: any): Attachment { export function convertAttachmentFromApi(data: any): Attachment {
@ -1137,8 +1135,22 @@ export function convertAttachmentFromApi(data: any): Attachment {
}; };
} }
/** /**
* Convert Authentication from API response, parsing date fields * Convert AuthResponse from API response
* Nested models: user (Candidate)
*/
export function convertAuthResponseFromApi(data: any): AuthResponse {
if (!data) return data;
return {
...data,
// Convert nested Candidate model
user: convertCandidateFromApi(data.user),
};
}
/**
* Convert Authentication from API response
* Date fields: resetPasswordExpiry, lastPasswordChange, lockedUntil * Date fields: resetPasswordExpiry, lastPasswordChange, lockedUntil
* Nested models: refreshTokens (RefreshToken)
*/ */
export function convertAuthenticationFromApi(data: any): Authentication { export function convertAuthenticationFromApi(data: any): Authentication {
if (!data) return data; if (!data) return data;
@ -1151,10 +1163,12 @@ export function convertAuthenticationFromApi(data: any): Authentication {
lastPasswordChange: new Date(data.lastPasswordChange), lastPasswordChange: new Date(data.lastPasswordChange),
// Convert lockedUntil from ISO string to Date // Convert lockedUntil from ISO string to Date
lockedUntil: data.lockedUntil ? new Date(data.lockedUntil) : undefined, lockedUntil: data.lockedUntil ? new Date(data.lockedUntil) : undefined,
// Convert nested RefreshToken model
refreshTokens: data.refreshTokens.map((item: any) => convertRefreshTokenFromApi(item)),
}; };
} }
/** /**
* Convert BaseUser from API response, parsing date fields * Convert BaseUser from API response
* Date fields: lastActivity, createdAt, updatedAt, lastLogin * Date fields: lastActivity, createdAt, updatedAt, lastLogin
*/ */
export function convertBaseUserFromApi(data: any): BaseUser { export function convertBaseUserFromApi(data: any): BaseUser {
@ -1165,15 +1179,15 @@ export function convertBaseUserFromApi(data: any): BaseUser {
// Convert lastActivity from ISO string to Date // Convert lastActivity from ISO string to Date
lastActivity: data.lastActivity ? new Date(data.lastActivity) : undefined, lastActivity: data.lastActivity ? new Date(data.lastActivity) : undefined,
// Convert createdAt from ISO string to Date // Convert createdAt from ISO string to Date
createdAt: new Date(data.createdAt), createdAt: data.createdAt ? new Date(data.createdAt) : undefined,
// Convert updatedAt from ISO string to Date // Convert updatedAt from ISO string to Date
updatedAt: new Date(data.updatedAt), updatedAt: data.updatedAt ? new Date(data.updatedAt) : undefined,
// Convert lastLogin from ISO string to Date // Convert lastLogin from ISO string to Date
lastLogin: data.lastLogin ? new Date(data.lastLogin) : undefined, lastLogin: data.lastLogin ? new Date(data.lastLogin) : undefined,
}; };
} }
/** /**
* Convert BaseUserWithType from API response, parsing date fields * Convert BaseUserWithType from API response
* Date fields: lastActivity * Date fields: lastActivity
*/ */
export function convertBaseUserWithTypeFromApi(data: any): BaseUserWithType { export function convertBaseUserWithTypeFromApi(data: any): BaseUserWithType {
@ -1186,8 +1200,9 @@ export function convertBaseUserWithTypeFromApi(data: any): BaseUserWithType {
}; };
} }
/** /**
* Convert Candidate from API response, parsing date fields * Convert Candidate from API response
* Date fields: lastActivity, createdAt, updatedAt, lastLogin, availabilityDate * Date fields: lastActivity, createdAt, updatedAt, lastLogin, availabilityDate
* Nested models: experience (WorkExperience), education (Education), certifications (Certification), jobApplications (JobApplication)
*/ */
export function convertCandidateFromApi(data: any): Candidate { export function convertCandidateFromApi(data: any): Candidate {
if (!data) return data; if (!data) return data;
@ -1197,18 +1212,27 @@ export function convertCandidateFromApi(data: any): Candidate {
// Convert lastActivity from ISO string to Date // Convert lastActivity from ISO string to Date
lastActivity: data.lastActivity ? new Date(data.lastActivity) : undefined, lastActivity: data.lastActivity ? new Date(data.lastActivity) : undefined,
// Convert createdAt from ISO string to Date // Convert createdAt from ISO string to Date
createdAt: new Date(data.createdAt), createdAt: data.createdAt ? new Date(data.createdAt) : undefined,
// Convert updatedAt from ISO string to Date // Convert updatedAt from ISO string to Date
updatedAt: new Date(data.updatedAt), updatedAt: data.updatedAt ? new Date(data.updatedAt) : undefined,
// Convert lastLogin from ISO string to Date // Convert lastLogin from ISO string to Date
lastLogin: data.lastLogin ? new Date(data.lastLogin) : undefined, lastLogin: data.lastLogin ? new Date(data.lastLogin) : undefined,
// Convert availabilityDate from ISO string to Date // Convert availabilityDate from ISO string to Date
availabilityDate: data.availabilityDate ? new Date(data.availabilityDate) : undefined, availabilityDate: data.availabilityDate ? new Date(data.availabilityDate) : undefined,
// Convert nested WorkExperience model
experience: data.experience ? convertWorkExperienceFromApi(data.experience) : undefined,
// Convert nested Education model
education: data.education ? convertEducationFromApi(data.education) : undefined,
// Convert nested Certification model
certifications: data.certifications ? convertCertificationFromApi(data.certifications) : undefined,
// Convert nested JobApplication model
jobApplications: data.jobApplications ? convertJobApplicationFromApi(data.jobApplications) : undefined,
}; };
} }
/** /**
* Convert CandidateAI from API response, parsing date fields * Convert CandidateAI from API response
* Date fields: lastActivity, createdAt, updatedAt, lastLogin, availabilityDate * Date fields: lastActivity, createdAt, updatedAt, lastLogin, availabilityDate
* Nested models: experience (WorkExperience), education (Education), certifications (Certification)
*/ */
export function convertCandidateAIFromApi(data: any): CandidateAI { export function convertCandidateAIFromApi(data: any): CandidateAI {
if (!data) return data; if (!data) return data;
@ -1218,17 +1242,49 @@ export function convertCandidateAIFromApi(data: any): CandidateAI {
// Convert lastActivity from ISO string to Date // Convert lastActivity from ISO string to Date
lastActivity: data.lastActivity ? new Date(data.lastActivity) : undefined, lastActivity: data.lastActivity ? new Date(data.lastActivity) : undefined,
// Convert createdAt from ISO string to Date // Convert createdAt from ISO string to Date
createdAt: new Date(data.createdAt), createdAt: data.createdAt ? new Date(data.createdAt) : undefined,
// Convert updatedAt from ISO string to Date // Convert updatedAt from ISO string to Date
updatedAt: new Date(data.updatedAt), updatedAt: data.updatedAt ? new Date(data.updatedAt) : undefined,
// Convert lastLogin from ISO string to Date // Convert lastLogin from ISO string to Date
lastLogin: data.lastLogin ? new Date(data.lastLogin) : undefined, lastLogin: data.lastLogin ? new Date(data.lastLogin) : undefined,
// Convert availabilityDate from ISO string to Date // Convert availabilityDate from ISO string to Date
availabilityDate: data.availabilityDate ? new Date(data.availabilityDate) : undefined, availabilityDate: data.availabilityDate ? new Date(data.availabilityDate) : undefined,
// Convert nested WorkExperience model
experience: data.experience ? convertWorkExperienceFromApi(data.experience) : undefined,
// Convert nested Education model
education: data.education ? convertEducationFromApi(data.education) : undefined,
// Convert nested Certification model
certifications: data.certifications ? convertCertificationFromApi(data.certifications) : undefined,
}; };
} }
/** /**
* Convert Certification from API response, parsing date fields * Convert CandidateListResponse from API response
* Nested models: data (Candidate)
*/
export function convertCandidateListResponseFromApi(data: any): CandidateListResponse {
if (!data) return data;
return {
...data,
// Convert nested Candidate model
data: data.data ? convertCandidateFromApi(data.data) : undefined,
};
}
/**
* Convert CandidateResponse from API response
* Nested models: data (Candidate)
*/
export function convertCandidateResponseFromApi(data: any): CandidateResponse {
if (!data) return data;
return {
...data,
// Convert nested Candidate model
data: data.data ? convertCandidateFromApi(data.data) : undefined,
};
}
/**
* Convert Certification from API response
* Date fields: issueDate, expirationDate * Date fields: issueDate, expirationDate
*/ */
export function convertCertificationFromApi(data: any): Certification { export function convertCertificationFromApi(data: any): Certification {
@ -1243,7 +1299,7 @@ export function convertCertificationFromApi(data: any): Certification {
}; };
} }
/** /**
* Convert ChatMessage from API response, parsing date fields * Convert ChatMessage from API response
* Date fields: timestamp * Date fields: timestamp
*/ */
export function convertChatMessageFromApi(data: any): ChatMessage { export function convertChatMessageFromApi(data: any): ChatMessage {
@ -1256,7 +1312,7 @@ export function convertChatMessageFromApi(data: any): ChatMessage {
}; };
} }
/** /**
* Convert ChatMessageError from API response, parsing date fields * Convert ChatMessageError from API response
* Date fields: timestamp * Date fields: timestamp
*/ */
export function convertChatMessageErrorFromApi(data: any): ChatMessageError { export function convertChatMessageErrorFromApi(data: any): ChatMessageError {
@ -1269,7 +1325,7 @@ export function convertChatMessageErrorFromApi(data: any): ChatMessageError {
}; };
} }
/** /**
* Convert ChatMessageRagSearch from API response, parsing date fields * Convert ChatMessageRagSearch from API response
* Date fields: timestamp * Date fields: timestamp
*/ */
export function convertChatMessageRagSearchFromApi(data: any): ChatMessageRagSearch { export function convertChatMessageRagSearchFromApi(data: any): ChatMessageRagSearch {
@ -1282,7 +1338,7 @@ export function convertChatMessageRagSearchFromApi(data: any): ChatMessageRagSea
}; };
} }
/** /**
* Convert ChatMessageResume from API response, parsing date fields * Convert ChatMessageResume from API response
* Date fields: timestamp * Date fields: timestamp
*/ */
export function convertChatMessageResumeFromApi(data: any): ChatMessageResume { export function convertChatMessageResumeFromApi(data: any): ChatMessageResume {
@ -1295,8 +1351,9 @@ export function convertChatMessageResumeFromApi(data: any): ChatMessageResume {
}; };
} }
/** /**
* Convert ChatMessageSkillAssessment from API response, parsing date fields * Convert ChatMessageSkillAssessment from API response
* Date fields: timestamp * Date fields: timestamp
* Nested models: skillAssessment (SkillAssessment)
*/ */
export function convertChatMessageSkillAssessmentFromApi(data: any): ChatMessageSkillAssessment { export function convertChatMessageSkillAssessmentFromApi(data: any): ChatMessageSkillAssessment {
if (!data) return data; if (!data) return data;
@ -1305,10 +1362,12 @@ export function convertChatMessageSkillAssessmentFromApi(data: any): ChatMessage
...data, ...data,
// Convert timestamp from ISO string to Date // Convert timestamp from ISO string to Date
timestamp: data.timestamp ? new Date(data.timestamp) : undefined, timestamp: data.timestamp ? new Date(data.timestamp) : undefined,
// Convert nested SkillAssessment model
skillAssessment: convertSkillAssessmentFromApi(data.skillAssessment),
}; };
} }
/** /**
* Convert ChatMessageStatus from API response, parsing date fields * Convert ChatMessageStatus from API response
* Date fields: timestamp * Date fields: timestamp
*/ */
export function convertChatMessageStatusFromApi(data: any): ChatMessageStatus { export function convertChatMessageStatusFromApi(data: any): ChatMessageStatus {
@ -1321,7 +1380,7 @@ export function convertChatMessageStatusFromApi(data: any): ChatMessageStatus {
}; };
} }
/** /**
* Convert ChatMessageStreaming from API response, parsing date fields * Convert ChatMessageStreaming from API response
* Date fields: timestamp * Date fields: timestamp
*/ */
export function convertChatMessageStreamingFromApi(data: any): ChatMessageStreaming { export function convertChatMessageStreamingFromApi(data: any): ChatMessageStreaming {
@ -1334,7 +1393,7 @@ export function convertChatMessageStreamingFromApi(data: any): ChatMessageStream
}; };
} }
/** /**
* Convert ChatMessageUser from API response, parsing date fields * Convert ChatMessageUser from API response
* Date fields: timestamp * Date fields: timestamp
*/ */
export function convertChatMessageUserFromApi(data: any): ChatMessageUser { export function convertChatMessageUserFromApi(data: any): ChatMessageUser {
@ -1347,7 +1406,7 @@ export function convertChatMessageUserFromApi(data: any): ChatMessageUser {
}; };
} }
/** /**
* Convert ChatSession from API response, parsing date fields * Convert ChatSession from API response
* Date fields: createdAt, lastActivity * Date fields: createdAt, lastActivity
*/ */
export function convertChatSessionFromApi(data: any): ChatSession { export function convertChatSessionFromApi(data: any): ChatSession {
@ -1362,7 +1421,7 @@ export function convertChatSessionFromApi(data: any): ChatSession {
}; };
} }
/** /**
* Convert DataSourceConfiguration from API response, parsing date fields * Convert DataSourceConfiguration from API response
* Date fields: lastRefreshed * Date fields: lastRefreshed
*/ */
export function convertDataSourceConfigurationFromApi(data: any): DataSourceConfiguration { export function convertDataSourceConfigurationFromApi(data: any): DataSourceConfiguration {
@ -1375,7 +1434,7 @@ export function convertDataSourceConfigurationFromApi(data: any): DataSourceConf
}; };
} }
/** /**
* Convert Document from API response, parsing date fields * Convert Document from API response
* Date fields: uploadDate * Date fields: uploadDate
*/ */
export function convertDocumentFromApi(data: any): Document { export function convertDocumentFromApi(data: any): Document {
@ -1388,8 +1447,22 @@ export function convertDocumentFromApi(data: any): Document {
}; };
} }
/** /**
* Convert DocumentMessage from API response, parsing date fields * Convert DocumentListResponse from API response
* Nested models: documents (Document)
*/
export function convertDocumentListResponseFromApi(data: any): DocumentListResponse {
if (!data) return data;
return {
...data,
// Convert nested Document model
documents: data.documents.map((item: any) => convertDocumentFromApi(item)),
};
}
/**
* Convert DocumentMessage from API response
* Date fields: timestamp * Date fields: timestamp
* Nested models: document (Document)
*/ */
export function convertDocumentMessageFromApi(data: any): DocumentMessage { export function convertDocumentMessageFromApi(data: any): DocumentMessage {
if (!data) return data; if (!data) return data;
@ -1398,10 +1471,12 @@ export function convertDocumentMessageFromApi(data: any): DocumentMessage {
...data, ...data,
// Convert timestamp from ISO string to Date // Convert timestamp from ISO string to Date
timestamp: data.timestamp ? new Date(data.timestamp) : undefined, timestamp: data.timestamp ? new Date(data.timestamp) : undefined,
// Convert nested Document model
document: convertDocumentFromApi(data.document),
}; };
} }
/** /**
* Convert EditHistory from API response, parsing date fields * Convert EditHistory from API response
* Date fields: editedAt * Date fields: editedAt
*/ */
export function convertEditHistoryFromApi(data: any): EditHistory { export function convertEditHistoryFromApi(data: any): EditHistory {
@ -1414,7 +1489,7 @@ export function convertEditHistoryFromApi(data: any): EditHistory {
}; };
} }
/** /**
* Convert Education from API response, parsing date fields * Convert Education from API response
* Date fields: startDate, endDate * Date fields: startDate, endDate
*/ */
export function convertEducationFromApi(data: any): Education { export function convertEducationFromApi(data: any): Education {
@ -1429,8 +1504,9 @@ export function convertEducationFromApi(data: any): Education {
}; };
} }
/** /**
* Convert Employer from API response, parsing date fields * Convert Employer from API response
* Date fields: lastActivity, createdAt, updatedAt, lastLogin * Date fields: lastActivity, createdAt, updatedAt, lastLogin
* Nested models: jobs (Job)
*/ */
export function convertEmployerFromApi(data: any): Employer { export function convertEmployerFromApi(data: any): Employer {
if (!data) return data; if (!data) return data;
@ -1440,15 +1516,30 @@ export function convertEmployerFromApi(data: any): Employer {
// Convert lastActivity from ISO string to Date // Convert lastActivity from ISO string to Date
lastActivity: data.lastActivity ? new Date(data.lastActivity) : undefined, lastActivity: data.lastActivity ? new Date(data.lastActivity) : undefined,
// Convert createdAt from ISO string to Date // Convert createdAt from ISO string to Date
createdAt: new Date(data.createdAt), createdAt: data.createdAt ? new Date(data.createdAt) : undefined,
// Convert updatedAt from ISO string to Date // Convert updatedAt from ISO string to Date
updatedAt: new Date(data.updatedAt), updatedAt: data.updatedAt ? new Date(data.updatedAt) : undefined,
// Convert lastLogin from ISO string to Date // Convert lastLogin from ISO string to Date
lastLogin: data.lastLogin ? new Date(data.lastLogin) : undefined, lastLogin: data.lastLogin ? new Date(data.lastLogin) : undefined,
// Convert nested Job model
jobs: data.jobs ? convertJobFromApi(data.jobs) : undefined,
}; };
} }
/** /**
* Convert Guest from API response, parsing date fields * Convert EmployerResponse from API response
* Nested models: data (Employer)
*/
export function convertEmployerResponseFromApi(data: any): EmployerResponse {
if (!data) return data;
return {
...data,
// Convert nested Employer model
data: data.data ? convertEmployerFromApi(data.data) : undefined,
};
}
/**
* Convert Guest from API response
* Date fields: lastActivity, createdAt, updatedAt, lastLogin * Date fields: lastActivity, createdAt, updatedAt, lastLogin
*/ */
export function convertGuestFromApi(data: any): Guest { export function convertGuestFromApi(data: any): Guest {
@ -1459,16 +1550,30 @@ export function convertGuestFromApi(data: any): Guest {
// Convert lastActivity from ISO string to Date // Convert lastActivity from ISO string to Date
lastActivity: data.lastActivity ? new Date(data.lastActivity) : undefined, lastActivity: data.lastActivity ? new Date(data.lastActivity) : undefined,
// Convert createdAt from ISO string to Date // Convert createdAt from ISO string to Date
createdAt: new Date(data.createdAt), createdAt: data.createdAt ? new Date(data.createdAt) : undefined,
// Convert updatedAt from ISO string to Date // Convert updatedAt from ISO string to Date
updatedAt: new Date(data.updatedAt), updatedAt: data.updatedAt ? new Date(data.updatedAt) : undefined,
// Convert lastLogin from ISO string to Date // Convert lastLogin from ISO string to Date
lastLogin: data.lastLogin ? new Date(data.lastLogin) : undefined, lastLogin: data.lastLogin ? new Date(data.lastLogin) : undefined,
}; };
} }
/** /**
* Convert InterviewFeedback from API response, parsing date fields * Convert GuestSessionResponse from API response
* Nested models: user (Guest)
*/
export function convertGuestSessionResponseFromApi(data: any): GuestSessionResponse {
if (!data) return data;
return {
...data,
// Convert nested Guest model
user: convertGuestFromApi(data.user),
};
}
/**
* Convert InterviewFeedback from API response
* Date fields: createdAt, updatedAt * Date fields: createdAt, updatedAt
* Nested models: skillAssessments (SkillAssessment)
*/ */
export function convertInterviewFeedbackFromApi(data: any): InterviewFeedback { export function convertInterviewFeedbackFromApi(data: any): InterviewFeedback {
if (!data) return data; if (!data) return data;
@ -1476,14 +1581,17 @@ export function convertInterviewFeedbackFromApi(data: any): InterviewFeedback {
return { return {
...data, ...data,
// Convert createdAt from ISO string to Date // Convert createdAt from ISO string to Date
createdAt: new Date(data.createdAt), createdAt: data.createdAt ? new Date(data.createdAt) : undefined,
// Convert updatedAt from ISO string to Date // Convert updatedAt from ISO string to Date
updatedAt: new Date(data.updatedAt), updatedAt: data.updatedAt ? new Date(data.updatedAt) : undefined,
// Convert nested SkillAssessment model
skillAssessments: data.skillAssessments ? convertSkillAssessmentFromApi(data.skillAssessments) : undefined,
}; };
} }
/** /**
* Convert InterviewSchedule from API response, parsing date fields * Convert InterviewSchedule from API response
* Date fields: scheduledDate, endDate * Date fields: scheduledDate, endDate
* Nested models: feedback (InterviewFeedback)
*/ */
export function convertInterviewScheduleFromApi(data: any): InterviewSchedule { export function convertInterviewScheduleFromApi(data: any): InterviewSchedule {
if (!data) return data; if (!data) return data;
@ -1494,11 +1602,14 @@ export function convertInterviewScheduleFromApi(data: any): InterviewSchedule {
scheduledDate: new Date(data.scheduledDate), scheduledDate: new Date(data.scheduledDate),
// Convert endDate from ISO string to Date // Convert endDate from ISO string to Date
endDate: new Date(data.endDate), endDate: new Date(data.endDate),
// Convert nested InterviewFeedback model
feedback: data.feedback ? convertInterviewFeedbackFromApi(data.feedback) : undefined,
}; };
} }
/** /**
* Convert Job from API response, parsing date fields * Convert Job from API response
* Date fields: createdAt, updatedAt * Date fields: createdAt, updatedAt
* Nested models: owner (BaseUser)
*/ */
export function convertJobFromApi(data: any): Job { export function convertJobFromApi(data: any): Job {
if (!data) return data; if (!data) return data;
@ -1506,14 +1617,17 @@ export function convertJobFromApi(data: any): Job {
return { return {
...data, ...data,
// Convert createdAt from ISO string to Date // Convert createdAt from ISO string to Date
createdAt: new Date(data.createdAt), createdAt: data.createdAt ? new Date(data.createdAt) : undefined,
// Convert updatedAt from ISO string to Date // Convert updatedAt from ISO string to Date
updatedAt: new Date(data.updatedAt), updatedAt: data.updatedAt ? new Date(data.updatedAt) : undefined,
// Convert nested BaseUser model
owner: data.owner ? convertBaseUserFromApi(data.owner) : undefined,
}; };
} }
/** /**
* Convert JobApplication from API response, parsing date fields * Convert JobApplication from API response
* Date fields: appliedDate, updatedDate * Date fields: appliedDate, updatedDate
* Nested models: interviewSchedules (InterviewSchedule), decision (ApplicationDecision)
*/ */
export function convertJobApplicationFromApi(data: any): JobApplication { export function convertJobApplicationFromApi(data: any): JobApplication {
if (!data) return data; if (!data) return data;
@ -1524,11 +1638,16 @@ export function convertJobApplicationFromApi(data: any): JobApplication {
appliedDate: new Date(data.appliedDate), appliedDate: new Date(data.appliedDate),
// Convert updatedDate from ISO string to Date // Convert updatedDate from ISO string to Date
updatedDate: new Date(data.updatedDate), updatedDate: new Date(data.updatedDate),
// Convert nested InterviewSchedule model
interviewSchedules: data.interviewSchedules ? convertInterviewScheduleFromApi(data.interviewSchedules) : undefined,
// Convert nested ApplicationDecision model
decision: data.decision ? convertApplicationDecisionFromApi(data.decision) : undefined,
}; };
} }
/** /**
* Convert JobFull from API response, parsing date fields * Convert JobFull from API response
* Date fields: createdAt, updatedAt, datePosted, applicationDeadline, featuredUntil * Date fields: createdAt, updatedAt, datePosted, applicationDeadline, featuredUntil
* Nested models: owner (BaseUser), applicants (JobApplication)
*/ */
export function convertJobFullFromApi(data: any): JobFull { export function convertJobFullFromApi(data: any): JobFull {
if (!data) return data; if (!data) return data;
@ -1536,20 +1655,38 @@ export function convertJobFullFromApi(data: any): JobFull {
return { return {
...data, ...data,
// Convert createdAt from ISO string to Date // Convert createdAt from ISO string to Date
createdAt: new Date(data.createdAt), createdAt: data.createdAt ? new Date(data.createdAt) : undefined,
// Convert updatedAt from ISO string to Date // Convert updatedAt from ISO string to Date
updatedAt: new Date(data.updatedAt), updatedAt: data.updatedAt ? new Date(data.updatedAt) : undefined,
// Convert datePosted from ISO string to Date // Convert datePosted from ISO string to Date
datePosted: new Date(data.datePosted), datePosted: data.datePosted ? new Date(data.datePosted) : undefined,
// Convert applicationDeadline from ISO string to Date // Convert applicationDeadline from ISO string to Date
applicationDeadline: data.applicationDeadline ? new Date(data.applicationDeadline) : undefined, applicationDeadline: data.applicationDeadline ? new Date(data.applicationDeadline) : undefined,
// Convert featuredUntil from ISO string to Date // Convert featuredUntil from ISO string to Date
featuredUntil: data.featuredUntil ? new Date(data.featuredUntil) : undefined, featuredUntil: data.featuredUntil ? new Date(data.featuredUntil) : undefined,
// Convert nested BaseUser model
owner: data.owner ? convertBaseUserFromApi(data.owner) : undefined,
// Convert nested JobApplication model
applicants: data.applicants ? convertJobApplicationFromApi(data.applicants) : undefined,
}; };
} }
/** /**
* Convert JobRequirementsMessage from API response, parsing date fields * Convert JobListResponse from API response
* Nested models: data (Job)
*/
export function convertJobListResponseFromApi(data: any): JobListResponse {
if (!data) return data;
return {
...data,
// Convert nested Job model
data: data.data ? convertJobFromApi(data.data) : undefined,
};
}
/**
* Convert JobRequirementsMessage from API response
* Date fields: timestamp * Date fields: timestamp
* Nested models: job (Job)
*/ */
export function convertJobRequirementsMessageFromApi(data: any): JobRequirementsMessage { export function convertJobRequirementsMessageFromApi(data: any): JobRequirementsMessage {
if (!data) return data; if (!data) return data;
@ -1558,10 +1695,25 @@ export function convertJobRequirementsMessageFromApi(data: any): JobRequirements
...data, ...data,
// Convert timestamp from ISO string to Date // Convert timestamp from ISO string to Date
timestamp: data.timestamp ? new Date(data.timestamp) : undefined, timestamp: data.timestamp ? new Date(data.timestamp) : undefined,
// Convert nested Job model
job: convertJobFromApi(data.job),
}; };
} }
/** /**
* Convert MessageReaction from API response, parsing date fields * Convert JobResponse from API response
* Nested models: data (Job)
*/
export function convertJobResponseFromApi(data: any): JobResponse {
if (!data) return data;
return {
...data,
// Convert nested Job model
data: data.data ? convertJobFromApi(data.data) : undefined,
};
}
/**
* Convert MessageReaction from API response
* Date fields: timestamp * Date fields: timestamp
*/ */
export function convertMessageReactionFromApi(data: any): MessageReaction { export function convertMessageReactionFromApi(data: any): MessageReaction {
@ -1574,8 +1726,9 @@ export function convertMessageReactionFromApi(data: any): MessageReaction {
}; };
} }
/** /**
* Convert RAGConfiguration from API response, parsing date fields * Convert RAGConfiguration from API response
* Date fields: createdAt, updatedAt * Date fields: createdAt, updatedAt
* Nested models: dataSourceConfigurations (DataSourceConfiguration)
*/ */
export function convertRAGConfigurationFromApi(data: any): RAGConfiguration { export function convertRAGConfigurationFromApi(data: any): RAGConfiguration {
if (!data) return data; if (!data) return data;
@ -1583,13 +1736,15 @@ export function convertRAGConfigurationFromApi(data: any): RAGConfiguration {
return { return {
...data, ...data,
// Convert createdAt from ISO string to Date // Convert createdAt from ISO string to Date
createdAt: new Date(data.createdAt), createdAt: data.createdAt ? new Date(data.createdAt) : undefined,
// Convert updatedAt from ISO string to Date // Convert updatedAt from ISO string to Date
updatedAt: new Date(data.updatedAt), updatedAt: data.updatedAt ? new Date(data.updatedAt) : undefined,
// Convert nested DataSourceConfiguration model
dataSourceConfigurations: data.dataSourceConfigurations.map((item: any) => convertDataSourceConfigurationFromApi(item)),
}; };
} }
/** /**
* Convert RateLimitResult from API response, parsing date fields * Convert RateLimitResult from API response
* Date fields: resetTimes * Date fields: resetTimes
*/ */
export function convertRateLimitResultFromApi(data: any): RateLimitResult { export function convertRateLimitResultFromApi(data: any): RateLimitResult {
@ -1602,7 +1757,7 @@ export function convertRateLimitResultFromApi(data: any): RateLimitResult {
}; };
} }
/** /**
* Convert RateLimitStatus from API response, parsing date fields * Convert RateLimitStatus from API response
* Date fields: resetTimes * Date fields: resetTimes
*/ */
export function convertRateLimitStatusFromApi(data: any): RateLimitStatus { export function convertRateLimitStatusFromApi(data: any): RateLimitStatus {
@ -1615,7 +1770,7 @@ export function convertRateLimitStatusFromApi(data: any): RateLimitStatus {
}; };
} }
/** /**
* Convert RefreshToken from API response, parsing date fields * Convert RefreshToken from API response
* Date fields: expiresAt * Date fields: expiresAt
*/ */
export function convertRefreshTokenFromApi(data: any): RefreshToken { export function convertRefreshTokenFromApi(data: any): RefreshToken {
@ -1628,7 +1783,7 @@ export function convertRefreshTokenFromApi(data: any): RefreshToken {
}; };
} }
/** /**
* Convert SkillAssessment from API response, parsing date fields * Convert SkillAssessment from API response
* Date fields: createdAt, updatedAt * Date fields: createdAt, updatedAt
*/ */
export function convertSkillAssessmentFromApi(data: any): SkillAssessment { export function convertSkillAssessmentFromApi(data: any): SkillAssessment {
@ -1643,7 +1798,7 @@ export function convertSkillAssessmentFromApi(data: any): SkillAssessment {
}; };
} }
/** /**
* Convert UserActivity from API response, parsing date fields * Convert UserActivity from API response
* Date fields: timestamp * Date fields: timestamp
*/ */
export function convertUserActivityFromApi(data: any): UserActivity { export function convertUserActivityFromApi(data: any): UserActivity {
@ -1656,7 +1811,7 @@ export function convertUserActivityFromApi(data: any): UserActivity {
}; };
} }
/** /**
* Convert WorkExperience from API response, parsing date fields * Convert WorkExperience from API response
* Date fields: startDate, endDate * Date fields: startDate, endDate
*/ */
export function convertWorkExperienceFromApi(data: any): WorkExperience { export function convertWorkExperienceFromApi(data: any): WorkExperience {
@ -1687,6 +1842,8 @@ export function convertFromApi<T>(data: any, modelType: string): T {
return convertApplicationDecisionFromApi(data) as T; return convertApplicationDecisionFromApi(data) as T;
case 'Attachment': case 'Attachment':
return convertAttachmentFromApi(data) as T; return convertAttachmentFromApi(data) as T;
case 'AuthResponse':
return convertAuthResponseFromApi(data) as T;
case 'Authentication': case 'Authentication':
return convertAuthenticationFromApi(data) as T; return convertAuthenticationFromApi(data) as T;
case 'BaseUser': case 'BaseUser':
@ -1697,6 +1854,10 @@ export function convertFromApi<T>(data: any, modelType: string): T {
return convertCandidateFromApi(data) as T; return convertCandidateFromApi(data) as T;
case 'CandidateAI': case 'CandidateAI':
return convertCandidateAIFromApi(data) as T; return convertCandidateAIFromApi(data) as T;
case 'CandidateListResponse':
return convertCandidateListResponseFromApi(data) as T;
case 'CandidateResponse':
return convertCandidateResponseFromApi(data) as T;
case 'Certification': case 'Certification':
return convertCertificationFromApi(data) as T; return convertCertificationFromApi(data) as T;
case 'ChatMessage': case 'ChatMessage':
@ -1721,6 +1882,8 @@ export function convertFromApi<T>(data: any, modelType: string): T {
return convertDataSourceConfigurationFromApi(data) as T; return convertDataSourceConfigurationFromApi(data) as T;
case 'Document': case 'Document':
return convertDocumentFromApi(data) as T; return convertDocumentFromApi(data) as T;
case 'DocumentListResponse':
return convertDocumentListResponseFromApi(data) as T;
case 'DocumentMessage': case 'DocumentMessage':
return convertDocumentMessageFromApi(data) as T; return convertDocumentMessageFromApi(data) as T;
case 'EditHistory': case 'EditHistory':
@ -1729,8 +1892,12 @@ export function convertFromApi<T>(data: any, modelType: string): T {
return convertEducationFromApi(data) as T; return convertEducationFromApi(data) as T;
case 'Employer': case 'Employer':
return convertEmployerFromApi(data) as T; return convertEmployerFromApi(data) as T;
case 'EmployerResponse':
return convertEmployerResponseFromApi(data) as T;
case 'Guest': case 'Guest':
return convertGuestFromApi(data) as T; return convertGuestFromApi(data) as T;
case 'GuestSessionResponse':
return convertGuestSessionResponseFromApi(data) as T;
case 'InterviewFeedback': case 'InterviewFeedback':
return convertInterviewFeedbackFromApi(data) as T; return convertInterviewFeedbackFromApi(data) as T;
case 'InterviewSchedule': case 'InterviewSchedule':
@ -1741,8 +1908,12 @@ export function convertFromApi<T>(data: any, modelType: string): T {
return convertJobApplicationFromApi(data) as T; return convertJobApplicationFromApi(data) as T;
case 'JobFull': case 'JobFull':
return convertJobFullFromApi(data) as T; return convertJobFullFromApi(data) as T;
case 'JobListResponse':
return convertJobListResponseFromApi(data) as T;
case 'JobRequirementsMessage': case 'JobRequirementsMessage':
return convertJobRequirementsMessageFromApi(data) as T; return convertJobRequirementsMessageFromApi(data) as T;
case 'JobResponse':
return convertJobResponseFromApi(data) as T;
case 'MessageReaction': case 'MessageReaction':
return convertMessageReactionFromApi(data) as T; return convertMessageReactionFromApi(data) as T;
case 'RAGConfiguration': case 'RAGConfiguration':

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

@ -2,14 +2,15 @@
""" """
Enhanced Type Generator - Generate TypeScript types from Pydantic models Enhanced Type Generator - Generate TypeScript types from Pydantic models
Now with command line parameters, pre-test validation, TypeScript compilation, Now with command line parameters, pre-test validation, TypeScript compilation,
automatic date field conversion functions, and proper enum default handling automatic date field conversion functions, proper enum default handling,
and NESTED MODEL CONVERSION SUPPORT
""" """
import sys import sys
import os import os
import argparse import argparse
import subprocess import subprocess
from typing import Any, Dict, List, Optional, Union, get_origin, get_args from typing import Any, Dict, List, Optional, Union, get_origin, get_args, Set
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
@ -64,7 +65,7 @@ current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, current_dir) sys.path.insert(0, current_dir)
try: try:
from pydantic import BaseModel from pydantic import BaseModel # type: ignore
except ImportError as e: except ImportError as e:
print(f"Error importing pydantic: {e}") print(f"Error importing pydantic: {e}")
print("Make sure pydantic is installed: pip install pydantic") print("Make sure pydantic is installed: pip install pydantic")
@ -138,6 +139,61 @@ def is_date_type(python_type: Any) -> bool:
return False return False
def is_pydantic_model_type(python_type: Any) -> bool:
"""Check if a Python type is a Pydantic model"""
# Unwrap any annotations first
python_type = unwrap_annotated_type(python_type)
# Handle Union types (like Optional[SomeModel])
origin = get_origin(python_type)
if origin is Union:
args = get_args(python_type)
# Check if any of the union args is a Pydantic model (excluding None)
return any(is_pydantic_model_type(arg) for arg in args if arg is not type(None))
# Handle List types (like List[SomeModel])
if origin is list or origin is List:
args = get_args(python_type)
if args:
return is_pydantic_model_type(args[0])
# Check if it's a Pydantic model
try:
if isinstance(python_type, type) and issubclass(python_type, BaseModel):
return python_type != BaseModel
except:
pass
return False
def get_model_name_from_type(python_type: Any) -> Optional[str]:
"""Extract the model name from a type"""
# Unwrap any annotations first
python_type = unwrap_annotated_type(python_type)
# Handle Union types
origin = get_origin(python_type)
if origin is Union:
args = get_args(python_type)
# Find the first Pydantic model in the union
for arg in args:
if arg is not type(None):
model_name = get_model_name_from_type(arg)
if model_name:
return model_name
# Handle List types
if origin is list or origin is List:
args = get_args(python_type)
if args:
return get_model_name_from_type(args[0])
# Direct model check
if isinstance(python_type, type) and issubclass(python_type, BaseModel):
return python_type.__name__
return None
def get_default_enum_value(field_info: Any, debug: bool = False) -> Optional[Any]: def get_default_enum_value(field_info: Any, debug: bool = False) -> Optional[Any]:
"""Extract the specific enum value from a field's default, if it exists""" """Extract the specific enum value from a field's default, if it exists"""
if not hasattr(field_info, 'default'): if not hasattr(field_info, 'default'):
@ -416,11 +472,12 @@ def is_field_optional(field_info: Any, field_type: Any, debug: bool = False) ->
print(f" └─ RESULT: Required (fallback - no Optional type, no default)") print(f" └─ RESULT: Required (fallback - no Optional type, no default)")
return False return False
def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]: def process_pydantic_model(model_class, all_models: Set[str], debug: bool = False) -> Dict[str, Any]:
"""Process a Pydantic model and return TypeScript interface definition""" """Process a Pydantic model and return TypeScript interface definition"""
interface_name = model_class.__name__ interface_name = model_class.__name__
properties = [] properties = []
date_fields = [] # Track date fields for conversion functions date_fields = [] # Track date fields for conversion functions
model_fields = [] # Track fields that are Pydantic models
if debug: if debug:
print(f" 🔍 Processing model: {interface_name}") print(f" 🔍 Processing model: {interface_name}")
@ -462,6 +519,21 @@ def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]:
elif debug and ('date' in str(field_type).lower() or 'time' in str(field_type).lower()): elif debug and ('date' in str(field_type).lower() or 'time' in str(field_type).lower()):
print(f" ⚠️ Field {ts_name} contains 'date'/'time' but not detected as date type: {field_type}") print(f" ⚠️ Field {ts_name} contains 'date'/'time' but not detected as date type: {field_type}")
# Check if this is a Pydantic model field
if is_pydantic_model_type(field_type):
model_name = get_model_name_from_type(field_type)
if model_name and model_name in all_models:
is_optional = is_field_optional(field_info, field_type, debug)
is_list = get_origin(unwrap_annotated_type(field_type)) in (list, List)
model_fields.append({
'name': ts_name,
'model': model_name,
'optional': is_optional,
'is_list': is_list
})
if debug:
print(f" 🔗 Model field detected: {ts_name} -> {model_name} (optional: {is_optional}, list: {is_list})")
# Pass field_info to the type converter for default enum handling # Pass field_info to the type converter for default enum handling
ts_type = python_type_to_typescript(field_type, field_info, debug) ts_type = python_type_to_typescript(field_type, field_info, debug)
@ -514,6 +586,21 @@ def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]:
elif debug and ('date' in str(field_type).lower() or 'time' in str(field_type).lower()): elif debug and ('date' in str(field_type).lower() or 'time' in str(field_type).lower()):
print(f" ⚠️ Field {ts_name} contains 'date'/'time' but not detected as date type: {field_type}") print(f" ⚠️ Field {ts_name} contains 'date'/'time' but not detected as date type: {field_type}")
# Check if this is a Pydantic model field
if is_pydantic_model_type(field_type):
model_name = get_model_name_from_type(field_type)
if model_name and model_name in all_models:
is_optional = is_field_optional(field_info, field_type)
is_list = get_origin(unwrap_annotated_type(field_type)) in (list, List)
model_fields.append({
'name': ts_name,
'model': model_name,
'optional': is_optional,
'is_list': is_list
})
if debug:
print(f" 🔗 Model field detected: {ts_name} -> {model_name} (optional: {is_optional}, list: {is_list})")
# Pass field_info to the type converter for default enum handling # Pass field_info to the type converter for default enum handling
ts_type = python_type_to_typescript(field_type, field_info, debug) ts_type = python_type_to_typescript(field_type, field_info, debug)
@ -535,7 +622,8 @@ def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]:
return { return {
'name': interface_name, 'name': interface_name,
'properties': properties, 'properties': properties,
'date_fields': date_fields 'date_fields': date_fields,
'model_fields': model_fields
} }
def process_enum(enum_class) -> Dict[str, Any]: def process_enum(enum_class) -> Dict[str, Any]:
@ -549,38 +637,87 @@ def process_enum(enum_class) -> Dict[str, Any]:
'values': " | ".join(values) 'values': " | ".join(values)
} }
def build_conversion_dependency_graph(interfaces: List[Dict[str, Any]]) -> Dict[str, Set[str]]:
"""Build a graph of which models depend on which other models for conversion"""
dependencies = {}
# First pass: identify which models have conversions
models_with_conversions = set()
for interface in interfaces:
if interface.get('date_fields') or interface.get('model_fields'):
models_with_conversions.add(interface['name'])
# Second pass: build dependency graph
for interface in interfaces:
interface_name = interface['name']
deps = set()
# Add dependencies for nested models
for model_field in interface.get('model_fields', []):
model_name = model_field['model']
if model_name in models_with_conversions:
deps.add(model_name)
if deps or interface.get('date_fields'):
dependencies[interface_name] = deps
return dependencies
def generate_conversion_functions(interfaces: List[Dict[str, Any]]) -> str: def generate_conversion_functions(interfaces: List[Dict[str, Any]]) -> str:
"""Generate TypeScript conversion functions for models with date fields""" """Generate TypeScript conversion functions for models with date fields or nested models"""
# Build dependency graph
dependencies = build_conversion_dependency_graph(interfaces)
if not dependencies:
return ""
conversion_functions = [] conversion_functions = []
for interface in interfaces: for interface in interfaces:
interface_name = interface['name'] interface_name = interface['name']
date_fields = interface.get('date_fields', []) date_fields = interface.get('date_fields', [])
model_fields = interface.get('model_fields', [])
if not date_fields: # Skip if no conversion needed
continue # Skip interfaces without date fields if not date_fields and not model_fields:
continue
# Check if any model fields need conversion
model_fields_needing_conversion = [
mf for mf in model_fields
if mf['model'] in dependencies
]
if not date_fields and not model_fields_needing_conversion:
continue
function_name = f"convert{interface_name}FromApi" function_name = f"convert{interface_name}FromApi"
# Generate function # Generate function
func_lines = [ func_lines = [
f"/**", f"/**",
f" * Convert {interface_name} from API response, parsing date fields", f" * Convert {interface_name} from API response",
f" * Date fields: {', '.join([f['name'] for f in date_fields])}", ]
if date_fields:
func_lines.append(f" * Date fields: {', '.join([f['name'] for f in date_fields])}")
if model_fields_needing_conversion:
func_lines.append(f" * Nested models: {', '.join([f'{mf['name']} ({mf['model']})' for mf in model_fields_needing_conversion])}")
func_lines.extend([
f" */", f" */",
f"export function {function_name}(data: any): {interface_name} {{", f"export function {function_name}(data: any): {interface_name} {{",
f" if (!data) return data;", f" if (!data) return data;",
f" ", f" ",
f" return {{", f" return {{",
f" ...data," f" ...data,"
] ])
# Add date field conversions with validation # Add date field conversions
for date_field in date_fields: for date_field in date_fields:
field_name = date_field['name'] field_name = date_field['name']
is_optional = date_field['optional'] is_optional = date_field['optional']
# Add a comment for clarity
func_lines.append(f" // Convert {field_name} from ISO string to Date") func_lines.append(f" // Convert {field_name} from ISO string to Date")
if is_optional: if is_optional:
@ -588,6 +725,26 @@ def generate_conversion_functions(interfaces: List[Dict[str, Any]]) -> str:
else: else:
func_lines.append(f" {field_name}: new Date(data.{field_name}),") func_lines.append(f" {field_name}: new Date(data.{field_name}),")
# Add nested model conversions
for model_field in model_fields_needing_conversion:
field_name = model_field['name']
model_name = model_field['model']
is_optional = model_field['optional']
is_list = model_field['is_list']
func_lines.append(f" // Convert nested {model_name} model")
if is_list:
if is_optional:
func_lines.append(f" {field_name}: data.{field_name} ? data.{field_name}.map((item: any) => convert{model_name}FromApi(item)) : undefined,")
else:
func_lines.append(f" {field_name}: data.{field_name}.map((item: any) => convert{model_name}FromApi(item)),")
else:
if is_optional:
func_lines.append(f" {field_name}: data.{field_name} ? convert{model_name}FromApi(data.{field_name}) : undefined,")
else:
func_lines.append(f" {field_name}: convert{model_name}FromApi(data.{field_name}),")
func_lines.extend([ func_lines.extend([
f" }};", f" }};",
f"}}" f"}}"
@ -601,11 +758,11 @@ def generate_conversion_functions(interfaces: List[Dict[str, Any]]) -> str:
# Generate the conversion functions section # Generate the conversion functions section
result = [ result = [
"// ============================", "// ============================",
"// Date Conversion Functions", "// Date and Nested Model Conversion Functions",
"// ============================", "// ============================",
"", "",
"// These functions convert API responses to properly typed objects", "// These functions convert API responses to properly typed objects",
"// with Date objects instead of ISO date strings", "// with Date objects instead of ISO date strings and nested model conversions",
"", "",
] ]
@ -613,9 +770,9 @@ def generate_conversion_functions(interfaces: List[Dict[str, Any]]) -> str:
result.append("") result.append("")
# Generate a generic converter function # Generate a generic converter function
models_with_dates = [interface['name'] for interface in interfaces if interface.get('date_fields')] models_with_conversions = list(dependencies.keys())
if models_with_dates: if models_with_conversions:
result.extend([ result.extend([
"/**", "/**",
" * Generic converter that automatically selects the right conversion function", " * Generic converter that automatically selects the right conversion function",
@ -627,7 +784,7 @@ def generate_conversion_functions(interfaces: List[Dict[str, Any]]) -> str:
" switch (modelType) {" " switch (modelType) {"
]) ])
for model_name in models_with_dates: for model_name in sorted(models_with_conversions):
result.append(f" case '{model_name}':") result.append(f" case '{model_name}':")
result.append(f" return convert{model_name}FromApi(data) as T;") result.append(f" return convert{model_name}FromApi(data) as T;")
@ -671,8 +828,17 @@ def generate_typescript_interfaces(source_file: str, debug: bool = False):
interfaces = [] interfaces = []
enums = [] enums = []
all_models = set()
# Scan the models module # First pass: collect all model names
for name in dir(models_module):
obj = getattr(models_module, name)
if (isinstance(obj, type) and
issubclass(obj, BaseModel) and
obj != BaseModel):
all_models.add(name)
# Second pass: process models with knowledge of all models
for name in dir(models_module): for name in dir(models_module):
obj = getattr(models_module, name) obj = getattr(models_module, name)
@ -686,10 +852,17 @@ def generate_typescript_interfaces(source_file: str, debug: bool = False):
issubclass(obj, BaseModel) and issubclass(obj, BaseModel) and
obj != BaseModel): obj != BaseModel):
interface = process_pydantic_model(obj, debug) interface = process_pydantic_model(obj, all_models, debug)
interfaces.append(interface) interfaces.append(interface)
date_count = len(interface.get('date_fields', [])) date_count = len(interface.get('date_fields', []))
print(f" ✅ Found Pydantic model: {name}" + (f" ({date_count} date fields)" if date_count > 0 else "")) nested_count = len(interface.get('model_fields', []))
status_parts = []
if date_count > 0:
status_parts.append(f"{date_count} date fields")
if nested_count > 0:
status_parts.append(f"{nested_count} nested models")
status = f" ({', '.join(status_parts)})" if status_parts else ""
print(f" ✅ Found Pydantic model: {name}{status}")
# Check if it's an Enum # Check if it's an Enum
elif (isinstance(obj, type) and elif (isinstance(obj, type) and
@ -707,8 +880,10 @@ def generate_typescript_interfaces(source_file: str, debug: bool = False):
continue continue
total_date_fields = sum(len(interface.get('date_fields', [])) for interface in interfaces) total_date_fields = sum(len(interface.get('date_fields', [])) for interface in interfaces)
total_nested_models = sum(len(interface.get('model_fields', [])) for interface in interfaces)
print(f"\n📊 Found {len(interfaces)} interfaces and {len(enums)} enums") print(f"\n📊 Found {len(interfaces)} interfaces and {len(enums)} enums")
print(f"🗓️ Found {total_date_fields} date fields across all models") print(f"🗓️ Found {total_date_fields} date fields across all models")
print(f"🔗 Found {total_nested_models} nested model fields requiring conversion")
# Generate TypeScript content # Generate TypeScript content
ts_content = f"""// Generated TypeScript types from Pydantic models ts_content = f"""// Generated TypeScript types from Pydantic models
@ -781,7 +956,7 @@ def compile_typescript(ts_file: str) -> bool:
def main(): def main():
"""Main function with command line argument parsing""" """Main function with command line argument parsing"""
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Generate TypeScript types from Pydantic models with date conversion functions and proper enum handling', description='Generate TypeScript types from Pydantic models with nested model conversion support',
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=""" epilog="""
Examples: Examples:
@ -792,12 +967,19 @@ Examples:
python generate_types.py --debug # Enable debug output python generate_types.py --debug # Enable debug output
python generate_types.py --source models.py --output types.ts --skip-test --skip-compile --debug python generate_types.py --source models.py --output types.ts --skip-test --skip-compile --debug
Generated conversion functions can be used like: Generated conversion functions now support nested models:
const candidate = convertCandidateFromApi(apiResponse); // If JobRequirementsMessage has a 'job' field of type Job
const jobs = convertArrayFromApi<Job>(apiResponse, 'Job'); const message = convertJobRequirementsMessageFromApi(apiResponse);
// The nested job field will also be converted automatically
Enum types are now properly handled: // Arrays of models are also supported
status: ApiStatusType = ApiStatusType.DONE -> status: ApiStatusType (not locked to "done") const messages = convertArrayFromApi<JobRequirementsMessage>(apiResponse, 'JobRequirementsMessage');
The conversion functions handle:
- Date field conversion (ISO strings to Date objects)
- Nested model conversion (recursive conversion of Pydantic model fields)
- Arrays of models
- Optional fields
""" """
) )
@ -834,12 +1016,12 @@ Enum types are now properly handled:
parser.add_argument( parser.add_argument(
'--version', '-v', '--version', '-v',
action='version', action='version',
version='TypeScript Generator 3.2 (Fixed Enum Default Handling)' version='TypeScript Generator 4.0 (With Nested Model Conversion Support)'
) )
args = parser.parse_args() args = parser.parse_args()
print("🚀 Enhanced TypeScript Type Generator with Fixed Enum Handling") print("🚀 Enhanced TypeScript Type Generator with Nested Model Conversion")
print("=" * 60) print("=" * 60)
print(f"📁 Source file: {args.source}") print(f"📁 Source file: {args.source}")
print(f"📁 Output file: {args.output}") print(f"📁 Output file: {args.output}")
@ -885,29 +1067,24 @@ Enum types are now properly handled:
# Count conversion functions and provide detailed feedback # Count conversion functions and provide detailed feedback
conversion_count = ts_content.count('export function convert') - ts_content.count('convertFromApi') - ts_content.count('convertArrayFromApi') conversion_count = ts_content.count('export function convert') - ts_content.count('convertFromApi') - ts_content.count('convertArrayFromApi')
enum_type_count = ts_content.count('export type') enum_type_count = ts_content.count('export type')
nested_conversion_count = ts_content.count('// Convert nested')
if conversion_count > 0: if conversion_count > 0:
print(f"🗓️ Generated {conversion_count} date conversion functions") print(f"🗓️ Generated {conversion_count} conversion functions")
if nested_conversion_count > 0:
print(f"🔗 Including {nested_conversion_count} nested model conversions")
if enum_type_count > 0: if enum_type_count > 0:
print(f"🎯 Generated {enum_type_count} enum types (properly allowing all values)") print(f"🎯 Generated {enum_type_count} enum types")
if args.debug: if args.debug:
# Show which models have date conversion # Show which models have conversions
models_with_dates = [] models_with_conversions = []
for line in ts_content.split('\n'): for line in ts_content.split('\n'):
if line.startswith('export function convert') and 'FromApi' in line and 'convertFromApi' not in line: if line.startswith('export function convert') and 'FromApi' in line and 'convertFromApi' not in line:
model_name = line.split('convert')[1].split('FromApi')[0] model_name = line.split('convert')[1].split('FromApi')[0]
models_with_dates.append(model_name) models_with_conversions.append(model_name)
if models_with_dates: if models_with_conversions:
print(f" Models with date conversion: {', '.join(models_with_dates)}") print(f" Models with conversions: {', '.join(models_with_conversions)}")
# Provide troubleshooting info if debug mode
if args.debug:
print(f"\n🐛 Debug mode was enabled. If you see incorrect type conversions:")
print(f" 1. Check the debug output above for enum default handling")
print(f" 2. Look for '📅 Date type check' lines for date handling")
print(f" 3. Look for '⚠️' warnings about fallback types")
print(f" 4. Verify your Pydantic model field types and defaults are correct")
# Step 5: Compile TypeScript (unless skipped) # Step 5: Compile TypeScript (unless skipped)
if not args.skip_compile: if not args.skip_compile:
@ -924,19 +1101,24 @@ Enum types are now properly handled:
print(f"✅ Generated {args.output} from {args.source}") print(f"✅ Generated {args.output} from {args.source}")
print(f"✅ File size: {file_size} characters") print(f"✅ File size: {file_size} characters")
if conversion_count > 0: if conversion_count > 0:
print(f"✅ Date conversion functions: {conversion_count}") print(f"✅ Conversion functions: {conversion_count}")
if nested_conversion_count > 0:
print(f"✅ Nested model conversions: {nested_conversion_count}")
if enum_type_count > 0: if enum_type_count > 0:
print(f"✅ Enum types (with full value range): {enum_type_count}") print(f"✅ Enum types: {enum_type_count}")
if not args.skip_test: if not args.skip_test:
print("✅ Model validation passed") print("✅ Model validation passed")
if not args.skip_compile: if not args.skip_compile:
print("✅ TypeScript syntax validated") print("✅ TypeScript syntax validated")
print(f"\n💡 Usage in your TypeScript project:") print(f"\n💡 Usage in your TypeScript project:")
print(f" import {{ Candidate, Employer, Job, convertCandidateFromApi }} from './{Path(args.output).stem}';") print(f" import {{ Candidate, Job, convertJobRequirementsMessageFromApi }} from './{Path(args.output).stem}';")
if conversion_count > 0: if conversion_count > 0:
print(f" const candidate = convertCandidateFromApi(apiResponse);") print(f"\n // Example with nested model conversion:")
print(f" const jobs = convertArrayFromApi<Job>(apiResponse, 'Job');") print(f" const message = convertJobRequirementsMessageFromApi(apiResponse);")
print(f" // The nested 'job' field is automatically converted too!")
print(f"\n // For arrays:")
print(f" const messages = convertArrayFromApi<JobRequirementsMessage>(apiResponse, 'JobRequirementsMessage');")
return True return True

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,9 +3439,10 @@ 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):
yield message async for message in create_job_from_content(database=database, current_user=current_user, content=file_content):
return yield message
return
try: try:
async def to_json(method): async def to_json(method):

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