Compare commits

..

No commits in common. "0216515492318a2f1953a223f73ea1c0e329345c" and "c946cc202a4dc8db8b0f1a4a7bd3157601a61a2d" have entirely different histories.

13 changed files with 218 additions and 311 deletions

View File

@ -24,7 +24,6 @@
"@types/react": "^19.0.12", "@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
"@uiw/react-json-view": "^2.0.0-alpha.31", "@uiw/react-json-view": "^2.0.0-alpha.31",
"jsonrepair": "^3.12.0",
"mermaid": "^11.6.0", "mermaid": "^11.6.0",
"mui-markdown": "^2.0.1", "mui-markdown": "^2.0.1",
"prism-react-renderer": "^2.4.1", "prism-react-renderer": "^2.4.1",
@ -14171,14 +14170,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/jsonrepair": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.12.0.tgz",
"integrity": "sha512-SWfjz8SuQ0wZjwsxtSJ3Zy8vvLg6aO/kxcp9TWNPGwJKgTZVfhNEQBMk/vPOpYCDFWRxD6QWuI6IHR1t615f0w==",
"bin": {
"jsonrepair": "bin/cli.js"
}
},
"node_modules/jsx-ast-utils": { "node_modules/jsx-ast-utils": {
"version": "3.3.5", "version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",

View File

@ -19,7 +19,6 @@
"@types/react": "^19.0.12", "@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
"@uiw/react-json-view": "^2.0.0-alpha.31", "@uiw/react-json-view": "^2.0.0-alpha.31",
"jsonrepair": "^3.12.0",
"mermaid": "^11.6.0", "mermaid": "^11.6.0",
"mui-markdown": "^2.0.1", "mui-markdown": "^2.0.1",
"prism-react-renderer": "^2.4.1", "prism-react-renderer": "^2.4.1",

View File

@ -19,12 +19,11 @@ interface ChatBubbleProps {
className?: string; className?: string;
title?: string; title?: string;
expanded?: boolean; expanded?: boolean;
expandable?: boolean;
onExpand?: () => void; onExpand?: () => void;
} }
function ChatBubble(props: ChatBubbleProps) { function ChatBubble(props: ChatBubbleProps) {
const { role, children, sx, className, title, onExpand, expandable }: ChatBubbleProps = props; const { role, children, sx, className, title, onExpand }: ChatBubbleProps = props;
const [expanded, setExpanded] = useState<boolean>((props.expanded === undefined) ? true : props.expanded); const [expanded, setExpanded] = useState<boolean>((props.expanded === undefined) ? true : props.expanded);
const theme = useTheme(); const theme = useTheme();
@ -49,7 +48,15 @@ function ChatBubble(props: ChatBubbleProps) {
} }
} }
const styles: any = { const styles = {
'user': {
...defaultStyle,
backgroundColor: theme.palette.background.default, // Warm Gray (#D3CDBF)
border: `1px solid ${theme.palette.custom.highlight}`, // Golden Ochre (#D4A017)
borderRadius: `${defaultRadius} ${defaultRadius} 0 ${defaultRadius}`, // Rounded, flat bottom-right for user
alignSelf: 'flex-end', // Right-aligned for user
color: theme.palette.primary.main, // Midnight Blue (#1A2536) for text
},
'assistant': { 'assistant': {
...defaultStyle, ...defaultStyle,
backgroundColor: theme.palette.primary.main, // Midnight Blue (#1A2536) backgroundColor: theme.palette.primary.main, // Midnight Blue (#1A2536)
@ -57,35 +64,17 @@ function ChatBubble(props: ChatBubbleProps) {
borderRadius: `${defaultRadius} ${defaultRadius} ${defaultRadius} 0`, // Rounded, flat bottom-left for assistant borderRadius: `${defaultRadius} ${defaultRadius} ${defaultRadius} 0`, // Rounded, flat bottom-left for assistant
color: theme.palette.primary.contrastText, // Warm Gray (#D3CDBF) for text color: theme.palette.primary.contrastText, // Warm Gray (#D3CDBF) for text
}, },
'content': { 'system': {
...defaultStyle, ...defaultStyle,
backgroundColor: '#F5F2EA', // Light cream background for easy reading backgroundColor: '#EDEAE0', // Soft warm gray that plays nice with #D3CDBF
border: `1px solid ${theme.palette.custom.highlight}`, // Golden Ochre border border: `1px dashed ${theme.palette.custom.highlight}`, // Golden Ochre
borderRadius: 0,
alignSelf: 'center', // Centered in the chat
color: theme.palette.text.primary, // Charcoal Black for maximum readability
padding: '8px 8px', // More generous padding for better text framing
marginBottom: '0px', // Space between content and conversation
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.05)', // Subtle elevation
fontSize: '0.9rem', // Slightly smaller than default
lineHeight: '1.3', // More compact line height
fontFamily: theme.typography.fontFamily, // Consistent font with your theme
},
'error': {
...defaultStyle,
backgroundColor: '#F8E7E7', // Soft light red background
border: `1px solid #D83A3A`, // Prominent red border
borderRadius: defaultRadius, borderRadius: defaultRadius,
maxWidth: '90%', maxWidth: '90%',
minWidth: '90%', minWidth: '90%',
alignSelf: 'center', alignSelf: 'center',
color: '#8B2525', // Deep red text for good contrast color: theme.palette.text.primary, // Charcoal Black
padding: '10px 16px', fontStyle: 'italic',
boxShadow: '0 1px 3px rgba(216, 58, 58, 0.15)', // Subtle shadow with red tint
}, },
'fact-check': 'qualifications',
'job-description': 'content',
'job-requirements': 'qualifications',
'info': { 'info': {
...defaultStyle, ...defaultStyle,
backgroundColor: '#BFD8D8', // Softened Dusty Teal backgroundColor: '#BFD8D8', // Softened Dusty Teal
@ -94,16 +83,6 @@ function ChatBubble(props: ChatBubbleProps) {
color: theme.palette.text.primary, // Charcoal Black (#2E2E2E) — much better contrast color: theme.palette.text.primary, // Charcoal Black (#2E2E2E) — much better contrast
opacity: 0.95, opacity: 0.95,
}, },
'processing': "status",
'qualifications': {
...defaultStyle,
backgroundColor: theme.palette.primary.light, // Lighter shade, e.g., Soft Blue (#2A3B56)
border: `1px solid ${theme.palette.secondary.main}`, // Keep Dusty Teal (#4A7A7D) for contrast
borderRadius: `${defaultRadius} ${defaultRadius} ${defaultRadius} 0`, // Unchanged
color: theme.palette.primary.contrastText, // Warm Gray (#D3CDBF) for readable text
},
'resume': 'content',
'searching': 'status',
'status': { 'status': {
...defaultStyle, ...defaultStyle,
backgroundColor: 'rgba(74, 122, 125, 0.15)', // Translucent dusty teal backgroundColor: 'rgba(74, 122, 125, 0.15)', // Translucent dusty teal
@ -119,45 +98,57 @@ function ChatBubble(props: ChatBubbleProps) {
opacity: 0.9, opacity: 0.9,
transition: 'opacity 0.3s ease-in-out', // Smooth fade effect for appearing/disappearing transition: 'opacity 0.3s ease-in-out', // Smooth fade effect for appearing/disappearing
}, },
'streaming': "assistant", 'error': {
'system': {
...defaultStyle, ...defaultStyle,
backgroundColor: '#EDEAE0', // Soft warm gray that plays nice with #D3CDBF backgroundColor: '#F8E7E7', // Soft light red background
border: `1px dashed ${theme.palette.custom.highlight}`, // Golden Ochre border: `1px solid #D83A3A`, // Prominent red border
borderRadius: defaultRadius, borderRadius: defaultRadius,
maxWidth: '90%', maxWidth: '90%',
minWidth: '90%', minWidth: '90%',
alignSelf: 'center', alignSelf: 'center',
color: theme.palette.text.primary, // Charcoal Black color: '#8B2525', // Deep red text for good contrast
fontStyle: 'italic', padding: '10px 16px',
boxShadow: '0 1px 3px rgba(216, 58, 58, 0.15)', // Subtle shadow with red tint
}, },
'thinking': "status", 'content': {
'user': {
...defaultStyle, ...defaultStyle,
backgroundColor: theme.palette.background.default, // Warm Gray (#D3CDBF) backgroundColor: '#F5F2EA', // Light cream background for easy reading
border: `1px solid ${theme.palette.custom.highlight}`, // Golden Ochre (#D4A017) border: `1px solid ${theme.palette.custom.highlight}`, // Golden Ochre border
borderRadius: `${defaultRadius} ${defaultRadius} 0 ${defaultRadius}`, // Rounded, flat bottom-right for user borderRadius: 0,
alignSelf: 'flex-end', // Right-aligned for user alignSelf: 'center', // Centered in the chat
color: theme.palette.primary.main, // Midnight Blue (#1A2536) for text color: theme.palette.text.primary, // Charcoal Black for maximum readability
padding: '8px 8px', // More generous padding for better text framing
marginBottom: '0px', // Space between content and conversation
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.05)', // Subtle elevation
fontSize: '0.9rem', // Slightly smaller than default
lineHeight: '1.3', // More compact line height
fontFamily: theme.typography.fontFamily, // Consistent font with your theme
},
'thinking': {
...defaultStyle
},
'streaming': {
...defaultStyle
},
'processing': {
...defaultStyle
}, },
}; };
for (const [key, value] of Object.entries(styles)) { styles["thinking"] = styles["status"]
if (typeof (value) === "string") { styles["streaming"] = styles["assistant"]
(styles as any)[key] = styles[value]; styles["processing"] = styles["status"]
}
}
const icons: any = { const icons: any = {
"error": <ErrorOutline color='error' />,
"info": <InfoOutline color='info' />,
"processing": <LocationSearchingIcon />,
// "streaming": <Stream />,
"searching": <Memory />, "searching": <Memory />,
"thinking": <Psychology />, "thinking": <Psychology />,
// "streaming": <Stream />,
"tooling": <LocationSearchingIcon />, "tooling": <LocationSearchingIcon />,
"processing": <LocationSearchingIcon />,
"error": <ErrorOutline color='error' />,
"info": <InfoOutline color='info' />,
}; };
if (expandable || (role === 'content' && title)) { if (role === 'content' && title) {
return ( return (
<Accordion <Accordion
expanded={expanded} expanded={expanded}
@ -169,7 +160,7 @@ function ChatBubble(props: ChatBubbleProps) {
expandIcon={<ExpandMoreIcon />} expandIcon={<ExpandMoreIcon />}
slotProps={{ content: { sx: { fontWeight: 'bold', fontSize: '1.1rem', m: 0, p: 0, display: 'flex', justifyItems: 'center' } } }} slotProps={{ content: { sx: { fontWeight: 'bold', fontSize: '1.1rem', m: 0, p: 0, display: 'flex', justifyItems: 'center' } } }}
> >
{title || ""} {title}
</AccordionSummary> </AccordionSummary>
<AccordionDetails sx={{ mt: 0, mb: 0, p: 0, pl: 2, pr: 2 }}> <AccordionDetails sx={{ mt: 0, mb: 0, p: 0, pl: 2, pr: 2 }}>
{children} {children}
@ -179,7 +170,7 @@ function ChatBubble(props: ChatBubbleProps) {
} }
return ( return (
<Box className={className} sx={{ ...(role in styles ? styles[role] : styles["status"]), gap: 1, display: "flex", ...sx, flexDirection: "row" }}> <Box className={className} sx={{ ...(styles[role] !== undefined ? styles[role] : styles["status"]), gap: 1, display: "flex", ...sx, flexDirection: "row" }}>
{icons[role] !== undefined && icons[role]} {icons[role] !== undefined && icons[role]}
<Box sx={{ p: 0, m: 0, gap: 0, display: "flex", flexGrow: 1, flexDirection: "column" }}> <Box sx={{ p: 0, m: 0, gap: 0, display: "flex", flexGrow: 1, flexDirection: "column" }}>
{children} {children}

View File

@ -6,7 +6,6 @@ interface DocumentProps {
title: string; title: string;
expanded?: boolean; expanded?: boolean;
filepath: string; filepath: string;
content?: string;
setSnack: SetSnackType; setSnack: SetSnackType;
submitQuery?: MessageSubmitQuery; submitQuery?: MessageSubmitQuery;
connectionBase: string; connectionBase: string;
@ -15,13 +14,13 @@ interface DocumentProps {
} }
const Document = (props: DocumentProps) => { const Document = (props: DocumentProps) => {
const { setSnack, submitQuery, connectionBase, filepath, content, title, expanded, disableCopy, onExpand } = props; const { setSnack, submitQuery, connectionBase, filepath, title, expanded, disableCopy, onExpand } = props;
const [document, setDocument] = useState<string>(""); const [document, setDocument] = useState<string>("");
// Get the markdown // Get the markdown
useEffect(() => { useEffect(() => {
if (document !== "" || !filepath) { if (document !== "") {
return; return;
} }
const fetchDocument = async () => { const fetchDocument = async () => {
@ -47,6 +46,7 @@ const Document = (props: DocumentProps) => {
}, [document, setDocument, filepath]) }, [document, setDocument, filepath])
return ( return (
<>
<Message <Message
{...{ {...{
sx: { sx: {
@ -56,7 +56,7 @@ const Document = (props: DocumentProps) => {
m: 0, m: 0,
flexGrow: 0, flexGrow: 0,
}, },
message: { role: 'content', title: title, content: document || content || "" }, message: { role: 'content', title: title, content: document },
connectionBase, connectionBase,
submitQuery, submitQuery,
setSnack, setSnack,
@ -64,6 +64,8 @@ const Document = (props: DocumentProps) => {
disableCopy, disableCopy,
onExpand, onExpand,
}} /> }} />
{/* <Box sx={{ display: "flex", flexGrow: 1, p: 0, m: 0 }} /> */}
</>
); );
}; };

View File

@ -35,6 +35,7 @@ const Mermaid: React.FC<MermaidProps> = (props: MermaidProps) => {
const renderMermaid = async () => { const renderMermaid = async () => {
if (containerRef.current && visible && chart) { if (containerRef.current && visible && chart) {
try { try {
console.log("Rendering Mermaid");
await mermaid.initialize(mermaidConfig || defaultMermaidConfig); await mermaid.initialize(mermaidConfig || defaultMermaidConfig);
await mermaid.run({ nodes: [containerRef.current] }); await mermaid.run({ nodes: [containerRef.current] });
} catch (e) { } catch (e) {

View File

@ -29,22 +29,7 @@ import { SetSnackType } from './Snack';
import { CopyBubble } from './CopyBubble'; import { CopyBubble } from './CopyBubble';
import { Scrollable } from './Scrollable'; import { Scrollable } from './Scrollable';
type MessageRoles = type MessageRoles = 'info' | 'user' | 'assistant' | 'system' | 'status' | 'error' | 'content' | 'thinking' | 'processing' | 'streaming';
'assistant' |
'content' |
'error' |
'fact-check' |
'info' |
'job-description' |
'job-requirements' |
'processing' |
'qualifications' |
'resume' |
'status' |
'streaming' |
'system' |
'thinking' |
'user';
type MessageData = { type MessageData = {
role: MessageRoles, role: MessageRoles,
@ -59,9 +44,7 @@ type MessageData = {
id?: string, id?: string,
isProcessing?: boolean, isProcessing?: boolean,
actions?: string[], actions?: string[],
metadata?: MessageMetaData, metadata?: MessageMetaData
expanded?: boolean,
expandable?: boolean,
}; };
interface MessageMetaData { interface MessageMetaData {
@ -90,7 +73,7 @@ type MessageList = MessageData[];
interface MessageProps { interface MessageProps {
sx?: SxProps<Theme>, sx?: SxProps<Theme>,
message: MessageData, message: MessageData,
// expanded?: boolean, // Provided as part of MessageData expanded?: boolean,
onExpand?: () => void, onExpand?: () => void,
submitQuery?: MessageSubmitQuery, submitQuery?: MessageSubmitQuery,
sessionId?: string, sessionId?: string,
@ -243,6 +226,7 @@ const MessageMeta = (props: MessageMetaProps) => {
const Message = (props: MessageProps) => { const Message = (props: MessageProps) => {
const { message, submitQuery, sx, className, onExpand } = props; const { message, submitQuery, sx, className, onExpand } = props;
const messageExpanded = props.expanded;
const [expanded, setExpanded] = useState<boolean>(false); const [expanded, setExpanded] = useState<boolean>(false);
const textFieldRef = useRef(null); const textFieldRef = useRef(null);
@ -264,7 +248,9 @@ const Message = (props: MessageProps) => {
return ( return (
<ChatBubble <ChatBubble
className={className || "Message"} className={className || "Message"}
{...message} role={message.role}
title={message.title}
expanded={messageExpanded}
onExpand={onExpand} onExpand={onExpand}
sx={{ sx={{
display: "flex", display: "flex",

View File

@ -11,8 +11,6 @@ import { MessageList, MessageData } from './Message';
import { SetSnackType } from './Snack'; import { SetSnackType } from './Snack';
import { Conversation } from './Conversation'; import { Conversation } from './Conversation';
import './ResumeBuilder.css';
interface ResumeBuilderProps { interface ResumeBuilderProps {
connectionBase: string, connectionBase: string,
sessionId: string | undefined, sessionId: string | undefined,
@ -69,88 +67,133 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
if (messages === undefined || messages.length === 0) { if (messages === undefined || messages.length === 0) {
return []; return [];
} }
console.log("filterJobDescriptionMessages disabled", messages)
if (messages.length > 2) { if (messages.length > 1) {
setHasResume(true); setHasResume(true);
setHasFacts(true);
} }
if (messages.length > 0) { messages[0].role = 'content';
messages[0].role = 'content'; messages[0].title = 'Job Description';
messages[0].title = 'Job Description'; messages[0].disableCopy = false;
messages[0].disableCopy = false;
messages[0].expandable = true;
}
if (messages.length > 3) { return messages;
// messages[2] is Show job requirements
messages[3].role = 'job-requirements';
messages[3].title = 'Job Requirements';
messages[3].disableCopy = false;
messages[3].expanded = true;
messages[3].expandable = true;
}
/* Filter out the 2nd and 3rd (0-based) */ // let reduced = messages.filter((m, i) => {
const filtered = messages.filter((m, i) => i !== 1 && i !== 2); // const keep = (m.metadata?.origin || m.origin || "no origin") === 'job_description';
// if ((m.metadata?.origin || m.origin || "no origin") === 'resume') {
// setHasResume(true);
// }
// // if (!keep) {
// // console.log(`filterJobDescriptionMessages: ${i + 1} filtered:`, m);
// // } else {
// // console.log(`filterJobDescriptionMessages: ${i + 1}:`, m);
// // }
return filtered; // return keep;
}, [setHasResume, setHasFacts]); // });
// /* If Resume hasn't occurred yet and there is still more than one message,
// * resume has been generated. */
// if (!hasResume && reduced.length > 1) {
// setHasResume(true);
// }
// if (reduced.length > 0) {
// // First message is always 'content'
// reduced[0].title = 'Job Description';
// reduced[0].role = 'content';
// setHasJobDescription(true);
// }
// /* Filter out any messages which the server injected for state management */
// reduced = reduced.filter(m => m.display !== "hide");
// return reduced;
}, [setHasResume/*, setHasJobDescription, hasResume*/]);
const filterResumeMessages = useCallback((messages: MessageList): MessageList => { const filterResumeMessages = useCallback((messages: MessageList): MessageList => {
if (messages === undefined || messages.length === 0) { if (messages === undefined || messages.length === 0) {
return []; return [];
} }
console.log("filterResumeMessages disabled")
if (messages.length > 1) {
// messages[0] is Show Qualifications
messages[1].role = 'qualifications';
messages[1].title = 'Candidate qualifications';
messages[1].disableCopy = false;
messages[1].expanded = false;
messages[1].expandable = true;
}
if (messages.length > 3) { if (messages.length > 3) {
// messages[2] is Show Resume setHasFacts(true);
messages[3].role = 'resume';
messages[3].title = 'Generated Resume';
messages[3].disableCopy = false;
messages[3].expanded = true;
messages[3].expandable = true;
} }
return messages;
/* Filter out the 1st and 3rd messages (0-based) */ // let reduced = messages.filter((m, i) => {
const filtered = messages.filter((m, i) => i !== 0 && i !== 2); // const keep = (m.metadata?.origin || m.origin || "no origin") === 'resume';
// if ((m.metadata?.origin || m.origin || "no origin") === 'fact_check') {
// setHasFacts(true);
// }
// if (!keep) {
// console.log(`filterResumeMessages: ${i + 1} filtered:`, m);
// } else {
// console.log(`filterResumeMessages: ${i + 1}:`, m);
// }
// return keep;
// });
return filtered; // /* If there is more than one message, it is user: "...JOB_DESCRIPTION...", assistant: "...RESUME..."
}, []); // * which means a resume has been generated. */
// if (reduced.length > 1) {
// /* Remove the assistant message from the UI */
// if (reduced[0].role === "user") {
// reduced.splice(0, 1);
// }
// }
// /* If Fact Check hasn't occurred yet and there is still more than one message,
// * facts have have been generated. */
// if (!hasFacts && reduced.length > 1) {
// setHasFacts(true);
// }
// /* Filter out any messages which the server injected for state management */
// reduced = reduced.filter(m => m.display !== "hide");
// /* If there are any messages, there is a resume */
// if (reduced.length > 0) {
// // First message is always 'content'
// reduced[0].title = 'Resume';
// reduced[0].role = 'content';
// setHasResume(true);
// }
// return reduced;
}, [/*setHasResume, hasFacts,*/ setHasFacts]);
const filterFactsMessages = useCallback((messages: MessageList): MessageList => { const filterFactsMessages = useCallback((messages: MessageList): MessageList => {
if (messages === undefined || messages.length === 0) { if (messages === undefined || messages.length === 0) {
return []; return [];
} }
console.log("filterFactsMessages disabled")
return messages;
if (messages.length > 1) { // messages.forEach((m, i) => console.log(`filterFactsMessages: ${i + 1}:`, m))
// messages[0] is Show verification
messages[1].role = 'fact-check';
messages[1].title = 'Fact Check';
messages[1].disableCopy = false;
messages[1].expanded = false;
messages[1].expandable = true;
}
/* Filter out the 1st (0-based) */ // const reduced = messages.filter(m => {
const filtered = messages.filter((m, i) => i !== 0); // return (m.metadata?.origin || m.origin || "no origin") === 'fact_check';
// });
return filtered; // /* If there is more than one message, it is user: "Fact check this resume...", assistant: "...FACT CHECK..."
}, []); // * which means facts have been generated. */
// if (reduced.length > 1) {
// /* Remove the user message from the UI */
// if (reduced[0].role === "user") {
// reduced.splice(0, 1);
// }
// // First message is always 'content'
// reduced[0].title = 'Fact Check';
// reduced[0].role = 'content';
// setHasFacts(true);
// }
// return reduced;
}, [/*setHasFacts*/]);
const jobResponse = useCallback(async (message: MessageData) => { const jobResponse = useCallback(async (message: MessageData) => {
console.log('onJobResponse', message); console.log('onJobResponse', message);
if (message.actions && message.actions.includes("job_description")) {
await jobConversationRef.current.fetchHistory();
}
if (message.actions && message.actions.includes("resume_generated")) { if (message.actions && message.actions.includes("resume_generated")) {
await resumeConversationRef.current.fetchHistory(); await resumeConversationRef.current.fetchHistory();
setHasResume(true); setHasResume(true);
@ -195,29 +238,12 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
</Box>, </Box>,
]; ];
const jobDescriptionPreamble: MessageList = [{
role: 'info',
content: `Once you paste a job description and press **Generate Resume**, the system will perform the following actions:
1. **RAG**: Collects information from the RAG database relavent to the job description
2. **Isolated Analysis**: Three sub-stages
1. **Job Analysis**: Extracts requirements from job description only
2. **Candidate Analysis**: Catalogs qualifications from resume/context only
3. **Mapping Analysis**: Identifies legitimate matches between requirements and qualifications
3. **Resume Generation**: Uses mapping output to create a tailored resume with evidence-based content
4. **Verification**: Performs fact-checking to catch any remaining fabrications
1. **Re-generation**: If verification does not pass, a second attempt is made to correct any issues`
}];
if (!hasJobDescription) { if (!hasJobDescription) {
return <Conversation return <Conversation
ref={jobConversationRef} ref={jobConversationRef}
{...{ {...{
type: "job_description", type: "job_description",
actionLabel: "Generate Resume", actionLabel: "Generate Resume",
preamble: jobDescriptionPreamble,
hidePreamble: true,
prompt: "Paste a job description, then click Generate...", prompt: "Paste a job description, then click Generate...",
multiline: true, multiline: true,
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`, resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
@ -331,8 +357,7 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
}, [connectionBase, sessionId, setSnack, factsResponse, filterFactsMessages, resetFacts, hasResume, hasFacts]); }, [connectionBase, sessionId, setSnack, factsResponse, filterFactsMessages, resetFacts, hasResume, hasFacts]);
return ( return (
<Box className="ResumeBuilder" <Box sx={{
sx={{
p: 0, p: 0,
m: 0, m: 0,
display: "flex", display: "flex",

View File

@ -12,8 +12,4 @@ pre:not(.MessageContent) {
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
overflow: visible; overflow: visible;
}
.MuiMarkdown > div {
width: 100%;
} }

View File

@ -4,11 +4,8 @@ import { SxProps, useTheme } from '@mui/material/styles';
import { Link } from '@mui/material'; import { Link } from '@mui/material';
import { ChatQuery, QueryOptions } from './ChatQuery'; import { ChatQuery, QueryOptions } from './ChatQuery';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import JsonView from '@uiw/react-json-view';
import { vscodeTheme } from '@uiw/react-json-view/vscode';
import { Mermaid } from './Mermaid'; import { Mermaid } from './Mermaid';
import { Scrollable } from './Scrollable';
import { jsonrepair } from 'jsonrepair';
import './StyledMarkdown.css'; import './StyledMarkdown.css';
@ -27,42 +24,10 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
pre: { pre: {
component: (element: any) => { component: (element: any) => {
const { className } = element.children.props; const { className } = element.children.props;
const content = element.children?.props?.children || ""; const chart = element.children?.props?.children || "";
if (className === "lang-mermaid") { if (className === "lang-mermaid") {
return <Mermaid className="Mermaid" chart={content} />; console.log(`StyledMarkdown pre: ${className}`);
} return <Mermaid className="Mermaid" chart={chart} />;
if (className === "lang-json") {
try {
const fixed = jsonrepair(content);
return <Scrollable className="JsonViewScrollable">
<JsonView
className="JsonView"
style={{
...vscodeTheme,
fontSize: "0.8rem",
maxHeight: "20rem",
padding: "14px 0",
overflow: "hidden",
width: "100%",
minHeight: "max-content",
backgroundColor: "transparent",
}}
displayDataTypes={false}
objectSortKeys={false}
collapsed={false}
value={JSON.parse(fixed)}>
<JsonView.String
render={({ children, ...reset }) => {
if (typeof (children) === "string" && children.match("\n")) {
return <pre {...reset} style={{ display: "flex", border: "none", ...reset.style }}>{children}</pre>
}
}}
/>
</JsonView>
</Scrollable>
} catch (e) {
console.log("jsonrepair error", e);
};
} }
return <pre><code className={className}>{element.children}</code></pre>; return <pre><code className={className}>{element.children}</code></pre>;
}, },

View File

@ -18,6 +18,7 @@ import { Scrollable } from './Scrollable';
import { StyledMarkdown } from './StyledMarkdown'; import { StyledMarkdown } from './StyledMarkdown';
import './VectorVisualizer.css'; import './VectorVisualizer.css';
import { calculatePoint } from 'mermaid/dist/utils';
interface Metadata { interface Metadata {
doc_type?: string; doc_type?: string;

View File

@ -881,6 +881,9 @@ class WebServer:
return return
logger.info(f"{agent_type}.process_message: {message.status} {f'...{message.response[-20:]}' if len(message.response) > 20 else message.response}") logger.info(f"{agent_type}.process_message: {message.status} {f'...{message.response[-20:]}' if len(message.response) > 20 else message.response}")
if message.metadata["eval_count"]:
agent.metrics.tokens_prompt.labels(agent=agent.agent_type).inc(message.metadata["prompt_eval_count"])
agent.metrics.tokens_eval.labels(agent=agent.agent_type).inc(message.metadata["eval_count"])
message.status = "done" message.status = "done"
yield message yield message
return return

View File

@ -263,11 +263,11 @@ class Agent(BaseModel, ABC):
for response in llm.chat( for response in llm.chat(
model=model, model=model,
messages=messages, messages=messages,
stream=True,
options={ options={
**message.metadata["options"], **message.metadata["options"],
# "temperature": 0.5, # "temperature": 0.5,
}, }
stream=True,
): ):
# logger.info(f"LLM::Tools: {'done' if response.done else 'processing'} - {response.message}") # logger.info(f"LLM::Tools: {'done' if response.done else 'processing'} - {response.message}")
message.status = "streaming" message.status = "streaming"
@ -278,7 +278,6 @@ class Agent(BaseModel, ABC):
yield message yield message
if response.done: if response.done:
self.collect_metrics(response)
message.metadata["eval_count"] += response.eval_count message.metadata["eval_count"] += response.eval_count
message.metadata["eval_duration"] += response.eval_duration message.metadata["eval_duration"] += response.eval_duration
message.metadata["prompt_eval_count"] += response.prompt_eval_count message.metadata["prompt_eval_count"] += response.prompt_eval_count
@ -291,10 +290,6 @@ class Agent(BaseModel, ABC):
message.metadata["timers"]["llm_with_tools"] = f"{(end_time - start_time):.4f}" message.metadata["timers"]["llm_with_tools"] = f"{(end_time - start_time):.4f}"
return return
def collect_metrics(self, response):
self.metrics.tokens_prompt.labels(agent=self.agent_type).inc(response.prompt_eval_count)
self.metrics.tokens_eval.labels(agent=self.agent_type).inc(response.eval_count)
async def generate_llm_response(self, llm: Any, model: str, message: Message, temperature = 0.7) -> AsyncGenerator[Message, None]: async def generate_llm_response(self, llm: Any, model: str, message: Message, temperature = 0.7) -> AsyncGenerator[Message, None]:
logger.info(f"{self.agent_type} - {inspect.stack()[0].function}") logger.info(f"{self.agent_type} - {inspect.stack()[0].function}")
@ -359,7 +354,6 @@ class Agent(BaseModel, ABC):
}, },
stream=False # No need to stream the probe stream=False # No need to stream the probe
) )
self.collect_metrics(response)
end_time = time.perf_counter() end_time = time.perf_counter()
message.metadata["timers"]["tool_check"] = f"{(end_time - start_time):.4f}" message.metadata["timers"]["tool_check"] = f"{(end_time - start_time):.4f}"
@ -388,7 +382,6 @@ class Agent(BaseModel, ABC):
}, },
stream=False stream=False
) )
self.collect_metrics(response)
end_time = time.perf_counter() end_time = time.perf_counter()
message.metadata["timers"]["non_streaming"] = f"{(end_time - start_time):.4f}" message.metadata["timers"]["non_streaming"] = f"{(end_time - start_time):.4f}"
@ -448,7 +441,6 @@ class Agent(BaseModel, ABC):
yield message yield message
if response.done: if response.done:
self.collect_metrics(response)
message.metadata["eval_count"] += response.eval_count message.metadata["eval_count"] += response.eval_count
message.metadata["eval_duration"] += response.eval_duration message.metadata["eval_duration"] += response.eval_duration
message.metadata["prompt_eval_count"] += response.prompt_eval_count message.metadata["prompt_eval_count"] += response.prompt_eval_count

View File

@ -191,25 +191,16 @@ class JobDescription(Agent):
if message.status == "error": if message.status == "error":
return return
# Add the "Job requirements" message
if "generate_factual_tailored_resume" in message.metadata and "job_requirements" in message.metadata["generate_factual_tailored_resume"]:
new_message = Message(prompt="Show job requirements")
job_requirements = message.metadata["generate_factual_tailored_resume"]["job_requirements"]["results"]
new_message.response = f"```json\n\n{json.dumps(job_requirements, indent=2)}\n```\n"
new_message.status = "done"
self.conversation.add(new_message)
self.system_prompt = system_user_qualifications self.system_prompt = system_user_qualifications
resume_agent = self.context.get_agent(agent_type="resume") resume_agent = self.context.get_agent(agent_type="resume")
fact_check_agent = self.context.get_agent(agent_type="fact_check") fact_check_agent = self.context.get_agent(agent_type="fact_check")
if not resume_agent: if not resume_agent:
# Add the "Generated Resume" message
if "generate_factual_tailored_resume" in message.metadata and "analyze_candidate_qualifications" in message.metadata["generate_factual_tailored_resume"]: if "generate_factual_tailored_resume" in message.metadata and "analyze_candidate_qualifications" in message.metadata["generate_factual_tailored_resume"]:
resume_agent = self.context.get_or_create_agent(agent_type="resume", resume=message.response) resume_agent = self.context.get_or_create_agent(agent_type="resume", resume=message.response)
resume_message = Message(prompt="Show candidate qualifications") resume_message = Message(prompt="Show candidate qualifications")
qualifications = message.metadata["generate_factual_tailored_resume"]["analyze_candidate_qualifications"]["results"] qualifications = message.metadata["generate_factual_tailored_resume"]["analyze_candidate_qualifications"]["results"]
resume_message.response = f"```json\n\n{json.dumps(qualifications, indent=2)}\n```\n" resume_message.response = f"# Candidate qualifications\n\n```json\n\n{json.dumps(qualifications, indent=2)}\n```\n"
resume_message.status = "done" resume_message.status = "done"
resume_agent.conversation.add(resume_message) resume_agent.conversation.add(resume_message)
@ -221,8 +212,6 @@ class JobDescription(Agent):
message.response = "Resume generated." message.response = "Resume generated."
message.actions.append("resume_generated") message.actions.append("resume_generated")
# Add the "Fact Check" message
if "generate_factual_tailored_resume" in message.metadata and "verify_resume" in message.metadata["generate_factual_tailored_resume"]: if "generate_factual_tailored_resume" in message.metadata and "verify_resume" in message.metadata["generate_factual_tailored_resume"]:
if "second_pass" in message.metadata["generate_factual_tailored_resume"]["verify_resume"]: if "second_pass" in message.metadata["generate_factual_tailored_resume"]["verify_resume"]:
verification = message.metadata["generate_factual_tailored_resume"]["verify_resume"]["second_pass"]["results"] verification = message.metadata["generate_factual_tailored_resume"]["verify_resume"]["second_pass"]["results"]
@ -232,7 +221,7 @@ class JobDescription(Agent):
fact_check_agent = self.context.get_or_create_agent(agent_type="fact_check", facts=json.dumps(verification, indent=2)) fact_check_agent = self.context.get_or_create_agent(agent_type="fact_check", facts=json.dumps(verification, indent=2))
fact_check_message = message.model_copy() fact_check_message = message.model_copy()
fact_check_message.prompt = "Show verification" fact_check_message.prompt = "Show verification"
fact_check_message.response = f"```json\n\n{json.dumps(verification, indent=2)}\n```\n" fact_check_message.response = f"# Resume verfication\n\n```json\n\n{json.dumps(verification, indent=2)}\n```\n"
fact_check_message.status = "done" fact_check_message.status = "done"
fact_check_agent.conversation.add(fact_check_message) fact_check_agent.conversation.add(fact_check_message)
@ -555,7 +544,6 @@ class JobDescription(Agent):
message.chunk = "" message.chunk = ""
if response.done: if response.done:
self.collect_metrics(response)
message.metadata["eval_count"] += response.eval_count message.metadata["eval_count"] += response.eval_count
message.metadata["eval_duration"] += response.eval_duration message.metadata["eval_duration"] += response.eval_duration
message.metadata["prompt_eval_count"] += response.prompt_eval_count message.metadata["prompt_eval_count"] += response.prompt_eval_count
@ -879,78 +867,45 @@ class JobDescription(Agent):
async def correct_resume_issues(self, message: Message, generated_resume: str, verification_results: Dict, skills_mapping: Dict, candidate_qualifications: Dict, original_header: str, metadata: Dict[str, Any]) -> AsyncGenerator[Message, None]: async def correct_resume_issues(self, message: Message, generated_resume: str, verification_results: Dict, skills_mapping: Dict, candidate_qualifications: Dict, original_header: str, metadata: Dict[str, Any]) -> AsyncGenerator[Message, None]:
"""Correct issues in the resume based on verification results.""" """Correct issues in the resume based on verification results."""
if verification_results["verification_results"]["overall_assessment"] == "APPROVED": if verification_results["verification_results"]["overall_assessment"] == "APPROVED":
message.status = "done" message.status = "done"
message.status = generated_resume message.status = generated_resume
yield message yield message
return return
system_prompt = """\ system_prompt = """
You are a professional resume editor with a focus on factual accuracy. Your task is to correct identified issues in a tailored resume according to the verification report. You are a professional resume editor with a focus on factual accuracy. Your task is to correct
the identified issues in a tailored resume according to the verification report.
## REFERENCE DATA:
The following sections contain reference information for you to use when making corrections. This information should NOT be included in your output resume: ## INSTRUCTIONS:
1. Original Resume - The resume you will correct 1. Make ONLY the changes specified in the verification report
2. Verification Results - Issues that need correction 2. Ensure all corrections maintain factual accuracy based on the skills mapping
3. Skills Mapping - How candidate skills align with job requirements 3. Do not introduce any new claims or skills not present in the verification data
4. Candidate Qualifications - Verified information about the candidate's background 4. Maintain the original format and structure of the resume
5. Original Resume Header - The formatting of the resume header 5. DO NOT directly list the verification report or skills mapping
6. Provide ONLY the fully corrected resume
## INSTRUCTIONS: 7. DO NOT provide Verification Results or other additional information beyond the corrected resume
1. Make ONLY the changes specified in the verification report ## PROCESS:
2. Ensure all corrections maintain factual accuracy based on the skills mapping
3. Do not introduce any new claims or skills not present in the verification data 1. For each issue in the verification report:
4. Maintain the original format and structure of the resume - Identify the problematic text in the resume
5. Provide ONLY the fully corrected resume as your output - Replace it with the suggested correction
6. DO NOT include any of the reference data sections in your output - Ensure the correction is consistent with the rest of the resume
7. DO NOT include any additional comments, explanations, or notes in your output
2. After making all corrections:
## PROCESS: - Review the revised resume for consistency
- Ensure no factual inaccuracies have been introduced
1. For each issue in the verification report: - Check that all formatting remains professional
- Identify the problematic text in the resume
- Replace it with the suggested correction Return the fully corrected resume.
- Ensure the correction is consistent with the rest of the resume """
2. After making all corrections: prompt = f"Original Resume:\n{generated_resume}\n\n"
- Review the revised resume for consistency prompt += f"Verification Results:\n{json.dumps(verification_results, indent=2)}\n\n"
- Ensure no factual inaccuracies have been introduced prompt += f"Skills Mapping:\n{json.dumps(skills_mapping, indent=2)}\n\n"
- Check that all formatting remains professional prompt += f"Candidate Qualifications:\n{json.dumps(candidate_qualifications, indent=2)}\n\n"
prompt += f"Original Resume Header:\n{original_header}"
Your output should contain ONLY the corrected resume text with no additional explanations or context.
"""
prompt = """
## REFERENCE DATA
### Original Resume:
"""
prompt += generated_resume
prompt += """
### Verification Results:
"""
prompt += json.dumps(verification_results, indent=2)
prompt += """
### Skills Mapping:
"""
prompt += json.dumps(skills_mapping, indent=2)
prompt += """
### Candidate Qualifications:
"""
prompt += json.dumps(candidate_qualifications, indent=2)
prompt += """
### Original Resume Header:
"""
prompt += generated_resume
prompt += """
## TASK
Based on the reference data above, please create a corrected version of the resume that addresses all issues identified in the verification report. Return ONLY the corrected resume.
"""
metadata["system_prompt"] = system_prompt metadata["system_prompt"] = system_prompt
metadata["prompt"] = prompt metadata["prompt"] = prompt