Skill tracking almost working
This commit is contained in:
parent
cb97cabfc3
commit
7586725f11
@ -20,58 +20,101 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|||||||
import ErrorIcon from '@mui/icons-material/Error';
|
import ErrorIcon from '@mui/icons-material/Error';
|
||||||
import PendingIcon from '@mui/icons-material/Pending';
|
import PendingIcon from '@mui/icons-material/Pending';
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
|
import { Candidate, ChatMessage, ChatMessageBase, ChatMessageUser, ChatSession, JobRequirements, SkillMatch } from 'types/types';
|
||||||
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
|
import { BackstoryPageProps } from './BackstoryTab';
|
||||||
|
import { toCamelCase } from 'types/conversion';
|
||||||
|
|
||||||
// Define TypeScript interfaces for our data structures
|
|
||||||
interface Citation {
|
interface Job {
|
||||||
text: string;
|
title: string;
|
||||||
source: string;
|
description: string;
|
||||||
relevance: number; // 0-100 scale
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SkillMatch {
|
interface JobAnalysisProps extends BackstoryPageProps {
|
||||||
requirement: string;
|
job: Job;
|
||||||
status: 'pending' | 'complete' | 'error';
|
candidate: Candidate;
|
||||||
matchScore: number; // 0-100 scale
|
|
||||||
assessment: string;
|
|
||||||
citations: Citation[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface JobAnalysisProps {
|
const defaultMessage: ChatMessageUser = {
|
||||||
jobTitle: string;
|
type: "preparing", status: "done", sender: "user", sessionId: "", timestamp: new Date(), content: ""
|
||||||
candidateName: string;
|
};
|
||||||
// This function would connect to your backend and return updates
|
|
||||||
fetchRequirements: () => Promise<string[]>;
|
|
||||||
// This function would fetch match data for a specific requirement
|
|
||||||
fetchMatchForRequirement: (requirement: string) => Promise<SkillMatch>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const JobMatchAnalysis: React.FC<JobAnalysisProps> = ({
|
const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) => {
|
||||||
jobTitle,
|
const {
|
||||||
candidateName,
|
job,
|
||||||
fetchRequirements,
|
candidate,
|
||||||
fetchMatchForRequirement
|
setSnack,
|
||||||
}) => {
|
} = props
|
||||||
|
const { apiClient } = useAuth();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const [jobRequirements, setJobRequirements] = useState<JobRequirements | null>(null);
|
||||||
const [requirements, setRequirements] = useState<string[]>([]);
|
const [requirements, setRequirements] = useState<string[]>([]);
|
||||||
const [skillMatches, setSkillMatches] = useState<SkillMatch[]>([]);
|
const [skillMatches, setSkillMatches] = useState<SkillMatch[]>([]);
|
||||||
const [loadingRequirements, setLoadingRequirements] = useState<boolean>(true);
|
const [creatingSession, setCreatingSession] = useState<boolean>(false);
|
||||||
|
const [loadingRequirements, setLoadingRequirements] = useState<boolean>(false);
|
||||||
const [expanded, setExpanded] = useState<string | false>(false);
|
const [expanded, setExpanded] = useState<string | false>(false);
|
||||||
const [overallScore, setOverallScore] = useState<number>(0);
|
const [overallScore, setOverallScore] = useState<number>(0);
|
||||||
|
const [requirementsSession, setRequirementsSession] = useState<ChatSession | null>(null);
|
||||||
|
const [statusMessage, setStatusMessage] = useState<ChatMessage | null>(null);
|
||||||
|
|
||||||
// Handle accordion expansion
|
// Handle accordion expansion
|
||||||
const handleAccordionChange = (panel: string) => (event: React.SyntheticEvent, isExpanded: boolean) => {
|
const handleAccordionChange = (panel: string) => (event: React.SyntheticEvent, isExpanded: boolean) => {
|
||||||
setExpanded(isExpanded ? panel : false);
|
setExpanded(isExpanded ? panel : false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (requirementsSession || creatingSession) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createSession = async () => {
|
||||||
|
try {
|
||||||
|
const session: ChatSession = await apiClient.createCandidateChatSession(
|
||||||
|
candidate.username,
|
||||||
|
'job_requirements',
|
||||||
|
`Generate requirements for ${job.title}`
|
||||||
|
);
|
||||||
|
setSnack("Job analysis session started");
|
||||||
|
setRequirementsSession(session);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
setSnack("Unable to create requirements session", "error");
|
||||||
|
}
|
||||||
|
setCreatingSession(false);
|
||||||
|
};
|
||||||
|
setCreatingSession(true);
|
||||||
|
createSession();
|
||||||
|
}, [requirementsSession, apiClient, candidate]);
|
||||||
|
|
||||||
// Fetch initial requirements
|
// Fetch initial requirements
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!job.description || !requirementsSession || loadingRequirements || jobRequirements) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const getRequirements = async () => {
|
const getRequirements = async () => {
|
||||||
|
setLoadingRequirements(true);
|
||||||
try {
|
try {
|
||||||
const fetchedRequirements = await fetchRequirements();
|
const chatMessage: ChatMessageUser = { ...defaultMessage, sessionId: requirementsSession.id || '', content: job.description };
|
||||||
setRequirements(fetchedRequirements);
|
apiClient.sendMessageStream(chatMessage, {
|
||||||
|
onMessage: (msg: ChatMessage) => {
|
||||||
|
console.log(`onMessage: ${msg.type}`, msg);
|
||||||
|
if (msg.type === "response") {
|
||||||
|
const incoming: any = toCamelCase<JobRequirements>(JSON.parse(msg.content || ''));
|
||||||
|
const requirements: string[] = ['technicalSkills', 'experienceRequirements'].flatMap((type) => {
|
||||||
|
return ['required', 'preferred'].flatMap((level) => {
|
||||||
|
return incoming[type][level].map((s: string) => s);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
['softSkills', 'experience', 'education', 'certifications', 'preferredAttributes'].forEach(l => {
|
||||||
|
if (incoming[l]) {
|
||||||
|
incoming[l].forEach((s: string) => requirements.push(s));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Initialize skill matches with pending status
|
// Initialize skill matches with pending status
|
||||||
const initialSkillMatches = fetchedRequirements.map(req => ({
|
const initialSkillMatches = requirements.map(req => ({
|
||||||
requirement: req,
|
requirement: req,
|
||||||
status: 'pending' as const,
|
status: 'pending' as const,
|
||||||
matchScore: 0,
|
matchScore: 0,
|
||||||
@ -79,16 +122,42 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = ({
|
|||||||
citations: []
|
citations: []
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
setRequirements(requirements);
|
||||||
setSkillMatches(initialSkillMatches);
|
setSkillMatches(initialSkillMatches);
|
||||||
|
setStatusMessage(null);
|
||||||
setLoadingRequirements(false);
|
setLoadingRequirements(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error: string | ChatMessageBase) => {
|
||||||
|
console.log("onError:", error);
|
||||||
|
// Type-guard to determine if this is a ChatMessageBase or a string
|
||||||
|
if (typeof error === "object" && error !== null && "content" in error) {
|
||||||
|
setSnack(error.content || 'Error obtaining requirements from job description.', "error");
|
||||||
|
} else {
|
||||||
|
setSnack(error as string, "error");
|
||||||
|
}
|
||||||
|
setLoadingRequirements(false);
|
||||||
|
},
|
||||||
|
onStreaming: (chunk: ChatMessageBase) => {
|
||||||
|
// console.log("onStreaming:", chunk);
|
||||||
|
},
|
||||||
|
onStatusChange: (status: string) => {
|
||||||
|
console.log(`onStatusChange: ${status}`);
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
console.log("onComplete");
|
||||||
|
setStatusMessage(null);
|
||||||
|
setLoadingRequirements(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching requirements:", error);
|
console.error('Failed to send message:', error);
|
||||||
setLoadingRequirements(false);
|
setLoadingRequirements(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
getRequirements();
|
getRequirements();
|
||||||
}, [fetchRequirements]);
|
}, [job, requirementsSession]);
|
||||||
|
|
||||||
// Fetch match data for each requirement
|
// Fetch match data for each requirement
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -98,8 +167,8 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = ({
|
|||||||
// Process requirements one by one
|
// Process requirements one by one
|
||||||
for (let i = 0; i < requirements.length; i++) {
|
for (let i = 0; i < requirements.length; i++) {
|
||||||
try {
|
try {
|
||||||
const match = await fetchMatchForRequirement(requirements[i]);
|
const match: SkillMatch = await apiClient.candidateMatchForRequirement(candidate.id || '', requirements[i]);
|
||||||
|
console.log(match);
|
||||||
setSkillMatches(prev => {
|
setSkillMatches(prev => {
|
||||||
const updated = [...prev];
|
const updated = [...prev];
|
||||||
updated[i] = match;
|
updated[i] = match;
|
||||||
@ -133,7 +202,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = ({
|
|||||||
if (!loadingRequirements) {
|
if (!loadingRequirements) {
|
||||||
fetchMatchData();
|
fetchMatchData();
|
||||||
}
|
}
|
||||||
}, [requirements, loadingRequirements, fetchMatchForRequirement]);
|
}, [requirements, loadingRequirements]);
|
||||||
|
|
||||||
// Get color based on match score
|
// Get color based on match score
|
||||||
const getMatchColor = (score: number): string => {
|
const getMatchColor = (score: number): string => {
|
||||||
@ -165,13 +234,13 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = ({
|
|||||||
|
|
||||||
<Grid size={{ xs: 12, md: 6 }}>
|
<Grid size={{ xs: 12, md: 6 }}>
|
||||||
<Typography variant="h6" component="h2">
|
<Typography variant="h6" component="h2">
|
||||||
Job: {jobTitle}
|
Job: {job.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid size={{ xs: 12, md: 6 }}>
|
<Grid size={{ xs: 12, md: 6 }}>
|
||||||
<Typography variant="h6" component="h2">
|
<Typography variant="h6" component="h2">
|
||||||
Candidate: {candidateName}
|
Candidate: {candidate.fullName}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@ -329,7 +398,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = ({
|
|||||||
Supporting Evidence:
|
Supporting Evidence:
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{match.citations.length > 0 ? (
|
{match.citations && match.citations.length > 0 ? (
|
||||||
match.citations.map((citation, citIndex) => (
|
match.citations.map((citation, citIndex) => (
|
||||||
<Card
|
<Card
|
||||||
key={citIndex}
|
key={citIndex}
|
||||||
|
@ -1,153 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { JobMatchAnalysis } from '../components/JobMatchAnalysis';
|
|
||||||
|
|
||||||
// Mock data and functions to simulate your backend
|
|
||||||
const mockRequirements = [
|
|
||||||
"5+ years of React development experience",
|
|
||||||
"Strong TypeScript skills",
|
|
||||||
"Experience with RESTful APIs",
|
|
||||||
"Knowledge of state management solutions (Redux, Context API)",
|
|
||||||
"Experience with CI/CD pipelines",
|
|
||||||
"Cloud platform experience (AWS, Azure, GCP)"
|
|
||||||
];
|
|
||||||
|
|
||||||
// Simulates fetching requirements with a delay
|
|
||||||
const mockFetchRequirements = async (): Promise<string[]> => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve(mockRequirements);
|
|
||||||
}, 1500); // Simulate network delay
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Simulates fetching match data for a requirement with varying delays
|
|
||||||
const mockFetchMatchForRequirement = async (requirement: string): Promise<any> => {
|
|
||||||
// Create different mock responses based on the requirement
|
|
||||||
const mockResponses: Record<string, any> = {
|
|
||||||
"5+ years of React development experience": {
|
|
||||||
requirement: "5+ years of React development experience",
|
|
||||||
status: "complete",
|
|
||||||
matchScore: 85,
|
|
||||||
assessment: "The candidate demonstrates extensive React experience spanning over 6 years, with a strong portfolio of complex applications and deep understanding of React's component lifecycle and hooks.",
|
|
||||||
citations: [
|
|
||||||
{
|
|
||||||
text: "Led frontend development team of 5 engineers to rebuild our customer portal using React and TypeScript, resulting in 40% improved performance and 30% reduction in bugs.",
|
|
||||||
source: "Resume, Work Experience",
|
|
||||||
relevance: 95
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Developed and maintained reusable React component library used across 12 different products within the organization.",
|
|
||||||
source: "Resume, Work Experience",
|
|
||||||
relevance: 90
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "I've been working with React since 2017, building everything from small widgets to enterprise applications.",
|
|
||||||
source: "Cover Letter",
|
|
||||||
relevance: 85
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Strong TypeScript skills": {
|
|
||||||
requirement: "Strong TypeScript skills",
|
|
||||||
status: "complete",
|
|
||||||
matchScore: 90,
|
|
||||||
assessment: "The candidate shows excellent TypeScript proficiency through their work history and personal projects. They have implemented complex type systems and demonstrate an understanding of advanced TypeScript features.",
|
|
||||||
citations: [
|
|
||||||
{
|
|
||||||
text: "Converted a legacy JavaScript codebase of 100,000+ lines to TypeScript, implementing strict type checking and reducing runtime errors by 70%.",
|
|
||||||
source: "Resume, Projects",
|
|
||||||
relevance: 98
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Created comprehensive TypeScript interfaces for our GraphQL API, ensuring type safety across the entire application stack.",
|
|
||||||
source: "Resume, Technical Skills",
|
|
||||||
relevance: 95
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Experience with RESTful APIs": {
|
|
||||||
requirement: "Experience with RESTful APIs",
|
|
||||||
status: "complete",
|
|
||||||
matchScore: 75,
|
|
||||||
assessment: "The candidate has good experience with RESTful APIs, having both consumed and designed them. They understand REST principles but have less documented experience with API versioning and caching strategies.",
|
|
||||||
citations: [
|
|
||||||
{
|
|
||||||
text: "Designed and implemented a RESTful API serving over 1M requests daily with a focus on performance and scalability.",
|
|
||||||
source: "Resume, Technical Projects",
|
|
||||||
relevance: 85
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Worked extensively with third-party APIs including Stripe, Twilio, and Salesforce to integrate payment processing and communication features.",
|
|
||||||
source: "Resume, Work Experience",
|
|
||||||
relevance: 70
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Knowledge of state management solutions (Redux, Context API)": {
|
|
||||||
requirement: "Knowledge of state management solutions (Redux, Context API)",
|
|
||||||
status: "complete",
|
|
||||||
matchScore: 65,
|
|
||||||
assessment: "The candidate has moderate experience with state management, primarily using Redux. There is less evidence of Context API usage, which could indicate a knowledge gap in more modern React state management approaches.",
|
|
||||||
citations: [
|
|
||||||
{
|
|
||||||
text: "Implemented Redux for global state management in an e-commerce application, handling complex state logic for cart, user preferences, and product filtering.",
|
|
||||||
source: "Resume, Skills",
|
|
||||||
relevance: 80
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "My experience includes working with state management libraries like Redux and MobX.",
|
|
||||||
source: "Cover Letter",
|
|
||||||
relevance: 60
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Experience with CI/CD pipelines": {
|
|
||||||
requirement: "Experience with CI/CD pipelines",
|
|
||||||
status: "complete",
|
|
||||||
matchScore: 40,
|
|
||||||
assessment: "The candidate shows limited experience with CI/CD pipelines. While they mention some exposure to Jenkins and GitLab CI, there is insufficient evidence of setting up or maintaining comprehensive CI/CD workflows.",
|
|
||||||
citations: [
|
|
||||||
{
|
|
||||||
text: "Familiar with CI/CD tools including Jenkins and GitLab CI.",
|
|
||||||
source: "Resume, Skills",
|
|
||||||
relevance: 40
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Cloud platform experience (AWS, Azure, GCP)": {
|
|
||||||
requirement: "Cloud platform experience (AWS, Azure, GCP)",
|
|
||||||
status: "complete",
|
|
||||||
matchScore: 30,
|
|
||||||
assessment: "The candidate demonstrates minimal experience with cloud platforms. There is a brief mention of AWS S3 and Lambda, but no substantial evidence of deeper cloud architecture knowledge or experience with Azure or GCP.",
|
|
||||||
citations: [
|
|
||||||
{
|
|
||||||
text: "Used AWS S3 for file storage and Lambda for image processing in a photo sharing application.",
|
|
||||||
source: "Resume, Projects",
|
|
||||||
relevance: 35
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Return a promise that resolves with the mock data after a delay
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
// Different requirements resolve at different speeds to simulate real-world analysis
|
|
||||||
const delay = Math.random() * 5000 + 2000; // 2-7 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve(mockResponses[requirement]);
|
|
||||||
}, delay);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const DemoComponent: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<JobMatchAnalysis
|
|
||||||
jobTitle="Senior Frontend Developer"
|
|
||||||
candidateName="Alex Johnson"
|
|
||||||
fetchRequirements={mockFetchRequirements}
|
|
||||||
fetchMatchForRequirement={mockFetchMatchForRequirement}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { DemoComponent };
|
|
@ -187,11 +187,11 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
|
|||||||
controllerRef.current = apiClient.sendMessageStream(chatMessage, {
|
controllerRef.current = apiClient.sendMessageStream(chatMessage, {
|
||||||
onMessage: async (msg: ChatMessage) => {
|
onMessage: async (msg: ChatMessage) => {
|
||||||
console.log(`onMessage: ${msg.type} ${msg.content}`, msg);
|
console.log(`onMessage: ${msg.type} ${msg.content}`, msg);
|
||||||
if (msg.type === "heartbeat") {
|
if (msg.type === "heartbeat" && msg.content) {
|
||||||
const heartbeat = JSON.parse(msg.content);
|
const heartbeat = JSON.parse(msg.content);
|
||||||
setTimestamp(heartbeat.timestamp);
|
setTimestamp(heartbeat.timestamp);
|
||||||
}
|
}
|
||||||
if (msg.type === "thinking") {
|
if (msg.type === "thinking" && msg.content) {
|
||||||
const status = JSON.parse(msg.content);
|
const status = JSON.parse(msg.content);
|
||||||
setProcessingMessage({ ...defaultMessage, content: status.message });
|
setProcessingMessage({ ...defaultMessage, content: status.message });
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,8 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate()
|
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate()
|
||||||
|
const { setSnack, submitQuery } = props;
|
||||||
|
const backstoryProps = { setSnack, submitQuery };
|
||||||
// State management
|
// State management
|
||||||
const [activeStep, setActiveStep] = useState(0);
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
const [jobDescription, setJobDescription] = useState('');
|
const [jobDescription, setJobDescription] = useState('');
|
||||||
@ -57,7 +58,6 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [openUploadDialog, setOpenUploadDialog] = useState(false);
|
const [openUploadDialog, setOpenUploadDialog] = useState(false);
|
||||||
const { apiClient } = useAuth();
|
const { apiClient } = useAuth();
|
||||||
const { setSnack } = props;
|
|
||||||
const [candidates, setCandidates] = useState<Candidate[] | null>(null);
|
const [candidates, setCandidates] = useState<Candidate[] | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -421,6 +421,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
|||||||
<JobMatchAnalysis
|
<JobMatchAnalysis
|
||||||
job={{ title: jobTitle, description: jobDescription }}
|
job={{ title: jobTitle, description: jobDescription }}
|
||||||
candidate={selectedCandidate}
|
candidate={selectedCandidate}
|
||||||
|
{...backstoryProps}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -824,6 +824,18 @@ class ApiClient {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async candidateMatchForRequirement(candidate_id: string, requirement: string) : Promise<Types.SkillMatch> {
|
||||||
|
const response = await fetch(`${this.baseUrl}/candidates/${candidate_id}/skill-match`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: this.defaultHeaders,
|
||||||
|
body: JSON.stringify(requirement)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await handleApiResponse<Types.SkillMatch>(response);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
async updateCandidateDocument(document: Types.Document) : Promise<Types.Document> {
|
async updateCandidateDocument(document: Types.Document) : Promise<Types.Document> {
|
||||||
const request : Types.DocumentUpdateRequest = {
|
const request : Types.DocumentUpdateRequest = {
|
||||||
filename: document.filename,
|
filename: document.filename,
|
||||||
@ -1040,7 +1052,11 @@ class ApiClient {
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
incomingMessageList.push(convertedIncoming);
|
incomingMessageList.push(convertedIncoming);
|
||||||
|
try {
|
||||||
options.onMessage?.(convertedIncoming);
|
options.onMessage?.(convertedIncoming);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('onMessage handler failed: ', error);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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-03T23:59:28.355326
|
// Generated on: 2025-06-04T03:59:11.250216
|
||||||
// DO NOT EDIT MANUALLY - This file is auto-generated
|
// DO NOT EDIT MANUALLY - This file is auto-generated
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
@ -13,11 +13,11 @@ export type ActivityType = "login" | "search" | "view_job" | "apply_job" | "mess
|
|||||||
|
|
||||||
export type ApplicationStatus = "applied" | "reviewing" | "interview" | "offer" | "rejected" | "accepted" | "withdrawn";
|
export type ApplicationStatus = "applied" | "reviewing" | "interview" | "offer" | "rejected" | "accepted" | "withdrawn";
|
||||||
|
|
||||||
export type ChatContextType = "job_search" | "job_requirements" | "candidate_chat" | "interview_prep" | "resume_review" | "general" | "generate_persona" | "generate_profile" | "generate_image" | "rag_search";
|
export type ChatContextType = "job_search" | "job_requirements" | "candidate_chat" | "interview_prep" | "resume_review" | "general" | "generate_persona" | "generate_profile" | "generate_image" | "rag_search" | "skill_match";
|
||||||
|
|
||||||
export type ChatMessageType = "error" | "generating" | "info" | "preparing" | "processing" | "heartbeat" | "response" | "searching" | "rag_result" | "system" | "thinking" | "tooling" | "user";
|
export type ChatMessageType = "error" | "generating" | "info" | "preparing" | "processing" | "heartbeat" | "response" | "searching" | "rag_result" | "system" | "thinking" | "tooling" | "user";
|
||||||
|
|
||||||
export type ChatSenderType = "user" | "assistant" | "system";
|
export type ChatSenderType = "user" | "assistant" | "agent" | "system";
|
||||||
|
|
||||||
export type ChatStatusType = "initializing" | "streaming" | "status" | "done" | "error";
|
export type ChatStatusType = "initializing" | "streaming" | "status" | "done" | "error";
|
||||||
|
|
||||||
@ -49,6 +49,8 @@ export type SearchType = "similarity" | "mmr" | "hybrid" | "keyword";
|
|||||||
|
|
||||||
export type SkillLevel = "beginner" | "intermediate" | "advanced" | "expert";
|
export type SkillLevel = "beginner" | "intermediate" | "advanced" | "expert";
|
||||||
|
|
||||||
|
export type SkillStatus = "pending" | "complete" | "error";
|
||||||
|
|
||||||
export type SocialPlatform = "linkedin" | "twitter" | "github" | "dribbble" | "behance" | "website" | "other";
|
export type SocialPlatform = "linkedin" | "twitter" | "github" | "dribbble" | "behance" | "website" | "other";
|
||||||
|
|
||||||
export type SortOrder = "asc" | "desc";
|
export type SortOrder = "asc" | "desc";
|
||||||
@ -272,7 +274,7 @@ export interface Certification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatContext {
|
export interface ChatContext {
|
||||||
type: "job_search" | "job_requirements" | "candidate_chat" | "interview_prep" | "resume_review" | "general" | "generate_persona" | "generate_profile" | "generate_image" | "rag_search";
|
type: "job_search" | "job_requirements" | "candidate_chat" | "interview_prep" | "resume_review" | "general" | "generate_persona" | "generate_profile" | "generate_image" | "rag_search" | "skill_match";
|
||||||
relatedEntityId?: string;
|
relatedEntityId?: string;
|
||||||
relatedEntityType?: "job" | "candidate" | "employer";
|
relatedEntityType?: "job" | "candidate" | "employer";
|
||||||
additionalContext?: Record<string, any>;
|
additionalContext?: Record<string, any>;
|
||||||
@ -284,7 +286,7 @@ export interface ChatMessage {
|
|||||||
senderId?: string;
|
senderId?: string;
|
||||||
status: "initializing" | "streaming" | "status" | "done" | "error";
|
status: "initializing" | "streaming" | "status" | "done" | "error";
|
||||||
type: "error" | "generating" | "info" | "preparing" | "processing" | "heartbeat" | "response" | "searching" | "rag_result" | "system" | "thinking" | "tooling" | "user";
|
type: "error" | "generating" | "info" | "preparing" | "processing" | "heartbeat" | "response" | "searching" | "rag_result" | "system" | "thinking" | "tooling" | "user";
|
||||||
sender: "user" | "assistant" | "system";
|
sender: "user" | "assistant" | "agent" | "system";
|
||||||
timestamp?: Date;
|
timestamp?: Date;
|
||||||
tunables?: Tunables;
|
tunables?: Tunables;
|
||||||
content: string;
|
content: string;
|
||||||
@ -297,7 +299,7 @@ export interface ChatMessageBase {
|
|||||||
senderId?: string;
|
senderId?: string;
|
||||||
status: "initializing" | "streaming" | "status" | "done" | "error";
|
status: "initializing" | "streaming" | "status" | "done" | "error";
|
||||||
type: "error" | "generating" | "info" | "preparing" | "processing" | "heartbeat" | "response" | "searching" | "rag_result" | "system" | "thinking" | "tooling" | "user";
|
type: "error" | "generating" | "info" | "preparing" | "processing" | "heartbeat" | "response" | "searching" | "rag_result" | "system" | "thinking" | "tooling" | "user";
|
||||||
sender: "user" | "assistant" | "system";
|
sender: "user" | "assistant" | "agent" | "system";
|
||||||
timestamp?: Date;
|
timestamp?: Date;
|
||||||
tunables?: Tunables;
|
tunables?: Tunables;
|
||||||
content: string;
|
content: string;
|
||||||
@ -328,7 +330,7 @@ export interface ChatMessageRagSearch {
|
|||||||
senderId?: string;
|
senderId?: string;
|
||||||
status: "initializing" | "streaming" | "status" | "done" | "error";
|
status: "initializing" | "streaming" | "status" | "done" | "error";
|
||||||
type: "error" | "generating" | "info" | "preparing" | "processing" | "heartbeat" | "response" | "searching" | "rag_result" | "system" | "thinking" | "tooling" | "user";
|
type: "error" | "generating" | "info" | "preparing" | "processing" | "heartbeat" | "response" | "searching" | "rag_result" | "system" | "thinking" | "tooling" | "user";
|
||||||
sender: "user" | "assistant" | "system";
|
sender: "user" | "assistant" | "agent" | "system";
|
||||||
timestamp?: Date;
|
timestamp?: Date;
|
||||||
tunables?: Tunables;
|
tunables?: Tunables;
|
||||||
content: string;
|
content: string;
|
||||||
@ -341,7 +343,7 @@ export interface ChatMessageUser {
|
|||||||
senderId?: string;
|
senderId?: string;
|
||||||
status: "initializing" | "streaming" | "status" | "done" | "error";
|
status: "initializing" | "streaming" | "status" | "done" | "error";
|
||||||
type: "error" | "generating" | "info" | "preparing" | "processing" | "heartbeat" | "response" | "searching" | "rag_result" | "system" | "thinking" | "tooling" | "user";
|
type: "error" | "generating" | "info" | "preparing" | "processing" | "heartbeat" | "response" | "searching" | "rag_result" | "system" | "thinking" | "tooling" | "user";
|
||||||
sender: "user" | "assistant" | "system";
|
sender: "user" | "assistant" | "agent" | "system";
|
||||||
timestamp?: Date;
|
timestamp?: Date;
|
||||||
tunables?: Tunables;
|
tunables?: Tunables;
|
||||||
content: string;
|
content: string;
|
||||||
@ -386,6 +388,12 @@ export interface ChromaDBGetResponse {
|
|||||||
umapEmbedding3D?: Array<number>;
|
umapEmbedding3D?: Array<number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Citation {
|
||||||
|
text: string;
|
||||||
|
source: string;
|
||||||
|
relevance: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateCandidateRequest {
|
export interface CreateCandidateRequest {
|
||||||
email: string;
|
email: string;
|
||||||
username: string;
|
username: string;
|
||||||
@ -613,6 +621,16 @@ export interface JobListResponse {
|
|||||||
meta?: Record<string, any>;
|
meta?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface JobRequirements {
|
||||||
|
technicalSkills: Requirements;
|
||||||
|
experienceRequirements: Requirements;
|
||||||
|
softSkills?: Array<string>;
|
||||||
|
experience?: Array<string>;
|
||||||
|
education?: Array<string>;
|
||||||
|
certifications?: Array<string>;
|
||||||
|
preferredAttributes?: Array<string>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface JobResponse {
|
export interface JobResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data?: Job;
|
data?: Job;
|
||||||
@ -765,6 +783,11 @@ export interface RefreshToken {
|
|||||||
revokedReason?: string;
|
revokedReason?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Requirements {
|
||||||
|
required?: Array<string>;
|
||||||
|
preferred?: Array<string>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ResendVerificationRequest {
|
export interface ResendVerificationRequest {
|
||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
@ -810,6 +833,14 @@ export interface SkillAssessment {
|
|||||||
comments?: string;
|
comments?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SkillMatch {
|
||||||
|
requirement: string;
|
||||||
|
status: "pending" | "complete" | "error";
|
||||||
|
matchScore: number;
|
||||||
|
assessment: string;
|
||||||
|
citations?: Array<Citation>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SocialLink {
|
export interface SocialLink {
|
||||||
platform: "linkedin" | "twitter" | "github" | "dribbble" | "behance" | "website" | "other";
|
platform: "linkedin" | "twitter" | "github" | "dribbble" | "behance" | "website" | "other";
|
||||||
url: string;
|
url: string;
|
||||||
@ -857,233 +888,6 @@ export interface WorkExperience {
|
|||||||
achievements?: Array<string>;
|
achievements?: Array<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================
|
|
||||||
// Default Objects
|
|
||||||
// ============================
|
|
||||||
|
|
||||||
// These objects contain the default values from your Pydantic models
|
|
||||||
// Use them to initialize objects with sensible defaults:
|
|
||||||
// const message: ChatMessage = { ...DefaultChatMessage, sessionId: '123', content: 'Hello' };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default values for BaseUser
|
|
||||||
* Fields with defaults: isAdmin
|
|
||||||
*/
|
|
||||||
export const DefaultBaseUser: Partial<BaseUser> = {
|
|
||||||
isAdmin: False
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default values for BaseUserWithType
|
|
||||||
* Fields with defaults: isAdmin
|
|
||||||
*/
|
|
||||||
export const DefaultBaseUserWithType: Partial<BaseUserWithType> = {
|
|
||||||
isAdmin: False
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default values for Candidate
|
|
||||||
* Fields with defaults: isAdmin, userType, ragContentSize
|
|
||||||
*/
|
|
||||||
export const DefaultCandidate: Partial<Candidate> = {
|
|
||||||
isAdmin: False,
|
|
||||||
userType: "candidate",
|
|
||||||
ragContentSize: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default values for CandidateAI
|
|
||||||
* Fields with defaults: isAdmin, userType, ragContentSize, isAI
|
|
||||||
*/
|
|
||||||
export const DefaultCandidateAI: Partial<CandidateAI> = {
|
|
||||||
isAdmin: False,
|
|
||||||
userType: "candidate",
|
|
||||||
ragContentSize: 0,
|
|
||||||
isAI: True
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default values for ChatContext
|
|
||||||
* Fields with defaults: additionalContext
|
|
||||||
*/
|
|
||||||
export const DefaultChatContext: Partial<ChatContext> = {
|
|
||||||
additionalContext: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default values for ChatMessage
|
|
||||||
* Fields with defaults: status, type, sender, content
|
|
||||||
*/
|
|
||||||
export const DefaultChatMessage: Partial<ChatMessage> = {
|
|
||||||
status: "initializing",
|
|
||||||
type: "preparing",
|
|
||||||
sender: "system",
|
|
||||||
content: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default values for ChatMessageBase
|
|
||||||
* Fields with defaults: status, type, sender, content
|
|
||||||
*/
|
|
||||||
export const DefaultChatMessageBase: Partial<ChatMessageBase> = {
|
|
||||||
status: "initializing",
|
|
||||||
type: "preparing",
|
|
||||||
sender: "system",
|
|
||||||
content: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default values for ChatMessageMetaData
|
|
||||||
* Fields with defaults: model, temperature, maxTokens, topP, evalCount, evalDuration, promptEvalCount, promptEvalDuration
|
|
||||||
*/
|
|
||||||
export const DefaultChatMessageMetaData: Partial<ChatMessageMetaData> = {
|
|
||||||
model: "qwen2.5",
|
|
||||||
temperature: 0.7,
|
|
||||||
maxTokens: 8092,
|
|
||||||
topP: 1,
|
|
||||||
evalCount: 0,
|
|
||||||
evalDuration: 0,
|
|
||||||
promptEvalCount: 0,
|
|
||||||
promptEvalDuration: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default values for ChatMessageRagSearch
|
|
||||||
* Fields with defaults: status, type, sender, content, dimensions
|
|
||||||
*/
|
|
||||||
export const DefaultChatMessageRagSearch: Partial<ChatMessageRagSearch> = {
|
|
||||||
status: "done",
|
|
||||||
type: "rag_result",
|
|
||||||
sender: "user",
|
|
||||||
content: "",
|
|
||||||
dimensions: 3
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default values for ChatMessageUser
|
|
||||||
* Fields with defaults: status, type, sender, content
|
|
||||||
*/
|
|
||||||
export const DefaultChatMessageUser: Partial<ChatMessageUser> = {
|
|
||||||
status: "done",
|
|
||||||
type: "user",
|
|
||||||
sender: "user",
|
|
||||||
content: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default values for ChatOptions
|
|
||||||
* Fields with defaults: seed, temperature
|
|
||||||
*/
|
|
||||||
export const DefaultChatOptions: Partial<ChatOptions> = {
|
|
||||||
seed: 8911,
|
|
||||||
temperature: 0.7
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default values for ChatSession
|
|
||||||
* Fields with defaults: isArchived
|
|
||||||
*/
|
|
||||||
export const DefaultChatSession: Partial<ChatSession> = {
|
|
||||||
isArchived: False
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default values for ChromaDBGetResponse
|
|
||||||
* Fields with defaults: ids, embeddings, documents, metadatas, distances, name, size, dimensions, query
|
|
||||||
*/
|
|
||||||
export const DefaultChromaDBGetResponse: Partial<ChromaDBGetResponse> = {
|
|
||||||
ids: [],
|
|
||||||
embeddings: [],
|
|
||||||
documents: [],
|
|
||||||
metadatas: [],
|
|
||||||
distances: [],
|
|
||||||
name: "",
|
|
||||||
size: 0,
|
|
||||||
dimensions: 3,
|
|
||||||
query: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default values for Document
|
|
||||||
* Fields with defaults: includeInRAG, ragChunks
|
|
||||||
*/
|
|
||||||
export const DefaultDocument: Partial<Document> = {
|
|
||||||
includeInRAG: True,
|
|
||||||
ragChunks: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default values for Employer
|
|
||||||
* Fields with defaults: isAdmin, userType
|
|
||||||
*/
|
|
||||||
export const DefaultEmployer: Partial<Employer> = {
|
|
||||||
isAdmin: False,
|
|
||||||
userType: "employer"
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default values for Job
|
|
||||||
* Fields with defaults: views, applicationCount
|
|
||||||
*/
|
|
||||||
export const DefaultJob: Partial<Job> = {
|
|
||||||
views: 0,
|
|
||||||
applicationCount: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default values for LLMMessage
|
|
||||||
* Fields with defaults: role, content, toolCalls
|
|
||||||
*/
|
|
||||||
export const DefaultLLMMessage: Partial<LLMMessage> = {
|
|
||||||
role: "",
|
|
||||||
content: "",
|
|
||||||
toolCalls: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default values for MFAVerifyRequest
|
|
||||||
* Fields with defaults: rememberDevice
|
|
||||||
*/
|
|
||||||
export const DefaultMFAVerifyRequest: Partial<MFAVerifyRequest> = {
|
|
||||||
rememberDevice: False
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default values for PaginatedRequest
|
|
||||||
* Fields with defaults: page, limit
|
|
||||||
*/
|
|
||||||
export const DefaultPaginatedRequest: Partial<PaginatedRequest> = {
|
|
||||||
page: 1,
|
|
||||||
limit: 20
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default values for RagEntry
|
|
||||||
* Fields with defaults: description, enabled
|
|
||||||
*/
|
|
||||||
export const DefaultRagEntry: Partial<RagEntry> = {
|
|
||||||
description: "",
|
|
||||||
enabled: True
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default values for SearchQuery
|
|
||||||
* Fields with defaults: page, limit
|
|
||||||
*/
|
|
||||||
export const DefaultSearchQuery: Partial<SearchQuery> = {
|
|
||||||
page: 1,
|
|
||||||
limit: 20
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default values for Tunables
|
|
||||||
* Fields with defaults: enableRAG, enableTools, enableContext
|
|
||||||
*/
|
|
||||||
export const DefaultTunables: Partial<Tunables> = {
|
|
||||||
enableRAG: True,
|
|
||||||
enableTools: True,
|
|
||||||
enableContext: True
|
|
||||||
};
|
|
||||||
// ============================
|
// ============================
|
||||||
// Date Conversion Functions
|
// Date Conversion Functions
|
||||||
// ============================
|
// ============================
|
||||||
|
@ -17,6 +17,7 @@ from typing import (
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import inspect
|
import inspect
|
||||||
|
import re
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime, UTC
|
from datetime import datetime, UTC
|
||||||
@ -79,6 +80,12 @@ class Agent(BaseModel, ABC):
|
|||||||
def context_size(self, value: int):
|
def context_size(self, value: int):
|
||||||
Agent._context_size = value
|
Agent._context_size = value
|
||||||
|
|
||||||
|
async def get_last_item(self, generator):
|
||||||
|
last_item = None
|
||||||
|
async for item in generator:
|
||||||
|
last_item = item
|
||||||
|
return last_item
|
||||||
|
|
||||||
def set_optimal_context_size(
|
def set_optimal_context_size(
|
||||||
self, llm: Any, model: str, prompt: str, ctx_buffer=2048
|
self, llm: Any, model: str, prompt: str, ctx_buffer=2048
|
||||||
) -> int:
|
) -> int:
|
||||||
@ -297,7 +304,7 @@ class Agent(BaseModel, ABC):
|
|||||||
self,
|
self,
|
||||||
chat_message: ChatMessage,
|
chat_message: ChatMessage,
|
||||||
top_k: int=defines.default_rag_top_k,
|
top_k: int=defines.default_rag_top_k,
|
||||||
threshold: float=defines.default_rag_threshold
|
threshold: float=defines.default_rag_threshold,
|
||||||
) -> AsyncGenerator[ChatMessage, None]:
|
) -> AsyncGenerator[ChatMessage, None]:
|
||||||
"""
|
"""
|
||||||
Generate RAG results for the given query.
|
Generate RAG results for the given query.
|
||||||
@ -320,24 +327,29 @@ class Agent(BaseModel, ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not self.user:
|
if not self.user:
|
||||||
|
logger.error("No user set for RAG generation")
|
||||||
rag_message.status = ChatStatusType.DONE
|
rag_message.status = ChatStatusType.DONE
|
||||||
rag_message.content = "No user connected to this chat, so no RAG content."
|
rag_message.content = ""
|
||||||
yield rag_message
|
yield rag_message
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
entries: int = 0
|
entries: int = 0
|
||||||
user: Candidate = self.user
|
user: Candidate = self.user
|
||||||
|
rag_content: str = ""
|
||||||
for rag in user.rags:
|
for rag in user.rags:
|
||||||
if not rag.enabled:
|
if not rag.enabled:
|
||||||
continue
|
continue
|
||||||
rag_message.type = ChatMessageType.SEARCHING
|
status_message = ChatMessage(
|
||||||
rag_message.status = ChatStatusType.INITIALIZING
|
session_id=chat_message.session_id,
|
||||||
rag_message.content = f"Checking RAG context {rag.name}..."
|
sender=ChatSenderType.AGENT,
|
||||||
yield rag_message
|
status = ChatStatusType.INITIALIZING,
|
||||||
|
type = ChatMessageType.SEARCHING,
|
||||||
|
content = f"Checking RAG context {rag.name}...")
|
||||||
|
yield status_message
|
||||||
|
|
||||||
chroma_results = user.file_watcher.find_similar(
|
chroma_results = user.file_watcher.find_similar(
|
||||||
query=rag_message.content, top_k=top_k, threshold=threshold
|
query=chat_message.content, top_k=top_k, threshold=threshold
|
||||||
)
|
)
|
||||||
if chroma_results:
|
if chroma_results:
|
||||||
query_embedding = np.array(chroma_results["query_embedding"]).flatten()
|
query_embedding = np.array(chroma_results["query_embedding"]).flatten()
|
||||||
@ -360,15 +372,26 @@ class Agent(BaseModel, ABC):
|
|||||||
|
|
||||||
entries += len(rag_metadata.documents)
|
entries += len(rag_metadata.documents)
|
||||||
rag_message.metadata.rag_results.append(rag_metadata)
|
rag_message.metadata.rag_results.append(rag_metadata)
|
||||||
rag_message.content = f"Results from {rag.name} RAG: {len(rag_metadata.documents)} results."
|
|
||||||
yield rag_message
|
|
||||||
|
|
||||||
rag_message.content = (
|
for index, metadata in enumerate(chroma_results["metadatas"]):
|
||||||
f"RAG context gathered from results from {entries} documents."
|
content = "\n".join(
|
||||||
)
|
[
|
||||||
|
line.strip()
|
||||||
|
for line in chroma_results["documents"][index].split("\n")
|
||||||
|
if line
|
||||||
|
]
|
||||||
|
).strip()
|
||||||
|
rag_content += f"""
|
||||||
|
Source: {metadata.get("doc_type", "unknown")}: {metadata.get("path", "")}
|
||||||
|
Document reference: {chroma_results["ids"][index]}
|
||||||
|
Content: { content }
|
||||||
|
"""
|
||||||
|
rag_message.content = rag_content.strip()
|
||||||
|
rag_message.type = ChatMessageType.RAG_RESULT
|
||||||
rag_message.status = ChatStatusType.DONE
|
rag_message.status = ChatStatusType.DONE
|
||||||
yield rag_message
|
yield rag_message
|
||||||
return
|
return
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
rag_message.status = ChatStatusType.ERROR
|
rag_message.status = ChatStatusType.ERROR
|
||||||
rag_message.content = f"Error generating RAG results: {str(e)}"
|
rag_message.content = f"Error generating RAG results: {str(e)}"
|
||||||
@ -377,6 +400,80 @@ class Agent(BaseModel, ABC):
|
|||||||
yield rag_message
|
yield rag_message
|
||||||
return
|
return
|
||||||
|
|
||||||
|
async def llm_one_shot(self, llm: Any, model: str, user_message: ChatMessageUser, system_prompt: str, temperature=0.7):
|
||||||
|
chat_message = ChatMessage(
|
||||||
|
session_id=user_message.session_id,
|
||||||
|
tunables=user_message.tunables,
|
||||||
|
status=ChatStatusType.INITIALIZING,
|
||||||
|
type=ChatMessageType.PREPARING,
|
||||||
|
sender=ChatSenderType.AGENT,
|
||||||
|
content="",
|
||||||
|
timestamp=datetime.now(UTC)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.set_optimal_context_size(
|
||||||
|
llm, model, prompt=chat_message.content
|
||||||
|
)
|
||||||
|
|
||||||
|
chat_message.metadata = ChatMessageMetaData()
|
||||||
|
chat_message.metadata.options = ChatOptions(
|
||||||
|
seed=8911,
|
||||||
|
num_ctx=self.context_size,
|
||||||
|
temperature=temperature, # Higher temperature to encourage tool usage
|
||||||
|
)
|
||||||
|
|
||||||
|
messages: List[LLMMessage] = [
|
||||||
|
LLMMessage(role="system", content=system_prompt),
|
||||||
|
LLMMessage(role="user", content=user_message.content),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Reset the response for streaming
|
||||||
|
chat_message.content = ""
|
||||||
|
chat_message.type = ChatMessageType.GENERATING
|
||||||
|
chat_message.status = ChatStatusType.STREAMING
|
||||||
|
|
||||||
|
logger.info(f"Message options: {chat_message.metadata.options.model_dump(exclude_unset=True)}")
|
||||||
|
response = None
|
||||||
|
for response in llm.chat(
|
||||||
|
model=model,
|
||||||
|
messages=messages,
|
||||||
|
options={
|
||||||
|
**chat_message.metadata.options.model_dump(exclude_unset=True),
|
||||||
|
},
|
||||||
|
stream=True,
|
||||||
|
):
|
||||||
|
if not response:
|
||||||
|
chat_message.status = ChatStatusType.ERROR
|
||||||
|
chat_message.content = "No response from LLM."
|
||||||
|
yield chat_message
|
||||||
|
return
|
||||||
|
|
||||||
|
chat_message.content += response.message.content
|
||||||
|
|
||||||
|
if not response.done:
|
||||||
|
chat_chunk = model_cast.cast_to_model(ChatMessageBase, chat_message)
|
||||||
|
chat_chunk.content = response.message.content
|
||||||
|
yield chat_message
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not response:
|
||||||
|
chat_message.status = ChatStatusType.ERROR
|
||||||
|
chat_message.content = "No response from LLM."
|
||||||
|
yield chat_message
|
||||||
|
return
|
||||||
|
|
||||||
|
self.collect_metrics(response)
|
||||||
|
chat_message.metadata.eval_count += response.eval_count
|
||||||
|
chat_message.metadata.eval_duration += response.eval_duration
|
||||||
|
chat_message.metadata.prompt_eval_count += response.prompt_eval_count
|
||||||
|
chat_message.metadata.prompt_eval_duration += response.prompt_eval_duration
|
||||||
|
self.context_tokens = (
|
||||||
|
response.prompt_eval_count + response.eval_count
|
||||||
|
)
|
||||||
|
chat_message.type = ChatMessageType.RESPONSE
|
||||||
|
chat_message.status = ChatStatusType.DONE
|
||||||
|
yield chat_message
|
||||||
|
|
||||||
async def generate(
|
async def generate(
|
||||||
self, llm: Any, model: str, user_message: ChatMessageUser, user: Candidate | None, temperature=0.7
|
self, llm: Any, model: str, user_message: ChatMessageUser, user: Candidate | None, temperature=0.7
|
||||||
) -> AsyncGenerator[ChatMessage | ChatMessageBase, None]:
|
) -> AsyncGenerator[ChatMessage | ChatMessageBase, None]:
|
||||||
@ -392,6 +489,10 @@ class Agent(BaseModel, ABC):
|
|||||||
timestamp=datetime.now(UTC)
|
timestamp=datetime.now(UTC)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.set_optimal_context_size(
|
||||||
|
llm, model, prompt=chat_message.content
|
||||||
|
)
|
||||||
|
|
||||||
chat_message.metadata = ChatMessageMetaData()
|
chat_message.metadata = ChatMessageMetaData()
|
||||||
chat_message.metadata.options = ChatOptions(
|
chat_message.metadata.options = ChatOptions(
|
||||||
seed=8911,
|
seed=8911,
|
||||||
@ -679,6 +780,20 @@ Content: { content }
|
|||||||
|
|
||||||
# return
|
# return
|
||||||
|
|
||||||
|
def extract_json_from_text(self, text: str) -> str:
|
||||||
|
"""Extract JSON string from text that may contain other content."""
|
||||||
|
json_pattern = r"```json\s*([\s\S]*?)\s*```"
|
||||||
|
match = re.search(json_pattern, text)
|
||||||
|
if match:
|
||||||
|
return match.group(1).strip()
|
||||||
|
|
||||||
|
# Try to find JSON without the markdown code block
|
||||||
|
json_pattern = r"({[\s\S]*})"
|
||||||
|
match = re.search(json_pattern, text)
|
||||||
|
if match:
|
||||||
|
return match.group(1).strip()
|
||||||
|
|
||||||
|
raise ValueError("No JSON 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)
|
||||||
|
@ -315,69 +315,6 @@ class GeneratePersona(Agent):
|
|||||||
self.first_name, self.last_name, self.ethnicity, self.gender = self.generator.generate_random_name()
|
self.first_name, self.last_name, self.ethnicity, self.gender = self.generator.generate_random_name()
|
||||||
self.full_name = f"{self.first_name} {self.last_name}"
|
self.full_name = f"{self.first_name} {self.last_name}"
|
||||||
|
|
||||||
async def call_llm(self, llm: Any, model: str, user_message: ChatMessageUser, system_prompt: str, temperature=0.7):
|
|
||||||
chat_message = ChatMessage(
|
|
||||||
session_id=user_message.session_id,
|
|
||||||
tunables=user_message.tunables,
|
|
||||||
status=ChatStatusType.INITIALIZING,
|
|
||||||
type=ChatMessageType.PREPARING,
|
|
||||||
sender=ChatSenderType.ASSISTANT,
|
|
||||||
content="",
|
|
||||||
timestamp=datetime.now(UTC)
|
|
||||||
)
|
|
||||||
|
|
||||||
chat_message.metadata = ChatMessageMetaData()
|
|
||||||
chat_message.metadata.options = ChatOptions(
|
|
||||||
seed=8911,
|
|
||||||
num_ctx=self.context_size,
|
|
||||||
temperature=temperature, # Higher temperature to encourage tool usage
|
|
||||||
)
|
|
||||||
|
|
||||||
messages: List[LLMMessage] = [
|
|
||||||
LLMMessage(role="system", content=system_prompt),
|
|
||||||
LLMMessage(role="user", content=user_message.content),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Reset the response for streaming
|
|
||||||
chat_message.content = ""
|
|
||||||
chat_message.type = ChatMessageType.GENERATING
|
|
||||||
chat_message.status = ChatStatusType.STREAMING
|
|
||||||
|
|
||||||
for response in llm.chat(
|
|
||||||
model=model,
|
|
||||||
messages=messages,
|
|
||||||
options={
|
|
||||||
**chat_message.metadata.options.model_dump(exclude_unset=True),
|
|
||||||
},
|
|
||||||
stream=True,
|
|
||||||
):
|
|
||||||
if not response:
|
|
||||||
chat_message.status = ChatStatusType.ERROR
|
|
||||||
chat_message.content = "No response from LLM."
|
|
||||||
yield chat_message
|
|
||||||
return
|
|
||||||
|
|
||||||
chat_message.content += response.message.content
|
|
||||||
|
|
||||||
if not response.done:
|
|
||||||
chat_chunk = model_cast.cast_to_model(ChatMessageBase, chat_message)
|
|
||||||
chat_chunk.content = response.message.content
|
|
||||||
yield chat_message
|
|
||||||
continue
|
|
||||||
|
|
||||||
if response.done:
|
|
||||||
self.collect_metrics(response)
|
|
||||||
chat_message.metadata.eval_count += response.eval_count
|
|
||||||
chat_message.metadata.eval_duration += response.eval_duration
|
|
||||||
chat_message.metadata.prompt_eval_count += response.prompt_eval_count
|
|
||||||
chat_message.metadata.prompt_eval_duration += response.prompt_eval_duration
|
|
||||||
self.context_tokens = (
|
|
||||||
response.prompt_eval_count + response.eval_count
|
|
||||||
)
|
|
||||||
chat_message.type = ChatMessageType.RESPONSE
|
|
||||||
chat_message.status = ChatStatusType.DONE
|
|
||||||
yield chat_message
|
|
||||||
|
|
||||||
async def generate(
|
async def generate(
|
||||||
self, llm: Any, model: str, user_message: ChatMessageUser, user: Candidate, temperature=0.7
|
self, llm: Any, model: str, user_message: ChatMessageUser, user: Candidate, temperature=0.7
|
||||||
):
|
):
|
||||||
@ -409,7 +346,7 @@ Incorporate the following into the job description: {original_prompt}
|
|||||||
#
|
#
|
||||||
logger.info(f"🤖 Generating persona for {self.full_name}")
|
logger.info(f"🤖 Generating persona for {self.full_name}")
|
||||||
generating_message = None
|
generating_message = None
|
||||||
async for generating_message in self.call_llm(
|
async for generating_message in self.llm_one_shot(
|
||||||
llm=llm, model=model,
|
llm=llm, model=model,
|
||||||
user_message=user_message,
|
user_message=user_message,
|
||||||
system_prompt=generate_persona_system_prompt,
|
system_prompt=generate_persona_system_prompt,
|
||||||
@ -515,7 +452,7 @@ Incorporate the following into the job description: {original_prompt}
|
|||||||
user_message.content += f"""
|
user_message.content += f"""
|
||||||
Make sure at least one of the candidate's job descriptions take into account the following: {original_prompt}."""
|
Make sure at least one of the candidate's job descriptions take into account the following: {original_prompt}."""
|
||||||
|
|
||||||
async for generating_message in self.call_llm(
|
async for generating_message in self.llm_one_shot(
|
||||||
llm=llm, model=model,
|
llm=llm, model=model,
|
||||||
user_message=user_message,
|
user_message=user_message,
|
||||||
system_prompt=generate_resume_system_prompt,
|
system_prompt=generate_resume_system_prompt,
|
||||||
|
@ -138,14 +138,10 @@ def is_date_type(python_type: Any) -> bool:
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_field_default_value(field_info: Any, debug: bool = False) -> tuple[bool, Any]:
|
def get_default_enum_value(field_info: Any, debug: bool = False) -> Optional[Any]:
|
||||||
"""Extract the default value from a field, if it exists
|
"""Extract the specific enum value from a field's default, if it exists"""
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple: (has_default, default_value)
|
|
||||||
"""
|
|
||||||
if not hasattr(field_info, 'default'):
|
if not hasattr(field_info, 'default'):
|
||||||
return False, None
|
return None
|
||||||
|
|
||||||
default_val = field_info.default
|
default_val = field_info.default
|
||||||
|
|
||||||
@ -156,7 +152,7 @@ def get_field_default_value(field_info: Any, debug: bool = False) -> tuple[bool,
|
|||||||
if default_val is ... or default_val is None:
|
if default_val is ... or default_val is None:
|
||||||
if debug:
|
if debug:
|
||||||
print(f" └─ Default is undefined marker")
|
print(f" └─ Default is undefined marker")
|
||||||
return False, None
|
return None
|
||||||
|
|
||||||
# Check for Pydantic's internal "PydanticUndefined" or similar markers
|
# Check for Pydantic's internal "PydanticUndefined" or similar markers
|
||||||
default_str = str(default_val)
|
default_str = str(default_val)
|
||||||
@ -177,72 +173,17 @@ def get_field_default_value(field_info: Any, debug: bool = False) -> tuple[bool,
|
|||||||
if is_undefined_marker:
|
if is_undefined_marker:
|
||||||
if debug:
|
if debug:
|
||||||
print(f" └─ Default is undefined marker pattern")
|
print(f" └─ Default is undefined marker pattern")
|
||||||
return False, None
|
return None
|
||||||
|
|
||||||
# We have a real default value
|
# Check if it's an enum instance
|
||||||
if debug:
|
|
||||||
print(f" └─ Has real default value: {repr(default_val)}")
|
|
||||||
return True, default_val
|
|
||||||
|
|
||||||
def convert_default_to_typescript(default_val: Any, debug: bool = False) -> str:
|
|
||||||
"""Convert a Python default value to TypeScript literal"""
|
|
||||||
if debug:
|
|
||||||
print(f" 🔄 Converting default: {repr(default_val)} (type: {type(default_val)})")
|
|
||||||
|
|
||||||
# Handle None
|
|
||||||
if default_val is None:
|
|
||||||
return "undefined"
|
|
||||||
|
|
||||||
# Handle Enum instances
|
|
||||||
if isinstance(default_val, Enum):
|
if isinstance(default_val, Enum):
|
||||||
return f'"{default_val.value}"'
|
|
||||||
|
|
||||||
# Handle basic types
|
|
||||||
if isinstance(default_val, str):
|
|
||||||
# Escape quotes and special characters
|
|
||||||
escaped = default_val.replace('\\', '\\\\').replace('"', '\\"')
|
|
||||||
return f'"{escaped}"'
|
|
||||||
elif isinstance(default_val, (int, float)):
|
|
||||||
return str(default_val)
|
|
||||||
elif isinstance(default_val, bool):
|
|
||||||
return "true" if default_val else "false"
|
|
||||||
elif isinstance(default_val, list):
|
|
||||||
if not default_val: # Empty list
|
|
||||||
return "[]"
|
|
||||||
# For non-empty lists, convert each item
|
|
||||||
items = [convert_default_to_typescript(item, debug) for item in default_val]
|
|
||||||
return f"[{', '.join(items)}]"
|
|
||||||
elif isinstance(default_val, dict):
|
|
||||||
if not default_val: # Empty dict
|
|
||||||
return "{}"
|
|
||||||
# For non-empty dicts, convert each key-value pair
|
|
||||||
items = []
|
|
||||||
for key, value in default_val.items():
|
|
||||||
key_str = f'"{key}"' if isinstance(key, str) else str(key)
|
|
||||||
value_str = convert_default_to_typescript(value, debug)
|
|
||||||
items.append(f"{key_str}: {value_str}")
|
|
||||||
return f"{{{', '.join(items)}}}"
|
|
||||||
elif isinstance(default_val, datetime):
|
|
||||||
# Convert datetime to ISO string, then wrap in new Date()
|
|
||||||
iso_string = default_val.isoformat()
|
|
||||||
return f'new Date("{iso_string}")'
|
|
||||||
|
|
||||||
# For other types, try to convert to string
|
|
||||||
if debug:
|
if debug:
|
||||||
print(f" ⚠️ Unknown default type, converting to string: {type(default_val)}")
|
print(f" └─ Default is enum instance: {default_val.value}")
|
||||||
|
return default_val
|
||||||
|
|
||||||
# Try to convert to a reasonable TypeScript representation
|
if debug:
|
||||||
try:
|
print(f" └─ Default is not an enum instance")
|
||||||
if hasattr(default_val, '__dict__'):
|
return None
|
||||||
# It's an object, try to serialize its properties
|
|
||||||
return "{}" # Fallback to empty object for complex types
|
|
||||||
else:
|
|
||||||
# Try string conversion
|
|
||||||
str_val = str(default_val)
|
|
||||||
escaped = str_val.replace('\\', '\\\\').replace('"', '\\"')
|
|
||||||
return f'"{escaped}"'
|
|
||||||
except:
|
|
||||||
return "undefined"
|
|
||||||
|
|
||||||
def python_type_to_typescript(python_type: Any, field_info: Any = None, debug: bool = False) -> str:
|
def python_type_to_typescript(python_type: Any, field_info: Any = None, debug: bool = False) -> str:
|
||||||
"""Convert a Python type to TypeScript type string, considering field defaults"""
|
"""Convert a Python type to TypeScript type string, considering field defaults"""
|
||||||
@ -257,9 +198,14 @@ def python_type_to_typescript(python_type: Any, field_info: Any = None, debug: b
|
|||||||
if debug and original_type != python_type:
|
if debug and original_type != python_type:
|
||||||
print(f" 🔄 Unwrapped: {original_type} -> {python_type}")
|
print(f" 🔄 Unwrapped: {original_type} -> {python_type}")
|
||||||
|
|
||||||
# REMOVED: The problematic enum default checking that returns only the default value
|
# FIXED: Don't lock enum types to their default values
|
||||||
# This was causing the issue where enum fields would only show the default value
|
# Instead, always return the full enum type
|
||||||
# instead of all possible enum values
|
if field_info:
|
||||||
|
default_enum = get_default_enum_value(field_info, debug)
|
||||||
|
if default_enum is not None:
|
||||||
|
if debug:
|
||||||
|
print(f" 🎯 Field has specific enum default: {default_enum.value}, but returning full enum type")
|
||||||
|
# Don't return just the default value - continue to process the full enum type
|
||||||
|
|
||||||
# Handle None/null
|
# Handle None/null
|
||||||
if python_type is type(None):
|
if python_type is type(None):
|
||||||
@ -323,12 +269,9 @@ def python_type_to_typescript(python_type: Any, field_info: Any = None, debug: b
|
|||||||
literal_values.append(str(arg))
|
literal_values.append(str(arg))
|
||||||
return " | ".join(literal_values)
|
return " | ".join(literal_values)
|
||||||
|
|
||||||
# Handle Enum types - THIS IS THE CORRECT BEHAVIOR
|
# Handle Enum types
|
||||||
# Return all possible enum values, not just the default
|
|
||||||
if isinstance(python_type, type) and issubclass(python_type, Enum):
|
if isinstance(python_type, type) and issubclass(python_type, Enum):
|
||||||
enum_values = [f'"{v.value}"' for v in python_type]
|
enum_values = [f'"{v.value}"' for v in python_type]
|
||||||
if debug:
|
|
||||||
print(f" 🎯 Enum type detected: {python_type.__name__} with values: {enum_values}")
|
|
||||||
return " | ".join(enum_values)
|
return " | ".join(enum_values)
|
||||||
|
|
||||||
# Handle individual enum instances
|
# Handle individual enum instances
|
||||||
@ -433,12 +376,18 @@ def is_field_optional(field_info: Any, field_type: Any, debug: bool = False) ->
|
|||||||
print(f" └─ RESULT: Required (default is undefined marker)")
|
print(f" └─ RESULT: Required (default is undefined marker)")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# FIXED: Fields with actual default values (including enums) should be REQUIRED
|
# Special case: if field has a specific default value (like enum), it's required
|
||||||
|
# because it will always have a value, just not optional for the consumer
|
||||||
|
if isinstance(default_val, Enum):
|
||||||
|
if debug:
|
||||||
|
print(f" └─ RESULT: Required (has specific enum default: {default_val.value})")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# FIXED: Fields with actual default values (like [], "", 0) should be REQUIRED
|
||||||
# because they will always have a value (either provided or the default)
|
# because they will always have a value (either provided or the default)
|
||||||
# This applies to enum fields with defaults as well
|
|
||||||
if debug:
|
if debug:
|
||||||
print(f" └─ RESULT: Required (has actual default value - field will always have a value)")
|
print(f" └─ RESULT: Required (has actual default value - field will always have a value)")
|
||||||
return False
|
return False # Changed from True to False
|
||||||
else:
|
else:
|
||||||
if debug:
|
if debug:
|
||||||
print(f" └─ No default attribute found")
|
print(f" └─ No default attribute found")
|
||||||
@ -472,7 +421,6 @@ def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]:
|
|||||||
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
|
||||||
default_fields = [] # Track fields with default values for default object generation
|
|
||||||
|
|
||||||
if debug:
|
if debug:
|
||||||
print(f" 🔍 Processing model: {interface_name}")
|
print(f" 🔍 Processing model: {interface_name}")
|
||||||
@ -498,17 +446,6 @@ def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]:
|
|||||||
if debug:
|
if debug:
|
||||||
print(f" Raw type: {field_type}")
|
print(f" Raw type: {field_type}")
|
||||||
|
|
||||||
# Check for default values
|
|
||||||
has_default, default_value = get_field_default_value(field_info, debug)
|
|
||||||
if has_default:
|
|
||||||
ts_default = convert_default_to_typescript(default_value, debug)
|
|
||||||
default_fields.append({
|
|
||||||
'name': ts_name,
|
|
||||||
'value': ts_default
|
|
||||||
})
|
|
||||||
if debug:
|
|
||||||
print(f" 🎯 Default value: {repr(default_value)} -> {ts_default}")
|
|
||||||
|
|
||||||
# Check if this is a date field
|
# Check if this is a date field
|
||||||
is_date = is_date_type(field_type)
|
is_date = is_date_type(field_type)
|
||||||
if debug:
|
if debug:
|
||||||
@ -525,7 +462,7 @@ 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}")
|
||||||
|
|
||||||
# Pass field_info to the type converter (but now it won't override enum types)
|
# 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)
|
||||||
|
|
||||||
# Check if optional
|
# Check if optional
|
||||||
@ -561,17 +498,6 @@ def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]:
|
|||||||
if debug:
|
if debug:
|
||||||
print(f" Raw type: {field_type}")
|
print(f" Raw type: {field_type}")
|
||||||
|
|
||||||
# Check for default values
|
|
||||||
has_default, default_value = get_field_default_value(field_info, debug)
|
|
||||||
if has_default:
|
|
||||||
ts_default = convert_default_to_typescript(default_value, debug)
|
|
||||||
default_fields.append({
|
|
||||||
'name': ts_name,
|
|
||||||
'value': ts_default
|
|
||||||
})
|
|
||||||
if debug:
|
|
||||||
print(f" 🎯 Default value: {repr(default_value)} -> {ts_default}")
|
|
||||||
|
|
||||||
# Check if this is a date field
|
# Check if this is a date field
|
||||||
is_date = is_date_type(field_type)
|
is_date = is_date_type(field_type)
|
||||||
if debug:
|
if debug:
|
||||||
@ -588,7 +514,7 @@ 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}")
|
||||||
|
|
||||||
# Pass field_info to the type converter (but now it won't override enum types)
|
# 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)
|
||||||
|
|
||||||
# For Pydantic v1, check required and default
|
# For Pydantic v1, check required and default
|
||||||
@ -609,8 +535,7 @@ 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
|
||||||
'default_fields': default_fields
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def process_enum(enum_class) -> Dict[str, Any]:
|
def process_enum(enum_class) -> Dict[str, Any]:
|
||||||
@ -624,159 +549,6 @@ def process_enum(enum_class) -> Dict[str, Any]:
|
|||||||
'values': " | ".join(values)
|
'values': " | ".join(values)
|
||||||
}
|
}
|
||||||
|
|
||||||
def generate_default_objects(interfaces: List[Dict[str, Any]]) -> str:
|
|
||||||
"""Generate TypeScript default objects for models with default values"""
|
|
||||||
default_objects = []
|
|
||||||
|
|
||||||
for interface in interfaces:
|
|
||||||
interface_name = interface['name']
|
|
||||||
default_fields = interface.get('default_fields', [])
|
|
||||||
|
|
||||||
if not default_fields:
|
|
||||||
continue # Skip interfaces without default values
|
|
||||||
|
|
||||||
object_name = f"Default{interface_name}"
|
|
||||||
|
|
||||||
# Generate default object
|
|
||||||
obj_lines = [
|
|
||||||
f"/**",
|
|
||||||
f" * Default values for {interface_name}",
|
|
||||||
f" * Fields with defaults: {', '.join([f['name'] for f in default_fields])}",
|
|
||||||
f" */",
|
|
||||||
f"export const {object_name}: Partial<{interface_name}> = {{"
|
|
||||||
]
|
|
||||||
|
|
||||||
# Add default field values
|
|
||||||
for i, default_field in enumerate(default_fields):
|
|
||||||
field_name = default_field['name']
|
|
||||||
field_value = default_field['value']
|
|
||||||
|
|
||||||
# Add comma for all but the last field
|
|
||||||
comma = "," if i < len(default_fields) - 1 else ""
|
|
||||||
obj_lines.append(f" {field_name}: {field_value}{comma}")
|
|
||||||
|
|
||||||
obj_lines.append("};")
|
|
||||||
obj_lines.append("") # Empty line after each object
|
|
||||||
|
|
||||||
default_objects.append('\n'.join(obj_lines))
|
|
||||||
|
|
||||||
if not default_objects:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
# Generate the default objects section
|
|
||||||
result = [
|
|
||||||
"// ============================",
|
|
||||||
"// Default Objects",
|
|
||||||
"// ============================",
|
|
||||||
"",
|
|
||||||
"// These objects contain the default values from your Pydantic models",
|
|
||||||
"// Use them to initialize objects with sensible defaults:",
|
|
||||||
"// const message: ChatMessage = { ...DefaultChatMessage, sessionId: '123', content: 'Hello' };",
|
|
||||||
"",
|
|
||||||
]
|
|
||||||
|
|
||||||
result.extend(default_objects)
|
|
||||||
|
|
||||||
return '\n'.join(result)
|
|
||||||
"""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', [])
|
|
||||||
|
|
||||||
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, 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 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:
|
|
||||||
func_lines.append(f" {field_name}: data.{field_name} ? new Date(data.{field_name}) : undefined,")
|
|
||||||
else:
|
|
||||||
func_lines.append(f" {field_name}: new Date(data.{field_name}),")
|
|
||||||
|
|
||||||
func_lines.extend([
|
|
||||||
f" }};",
|
|
||||||
f"}}"
|
|
||||||
])
|
|
||||||
|
|
||||||
conversion_functions.append('\n'.join(func_lines))
|
|
||||||
|
|
||||||
if not conversion_functions:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
# Generate the conversion functions section
|
|
||||||
result = [
|
|
||||||
"// ============================",
|
|
||||||
"// Date Conversion Functions",
|
|
||||||
"// ============================",
|
|
||||||
"",
|
|
||||||
"// These functions convert API responses to properly typed objects",
|
|
||||||
"// with Date objects instead of ISO date strings",
|
|
||||||
"",
|
|
||||||
]
|
|
||||||
|
|
||||||
result.extend(conversion_functions)
|
|
||||||
result.append("")
|
|
||||||
|
|
||||||
# Generate a generic converter function
|
|
||||||
models_with_dates = [interface['name'] for interface in interfaces if interface.get('date_fields')]
|
|
||||||
|
|
||||||
if models_with_dates:
|
|
||||||
result.extend([
|
|
||||||
"/**",
|
|
||||||
" * Generic converter that automatically selects the right conversion function",
|
|
||||||
" * based on the model type",
|
|
||||||
" */",
|
|
||||||
"export function convertFromApi<T>(data: any, modelType: string): T {",
|
|
||||||
" if (!data) return data;",
|
|
||||||
" ",
|
|
||||||
" switch (modelType) {"
|
|
||||||
])
|
|
||||||
|
|
||||||
for model_name in models_with_dates:
|
|
||||||
result.append(f" case '{model_name}':")
|
|
||||||
result.append(f" return convert{model_name}FromApi(data) as T;")
|
|
||||||
|
|
||||||
result.extend([
|
|
||||||
" default:",
|
|
||||||
" return data as T;",
|
|
||||||
" }",
|
|
||||||
"}",
|
|
||||||
"",
|
|
||||||
"/**",
|
|
||||||
" * Convert array of items using the appropriate converter",
|
|
||||||
" */",
|
|
||||||
"export function convertArrayFromApi<T>(data: any[], modelType: string): T[] {",
|
|
||||||
" if (!data || !Array.isArray(data)) return data;",
|
|
||||||
" return data.map(item => convertFromApi<T>(item, modelType));",
|
|
||||||
"}",
|
|
||||||
""
|
|
||||||
])
|
|
||||||
|
|
||||||
return '\n'.join(result)
|
|
||||||
|
|
||||||
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"""
|
||||||
conversion_functions = []
|
conversion_functions = []
|
||||||
@ -935,10 +707,8 @@ 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_default_fields = sum(len(interface.get('default_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_default_fields} fields with default values across all models")
|
|
||||||
|
|
||||||
# Generate TypeScript content
|
# Generate TypeScript content
|
||||||
ts_content = f"""// Generated TypeScript types from Pydantic models
|
ts_content = f"""// Generated TypeScript types from Pydantic models
|
||||||
@ -972,11 +742,6 @@ def generate_typescript_interfaces(source_file: str, debug: bool = False):
|
|||||||
|
|
||||||
ts_content += "}\n\n"
|
ts_content += "}\n\n"
|
||||||
|
|
||||||
# Add default objects
|
|
||||||
default_objects = generate_default_objects(interfaces)
|
|
||||||
if default_objects:
|
|
||||||
ts_content += default_objects
|
|
||||||
|
|
||||||
# Add conversion functions
|
# Add conversion functions
|
||||||
conversion_functions = generate_conversion_functions(interfaces)
|
conversion_functions = generate_conversion_functions(interfaces)
|
||||||
if conversion_functions:
|
if conversion_functions:
|
||||||
@ -1016,7 +781,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, default objects, and proper enum handling',
|
description='Generate TypeScript types from Pydantic models with date conversion functions and proper enum handling',
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
epilog="""
|
epilog="""
|
||||||
Examples:
|
Examples:
|
||||||
@ -1031,12 +796,8 @@ Generated conversion functions can be used like:
|
|||||||
const candidate = convertCandidateFromApi(apiResponse);
|
const candidate = convertCandidateFromApi(apiResponse);
|
||||||
const jobs = convertArrayFromApi<Job>(apiResponse, 'Job');
|
const jobs = convertArrayFromApi<Job>(apiResponse, 'Job');
|
||||||
|
|
||||||
Generated default objects can be used like:
|
Enum types are now properly handled:
|
||||||
const message: ChatMessage = { ...DefaultChatMessage, sessionId: '123', content: 'Hello' };
|
status: ChatStatusType = ChatStatusType.DONE -> status: ChatStatusType (not locked to "done")
|
||||||
const overrideMessage: ChatMessage = { ...DefaultChatMessage, status: 'error' };
|
|
||||||
|
|
||||||
Enum fields now properly support all enum values:
|
|
||||||
status: ChatStatusType = ChatStatusType.DONE -> status: "pending" | "processing" | "done" | "error"
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1073,12 +834,12 @@ Enum fields now properly support all enum values:
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--version', '-v',
|
'--version', '-v',
|
||||||
action='version',
|
action='version',
|
||||||
version='TypeScript Generator 3.3 (with Default Objects and Fixed Enum Handling)'
|
version='TypeScript Generator 3.2 (Fixed Enum Default Handling)'
|
||||||
)
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
print("🚀 Enhanced TypeScript Type Generator with Default Objects and Fixed Enum Handling")
|
print("🚀 Enhanced TypeScript Type Generator with Fixed Enum Handling")
|
||||||
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}")
|
||||||
@ -1123,37 +884,27 @@ Enum fields now properly support all enum values:
|
|||||||
|
|
||||||
# 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')
|
||||||
default_objects_count = ts_content.count('export const Default')
|
enum_type_count = ts_content.count('export type')
|
||||||
enum_union_count = ts_content.count(' | ')
|
|
||||||
|
|
||||||
if conversion_count > 0:
|
if conversion_count > 0:
|
||||||
print(f"🗓️ Generated {conversion_count} date conversion functions")
|
print(f"🗓️ Generated {conversion_count} date conversion functions")
|
||||||
if default_objects_count > 0:
|
if enum_type_count > 0:
|
||||||
print(f"🎯 Generated {default_objects_count} default objects")
|
print(f"🎯 Generated {enum_type_count} enum types (properly allowing all values)")
|
||||||
if enum_union_count > 0:
|
|
||||||
print(f"🔗 Generated {enum_union_count} union types (including proper enum types)")
|
|
||||||
|
|
||||||
if args.debug:
|
if args.debug:
|
||||||
# Show which models have date conversion
|
# Show which models have date conversion
|
||||||
models_with_dates = []
|
models_with_dates = []
|
||||||
models_with_defaults = []
|
|
||||||
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_dates.append(model_name)
|
||||||
elif line.startswith('export const Default'):
|
|
||||||
model_name = line.split('export const Default')[1].split(':')[0]
|
|
||||||
models_with_defaults.append(model_name)
|
|
||||||
|
|
||||||
if models_with_dates:
|
if models_with_dates:
|
||||||
print(f" Models with date conversion: {', '.join(models_with_dates)}")
|
print(f" Models with date conversion: {', '.join(models_with_dates)}")
|
||||||
if models_with_defaults:
|
|
||||||
print(f" Models with default objects: {', '.join(models_with_defaults)}")
|
|
||||||
|
|
||||||
# Provide troubleshooting info if debug mode
|
# Provide troubleshooting info if debug mode
|
||||||
if args.debug:
|
if args.debug:
|
||||||
print(f"\n🐛 Debug mode was enabled. If you see incorrect type conversions:")
|
print(f"\n🐛 Debug mode was enabled. If you see incorrect type conversions:")
|
||||||
print(f" 1. Look for '🎯 Enum type detected' lines to verify enum handling")
|
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" 2. Look for '📅 Date type check' lines for date handling")
|
||||||
print(f" 3. Look for '⚠️' warnings about fallback types")
|
print(f" 3. Look for '⚠️' warnings about fallback types")
|
||||||
print(f" 4. Verify your Pydantic model field types and defaults are correct")
|
print(f" 4. Verify your Pydantic model field types and defaults are correct")
|
||||||
@ -1174,23 +925,18 @@ Enum fields now properly support all enum values:
|
|||||||
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"✅ Date conversion functions: {conversion_count}")
|
||||||
if default_objects_count > 0:
|
if enum_type_count > 0:
|
||||||
print(f"✅ Default objects: {default_objects_count}")
|
print(f"✅ Enum types (with full value range): {enum_type_count}")
|
||||||
if enum_union_count > 0:
|
|
||||||
print(f"✅ Union types (proper enum support): {enum_union_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 {{ ChatMessage, ChatStatusType, DefaultChatMessage, convertChatMessageFromApi }} from './{Path(args.output).stem}';")
|
print(f" import {{ Candidate, Employer, Job, convertCandidateFromApi }} from './{Path(args.output).stem}';")
|
||||||
print(f" const message: ChatMessage = {{ ...DefaultChatMessage, sessionId: '123', content: 'Hello' }};")
|
|
||||||
if conversion_count > 0:
|
if conversion_count > 0:
|
||||||
print(f" const message = convertChatMessageFromApi(apiResponse);")
|
print(f" const candidate = convertCandidateFromApi(apiResponse);")
|
||||||
print(f" const messages = convertArrayFromApi<ChatMessage>(apiResponse, 'ChatMessage');")
|
print(f" const jobs = convertArrayFromApi<Job>(apiResponse, 'Job');")
|
||||||
if default_objects_count > 0:
|
|
||||||
print(f" const overrideMessage: ChatMessage = {{ ...DefaultChatMessage, status: 'error' }};")
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -279,6 +279,12 @@ async def get_database() -> RedisDatabase:
|
|||||||
"""
|
"""
|
||||||
return db_manager.get_database()
|
return db_manager.get_database()
|
||||||
|
|
||||||
|
async def get_last_item(generator):
|
||||||
|
last_item = None
|
||||||
|
async for item in generator:
|
||||||
|
last_item = item
|
||||||
|
return last_item
|
||||||
|
|
||||||
def create_success_response(data: Any, meta: Optional[Dict] = None) -> Dict:
|
def create_success_response(data: Any, meta: Optional[Dict] = None) -> Dict:
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
@ -3050,7 +3056,7 @@ async def post_chat_session_message_stream(
|
|||||||
status_code=404,
|
status_code=404,
|
||||||
content=create_error_response("CANDIDATE_NOT_FOUND", "Candidate not found for this chat session")
|
content=create_error_response("CANDIDATE_NOT_FOUND", "Candidate not found for this chat session")
|
||||||
)
|
)
|
||||||
logger.info(f"🔗 User {current_user.id} posting message to chat session {user_message.session_id} with query: {user_message.content}")
|
logger.info(f"🔗 User {current_user.id} posting message to chat session {user_message.session_id} with query length: {len(user_message.content)}")
|
||||||
|
|
||||||
async with entities.get_candidate_entity(candidate=candidate) as candidate_entity:
|
async with entities.get_candidate_entity(candidate=candidate) as candidate_entity:
|
||||||
# Entity automatically released when done
|
# Entity automatically released when done
|
||||||
@ -3343,6 +3349,68 @@ async def reset_chat_session(
|
|||||||
content=create_error_response("RESET_ERROR", str(e))
|
content=create_error_response("RESET_ERROR", str(e))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@api_router.post("/candidates/{candidate_id}/skill-match")
|
||||||
|
async def get_candidate_skill_match(
|
||||||
|
candidate_id: str = Path(...),
|
||||||
|
requirement: str = Body(...),
|
||||||
|
current_user = Depends(get_current_user),
|
||||||
|
database: RedisDatabase = Depends(get_database)
|
||||||
|
):
|
||||||
|
"""Get skill match for a candidate against a requirement"""
|
||||||
|
try:
|
||||||
|
# Find candidate by ID
|
||||||
|
candidate_data = await database.get_candidate(candidate_id)
|
||||||
|
if not candidate_data:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=404,
|
||||||
|
content=create_error_response("CANDIDATE_NOT_FOUND", f"Candidate with ID '{candidate_id}' not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
candidate = Candidate.model_validate(candidate_data)
|
||||||
|
|
||||||
|
logger.info(f"🔍 Running skill match for candidate {candidate.id} against requirement: {requirement}")
|
||||||
|
async with entities.get_candidate_entity(candidate=candidate) as candidate_entity:
|
||||||
|
agent = candidate_entity.get_or_create_agent(agent_type=ChatContextType.SKILL_MATCH)
|
||||||
|
if not agent:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=400,
|
||||||
|
content=create_error_response("AGENT_NOT_FOUND", "No skill match agent found for this candidate")
|
||||||
|
)
|
||||||
|
# Entity automatically released when done
|
||||||
|
skill_match = await get_last_item(
|
||||||
|
agent.generate(
|
||||||
|
llm=llm_manager.get_llm(),
|
||||||
|
model=defines.model,
|
||||||
|
user_message=ChatMessageUser(
|
||||||
|
sender_id=candidate.id,
|
||||||
|
session_id="",
|
||||||
|
content=requirement,
|
||||||
|
timestamp=datetime.now(UTC)
|
||||||
|
),
|
||||||
|
user=candidate,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if skill_match is None:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=500,
|
||||||
|
content=create_error_response("NO_MATCH", "No skill match found for the given requirement")
|
||||||
|
)
|
||||||
|
skill_match = skill_match.content.strip()
|
||||||
|
logger.info(f"✅ Skill match found for candidate {candidate.id}: {skill_match}")
|
||||||
|
|
||||||
|
return create_success_response({
|
||||||
|
"candidateId": candidate.id,
|
||||||
|
"skillMatch": skill_match
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
logger.error(f"❌ Get candidate skill match error: {e}")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=500,
|
||||||
|
content=create_error_response("SKILL_MATCH_ERROR", str(e))
|
||||||
|
)
|
||||||
|
|
||||||
@api_router.get("/candidates/{username}/chat-sessions")
|
@api_router.get("/candidates/{username}/chat-sessions")
|
||||||
async def get_candidate_chat_sessions(
|
async def get_candidate_chat_sessions(
|
||||||
username: str = Path(...),
|
username: str = Path(...),
|
||||||
|
@ -71,8 +71,51 @@ class InterviewRecommendation(str, Enum):
|
|||||||
class ChatSenderType(str, Enum):
|
class ChatSenderType(str, Enum):
|
||||||
USER = "user"
|
USER = "user"
|
||||||
ASSISTANT = "assistant"
|
ASSISTANT = "assistant"
|
||||||
|
AGENT = "agent"
|
||||||
SYSTEM = "system"
|
SYSTEM = "system"
|
||||||
|
|
||||||
|
class Requirements(BaseModel):
|
||||||
|
required: List[str] = Field(default_factory=list)
|
||||||
|
preferred: List[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
@model_validator(mode='before')
|
||||||
|
def validate_requirements(cls, values):
|
||||||
|
if not isinstance(values, dict):
|
||||||
|
raise ValueError("Requirements must be a dictionary with 'required' and 'preferred' keys.")
|
||||||
|
return values
|
||||||
|
|
||||||
|
class Citation(BaseModel):
|
||||||
|
text: str
|
||||||
|
source: str
|
||||||
|
relevance: int # 0-100 scale
|
||||||
|
|
||||||
|
class SkillStatus(str, Enum):
|
||||||
|
PENDING = "pending"
|
||||||
|
COMPLETE = "complete"
|
||||||
|
ERROR = "error"
|
||||||
|
|
||||||
|
class SkillMatch(BaseModel):
|
||||||
|
requirement: str
|
||||||
|
status: SkillStatus
|
||||||
|
match_score: int = Field(..., alias='matchScore')
|
||||||
|
assessment: str
|
||||||
|
citations: List[Citation] = Field(default_factory=list)
|
||||||
|
model_config = {
|
||||||
|
"populate_by_name": True # Allow both field names and aliases
|
||||||
|
}
|
||||||
|
|
||||||
|
class JobRequirements(BaseModel):
|
||||||
|
technical_skills: Requirements = Field(..., alias="technicalSkills")
|
||||||
|
experience_requirements: Requirements = Field(..., alias="experienceRequirements")
|
||||||
|
soft_skills: Optional[List[str]] = Field(default_factory=list, alias="softSkills")
|
||||||
|
experience: Optional[List[str]] = []
|
||||||
|
education: Optional[List[str]] = []
|
||||||
|
certifications: Optional[List[str]] = []
|
||||||
|
preferred_attributes: Optional[List[str]] = Field(None, alias="preferredAttributes")
|
||||||
|
model_config = {
|
||||||
|
"populate_by_name": True # Allow both field names and aliases
|
||||||
|
}
|
||||||
|
|
||||||
class ChatMessageType(str, Enum):
|
class ChatMessageType(str, Enum):
|
||||||
ERROR = "error"
|
ERROR = "error"
|
||||||
GENERATING = "generating"
|
GENERATING = "generating"
|
||||||
@ -106,6 +149,7 @@ class ChatContextType(str, Enum):
|
|||||||
GENERATE_PROFILE = "generate_profile"
|
GENERATE_PROFILE = "generate_profile"
|
||||||
GENERATE_IMAGE = "generate_image"
|
GENERATE_IMAGE = "generate_image"
|
||||||
RAG_SEARCH = "rag_search"
|
RAG_SEARCH = "rag_search"
|
||||||
|
SKILL_MATCH = "skill_match"
|
||||||
|
|
||||||
class AIModelType(str, Enum):
|
class AIModelType(str, Enum):
|
||||||
QWEN2_5 = "qwen2.5"
|
QWEN2_5 = "qwen2.5"
|
||||||
@ -710,20 +754,24 @@ class ChatOptions(BaseModel):
|
|||||||
seed: Optional[int] = 8911
|
seed: Optional[int] = 8911
|
||||||
num_ctx: Optional[int] = Field(default=None, alias="numCtx") # Number of context tokens
|
num_ctx: Optional[int] = Field(default=None, alias="numCtx") # Number of context tokens
|
||||||
temperature: Optional[float] = Field(default=0.7) # Higher temperature to encourage tool usage
|
temperature: Optional[float] = Field(default=0.7) # Higher temperature to encourage tool usage
|
||||||
|
model_config = {
|
||||||
|
"populate_by_name": True # Allow both field names and aliases
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class LLMMessage(BaseModel):
|
class LLMMessage(BaseModel):
|
||||||
role: str = Field(default="")
|
role: str = Field(default="")
|
||||||
content: str = Field(default="")
|
content: str = Field(default="")
|
||||||
tool_calls: Optional[List[Dict]] = Field(default={}, exclude=True)
|
tool_calls: Optional[List[Dict]] = Field(default=[], exclude=True)
|
||||||
|
|
||||||
|
|
||||||
class ChatMessageBase(BaseModel):
|
class ChatMessageBase(BaseModel):
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
||||||
session_id: str = Field(..., alias="sessionId")
|
session_id: str = Field(..., alias="sessionId")
|
||||||
sender_id: Optional[str] = Field(None, alias="senderId")
|
sender_id: Optional[str] = Field(None, alias="senderId")
|
||||||
status: ChatStatusType = ChatStatusType.INITIALIZING
|
status: ChatStatusType #= ChatStatusType.INITIALIZING
|
||||||
type: ChatMessageType = ChatMessageType.PREPARING
|
type: ChatMessageType #= ChatMessageType.PREPARING
|
||||||
sender: ChatSenderType = ChatSenderType.SYSTEM
|
sender: ChatSenderType #= ChatSenderType.SYSTEM
|
||||||
timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="timestamp")
|
timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="timestamp")
|
||||||
tunables: Optional[Tunables] = None
|
tunables: Optional[Tunables] = None
|
||||||
content: str = ""
|
content: str = ""
|
||||||
@ -759,8 +807,8 @@ class ChatMessageMetaData(BaseModel):
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ChatMessageUser(ChatMessageBase):
|
class ChatMessageUser(ChatMessageBase):
|
||||||
status: ChatStatusType = ChatStatusType.DONE
|
status: ChatStatusType = ChatStatusType.INITIALIZING
|
||||||
type: ChatMessageType = ChatMessageType.USER
|
type: ChatMessageType = ChatMessageType.GENERATING
|
||||||
sender: ChatSenderType = ChatSenderType.USER
|
sender: ChatSenderType = ChatSenderType.USER
|
||||||
|
|
||||||
class ChatMessage(ChatMessageBase):
|
class ChatMessage(ChatMessageBase):
|
||||||
|
@ -473,6 +473,7 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
|
|||||||
logging.error(chunk)
|
logging.error(chunk)
|
||||||
|
|
||||||
def prepare_metadata(self, meta: Dict[str, Any], buffer=defines.chunk_buffer)-> str | None:
|
def prepare_metadata(self, meta: Dict[str, Any], buffer=defines.chunk_buffer)-> str | None:
|
||||||
|
source_file = meta.get("source_file")
|
||||||
try:
|
try:
|
||||||
source_file = meta["source_file"]
|
source_file = meta["source_file"]
|
||||||
path_parts = source_file.split(os.sep)
|
path_parts = source_file.split(os.sep)
|
||||||
@ -487,7 +488,7 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
|
|||||||
meta["chunk_end"] = end
|
meta["chunk_end"] = end
|
||||||
return "".join(lines[start:end])
|
return "".join(lines[start:end])
|
||||||
except:
|
except:
|
||||||
logging.warning(f"Unable to open {meta["source_file"]}")
|
logging.warning(f"Unable to open {source_file}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Cosine Distance Equivalent Similarity Retrieval Characteristics
|
# Cosine Distance Equivalent Similarity Retrieval Characteristics
|
||||||
|
Loading…
x
Reference in New Issue
Block a user