Resume generation almost working
This commit is contained in:
parent
dd0ab5eda6
commit
a197535bea
@ -1,12 +1,21 @@
|
|||||||
import React, { useState, useCallback, useRef } from 'react';
|
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Tabs,
|
Tabs,
|
||||||
Tab,
|
Tab,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
Paper,
|
||||||
|
Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Job, Candidate, SkillAssessment } from "types/types";
|
import { Job, Candidate, SkillAssessment } from "types/types";
|
||||||
import JsonView from '@uiw/react-json-view';
|
import { Scrollable } from './Scrollable';
|
||||||
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
|
import * as Types from 'types/types';
|
||||||
|
import { StyledMarkdown } from './StyledMarkdown';
|
||||||
|
import { Message } from './Message';
|
||||||
|
import InputIcon from '@mui/icons-material/Input';
|
||||||
|
import TuneIcon from '@mui/icons-material/Tune';
|
||||||
|
import ArticleIcon from '@mui/icons-material/Article';
|
||||||
|
|
||||||
interface ResumeGeneratorProps {
|
interface ResumeGeneratorProps {
|
||||||
job: Job;
|
job: Job;
|
||||||
@ -15,31 +24,75 @@ interface ResumeGeneratorProps {
|
|||||||
onComplete?: (resume: string) => void;
|
onComplete?: (resume: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultMessage: Types.ChatMessageStatus = {
|
||||||
|
status: "done", type: "text", sessionId: "", timestamp: new Date(), content: "", activity: 'info'
|
||||||
|
};
|
||||||
|
|
||||||
const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorProps) => {
|
const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorProps) => {
|
||||||
const { job, candidate, skills, onComplete } = props;
|
const { job, candidate, skills, onComplete } = props;
|
||||||
const [resume, setResume] = useState<string>('Generated resume goes here...');
|
const { apiClient } = useAuth();
|
||||||
|
const [resume, setResume] = useState<string>('');
|
||||||
|
const [prompt, setPrompt] = useState<string>('');
|
||||||
|
const [systemPrompt, setSystemPrompt] = useState<string>('');
|
||||||
const [generating, setGenerating] = useState<boolean>(false);
|
const [generating, setGenerating] = useState<boolean>(false);
|
||||||
|
const [statusMessage, setStatusMessage] = useState<Types.ChatMessageStatus | null>(null);
|
||||||
|
const [tabValue, setTabValue] = useState<string>('resume');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const handleTabChange = (event: React.SyntheticEvent, newValue: string) => {
|
||||||
|
setTabValue(newValue);
|
||||||
|
}
|
||||||
// State for editing job description
|
// State for editing job description
|
||||||
|
|
||||||
const generateResume = () => {
|
const generateResumeHandlers = {
|
||||||
setResume('Generation begins...');
|
onStatus: (status: Types.ChatMessageStatus) => {
|
||||||
setGenerating(true);
|
setStatusMessage({...defaultMessage, content: status.content.toLowerCase});
|
||||||
setTimeout(() => {
|
},
|
||||||
setGenerating(false);
|
onStreaming: (chunk: Types.ChatMessageStreaming) =>{
|
||||||
setResume('Generation complete');
|
setResume(chunk.content);
|
||||||
onComplete && onComplete(resume);
|
},
|
||||||
}, 3000);
|
onComplete: () => {
|
||||||
|
setStatusMessage(null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!job || !candidate || !skills || resume || generating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const generateResume = async () => {
|
||||||
|
setGenerating(true);
|
||||||
|
const request : any = await apiClient.generateResume(candidate.id || '', skills, generateResumeHandlers);
|
||||||
|
const result = await request.promise;
|
||||||
|
setSystemPrompt(result.systemPrompt)
|
||||||
|
setPrompt(result.prompt)
|
||||||
|
setResume(result.resume)
|
||||||
|
setGenerating(false);
|
||||||
|
};
|
||||||
|
generateResume()
|
||||||
|
}, [job, candidate, apiClient, resume, skills, generating]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
className="ResumeGenerator"
|
className="ResumeGenerator"
|
||||||
sx={{display: "flex", flexDirection: "row", width: "100%"}}>
|
sx={{
|
||||||
<JsonView value={skills}/>
|
display: "flex",
|
||||||
<Box sx={{display: "flex", flexDirection: "column"}}>
|
flexDirection: "column",
|
||||||
<Box>{resume}</Box>
|
}}>
|
||||||
<Button disabled={generating} onClick={generateResume} variant="contained">Generate Resume</Button>
|
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
||||||
</Box>
|
<Tabs value={tabValue} onChange={handleTabChange} centered>
|
||||||
|
<Tab value="system" icon={<TuneIcon />} label="System" />
|
||||||
|
<Tab value="prompt" icon={<InputIcon />} label="Prompt" />
|
||||||
|
<Tab value="resume" icon={<ArticleIcon />} label="Resume" />
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
{ statusMessage && <Message message={statusMessage} />}
|
||||||
|
<Paper elevation={3} sx={{ p: 3, m: 4, mt: 0 }}><Scrollable autoscroll sx={{display: "flex", flexGrow: 1}}>
|
||||||
|
{ tabValue === 'system' && <pre>{systemPrompt}</pre> }
|
||||||
|
{ tabValue === 'prompt' && <pre>{prompt}</pre> }
|
||||||
|
{ tabValue === 'resume' && <StyledMarkdown content={resume} />}
|
||||||
|
</Scrollable></Paper>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -257,7 +257,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
// Render desktop navigation with dropdowns
|
// Render desktop navigation with dropdowns
|
||||||
const renderDesktopNavigation = () => {
|
const renderDesktopNavigation = () => {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', width: "100%", alignItems: 'center', justifyContent: "space-between"}}>
|
<Box sx={{ display: 'flex', width: "100%", alignItems: 'center', justifyContent: "space-between", "& > :last-of-type": { marginRight: "auto"} }}>
|
||||||
{navigationItems.map((item, index) => {
|
{navigationItems.map((item, index) => {
|
||||||
const hasChildren = item.children && item.children.length > 0;
|
const hasChildren = item.children && item.children.length > 0;
|
||||||
const isActive = isCurrentPath(item) || hasActiveChild(item);
|
const isActive = isCurrentPath(item) || hasActiveChild(item);
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
interface LoginRequiredProps {
|
interface LoginRequiredProps {
|
||||||
asset: string;
|
asset: string;
|
||||||
}
|
}
|
||||||
const LoginRequired = (props: LoginRequiredProps) => {
|
const LoginRequired = (props: LoginRequiredProps) => {
|
||||||
const { asset } = props;
|
const { asset } = props;
|
||||||
|
@ -228,13 +228,6 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
// If no user is logged in, show message
|
|
||||||
if (!user?.id) {
|
|
||||||
return (
|
|
||||||
<LoginRequired asset="candidate analysis" />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: "flex", flexDirection: "column",
|
display: "flex", flexDirection: "column",
|
||||||
|
@ -909,6 +909,14 @@ class ApiClient {
|
|||||||
return this.streamify<Types.DocumentMessage>(`/jobs/requirements/${jobId}`, null, streamingOptions);
|
return this.streamify<Types.DocumentMessage>(`/jobs/requirements/${jobId}`, null, streamingOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateResume(candidateId: string, skills: Types.SkillAssessment[], streamingOptions?: StreamingOptions<Types.ChatMessageResume>): StreamingResponse<Types.ChatMessageResume> {
|
||||||
|
const body = JSON.stringify(skills);
|
||||||
|
streamingOptions = {
|
||||||
|
...streamingOptions,
|
||||||
|
headers: this.defaultHeaders,
|
||||||
|
};
|
||||||
|
return this.streamify<Types.ChatMessageResume>(`/candidates/${candidateId}/generate-resume`, body, streamingOptions);
|
||||||
|
}
|
||||||
candidateMatchForRequirement(candidate_id: string, requirement: string,
|
candidateMatchForRequirement(candidate_id: string, requirement: string,
|
||||||
streamingOptions?: StreamingOptions<Types.ChatMessageSkillAssessment>)
|
streamingOptions?: StreamingOptions<Types.ChatMessageSkillAssessment>)
|
||||||
: StreamingResponse<Types.ChatMessageSkillAssessment> {
|
: StreamingResponse<Types.ChatMessageSkillAssessment> {
|
||||||
|
@ -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-09T17:45:24.922154
|
// Generated on: 2025-06-09T20:36:06.432367
|
||||||
// DO NOT EDIT MANUALLY - This file is auto-generated
|
// DO NOT EDIT MANUALLY - This file is auto-generated
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
@ -19,7 +19,7 @@ export type ApiStatusType = "streaming" | "status" | "done" | "error";
|
|||||||
|
|
||||||
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" | "skill_match";
|
export type ChatContextType = "job_search" | "job_requirements" | "candidate_chat" | "interview_prep" | "resume_review" | "general" | "generate_persona" | "generate_profile" | "generate_resume" | "generate_image" | "rag_search" | "skill_match";
|
||||||
|
|
||||||
export type ChatSenderType = "user" | "assistant" | "system" | "information" | "warning" | "error";
|
export type ChatSenderType = "user" | "assistant" | "system" | "information" | "warning" | "error";
|
||||||
|
|
||||||
@ -282,7 +282,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" | "skill_match";
|
type: "job_search" | "job_requirements" | "candidate_chat" | "interview_prep" | "resume_review" | "general" | "generate_persona" | "generate_profile" | "generate_resume" | "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>;
|
||||||
@ -341,6 +341,20 @@ export interface ChatMessageRagSearch {
|
|||||||
content: Array<ChromaDBGetResponse>;
|
content: Array<ChromaDBGetResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChatMessageResume {
|
||||||
|
id?: string;
|
||||||
|
sessionId: string;
|
||||||
|
senderId?: string;
|
||||||
|
status: "streaming" | "status" | "done" | "error";
|
||||||
|
type: "binary" | "text" | "json";
|
||||||
|
timestamp?: Date;
|
||||||
|
role: "user" | "assistant" | "system" | "information" | "warning" | "error";
|
||||||
|
content: string;
|
||||||
|
tunables?: Tunables;
|
||||||
|
metadata: ChatMessageMetaData;
|
||||||
|
resume: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ChatMessageSkillAssessment {
|
export interface ChatMessageSkillAssessment {
|
||||||
id?: string;
|
id?: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@ -1267,6 +1281,19 @@ export function convertChatMessageRagSearchFromApi(data: any): ChatMessageRagSea
|
|||||||
timestamp: data.timestamp ? new Date(data.timestamp) : undefined,
|
timestamp: data.timestamp ? new Date(data.timestamp) : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Convert ChatMessageResume from API response, parsing date fields
|
||||||
|
* Date fields: timestamp
|
||||||
|
*/
|
||||||
|
export function convertChatMessageResumeFromApi(data: any): ChatMessageResume {
|
||||||
|
if (!data) return data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
// Convert timestamp from ISO string to Date
|
||||||
|
timestamp: data.timestamp ? new Date(data.timestamp) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Convert ChatMessageSkillAssessment from API response, parsing date fields
|
* Convert ChatMessageSkillAssessment from API response, parsing date fields
|
||||||
* Date fields: timestamp
|
* Date fields: timestamp
|
||||||
@ -1678,6 +1705,8 @@ export function convertFromApi<T>(data: any, modelType: string): T {
|
|||||||
return convertChatMessageErrorFromApi(data) as T;
|
return convertChatMessageErrorFromApi(data) as T;
|
||||||
case 'ChatMessageRagSearch':
|
case 'ChatMessageRagSearch':
|
||||||
return convertChatMessageRagSearchFromApi(data) as T;
|
return convertChatMessageRagSearchFromApi(data) as T;
|
||||||
|
case 'ChatMessageResume':
|
||||||
|
return convertChatMessageResumeFromApi(data) as T;
|
||||||
case 'ChatMessageSkillAssessment':
|
case 'ChatMessageSkillAssessment':
|
||||||
return convertChatMessageSkillAssessmentFromApi(data) as T;
|
return convertChatMessageSkillAssessmentFromApi(data) as T;
|
||||||
case 'ChatMessageStatus':
|
case 'ChatMessageStatus':
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from pydantic import model_validator, Field, BaseModel
|
from pydantic import model_validator, Field, BaseModel # type: ignore
|
||||||
from typing import (
|
from typing import (
|
||||||
Dict,
|
Dict,
|
||||||
Literal,
|
Literal,
|
||||||
@ -23,7 +23,7 @@ import asyncio
|
|||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
from names_dataset import NameDataset, NameWrapper
|
from names_dataset import NameDataset, NameWrapper # type: ignore
|
||||||
|
|
||||||
from .base import Agent, agent_registry, LLMMessage
|
from .base import Agent, agent_registry, LLMMessage
|
||||||
from models import ApiActivityType, Candidate, ChatMessage, ChatMessageError, ChatMessageMetaData, ApiMessageType, ChatMessageStatus, ChatMessageStreaming, ChatMessageUser, ChatOptions, ChatSenderType, ApiStatusType, Tunables
|
from models import ApiActivityType, Candidate, ChatMessage, ChatMessageError, ChatMessageMetaData, ApiMessageType, ChatMessageStatus, ChatMessageStreaming, ChatMessageUser, ChatOptions, ChatSenderType, ApiStatusType, Tunables
|
||||||
@ -128,7 +128,7 @@ logger = logging.getLogger(__name__)
|
|||||||
class EthnicNameGenerator:
|
class EthnicNameGenerator:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
try:
|
try:
|
||||||
from names_dataset import NameDataset
|
from names_dataset import NameDataset # type: ignore
|
||||||
self.nd = NameDataset()
|
self.nd = NameDataset()
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.error("NameDataset not available. Please install: pip install names-dataset")
|
logger.error("NameDataset not available. Please install: pip install names-dataset")
|
||||||
@ -292,7 +292,7 @@ class EthnicNameGenerator:
|
|||||||
return names
|
return names
|
||||||
|
|
||||||
class GeneratePersona(Agent):
|
class GeneratePersona(Agent):
|
||||||
agent_type: Literal["generate_persona"] = "generate_persona"
|
agent_type: Literal["generate_persona"] = "generate_persona" # type: ignore
|
||||||
_agent_type: ClassVar[str] = agent_type # Add this for registration
|
_agent_type: ClassVar[str] = agent_type # Add this for registration
|
||||||
agent_persist: bool = False
|
agent_persist: bool = False
|
||||||
|
|
||||||
|
198
src/backend/agents/generate_resume.py
Normal file
198
src/backend/agents/generate_resume.py
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from pydantic import model_validator, Field # type: ignore
|
||||||
|
from typing import (
|
||||||
|
Dict,
|
||||||
|
Literal,
|
||||||
|
ClassVar,
|
||||||
|
Any,
|
||||||
|
AsyncGenerator,
|
||||||
|
List,
|
||||||
|
Optional
|
||||||
|
# override
|
||||||
|
) # NOTE: You must import Optional for late binding to work
|
||||||
|
import inspect
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import traceback
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
import asyncio
|
||||||
|
import numpy as np # type: ignore
|
||||||
|
|
||||||
|
from logger import logger
|
||||||
|
from .base import Agent, agent_registry
|
||||||
|
from models import (ApiActivityType, ApiStatusType, Candidate, ChatMessage, ChatMessageError, ChatMessageResume, ChatMessageStatus, JobRequirements, JobRequirementsMessage, SkillAssessment, SkillStrength, Tunables)
|
||||||
|
|
||||||
|
class GenerateResume(Agent):
|
||||||
|
agent_type: Literal["generate_resume"] = "generate_resume" # type: ignore
|
||||||
|
_agent_type: ClassVar[str] = agent_type # Add this for registration
|
||||||
|
|
||||||
|
def generate_resume_prompt(
|
||||||
|
self,
|
||||||
|
skills: List[SkillAssessment]
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Generate a professional resume based on skill assessment results
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- candidate_name (str): The candidate's full name
|
||||||
|
- candidate_contact_info (dict): Contact details like email, phone, location
|
||||||
|
- skill_assessment_results (list): List of individual skill assessment results from LLM queries
|
||||||
|
- original_resume (str): Original resume text for reference
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- str: System prompt for generating a professional resume
|
||||||
|
"""
|
||||||
|
if not self.user:
|
||||||
|
raise ValueError("User must be bound to agent")
|
||||||
|
|
||||||
|
# Extract and organize skill assessment data
|
||||||
|
skills_by_strength = {
|
||||||
|
SkillStrength.STRONG: [],
|
||||||
|
SkillStrength.MODERATE: [],
|
||||||
|
SkillStrength.WEAK: [],
|
||||||
|
SkillStrength.NONE: []
|
||||||
|
}
|
||||||
|
|
||||||
|
experience_evidence = {}
|
||||||
|
|
||||||
|
# Process each skill assessment
|
||||||
|
for assessment in skills:
|
||||||
|
skill = assessment.skill
|
||||||
|
strength = assessment.evidence_strength
|
||||||
|
|
||||||
|
# Add to appropriate strength category
|
||||||
|
if skill and strength in skills_by_strength:
|
||||||
|
skills_by_strength[strength].append(skill)
|
||||||
|
|
||||||
|
# Collect experience evidence
|
||||||
|
for evidence in assessment.evidence_details:
|
||||||
|
source = evidence.source
|
||||||
|
if source:
|
||||||
|
if source not in experience_evidence:
|
||||||
|
experience_evidence[source] = []
|
||||||
|
|
||||||
|
experience_evidence[source].append(
|
||||||
|
{
|
||||||
|
"skill": skill,
|
||||||
|
"quote": evidence.quote,
|
||||||
|
"context": evidence.context
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build the system prompt
|
||||||
|
system_prompt = f"""You are a professional resume writer with expertise in highlighting candidate strengths and experiences.
|
||||||
|
Create a polished, concise, and ATS-friendly resume for the candidate based on the assessment data provided.
|
||||||
|
|
||||||
|
## CANDIDATE INFORMATION:
|
||||||
|
Name: {self.user.full_name}
|
||||||
|
Email: {self.user.email or 'N/A'}
|
||||||
|
Phone: {self.user.phone or 'N/A'}
|
||||||
|
{f'Location: {json.dumps(self.user.location.model_dump())}' if self.user.location else ''}
|
||||||
|
|
||||||
|
## SKILL ASSESSMENT RESULTS:
|
||||||
|
"""
|
||||||
|
|
||||||
|
if len(skills_by_strength[SkillStrength.STRONG]):
|
||||||
|
system_prompt += f"""\
|
||||||
|
|
||||||
|
### Strong Skills (prominent in resume):
|
||||||
|
{", ".join(skills_by_strength[SkillStrength.STRONG])}
|
||||||
|
"""
|
||||||
|
|
||||||
|
if len(skills_by_strength[SkillStrength.MODERATE]):
|
||||||
|
system_prompt += f"""\
|
||||||
|
|
||||||
|
### Moderate Skills (demonstrated in resume):
|
||||||
|
{", ".join(skills_by_strength[SkillStrength.MODERATE])}
|
||||||
|
"""
|
||||||
|
|
||||||
|
if len(skills_by_strength[SkillStrength.WEAK]):
|
||||||
|
system_prompt += f"""\
|
||||||
|
|
||||||
|
### Weaker Skills (mentioned or implied):
|
||||||
|
{", ".join(skills_by_strength[SkillStrength.WEAK])}
|
||||||
|
"""
|
||||||
|
|
||||||
|
system_prompt += """\
|
||||||
|
|
||||||
|
## EXPERIENCE EVIDENCE:
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Add experience evidence by source/position
|
||||||
|
for source, evidences in experience_evidence.items():
|
||||||
|
system_prompt += f"\n### {source}:\n"
|
||||||
|
for evidence in evidences:
|
||||||
|
system_prompt += f"- {evidence['skill']}: {evidence['context']}\n"
|
||||||
|
|
||||||
|
# Add instructions for the resume creation
|
||||||
|
system_prompt += """\
|
||||||
|
|
||||||
|
## INSTRUCTIONS:
|
||||||
|
|
||||||
|
1. Create a professional resume that emphasizes the candidate's strongest skills and most relevant experiences.
|
||||||
|
2. Format the resume in a clean, concise, and modern style that will pass ATS systems.
|
||||||
|
3. Include these sections:
|
||||||
|
- Professional Summary (highlight strongest skills and experience level)
|
||||||
|
- Skills (organized by strength)
|
||||||
|
- Professional Experience (focus on achievements and evidence of the skill)
|
||||||
|
- If present in material, provide an Education section
|
||||||
|
- If present in material, provide a Certifications section
|
||||||
|
- Additional sections as appropriate
|
||||||
|
4. Use action verbs and quantifiable achievements where possible.
|
||||||
|
5. Maintain a professional tone throughout.
|
||||||
|
6. Be concise and impactful - the resume should be 1-2 pages MAXIMUM.
|
||||||
|
7. Ensure all information is accurate to the original resume - do not embellish or fabricate experiences.
|
||||||
|
|
||||||
|
## OUTPUT FORMAT:
|
||||||
|
Provide the resume in clean markdown format, ready for the candidate to use.
|
||||||
|
|
||||||
|
"""
|
||||||
|
prompt = "Create a tailored professional resume that highlights candidate's skills and experience most relevant to the job requirements. Format it in clean, ATS-friendly markdown. Provide ONLY the resume with no commentary before or after."
|
||||||
|
return system_prompt, prompt
|
||||||
|
|
||||||
|
async def generate_resume(
|
||||||
|
self, llm: Any, model: str, session_id: str, skills: List[SkillAssessment]
|
||||||
|
) -> AsyncGenerator[ChatMessage | ChatMessageError, None]:
|
||||||
|
# Stage 1A: Analyze job requirements
|
||||||
|
status_message = ChatMessageStatus(
|
||||||
|
session_id=session_id,
|
||||||
|
content = f"Analyzing job requirements",
|
||||||
|
activity=ApiActivityType.THINKING
|
||||||
|
)
|
||||||
|
yield status_message
|
||||||
|
|
||||||
|
system_prompt, prompt = self.generate_resume_prompt(skills=skills)
|
||||||
|
|
||||||
|
generated_message = None
|
||||||
|
async for generated_message in self.llm_one_shot(llm=llm, model=model, session_id=session_id, prompt=prompt, system_prompt=system_prompt):
|
||||||
|
if generated_message.status == ApiStatusType.ERROR:
|
||||||
|
yield generated_message
|
||||||
|
return
|
||||||
|
if generated_message.status != ApiStatusType.DONE:
|
||||||
|
yield generated_message
|
||||||
|
|
||||||
|
if not generated_message:
|
||||||
|
error_message = ChatMessageError(
|
||||||
|
session_id=session_id,
|
||||||
|
content="Job requirements analysis failed to generate a response."
|
||||||
|
)
|
||||||
|
logger.error(f"⚠️ {error_message.content}")
|
||||||
|
yield error_message
|
||||||
|
return
|
||||||
|
|
||||||
|
resume_message = ChatMessageResume(
|
||||||
|
session_id=session_id,
|
||||||
|
status=ApiStatusType.DONE,
|
||||||
|
content="Resume generation completed successfully.",
|
||||||
|
metadata=generated_message.metadata,
|
||||||
|
resume=generated_message.content,
|
||||||
|
prompt=prompt,
|
||||||
|
system_prompt=system_prompt,
|
||||||
|
)
|
||||||
|
yield resume_message
|
||||||
|
logger.info(f"✅ Resume generation completed successfully.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Register the base agent
|
||||||
|
agent_registry.register(GenerateResume._agent_type, GenerateResume)
|
@ -1,5 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from pydantic import model_validator, Field
|
from pydantic import model_validator, Field # type: ignore
|
||||||
from typing import (
|
from typing import (
|
||||||
Dict,
|
Dict,
|
||||||
Literal,
|
Literal,
|
||||||
@ -16,7 +16,7 @@ import json
|
|||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
import asyncio
|
import asyncio
|
||||||
import numpy as np
|
import numpy as np # type: ignore
|
||||||
|
|
||||||
from .base import Agent, agent_registry, LLMMessage
|
from .base import Agent, agent_registry, LLMMessage
|
||||||
from models import ApiActivityType, Candidate, ChatMessage, ChatMessageError, ChatMessageMetaData, ApiMessageType, ChatMessageStatus, ChatMessageUser, ChatOptions, ChatSenderType, ApiStatusType, JobRequirements, JobRequirementsMessage, Tunables
|
from models import ApiActivityType, Candidate, ChatMessage, ChatMessageError, ChatMessageMetaData, ApiMessageType, ChatMessageStatus, ChatMessageUser, ChatOptions, ChatSenderType, ApiStatusType, JobRequirements, JobRequirementsMessage, Tunables
|
||||||
@ -26,7 +26,7 @@ import defines
|
|||||||
import backstory_traceback as traceback
|
import backstory_traceback as traceback
|
||||||
|
|
||||||
class JobRequirementsAgent(Agent):
|
class JobRequirementsAgent(Agent):
|
||||||
agent_type: Literal["job_requirements"] = "job_requirements"
|
agent_type: Literal["job_requirements"] = "job_requirements" # type: ignore
|
||||||
_agent_type: ClassVar[str] = agent_type # Add this for registration
|
_agent_type: ClassVar[str] = agent_type # Add this for registration
|
||||||
|
|
||||||
# Stage 1A: Job Analysis Implementation
|
# Stage 1A: Job Analysis Implementation
|
||||||
|
@ -170,8 +170,8 @@ JSON RESPONSE:"""
|
|||||||
yield error_message
|
yield error_message
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info(f"🔍 RAG content retrieved: {len(rag_message.content)} bytes")
|
|
||||||
rag_context = self.get_rag_context(rag_message)
|
rag_context = self.get_rag_context(rag_message)
|
||||||
|
logger.info(f"🔍 RAG content retrieved {len(rag_context)} bytes of context")
|
||||||
system_prompt, prompt = self.generate_skill_assessment_prompt(skill=prompt, rag_context=rag_context)
|
system_prompt, prompt = self.generate_skill_assessment_prompt(skill=prompt, rag_context=rag_context)
|
||||||
|
|
||||||
skill_message = None
|
skill_message = None
|
||||||
|
@ -272,9 +272,8 @@ class RedisDatabase:
|
|||||||
"""Cache skill match assessment"""
|
"""Cache skill match assessment"""
|
||||||
try:
|
try:
|
||||||
# Cache for 1 hour by default
|
# Cache for 1 hour by default
|
||||||
await self.redis.setex(
|
await self.redis.set(
|
||||||
cache_key,
|
cache_key,
|
||||||
3600,
|
|
||||||
json.dumps(assessment.model_dump(mode='json', by_alias=True), default=str) # Serialize with datetime handling
|
json.dumps(assessment.model_dump(mode='json', by_alias=True), default=str) # Serialize with datetime handling
|
||||||
)
|
)
|
||||||
logger.debug(f"💾 Skill match cached: {cache_key}")
|
logger.debug(f"💾 Skill match cached: {cache_key}")
|
||||||
@ -349,7 +348,7 @@ class RedisDatabase:
|
|||||||
try:
|
try:
|
||||||
# This assumes all candidates belonging to this user need cache invalidation
|
# This assumes all candidates belonging to this user need cache invalidation
|
||||||
# You might need to adjust the pattern based on how you associate candidates with users
|
# You might need to adjust the pattern based on how you associate candidates with users
|
||||||
pattern = f"skill_match:*"
|
pattern = f"skill_match:{user_id}:*"
|
||||||
keys = await self.redis.keys(pattern)
|
keys = await self.redis.keys(pattern)
|
||||||
|
|
||||||
# Filter keys that belong to candidates owned by this user
|
# Filter keys that belong to candidates owned by this user
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import hashlib
|
||||||
import time
|
import time
|
||||||
from fastapi import FastAPI, HTTPException, Depends, Query, Path, Body, status, APIRouter, Request, BackgroundTasks, File, UploadFile, Form# type: ignore
|
from fastapi import FastAPI, HTTPException, Depends, Query, Path, Body, status, APIRouter, Request, BackgroundTasks, File, UploadFile, Form# type: ignore
|
||||||
from fastapi.middleware.cors import CORSMiddleware # type: ignore
|
from fastapi.middleware.cors import CORSMiddleware # type: ignore
|
||||||
@ -75,7 +76,7 @@ import agents
|
|||||||
# =============================
|
# =============================
|
||||||
from models import (
|
from models import (
|
||||||
# API
|
# API
|
||||||
MOCK_UUID, ApiActivityType, ChatMessageError, ChatMessageSkillAssessment, ChatMessageStatus, ChatMessageStreaming, ChatMessageUser, DocumentMessage, DocumentOptions, Job, JobRequirementsMessage, LoginRequest, CreateCandidateRequest, CreateEmployerRequest,
|
MOCK_UUID, ApiActivityType, ChatMessageError, ChatMessageResume, ChatMessageSkillAssessment, ChatMessageStatus, ChatMessageStreaming, ChatMessageUser, DocumentMessage, DocumentOptions, Job, JobRequirements, JobRequirementsMessage, LoginRequest, CreateCandidateRequest, CreateEmployerRequest,
|
||||||
|
|
||||||
# User models
|
# User models
|
||||||
Candidate, Employer, BaseUserWithType, BaseUser, Guest, Authentication, AuthResponse, CandidateAI,
|
Candidate, Employer, BaseUserWithType, BaseUser, Guest, Authentication, AuthResponse, CandidateAI,
|
||||||
@ -4518,17 +4519,18 @@ async def get_candidate_skill_match(
|
|||||||
candidate = Candidate.model_validate(candidate_data)
|
candidate = Candidate.model_validate(candidate_data)
|
||||||
|
|
||||||
# Create cache key for this specific candidate + requirement combination
|
# Create cache key for this specific candidate + requirement combination
|
||||||
cache_key = f"skill_match:{candidate_id}:{hash(requirement)}"
|
requirement_hash = hashlib.md5(requirement.encode()).hexdigest()[:8]
|
||||||
|
cache_key = f"skill_match:{candidate.id}:{requirement_hash}"
|
||||||
|
|
||||||
# Get cached assessment if it exists
|
# Get cached assessment if it exists
|
||||||
assessment : SkillAssessment | None = await database.get_cached_skill_match(cache_key)
|
assessment : SkillAssessment | None = await database.get_cached_skill_match(cache_key)
|
||||||
|
|
||||||
# Get the latest RAG data update time for the current user
|
|
||||||
user_rag_update_time = await database.get_user_rag_update_time(current_user.id)
|
|
||||||
|
|
||||||
# Determine if we need to regenerate the assessment
|
# Determine if we need to regenerate the assessment
|
||||||
cached_date = None
|
cached_date = None
|
||||||
if assessment:
|
if assessment:
|
||||||
|
# Get the latest RAG data update time for the current user
|
||||||
|
user_rag_update_time = await database.get_user_rag_update_time(current_user.id)
|
||||||
|
|
||||||
updated = assessment.updated_at if "updated_at" in assessment else assessment.created_at
|
updated = assessment.updated_at if "updated_at" in assessment else assessment.created_at
|
||||||
# Check if cached result is still valid
|
# Check if cached result is still valid
|
||||||
# Regenerate if user's RAG data was updated after cache date
|
# Regenerate if user's RAG data was updated after cache date
|
||||||
@ -4537,6 +4539,8 @@ async def get_candidate_skill_match(
|
|||||||
assessment = None
|
assessment = None
|
||||||
else:
|
else:
|
||||||
cached_date = updated
|
cached_date = updated
|
||||||
|
else:
|
||||||
|
logger.info(f"💾 No cached skill match data: {cache_key}, {candidate.id}, {requirement}")
|
||||||
|
|
||||||
if assessment:
|
if assessment:
|
||||||
logger.info(f"✅ Found cached skill match for candidate {candidate.username} against requirement: {requirement}")
|
logger.info(f"✅ Found cached skill match for candidate {candidate.username} against requirement: {requirement}")
|
||||||
@ -4657,6 +4661,249 @@ async def get_candidate_skill_match(
|
|||||||
media_type="text/event-stream"
|
media_type="text/event-stream"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@api_router.post("/candidates/job-score")
|
||||||
|
async def get_candidate_job_score(
|
||||||
|
job_requirements: JobRequirements = Body(...),
|
||||||
|
skills: List[SkillAssessment] = Body(...),
|
||||||
|
current_user = Depends(get_current_user_or_guest),
|
||||||
|
database: RedisDatabase = Depends(get_database)
|
||||||
|
) -> StreamingResponse:
|
||||||
|
# Initialize counters
|
||||||
|
required_skills_total = 0
|
||||||
|
required_skills_matched = 0
|
||||||
|
preferred_skills_total = 0
|
||||||
|
preferred_skills_matched = 0
|
||||||
|
|
||||||
|
# Count required technical skills
|
||||||
|
tech_required = job_requirements.technical_skills.required
|
||||||
|
required_skills_total += len(tech_required)
|
||||||
|
|
||||||
|
# Count preferred technical skills
|
||||||
|
tech_preferred = job_requirements.technical_skills.preferred
|
||||||
|
preferred_skills_total += len(tech_preferred)
|
||||||
|
|
||||||
|
# Count required experience
|
||||||
|
exp_required = job_requirements.experience_requirements.required
|
||||||
|
required_skills_total += len(exp_required)
|
||||||
|
|
||||||
|
# Count preferred experience
|
||||||
|
exp_preferred = job_requirements.experience_requirements.preferred
|
||||||
|
preferred_skills_total += len(exp_preferred)
|
||||||
|
|
||||||
|
# Education requirements count toward required
|
||||||
|
edu_required = job_requirements.education or []
|
||||||
|
required_skills_total += len(edu_required)
|
||||||
|
|
||||||
|
# Soft skills count toward preferred
|
||||||
|
soft_skills = job_requirements.soft_skills or []
|
||||||
|
preferred_skills_total += len(soft_skills)
|
||||||
|
|
||||||
|
# Industry knowledge counts toward preferred
|
||||||
|
certifications = job_requirements.certifications or []
|
||||||
|
preferred_skills_total += len(certifications)
|
||||||
|
|
||||||
|
preferred_attributes = job_requirements.preferred_attributes or []
|
||||||
|
preferred_skills_total += len(preferred_attributes)
|
||||||
|
|
||||||
|
# Check matches in assessment results
|
||||||
|
for assessment in skills:
|
||||||
|
evidence_found = assessment.evidence_found
|
||||||
|
evidence_strength = assessment.evidence_strength
|
||||||
|
|
||||||
|
# Consider STRONG and MODERATE evidence as matches
|
||||||
|
is_match = evidence_found and evidence_strength in ["STRONG", "MODERATE"]
|
||||||
|
|
||||||
|
if not is_match:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Loop through each of the job requirements categories
|
||||||
|
# and see if the skill matches the required or preferred skills
|
||||||
|
if assessment.skill in tech_required:
|
||||||
|
required_skills_matched += 1
|
||||||
|
elif assessment.skill in tech_preferred:
|
||||||
|
preferred_skills_matched += 1
|
||||||
|
elif assessment.skill in exp_required:
|
||||||
|
required_skills_matched += 1
|
||||||
|
elif assessment.skill in exp_preferred:
|
||||||
|
preferred_skills_matched += 1
|
||||||
|
elif assessment.skill in edu_required:
|
||||||
|
required_skills_matched += 1
|
||||||
|
elif assessment.skill in soft_skills:
|
||||||
|
preferred_skills_matched += 1
|
||||||
|
elif assessment.skill in certifications:
|
||||||
|
preferred_skills_matched += 1
|
||||||
|
elif assessment.skill in preferred_attributes:
|
||||||
|
preferred_skills_matched += 1
|
||||||
|
# If no skills were found, return empty statistics
|
||||||
|
if required_skills_total == 0 and preferred_skills_total == 0:
|
||||||
|
return create_success_response({
|
||||||
|
"required_skills": {
|
||||||
|
"total": 0,
|
||||||
|
"matched": 0,
|
||||||
|
"percentage": 0.0,
|
||||||
|
},
|
||||||
|
"preferred_skills": {
|
||||||
|
"total": 0,
|
||||||
|
"matched": 0,
|
||||||
|
"percentage": 0.0,
|
||||||
|
},
|
||||||
|
"overall_match": {
|
||||||
|
"total": 0,
|
||||||
|
"matched": 0,
|
||||||
|
"percentage": 0.0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
# Calculate percentages
|
||||||
|
required_match_percent = (
|
||||||
|
(required_skills_matched / required_skills_total * 100)
|
||||||
|
if required_skills_total > 0
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
preferred_match_percent = (
|
||||||
|
(preferred_skills_matched / preferred_skills_total * 100)
|
||||||
|
if preferred_skills_total > 0
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
overall_total = required_skills_total + preferred_skills_total
|
||||||
|
overall_matched = required_skills_matched + preferred_skills_matched
|
||||||
|
overall_match_percent = (
|
||||||
|
(overall_matched / overall_total * 100) if overall_total > 0 else 0
|
||||||
|
)
|
||||||
|
|
||||||
|
return create_success_response({
|
||||||
|
"required_skills": {
|
||||||
|
"total": required_skills_total,
|
||||||
|
"matched": required_skills_matched,
|
||||||
|
"percentage": round(required_match_percent, 1),
|
||||||
|
},
|
||||||
|
"preferred_skills": {
|
||||||
|
"total": preferred_skills_total,
|
||||||
|
"matched": preferred_skills_matched,
|
||||||
|
"percentage": round(preferred_match_percent, 1),
|
||||||
|
},
|
||||||
|
"overall_match": {
|
||||||
|
"total": overall_total,
|
||||||
|
"matched": overall_matched,
|
||||||
|
"percentage": round(overall_match_percent, 1),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
@api_router.post("/candidates/{candidate_id}/generate-resume")
|
||||||
|
async def generate_resume(
|
||||||
|
candidate_id: str = Path(...),
|
||||||
|
skills: List[SkillAssessment] = Body(...),
|
||||||
|
current_user = Depends(get_current_user_or_guest),
|
||||||
|
database: RedisDatabase = Depends(get_database)
|
||||||
|
) -> StreamingResponse:
|
||||||
|
|
||||||
|
"""Get skill match for a candidate against a requirement with caching"""
|
||||||
|
async def message_stream_generator():
|
||||||
|
candidate_data = await database.get_candidate(candidate_id)
|
||||||
|
if not candidate_data:
|
||||||
|
error_message = ChatMessageError(
|
||||||
|
sessionId=MOCK_UUID, # No session ID for document uploads
|
||||||
|
content=f"Candidate with ID '{candidate_id}' not found"
|
||||||
|
)
|
||||||
|
yield error_message
|
||||||
|
return
|
||||||
|
|
||||||
|
candidate = Candidate.model_validate(candidate_data)
|
||||||
|
|
||||||
|
logger.info(f"🔍 Generating resume for candidate {candidate.username}")
|
||||||
|
|
||||||
|
async with entities.get_candidate_entity(candidate=candidate) as candidate_entity:
|
||||||
|
agent = candidate_entity.get_or_create_agent(agent_type=ChatContextType.GENERATE_RESUME)
|
||||||
|
if not agent:
|
||||||
|
error_message = ChatMessageError(
|
||||||
|
sessionId=MOCK_UUID, # No session ID for document uploads
|
||||||
|
content=f"No skill match agent found for this candidate"
|
||||||
|
)
|
||||||
|
yield error_message
|
||||||
|
return
|
||||||
|
|
||||||
|
# Generate new skill match
|
||||||
|
final_message = None
|
||||||
|
async for generated_message in agent.generate_resume(
|
||||||
|
llm=llm_manager.get_llm(),
|
||||||
|
model=defines.model,
|
||||||
|
session_id=MOCK_UUID,
|
||||||
|
skills=skills,
|
||||||
|
):
|
||||||
|
if generated_message.status == ApiStatusType.ERROR:
|
||||||
|
logger.error(f"❌ AI generation error: {generated_message.content}")
|
||||||
|
yield f"data: {json.dumps({'status': 'error'})}\n\n"
|
||||||
|
return
|
||||||
|
|
||||||
|
# If the message is not done, convert it to a ChatMessageBase to remove
|
||||||
|
# metadata and other unnecessary fields for streaming
|
||||||
|
if generated_message.status != ApiStatusType.DONE:
|
||||||
|
if not isinstance(generated_message, ChatMessageStreaming) and not isinstance(generated_message, ChatMessageStatus):
|
||||||
|
raise TypeError(
|
||||||
|
f"Expected ChatMessageStreaming or ChatMessageStatus, got {type(generated_message)}"
|
||||||
|
)
|
||||||
|
yield generated_message# Convert to ChatMessageBase for streaming
|
||||||
|
|
||||||
|
# Store reference to the complete AI message
|
||||||
|
if generated_message.status == ApiStatusType.DONE:
|
||||||
|
final_message = generated_message
|
||||||
|
break
|
||||||
|
|
||||||
|
if final_message is None:
|
||||||
|
error_message = ChatMessageError(
|
||||||
|
sessionId=MOCK_UUID, # No session ID for document uploads
|
||||||
|
content=f"No skill match found for the given requirement"
|
||||||
|
)
|
||||||
|
yield error_message
|
||||||
|
return
|
||||||
|
|
||||||
|
if not isinstance(final_message, ChatMessageResume):
|
||||||
|
error_message = ChatMessageError(
|
||||||
|
sessionId=MOCK_UUID, # No session ID for document uploads
|
||||||
|
content=f"Skill match response is not valid"
|
||||||
|
)
|
||||||
|
yield error_message
|
||||||
|
return
|
||||||
|
|
||||||
|
resume : ChatMessageResume = final_message
|
||||||
|
yield resume
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
async def to_json(method):
|
||||||
|
try:
|
||||||
|
async for message in method:
|
||||||
|
json_data = message.model_dump(mode='json', by_alias=True)
|
||||||
|
json_str = json.dumps(json_data)
|
||||||
|
yield f"data: {json_str}\n\n".encode("utf-8")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(backstory_traceback.format_exc())
|
||||||
|
logger.error(f"Error in to_json conversion: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
to_json(message_stream_generator()),
|
||||||
|
media_type="text/event-stream",
|
||||||
|
headers={
|
||||||
|
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"X-Accel-Buffering": "no", # Nginx
|
||||||
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
"Access-Control-Allow-Origin": "*", # Adjust for your CORS needs
|
||||||
|
"Transfer-Encoding": "chunked",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(backstory_traceback.format_exc())
|
||||||
|
logger.error(f"❌ Document upload error: {e}")
|
||||||
|
return StreamingResponse(
|
||||||
|
iter([json.dumps(ChatMessageError(
|
||||||
|
sessionId=MOCK_UUID, # No session ID for document uploads
|
||||||
|
content="Failed to generate skill assessment"
|
||||||
|
).model_dump(mode='json', by_alias=True))]),
|
||||||
|
media_type="text/event-stream"
|
||||||
|
)
|
||||||
|
|
||||||
@rate_limited(guest_per_minute=5, user_per_minute=30, admin_per_minute=100)
|
@rate_limited(guest_per_minute=5, user_per_minute=30, admin_per_minute=100)
|
||||||
@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(
|
||||||
|
@ -78,16 +78,6 @@ class ChatSenderType(str, Enum):
|
|||||||
WARNING = "warning"
|
WARNING = "warning"
|
||||||
ERROR = "error"
|
ERROR = "error"
|
||||||
|
|
||||||
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 SkillStatus(str, Enum):
|
class SkillStatus(str, Enum):
|
||||||
PENDING = "pending"
|
PENDING = "pending"
|
||||||
COMPLETE = "complete"
|
COMPLETE = "complete"
|
||||||
@ -142,6 +132,7 @@ class ChatContextType(str, Enum):
|
|||||||
GENERAL = "general"
|
GENERAL = "general"
|
||||||
GENERATE_PERSONA = "generate_persona"
|
GENERATE_PERSONA = "generate_persona"
|
||||||
GENERATE_PROFILE = "generate_profile"
|
GENERATE_PROFILE = "generate_profile"
|
||||||
|
GENERATE_RESUME = "generate_resume"
|
||||||
GENERATE_IMAGE = "generate_image"
|
GENERATE_IMAGE = "generate_image"
|
||||||
RAG_SEARCH = "rag_search"
|
RAG_SEARCH = "rag_search"
|
||||||
SKILL_MATCH = "skill_match"
|
SKILL_MATCH = "skill_match"
|
||||||
@ -667,6 +658,16 @@ class GuestCleanupRequest(BaseModel):
|
|||||||
"populate_by_name": True
|
"populate_by_name": True
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 JobRequirements(BaseModel):
|
class JobRequirements(BaseModel):
|
||||||
technical_skills: Requirements = Field(..., alias="technicalSkills")
|
technical_skills: Requirements = Field(..., alias="technicalSkills")
|
||||||
experience_requirements: Requirements = Field(..., alias="experienceRequirements")
|
experience_requirements: Requirements = Field(..., alias="experienceRequirements")
|
||||||
@ -1010,6 +1011,13 @@ class ChatMessageSkillAssessment(ChatMessageUser):
|
|||||||
metadata: ChatMessageMetaData = Field(default=ChatMessageMetaData())
|
metadata: ChatMessageMetaData = Field(default=ChatMessageMetaData())
|
||||||
skill_assessment: SkillAssessment = Field(..., alias="skillAssessment")
|
skill_assessment: SkillAssessment = Field(..., alias="skillAssessment")
|
||||||
|
|
||||||
|
class ChatMessageResume(ChatMessageUser):
|
||||||
|
role: ChatSenderType = ChatSenderType.ASSISTANT
|
||||||
|
metadata: ChatMessageMetaData = Field(default=ChatMessageMetaData())
|
||||||
|
resume: str = Field(..., alias="resume")
|
||||||
|
system_prompt: Optional[str] = Field(None, alias="systemPrompt")
|
||||||
|
prompt: Optional[str] = Field(None, alias="prompt")
|
||||||
|
|
||||||
class GPUInfo(BaseModel):
|
class GPUInfo(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
memory: int
|
memory: int
|
||||||
|
@ -391,8 +391,7 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
# Log diagnostics
|
# Log diagnostics
|
||||||
logging.info(f"Input text: {text}")
|
logging.debug(f"Embedding shape: {embedding.shape}, First 5 values: {embedding[:5]}")
|
||||||
logging.info(f"Embedding shape: {embedding.shape}, First 5 values: {embedding[:5]}")
|
|
||||||
|
|
||||||
# Check for invalid embeddings
|
# Check for invalid embeddings
|
||||||
if embedding.size == 0 or np.any(np.isnan(embedding)) or np.any(np.isinf(embedding)):
|
if embedding.size == 0 or np.any(np.isnan(embedding)) or np.any(np.isinf(embedding)):
|
||||||
@ -402,12 +401,12 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
|
|||||||
# Check normalization
|
# Check normalization
|
||||||
norm = np.linalg.norm(embedding)
|
norm = np.linalg.norm(embedding)
|
||||||
is_normalized = np.allclose(norm, 1.0, atol=1e-3)
|
is_normalized = np.allclose(norm, 1.0, atol=1e-3)
|
||||||
logging.info(f"Embedding norm: {norm}, Is normalized: {is_normalized}")
|
logging.debug(f"Embedding norm: {norm}, Is normalized: {is_normalized}")
|
||||||
|
|
||||||
# Normalize if needed
|
# Normalize if needed
|
||||||
if not is_normalized:
|
if not is_normalized:
|
||||||
embedding = embedding / norm
|
embedding = embedding / norm
|
||||||
logging.info("Embedding normalized manually.")
|
logging.debug("Embedding normalized manually.")
|
||||||
|
|
||||||
return embedding
|
return embedding
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user