Added interactive resume chat with agentic backend
This commit is contained in:
parent
574d040492
commit
2ac5f5f078
155
frontend/src/components/ui/ResumePreview.css
Normal file
155
frontend/src/components/ui/ResumePreview.css
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
|
||||||
|
/* A4 Portrait simulation for MuiMarkdown */
|
||||||
|
.a4-document .MuiTypography-root {
|
||||||
|
font-family: 'Roboto', 'Times New Roman', serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a4-document.with-margins {
|
||||||
|
/* Page break lines - repeating dotted lines every A4 height */
|
||||||
|
background-image:
|
||||||
|
repeating-linear-gradient(
|
||||||
|
#ddd,
|
||||||
|
#ddd 12mm,
|
||||||
|
transparent calc(12mm + 1px),
|
||||||
|
transparent calc(285mm - 1px), /* 297mm - 8mm top/bottom margins */
|
||||||
|
#ddd calc(285mm),
|
||||||
|
#ddd 297mm
|
||||||
|
);
|
||||||
|
background-size: 100% 297mm;
|
||||||
|
background-repeat: repeat-y;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a4-document {
|
||||||
|
/* display: flex; */
|
||||||
|
/* position: relative; */
|
||||||
|
/* A4 dimensions: 210mm x 297mm */
|
||||||
|
width: 210mm;
|
||||||
|
min-height: 297mm;
|
||||||
|
|
||||||
|
/* Alternative pixel-based approach (96 DPI) */
|
||||||
|
/* width: 794px; */
|
||||||
|
/* height: 1123px; */
|
||||||
|
|
||||||
|
/* Document styling */
|
||||||
|
background: white;
|
||||||
|
padding: 12mm; /* 1/4" margins all around */
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
|
||||||
|
/* Typography for document feel */
|
||||||
|
font-family: 'Roboto', 'Times New Roman', serif;
|
||||||
|
font-size: 12pt;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
/* Ensure proper page breaks for printing */
|
||||||
|
page-break-after: always;
|
||||||
|
|
||||||
|
/* Prevent content overflow */
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* overflow: hidden; */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container to center the document */
|
||||||
|
.document-container {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: fit-content;
|
||||||
|
height: fit-content;
|
||||||
|
background-color: #f5f5f5; /* Light gray background for contrast */
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
.a4-document {
|
||||||
|
width: 95vw;
|
||||||
|
height: auto;
|
||||||
|
min-height: 134vw; /* Maintains A4 aspect ratio (297/210 ≈ 1.414) */
|
||||||
|
margin: 10px auto;
|
||||||
|
padding: 6vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print styles */
|
||||||
|
@media print {
|
||||||
|
.document-container {
|
||||||
|
background: none;
|
||||||
|
padding: 0mm !important;
|
||||||
|
margin: 0mm !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a4-document {
|
||||||
|
width: 210mm;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
border: none;
|
||||||
|
page-break-after: always;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Additional MuiMarkdown specific adjustments */
|
||||||
|
.a4-document h1,
|
||||||
|
.a4-document h2,
|
||||||
|
.a4-document h3,
|
||||||
|
.a4-document h4,
|
||||||
|
.a4-document h5,
|
||||||
|
.a4-document h6 {
|
||||||
|
font-size: 1em;
|
||||||
|
margin-top: 0.25em;
|
||||||
|
margin-bottom: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Put after above so they take precedence */
|
||||||
|
.a4-document h1,
|
||||||
|
.a4-document h2 {
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.a4-document p {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a4-document ul,
|
||||||
|
.a4-document ol {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a4-document blockquote {
|
||||||
|
margin: 1em 0;
|
||||||
|
padding-left: 1em;
|
||||||
|
border-left: 3px solid #ccc;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a4-document code {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a4-document pre {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.BackstoryResumeHeader {
|
||||||
|
gap: 1rem;
|
||||||
|
display: flex;
|
||||||
|
/* flex-direction: column; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.BackstoryResumeHeader p {
|
||||||
|
/* border: 3px solid purple; */
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
532
frontend/src/components/ui/ResumePreview.tsx
Normal file
532
frontend/src/components/ui/ResumePreview.tsx
Normal file
@ -0,0 +1,532 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Typography, SxProps, Theme } from '@mui/material';
|
||||||
|
import {
|
||||||
|
Email as EmailIcon,
|
||||||
|
Phone as PhoneIcon,
|
||||||
|
LocationOn as LocationIcon,
|
||||||
|
} from '@mui/icons-material';
|
||||||
|
import { parsePhoneNumberFromString } from 'libphonenumber-js';
|
||||||
|
import { StyledMarkdown } from 'components/StyledMarkdown';
|
||||||
|
import * as Types from 'types/types';
|
||||||
|
|
||||||
|
import './ResumePreview.css';
|
||||||
|
|
||||||
|
// Resume Style Definitions
|
||||||
|
export interface ResumeStyle {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
headerStyle: SxProps<Theme>;
|
||||||
|
footerStyle: SxProps<Theme>;
|
||||||
|
contentStyle: SxProps<Theme>;
|
||||||
|
markdownStyle: SxProps<Theme>;
|
||||||
|
color: {
|
||||||
|
primary: string;
|
||||||
|
secondary: string;
|
||||||
|
accent: string;
|
||||||
|
text: string;
|
||||||
|
background: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateResumeStyles = (): Record<string, ResumeStyle> => {
|
||||||
|
return {
|
||||||
|
classic: {
|
||||||
|
name: 'Classic',
|
||||||
|
description: 'Traditional, professional serif design',
|
||||||
|
headerStyle: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
fontFamily: '"Times New Roman", Times, serif',
|
||||||
|
borderBottom: '2px solid #2c3e50',
|
||||||
|
paddingBottom: 2,
|
||||||
|
marginBottom: 3,
|
||||||
|
} as SxProps<Theme>,
|
||||||
|
footerStyle: {
|
||||||
|
fontFamily: '"Times New Roman", Times, serif',
|
||||||
|
borderTop: '2px solid #2c3e50',
|
||||||
|
paddingTop: 2,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
alignContent: 'center',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
pb: 2,
|
||||||
|
mb: 2,
|
||||||
|
} as SxProps<Theme>,
|
||||||
|
contentStyle: {
|
||||||
|
fontFamily: '"Times New Roman", Times, serif',
|
||||||
|
lineHeight: 1.6,
|
||||||
|
color: '#2c3e50',
|
||||||
|
} as SxProps<Theme>,
|
||||||
|
markdownStyle: {
|
||||||
|
fontFamily: '"Times New Roman", Times, serif',
|
||||||
|
'& h1, & h2, & h3': {
|
||||||
|
fontFamily: '"Times New Roman", Times, serif',
|
||||||
|
color: '#2c3e50',
|
||||||
|
borderBottom: '1px solid #bdc3c7',
|
||||||
|
paddingBottom: 1,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
'& p, & li': {
|
||||||
|
lineHeight: 1.6,
|
||||||
|
marginBottom: 1,
|
||||||
|
},
|
||||||
|
'& ul': {
|
||||||
|
paddingLeft: 3,
|
||||||
|
},
|
||||||
|
} as SxProps<Theme>,
|
||||||
|
color: {
|
||||||
|
primary: '#2c3e50',
|
||||||
|
secondary: '#34495e',
|
||||||
|
accent: '#3498db',
|
||||||
|
text: '#2c3e50',
|
||||||
|
background: '#ffffff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modern: {
|
||||||
|
name: 'Modern',
|
||||||
|
description: 'Clean, minimalist sans-serif layout',
|
||||||
|
headerStyle: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||||
|
borderLeft: '4px solid #3498db',
|
||||||
|
paddingLeft: 2,
|
||||||
|
marginBottom: 3,
|
||||||
|
backgroundColor: '#f8f9fa',
|
||||||
|
padding: 2,
|
||||||
|
borderRadius: 1,
|
||||||
|
} as SxProps<Theme>,
|
||||||
|
footerStyle: {
|
||||||
|
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||||
|
borderLeft: '4px solid #3498db',
|
||||||
|
backgroundColor: '#f8f9fa',
|
||||||
|
paddingTop: 2,
|
||||||
|
borderRadius: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
alignContent: 'center',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
pb: 2,
|
||||||
|
mb: 2,
|
||||||
|
} as SxProps<Theme>,
|
||||||
|
contentStyle: {
|
||||||
|
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
color: '#2c3e50',
|
||||||
|
} as SxProps<Theme>,
|
||||||
|
markdownStyle: {
|
||||||
|
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||||
|
'& h1, & h2, & h3': {
|
||||||
|
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||||
|
color: '#3498db',
|
||||||
|
fontWeight: 300,
|
||||||
|
marginBottom: 1.5,
|
||||||
|
},
|
||||||
|
'& h1': {
|
||||||
|
fontSize: '1.75rem',
|
||||||
|
},
|
||||||
|
'& h2': {
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
},
|
||||||
|
'& h3': {
|
||||||
|
fontSize: '1.25rem',
|
||||||
|
},
|
||||||
|
'& p, & li': {
|
||||||
|
lineHeight: 1.5,
|
||||||
|
marginBottom: 0.75,
|
||||||
|
},
|
||||||
|
'& ul': {
|
||||||
|
paddingLeft: 2.5,
|
||||||
|
},
|
||||||
|
} as SxProps<Theme>,
|
||||||
|
color: {
|
||||||
|
primary: '#3498db',
|
||||||
|
secondary: '#2c3e50',
|
||||||
|
accent: '#e74c3c',
|
||||||
|
text: '#2c3e50',
|
||||||
|
background: '#ffffff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
creative: {
|
||||||
|
name: 'Creative',
|
||||||
|
description: 'Colorful, unique design with personality',
|
||||||
|
headerStyle: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
fontFamily: '"Montserrat", "Helvetica Neue", Arial, sans-serif',
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
color: '#ffffff',
|
||||||
|
padding: 2.5,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
marginBottom: 3,
|
||||||
|
} as SxProps<Theme>,
|
||||||
|
footerStyle: {
|
||||||
|
fontFamily: '"Montserrat", "Helvetica Neue", Arial, sans-serif',
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
color: '#ffffff',
|
||||||
|
paddingTop: 2,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
alignContent: 'center',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
pb: 2,
|
||||||
|
mb: 2,
|
||||||
|
} as SxProps<Theme>,
|
||||||
|
contentStyle: {
|
||||||
|
fontFamily: '"Open Sans", Arial, sans-serif',
|
||||||
|
lineHeight: 1.6,
|
||||||
|
color: '#444444',
|
||||||
|
} as SxProps<Theme>,
|
||||||
|
markdownStyle: {
|
||||||
|
fontFamily: '"Open Sans", Arial, sans-serif',
|
||||||
|
'& h1, & h2, & h3': {
|
||||||
|
fontFamily: '"Montserrat", "Helvetica Neue", Arial, sans-serif',
|
||||||
|
color: '#667eea',
|
||||||
|
fontWeight: 600,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
'& h1': {
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
},
|
||||||
|
'& h2': {
|
||||||
|
fontSize: '1.25rem',
|
||||||
|
},
|
||||||
|
'& h3': {
|
||||||
|
fontSize: '1.1rem',
|
||||||
|
},
|
||||||
|
'& p, & li': {
|
||||||
|
lineHeight: 1.6,
|
||||||
|
marginBottom: 1,
|
||||||
|
color: '#444444',
|
||||||
|
},
|
||||||
|
'& strong': {
|
||||||
|
color: '#764ba2',
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
'& ul': {
|
||||||
|
paddingLeft: 3,
|
||||||
|
},
|
||||||
|
} as SxProps<Theme>,
|
||||||
|
color: {
|
||||||
|
primary: '#667eea',
|
||||||
|
secondary: '#764ba2',
|
||||||
|
accent: '#f093fb',
|
||||||
|
text: '#444444',
|
||||||
|
background: '#ffffff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
corporate: {
|
||||||
|
name: 'Corporate',
|
||||||
|
description: 'Formal, structured business format',
|
||||||
|
headerStyle: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
fontFamily: '"Arial", sans-serif',
|
||||||
|
border: '2px solid #34495e',
|
||||||
|
padding: 2.5,
|
||||||
|
marginBottom: 3,
|
||||||
|
backgroundColor: '#ecf0f1',
|
||||||
|
} as SxProps<Theme>,
|
||||||
|
footerStyle: {
|
||||||
|
fontFamily: '"Arial", sans-serif',
|
||||||
|
border: '2px solid #34495e',
|
||||||
|
backgroundColor: '#ecf0f1',
|
||||||
|
paddingTop: 2,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
alignContent: 'center',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
pb: 2,
|
||||||
|
mb: 2,
|
||||||
|
} as SxProps<Theme>,
|
||||||
|
contentStyle: {
|
||||||
|
fontFamily: '"Arial", sans-serif',
|
||||||
|
lineHeight: 1.4,
|
||||||
|
color: '#2c3e50',
|
||||||
|
} as SxProps<Theme>,
|
||||||
|
markdownStyle: {
|
||||||
|
fontFamily: '"Arial", sans-serif',
|
||||||
|
'& h1, & h2, & h3': {
|
||||||
|
fontFamily: '"Arial", sans-serif',
|
||||||
|
color: '#34495e',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
letterSpacing: '1px',
|
||||||
|
marginBottom: 1.5,
|
||||||
|
borderBottom: '1px solid #bdc3c7',
|
||||||
|
paddingBottom: 0.5,
|
||||||
|
},
|
||||||
|
'& h1': {
|
||||||
|
fontSize: '1rem',
|
||||||
|
},
|
||||||
|
'& h2': {
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
},
|
||||||
|
'& h3': {
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
},
|
||||||
|
'& p, & li': {
|
||||||
|
lineHeight: 1.4,
|
||||||
|
marginBottom: 0.75,
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
},
|
||||||
|
'& ul': {
|
||||||
|
paddingLeft: 2,
|
||||||
|
},
|
||||||
|
} as SxProps<Theme>,
|
||||||
|
color: {
|
||||||
|
primary: '#34495e',
|
||||||
|
secondary: '#2c3e50',
|
||||||
|
accent: '#95a5a6',
|
||||||
|
text: '#2c3e50',
|
||||||
|
background: '#ffffff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resumeStyles: Record<string, ResumeStyle> = generateResumeStyles();
|
||||||
|
|
||||||
|
// Styled Header Component
|
||||||
|
interface StyledHeaderProps {
|
||||||
|
candidate: Types.Candidate;
|
||||||
|
style: ResumeStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledHeader: React.FC<StyledHeaderProps> = ({ candidate, style }) => {
|
||||||
|
const phone = parsePhoneNumberFromString(candidate.phone || '', 'US');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className="BackstoryResumeHeader" sx={style.headerStyle}>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
|
||||||
|
<Box sx={{ display: 'flex' }}>
|
||||||
|
<Typography
|
||||||
|
variant="h4"
|
||||||
|
sx={{
|
||||||
|
fontWeight: 'bold',
|
||||||
|
mb: 1,
|
||||||
|
color: style.name === 'creative' ? '#ffffff' : style.color.primary,
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{candidate.fullName}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{candidate.description && (
|
||||||
|
<Box sx={{ display: 'flex' }}>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{
|
||||||
|
mb: 2,
|
||||||
|
fontWeight: 300,
|
||||||
|
color: style.name === 'creative' ? '#ffffff' : style.color.secondary,
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
fontSize: '0.8rem !important',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{candidate.description}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
alignContent: 'center',
|
||||||
|
flexGrow: 1,
|
||||||
|
minWidth: 'fit-content',
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{candidate.email && (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', m: 0, p: 0 }}>
|
||||||
|
<EmailIcon
|
||||||
|
fontSize="small"
|
||||||
|
sx={{ mr: 1, color: style.name === 'creative' ? '#ffffff' : style.color.accent }}
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
color: style.name === 'creative' ? '#ffffff' : style.color.text,
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{candidate.email}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{phone?.isValid() && (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<PhoneIcon
|
||||||
|
fontSize="small"
|
||||||
|
sx={{ mr: 1, color: style.name === 'creative' ? '#ffffff' : style.color.accent }}
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
color: style.name === 'creative' ? '#ffffff' : style.color.text,
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{phone.formatInternational()}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{candidate.location && (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<LocationIcon
|
||||||
|
fontSize="small"
|
||||||
|
sx={{ mr: 1, color: style.name === 'creative' ? '#ffffff' : style.color.accent }}
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
color: style.name === 'creative' ? '#ffffff' : style.color.text,
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{candidate.location.city
|
||||||
|
? `${candidate.location.city}, ${candidate.location.state}`
|
||||||
|
: candidate.location.text}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Styled Footer Component
|
||||||
|
interface StyledFooterProps {
|
||||||
|
candidate: Types.Candidate;
|
||||||
|
job?: Types.Job;
|
||||||
|
style: ResumeStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledFooter: React.FC<StyledFooterProps> = ({ candidate, job, style }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
className="BackstoryResumeFooter"
|
||||||
|
sx={{
|
||||||
|
...style.footerStyle,
|
||||||
|
color: style.color.secondary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Dive deeper into my qualifications at Backstory...
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={`/api/1.0/candidates/qr-code/${candidate.id || ''}/${(job && job.id) || ''}`}
|
||||||
|
alt="QR Code"
|
||||||
|
className="qr-code"
|
||||||
|
sx={{ display: 'flex', mt: 1, mb: 1 }}
|
||||||
|
/>
|
||||||
|
{candidate?.username
|
||||||
|
? `${window.location.protocol}://${window.location.host}/u/${candidate?.username}`
|
||||||
|
: 'backstory'}
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ pb: 2 }}> </Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Main ResumePreview Component
|
||||||
|
export interface ResumePreviewProps {
|
||||||
|
resume: Types.Resume;
|
||||||
|
selectedStyle?: string;
|
||||||
|
shadeMargins?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ResumePreview: React.FC<ResumePreviewProps> = (props: ResumePreviewProps) => {
|
||||||
|
const { resume, selectedStyle = 'corporate', shadeMargins = true } = props;
|
||||||
|
const currentStyle = resumeStyles[selectedStyle] || resumeStyles.corporate;
|
||||||
|
const job: Types.Job | null = resume.job || null;
|
||||||
|
const candidate: Types.Candidate | null = resume.candidate || null;
|
||||||
|
|
||||||
|
if (!resume || !candidate || !job) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<Typography variant="body1">No resume data available.</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className="document-container"
|
||||||
|
sx={{
|
||||||
|
...currentStyle.contentStyle,
|
||||||
|
position: 'relative',
|
||||||
|
height: 'fit-content',
|
||||||
|
minHeight: 'fit-content',
|
||||||
|
display: 'flex',
|
||||||
|
m: 0,
|
||||||
|
p: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
className={`a4-document ${shadeMargins ? 'with-margins' : ''}`}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: currentStyle.color.background,
|
||||||
|
padding: 5,
|
||||||
|
minHeight: '100vh',
|
||||||
|
height: 'fit-content',
|
||||||
|
boxShadow: '0 0 10px rgba(0,0,0,0.1)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Custom Header */}
|
||||||
|
<StyledHeader candidate={candidate} style={currentStyle} />
|
||||||
|
|
||||||
|
{/* Styled Markdown Content */}
|
||||||
|
<Box sx={currentStyle.markdownStyle}>
|
||||||
|
<StyledMarkdown
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
maxHeight: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexGrow: 1,
|
||||||
|
flex: 1,
|
||||||
|
...currentStyle.markdownStyle,
|
||||||
|
}}
|
||||||
|
content={resume.resume}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* QR Code Footer */}
|
||||||
|
{job && <StyledFooter candidate={candidate} job={job} style={currentStyle} />}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResumePreview;
|
@ -17,7 +17,7 @@ import { ConversationHandle } from 'components/Conversation';
|
|||||||
import { Message } from 'components/Message';
|
import { Message } from 'components/Message';
|
||||||
import { DeleteConfirmation } from 'components/DeleteConfirmation';
|
import { DeleteConfirmation } from 'components/DeleteConfirmation';
|
||||||
import { CandidateInfo } from 'components/ui/CandidateInfo';
|
import { CandidateInfo } from 'components/ui/CandidateInfo';
|
||||||
import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
|
import { useAppState, useSelectedCandidate, useSelectedJob } from 'hooks/GlobalContext';
|
||||||
import PropagateLoader from 'react-spinners/PropagateLoader';
|
import PropagateLoader from 'react-spinners/PropagateLoader';
|
||||||
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
|
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
|
||||||
import { BackstoryQuery } from 'components/BackstoryQuery';
|
import { BackstoryQuery } from 'components/BackstoryQuery';
|
||||||
@ -53,6 +53,11 @@ const defaultMessage: ChatMessage = {
|
|||||||
metadata: emptyMetadata,
|
metadata: emptyMetadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultQuestion: CandidateQuestion = {
|
||||||
|
question:
|
||||||
|
'How well does the resume align with the job description? What are the three key strengths and two greatest weaknesses?',
|
||||||
|
};
|
||||||
|
|
||||||
interface CandidateChatPageProps {
|
interface CandidateChatPageProps {
|
||||||
sx?: SxProps; // Optional styles for the component
|
sx?: SxProps; // Optional styles for the component
|
||||||
}
|
}
|
||||||
@ -60,10 +65,11 @@ interface CandidateChatPageProps {
|
|||||||
const CandidateChatPage = forwardRef<ConversationHandle, CandidateChatPageProps>(
|
const CandidateChatPage = forwardRef<ConversationHandle, CandidateChatPageProps>(
|
||||||
(props: CandidateChatPageProps, ref): JSX.Element => {
|
(props: CandidateChatPageProps, ref): JSX.Element => {
|
||||||
const { resumeId } = useParams<{ resumeId?: string }>();
|
const { resumeId } = useParams<{ resumeId?: string }>();
|
||||||
|
const { selectedJob, setSelectedJob } = useSelectedJob();
|
||||||
const [resume, setResume] = useState<Resume | null>(null);
|
const [resume, setResume] = useState<Resume | null>(null);
|
||||||
const { sx } = props;
|
const { sx } = props;
|
||||||
const { apiClient } = useAuth();
|
const { apiClient } = useAuth();
|
||||||
const { selectedCandidate } = useSelectedCandidate();
|
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
|
||||||
const [processingMessage, setProcessingMessage] = useState<
|
const [processingMessage, setProcessingMessage] = useState<
|
||||||
ChatMessageStatus | ChatMessageError | null
|
ChatMessageStatus | ChatMessageError | null
|
||||||
>(null);
|
>(null);
|
||||||
@ -84,6 +90,12 @@ const CandidateChatPage = forwardRef<ConversationHandle, CandidateChatPageProps>
|
|||||||
.getResume(resumeId)
|
.getResume(resumeId)
|
||||||
.then(resume => {
|
.then(resume => {
|
||||||
setResume(resume);
|
setResume(resume);
|
||||||
|
if (resume.candidate && resume.candidate !== selectedCandidate) {
|
||||||
|
setSelectedCandidate(resume.candidate);
|
||||||
|
}
|
||||||
|
if (resume.job && resume.job !== selectedJob) {
|
||||||
|
setSelectedJob(resume.job);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Failed to load resume:', error);
|
console.error('Failed to load resume:', error);
|
||||||
@ -121,9 +133,9 @@ const CandidateChatPage = forwardRef<ConversationHandle, CandidateChatPageProps>
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
extraContext: {
|
extraContext: {
|
||||||
candidateId: resume?.job?.id,
|
candidateId: resume?.candidate?.id,
|
||||||
jobId: resume?.job?.id || '',
|
jobId: resume?.job?.id,
|
||||||
resumeId: resume?.id || '',
|
resumeId: resume?.id,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -152,14 +164,12 @@ const CandidateChatPage = forwardRef<ConversationHandle, CandidateChatPageProps>
|
|||||||
onError: (error: string | ChatMessageError): void => {
|
onError: (error: string | ChatMessageError): void => {
|
||||||
console.log('onError:', error);
|
console.log('onError:', error);
|
||||||
// Type-guard to determine if this is a ChatMessageBase or a string
|
// Type-guard to determine if this is a ChatMessageBase or a string
|
||||||
if (typeof error === 'object' && error !== null && 'content' in error) {
|
if (typeof error === 'object' && error !== null && 'error' in error) {
|
||||||
setProcessingMessage(error);
|
setSnack(`Error: ${error.error}`, 'error');
|
||||||
|
} else if (typeof error === 'string') {
|
||||||
|
setSnack(`Error: ${error}`, 'error');
|
||||||
} else {
|
} else {
|
||||||
setProcessingMessage({
|
setSnack(`An unknown error occurred: ${JSON.stringify(error)}`, 'error');
|
||||||
...defaultMessage,
|
|
||||||
status: 'error',
|
|
||||||
content: error,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
setStreaming(false);
|
setStreaming(false);
|
||||||
},
|
},
|
||||||
@ -251,9 +261,9 @@ const CandidateChatPage = forwardRef<ConversationHandle, CandidateChatPageProps>
|
|||||||
content:
|
content:
|
||||||
`Welcome to the Backstory Chat about ${selectedCandidate.fullName}` +
|
`Welcome to the Backstory Chat about ${selectedCandidate.fullName}` +
|
||||||
(resume && ` and the ${resume.job?.title} position at ${resume.job?.company}`) +
|
(resume && ` and the ${resume.job?.title} position at ${resume.job?.company}`) +
|
||||||
`. Ask any questions you have about ${selectedCandidate.firstName}'${
|
`. Enter any questions you have about ${selectedCandidate.firstName}'${
|
||||||
selectedCandidate.firstName.slice(-1) !== 's' ? 's' : ''
|
selectedCandidate.firstName.slice(-1) !== 's' ? 's' : ''
|
||||||
} resume or skills.`,
|
} resume or skills, or select from the available questions.`,
|
||||||
metadata: emptyMetadata,
|
metadata: emptyMetadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -376,14 +386,14 @@ const CandidateChatPage = forwardRef<ConversationHandle, CandidateChatPageProps>
|
|||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</Scrollable>
|
</Scrollable>
|
||||||
)}
|
)}
|
||||||
{selectedCandidate.questions?.length !== 0 && (
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 1, p: 1, flex: 0 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 1, p: 1, flex: 0 }}>
|
||||||
{' '}
|
|
||||||
{selectedCandidate.questions?.map((q, i) => (
|
{selectedCandidate.questions?.map((q, i) => (
|
||||||
<BackstoryQuery key={i} question={q} submitQuery={handleSubmitQuestion} />
|
<BackstoryQuery key={i} question={q} submitQuery={handleSubmitQuestion} />
|
||||||
))}
|
))}
|
||||||
</Box>
|
{resume && (
|
||||||
|
<BackstoryQuery question={defaultQuestion} submitQuery={handleSubmitQuestion} />
|
||||||
)}
|
)}
|
||||||
|
</Box>
|
||||||
{/* Fixed Message Input */}
|
{/* Fixed Message Input */}
|
||||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
<DeleteConfirmation
|
<DeleteConfirmation
|
||||||
|
@ -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-07-16T21:30:37.986984
|
// Generated on: 2025-07-16T23:28:16.752031
|
||||||
// DO NOT EDIT MANUALLY - This file is auto-generated
|
// DO NOT EDIT MANUALLY - This file is auto-generated
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
@ -299,7 +299,7 @@ export interface ChatMessage {
|
|||||||
timestamp?: Date;
|
timestamp?: Date;
|
||||||
role: "user" | "assistant" | "system" | "information" | "warning" | "error";
|
role: "user" | "assistant" | "system" | "information" | "warning" | "error";
|
||||||
content: string;
|
content: string;
|
||||||
extraContext?: Record<string, any>;
|
extraContext?: ExtraChatContext;
|
||||||
tunables?: Tunables;
|
tunables?: Tunables;
|
||||||
metadata: ChatMessageMetaData;
|
metadata: ChatMessageMetaData;
|
||||||
}
|
}
|
||||||
@ -350,7 +350,7 @@ export interface ChatMessageResume {
|
|||||||
timestamp?: Date;
|
timestamp?: Date;
|
||||||
role: "user" | "assistant" | "system" | "information" | "warning" | "error";
|
role: "user" | "assistant" | "system" | "information" | "warning" | "error";
|
||||||
content: string;
|
content: string;
|
||||||
extraContext?: Record<string, any>;
|
extraContext?: ExtraChatContext;
|
||||||
tunables?: Tunables;
|
tunables?: Tunables;
|
||||||
metadata: ChatMessageMetaData;
|
metadata: ChatMessageMetaData;
|
||||||
resume: Resume;
|
resume: Resume;
|
||||||
@ -365,7 +365,7 @@ export interface ChatMessageSkillAssessment {
|
|||||||
timestamp?: Date;
|
timestamp?: Date;
|
||||||
role: "user" | "assistant" | "system" | "information" | "warning" | "error";
|
role: "user" | "assistant" | "system" | "information" | "warning" | "error";
|
||||||
content: string;
|
content: string;
|
||||||
extraContext?: Record<string, any>;
|
extraContext?: ExtraChatContext;
|
||||||
tunables?: Tunables;
|
tunables?: Tunables;
|
||||||
metadata: ChatMessageMetaData;
|
metadata: ChatMessageMetaData;
|
||||||
skillAssessment: SkillAssessment;
|
skillAssessment: SkillAssessment;
|
||||||
@ -401,7 +401,7 @@ export interface ChatMessageUser {
|
|||||||
timestamp?: Date;
|
timestamp?: Date;
|
||||||
role: "user" | "assistant" | "system" | "information" | "warning" | "error";
|
role: "user" | "assistant" | "system" | "information" | "warning" | "error";
|
||||||
content: string;
|
content: string;
|
||||||
extraContext?: Record<string, any>;
|
extraContext?: ExtraChatContext;
|
||||||
tunables?: Tunables;
|
tunables?: Tunables;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -616,6 +616,14 @@ export interface EvidenceDetail {
|
|||||||
context: string;
|
context: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExtraChatContext {
|
||||||
|
jobId?: string;
|
||||||
|
candidateId?: string;
|
||||||
|
resumeId?: string;
|
||||||
|
resume?: string;
|
||||||
|
isAnswer?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GPUInfo {
|
export interface GPUInfo {
|
||||||
name: string;
|
name: string;
|
||||||
memory: number;
|
memory: number;
|
||||||
@ -1009,7 +1017,7 @@ export interface ResumeMessage {
|
|||||||
timestamp?: Date;
|
timestamp?: Date;
|
||||||
role: "user" | "assistant" | "system" | "information" | "warning" | "error";
|
role: "user" | "assistant" | "system" | "information" | "warning" | "error";
|
||||||
content: string;
|
content: string;
|
||||||
extraContext?: Record<string, any>;
|
extraContext?: ExtraChatContext;
|
||||||
tunables?: Tunables;
|
tunables?: Tunables;
|
||||||
resume: Resume;
|
resume: Resume;
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ import defines
|
|||||||
from logger import logger
|
from logger import logger
|
||||||
from models import (
|
from models import (
|
||||||
ChatResponse,
|
ChatResponse,
|
||||||
|
ExtraChatContext,
|
||||||
Tunables,
|
Tunables,
|
||||||
ChatMessageUser,
|
ChatMessageUser,
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
@ -644,7 +645,7 @@ Content: {content}
|
|||||||
session_id: str,
|
session_id: str,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
database: RedisDatabase,
|
database: RedisDatabase,
|
||||||
extra_context: Optional[dict[str, str | int | float | bool]] = None,
|
extra_context: Optional[ExtraChatContext] = None,
|
||||||
tunables: Optional[Tunables] = None,
|
tunables: Optional[Tunables] = None,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
) -> AsyncGenerator[ApiMessage, None]:
|
) -> AsyncGenerator[ApiMessage, None]:
|
||||||
@ -693,10 +694,6 @@ Content: {content}
|
|||||||
rag_message = message
|
rag_message = message
|
||||||
context = self.get_rag_context(rag_message)
|
context = self.get_rag_context(rag_message)
|
||||||
|
|
||||||
if extra_context:
|
|
||||||
# Add extra context to the messages if provided
|
|
||||||
context = f"{context}\n\n".join(f"<{key}>\n{value}</{key}>" for key, value in extra_context.items())
|
|
||||||
|
|
||||||
# Add the RAG context to the messages if available
|
# Add the RAG context to the messages if available
|
||||||
if context:
|
if context:
|
||||||
messages.append(
|
messages.append(
|
||||||
|
@ -7,7 +7,7 @@ from database.core import RedisDatabase
|
|||||||
from .base import Agent, agent_registry
|
from .base import Agent, agent_registry
|
||||||
from logger import logger
|
from logger import logger
|
||||||
|
|
||||||
from models import ApiMessage, Tunables, ApiStatusType
|
from models import ApiMessage, ExtraChatContext, Tunables, ApiStatusType
|
||||||
|
|
||||||
|
|
||||||
system_message = """
|
system_message = """
|
||||||
@ -41,7 +41,7 @@ class CandidateChat(Agent):
|
|||||||
session_id: str,
|
session_id: str,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
database: RedisDatabase,
|
database: RedisDatabase,
|
||||||
extra_context: Optional[dict[str, str | int | float | bool]] = None,
|
extra_context: Optional[ExtraChatContext] = None,
|
||||||
tunables: Optional[Tunables] = None,
|
tunables: Optional[Tunables] = None,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
) -> AsyncGenerator[ApiMessage, None]:
|
) -> AsyncGenerator[ApiMessage, None]:
|
||||||
|
@ -19,6 +19,7 @@ from models import (
|
|||||||
ChatMessageStreaming,
|
ChatMessageStreaming,
|
||||||
ChatMessageUser,
|
ChatMessageUser,
|
||||||
ChatOptions,
|
ChatOptions,
|
||||||
|
ExtraChatContext,
|
||||||
Tunables,
|
Tunables,
|
||||||
ApiStatusType,
|
ApiStatusType,
|
||||||
UsageStats,
|
UsageStats,
|
||||||
@ -41,7 +42,7 @@ class EditResume(Agent):
|
|||||||
session_id: str,
|
session_id: str,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
database: RedisDatabase,
|
database: RedisDatabase,
|
||||||
extra_context: Optional[dict[str, str | int | float | bool]] = None,
|
extra_context: Optional[ExtraChatContext] = None,
|
||||||
tunables: Optional[Tunables] = None,
|
tunables: Optional[Tunables] = None,
|
||||||
temperature=0.2,
|
temperature=0.2,
|
||||||
):
|
):
|
||||||
@ -74,16 +75,9 @@ class EditResume(Agent):
|
|||||||
context = None
|
context = None
|
||||||
rag_message: ChatMessageRagSearch | None = None
|
rag_message: ChatMessageRagSearch | None = None
|
||||||
if self.user:
|
if self.user:
|
||||||
logger.info("Generating resume enhanced RAG results")
|
logger.info("Generating results")
|
||||||
rag_prompt = ""
|
|
||||||
if extra_context:
|
|
||||||
# Add extra context to the messages if provided
|
|
||||||
rag_prompt = f"{context}\n\n".join(f"<{key}>\n{value}</{key}>" for key, value in extra_context.items())
|
|
||||||
rag_prompt += f"\n\nPrompt to respond to:\n{prompt}\n"
|
|
||||||
else:
|
|
||||||
rag_prompt = prompt
|
|
||||||
message = None
|
message = None
|
||||||
async for message in self.generate_rag_results(session_id=session_id, prompt=rag_prompt, top_k=10):
|
async for message in self.generate_rag_results(session_id=session_id, prompt=prompt, top_k=10):
|
||||||
if message.status == ApiStatusType.ERROR:
|
if message.status == ApiStatusType.ERROR:
|
||||||
yield message
|
yield message
|
||||||
return
|
return
|
||||||
@ -97,16 +91,17 @@ class EditResume(Agent):
|
|||||||
rag_message = message
|
rag_message = message
|
||||||
context = self.get_rag_context(rag_message)
|
context = self.get_rag_context(rag_message)
|
||||||
|
|
||||||
if extra_context:
|
if extra_context and extra_context.resume:
|
||||||
# Add extra context to the messages if provided
|
# Add extra context to the messages if provided
|
||||||
context = f"{context}\n\n".join(f"<{key}>\n{value}</{key}>" for key, value in extra_context.items())
|
context = f"<context>\n{context}\n</context>\n\n<resume>\n{extra_context.resume}</resume>"
|
||||||
|
elif context:
|
||||||
|
context = "<context>\n{context}\n</context>"
|
||||||
# Add the RAG context to the messages if available
|
# Add the RAG context to the messages if available
|
||||||
if context:
|
if context:
|
||||||
messages.append(
|
messages.append(
|
||||||
LLMMessage(
|
LLMMessage(
|
||||||
role="user",
|
role="user",
|
||||||
content=f"<|context|>\nThe following is context information about {self.user.full_name}:\n{context}\n</|context|>\n\nPrompt to respond to:\n{prompt}\n",
|
content=f"{context}\n\nPrompt to respond to:\n{prompt}\n",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -197,7 +192,7 @@ class EditResume(Agent):
|
|||||||
session_id: str,
|
session_id: str,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
database: RedisDatabase,
|
database: RedisDatabase,
|
||||||
extra_context: Optional[dict[str, str | int | float | bool]] = None,
|
extra_context: Optional[ExtraChatContext] = None,
|
||||||
tunables: Optional[Tunables] = None,
|
tunables: Optional[Tunables] = None,
|
||||||
temperature=0.2,
|
temperature=0.2,
|
||||||
) -> AsyncGenerator[ApiMessage, None]:
|
) -> AsyncGenerator[ApiMessage, None]:
|
||||||
@ -209,7 +204,7 @@ class EditResume(Agent):
|
|||||||
You are a professional copy editor. Your task is to edit and enhance the provided resume content based on the requested edits.
|
You are a professional copy editor. Your task is to edit and enhance the provided resume content based on the requested edits.
|
||||||
|
|
||||||
**CRITICAL: NEVER INVENT OR FABRICATE ANY INFORMATION**
|
**CRITICAL: NEVER INVENT OR FABRICATE ANY INFORMATION**
|
||||||
- DO NOT create any metrics, percentages, dollar amounts, timeframes, or statistics that are not explicitly stated in the original resume or <|context|>
|
- DO NOT create any metrics, percentages, dollar amounts, timeframes, or statistics that are not explicitly stated in the original resume or <context>
|
||||||
- DO NOT add quantitative claims like "increased by X%", "reduced by X hours", "saved $X", "improved by X%" unless these exact figures are provided
|
- DO NOT add quantitative claims like "increased by X%", "reduced by X hours", "saved $X", "improved by X%" unless these exact figures are provided
|
||||||
- DO NOT estimate, approximate, or infer numerical data
|
- DO NOT estimate, approximate, or infer numerical data
|
||||||
|
|
||||||
@ -225,7 +220,7 @@ You are a professional copy editor. Your task is to edit and enhance the provide
|
|||||||
- You are provided the current resume content in the <resume> section
|
- You are provided the current resume content in the <resume> section
|
||||||
- Only make edits that are requested by the user
|
- Only make edits that are requested by the user
|
||||||
- Do not add any additional information that is not present in the original resume
|
- Do not add any additional information that is not present in the original resume
|
||||||
- Only add factual information supported by <|context|> or the <resume> content
|
- Only add factual information supported by <context> or the <resume> content
|
||||||
- DO NOT make assumptions about the candidate's experience or skills
|
- DO NOT make assumptions about the candidate's experience or skills
|
||||||
|
|
||||||
**For impact summaries specifically:**
|
**For impact summaries specifically:**
|
||||||
@ -250,7 +245,7 @@ If the user did not ask a question, return the entire resume with the requested
|
|||||||
database=database,
|
database=database,
|
||||||
temperature=temperature,
|
temperature=temperature,
|
||||||
tunables=tunables,
|
tunables=tunables,
|
||||||
extra_context=extra_context or {},
|
extra_context=extra_context,
|
||||||
):
|
):
|
||||||
if message.status == ApiStatusType.ERROR:
|
if message.status == ApiStatusType.ERROR:
|
||||||
yield message
|
yield message
|
||||||
|
@ -21,6 +21,7 @@ from models import (
|
|||||||
ChatMessageStatus,
|
ChatMessageStatus,
|
||||||
ChatMessageStreaming,
|
ChatMessageStreaming,
|
||||||
ApiStatusType,
|
ApiStatusType,
|
||||||
|
ExtraChatContext,
|
||||||
Tunables,
|
Tunables,
|
||||||
)
|
)
|
||||||
from logger import logger
|
from logger import logger
|
||||||
@ -47,7 +48,7 @@ class ImageGenerator(Agent):
|
|||||||
session_id: str,
|
session_id: str,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
database: RedisDatabase,
|
database: RedisDatabase,
|
||||||
extra_context: Optional[dict[str, str | int | float | bool]] = None,
|
extra_context: Optional[ExtraChatContext] = None,
|
||||||
tunables: Optional[Tunables] = None,
|
tunables: Optional[Tunables] = None,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
) -> AsyncGenerator[ChatMessage | ChatMessageStatus | ChatMessageError | ChatMessageStreaming, None]:
|
) -> AsyncGenerator[ChatMessage | ChatMessageStatus | ChatMessageError | ChatMessageStreaming, None]:
|
||||||
|
@ -28,6 +28,7 @@ from models import (
|
|||||||
ChatMessageStatus,
|
ChatMessageStatus,
|
||||||
ChatMessageStreaming,
|
ChatMessageStreaming,
|
||||||
ApiStatusType,
|
ApiStatusType,
|
||||||
|
ExtraChatContext,
|
||||||
Tunables,
|
Tunables,
|
||||||
)
|
)
|
||||||
from logger import logger
|
from logger import logger
|
||||||
@ -311,7 +312,7 @@ class GeneratePersona(Agent):
|
|||||||
session_id: str,
|
session_id: str,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
database: RedisDatabase,
|
database: RedisDatabase,
|
||||||
extra_context: Optional[dict[str, str | int | float | bool]] = None,
|
extra_context: Optional[ExtraChatContext] = None,
|
||||||
tunables: Optional[Tunables] = None,
|
tunables: Optional[Tunables] = None,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
) -> AsyncGenerator[ChatMessage | ChatMessageStatus | ChatMessageError | ChatMessageStreaming, None]:
|
) -> AsyncGenerator[ChatMessage | ChatMessageStatus | ChatMessageError | ChatMessageStreaming, None]:
|
||||||
|
@ -22,6 +22,7 @@ from models import (
|
|||||||
ChatMessageStatus,
|
ChatMessageStatus,
|
||||||
ChatMessageStreaming,
|
ChatMessageStreaming,
|
||||||
ApiStatusType,
|
ApiStatusType,
|
||||||
|
ExtraChatContext,
|
||||||
Job,
|
Job,
|
||||||
JobRequirements,
|
JobRequirements,
|
||||||
JobRequirementsMessage,
|
JobRequirementsMessage,
|
||||||
@ -163,7 +164,7 @@ Avoid vague categorizations and be precise about whether skills are explicitly r
|
|||||||
session_id: str,
|
session_id: str,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
database: RedisDatabase,
|
database: RedisDatabase,
|
||||||
extra_context: Optional[dict[str, str | int | float | bool]] = None,
|
extra_context: Optional[ExtraChatContext] = None,
|
||||||
tunables: Optional[Tunables] = None,
|
tunables: Optional[Tunables] = None,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
) -> AsyncGenerator[ApiMessage, None]:
|
) -> AsyncGenerator[ApiMessage, None]:
|
||||||
|
@ -6,7 +6,7 @@ from database.core import RedisDatabase
|
|||||||
from .base import Agent, agent_registry
|
from .base import Agent, agent_registry
|
||||||
from logger import logger
|
from logger import logger
|
||||||
|
|
||||||
from models import ApiMessage, ApiStatusType, ChatMessageError, ChatMessageRagSearch, Tunables
|
from models import ApiMessage, ApiStatusType, ChatMessageError, ChatMessageRagSearch, ExtraChatContext, Tunables
|
||||||
|
|
||||||
|
|
||||||
class RagSearchChat(Agent):
|
class RagSearchChat(Agent):
|
||||||
@ -24,7 +24,7 @@ class RagSearchChat(Agent):
|
|||||||
session_id: str,
|
session_id: str,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
database: RedisDatabase,
|
database: RedisDatabase,
|
||||||
extra_context: Optional[dict[str, str | int | float | bool]] = None,
|
extra_context: Optional[ExtraChatContext] = None,
|
||||||
tunables: Optional[Tunables] = None,
|
tunables: Optional[Tunables] = None,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
) -> AsyncGenerator[ApiMessage, None]:
|
) -> AsyncGenerator[ApiMessage, None]:
|
||||||
|
291
src/backend/agents/resume_chat.py
Normal file
291
src/backend/agents/resume_chat.py
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import time
|
||||||
|
from typing import List, Literal, AsyncGenerator, ClassVar, Optional, Any
|
||||||
|
|
||||||
|
|
||||||
|
from database.core import RedisDatabase
|
||||||
|
|
||||||
|
from .base import Agent, agent_registry
|
||||||
|
from logger import logger
|
||||||
|
|
||||||
|
from models import (
|
||||||
|
ApiActivityType,
|
||||||
|
ApiMessage,
|
||||||
|
Candidate,
|
||||||
|
ChatMessage,
|
||||||
|
ChatMessageError,
|
||||||
|
ChatMessageMetaData,
|
||||||
|
ChatMessageRagSearch,
|
||||||
|
ChatMessageStatus,
|
||||||
|
ChatMessageStreaming,
|
||||||
|
ChatMessageUser,
|
||||||
|
ChatOptions,
|
||||||
|
ExtraChatContext,
|
||||||
|
Job,
|
||||||
|
Resume,
|
||||||
|
Tunables,
|
||||||
|
ApiStatusType,
|
||||||
|
UsageStats,
|
||||||
|
LLMMessage,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ResumeChat(Agent):
|
||||||
|
"""
|
||||||
|
ResumeChat Agent
|
||||||
|
"""
|
||||||
|
|
||||||
|
agent_type: Literal["resume_chat"] = "resume_chat" # type: ignore
|
||||||
|
_agent_type: ClassVar[str] = agent_type # Add this for registration
|
||||||
|
|
||||||
|
async def resume_chat(
|
||||||
|
self,
|
||||||
|
llm: Any,
|
||||||
|
model: str,
|
||||||
|
session_id: str,
|
||||||
|
prompt: str,
|
||||||
|
database: RedisDatabase,
|
||||||
|
extra_context: Optional[ExtraChatContext] = None,
|
||||||
|
tunables: Optional[Tunables] = None,
|
||||||
|
temperature=0.2,
|
||||||
|
):
|
||||||
|
if not self.user:
|
||||||
|
error_message = ChatMessageError(session_id=session_id, content="No user set for chat generation.")
|
||||||
|
yield error_message
|
||||||
|
return
|
||||||
|
|
||||||
|
if not extra_context or not extra_context.job_id or not extra_context.candidate_id or not extra_context.resume_id:
|
||||||
|
error_message = ChatMessageError(
|
||||||
|
session_id=session_id,
|
||||||
|
content="Missing required extra context: job_id, candidate_id, or resume_id.",
|
||||||
|
)
|
||||||
|
yield error_message
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"Generating resume_chat response to: {prompt} with context: {extra_context.model_dump(exclude_none=True)}")
|
||||||
|
|
||||||
|
job_data = await database.get_job(extra_context.job_id)
|
||||||
|
job = Job.model_validate(job_data) if job_data else None
|
||||||
|
if not job:
|
||||||
|
error_message = ChatMessageError(
|
||||||
|
session_id=session_id, content=f"Job with ID {extra_context.job_id} not found."
|
||||||
|
)
|
||||||
|
yield error_message
|
||||||
|
return
|
||||||
|
candidate_data = await database.get_candidate(extra_context.candidate_id)
|
||||||
|
candidate = Candidate.model_validate(candidate_data) if candidate_data else None
|
||||||
|
if not candidate:
|
||||||
|
error_message = ChatMessageError(
|
||||||
|
session_id=session_id, content=f"Candidate with ID {extra_context.candidate_id} not found."
|
||||||
|
)
|
||||||
|
yield error_message
|
||||||
|
return
|
||||||
|
resume_data = await database.get_resume(user_id=candidate.id, resume_id=extra_context.resume_id)
|
||||||
|
resume = Resume.model_validate(resume_data) if resume_data else None
|
||||||
|
if not resume:
|
||||||
|
error_message = ChatMessageError(
|
||||||
|
session_id=session_id, content=f"Resume with ID {extra_context.resume_id} not found."
|
||||||
|
)
|
||||||
|
yield error_message
|
||||||
|
return
|
||||||
|
|
||||||
|
user_message = ChatMessageUser(
|
||||||
|
session_id=session_id,
|
||||||
|
content=prompt,
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.add_chat_message(session_id, user_message.model_dump())
|
||||||
|
logger.info(f"💬 User message saved to database for session {session_id}")
|
||||||
|
|
||||||
|
# Create a pruned down message list based purely on the prompt and responses,
|
||||||
|
# discarding the full preamble generated by prepare_message
|
||||||
|
messages: List[LLMMessage] = [LLMMessage(role="system", content=self.system_prompt)]
|
||||||
|
# Add the conversation history to the messages
|
||||||
|
messages.extend(
|
||||||
|
[
|
||||||
|
LLMMessage(role=m["role"], content=m["content"])
|
||||||
|
for m in await database.get_recent_chat_messages(session_id=session_id)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.user.metrics.generate_count.labels(agent=self.agent_type).inc()
|
||||||
|
with self.user.metrics.generate_duration.labels(agent=self.agent_type).time():
|
||||||
|
rag_context = None
|
||||||
|
rag_message: ChatMessageRagSearch | None = None
|
||||||
|
if self.user:
|
||||||
|
logger.info("Generating RAG results")
|
||||||
|
rag_prompt = prompt
|
||||||
|
message = None
|
||||||
|
async for message in self.generate_rag_results(session_id=session_id, prompt=rag_prompt, top_k=10):
|
||||||
|
if message.status == ApiStatusType.ERROR:
|
||||||
|
yield message
|
||||||
|
return
|
||||||
|
# Only yield messages that are in a streaming state
|
||||||
|
if message.status == ApiStatusType.STATUS:
|
||||||
|
yield message
|
||||||
|
|
||||||
|
if not isinstance(message, ChatMessageRagSearch):
|
||||||
|
raise ValueError(f"Expected ChatMessageRagSearch, got {type(rag_message)}")
|
||||||
|
|
||||||
|
rag_message = message
|
||||||
|
rag_context = self.get_rag_context(rag_message)
|
||||||
|
|
||||||
|
context = f"""
|
||||||
|
<job>
|
||||||
|
The following is the job description for the position {job.title} at {job.company}:
|
||||||
|
{job.description}
|
||||||
|
</job>
|
||||||
|
|
||||||
|
<resume>
|
||||||
|
The following is the resume of {candidate.full_name}:
|
||||||
|
{resume.resume}
|
||||||
|
</resume>
|
||||||
|
|
||||||
|
<candidate>
|
||||||
|
{candidate.model_dump(include={"first_name", "last_name", "full_name", "email", "location"}, exclude_none=True)}
|
||||||
|
</candidate>
|
||||||
|
"""
|
||||||
|
if rag_context:
|
||||||
|
context = f"""
|
||||||
|
<context>
|
||||||
|
The following is context information about {candidate.full_name} based on the prompt:
|
||||||
|
{rag_context}
|
||||||
|
</context>
|
||||||
|
|
||||||
|
{context}
|
||||||
|
"""
|
||||||
|
|
||||||
|
messages.append(
|
||||||
|
LLMMessage(
|
||||||
|
role="user",
|
||||||
|
content=f"{context}\n\nPrompt to respond to:\n{prompt}\n",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# not use_tools
|
||||||
|
status_message = ChatMessageStatus(
|
||||||
|
session_id=session_id, activity=ApiActivityType.GENERATING, content="Generating response..."
|
||||||
|
)
|
||||||
|
yield status_message
|
||||||
|
|
||||||
|
# Set the response for streaming
|
||||||
|
self.set_optimal_context_size(llm, model, prompt=prompt)
|
||||||
|
|
||||||
|
options = ChatOptions(
|
||||||
|
seed=8911,
|
||||||
|
num_ctx=self.context_size,
|
||||||
|
temperature=temperature,
|
||||||
|
)
|
||||||
|
logger.info(f"Message options: {options.model_dump(exclude_unset=True)} with {len(messages)} messages")
|
||||||
|
content = ""
|
||||||
|
start_time = time.perf_counter()
|
||||||
|
response = None
|
||||||
|
async for response in llm.chat_stream(
|
||||||
|
model=model,
|
||||||
|
messages=messages,
|
||||||
|
options={
|
||||||
|
**options.model_dump(exclude_unset=True),
|
||||||
|
},
|
||||||
|
stream=True,
|
||||||
|
):
|
||||||
|
if not response:
|
||||||
|
error_message = ChatMessageError(session_id=session_id, content="No response from LLM.")
|
||||||
|
yield error_message
|
||||||
|
return
|
||||||
|
|
||||||
|
content += response.content
|
||||||
|
|
||||||
|
if not response.finish_reason:
|
||||||
|
streaming_message = ChatMessageStreaming(
|
||||||
|
session_id=session_id,
|
||||||
|
content=response.content,
|
||||||
|
)
|
||||||
|
yield streaming_message
|
||||||
|
|
||||||
|
if not response:
|
||||||
|
error_message = ChatMessageError(session_id=session_id, content="No response from LLM.")
|
||||||
|
yield error_message
|
||||||
|
return
|
||||||
|
|
||||||
|
self.user.collect_metrics(agent=self, response=response)
|
||||||
|
end_time = time.perf_counter()
|
||||||
|
|
||||||
|
chat_message = ChatMessage(
|
||||||
|
session_id=session_id,
|
||||||
|
tunables=tunables,
|
||||||
|
status=ApiStatusType.DONE,
|
||||||
|
content=content,
|
||||||
|
metadata=ChatMessageMetaData(
|
||||||
|
options=options,
|
||||||
|
usage=UsageStats(
|
||||||
|
eval_count=response.usage.eval_count,
|
||||||
|
eval_duration=response.usage.eval_duration,
|
||||||
|
prompt_eval_count=response.usage.prompt_eval_count,
|
||||||
|
prompt_eval_duration=response.usage.prompt_eval_duration,
|
||||||
|
),
|
||||||
|
rag_results=rag_message.content if rag_message else [],
|
||||||
|
llm_history=messages,
|
||||||
|
timers={
|
||||||
|
"llm_streamed": end_time - start_time,
|
||||||
|
"llm_with_tools": 0, # Placeholder for tool processing time
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.add_chat_message(session_id, chat_message.model_dump())
|
||||||
|
logger.info(f"🤖 Assistent response saved to database for session {session_id}")
|
||||||
|
|
||||||
|
# Add the user and chat messages to the conversation
|
||||||
|
yield chat_message
|
||||||
|
return
|
||||||
|
|
||||||
|
async def generate(
|
||||||
|
self,
|
||||||
|
llm: Any,
|
||||||
|
model: str,
|
||||||
|
session_id: str,
|
||||||
|
prompt: str,
|
||||||
|
database: RedisDatabase,
|
||||||
|
extra_context: Optional[ExtraChatContext] = None,
|
||||||
|
tunables: Optional[Tunables] = None,
|
||||||
|
temperature=0.2,
|
||||||
|
) -> AsyncGenerator[ApiMessage, None]:
|
||||||
|
user = self.user
|
||||||
|
if not user:
|
||||||
|
logger.error("User is not set for Edit Resume agent.")
|
||||||
|
raise ValueError("User must be set before generating candidate chat responses.")
|
||||||
|
self.system_prompt = """
|
||||||
|
You are a professional staffing agent. Your task is to answer questions about a candidate based on their <resume> and <context>.
|
||||||
|
|
||||||
|
**CRITICAL: NEVER INVENT OR FABRICATE ANY INFORMATION**
|
||||||
|
- DO NOT create any metrics, percentages, dollar amounts, timeframes, or statistics that are not explicitly stated in the <resume> or <context>
|
||||||
|
- DO NOT add quantitative claims like "increased by X%", "reduced by X hours", "saved $X", "improved by X%" unless these exact figures are provided
|
||||||
|
- DO NOT estimate, approximate, or infer numerical data
|
||||||
|
|
||||||
|
**Guidelines:**
|
||||||
|
- You are provided the current resume about the candidate in the <resume> section
|
||||||
|
- You are provided additional context about the question in the <context> section
|
||||||
|
- You are provided the candidate's name and other details in the <candidate> section
|
||||||
|
- You are provided the job description in the <job> section
|
||||||
|
|
||||||
|
- You must use the provided information to answer questions about the candidate's skills, experience, and qualifications in relation to the job description.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async for message in self.resume_chat(
|
||||||
|
llm=llm,
|
||||||
|
model=model,
|
||||||
|
session_id=session_id,
|
||||||
|
prompt=prompt,
|
||||||
|
database=database,
|
||||||
|
temperature=temperature,
|
||||||
|
tunables=tunables,
|
||||||
|
extra_context=extra_context,
|
||||||
|
):
|
||||||
|
if message.status == ApiStatusType.ERROR:
|
||||||
|
yield message
|
||||||
|
return
|
||||||
|
yield message
|
||||||
|
|
||||||
|
|
||||||
|
# Register the base agent
|
||||||
|
agent_registry.register(ResumeChat._agent_type, ResumeChat)
|
@ -22,6 +22,7 @@ from models import (
|
|||||||
ApiStatusType,
|
ApiStatusType,
|
||||||
ChatMessageStatus,
|
ChatMessageStatus,
|
||||||
EvidenceDetail,
|
EvidenceDetail,
|
||||||
|
ExtraChatContext,
|
||||||
SkillAssessment,
|
SkillAssessment,
|
||||||
Tunables,
|
Tunables,
|
||||||
)
|
)
|
||||||
@ -116,7 +117,7 @@ JSON RESPONSE:"""
|
|||||||
session_id: str,
|
session_id: str,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
database: RedisDatabase,
|
database: RedisDatabase,
|
||||||
extra_context: Optional[dict[str, str | int | float | bool]] = None,
|
extra_context: Optional[ExtraChatContext] = None,
|
||||||
tunables: Optional[Tunables] = None,
|
tunables: Optional[Tunables] = None,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
) -> AsyncGenerator[ApiMessage, None]:
|
) -> AsyncGenerator[ApiMessage, None]:
|
||||||
|
@ -1169,13 +1169,22 @@ class SkillMatchRequest(BaseModel):
|
|||||||
skill: str
|
skill: str
|
||||||
regenerate: bool = Field(default=False, description="Whether to regenerate the skill match even if cached")
|
regenerate: bool = Field(default=False, description="Whether to regenerate the skill match even if cached")
|
||||||
|
|
||||||
|
class ExtraChatContext(BaseModel):
|
||||||
|
"""Extra context for chat messages"""
|
||||||
|
|
||||||
|
job_id: Optional[str] = Field(default=None, alias=str("jobId"))
|
||||||
|
candidate_id: Optional[str] = Field(default=None, alias=str("candidateId"))
|
||||||
|
resume_id: Optional[str] = Field(default=None, alias=str("resumeId"))
|
||||||
|
resume: Optional[str] = Field(default=None)
|
||||||
|
is_answer: Optional[bool] = Field(default=None, alias=str("isAnswer"))
|
||||||
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
class ChatMessageUser(ApiMessage):
|
class ChatMessageUser(ApiMessage):
|
||||||
type: ApiMessageType = ApiMessageType.TEXT
|
type: ApiMessageType = ApiMessageType.TEXT
|
||||||
status: ApiStatusType = ApiStatusType.DONE
|
status: ApiStatusType = ApiStatusType.DONE
|
||||||
role: ChatSenderType = ChatSenderType.USER
|
role: ChatSenderType = ChatSenderType.USER
|
||||||
content: str = ""
|
content: str = ""
|
||||||
extra_context: Optional[Dict[str, str | int | float | bool]] = Field(default=None, alias=str("extraContext"))
|
extra_context: Optional[ExtraChatContext] = Field(default=None, alias=str("extraContext"))
|
||||||
tunables: Optional[Tunables] = None
|
tunables: Optional[Tunables] = None
|
||||||
|
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ async def stream_agent_response(
|
|||||||
):
|
):
|
||||||
if generated_message.status == ApiStatusType.ERROR:
|
if generated_message.status == ApiStatusType.ERROR:
|
||||||
logger.error(f"❌ AI generation error: {generated_message.content}")
|
logger.error(f"❌ AI generation error: {generated_message.content}")
|
||||||
yield f"data: {json.dumps({'status': 'error'})}\n\n"
|
yield f"data: {json.dumps({'status': 'error', 'error': generated_message.content})}\n\n"
|
||||||
return
|
return
|
||||||
|
|
||||||
# Store reference to the complete AI message
|
# Store reference to the complete AI message
|
||||||
|
Loading…
x
Reference in New Issue
Block a user