diff --git a/frontend/deployed/docs/about-app.md b/frontend/deployed/docs/about-app.md new file mode 100644 index 0000000..3d86792 --- /dev/null +++ b/frontend/deployed/docs/about-app.md @@ -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 \ No newline at end of file diff --git a/frontend/deployed/docs/resume-generation.md b/frontend/deployed/docs/resume-generation.md new file mode 100644 index 0000000..2ed3143 --- /dev/null +++ b/frontend/deployed/docs/resume-generation.md @@ -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. + diff --git a/frontend/src/AboutPage.tsx b/frontend/src/AboutPage.tsx index 9b33c68..7fe2224 100644 --- a/frontend/src/AboutPage.tsx +++ b/frontend/src/AboutPage.tsx @@ -10,11 +10,8 @@ const AboutPage = (props: BackstoryPageProps) => { const [ subRoute, setSubRoute] = useState(""); 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(""); } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 783b12d..90396f8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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: { ]; }, [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`, { diff --git a/frontend/src/ChatBubble.tsx b/frontend/src/ChatBubble.tsx index 9b221e7..bb82895 100644 --- a/frontend/src/ChatBubble.tsx +++ b/frontend/src/ChatBubble.tsx @@ -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": , - "info": , - "processing": , - // "streaming": , - "searching": , - "thinking": , - "tooling": , + error: , + info: , + processing: , + searching: , + thinking: , + tooling: , }; + // Render Accordion for expandable content if (expandable || (role === 'content' && title)) { + // Determine if Accordion is controlled + const isControlled = typeof expanded === 'boolean' && typeof onExpand === 'function'; + return ( { 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 }} > } - 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 || ''} {children} @@ -179,10 +198,20 @@ function ChatBubble(props: ChatBubbleProps) { ); } + // Render non-expandable content return ( - + {icons[role] !== undefined && icons[role]} - + {children} diff --git a/frontend/src/Conversation.tsx b/frontend/src/Conversation.tsx index 7ffc175..2b7672e 100644 --- a/frontend/src/Conversation.tsx +++ b/frontend/src/Conversation.tsx @@ -511,7 +511,7 @@ const Conversation = forwardRef(({ > { filteredConversation.map((message, index) => - + ) } { diff --git a/frontend/src/Document.tsx b/frontend/src/Document.tsx index 3de3294..207dcdd 100644 --- a/frontend/src/Document.tsx +++ b/frontend/src/Document.tsx @@ -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(""); // Get the markdown diff --git a/frontend/src/Global.tsx b/frontend/src/Global.tsx index 338e94d..af984fe 100644 --- a/frontend/src/Global.tsx +++ b/frontend/src/Global.tsx @@ -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 { diff --git a/frontend/src/HomePage.tsx b/frontend/src/HomePage.tsx index 7f3ef4d..af6ab32 100644 --- a/frontend/src/HomePage.tsx +++ b/frontend/src/HomePage.tsx @@ -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? -` +`, } ]; diff --git a/frontend/src/Message.tsx b/frontend/src/Message.tsx index d6aff5d..830f24e 100644 --- a/frontend/src/Message.tsx +++ b/frontend/src/Message.tsx @@ -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(props.expanded || false); + const { message, submitQuery, sx, className, onExpand, expanded, sessionId, setSnack } = props; + const [metaExpanded, setMetaExpanded] = useState(false); const textFieldRef = useRef(null); const handleMetaExpandClick = () => { @@ -287,7 +287,7 @@ const Message = (props: MessageProps) => { overflow: "auto", /* Handles scrolling for the div */ }} > - + : { )} {message.metadata && <> - + diff --git a/frontend/src/StyledMarkdown.tsx b/frontend/src/StyledMarkdown.tsx index 559c603..da8bf8f 100644 --- a/frontend/src/StyledMarkdown.tsx +++ b/frontend/src/StyledMarkdown.tsx @@ -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 = (props: StyledMarkdownProps) => { @@ -53,6 +53,7 @@ const StyledMarkdown: React.FC = (props: StyledMarkdownProp displayDataTypes={false} objectSortKeys={false} collapsed={false} + shortenTextAfterLength={100} value={JSON.parse(fixed)}> { @@ -84,17 +85,13 @@ const StyledMarkdown: React.FC = (props: StyledMarkdownProp } } }, - chatQuery: undefined - }; - - if (submitQuery) { - overrides.ChatQuery = { + ChatQuery: { component: ChatQuery, props: { submitQuery, }, - }; - } + } + }; return = { }; const VectorVisualizer: React.FC = (props: VectorVisualizerProps) => { - const { setSnack, rag, inline, sessionId, sx } = props; + const { setSnack, rag, inline, sessionId, sx, submitQuery } = props; const [plotData, setPlotData] = useState(null); const [newQuery, setNewQuery] = useState(''); const [newQueryEmbedding, setNewQueryEmbedding] = useState(undefined); @@ -436,7 +436,7 @@ const VectorVisualizer: React.FC = (props: VectorVisualiz wordBreak: 'break-all', }} > - + }