Compare commits

..

No commits in common. "4f4187eba49e83481386a2cadaf3eea0e08e7f36" and "1531a05de00b90351c067f0c378fe2c342b1f5a7" have entirely different histories.

11 changed files with 240 additions and 632 deletions

View File

@ -1,19 +1,27 @@
import React, { useState, useRef, JSX } from 'react';
import React, { useState, useEffect, useRef, JSX } from 'react';
import {
Box,
Button,
Typography,
Paper,
TextField,
Grid,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
IconButton,
useTheme,
useMediaQuery,
Chip,
Divider,
Card,
CardContent,
CardHeader,
LinearProgress,
Stack,
Paper,
Alert
} from '@mui/material';
import {
SyncAlt,
@ -28,22 +36,21 @@ import {
CloudUpload,
Description,
Business,
LocationOn,
Work,
CheckCircle,
Star
} from '@mui/icons-material';
import { styled } from '@mui/material/styles';
import DescriptionIcon from '@mui/icons-material/Description';
import FileUploadIcon from '@mui/icons-material/FileUpload';
import { useAuth } from 'hooks/AuthContext';
import { useAppState, useSelectedJob } from 'hooks/GlobalContext';
import { useAppState, useSelectedCandidate, useSelectedJob } from 'hooks/GlobalContext';
import { BackstoryElementProps } from './BackstoryTab';
import { LoginRequired } from 'components/ui/LoginRequired';
import * as Types from 'types/types';
import { StyledMarkdown } from './StyledMarkdown';
import { JobInfo } from './ui/JobInfo';
import { Scrollable } from './Scrollable';
const VisuallyHiddenInput = styled('input')({
clip: 'rect(0 0 0 0)',
@ -107,27 +114,35 @@ const getIcon = (type: Types.ApiActivityType) => {
}
};
interface JobCreatorProps extends BackstoryElementProps {
interface JobCreator extends BackstoryElementProps {
onSave?: (job: Types.Job) => void;
}
const JobCreator = (props: JobCreatorProps) => {
const JobCreator = (props: JobCreator) => {
const { user, apiClient } = useAuth();
const { onSave } = props;
const { onSave } = props;
const { selectedCandidate } = useSelectedCandidate();
const { selectedJob, setSelectedJob } = useSelectedJob();
const { setSnack } = useAppState();
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 [jobRequirements, setJobRequirements] = useState<Types.JobRequirements | null>(null);
const [jobTitle, setJobTitle] = useState<string>('');
const [company, setCompany] = useState<string>('');
const [summary, setSummary] = useState<string>('');
const [job, setJob] = useState<Types.Job | null>(null);
const [jobLocation, setJobLocation] = useState<string>('');
const [jobId, setJobId] = useState<string>('');
const [jobStatus, setJobStatus] = useState<string>('');
const [jobStatusIcon, setJobStatusIcon] = useState<JSX.Element>(<></>);
const [isProcessing, setIsProcessing] = useState<boolean>(false);
useEffect(() => {
}, [jobTitle, jobDescription, company]);
const fileInputRef = useRef<HTMLInputElement>(null);
@ -143,10 +158,8 @@ const JobCreator = (props: JobCreatorProps) => {
setJobStatusIcon(getIcon(status.activity));
setJobStatus(status.content);
},
onMessage: (jobMessage: Types.JobRequirementsMessage) => {
const job: Types.Job = jobMessage.job
onMessage: (job: Types.Job) => {
console.log('onMessage - job', job);
setJob(job);
setCompany(job.company || '');
setJobDescription(job.description);
setSummary(job.summary || '');
@ -320,9 +333,8 @@ const JobCreator = (props: JobCreatorProps) => {
updatedAt: new Date(),
};
setIsProcessing(true);
const jobMessage = await apiClient.createJob(newJob);
const job = await apiClient.createJob(newJob);
setIsProcessing(false);
const job: Types.Job = jobMessage.job;
onSave ? onSave(job) : setSelectedJob(job);
};
@ -345,6 +357,10 @@ const JobCreator = (props: JobCreatorProps) => {
};
const renderJobCreation = () => {
if (!user) {
return <Box>You must be logged in</Box>;
}
return (
<Box sx={{
mx: 'auto', p: { xs: 2, sm: 3 },
@ -484,6 +500,21 @@ const JobCreator = (props: JobCreatorProps) => {
}}
/>
</Grid> */}
<Grid size={{ xs: 12, md: 6 }}>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-end', height: '100%' }}>
<Button
variant="contained"
onClick={handleSave}
disabled={!jobTitle || !company || !jobDescription || isProcessing}
fullWidth={isMobile}
size="large"
startIcon={<CheckCircle />}
>
Save Job
</Button>
</Box>
</Grid>
</Grid>
</CardContent>
</Card>
@ -517,27 +548,7 @@ const JobCreator = (props: JobCreatorProps) => {
width: "100%",
display: "flex", flexDirection: "column"
}}>
{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>
}
{selectedJob === null && renderJobCreation()}
</Box>
);
};

View File

@ -8,7 +8,7 @@ import {
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import { useMediaQuery } from '@mui/material';
import { Job, JobFull } from 'types/types';
import { JobFull } from 'types/types';
import { CopyBubble } from "components/CopyBubble";
import { rest } from 'lodash';
import { AIBanner } from 'components/ui/AIBanner';
@ -17,7 +17,7 @@ import { DeleteConfirmation } from '../DeleteConfirmation';
import { Build, CheckCircle, Description, Psychology, Star, Work } from '@mui/icons-material';
interface JobInfoProps {
job: Job | JobFull;
job: JobFull;
sx?: SxProps;
action?: string;
elevation?: number;
@ -153,7 +153,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
>
<CardContent sx={{ display: "flex", flexGrow: 1, p: 3, height: '100%', flexDirection: 'column', alignItems: 'stretch', position: "relative" }}>
{variant !== "small" && <>
{'location' in job &&
{job.location &&
<Typography variant="body2" sx={{ mb: 1 }}>
<strong>Location:</strong> {job.location.city}, {job.location.state || job.location.country}
</Typography>

View File

@ -83,7 +83,7 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
Session ID: {guest.sessionId}
</Typography>
<Typography variant="body2" color="text.secondary">
Created: {guest.createdAt?.toLocaleString()}
Created: {guest.createdAt.toLocaleString()}
</Typography>
</CardContent>
</Card>

View File

@ -39,7 +39,6 @@ const TOKEN_STORAGE = {
PENDING_VERIFICATION_EMAIL: 'pendingVerificationEmail'
} as const;
// ============================
// Streaming Types and Interfaces
// ============================
@ -648,12 +647,12 @@ class ApiClient {
// Job Methods with Date Conversion
// ============================
createJobFromDescription(job_description: string, streamingOptions?: StreamingOptions<Types.JobRequirementsMessage>): StreamingResponse<Types.JobRequirementsMessage> {
createJobFromDescription(job_description: string, streamingOptions?: StreamingOptions<Types.Job>): StreamingResponse<Types.Job> {
const body = JSON.stringify(job_description);
return this.streamify<Types.JobRequirementsMessage>('/jobs/from-content', body, streamingOptions, "JobRequirementsMessage");
return this.streamify<Types.Job>('/jobs/from-content', body, streamingOptions);
}
async createJob(job: Omit<Types.Job, 'id' | 'datePosted' | 'views' | 'applicationCount'>): Promise<Types.JobRequirementsMessage> {
async createJob(job: Omit<Types.Job, 'id' | 'datePosted' | 'views' | 'applicationCount'>): Promise<Types.Job> {
const body = JSON.stringify(formatApiRequest(job));
const response = await fetch(`${this.baseUrl}/jobs`, {
method: 'POST',
@ -661,7 +660,7 @@ class ApiClient {
body: body
});
return this.handleApiResponseWithConversion<Types.JobRequirementsMessage>(response, 'JobRequirementsMessage');
return this.handleApiResponseWithConversion<Types.Job>(response, 'Job');
}
async getJob(id: string): Promise<Types.Job> {
@ -885,10 +884,10 @@ class ApiClient {
'Authorization': this.defaultHeaders['Authorization']
}
};
return this.streamify<Types.DocumentMessage>('/candidates/documents/upload', formData, streamingOptions, "DocumentMessage");
return this.streamify<Types.DocumentMessage>('/candidates/documents/upload', formData, streamingOptions);
}
createJobFromFile(file: File, streamingOptions?: StreamingOptions<Types.JobRequirementsMessage>): StreamingResponse<Types.JobRequirementsMessage> {
createJobFromFile(file: File, streamingOptions?: StreamingOptions<Types.Job>): StreamingResponse<Types.Job> {
const formData = new FormData()
formData.append('file', file);
formData.append('filename', file.name);
@ -899,7 +898,7 @@ class ApiClient {
'Authorization': this.defaultHeaders['Authorization']
}
};
return this.streamify<Types.JobRequirementsMessage>('/jobs/upload', formData, streamingOptions, "JobRequirementsMessage");
return this.streamify<Types.Job>('/jobs/upload', formData, streamingOptions);
}
getJobRequirements(jobId: string, streamingOptions?: StreamingOptions<Types.DocumentMessage>): StreamingResponse<Types.DocumentMessage> {
@ -907,7 +906,7 @@ class ApiClient {
...streamingOptions,
headers: this.defaultHeaders,
};
return this.streamify<Types.DocumentMessage>(`/jobs/requirements/${jobId}`, null, streamingOptions, "DocumentMessage");
return this.streamify<Types.DocumentMessage>(`/jobs/requirements/${jobId}`, null, streamingOptions);
}
generateResume(candidateId: string, skills: Types.SkillAssessment[], streamingOptions?: StreamingOptions<Types.ChatMessageResume>): StreamingResponse<Types.ChatMessageResume> {
@ -916,7 +915,7 @@ class ApiClient {
...streamingOptions,
headers: this.defaultHeaders,
};
return this.streamify<Types.ChatMessageResume>(`/candidates/${candidateId}/generate-resume`, body, streamingOptions, "ChatMessageResume");
return this.streamify<Types.ChatMessageResume>(`/candidates/${candidateId}/generate-resume`, body, streamingOptions);
}
candidateMatchForRequirement(candidate_id: string, requirement: string,
streamingOptions?: StreamingOptions<Types.ChatMessageSkillAssessment>)
@ -926,7 +925,7 @@ class ApiClient {
...streamingOptions,
headers: this.defaultHeaders,
};
return this.streamify<Types.ChatMessageSkillAssessment>(`/candidates/${candidate_id}/skill-match`, body, streamingOptions, "ChatMessageSkillAssessment");
return this.streamify<Types.ChatMessageSkillAssessment>(`/candidates/${candidate_id}/skill-match`, body, streamingOptions);
}
async updateCandidateDocument(document: Types.Document) : Promise<Types.Document> {
@ -1227,7 +1226,7 @@ class ApiClient {
* @param options callbacks, headers, and method
* @returns
*/
streamify<T = Types.ChatMessage[]>(api: string, data: BodyInit | null, options: StreamingOptions<T> = {}, modelType?: string) : StreamingResponse<T> {
streamify<T = Types.ChatMessage[]>(api: string, data: BodyInit | null, options: StreamingOptions<T> = {}) : StreamingResponse<T> {
const abortController = new AbortController();
const signal = options.signal || abortController.signal;
const headers = options.headers || null;
@ -1309,8 +1308,8 @@ class ApiClient {
break;
case 'done':
const message = (modelType ? convertFromApi<T>(incoming, modelType) : incoming) as T;
finalMessage = message;
const message = Types.convertApiMessageFromApi(incoming) as T;
finalMessage = message as any;
try {
options.onMessage?.(message);
} catch (error) {
@ -1362,7 +1361,7 @@ class ApiClient {
options: StreamingOptions = {}
): StreamingResponse {
const body = JSON.stringify(formatApiRequest(chatMessage));
return this.streamify(`/chat/sessions/${chatMessage.sessionId}/messages/stream`, body, options, "ChatMessage")
return this.streamify(`/chat/sessions/${chatMessage.sessionId}/messages/stream`, body, options)
}
/**
@ -1471,6 +1470,7 @@ class ApiClient {
// ============================
// Error Handling Helper
// ============================
async handleRequest<T>(requestFn: () => Promise<Response>, modelType?: string): Promise<T> {
try {
const response = await requestFn();

View File

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

View File

@ -823,16 +823,5 @@ Content: {content}
raise ValueError("No JSON found in the response")
def extract_markdown_from_text(self, text: str) -> str:
"""Extract Markdown string from text that may contain other content."""
markdown_pattern = r"```(md|markdown)\s*([\s\S]*?)\s*```"
match = re.search(markdown_pattern, text)
if match:
return match.group(2).strip()
raise ValueError("No Markdown found in the response")
# Register the base agent
agent_registry.register(Agent._agent_type, Agent)

View File

@ -511,5 +511,14 @@ Make sure at least one of the candidate's job descriptions take into account the
raise ValueError("No JSON found in the response")
def extract_markdown_from_text(self, text: str) -> str:
"""Extract Markdown string from text that may contain other content."""
markdown_pattern = r"```(md|markdown)\s*([\s\S]*?)\s*```"
match = re.search(markdown_pattern, text)
if match:
return match.group(2).strip()
raise ValueError("No Markdown found in the response")
# Register the base agent
agent_registry.register(GeneratePersona._agent_type, GeneratePersona)

View File

@ -19,7 +19,7 @@ import asyncio
import numpy as np # type: ignore
from .base import Agent, agent_registry, LLMMessage
from models import ApiActivityType, Candidate, ChatMessage, ChatMessageError, ChatMessageMetaData, ApiMessageType, ChatMessageStatus, ChatMessageUser, ChatOptions, ChatSenderType, ApiStatusType, Job, JobRequirements, JobRequirementsMessage, Tunables
from models import ApiActivityType, Candidate, ChatMessage, ChatMessageError, ChatMessageMetaData, ApiMessageType, ChatMessageStatus, ChatMessageUser, ChatOptions, ChatSenderType, ApiStatusType, JobRequirements, JobRequirementsMessage, Tunables
import model_cast
from logger import logger
import defines
@ -107,15 +107,6 @@ class JobRequirementsAgent(Agent):
async def generate(
self, llm: Any, model: str, session_id: str, prompt: str, tunables: Optional[Tunables] = None, temperature=0.7
) -> AsyncGenerator[ChatMessage, None]:
if not self.user:
error_message = ChatMessageError(
session_id=session_id,
content="User is not set for this agent."
)
logger.error(f"⚠️ {error_message.content}")
yield error_message
return
# Stage 1A: Analyze job requirements
status_message = ChatMessageStatus(
session_id=session_id,
@ -175,20 +166,14 @@ class JobRequirementsAgent(Agent):
logger.error(f"⚠️ {status_message.content}")
yield status_message
return
job = Job(
owner_id=self.user.id,
owner_type=self.user.user_type,
company=company,
title=title,
summary=summary,
requirements=requirements,
session_id=session_id,
description=prompt,
)
job_requirements_message = JobRequirementsMessage(
session_id=session_id,
status=ApiStatusType.DONE,
job=job,
requirements=requirements,
company=company,
title=title,
summary=summary,
description=prompt,
)
yield job_requirements_message
logger.info(f"✅ Job requirements analysis completed successfully.")

View File

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

View File

@ -2357,6 +2357,7 @@ async def create_job_from_content(database: RedisDatabase, current_user: Candida
activity=ApiActivityType.SEARCHING
)
yield status_message
await asyncio.sleep(0)
async for message in chat_agent.generate(
llm=llm_manager.get_llm(),
@ -2365,52 +2366,16 @@ async def create_job_from_content(database: RedisDatabase, current_user: Candida
prompt=content
):
pass
if not message or not isinstance(message, JobRequirementsMessage):
error_message = ChatMessageError(
sessionId=MOCK_UUID, # No session ID for document uploads
content="Job extraction did not convert successfully"
content="Failed to process job description file"
)
yield error_message
return
status_message = ChatMessageStatus(
sessionId=MOCK_UUID, # No session ID for document uploads
content=f"Reformatting job description as markdown...",
activity=ApiActivityType.CONVERTING
)
yield status_message
job_requirements : JobRequirementsMessage = message
async for message in chat_agent.llm_one_shot(
llm=llm_manager.get_llm(),
model=defines.model,
session_id=MOCK_UUID,
prompt=content,
system_prompt="""
You are a document editor. Take the provided job description and reformat as legible markdown.
Return only the markdown content, no other text. Make sure all content is included.
"""
):
pass
if not message or not isinstance(message, ChatMessage):
logger.error("❌ Failed to reformat job description to markdown")
error_message = ChatMessageError(
sessionId=MOCK_UUID, # No session ID for document uploads
content="Failed to reformat job description"
)
yield error_message
return
chat_message : ChatMessage = message
markdown = chat_message.content
try:
markdown = chat_agent.extract_markdown_from_text(chat_message.content)
except Exception as e:
pass
job_requirements.job.description = markdown
logger.info(f"✅ Successfully saved job requirements job {job_requirements.id}")
yield job_requirements
logger.info(f"✅ Successfully saved job requirements job {message.id}")
yield message
return
@api_router.post("/candidates/profile/upload")
@ -3307,7 +3272,6 @@ async def create_job_from_description(
logger.info(f"📁 Received file content: size='{len(content)} bytes'")
async for message in create_job_from_content(database=database, current_user=current_user, content=content):
logger.info(f"📄 Yielding job creation message status: {message.status}")
yield message
return
@ -3439,10 +3403,9 @@ async def create_job_from_file(
yield error_message
logger.error(f"❌ Error converting {file.filename} to Markdown: {e}")
return
async for message in create_job_from_content(database=database, current_user=current_user, content=file_content):
yield message
return
async for message in create_job_from_content(database=database, current_user=current_user, content=file_content):
yield message
return
try:
async def to_json(method):

View File

@ -480,8 +480,8 @@ class BaseUser(BaseUserWithType):
full_name: str = Field(..., alias="fullName")
phone: Optional[str] = None
location: Optional[Location] = None
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt")
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="updatedAt")
created_at: datetime = Field(..., alias="createdAt")
updated_at: datetime = Field(..., alias="updatedAt")
last_login: Optional[datetime] = Field(None, alias="lastLogin")
profile_image: Optional[str] = Field(None, alias="profileImage")
status: UserStatus
@ -613,7 +613,7 @@ class Guest(BaseUser):
username: str # Add username for consistency with other user types
converted_to_user_id: Optional[str] = Field(None, alias="convertedToUserId")
ip_address: Optional[str] = Field(None, alias="ipAddress")
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt")
created_at: datetime = Field(..., alias="createdAt")
user_agent: Optional[str] = Field(None, alias="userAgent")
rag_content_size: int = 0
model_config = {
@ -690,8 +690,8 @@ class Job(BaseModel):
company: Optional[str]
description: str
requirements: Optional[JobRequirements]
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt")
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="updatedAt")
created_at: datetime = Field(..., alias="createdAt")
updated_at: datetime = Field(..., alias="updatedAt")
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
@ -700,7 +700,7 @@ class JobFull(Job):
location: Location
salary_range: Optional[SalaryRange] = Field(None, alias="salaryRange")
employment_type: EmploymentType = Field(..., alias="employmentType")
date_posted: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="datePosted")
date_posted: datetime = Field(..., alias="datePosted")
application_deadline: Optional[datetime] = Field(None, alias="applicationDeadline")
is_active: bool = Field(..., alias="isActive")
applicants: Optional[List["JobApplication"]] = None
@ -723,8 +723,8 @@ class InterviewFeedback(BaseModel):
weaknesses: List[str]
recommendation: InterviewRecommendation
comments: str
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt")
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="updatedAt")
created_at: datetime = Field(..., alias="createdAt")
updated_at: datetime = Field(..., alias="updatedAt")
is_visible: bool = Field(..., alias="isVisible")
skill_assessments: Optional[List[SkillAssessment]] = Field(None, alias="skillAssessments")
model_config = {
@ -954,7 +954,11 @@ class ChatMessageRagSearch(ApiMessage):
class JobRequirementsMessage(ApiMessage):
type: ApiMessageType = ApiMessageType.JSON
job: Job = Field(..., alias="job")
title: Optional[str]
summary: Optional[str]
company: Optional[str]
description: str
requirements: Optional[JobRequirements]
class DocumentMessage(ApiMessage):
type: ApiMessageType = ApiMessageType.JSON
@ -1075,8 +1079,8 @@ class RAGConfiguration(BaseModel):
embedding_model: str = Field(..., alias="embeddingModel")
vector_store_type: VectorStoreType = Field(..., alias="vectorStoreType")
retrieval_parameters: RetrievalParameters = Field(..., alias="retrievalParameters")
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt")
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="updatedAt")
created_at: datetime = Field(..., alias="createdAt")
updated_at: datetime = Field(..., alias="updatedAt")
version: int
is_active: bool = Field(..., alias="isActive")
model_config = {
@ -1247,7 +1251,7 @@ class EmployerResponse(BaseModel):
class JobResponse(BaseModel):
success: bool
data: Optional[Job] = None
data: Optional["Job"] = None
error: Optional[ErrorDetail] = None
meta: Optional[Dict[str, Any]] = None