This commit is contained in:
James Ketr 2025-05-10 16:50:01 -07:00
parent b6dd4878c8
commit 538caba9f4
12 changed files with 261 additions and 111 deletions

View File

@ -0,0 +1,21 @@
Backstory is developed using:
## Frontend
* React
* MUI
* Plotly.js
* MuiMarkdown
* Mermaid
## Backend
* Python
* FastAPI
* HuggingFace Transformers
* Ollama
* Backstory Agent Framework
* Prometheus
* Grafana
* ze-monitor
* Jupyter Notebook

View File

@ -0,0 +1,100 @@
The system follows a carefully designed pipeline with isolated stages to prevent fabrication:
## System Architecture Overview
The system uses a pipeline of isolated analysis and generation steps:
1. **Stage 1: Isolated Analysis** (three sub-stages)
- **1A: Job Analysis** - Extracts requirements from job description only
- **1B: Candidate Analysis** - Catalogs qualifications from resume/context only
- **1C: Mapping Analysis** - Identifies legitimate matches between requirements and qualifications
2. **Stage 2: Resume Generation**
- Uses mapping output to create a tailored resume with evidence-based content
3. **Stage 3: Verification**
- Performs fact-checking to catch any remaining fabrications
```mermaid
flowchart TD
subgraph "Stage 1: Isolated Analysis"
subgraph "Stage 1A: Job Analysis"
A1[Job Description Input] --> A2[Job Analysis LLM]
A2 --> A3[Job Requirements JSON]
end
subgraph "Stage 1B: Candidate Analysis"
B1[Resume & Context Input] --> B2[Candidate Analysis LLM]
B2 --> B3[Candidate Qualifications JSON]
end
subgraph "Stage 1C: Mapping Analysis"
C1[Job Requirements JSON] --> C2[Candidate Qualifications JSON]
C2 --> C3[Mapping Analysis LLM]
C3 --> C4[Skills Mapping JSON]
end
end
subgraph "Stage 2: Resume Generation"
D1[Skills Mapping JSON] --> D2[Original Resume Reference]
D2 --> D3[Resume Generation LLM]
D3 --> D4[Tailored Resume Draft]
end
subgraph "Stage 3: Verification"
E1[Skills Mapping JSON] --> E2[Original Materials]
E2 --> E3[Tailored Resume Draft]
E3 --> E4[Verification LLM]
E4 --> E5{Verification Check}
E5 -->|PASS| E6[Approved Resume]
E5 -->|FAIL| E7[Correction Instructions]
E7 --> D3
end
A3 --> C1
B3 --> C2
C4 --> D1
D4 --> E3
style A2 fill:#f9d77e,stroke:#333,stroke-width:2px
style B2 fill:#f9d77e,stroke:#333,stroke-width:2px
style C3 fill:#f9d77e,stroke:#333,stroke-width:2px
style D3 fill:#f9d77e,stroke:#333,stroke-width:2px
style E4 fill:#f9d77e,stroke:#333,stroke-width:2px
style E5 fill:#a3e4d7,stroke:#333,stroke-width:2px
style E6 fill:#aed6f1,stroke:#333,stroke-width:2px
style E7 fill:#f5b7b1,stroke:#333,stroke-width:2px
```
## Stage 1: Isolated Analysis (three separate sub-stages)
1. **Job Analysis**: Extracts requirements from just the job description
2. **Candidate Analysis**: Catalogs qualifications from just the resume/context
3. **Mapping Analysis**: Identifies legitimate matches between requirements and qualifications
## Stage 2: Resume Generation
Creates a tailored resume using only verified information from the mapping
## Stage 3: Verification
1. Performs fact-checking to catch any remaining fabrications
2. Corrects issues if needed and re-verifies
### Key Anti-Fabrication Mechanisms
The system uses several techniques to prevent fabrication:
* **Isolation of Analysis Stages**: By analyzing the job and candidate separately, the system prevents the LLM from prematurely creating connections that might lead to fabrication.
* **Evidence Requirements**: Each qualification included must have explicit evidence from the original materials.
* **Conservative Transferability**: The system is instructed to be conservative when claiming skills are transferable.
* **Verification Layer**: A dedicated verification step acts as a safety check to catch any remaining fabrications.
* **Strict JSON Structures**: Using structured JSON formats ensures information flows properly between stages.
## Implementation Details
* **Prompt Engineering**: Each stage has carefully designed prompts with clear instructions and output formats.
* **Error Handling**: Comprehensive validation and error handling throughout the pipeline.
* **Correction Loop**: If verification fails, the system attempts to correct issues and re-verify.
* **Traceability**: Information in the final resume can be traced back to specific evidence in the original materials.

View File

@ -10,11 +10,8 @@ const AboutPage = (props: BackstoryPageProps) => {
const [ subRoute, setSubRoute] = useState<string>("");
useEffect(() => {
console.log(`AboutPage: ${page}`);
}, [page]);
useEffect(() => {
console.log(`AboutPage: ${page} - subRoute: ${subRoute}`);
}, [subRoute]);
console.log(`AboutPage: ${page} - route - ${route} - subRoute: ${subRoute}`);
}, [page, route, subRoute]);
useEffect(() => {
if (route === undefined) { return; }
@ -31,13 +28,10 @@ const AboutPage = (props: BackstoryPageProps) => {
console.log("Document expanded:", document, open);
if (open) {
setPage(document);
if (setRoute) setRoute(document);
} else {
setPage("");
}
/* This is just to quiet warnings for now...*/
if (route === "never" && subRoute && setRoute) {
setRoute(document);
setSubRoute(document);
if (setRoute) setRoute("");
}
}

View File

@ -79,14 +79,12 @@ const App = () => {
};
const tabs: BackstoryTabProps[] = useMemo(() => {
const tabSx = { flexGrow: 1, fontSize: '1rem' };
const homeTab: BackstoryTabProps = {
label: "",
path: "",
tabProps: {
label: "Backstory",
sx: tabSx,
sx: { flexGrow: 1, fontSize: '1rem' },
icon:
<Avatar sx={{
width: 24,
@ -151,6 +149,20 @@ const App = () => {
];
}, [sessionId, setSnack, subRoute]);
useEffect(() => {
if (sessionId === undefined || activeTab > tabs.length - 1) { return; }
console.log(`route - '${tabs[activeTab].path}', subRoute - '${subRoute}'`);
let path = tabs[activeTab].path ? `/${tabs[activeTab].path}` : '';
if (subRoute) {
path += `/${subRoute}`;
}
path += `/${sessionId}`;
console.log('pushState: ', path);
// window.history.pushState({}, '', path);
}, [activeTab, sessionId, subRoute, tabs]);
const fetchSession = useCallback((async (pathParts?: string[]) => {
try {
const response = await fetch(connectionBase + `/api/context`, {

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React from 'react';
import { Box } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { SxProps, Theme } from '@mui/material';
@ -9,7 +9,7 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import LocationSearchingIcon from '@mui/icons-material/LocationSearching';
import { MessageRoles } from './Message';
import { ErrorOutline, InfoOutline, Memory, Message, Psychology, /* Stream, */ } from '@mui/icons-material';
import { ErrorOutline, InfoOutline, Memory, Psychology, /* Stream, */ } from '@mui/icons-material';
interface ChatBubbleProps {
role: MessageRoles,
@ -24,9 +24,7 @@ interface ChatBubbleProps {
}
function ChatBubble(props: ChatBubbleProps) {
const { role, children, sx, className, title, onExpand, expandable, expanded }: ChatBubbleProps = props;
console.log("ChatBubbble():", props.expanded);
const { role, children, sx, className, title, onExpand, expandable, expanded } = props;
const theme = useTheme();
@ -34,12 +32,12 @@ function ChatBubble(props: ChatBubbleProps) {
const defaultStyle = {
padding: theme.spacing(1, 2),
fontSize: '0.875rem',
alignSelf: 'flex-start', // Left-aligned is used by default
alignSelf: 'flex-start',
maxWidth: '100%',
minWidth: '100%',
height: 'fit-content',
'& > *': {
color: 'inherit', // Children inherit 'color' from parent
color: 'inherit',
overflow: 'hidden',
m: 0,
},
@ -47,130 +45,151 @@ function ChatBubble(props: ChatBubbleProps) {
mb: 0,
m: 0,
p: 0,
}
}
},
};
const styles: any = {
'assistant': {
assistant: {
...defaultStyle,
backgroundColor: theme.palette.primary.main, // Midnight Blue (#1A2536)
border: `1px solid ${theme.palette.secondary.main}`, // Dusty Teal (#4A7A7D)
borderRadius: `${defaultRadius} ${defaultRadius} ${defaultRadius} 0`, // Rounded, flat bottom-left for assistant
color: theme.palette.primary.contrastText, // Warm Gray (#D3CDBF) for text
backgroundColor: theme.palette.primary.main,
border: `1px solid ${theme.palette.secondary.main}`,
borderRadius: `${defaultRadius} ${defaultRadius} ${defaultRadius} 0`,
color: theme.palette.primary.contrastText,
},
'content': {
content: {
...defaultStyle,
backgroundColor: '#F5F2EA', // Light cream background for easy reading
border: `1px solid ${theme.palette.custom.highlight}`, // Golden Ochre border
backgroundColor: '#F5F2EA',
border: `1px solid ${theme.palette.custom.highlight}`,
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
alignSelf: 'center',
color: theme.palette.text.primary,
padding: '8px 8px',
marginBottom: '0px',
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.05)',
fontSize: '0.9rem',
lineHeight: '1.3',
fontFamily: theme.typography.fontFamily,
},
'error': {
error: {
...defaultStyle,
backgroundColor: '#F8E7E7', // Soft light red background
border: `1px solid #D83A3A`, // Prominent red border
backgroundColor: '#F8E7E7',
border: `1px solid #D83A3A`,
borderRadius: defaultRadius,
maxWidth: '90%',
minWidth: '90%',
alignSelf: 'center',
color: '#8B2525', // Deep red text for good contrast
color: '#8B2525',
padding: '10px 16px',
boxShadow: '0 1px 3px rgba(216, 58, 58, 0.15)', // Subtle shadow with red tint
boxShadow: '0 1px 3px rgba(216, 58, 58, 0.15)',
},
'fact-check': 'qualifications',
'job-description': 'content',
'job-requirements': 'qualifications',
'info': {
info: {
...defaultStyle,
backgroundColor: '#BFD8D8', // Softened Dusty Teal
border: `1px solid ${theme.palette.secondary.main}`, // Dusty Teal
backgroundColor: '#BFD8D8',
border: `1px solid ${theme.palette.secondary.main}`,
borderRadius: defaultRadius,
color: theme.palette.text.primary, // Charcoal Black (#2E2E2E) — much better contrast
color: theme.palette.text.primary,
opacity: 0.95,
},
'processing': "status",
'qualifications': {
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
backgroundColor: theme.palette.primary.light,
border: `1px solid ${theme.palette.secondary.main}`,
borderRadius: `${defaultRadius} ${defaultRadius} ${defaultRadius} 0`,
color: theme.palette.primary.contrastText,
},
'resume': 'content',
'searching': 'status',
'status': {
resume: 'content',
searching: 'status',
status: {
...defaultStyle,
backgroundColor: 'rgba(74, 122, 125, 0.15)', // Translucent dusty teal
border: `1px solid ${theme.palette.secondary.light}`, // Lighter dusty teal
backgroundColor: 'rgba(74, 122, 125, 0.15)',
border: `1px solid ${theme.palette.secondary.light}`,
borderRadius: '4px',
maxWidth: '75%',
minWidth: '75%',
alignSelf: 'center',
color: theme.palette.secondary.dark, // Darker dusty teal for text
fontWeight: 500, // Slightly bolder than normal
fontSize: '0.95rem', // Slightly smaller
color: theme.palette.secondary.dark,
fontWeight: 500,
fontSize: '0.95rem',
padding: '8px 12px',
opacity: 0.9,
transition: 'opacity 0.3s ease-in-out', // Smooth fade effect for appearing/disappearing
transition: 'opacity 0.3s ease-in-out',
},
'streaming': "assistant",
'system': {
streaming: 'assistant',
system: {
...defaultStyle,
backgroundColor: '#EDEAE0', // Soft warm gray that plays nice with #D3CDBF
border: `1px dashed ${theme.palette.custom.highlight}`, // Golden Ochre
backgroundColor: '#EDEAE0',
border: `1px dashed ${theme.palette.custom.highlight}`,
borderRadius: defaultRadius,
maxWidth: '90%',
minWidth: '90%',
alignSelf: 'center',
color: theme.palette.text.primary, // Charcoal Black
color: theme.palette.text.primary,
fontStyle: 'italic',
},
'thinking': "status",
'user': {
thinking: 'status',
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
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.custom.highlight}`,
borderRadius: `${defaultRadius} ${defaultRadius} 0 ${defaultRadius}`,
alignSelf: 'flex-end',
color: theme.palette.primary.main,
},
};
// Resolve string references in styles
for (const [key, value] of Object.entries(styles)) {
if (typeof (value) === "string") {
(styles as any)[key] = styles[value];
if (typeof value === 'string') {
styles[key] = styles[value];
}
}
const icons: any = {
"error": <ErrorOutline color='error' />,
"info": <InfoOutline color='info' />,
"processing": <LocationSearchingIcon />,
// "streaming": <Stream />,
"searching": <Memory />,
"thinking": <Psychology />,
"tooling": <LocationSearchingIcon />,
error: <ErrorOutline color="error" />,
info: <InfoOutline color="info" />,
processing: <LocationSearchingIcon />,
searching: <Memory />,
thinking: <Psychology />,
tooling: <LocationSearchingIcon />,
};
// Render Accordion for expandable content
if (expandable || (role === 'content' && title)) {
// Determine if Accordion is controlled
const isControlled = typeof expanded === 'boolean' && typeof onExpand === 'function';
return (
<Accordion
expanded={expanded}
expanded={isControlled ? expanded : undefined} // Omit expanded prop for uncontrolled
defaultExpanded={expanded} // Default to collapsed for uncontrolled Accordion
className={className}
onChange={() => { console.log(`onChange(${expanded} inverse)`); onExpand && onExpand(!expanded); }}
onChange={(_event, newExpanded) => {
if (isControlled && onExpand) {
onExpand(newExpanded); // Call onExpand with new state
}
}}
sx={{ ...styles[role], ...sx }}
>
<AccordionSummary
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>
<AccordionDetails sx={{ mt: 0, mb: 0, p: 0, pl: 2, pr: 2 }}>
{children}
@ -179,10 +198,20 @@ function ChatBubble(props: ChatBubbleProps) {
);
}
// Render non-expandable content
return (
<Box className={className} sx={{ ...(role in styles ? styles[role] : styles["status"]), gap: 1, display: "flex", ...sx, flexDirection: "row" }}>
<Box
className={className}
sx={{
...(role in styles ? styles[role] : styles['status']),
gap: 1,
display: 'flex',
...sx,
flexDirection: 'row',
}}
>
{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}
</Box>
</Box>

View File

@ -511,7 +511,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
>
{
filteredConversation.map((message, index) =>
<Message key={index} {...{ sendQuery, message, connectionBase, sessionId, setSnack, submitQuery }} />
<Message key={index} expanded={message.expanded === undefined ? true : message.expanded} {...{ sendQuery, message, connectionBase, sessionId, setSnack, submitQuery }} />
)
}
{

View File

@ -15,8 +15,6 @@ interface DocumentProps extends BackstoryElementProps {
const Document = (props: DocumentProps) => {
const { setSnack, submitQuery, filepath, content, title, expanded, disableCopy, onExpand, sessionId } = props;
console.log(`${filepath} expanded: ${expanded}`);
const [document, setDocument] = useState<string>("");
// Get the markdown

View File

@ -1,5 +1,4 @@
const getConnectionBase = (loc: any): string => {
console.log(`getConnectionBase(${loc})`)
if (!loc.host.match(/.*battle-linux.*/)) {
return loc.protocol + "//" + loc.host;
} else {

View File

@ -26,7 +26,7 @@ It was written by James Ketrenos in order to provide answers to
questions potential employers may have about his work history.
What would you like to know about James?
`
`,
}
];

View File

@ -1,4 +1,4 @@
import { useState, useRef, useEffect } from 'react';
import { useState, useRef } from 'react';
import Divider from '@mui/material/Divider';
import Accordion from '@mui/material/Accordion';
import AccordionSummary from '@mui/material/AccordionSummary';
@ -237,8 +237,8 @@ const MessageMeta = (props: MessageMetaProps) => {
};
const Message = (props: MessageProps) => {
const { message, submitQuery, sx, className, onExpand, expanded } = props;
const [metaExpanded, setMetaExpanded] = useState<boolean>(props.expanded || false);
const { message, submitQuery, sx, className, onExpand, expanded, sessionId, setSnack } = props;
const [metaExpanded, setMetaExpanded] = useState<boolean>(false);
const textFieldRef = useRef(null);
const handleMetaExpandClick = () => {
@ -287,7 +287,7 @@ const Message = (props: MessageProps) => {
overflow: "auto", /* Handles scrolling for the div */
}}
>
<StyledMarkdown {...{ content: formattedContent, submitQuery }} />
<StyledMarkdown {...{ content: formattedContent, submitQuery, sessionId, setSnack }} />
</Scrollable>
:
<Typography
@ -318,7 +318,7 @@ const Message = (props: MessageProps) => {
)}
</CardActions>
{message.metadata && <>
<Collapse in={expanded} timeout="auto" unmountOnExit>
<Collapse in={metaExpanded} timeout="auto" unmountOnExit>
<CardContent>
<MessageMeta messageProps={props} metadata={message.metadata} />
</CardContent>

View File

@ -2,7 +2,7 @@ import React from 'react';
import { MuiMarkdown } from 'mui-markdown';
import { SxProps, useTheme } from '@mui/material/styles';
import { Link } from '@mui/material';
import { ChatQuery, QueryOptions } from './ChatQuery';
import { ChatQuery } from './ChatQuery';
import Box from '@mui/material/Box';
import JsonView from '@uiw/react-json-view';
import { vscodeTheme } from '@uiw/react-json-view/vscode';
@ -11,12 +11,12 @@ import { Scrollable } from './Scrollable';
import { jsonrepair } from 'jsonrepair';
import './StyledMarkdown.css';
import { BackstoryElementProps } from './BackstoryTab';
interface StyledMarkdownProps {
interface StyledMarkdownProps extends BackstoryElementProps {
className?: string,
content: string,
sx?: SxProps,
submitQuery?: (prompt: string, tunables?: QueryOptions) => void,
};
const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProps) => {
@ -53,6 +53,7 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
displayDataTypes={false}
objectSortKeys={false}
collapsed={false}
shortenTextAfterLength={100}
value={JSON.parse(fixed)}>
<JsonView.String
render={({ children, ...reset }) => {
@ -84,17 +85,13 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
}
}
},
chatQuery: undefined
};
if (submitQuery) {
overrides.ChatQuery = {
ChatQuery: {
component: ChatQuery,
props: {
submitQuery,
},
};
}
}
};
return <Box
className={`MuiMarkdown ${className || ""}`}

View File

@ -108,7 +108,7 @@ const symbolMap: Record<string, string> = {
};
const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualizerProps) => {
const { setSnack, rag, inline, sessionId, sx } = props;
const { setSnack, rag, inline, sessionId, sx, submitQuery } = props;
const [plotData, setPlotData] = useState<PlotData | null>(null);
const [newQuery, setNewQuery] = useState<string>('');
const [newQueryEmbedding, setNewQueryEmbedding] = useState<ChromaResult | undefined>(undefined);
@ -436,7 +436,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
wordBreak: 'break-all',
}}
>
<StyledMarkdown sx={{ p: 1, pt: 0 }} content={tooltip?.content || "Select a node in the visualization."} />
<StyledMarkdown sx={{ p: 1, pt: 0 }} content={tooltip?.content || "Select a node in the visualization."} {...{ sessionId, setSnack, submitQuery }} />
</Scrollable>
}
</Card>