Compare commits
2 Commits
02915b9a23
...
ca9dd950b3
Author | SHA1 | Date | |
---|---|---|---|
ca9dd950b3 | |||
4ce616b64b |
@ -4,6 +4,11 @@ div {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.gl-container #scene {
|
||||
top: 0px !important;
|
||||
left: 0px !important;
|
||||
}
|
||||
|
||||
pre {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
|
@ -19,8 +19,7 @@ import MenuIcon from '@mui/icons-material/Menu';
|
||||
|
||||
|
||||
import { ResumeBuilder } from './ResumeBuilder';
|
||||
import { Message } from './Message';
|
||||
import { MessageData } from './MessageMeta';
|
||||
import { Message, ChatQuery, MessageList, MessageData } from './Message';
|
||||
import { SeverityType } from './Snack';
|
||||
import { VectorVisualizer } from './VectorVisualizer';
|
||||
import { Controls } from './Controls';
|
||||
@ -33,6 +32,8 @@ import '@fontsource/roboto/400.css';
|
||||
import '@fontsource/roboto/500.css';
|
||||
import '@fontsource/roboto/700.css';
|
||||
|
||||
import MuiMarkdown from 'mui-markdown';
|
||||
|
||||
|
||||
const getConnectionBase = (loc: any): string => {
|
||||
if (!loc.host.match(/.*battle-linux.*/)) {
|
||||
@ -130,10 +131,41 @@ const App = () => {
|
||||
}, [about, setAbout])
|
||||
|
||||
|
||||
const handleSubmitChatQuery = () => {
|
||||
chatRef.current?.submitQuery();
|
||||
const handleSubmitChatQuery = (query: string) => {
|
||||
console.log(`handleSubmitChatQuery: ${query} -- `, chatRef.current ? ' sending' : 'no handler');
|
||||
chatRef.current?.submitQuery(query);
|
||||
};
|
||||
|
||||
const chatPreamble: MessageList = [
|
||||
{
|
||||
role: 'info',
|
||||
content: `
|
||||
# Welcome to Backstory
|
||||
|
||||
Backstory is a RAG enabled expert system with access to real-time data running self-hosted
|
||||
(no cloud) versions of industry leading Large and Small Language Models (LLM/SLMs).
|
||||
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?
|
||||
`
|
||||
}
|
||||
];
|
||||
|
||||
const chatQuestions = [
|
||||
<Box sx={{ display: "flex", flexDirection: "row" }}>
|
||||
<ChatQuery text="What is James Ketrenos' work history?" submitQuery={handleSubmitChatQuery} />
|
||||
<ChatQuery text="What programming languages has James used?" submitQuery={handleSubmitChatQuery} />
|
||||
<ChatQuery text="What are James' professional strengths?" submitQuery={handleSubmitChatQuery} />
|
||||
<ChatQuery text="What are today's headlines on CNBC.com?" submitQuery={handleSubmitChatQuery} />
|
||||
</Box>,
|
||||
<MuiMarkdown>
|
||||
As with all LLM interactions, the results may not be 100% accurate. If you have questions about my career,
|
||||
I'd love to hear from you. You can send me an email at **james_backstory@ketrenos.com**.
|
||||
</MuiMarkdown>
|
||||
];
|
||||
|
||||
|
||||
// Extract the sessionId from the URL if present, otherwise
|
||||
// request a sessionId from the server.
|
||||
useEffect(() => {
|
||||
@ -368,10 +400,12 @@ const App = () => {
|
||||
ref={chatRef}
|
||||
{...{
|
||||
type: "chat",
|
||||
prompt: "Enter your question...",
|
||||
prompt: "What would you like to know about James?",
|
||||
sessionId,
|
||||
connectionBase,
|
||||
setSnack
|
||||
setSnack,
|
||||
preamble: chatPreamble,
|
||||
defaultPrompts: chatQuestions
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
@ -392,7 +426,7 @@ const App = () => {
|
||||
<CustomTabPanel tab={tab} index={3}>
|
||||
<Box className="ChatBox">
|
||||
<Box className="Conversation">
|
||||
<Message {...{ message: { role: 'assistant', content: about }, submitQuery: handleSubmitChatQuery }} />
|
||||
<Message {...{ message: { role: 'assistant', content: about }, submitQuery: handleSubmitChatQuery, connectionBase, sessionId, setSnack }} />
|
||||
</Box>
|
||||
</Box>
|
||||
</CustomTabPanel>
|
||||
|
@ -2,7 +2,7 @@ import { Box } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { SxProps, Theme } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { MessageRoles } from './MessageMeta';
|
||||
import { MessageRoles } from './Message';
|
||||
|
||||
interface ChatBubbleProps {
|
||||
role: MessageRoles,
|
||||
@ -16,64 +16,52 @@ interface ChatBubbleProps {
|
||||
function ChatBubble({ role, isFullWidth, children, sx, className }: ChatBubbleProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
const styles = {
|
||||
'user': {
|
||||
backgroundColor: theme.palette.background.default, // Warm Gray (#D3CDBF)
|
||||
border: `1px solid ${theme.palette.custom.highlight}`, // Golden Ochre (#D4A017)
|
||||
borderRadius: '16px 16px 0 16px', // Rounded, flat bottom-right for user
|
||||
padding: theme.spacing(1, 2),
|
||||
maxWidth: isFullWidth ? '100%' : '100%',
|
||||
minWidth: '80%',
|
||||
alignSelf: 'flex-end', // Right-aligned for user
|
||||
color: theme.palette.primary.main, // Midnight Blue (#1A2536) for text
|
||||
'& > *': {
|
||||
color: 'inherit', // Children inherit Midnight Blue unless overridden
|
||||
},
|
||||
},
|
||||
'assistant': {
|
||||
backgroundColor: theme.palette.primary.main, // Midnight Blue (#1A2536)
|
||||
border: `1px solid ${theme.palette.secondary.main}`, // Dusty Teal (#4A7A7D)
|
||||
borderRadius: '16px 16px 16px 0', // Rounded, flat bottom-left for assistant
|
||||
padding: theme.spacing(1, 2),
|
||||
maxWidth: isFullWidth ? '100%' : '100%',
|
||||
minWidth: '80%',
|
||||
alignSelf: 'flex-start', // Left-aligned for assistant
|
||||
color: theme.palette.primary.contrastText, // Warm Gray (#D3CDBF) for text
|
||||
'& > *': {
|
||||
color: 'inherit', // Children inherit Warm Gray unless overridden
|
||||
},
|
||||
},
|
||||
'system': {
|
||||
backgroundColor: '#EDEAE0', // Soft warm gray that plays nice with #D3CDBF
|
||||
border: `1px dashed ${theme.palette.custom.highlight}`, // Golden Ochre
|
||||
borderRadius: '12px',
|
||||
padding: theme.spacing(1, 2),
|
||||
maxWidth: isFullWidth ? '100%' : '90%',
|
||||
minWidth: '60%',
|
||||
alignSelf: 'center',
|
||||
color: theme.palette.text.primary, // Charcoal Black
|
||||
fontStyle: 'italic',
|
||||
fontSize: '0.95rem',
|
||||
'& > *': {
|
||||
color: 'inherit',
|
||||
},
|
||||
},
|
||||
'info': {
|
||||
backgroundColor: '#BFD8D8', // Softened Dusty Teal
|
||||
border: `1px solid ${theme.palette.secondary.main}`, // Dusty Teal
|
||||
borderRadius: '16px',
|
||||
padding: theme.spacing(1, 2),
|
||||
maxWidth: isFullWidth ? '100%' : '100%',
|
||||
minWidth: '70%',
|
||||
alignSelf: 'flex-start',
|
||||
color: theme.palette.text.primary, // Charcoal Black (#2E2E2E) — much better contrast
|
||||
opacity: 0.95,
|
||||
const defaultRadius = '16px';
|
||||
const defaultStyle = {
|
||||
padding: theme.spacing(1, 1),
|
||||
fontSize: '0.875rem',
|
||||
alignSelf: 'flex-start', // Left-aligned is used by default
|
||||
maxWidth: '100%',
|
||||
minWidth: '80%',
|
||||
'& > *': {
|
||||
color: 'inherit',
|
||||
color: 'inherit', // Children inherit 'color' from parent
|
||||
},
|
||||
}
|
||||
|
||||
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': {
|
||||
...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
|
||||
},
|
||||
'system': {
|
||||
...defaultStyle,
|
||||
backgroundColor: '#EDEAE0', // Soft warm gray that plays nice with #D3CDBF
|
||||
border: `1px dashed ${theme.palette.custom.highlight}`, // Golden Ochre
|
||||
borderRadius: defaultRadius,
|
||||
maxWidth: isFullWidth ? '100%' : '90%',
|
||||
alignSelf: 'center',
|
||||
color: theme.palette.text.primary, // Charcoal Black
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
'info': {
|
||||
...defaultStyle,
|
||||
backgroundColor: '#BFD8D8', // Softened Dusty Teal
|
||||
border: `1px solid ${theme.palette.secondary.main}`, // Dusty Teal
|
||||
borderRadius: defaultRadius,
|
||||
color: theme.palette.text.primary, // Charcoal Black (#2E2E2E) — much better contrast
|
||||
opacity: 0.95,
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -109,11 +109,11 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ "system-prompt": prompt }),
|
||||
body: JSON.stringify({ "system_prompt": prompt }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const newPrompt = data["system-prompt"];
|
||||
const newPrompt = data["system_prompt"];
|
||||
if (newPrompt !== serverSystemPrompt) {
|
||||
setServerSystemPrompt(newPrompt);
|
||||
setSystemPrompt(newPrompt)
|
||||
@ -141,11 +141,11 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ "message-history-length": length }),
|
||||
body: JSON.stringify({ "message_history_length": length }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const newLength = data["message-history-length"];
|
||||
const newLength = data["message_history_length"];
|
||||
if (newLength !== messageHistoryLength) {
|
||||
setMessageHistoryLength(newLength);
|
||||
setSnack("Message history length updated", "success");
|
||||
@ -159,7 +159,7 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
||||
sendMessageHistoryLength(messageHistoryLength);
|
||||
|
||||
}, [messageHistoryLength, setMessageHistoryLength, connectionBase, sessionId, setSnack]);
|
||||
const reset = async (types: ("rags" | "tools" | "history" | "system-prompt" | "message-history-length")[], message: string = "Update successful.") => {
|
||||
const reset = async (types: ("rags" | "tools" | "history" | "system_prompt" | "message_history_length")[], message: string = "Update successful.") => {
|
||||
try {
|
||||
const response = await fetch(connectionBase + `/api/reset/${sessionId}`, {
|
||||
method: 'PUT',
|
||||
@ -183,9 +183,9 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
||||
case "tools":
|
||||
setTools(value as Tool[]);
|
||||
break;
|
||||
case "system-prompt":
|
||||
setServerSystemPrompt((value as any)["system-prompt"].trim());
|
||||
setSystemPrompt((value as any)["system-prompt"].trim());
|
||||
case "system_prompt":
|
||||
setServerSystemPrompt((value as any)["system_prompt"].trim());
|
||||
setSystemPrompt((value as any)["system_prompt"].trim());
|
||||
break;
|
||||
case "history":
|
||||
console.log('TODO: handle history reset');
|
||||
@ -346,10 +346,10 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
const serverSystemPrompt = data["system-prompt"].trim();
|
||||
const serverSystemPrompt = data["system_prompt"].trim();
|
||||
setServerSystemPrompt(serverSystemPrompt);
|
||||
setSystemPrompt(serverSystemPrompt);
|
||||
setMessageHistoryLength(data["message-history-length"]);
|
||||
setMessageHistoryLength(data["message_history_length"]);
|
||||
}
|
||||
|
||||
fetchTunables();
|
||||
@ -402,7 +402,7 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
||||
/>
|
||||
<div style={{ display: "flex", flexDirection: "row", gap: "8px", paddingTop: "8px" }}>
|
||||
<Button variant="contained" disabled={editSystemPrompt === systemPrompt} onClick={() => { setSystemPrompt(editSystemPrompt); }}>Set</Button>
|
||||
<Button variant="outlined" onClick={() => { reset(["system-prompt"], "System prompt reset."); }} color="error">Reset</Button>
|
||||
<Button variant="outlined" onClick={() => { reset(["system_prompt"], "System prompt reset."); }} color="error">Reset</Button>
|
||||
</div>
|
||||
</AccordionActions>
|
||||
</Accordion>
|
||||
@ -481,7 +481,7 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
||||
</AccordionActions>
|
||||
</Accordion>
|
||||
<Button startIcon={<ResetIcon />} onClick={() => { reset(["history"], "History cleared."); }}>Clear Backstory History</Button>
|
||||
<Button onClick={() => { reset(["rags", "tools", "system-prompt", "message-history-length"], "Default settings restored.") }}>Reset system prompt, tunables, and RAG to defaults</Button>
|
||||
<Button onClick={() => { reset(["rags", "tools", "system_prompt", "message_history_length"], "Default settings restored.") }}>Reset system prompt, tunables, and RAG to defaults</Button>
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
@ -3,55 +3,44 @@ import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Box from '@mui/material/Box';
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
|
||||
import ResetIcon from '@mui/icons-material/RestartAlt';
|
||||
import { SxProps, Theme } from '@mui/material';
|
||||
import PropagateLoader from "react-spinners/PropagateLoader";
|
||||
|
||||
import { Message, MessageList } from './Message';
|
||||
import { Message, MessageList, MessageData } from './Message';
|
||||
import { SeverityType } from './Snack';
|
||||
import { ContextStatus } from './ContextStatus';
|
||||
import { MessageData } from './MessageMeta';
|
||||
|
||||
const welcomeMarkdown = `
|
||||
# Welcome to Backstory
|
||||
const loadingMessage: MessageData = { "role": "assistant", "content": "Establishing connection with server..." };
|
||||
|
||||
Backstory was written by James Ketrenos in order to provide answers to
|
||||
questions potential employers may have about his work history.
|
||||
You can ask things like:
|
||||
|
||||
<ChatQuery text="What is James Ketrenos' work history?"/>
|
||||
<ChatQuery text="What programming languages has James used?"/>
|
||||
<ChatQuery text="What are James' professional strengths?"/>
|
||||
<ChatQuery text="What are today's headlines on CNBC.com?"/>
|
||||
|
||||
You can click the text above to submit that query, or type it in yourself (or whatever questions you may have.)
|
||||
|
||||
Backstory is a RAG enabled expert system with access to real-time data running self-hosted
|
||||
(no cloud) versions of industry leading Large and Small Language Models (LLM/SLMs).
|
||||
|
||||
As with all LLM interactions, the results may not be 100% accurate. If you have questions about my career, I'd love to hear from you. You can send me an email at **james_backstory@ketrenos.com**.`;
|
||||
|
||||
const welcomeMessage: MessageData = {
|
||||
"role": "assistant", "content": welcomeMarkdown
|
||||
};
|
||||
const loadingMessage: MessageData = { "role": "assistant", "content": "Instancing chat session..." };
|
||||
|
||||
type ConversationMode = 'chat' | 'fact-check' | 'system';
|
||||
type ConversationMode = 'chat' | 'job_description' | 'resume' | 'fact_check';
|
||||
|
||||
interface ConversationHandle {
|
||||
submitQuery: () => void;
|
||||
submitQuery: (query: string) => void;
|
||||
}
|
||||
|
||||
interface ConversationProps {
|
||||
className?: string,
|
||||
type: ConversationMode
|
||||
prompt: string,
|
||||
actionLabel?: string,
|
||||
resetAction?: () => void,
|
||||
resetLabel?: string,
|
||||
connectionBase: string,
|
||||
sessionId: string | undefined,
|
||||
sessionId?: string,
|
||||
setSnack: (message: string, severity: SeverityType) => void,
|
||||
defaultPrompts?: React.ReactElement[],
|
||||
preamble?: MessageList,
|
||||
hideDefaultPrompts?: boolean,
|
||||
messageFilter?: (messages: MessageList) => MessageList,
|
||||
messages?: MessageList,
|
||||
sx?: SxProps<Theme>,
|
||||
};
|
||||
|
||||
const Conversation = forwardRef<ConversationHandle, ConversationProps>(({prompt, type, sessionId, setSnack, connectionBase} : ConversationProps, ref) => {
|
||||
const Conversation = forwardRef<ConversationHandle, ConversationProps>(({ ...props }: ConversationProps, ref) => {
|
||||
const [query, setQuery] = useState<string>("");
|
||||
const [contextUsedPercentage, setContextUsedPercentage] = useState<number>(0);
|
||||
const [processing, setProcessing] = useState<boolean>(false);
|
||||
@ -62,12 +51,14 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({prompt,
|
||||
const [lastPromptTPS, setLastPromptTPS] = useState<number>(430);
|
||||
const [contextStatus, setContextStatus] = useState<ContextStatus>({ context_used: 0, max_context: 0 });
|
||||
const [contextWarningShown, setContextWarningShown] = useState<boolean>(false);
|
||||
const [noInteractions, setNoInteractions] = useState<boolean>(true);
|
||||
const setSnack = props.setSnack;
|
||||
|
||||
// Update the context status
|
||||
const updateContextStatus = useCallback(() => {
|
||||
const fetchContextStatus = async () => {
|
||||
try {
|
||||
const response = await fetch(connectionBase + `/api/context-status/${sessionId}`, {
|
||||
const response = await fetch(props.connectionBase + `/api/context-status/${props.sessionId}/${props.type}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -87,35 +78,51 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({prompt,
|
||||
}
|
||||
};
|
||||
fetchContextStatus();
|
||||
}, [setContextStatus, connectionBase, setSnack, sessionId]);
|
||||
}, [setContextStatus, props.connectionBase, setSnack, props.sessionId, props.type]);
|
||||
|
||||
// Set the initial chat history to "loading" or the welcome message if loaded.
|
||||
useEffect(() => {
|
||||
if (sessionId === undefined) {
|
||||
if (props.sessionId === undefined) {
|
||||
setConversation([loadingMessage]);
|
||||
} else {
|
||||
fetch(connectionBase + `/api/history/${sessionId}`, {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchHistory = async () => {
|
||||
try {
|
||||
const response = await fetch(props.connectionBase + `/api/history/${props.sessionId}/${props.type}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log(`Session id: ${sessionId} -- history returned from server with ${data.length} entries`)
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
console.log(`History returned from server with ${data.length} entries`)
|
||||
if (data.length === 0) {
|
||||
setConversation([
|
||||
welcomeMessage,
|
||||
...data
|
||||
...(props.preamble || []),
|
||||
...(props.messages || []),
|
||||
]);
|
||||
})
|
||||
.catch(error => {
|
||||
setNoInteractions(true);
|
||||
} else {
|
||||
setConversation([
|
||||
...(props.messages || []),
|
||||
...(props.messageFilter ? props.messageFilter(data) : data)
|
||||
]);
|
||||
setNoInteractions(false);
|
||||
}
|
||||
updateContextStatus();
|
||||
} catch (error) {
|
||||
console.error('Error generating session ID:', error);
|
||||
setSnack("Unable to obtain chat history.", "error");
|
||||
});
|
||||
updateContextStatus();
|
||||
}
|
||||
}, [sessionId, setConversation, updateContextStatus, connectionBase, setSnack]);
|
||||
|
||||
};
|
||||
if (props.sessionId !== undefined) {
|
||||
fetchHistory();
|
||||
}
|
||||
}, [props.sessionId, setConversation, updateContextStatus, props.connectionBase, setSnack, props.preamble, props.type]);
|
||||
|
||||
const isScrolledToBottom = useCallback(()=> {
|
||||
// Current vertical scroll position
|
||||
@ -138,7 +145,6 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({prompt,
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
const startCountdown = (seconds: number) => {
|
||||
if (timerRef.current) clearInterval(timerRef.current);
|
||||
setCountdown(seconds);
|
||||
@ -159,10 +165,6 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({prompt,
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const submitQuery = (text: string) => {
|
||||
sendQuery(text);
|
||||
}
|
||||
|
||||
const stopCountdown = () => {
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
@ -182,11 +184,15 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({prompt,
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
submitQuery: () => {
|
||||
submitQuery: (query: string) => {
|
||||
sendQuery(query);
|
||||
}
|
||||
}));
|
||||
|
||||
const submitQuery = (query: string) => {
|
||||
sendQuery(query);
|
||||
}
|
||||
|
||||
// If context status changes, show a warning if necessary. If it drops
|
||||
// back below the threshold, clear the warning trigger
|
||||
useEffect(() => {
|
||||
@ -201,7 +207,43 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({prompt,
|
||||
setContextUsedPercentage(context_used_percentage)
|
||||
}, [contextStatus, setContextWarningShown, contextWarningShown, setContextUsedPercentage, setSnack]);
|
||||
|
||||
const reset = async () => {
|
||||
try {
|
||||
const response = await fetch(props.connectionBase + `/api/reset/${props.sessionId}/${props.type}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ reset: 'history' })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('Response body is null');
|
||||
}
|
||||
|
||||
props.messageFilter && props.messageFilter([]);
|
||||
|
||||
setConversation([
|
||||
...(props.preamble || []),
|
||||
...(props.messages || []),
|
||||
]);
|
||||
|
||||
setNoInteractions(true);
|
||||
|
||||
} catch (e) {
|
||||
setSnack("Error resetting history", "error")
|
||||
console.error('Error resetting history:', e);
|
||||
}
|
||||
};
|
||||
|
||||
const sendQuery = async (query: string) => {
|
||||
setNoInteractions(false);
|
||||
|
||||
if (!query.trim()) return;
|
||||
|
||||
//setTab(0);
|
||||
@ -237,7 +279,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({prompt,
|
||||
}
|
||||
|
||||
// Make the fetch request with proper headers
|
||||
const response = await fetch(connectionBase + `/api/chat/${sessionId}`, {
|
||||
const response = await fetch(props.connectionBase + `/api/chat/${props.sessionId}/${props.type}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -381,9 +423,12 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({prompt,
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="ConversationContainer" sx={{ display: "flex", flexDirection: "column", height: "100%", overflowY: "auto" }}>
|
||||
<Box className="Conversation" sx={{ flexGrow: 2, p: 1 }}>
|
||||
{conversation.map((message, index) => <Message key={index} submitQuery={submitQuery} message={message} />)}
|
||||
<Box className={props.className || "Conversation"} sx={{ ...props.sx, display: "flex", flexDirection: "column" }}>
|
||||
{
|
||||
conversation.map((message, index) =>
|
||||
<Message key={index} {...{ submitQuery, message, connectionBase: props.connectionBase, sessionId: props.sessionId, setSnack }} />
|
||||
)
|
||||
}
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
@ -407,7 +452,51 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({prompt,
|
||||
>Estimated response time: {countdown}s</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box sx={{ ml: "0.25rem", fontSize: "0.6rem", color: "darkgrey", display: "flex", flexDirection: "row", gap: 1, mt: "auto" }}>
|
||||
<Box className="Query" sx={{ display: "flex", flexDirection: props.type === "job_description" ? "column" : "row", p: 1 }}>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
disabled={processing}
|
||||
fullWidth
|
||||
multiline={props.type === "job_description"}
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={handleKeyPress}
|
||||
placeholder={props.prompt}
|
||||
id="QueryInput"
|
||||
/>
|
||||
<Box key="jobActions" sx={{ display: "flex", justifyContent: "center", flexDirection: "row" }}>
|
||||
<IconButton
|
||||
sx={{ display: "flex", margin: 'auto 0px' }}
|
||||
size="large"
|
||||
edge="start"
|
||||
color="inherit"
|
||||
onClick={() => { reset(); }}
|
||||
>
|
||||
<Tooltip title={props.resetLabel || "Reset"} >
|
||||
<ResetIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<Tooltip title={props.actionLabel || "Send"}>
|
||||
<Button
|
||||
sx={{ m: 1, gap: 1, flexGrow: 1 }}
|
||||
variant="contained"
|
||||
onClick={() => { sendQuery(query); }}>
|
||||
{props.actionLabel}<SendIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
{(noInteractions || !props.hideDefaultPrompts) && props.defaultPrompts !== undefined && props.defaultPrompts.length &&
|
||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||
{
|
||||
props.defaultPrompts.map((element, index) => {
|
||||
return (<Box key={index}>{element}</Box>);
|
||||
})
|
||||
}
|
||||
</Box>
|
||||
}
|
||||
<Box sx={{ ml: "0.25rem", fontSize: "0.6rem", color: "darkgrey", display: "flex", flexShrink: 1, flexDirection: "row", gap: 1, mb: "auto", mt: 1 }}>
|
||||
Context used: {contextUsedPercentage}% {contextStatus.context_used}/{contextStatus.max_context}
|
||||
{
|
||||
contextUsedPercentage >= 90 ? <Typography sx={{ fontSize: "0.6rem", color: "red" }}>WARNING: Context almost exhausted. You should start a new chat.</Typography>
|
||||
@ -415,23 +504,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({prompt,
|
||||
: <></>)
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box className="Query" sx={{ display: "flex", flexDirection: "row", p: 1 }}>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
disabled={processing}
|
||||
fullWidth
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={handleKeyPress}
|
||||
placeholder="Enter your question..."
|
||||
id="QueryInput"
|
||||
/>
|
||||
<Tooltip title="Send">
|
||||
<Button sx={{ m: 1 }} variant="contained" onClick={() => { sendQuery(query); }}><SendIcon /></Button>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", flexGrow: 1 }}></Box>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
@ -1,27 +0,0 @@
|
||||
import { SxProps, Theme } from '@mui/material';
|
||||
import { MessageData } from './MessageMeta';
|
||||
|
||||
/**
|
||||
* Props for the DocumentViewer component
|
||||
* @interface DocumentViewerProps
|
||||
* @property {function} generateResume - Function to generate a resume based on job description
|
||||
* @property {MessageData | undefined} resume - The generated resume data
|
||||
* @property {function} setResume - Function to set the generated resume
|
||||
* @property {function} factCheck - Function to fact check the generated resume
|
||||
* @property {MessageData | undefined} facts - The fact check results
|
||||
* @property {function} setFacts - Function to set the fact check results
|
||||
* @property {string} jobDescription - The initial job description
|
||||
* @property {function} setJobDescription - Function to set the job description
|
||||
* @property {SxProps<Theme>} [sx] - Optional styling properties
|
||||
*/
|
||||
export interface DocumentViewerProps {
|
||||
generateResume: (jobDescription: string) => void;
|
||||
resume: MessageData | undefined;
|
||||
setResume: (resume: MessageData | undefined) => void;
|
||||
factCheck: (resume: string) => void;
|
||||
facts: MessageData | undefined;
|
||||
setFacts: (facts: MessageData | undefined) => void;
|
||||
jobDescription: string | undefined;
|
||||
setJobDescription: (jobDescription: string | undefined) => void;
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
@ -24,12 +24,30 @@ import {
|
||||
RestartAlt as ResetIcon,
|
||||
} from '@mui/icons-material';
|
||||
import PropagateLoader from "react-spinners/PropagateLoader";
|
||||
import { SxProps, Theme } from '@mui/material';
|
||||
|
||||
import { Message } from './Message';
|
||||
import { Document } from './Document';
|
||||
import { DocumentViewerProps } from './DocumentTypes';
|
||||
import MuiMarkdown from 'mui-markdown';
|
||||
|
||||
import { Message, ChatQuery } from './Message';
|
||||
import { Document } from './Document';
|
||||
import { MessageData, MessageList } from './Message';
|
||||
import { SeverityType } from './Snack';
|
||||
import { Conversation } from './Conversation';
|
||||
|
||||
/**
|
||||
* Props for the DocumentViewer component
|
||||
* @interface DocumentViewerProps
|
||||
* @property {SxProps<Theme>} [sx] - Optional styling properties
|
||||
* @property {string} [connectionBase] - Base URL for fetch calls
|
||||
* @property {string} [sessionId] - Session ID
|
||||
* @property {(message: string, severity: SeverityType) => void} - setSnack UI callback
|
||||
*/
|
||||
export interface DocumentViewerProps {
|
||||
sx?: SxProps<Theme>;
|
||||
connectionBase: string;
|
||||
sessionId: string;
|
||||
setSnack: (message: string, severity: SeverityType) => void,
|
||||
}
|
||||
/**
|
||||
* DocumentViewer component
|
||||
*
|
||||
@ -37,16 +55,16 @@ import MuiMarkdown from 'mui-markdown';
|
||||
* with different layouts for mobile and desktop views.
|
||||
*/
|
||||
const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
||||
generateResume,
|
||||
jobDescription,
|
||||
factCheck,
|
||||
resume,
|
||||
setResume,
|
||||
facts,
|
||||
setFacts,
|
||||
sx
|
||||
sx,
|
||||
connectionBase,
|
||||
sessionId,
|
||||
setSnack
|
||||
}) => {
|
||||
// State for editing job description
|
||||
const [jobDescription, setJobDescription] = useState<string | undefined>(undefined);
|
||||
const [facts, setFacts] = useState<MessageData | undefined>(undefined);
|
||||
const [resume, setResume] = useState<MessageData | undefined>(undefined);
|
||||
|
||||
const [editJobDescription, setEditJobDescription] = useState<string | undefined>(jobDescription);
|
||||
// Processing state to show loading indicators
|
||||
const [processing, setProcessing] = useState<string | undefined>(undefined);
|
||||
@ -89,8 +107,8 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
||||
}
|
||||
setProcessing("resume");
|
||||
setTimeout(() => { setActiveTab(1); }, 250); // Switch to resume view on mobile
|
||||
generateResume(description);
|
||||
}, [generateResume, setProcessing, setActiveTab, setResume]);
|
||||
console.log('generateResume(description);');
|
||||
}, [/*generateResume*/, setProcessing, setActiveTab, setResume]);
|
||||
|
||||
/**
|
||||
* Trigger fact check and update UI state
|
||||
@ -104,9 +122,9 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
||||
return;
|
||||
}
|
||||
setProcessing("facts");
|
||||
factCheck(resume);
|
||||
console.log('factCheck(resume)');
|
||||
setTimeout(() => { setActiveTab(2); }, 250); // Switch to resume view on mobile
|
||||
}, [factCheck, setResume, setProcessing, setActiveTab, setFacts]);
|
||||
}, [/*factCheck,*/ setResume, setProcessing, setActiveTab, setFacts]);
|
||||
|
||||
useEffect(() => {
|
||||
setEditJobDescription(jobDescription);
|
||||
@ -159,62 +177,73 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
||||
triggerGeneration(editJobDescription || "");
|
||||
}
|
||||
};
|
||||
const handleJobQuery = (query: string) => {
|
||||
triggerGeneration(query);
|
||||
};
|
||||
|
||||
const renderJobDescriptionView = () => {
|
||||
const children = [];
|
||||
const jobDescriptionQuestions = [
|
||||
<Box sx={{ display: "flex", flexDirection: "row" }}>
|
||||
<ChatQuery text="What are the key skills necessary for this position?" submitQuery={handleJobQuery} />
|
||||
<ChatQuery text="How much should this position pay (accounting for inflation)?" submitQuery={handleJobQuery} />
|
||||
</Box>,
|
||||
];
|
||||
|
||||
if (resume === undefined && processing === undefined) {
|
||||
children.push(
|
||||
<Document key="jobDescription" sx={{ display: "flex", flexGrow: 1 }} title="">
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
multiline
|
||||
type="text"
|
||||
sx={{
|
||||
flex: 1,
|
||||
flexGrow: 1,
|
||||
maxHeight: '100%',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
value={editJobDescription}
|
||||
onChange={(e) => setEditJobDescription(e.target.value)}
|
||||
onKeyDown={handleKeyPress}
|
||||
placeholder="Paste a job description, then click Generate..."
|
||||
/>
|
||||
</Document>
|
||||
);
|
||||
} else {
|
||||
children.push(<MuiMarkdown key="jobDescription" >{editJobDescription}</MuiMarkdown>)
|
||||
const filterJobDescriptionMessages = (messages: MessageList): MessageList => {
|
||||
/* The second messages is the RESUME (the LLM response to the JOB-DESCRIPTION) */
|
||||
if (messages.length > 1) {
|
||||
setResume(messages[1]);
|
||||
} else if (resume !== undefined) {
|
||||
setResume(undefined);
|
||||
}
|
||||
|
||||
children.push(
|
||||
<Box key="jobActions" sx={{ display: "flex", justifyContent: "center", flexDirection: "row" }}>
|
||||
<IconButton
|
||||
sx={{ display: "flex", margin: 'auto 0px' }}
|
||||
size="large"
|
||||
edge="start"
|
||||
color="inherit"
|
||||
disabled={processing !== undefined}
|
||||
onClick={() => { setEditJobDescription(""); triggerGeneration(undefined); }}
|
||||
>
|
||||
<Tooltip title="Reset Job Description">
|
||||
<ResetIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<Tooltip title="Generate">
|
||||
<Button
|
||||
sx={{ m: 1, gap: 1, flexGrow: 1 }}
|
||||
variant="contained"
|
||||
onClick={() => { triggerGeneration(editJobDescription); }}
|
||||
>
|
||||
Generate<SendIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
);
|
||||
/* Filter out the RESUME */
|
||||
const reduced = messages.filter((message, index) => index != 1);
|
||||
|
||||
return children;
|
||||
/* Set the first message as coming from the assistant (rendered as markdown) */
|
||||
if (reduced.length > 0) {
|
||||
reduced[0].role = 'assistant';
|
||||
}
|
||||
return reduced;
|
||||
};
|
||||
|
||||
const jobDescriptionMessages: MessageList = [];
|
||||
|
||||
const renderJobDescriptionView = () => {
|
||||
if (resume === undefined) {
|
||||
return <Conversation
|
||||
{...{
|
||||
sx: { display: "flex", flexGrow: 1 },
|
||||
actionLabel: "Generate Resume",
|
||||
multiline: true,
|
||||
type: "job_description",
|
||||
prompt: "Paste a job description, then click Generate...",
|
||||
messageFilter: filterJobDescriptionMessages,
|
||||
messages: jobDescriptionMessages,
|
||||
sessionId,
|
||||
connectionBase,
|
||||
setSnack,
|
||||
defaultPrompts: jobDescriptionQuestions
|
||||
}}
|
||||
/>
|
||||
|
||||
} else {
|
||||
return <Conversation
|
||||
{...{
|
||||
className: "ChatBox",
|
||||
sx: { display: "flex", flexGrow: 1 },
|
||||
type: "job_description",
|
||||
actionLabel: "Send",
|
||||
prompt: "Ask a question about this job description...",
|
||||
messageFilter: filterJobDescriptionMessages,
|
||||
messages: jobDescriptionMessages,
|
||||
sessionId,
|
||||
connectionBase,
|
||||
setSnack,
|
||||
defaultPrompts: jobDescriptionQuestions
|
||||
}}
|
||||
/>
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -223,7 +252,7 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
||||
const renderResumeView = () => (
|
||||
<Box key="ResumeView" sx={{ display: "flex", flexDirection: "column", overflow: "auto", flexGrow: 1, flexBasis: 0 }}>
|
||||
<Document sx={{ display: "flex", flexGrow: 1 }} title="">
|
||||
{resume !== undefined && <Message message={resume} />}
|
||||
{resume !== undefined && <Message {...{ message: resume, connectionBase, sessionId, setSnack }} />}
|
||||
</Document>
|
||||
{processing === "resume" && (
|
||||
<Box sx={{
|
||||
@ -257,13 +286,7 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
||||
const renderFactCheckView = () => (
|
||||
<Box key="FactView" sx={{ display: "flex", flexDirection: "column", overflow: "auto", flexGrow: 1, flexBasis: 0, p: 0 }}>
|
||||
<Document sx={{ display: "flex", flexGrow: 1 }} title="">
|
||||
{facts !== undefined && <Message message={facts} />}
|
||||
{/* <pre>
|
||||
|
||||
With over 20 years of experience as a software architect, team lead, and developer, James Ketrenos brings a unique blend of technical expertise and leadership to the table. Focused on advancing energy-efficient AI solutions, he excels in designing, building, and deploying scalable systems that enable rapid product development. His extensive background in Linux software architecture, DevOps, and open-source technologies makes him an ideal candidate for leading roles at technology-driven companies.
|
||||
|
||||
---
|
||||
</pre> */}
|
||||
{facts !== undefined && <Message {...{ message: facts, connectionBase, sessionId, setSnack }} />}
|
||||
</Document>
|
||||
{processing === "facts" && (
|
||||
<Box sx={{
|
||||
@ -336,7 +359,7 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
||||
const otherRatio = showResume ? (100 - splitRatio / 2) : 100;
|
||||
const children = [];
|
||||
children.push(
|
||||
<Box key="JobDescription" sx={{ display: 'flex', flexDirection: 'column', width: `${otherRatio}%`, p: 0, flexGrow: 1, overflow: 'hidden' }}>
|
||||
<Box key="JobDescription" className="ChatBox" sx={{ display: 'flex', flexDirection: 'column', width: `${otherRatio}%`, p: 0, flexGrow: 1, overflowY: 'auto' }}>
|
||||
{renderJobDescriptionView()}
|
||||
</Box>);
|
||||
|
||||
@ -391,7 +414,7 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexGrow: 1, flexDirection: 'column', overflow: 'hidden', p: 0 }}>
|
||||
<Box sx={{ ...sx, display: 'flex', flexGrow: 1, flexDirection: 'column', p: 0 }}>
|
||||
<Box sx={{ display: 'flex', flexGrow: 1, flexDirection: 'row', overflow: 'hidden', p: 0 }}>
|
||||
{children}
|
||||
</Box>
|
||||
@ -401,7 +424,7 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1, ...sx }}>
|
||||
<Box sx={{ ...sx, display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
|
||||
{getActiveDesktopContent()}
|
||||
</Box>
|
||||
);
|
||||
|
@ -1,4 +1,15 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Accordion from '@mui/material/Accordion';
|
||||
import AccordionSummary from '@mui/material/AccordionSummary';
|
||||
import AccordionDetails from '@mui/material/AccordionDetails';
|
||||
import Card from '@mui/material/Card';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
@ -11,17 +22,50 @@ import { ExpandMore } from './ExpandMore';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
|
||||
import { MessageData, MessageMeta } from './MessageMeta';
|
||||
import { ChatBubble } from './ChatBubble';
|
||||
import { StyledMarkdown } from './StyledMarkdown';
|
||||
import { Tooltip } from '@mui/material';
|
||||
|
||||
import { VectorVisualizer } from './VectorVisualizer';
|
||||
import { SeverityType } from './Snack';
|
||||
|
||||
type MessageRoles = 'info' | 'user' | 'assistant' | 'system';
|
||||
|
||||
type MessageData = {
|
||||
role: MessageRoles,
|
||||
content: string,
|
||||
user?: string,
|
||||
type?: string,
|
||||
id?: string,
|
||||
isProcessing?: boolean,
|
||||
metadata?: MessageMetaProps
|
||||
};
|
||||
|
||||
interface MessageMetaProps {
|
||||
query?: {
|
||||
query_embedding: number[];
|
||||
vector_embedding: number[];
|
||||
},
|
||||
rag: any,
|
||||
tools: any[],
|
||||
eval_count: number,
|
||||
eval_duration: number,
|
||||
prompt_eval_count: number,
|
||||
prompt_eval_duration: number,
|
||||
sessionId?: string,
|
||||
connectionBase: string,
|
||||
setSnack: (message: string, severity: SeverityType) => void,
|
||||
}
|
||||
|
||||
type MessageList = MessageData[];
|
||||
|
||||
interface MessageInterface {
|
||||
interface MessageProps {
|
||||
message?: MessageData,
|
||||
isFullWidth?: boolean,
|
||||
submitQuery?: (text: string) => void
|
||||
submitQuery?: (text: string) => void,
|
||||
sessionId?: string,
|
||||
connectionBase: string,
|
||||
setSnack: (message: string, severity: SeverityType) => void,
|
||||
};
|
||||
|
||||
interface ChatQueryInterface {
|
||||
@ -29,18 +73,135 @@ interface ChatQueryInterface {
|
||||
submitQuery?: (text: string) => void
|
||||
}
|
||||
|
||||
|
||||
const MessageMeta = ({ ...props }: MessageMetaProps) => {
|
||||
return (<>
|
||||
<Box sx={{ fontSize: "0.8rem", mb: 1 }}>
|
||||
Below is the LLM performance of this query. Note that if tools are called, the
|
||||
entire context is processed for each separate tool request by the LLM. This
|
||||
can dramatically increase the total time for a response.
|
||||
</Box>
|
||||
<TableContainer component={Card} className="PromptStats" sx={{ mb: 1 }}>
|
||||
<Table aria-label="prompt stats" size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell></TableCell>
|
||||
<TableCell align="right" >Tokens</TableCell>
|
||||
<TableCell align="right">Time (s)</TableCell>
|
||||
<TableCell align="right">TPS</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow key="prompt" sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||
<TableCell component="th" scope="row">Prompt</TableCell>
|
||||
<TableCell align="right">{props.prompt_eval_count}</TableCell>
|
||||
<TableCell align="right">{Math.round(props.prompt_eval_duration / 10 ** 7) / 100}</TableCell>
|
||||
<TableCell align="right">{Math.round(props.prompt_eval_count * 10 ** 9 / props.prompt_eval_duration)}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow key="response" sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||
<TableCell component="th" scope="row">Response</TableCell>
|
||||
<TableCell align="right">{props.eval_count}</TableCell>
|
||||
<TableCell align="right">{Math.round(props.eval_duration / 10 ** 7) / 100}</TableCell>
|
||||
<TableCell align="right">{Math.round(props.eval_count * 10 ** 9 / props.eval_duration)}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow key="total" sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||
<TableCell component="th" scope="row">Total</TableCell>
|
||||
<TableCell align="right">{props.prompt_eval_count + props.eval_count}</TableCell>
|
||||
<TableCell align="right">{Math.round((props.prompt_eval_duration + props.eval_duration) / 10 ** 7) / 100}</TableCell>
|
||||
<TableCell align="right">{Math.round((props.prompt_eval_count + props.eval_count) * 10 ** 9 / (props.prompt_eval_duration + props.eval_duration))}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{
|
||||
props.tools !== undefined && props.tools.length !== 0 &&
|
||||
<Accordion sx={{ boxSizing: "border-box" }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Box sx={{ fontSize: "0.8rem" }}>
|
||||
Tools queried
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{props.tools.map((tool: any, index: number) => <Box key={index}>
|
||||
{index !== 0 && <Divider />}
|
||||
<Box sx={{ fontSize: "0.75rem", display: "flex", flexDirection: "column", mt: 0.5 }}>
|
||||
<div style={{ display: "flex", paddingRight: "1rem", whiteSpace: "nowrap" }}>
|
||||
{tool.tool}
|
||||
</div>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
padding: "3px",
|
||||
whiteSpace: "pre-wrap",
|
||||
flexGrow: 1,
|
||||
border: "1px solid #E0E0E0",
|
||||
wordBreak: "break-all",
|
||||
maxHeight: "5rem",
|
||||
overflow: "auto"
|
||||
}}>
|
||||
{JSON.stringify(tool.result, null, 2)}
|
||||
</div>
|
||||
</Box>
|
||||
</Box>)}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
}
|
||||
{
|
||||
props?.rag?.name !== undefined && <>
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Box sx={{ fontSize: "0.8rem" }}>
|
||||
Top RAG {props.rag.ids.length} matches from '{props.rag.name}' collection against embedding vector of {props.rag.query_embedding.length} dimensions
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{props.rag.ids.map((id: number, index: number) => <Box key={index}>
|
||||
{index !== 0 && <Divider />}
|
||||
<Box sx={{ fontSize: "0.75rem", display: "flex", flexDirection: "row", mb: 0.5, mt: 0.5 }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", paddingRight: "1rem", minWidth: "10rem" }}>
|
||||
<div style={{ whiteSpace: "nowrap" }}>Doc ID: {props.rag.ids[index].slice(-10)}</div>
|
||||
<div style={{ whiteSpace: "nowrap" }}>Similarity: {Math.round(props.rag.distances[index] * 100) / 100}</div>
|
||||
<div style={{ whiteSpace: "nowrap" }}>Type: {props.rag.metadatas[index].doc_type}</div>
|
||||
<div style={{ whiteSpace: "nowrap" }}>Chunk Len: {props.rag.documents[index].length}</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", padding: "3px", flexGrow: 1, border: "1px solid #E0E0E0", maxHeight: "5rem", overflow: "auto" }}>{props.rag.documents[index]}</div>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Box sx={{ fontSize: "0.8rem" }}>
|
||||
UMAP Vector Visualization of RAG
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<VectorVisualizer inline {...props} rag={props?.rag} />
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ChatQuery = ({ text, submitQuery }: ChatQueryInterface) => {
|
||||
return (submitQuery
|
||||
? <Button variant="outlined" sx={{
|
||||
if (submitQuery === undefined) {
|
||||
return (<Box>{text}</Box>);
|
||||
}
|
||||
return (
|
||||
<Button variant="outlined" sx={{
|
||||
color: theme => theme.palette.custom.highlight, // Golden Ochre (#D4A017)
|
||||
borderColor: theme => theme.palette.custom.highlight,
|
||||
m: 1
|
||||
}}
|
||||
size="small" onClick={(e: any) => { console.log(text); submitQuery(text); }}>{text}</Button>
|
||||
: <Box>{text}</Box>);
|
||||
size="small" onClick={(e: any) => { console.log(text); submitQuery(text); }}>
|
||||
{text}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
const Message = ({ message, submitQuery, isFullWidth }: MessageInterface) => {
|
||||
const Message = ({ message, submitQuery, isFullWidth, sessionId, setSnack, connectionBase }: MessageProps) => {
|
||||
const [expanded, setExpanded] = useState<boolean>(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const textFieldRef = useRef(null);
|
||||
@ -72,15 +233,15 @@ const Message = ({ message, submitQuery, isFullWidth }: MessageInterface) => {
|
||||
const formattedContent = message.content.trim();
|
||||
|
||||
return (
|
||||
<ChatBubble className="Message" isFullWidth={isFullWidth} role={message.role} sx={{ flexGrow: 1, pb: message.metadata ? 0 : "8px", m: 0, mb: 1, mt: 1, overflowX: "auto" }}>
|
||||
<ChatBubble className="Message" isFullWidth={isFullWidth} role={message.role} sx={{ display: "flex", flexDirection: "column", pb: message.metadata ? 0 : "8px", m: 0, mb: 1, mt: 1, overflowX: "auto" }}>
|
||||
<CardContent ref={textFieldRef} sx={{ position: "relative", display: "flex", flexDirection: "column", overflowX: "auto" }}>
|
||||
<Tooltip title="Copy to clipboard" placement="top" arrow>
|
||||
<IconButton
|
||||
onClick={handleCopy}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
top: 0,
|
||||
right: 0,
|
||||
width: 24,
|
||||
height: 24,
|
||||
bgcolor: 'background.paper',
|
||||
@ -97,7 +258,6 @@ const Message = ({ message, submitQuery, isFullWidth }: MessageInterface) => {
|
||||
<StyledMarkdown
|
||||
className="MessageContent"
|
||||
sx={{ display: "flex", color: 'text.secondary' }}
|
||||
|
||||
{...{ content: formattedContent, submitQuery }} />
|
||||
:
|
||||
<Typography
|
||||
@ -110,8 +270,8 @@ const Message = ({ message, submitQuery, isFullWidth }: MessageInterface) => {
|
||||
}
|
||||
</CardContent>
|
||||
{message.metadata && <>
|
||||
<CardActions disableSpacing>
|
||||
<Typography sx={{ color: "darkgrey", p: 1, textAlign: "end", flexGrow: 1 }}>LLM information for this query</Typography>
|
||||
<CardActions disableSpacing sx={{ justifySelf: "flex-end" }}>
|
||||
<Button variant="text" onClick={handleExpandClick} sx={{ color: "darkgrey", p: 1, flexGrow: 0 }}>LLM information for this query</Button>
|
||||
<ExpandMore
|
||||
expand={expanded}
|
||||
onClick={handleExpandClick}
|
||||
@ -123,7 +283,7 @@ const Message = ({ message, submitQuery, isFullWidth }: MessageInterface) => {
|
||||
</CardActions>
|
||||
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
||||
<CardContent>
|
||||
<MessageMeta metadata={message.metadata} />
|
||||
<MessageMeta {...{ ...message.metadata, sessionId, connectionBase, setSnack }} />
|
||||
</CardContent>
|
||||
</Collapse>
|
||||
</>}
|
||||
@ -132,12 +292,17 @@ const Message = ({ message, submitQuery, isFullWidth }: MessageInterface) => {
|
||||
};
|
||||
|
||||
export type {
|
||||
MessageInterface,
|
||||
MessageProps,
|
||||
MessageList,
|
||||
ChatQueryInterface,
|
||||
MessageMetaProps,
|
||||
MessageData,
|
||||
MessageRoles
|
||||
};
|
||||
|
||||
export {
|
||||
Message,
|
||||
ChatQuery,
|
||||
MessageMeta
|
||||
};
|
||||
|
||||
|
@ -1,138 +0,0 @@
|
||||
//import React, { useState, useEffect, useRef, useCallback, ReactElement } from 'react';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Accordion from '@mui/material/Accordion';
|
||||
import AccordionSummary from '@mui/material/AccordionSummary';
|
||||
import AccordionDetails from '@mui/material/AccordionDetails';
|
||||
import Box from '@mui/material/Box';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import Card from '@mui/material/Card';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
|
||||
type MessageMetadata = {
|
||||
rag: any,
|
||||
tools: any[],
|
||||
eval_count: number,
|
||||
eval_duration: number,
|
||||
prompt_eval_count: number,
|
||||
prompt_eval_duration: number
|
||||
};
|
||||
|
||||
type MessageRoles = 'info' | 'user' | 'assistant' | 'system';
|
||||
|
||||
type MessageData = {
|
||||
role: MessageRoles,
|
||||
content: string,
|
||||
user?: string,
|
||||
type?: string,
|
||||
id?: string,
|
||||
isProcessing?: boolean,
|
||||
metadata?: MessageMetadata
|
||||
};
|
||||
|
||||
interface MessageMetaInterface {
|
||||
metadata: MessageMetadata
|
||||
}
|
||||
const MessageMeta = ({ metadata }: MessageMetaInterface) => {
|
||||
if (metadata === undefined) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (<>
|
||||
<Box sx={{ fontSize: "0.8rem", mb: 1 }}>
|
||||
Below is the LLM performance of this query. Note that if tools are called, the entire context is processed for each separate tool request by the LLM. This can dramatically increase the total time for a response.
|
||||
</Box>
|
||||
<TableContainer component={Card} className="PromptStats" sx={{ mb: 1 }}>
|
||||
<Table aria-label="prompt stats" size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell></TableCell>
|
||||
<TableCell align="right" >Tokens</TableCell>
|
||||
<TableCell align="right">Time (s)</TableCell>
|
||||
<TableCell align="right">TPS</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow key="prompt" sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||
<TableCell component="th" scope="row">Prompt</TableCell>
|
||||
<TableCell align="right">{metadata.prompt_eval_count}</TableCell>
|
||||
<TableCell align="right">{Math.round(metadata.prompt_eval_duration / 10 ** 7) / 100}</TableCell>
|
||||
<TableCell align="right">{Math.round(metadata.prompt_eval_count * 10 ** 9 / metadata.prompt_eval_duration)}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow key="response" sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||
<TableCell component="th" scope="row">Response</TableCell>
|
||||
<TableCell align="right">{metadata.eval_count}</TableCell>
|
||||
<TableCell align="right">{Math.round(metadata.eval_duration / 10 ** 7) / 100}</TableCell>
|
||||
<TableCell align="right">{Math.round(metadata.eval_count * 10 ** 9 / metadata.eval_duration)}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow key="total" sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||
<TableCell component="th" scope="row">Total</TableCell>
|
||||
<TableCell align="right">{metadata.prompt_eval_count + metadata.eval_count}</TableCell>
|
||||
<TableCell align="right">{Math.round((metadata.prompt_eval_duration + metadata.eval_duration) / 10 ** 7) / 100}</TableCell>
|
||||
<TableCell align="right">{Math.round((metadata.prompt_eval_count + metadata.eval_count) * 10 ** 9 / (metadata.prompt_eval_duration + metadata.eval_duration))}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{
|
||||
metadata.tools !== undefined && metadata.tools.length !== 0 &&
|
||||
<Accordion sx={{ boxSizing: "border-box" }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Box sx={{ fontSize: "0.8rem" }}>
|
||||
Tools queried
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{metadata.tools.map((tool: any, index: number) => <Box key={index}>
|
||||
{index !== 0 && <Divider />}
|
||||
<Box sx={{ fontSize: "0.75rem", display: "flex", flexDirection: "column", mt: 0.5 }}>
|
||||
<div style={{ display: "flex", paddingRight: "1rem", whiteSpace: "nowrap" }}>
|
||||
{tool.tool}
|
||||
</div>
|
||||
<div style={{ display: "flex", padding: "3px", whiteSpace: "pre-wrap", flexGrow: 1, border: "1px solid #E0E0E0", wordBreak: "break-all", maxHeight: "5rem", overflow: "auto" }}>{JSON.stringify(tool.result, null, 2)}</div>
|
||||
</Box>
|
||||
</Box>)}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
}
|
||||
{
|
||||
metadata?.rag?.name !== undefined &&
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Box sx={{ fontSize: "0.8rem" }}>
|
||||
Top RAG {metadata.rag.ids.length} matches from '{metadata.rag.name}' collection against embedding vector of {metadata.rag.query_embedding.length} dimensions
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{metadata.rag.ids.map((id: number, index: number) => <Box key={index}>
|
||||
{index !== 0 && <Divider />}
|
||||
<Box sx={{ fontSize: "0.75rem", display: "flex", flexDirection: "row", mb: 0.5, mt: 0.5 }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", paddingRight: "1rem", minWidth: "10rem" }}>
|
||||
<div style={{ whiteSpace: "nowrap" }}>Doc ID: {metadata.rag.ids[index].slice(-10)}</div>
|
||||
<div style={{ whiteSpace: "nowrap" }}>Similarity: {Math.round(metadata.rag.distances[index] * 100) / 100}</div>
|
||||
<div style={{ whiteSpace: "nowrap" }}>Type: {metadata.rag.metadatas[index].doc_type}</div>
|
||||
<div style={{ whiteSpace: "nowrap" }}>Chunk Len: {metadata.rag.documents[index].length}</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", padding: "3px", flexGrow: 1, border: "1px solid #E0E0E0", maxHeight: "5rem", overflow: "auto" }}>{metadata.rag.documents[index]}</div>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export type {
|
||||
MessageMetadata,
|
||||
MessageMetaInterface,
|
||||
MessageData,
|
||||
MessageRoles,
|
||||
};
|
||||
|
||||
export { MessageMeta };
|
@ -1,8 +1,7 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { SeverityType } from './Snack';
|
||||
import { ContextStatus } from './ContextStatus';
|
||||
import { MessageData, MessageMetadata } from './MessageMeta';
|
||||
import { MessageData, MessageMetaProps } from './Message';
|
||||
import { DocumentViewer } from './DocumentViewer';
|
||||
|
||||
interface ResumeBuilderProps {
|
||||
@ -21,324 +20,15 @@ type Resume = {
|
||||
resume: MessageData | undefined,
|
||||
fact_check: MessageData | undefined,
|
||||
job_description: string,
|
||||
metadata: MessageMetadata
|
||||
metadata: MessageMetaProps
|
||||
};
|
||||
|
||||
const ResumeBuilder = ({ facts, setFacts, resume, setResume, setProcessing, processing, connectionBase, sessionId, setSnack }: ResumeBuilderProps) => {
|
||||
const [lastEvalTPS, setLastEvalTPS] = useState<number>(35);
|
||||
const [lastPromptTPS, setLastPromptTPS] = useState<number>(430);
|
||||
const [contextStatus, setContextStatus] = useState<ContextStatus>({ context_used: 0, max_context: 0 });
|
||||
const [jobDescription, setJobDescription] = useState<string | undefined>(undefined);
|
||||
|
||||
const updateContextStatus = useCallback(() => {
|
||||
fetch(connectionBase + `/api/context-status/${sessionId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
setContextStatus(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error getting context status:', error);
|
||||
setSnack("Unable to obtain context status.", "error");
|
||||
});
|
||||
}, [setContextStatus, connectionBase, setSnack, sessionId]);
|
||||
|
||||
// If the jobDescription and resume have not been set, fetch them from the server
|
||||
useEffect(() => {
|
||||
if (sessionId === undefined) {
|
||||
return;
|
||||
}
|
||||
if (jobDescription !== undefined) {
|
||||
return;
|
||||
}
|
||||
const fetchResume = async () => {
|
||||
try {
|
||||
// Make the fetch request with proper headers
|
||||
const response = await fetch(connectionBase + `/api/resume/${sessionId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw Error();
|
||||
}
|
||||
const data: Resume[] = await response.json();
|
||||
if (data.length) {
|
||||
const lastResume = data[data.length - 1];
|
||||
console.log(lastResume);
|
||||
setJobDescription(lastResume['job_description']);
|
||||
setResume(lastResume.resume);
|
||||
if (lastResume['fact_check'] !== undefined && lastResume['fact_check'] !== null) {
|
||||
lastResume['fact_check'].role = 'info';
|
||||
setFacts(lastResume['fact_check'])
|
||||
} else {
|
||||
setFacts(undefined)
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
setSnack("Unable to fetch resume", "error");
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
fetchResume();
|
||||
}, [sessionId, resume, jobDescription, setResume, setJobDescription, setSnack, setFacts, connectionBase]);
|
||||
|
||||
// const startCountdown = (seconds: number) => {
|
||||
// if (timerRef.current) clearInterval(timerRef.current);
|
||||
// setCountdown(seconds);
|
||||
// timerRef.current = setInterval(() => {
|
||||
// setCountdown((prev) => {
|
||||
// if (prev <= 1) {
|
||||
// clearInterval(timerRef.current);
|
||||
// timerRef.current = null;
|
||||
// if (isScrolledToBottom()) {
|
||||
// setTimeout(() => {
|
||||
// scrollToBottom();
|
||||
// }, 50)
|
||||
// }
|
||||
// return 0;
|
||||
// }
|
||||
// return prev - 1;
|
||||
// });
|
||||
// }, 1000);
|
||||
// };
|
||||
|
||||
// const stopCountdown = () => {
|
||||
// if (timerRef.current) {
|
||||
// clearInterval(timerRef.current);
|
||||
// timerRef.current = null;
|
||||
// setCountdown(0);
|
||||
// }
|
||||
// };
|
||||
|
||||
if (sessionId === undefined) {
|
||||
return (<></>);
|
||||
}
|
||||
|
||||
const generateResume = async (description: string) => {
|
||||
if (!description.trim()) return;
|
||||
setResume(undefined);
|
||||
setFacts(undefined);
|
||||
|
||||
try {
|
||||
setProcessing(true);
|
||||
|
||||
// Add initial processing message
|
||||
//setGenerateStatus({ role: 'assistant', content: 'Processing request...' });
|
||||
|
||||
// Make the fetch request with proper headers
|
||||
const response = await fetch(connectionBase + `/api/generate-resume/${sessionId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ content: description.trim() }),
|
||||
});
|
||||
|
||||
// We'll guess that the response will be around 500 tokens...
|
||||
const token_guess = 500;
|
||||
const estimate = Math.round(token_guess / lastEvalTPS + contextStatus.context_used / lastPromptTPS);
|
||||
|
||||
setSnack(`Job description sent. Response estimated in ${estimate}s.`, "info");
|
||||
//startCountdown(Math.round(estimate));
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('Response body is null');
|
||||
}
|
||||
|
||||
// Set up stream processing with explicit chunking
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
|
||||
// Process each complete line immediately
|
||||
buffer += chunk;
|
||||
let lines = buffer.split('\n');
|
||||
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
|
||||
try {
|
||||
const update = JSON.parse(line);
|
||||
|
||||
// Force an immediate state update based on the message type
|
||||
if (update.status === 'processing') {
|
||||
// Update processing message with immediate re-render
|
||||
//setGenerateStatus({ role: 'info', content: update.message });
|
||||
console.log(update.num_ctx);
|
||||
|
||||
// Add a small delay to ensure React has time to update the UI
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
} else if (update.status === 'done') {
|
||||
// Replace processing message with final result
|
||||
//setGenerateStatus(undefined);
|
||||
setResume(update.message);
|
||||
const metadata = update.message.metadata;
|
||||
const evalTPS = metadata.eval_count * 10 ** 9 / metadata.eval_duration;
|
||||
const promptTPS = metadata.prompt_eval_count * 10 ** 9 / metadata.prompt_eval_duration;
|
||||
setLastEvalTPS(evalTPS ? evalTPS : 35);
|
||||
setLastPromptTPS(promptTPS ? promptTPS : 35);
|
||||
updateContextStatus();
|
||||
} else if (update.status === 'error') {
|
||||
// Show error
|
||||
//setGenerateStatus({ role: 'error', content: update.message });
|
||||
}
|
||||
} catch (e) {
|
||||
setSnack("Error generating resume", "error")
|
||||
console.error('Error parsing JSON:', e, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process any remaining buffer content
|
||||
if (buffer.trim()) {
|
||||
try {
|
||||
const update = JSON.parse(buffer);
|
||||
|
||||
if (update.status === 'done') {
|
||||
//setGenerateStatus(undefined);
|
||||
setResume(update.message);
|
||||
}
|
||||
} catch (e) {
|
||||
setSnack("Error processing job description", "error")
|
||||
}
|
||||
}
|
||||
|
||||
//stopCountdown();
|
||||
setProcessing(false);
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
setSnack("Unable to process job description", "error");
|
||||
//setGenerateStatus({ role: 'error', content: `Error: ${error}` });
|
||||
setProcessing(false);
|
||||
//stopCountdown();
|
||||
}
|
||||
};
|
||||
|
||||
const factCheck = async (resume: string) => {
|
||||
if (!resume.trim()) return;
|
||||
setFacts(undefined);
|
||||
|
||||
try {
|
||||
setProcessing(true);
|
||||
|
||||
const response = await fetch(connectionBase + `/api/fact-check/${sessionId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ content: resume.trim() }),
|
||||
});
|
||||
|
||||
// We'll guess that the response will be around 500 tokens...
|
||||
const token_guess = 500;
|
||||
const estimate = Math.round(token_guess / lastEvalTPS + contextStatus.context_used / lastPromptTPS);
|
||||
|
||||
setSnack(`Resume sent for Fact Check. Response estimated in ${estimate}s.`, "info");
|
||||
//startCountdown(Math.round(estimate));
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('Response body is null');
|
||||
}
|
||||
|
||||
// Set up stream processing with explicit chunking
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
|
||||
// Process each complete line immediately
|
||||
buffer += chunk;
|
||||
let lines = buffer.split('\n');
|
||||
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
|
||||
try {
|
||||
const update = JSON.parse(line);
|
||||
|
||||
// Force an immediate state update based on the message type
|
||||
if (update.status === 'processing') {
|
||||
// Add a small delay to ensure React has time to update the UI
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
} else if (update.status === 'done') {
|
||||
// Replace processing message with final result
|
||||
update.message.role = 'info';
|
||||
setFacts(update.message);
|
||||
const metadata = update.message.metadata;
|
||||
const evalTPS = metadata.eval_count * 10 ** 9 / metadata.eval_duration;
|
||||
const promptTPS = metadata.prompt_eval_count * 10 ** 9 / metadata.prompt_eval_duration;
|
||||
setLastEvalTPS(evalTPS ? evalTPS : 35);
|
||||
setLastPromptTPS(promptTPS ? promptTPS : 35);
|
||||
updateContextStatus();
|
||||
} else if (update.status === 'error') {
|
||||
}
|
||||
} catch (e) {
|
||||
setSnack("Error generating resume", "error")
|
||||
console.error('Error parsing JSON:', e, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process any remaining buffer content
|
||||
if (buffer.trim()) {
|
||||
try {
|
||||
const update = JSON.parse(buffer);
|
||||
|
||||
if (update.status === 'done') {
|
||||
update.message.role = 'info';
|
||||
setFacts(update.message);
|
||||
}
|
||||
} catch (e) {
|
||||
setSnack("Error processing resume", "error")
|
||||
}
|
||||
}
|
||||
|
||||
//stopCountdown();
|
||||
setProcessing(false);
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
setSnack("Unable to process resume", "error");
|
||||
//setGenerateStatus({ role: 'error', content: `Error: ${error}` });
|
||||
setProcessing(false);
|
||||
//stopCountdown();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="DocBox">
|
||||
<Box className="Conversation" sx={{ p: 0, pt: 1 }}>
|
||||
@ -350,7 +40,7 @@ const ResumeBuilder = ({ facts, setFacts, resume, setResume, setProcessing, proc
|
||||
overflowY: "auto",
|
||||
flexDirection: "column",
|
||||
height: "calc(0vh - 0px)", // Hack to make the height work
|
||||
}} {...{ factCheck, facts, jobDescription, generateResume, resume, setFacts, setResume, setJobDescription }} />
|
||||
}} {...{ setSnack, connectionBase, sessionId }} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
@ -7,6 +7,8 @@ import TextField from '@mui/material/TextField';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Button from '@mui/material/Button';
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import Switch from '@mui/material/Switch';
|
||||
|
||||
import { SeverityType } from './Snack';
|
||||
|
||||
@ -19,6 +21,8 @@ interface ResultData {
|
||||
embeddings: number[][] | number[][][];
|
||||
documents: string[];
|
||||
metadatas: Metadata[];
|
||||
ids: string[];
|
||||
dimensions: number;
|
||||
}
|
||||
|
||||
interface PlotData {
|
||||
@ -39,6 +43,8 @@ interface VectorVisualizerProps {
|
||||
connectionBase: string;
|
||||
sessionId?: string;
|
||||
setSnack: (message: string, severity: SeverityType) => void;
|
||||
inline?: boolean;
|
||||
rag?: any;
|
||||
}
|
||||
|
||||
interface ChromaResult {
|
||||
@ -48,7 +54,8 @@ interface ChromaResult {
|
||||
metadatas: Metadata[];
|
||||
query_embedding: number[];
|
||||
query?: string;
|
||||
vector_embedding?: number[];
|
||||
umap_embedding_2d?: number[];
|
||||
umap_embedding_3d?: number[];
|
||||
}
|
||||
|
||||
const normalizeDimension = (arr: number[]): number[] => {
|
||||
@ -89,11 +96,12 @@ const symbolMap: Record<string, string> = {
|
||||
'query': 'circle',
|
||||
};
|
||||
|
||||
const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, connectionBase, sessionId }) => {
|
||||
const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, rag, inline, connectionBase, sessionId }) => {
|
||||
const [plotData, setPlotData] = useState<PlotData | null>(null);
|
||||
const [query, setQuery] = useState<string>('');
|
||||
const [queryEmbedding, setQueryEmbedding] = useState<ChromaResult | undefined>(undefined);
|
||||
const [newQuery, setNewQuery] = useState<string>('');
|
||||
const [newQueryEmbedding, setNewQueryEmbedding] = useState<ChromaResult | undefined>(undefined);
|
||||
const [result, setResult] = useState<ResultData | undefined>(undefined);
|
||||
const [view2D, setView2D] = useState<boolean>(true);
|
||||
const [tooltip, setTooltip] = useState<{
|
||||
visible: boolean,
|
||||
// x: number,
|
||||
@ -105,7 +113,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, connectio
|
||||
|
||||
// Get the collection to visualize
|
||||
useEffect(() => {
|
||||
if (result !== undefined || sessionId === undefined) {
|
||||
if ((result !== undefined && result.dimensions !== (view2D ? 3 : 2)) || sessionId === undefined) {
|
||||
return;
|
||||
}
|
||||
const fetchCollection = async () => {
|
||||
@ -115,9 +123,10 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, connectio
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ dimensions: 3 }),
|
||||
body: JSON.stringify({ dimensions: view2D ? 2 : 3 }),
|
||||
});
|
||||
const data = await response.json();
|
||||
const data: ResultData = await response.json();
|
||||
data.dimensions = view2D ? 2 : 3;
|
||||
setResult(data);
|
||||
} catch (error) {
|
||||
console.error('Error obtaining collection information:', error);
|
||||
@ -126,7 +135,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, connectio
|
||||
};
|
||||
|
||||
fetchCollection();
|
||||
}, [result, setResult, connectionBase, setSnack, sessionId])
|
||||
}, [result, setResult, connectionBase, setSnack, sessionId, view2D])
|
||||
|
||||
useEffect(() => {
|
||||
if (!result || !result.embeddings) return;
|
||||
@ -135,12 +144,31 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, connectio
|
||||
const vectors: number[][] = [...result.embeddings as number[][]];
|
||||
const documents = [...result.documents || []];
|
||||
const metadatas = [...result.metadatas || []];
|
||||
const ids = [...result.ids || []];
|
||||
|
||||
if (queryEmbedding !== undefined && queryEmbedding.vector_embedding !== undefined) {
|
||||
if (view2D && rag && rag.umap_embedding_2d) {
|
||||
metadatas.unshift({ doc_type: 'query' });
|
||||
documents.unshift(queryEmbedding.query || '');
|
||||
vectors.unshift(queryEmbedding.vector_embedding);
|
||||
documents.unshift('Query');
|
||||
vectors.unshift(rag.umap_embedding_2d);
|
||||
}
|
||||
|
||||
if (!view2D && rag && rag.umap_embedding_3d) {
|
||||
metadatas.unshift({ doc_type: 'query' });
|
||||
documents.unshift('Query');
|
||||
vectors.unshift(rag.umap_embedding_3d);
|
||||
}
|
||||
|
||||
if (newQueryEmbedding !== undefined) {
|
||||
metadatas.unshift({ doc_type: 'query' });
|
||||
documents.unshift(newQueryEmbedding.query || '');
|
||||
if (view2D && newQueryEmbedding.umap_embedding_2d) {
|
||||
vectors.unshift(newQueryEmbedding.umap_embedding_2d);
|
||||
}
|
||||
if (!view2D && newQueryEmbedding.umap_embedding_3d) {
|
||||
vectors.unshift(newQueryEmbedding.umap_embedding_3d);
|
||||
}
|
||||
}
|
||||
|
||||
const is2D = vectors.every((v: number[]) => v.length === 2);
|
||||
const is3D = vectors.every((v: number[]) => v.length === 3);
|
||||
if (!is2D && !is3D) {
|
||||
@ -148,11 +176,19 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, connectio
|
||||
return;
|
||||
}
|
||||
|
||||
const doc_types = metadatas.map(m => m.doc_type || 'unknown');
|
||||
const sizes = doc_types.map(type => {
|
||||
const doc_types = metadatas.map(m => m.doc_type || 'unknown')
|
||||
|
||||
const sizes = doc_types.map((type, index) => {
|
||||
if (!sizeMap[type]) {
|
||||
sizeMap[type] = 5;
|
||||
}
|
||||
/* If this is a match, increase the size */
|
||||
if (rag && rag.ids.includes(ids[index])) {
|
||||
return sizeMap[type] + 5;
|
||||
}
|
||||
if (newQueryEmbedding && newQueryEmbedding.ids && newQueryEmbedding.ids.includes(ids[index])) {
|
||||
return sizeMap[type] + 5;
|
||||
}
|
||||
return sizeMap[type];
|
||||
});
|
||||
const symbols = doc_types.map(type => {
|
||||
@ -189,7 +225,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, connectio
|
||||
},
|
||||
xaxis: { title: 'X', gridcolor: '#cccccc', zerolinecolor: '#aaaaaa' },
|
||||
yaxis: { title: 'Y', gridcolor: '#cccccc', zerolinecolor: '#aaaaaa' },
|
||||
margin: { r: 20, b: 10, l: 10, t: 40 },
|
||||
margin: { r: 0, b: 0, l: 0, t: 0 },
|
||||
};
|
||||
|
||||
const data: any = {
|
||||
@ -212,17 +248,23 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, connectio
|
||||
|
||||
setPlotData({ data, layout });
|
||||
|
||||
}, [result, queryEmbedding]);
|
||||
}, [result, newQueryEmbedding, rag, view2D, setPlotData, setSnack]);
|
||||
|
||||
if (setSnack === undefined) {
|
||||
console.error('setSnack function is undefined');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
const handleKeyPress = (event: any) => {
|
||||
if (event.key === 'Enter') {
|
||||
sendQuery(query);
|
||||
sendQuery(newQuery);
|
||||
}
|
||||
};
|
||||
|
||||
const sendQuery = async (query: string) => {
|
||||
if (!query.trim()) return;
|
||||
setQuery('');
|
||||
setNewQuery('');
|
||||
try {
|
||||
const response = await fetch(`${connectionBase}/api/similarity/${sessionId}`, {
|
||||
method: 'PUT',
|
||||
@ -231,11 +273,11 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, connectio
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: query,
|
||||
dimensions: view2D ? 2 : 3,
|
||||
})
|
||||
});
|
||||
const chroma: ChromaResult = await response.json();
|
||||
console.log('Chroma:', chroma);
|
||||
setQueryEmbedding(chroma);
|
||||
setNewQueryEmbedding(chroma);
|
||||
} catch (error) {
|
||||
console.error('Error obtaining query similarity information:', error);
|
||||
setSnack("Unable to obtain query similarity information.", "error");
|
||||
@ -249,13 +291,16 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, connectio
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box className="VectorVisualizer" sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1, overflow: 'hidden' }}>
|
||||
{
|
||||
!inline &&
|
||||
<Card sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', mb: 1, pt: 0 }}>
|
||||
<Typography variant="h6" sx={{ p: 1, pt: 0 }}>
|
||||
Similarity Visualization via Uniform Manifold Approximation and Projection (UMAP)
|
||||
</Typography>
|
||||
</Card>
|
||||
<Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
}
|
||||
<FormControlLabel sx={{ display: "inline-flex", width: "fit-content", mb: '-2.5rem', zIndex: 100, ml: 1, flexBasis: 0, flexGrow: 0 }} control={<Switch checked={!view2D} />} onChange={() => setView2D(!view2D)} label="3D" />
|
||||
<Plot
|
||||
onClick={(event: any) => {
|
||||
const point = event.points[0];
|
||||
@ -275,15 +320,15 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, connectio
|
||||
useResizeHandler={true}
|
||||
config={{
|
||||
responsive: true,
|
||||
displayModeBar: false,
|
||||
// displayModeBar: false,
|
||||
displaylogo: false,
|
||||
showSendToCloud: false,
|
||||
staticPlot: false,
|
||||
}}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
style={{ display: "flex", flexGrow: 1, justifyContent: 'center', alignItems: 'center', minHeight: '30vh', height: '30vh', padding: 0, margin: 0 }}
|
||||
layout={plotData.layout}
|
||||
/>
|
||||
</Box>
|
||||
{!inline &&
|
||||
<Card sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@ -305,30 +350,34 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, connectio
|
||||
{tooltip?.content}
|
||||
</Typography>
|
||||
</Card>
|
||||
{ queryEmbedding !== undefined &&
|
||||
}
|
||||
{!inline && newQueryEmbedding !== undefined &&
|
||||
<Card sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', mt: 1, pb: 0 }}>
|
||||
<Typography variant="h6" sx={{ p: 1, pt: 0, maxHeight: '5rem', overflow: 'auto' }}>
|
||||
Query: {queryEmbedding.query}
|
||||
Query: {newQueryEmbedding.query}
|
||||
</Typography>
|
||||
</Card>
|
||||
}
|
||||
|
||||
{
|
||||
!inline &&
|
||||
<Box className="Query" sx={{ display: "flex", flexDirection: "row", p: 1 }}>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
value={newQuery}
|
||||
onChange={(e) => setNewQuery(e.target.value)}
|
||||
onKeyDown={handleKeyPress}
|
||||
placeholder="Enter query to find related documents..."
|
||||
id="QueryInput"
|
||||
/>
|
||||
<Tooltip title="Send">
|
||||
<Button sx={{ m: 1 }} variant="contained" onClick={() => { sendQuery(query); }}><SendIcon /></Button>
|
||||
<Button sx={{ m: 1 }} variant="contained" onClick={() => { sendQuery(newQuery); }}><SendIcon /></Button>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
674
src/server.py
674
src/server.py
@ -11,6 +11,7 @@ import uuid
|
||||
import subprocess
|
||||
import re
|
||||
import math
|
||||
import copy
|
||||
|
||||
def try_import(module_name, pip_name=None):
|
||||
try:
|
||||
@ -52,6 +53,8 @@ from tools import (
|
||||
tools
|
||||
)
|
||||
|
||||
CONTEXT_VERSION=2
|
||||
|
||||
rags = [
|
||||
{ "name": "JPK", "enabled": True, "description": "Expert data about James Ketrenos, including work history, personal hobbies, and projects." },
|
||||
# { "name": "LKML", "enabled": False, "description": "Full associative data for entire LKML mailing list archive." },
|
||||
@ -164,6 +167,8 @@ Always use tools and [{context_tag}] when possible. Be concise, and never make u
|
||||
""".strip()
|
||||
|
||||
system_generate_resume = f"""
|
||||
Launched on {DateTime()}.
|
||||
|
||||
You are a professional resume writer. Your task is to write a polished, tailored resume for a specific job based only on the individual's [WORK HISTORY].
|
||||
|
||||
When answering queries, follow these steps:
|
||||
@ -188,9 +193,11 @@ Structure the resume professionally with the following sections where applicable
|
||||
|
||||
Do not include any information unless it is provided in [WORK HISTORY] or [INTRO].
|
||||
Ensure the langauge is clear, concise, and aligned with industry standards for professional resumes.
|
||||
"""
|
||||
""".strip()
|
||||
|
||||
system_fact_check = f"""
|
||||
Launched on {DateTime()}.
|
||||
|
||||
You are a professional resume fact checker. Your task is to identify any inaccuracies in the [RESUME] based on the individual's [WORK HISTORY].
|
||||
|
||||
If there are inaccuracies, list them in a bullet point format.
|
||||
@ -198,7 +205,20 @@ If there are inaccuracies, list them in a bullet point format.
|
||||
When answering queries, follow these steps:
|
||||
1. You must not invent or assume any information not explicitly present in the [WORK HISTORY].
|
||||
2. Analyze the [RESUME] to identify any discrepancies or inaccuracies based on the [WORK HISTORY].
|
||||
"""
|
||||
""".strip()
|
||||
|
||||
system_job_description = f"""
|
||||
Launched on {DateTime()}.
|
||||
|
||||
You are a hiring and job placing specialist. Your task is to answers about a job description.
|
||||
|
||||
When answering queries, follow these steps:
|
||||
1. Analyze the [JOB DESCRIPTION] to provide insights for the asked question.
|
||||
2. If any financial information is requested, be sure to account for inflation.
|
||||
""".strip()
|
||||
|
||||
def create_system_message(prompt):
|
||||
return [{"role": "system", "content": prompt}]
|
||||
|
||||
tool_log = []
|
||||
command_log = []
|
||||
@ -374,6 +394,9 @@ async def handle_tool_calls(message):
|
||||
final_result = all_responses[0] if len(all_responses) == 1 else all_responses
|
||||
yield (final_result, tools_used)
|
||||
|
||||
|
||||
|
||||
|
||||
# %%
|
||||
class WebServer:
|
||||
def __init__(self, logging, client, model=MODEL_NAME):
|
||||
@ -431,71 +454,6 @@ class WebServer:
|
||||
return RedirectResponse(url=f"/{context['id']}", status_code=307)
|
||||
#return JSONResponse({"redirect": f"/{context['id']}"})
|
||||
|
||||
@self.app.get("/api/query")
|
||||
async def query_documents(query: str, top_k: int = 3):
|
||||
if not self.file_watcher:
|
||||
return
|
||||
|
||||
"""Query the RAG system with the given prompt."""
|
||||
results = self.file_watcher.find_similar(query, top_k=top_k)
|
||||
return {
|
||||
"query": query,
|
||||
"results": [
|
||||
{
|
||||
"content": doc,
|
||||
"metadata": meta,
|
||||
"distance": dist
|
||||
}
|
||||
for doc, meta, dist in zip(
|
||||
results["documents"],
|
||||
results["metadatas"],
|
||||
results["distances"]
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
@self.app.post("/api/refresh/{file_path:path}")
|
||||
async def refresh_document(file_path: str, background_tasks: BackgroundTasks):
|
||||
if not self.file_watcher:
|
||||
return
|
||||
|
||||
"""Manually refresh a specific document in the collection."""
|
||||
full_path = os.path.join(defines.doc_dir, file_path)
|
||||
|
||||
if not os.path.exists(full_path):
|
||||
return {"status": "error", "message": "File not found"}
|
||||
|
||||
# Schedule the update in the background
|
||||
background_tasks.add_task(
|
||||
self.file_watcher.process_file_update, full_path
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Document refresh scheduled for {file_path}"
|
||||
}
|
||||
|
||||
# @self.app.post("/api/refresh-all")
|
||||
# async def refresh_all_documents():
|
||||
# if not self.file_watcher:
|
||||
# return
|
||||
|
||||
# """Refresh all documents in the collection."""
|
||||
# # Re-initialize file hashes and process all files
|
||||
# self.file_watcher._initialize_file_hashes()
|
||||
|
||||
# # Schedule updates for all files
|
||||
# file_paths = self.file_watcher.file_hashes.keys()
|
||||
# tasks = [self.file_watcher.process_file_update(path) for path in file_paths]
|
||||
|
||||
# # Wait for all updates to complete
|
||||
# await asyncio.gather(*tasks)
|
||||
|
||||
# return {
|
||||
# "status": "success",
|
||||
# "message": f"Refreshed {len(file_paths)} documents",
|
||||
# "document_count": file_watcher.collection.count()
|
||||
# }
|
||||
|
||||
@self.app.put("/api/umap/{context_id}")
|
||||
async def put_umap(context_id: str, request: Request):
|
||||
@ -515,12 +473,16 @@ class WebServer:
|
||||
dimensions = 2
|
||||
|
||||
try:
|
||||
result = self.file_watcher.collection.get(include=["embeddings", "documents", "metadatas"])
|
||||
vectors = np.array(result["embeddings"])
|
||||
umap_model = umap.UMAP(n_components=dimensions, random_state=42) #, n_neighbors=15, min_dist=0.1)
|
||||
embedding = umap_model.fit_transform(vectors)
|
||||
context["umap_model"] = umap_model
|
||||
result["embeddings"] = embedding.tolist()
|
||||
result = self.file_watcher.umap_collection
|
||||
if dimensions == 2:
|
||||
logging.info("Returning 2D UMAP")
|
||||
umap_embedding = self.file_watcher.umap_embedding_2d
|
||||
else:
|
||||
logging.info("Returning 3D UMAP")
|
||||
umap_embedding = self.file_watcher.umap_embedding_3d
|
||||
|
||||
result["embeddings"] = umap_embedding.tolist()
|
||||
|
||||
return JSONResponse(result)
|
||||
|
||||
except Exception as e:
|
||||
@ -536,10 +498,6 @@ class WebServer:
|
||||
logging.warning(f"Invalid context_id: {context_id}")
|
||||
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
|
||||
|
||||
context = self.upsert_context(context_id)
|
||||
if not context.get("umap_model"):
|
||||
return JSONResponse({"error": "No umap_model found in context"}, status_code=404)
|
||||
|
||||
try:
|
||||
data = await request.json()
|
||||
query = data.get("query", "")
|
||||
@ -552,28 +510,37 @@ class WebServer:
|
||||
chroma_results = self.file_watcher.find_similar(query=query, top_k=10)
|
||||
if not chroma_results:
|
||||
return JSONResponse({"error": "No results found"}, status_code=404)
|
||||
|
||||
chroma_embedding = chroma_results["query_embedding"]
|
||||
umap_embedding = context["umap_model"].transform([chroma_embedding])[0].tolist()
|
||||
return JSONResponse({ **chroma_results, "query": query, "vector_embedding": umap_embedding })
|
||||
|
||||
return JSONResponse({
|
||||
**chroma_results,
|
||||
"query": query,
|
||||
"umap_embedding_2d": self.file_watcher.umap_model_2d.transform([chroma_embedding])[0].tolist(),
|
||||
"umap_embedding_3d": self.file_watcher.umap_model_3d.transform([chroma_embedding])[0].tolist()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
#return JSONResponse({"error": str(e)}, 500)
|
||||
|
||||
@self.app.put("/api/reset/{context_id}")
|
||||
async def put_reset(context_id: str, request: Request):
|
||||
@self.app.put("/api/reset/{context_id}/{type}")
|
||||
async def put_reset(context_id: str, type: str, request: Request):
|
||||
if not is_valid_uuid(context_id):
|
||||
logging.warning(f"Invalid context_id: {context_id}")
|
||||
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
|
||||
context = self.upsert_context(context_id)
|
||||
if type not in context["sessions"]:
|
||||
return JSONResponse({ "error": f"{type} is not recognized", "context": context }, status_code=404)
|
||||
|
||||
data = await request.json()
|
||||
try:
|
||||
response = {}
|
||||
for reset in data["reset"]:
|
||||
match reset:
|
||||
case "system-prompt":
|
||||
context["system"] = [{"role": "system", "content": system_message}]
|
||||
response["system-prompt"] = { "system-prompt": system_message }
|
||||
case "system_prompt":
|
||||
context["sessions"][type]["system_prompt"] = system_message
|
||||
response["system_prompt"] = { "system_prompt": system_message }
|
||||
case "rags":
|
||||
context["rags"] = rags.copy()
|
||||
response["rags"] = context["rags"]
|
||||
@ -581,23 +548,23 @@ class WebServer:
|
||||
context["tools"] = default_tools(tools)
|
||||
response["tools"] = context["tools"]
|
||||
case "history":
|
||||
context["llm_history"] = []
|
||||
context["user_history"] = []
|
||||
context["sessions"][type]["llm_history"] = []
|
||||
context["sessions"][type]["user_history"] = []
|
||||
context["sessions"][type]["context_tokens"] = round(len(str(context["system"])) * 3 / 4) # Estimate context usage
|
||||
response["history"] = []
|
||||
context["context_tokens"] = round(len(str(context["system"])) * 3 / 4) # Estimate context usage
|
||||
response["context_used"] = context["context_tokens"]
|
||||
case "message-history-length":
|
||||
response["context_used"] = context["sessions"][type]["context_tokens"]
|
||||
case "message_history_length":
|
||||
context["message_history_length"] = DEFAULT_HISTORY_LENGTH
|
||||
response["message-history-length"] = DEFAULT_HISTORY_LENGTH
|
||||
response["message_history_length"] = DEFAULT_HISTORY_LENGTH
|
||||
|
||||
if not response:
|
||||
return JSONResponse({ "error": "Usage: { reset: rags|tools|history|system-prompt}"})
|
||||
return JSONResponse({ "error": "Usage: { reset: rags|tools|history|system_prompt}"})
|
||||
else:
|
||||
self.save_context(context_id)
|
||||
return JSONResponse(response)
|
||||
|
||||
except:
|
||||
return JSONResponse({ "error": "Usage: { reset: rags|tools|history|system-prompt}"})
|
||||
return JSONResponse({ "error": "Usage: { reset: rags|tools|history|system_prompt}"})
|
||||
|
||||
@self.app.put("/api/tunables/{context_id}")
|
||||
async def put_tunables(context_id: str, request: Request):
|
||||
@ -608,20 +575,20 @@ class WebServer:
|
||||
data = await request.json()
|
||||
for k in data.keys():
|
||||
match k:
|
||||
case "system-prompt":
|
||||
case "system_prompt":
|
||||
system_prompt = data[k].strip()
|
||||
if not system_prompt:
|
||||
return JSONResponse({ "status": "error", "message": "System prompt can not be empty." })
|
||||
context["system"] = [{"role": "system", "content": system_prompt}]
|
||||
self.save_context(context_id)
|
||||
return JSONResponse({ "system-prompt": system_prompt })
|
||||
case "message-history-length":
|
||||
return JSONResponse({ "system_prompt": system_prompt })
|
||||
case "message_history_length":
|
||||
value = max(0, int(data[k]))
|
||||
context["message_history_length"] = value
|
||||
self.save_context(context_id)
|
||||
return JSONResponse({ "message-history-length": value })
|
||||
return JSONResponse({ "message_history_length": value })
|
||||
case _:
|
||||
return JSONResponse({ "error": f"Unrecognized tunable {k}"}, 404)
|
||||
return JSONResponse({ "error": f"Unrecognized tunable {k}"}, status_code=404)
|
||||
|
||||
@self.app.get("/api/tunables/{context_id}")
|
||||
async def get_tunables(context_id: str):
|
||||
@ -630,33 +597,29 @@ class WebServer:
|
||||
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
|
||||
context = self.upsert_context(context_id)
|
||||
return JSONResponse({
|
||||
"system-prompt": context["system"][0]["content"],
|
||||
"message-history-length": context["message_history_length"]
|
||||
"system_prompt": context["system"][0]["content"],
|
||||
"message_history_length": context["message_history_length"]
|
||||
})
|
||||
|
||||
@self.app.get("/api/resume/{context_id}")
|
||||
async def get_resume(context_id: str):
|
||||
if not is_valid_uuid(context_id):
|
||||
logging.warning(f"Invalid context_id: {context_id}")
|
||||
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
|
||||
context = self.upsert_context(context_id)
|
||||
return JSONResponse(context["resume_history"])
|
||||
|
||||
@self.app.get("/api/system-info/{context_id}")
|
||||
async def get_system_info(context_id: str):
|
||||
return JSONResponse(system_info(self.model))
|
||||
|
||||
@self.app.post("/api/chat/{context_id}")
|
||||
async def chat_endpoint(context_id: str, request: Request):
|
||||
@self.app.post("/api/chat/{context_id}/{type}")
|
||||
async def chat_endpoint(context_id: str, type: str, request: Request):
|
||||
if not is_valid_uuid(context_id):
|
||||
logging.warning(f"Invalid context_id: {context_id}")
|
||||
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
|
||||
context = self.upsert_context(context_id)
|
||||
|
||||
if type not in context["sessions"]:
|
||||
return JSONResponse({ "error": f"{type} is not recognized", "context": context }, status_code=404)
|
||||
|
||||
data = await request.json()
|
||||
|
||||
# Create a custom generator that ensures flushing
|
||||
async def flush_generator():
|
||||
async for message in self.chat(context=context, content=data["content"]):
|
||||
async for message in self.chat(context=context, type=type, content=data["content"]):
|
||||
# Convert to JSON and add newline
|
||||
yield json.dumps(message) + "\n"
|
||||
# Save the history as its generated
|
||||
@ -675,74 +638,18 @@ class WebServer:
|
||||
}
|
||||
)
|
||||
|
||||
@self.app.post("/api/generate-resume/{context_id}")
|
||||
async def post_generate_resume(context_id: str, request: Request):
|
||||
if not is_valid_uuid(context_id):
|
||||
logging.warning(f"Invalid context_id: {context_id}")
|
||||
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
|
||||
context = self.upsert_context(context_id)
|
||||
data = await request.json()
|
||||
|
||||
# Create a custom generator that ensures flushing
|
||||
async def flush_generator():
|
||||
async for message in self.generate_resume(context=context, content=data["content"]):
|
||||
# Convert to JSON and add newline
|
||||
yield json.dumps(message) + "\n"
|
||||
# Save the history as its generated
|
||||
self.save_context(context_id)
|
||||
# Explicitly flush after each yield
|
||||
await asyncio.sleep(0) # Allow the event loop to process the write
|
||||
|
||||
# Return StreamingResponse with appropriate headers
|
||||
return StreamingResponse(
|
||||
flush_generator(),
|
||||
media_type="application/json",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no" # Prevents Nginx buffering if you're using it
|
||||
}
|
||||
)
|
||||
|
||||
@self.app.post("/api/fact-check/{context_id}")
|
||||
async def post_fact_check(context_id: str, request: Request):
|
||||
if not is_valid_uuid(context_id):
|
||||
logging.warning(f"Invalid context_id: {context_id}")
|
||||
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
|
||||
context = self.upsert_context(context_id)
|
||||
data = await request.json()
|
||||
|
||||
# Create a custom generator that ensures flushing
|
||||
async def flush_generator():
|
||||
async for message in self.fact_check(context=context, content=data["content"]):
|
||||
# Convert to JSON and add newline
|
||||
yield json.dumps(message) + "\n"
|
||||
# Save the history as its generated
|
||||
self.save_context(context_id)
|
||||
# Explicitly flush after each yield
|
||||
await asyncio.sleep(0) # Allow the event loop to process the write
|
||||
|
||||
# Return StreamingResponse with appropriate headers
|
||||
return StreamingResponse(
|
||||
flush_generator(),
|
||||
media_type="application/json",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no" # Prevents Nginx buffering if you"re using it
|
||||
}
|
||||
)
|
||||
|
||||
@self.app.post("/api/context")
|
||||
async def create_context():
|
||||
context = self.create_context()
|
||||
self.logging.info(f"Generated new session as {context['id']}")
|
||||
return JSONResponse(context)
|
||||
|
||||
@self.app.get("/api/history/{context_id}")
|
||||
async def get_history(context_id: str):
|
||||
@self.app.get("/api/history/{context_id}/{type}")
|
||||
async def get_history(context_id: str, type: str):
|
||||
context = self.upsert_context(context_id)
|
||||
return JSONResponse(context["user_history"])
|
||||
if type not in context["sessions"]:
|
||||
return JSONResponse({ "error": f"{type} is not recognized", "context": context }, status_code=404)
|
||||
return JSONResponse(context["sessions"][type]["user_history"])
|
||||
|
||||
@self.app.get("/api/tools/{context_id}")
|
||||
async def get_tools(context_id: str):
|
||||
@ -764,7 +671,7 @@ class WebServer:
|
||||
tool["enabled"] = enabled
|
||||
self.save_context(context_id)
|
||||
return JSONResponse(context["tools"])
|
||||
return JSONResponse({ "status": f"{modify} not found in tools." }), 404
|
||||
return JSONResponse({ "status": f"{modify} not found in tools." }, status_code=404)
|
||||
except:
|
||||
return JSONResponse({ "status": "error" }), 405
|
||||
|
||||
@ -788,17 +695,19 @@ class WebServer:
|
||||
tool["enabled"] = enabled
|
||||
self.save_context(context_id)
|
||||
return JSONResponse(context["rags"])
|
||||
return JSONResponse({ "status": f"{modify} not found in tools." }), 404
|
||||
return JSONResponse({ "status": f"{modify} not found in tools." }, status_code=404)
|
||||
except:
|
||||
return JSONResponse({ "status": "error" }), 405
|
||||
|
||||
@self.app.get("/api/context-status/{context_id}")
|
||||
async def get_context_status(context_id):
|
||||
@self.app.get("/api/context-status/{context_id}/{type}")
|
||||
async def get_context_status(context_id, type: str):
|
||||
if not is_valid_uuid(context_id):
|
||||
logging.warning(f"Invalid context_id: {context_id}")
|
||||
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
|
||||
context = self.upsert_context(context_id)
|
||||
return JSONResponse({"context_used": context["context_tokens"], "max_context": defines.max_context})
|
||||
if type not in context["sessions"]:
|
||||
return JSONResponse({ "error": f"{type} is not recognized", "context": context }, status_code=404)
|
||||
return JSONResponse({"context_used": context["sessions"][type]["context_tokens"], "max_context": defines.max_context})
|
||||
|
||||
@self.app.get("/api/health")
|
||||
async def health_check():
|
||||
@ -833,16 +742,80 @@ class WebServer:
|
||||
# Create the full file path
|
||||
file_path = os.path.join(defines.session_dir, session_id)
|
||||
|
||||
umap_model = context.get("umap_model")
|
||||
if umap_model:
|
||||
del context["umap_model"]
|
||||
# Serialize the data to JSON and write to file
|
||||
with open(file_path, "w") as f:
|
||||
json.dump(context, f)
|
||||
if umap_model:
|
||||
context["umap_model"] = umap_model
|
||||
|
||||
return session_id
|
||||
|
||||
|
||||
def migrate_context(self, context):
|
||||
# No version
|
||||
# context = {
|
||||
# "id": context_id,
|
||||
# "tools": default_tools(tools),
|
||||
# "rags": rags.copy(),
|
||||
# "context_tokens": round(len(str(system_context)) * 3 / 4), # Estimate context usage
|
||||
# "message_history_length": 5, # Number of messages to supply in context
|
||||
# "system": system_context,
|
||||
# "system_generate_resume": system_generate_resume,
|
||||
# "llm_history": [],
|
||||
# "user_history": [],
|
||||
# "resume_history": [],
|
||||
# }
|
||||
# Version 2:
|
||||
# context = {
|
||||
# "version": 2,
|
||||
# "id": context_id,
|
||||
# "sessions": {
|
||||
# **TYPE**: { # chat, job-description, resume, fact-check
|
||||
# "system_prompt": **SYSTEM_MESSAGE**,
|
||||
# "llm_history": [],
|
||||
# "user_history": [],
|
||||
# "context_tokens": round(len(str(**SYSTEM_MESSAGE**)) * 3 / 4),
|
||||
# }
|
||||
# },
|
||||
# "tools": default_tools(tools),
|
||||
# "rags": rags.copy(),
|
||||
# "message_history_length": 5 # Number of messages to supply in context
|
||||
# }
|
||||
if "version" not in context:
|
||||
logging.info(f"Migrating {context['id']}")
|
||||
context["version"] = CONTEXT_VERSION
|
||||
context["sessions"] = {
|
||||
"chat": {
|
||||
"system_prompt": system_message,
|
||||
"llm_history": context["llm_history"],
|
||||
"user_history": context["user_history"],
|
||||
"context_tokens": round(len(str(create_system_message(system_message))))
|
||||
},
|
||||
"job_description": {
|
||||
"system_prompt": system_job_description,
|
||||
"llm_history": [],
|
||||
"user_history": [],
|
||||
"context_tokens": round(len(str(create_system_message(system_job_description))))
|
||||
},
|
||||
"resume": {
|
||||
"system_prompt": system_generate_resume,
|
||||
"llm_history": [],
|
||||
"user_history": [],
|
||||
"context_tokens": round(len(str(create_system_message(system_generate_resume))))
|
||||
},
|
||||
"fact_check": {
|
||||
"system_prompt": system_fact_check,
|
||||
"llm_history": [],
|
||||
"user_history": [],
|
||||
"context_tokens": round(len(str(create_system_message(system_fact_check))))
|
||||
},
|
||||
}
|
||||
del context["system"]
|
||||
del context["system_generate_resume"]
|
||||
del context["llm_history"]
|
||||
del context["user_history"]
|
||||
del context["resume_history"]
|
||||
|
||||
return context
|
||||
|
||||
def load_context(self, session_id):
|
||||
"""
|
||||
Load a serialized Python dictionary from a file in the sessions directory.
|
||||
@ -863,22 +836,42 @@ class WebServer:
|
||||
with open(file_path, "r") as f:
|
||||
self.contexts[session_id] = json.load(f)
|
||||
|
||||
return self.contexts[session_id]
|
||||
return self.migrate_context(self.contexts[session_id])
|
||||
|
||||
def create_context(self, context_id = None):
|
||||
if not context_id:
|
||||
context_id = str(uuid.uuid4())
|
||||
system_context = [{"role": "system", "content": system_message}];
|
||||
context = {
|
||||
"id": context_id,
|
||||
"system": system_context,
|
||||
"system_generate_resume": system_generate_resume,
|
||||
"version": CONTEXT_VERSION,
|
||||
"sessions": {
|
||||
"chat": {
|
||||
"system_prompt": system_message,
|
||||
"llm_history": [],
|
||||
"user_history": [],
|
||||
"context_tokens": round(len(str(system_message)) * 3 / 4), # Estimate context usage
|
||||
},
|
||||
"job_description": {
|
||||
"system_prompt": system_job_description,
|
||||
"llm_history": [],
|
||||
"user_history": [],
|
||||
"context_tokens": round(len(str(system_job_description)) * 3 / 4), # Estimate context usage
|
||||
},
|
||||
"resume": {
|
||||
"system_prompt": system_generate_resume,
|
||||
"llm_history": [],
|
||||
"user_history": [],
|
||||
"context_tokens": round(len(str(system_generate_resume)) * 3 / 4), # Estimate context usage
|
||||
},
|
||||
"fact_check": {
|
||||
"system_prompt": system_fact_check,
|
||||
"llm_history": [],
|
||||
"user_history": [],
|
||||
"context_tokens": round(len(str(system_fact_check)) * 3 / 4), # Estimate context usage
|
||||
},
|
||||
},
|
||||
"tools": default_tools(tools),
|
||||
"resume_history": [],
|
||||
"rags": rags.copy(),
|
||||
"context_tokens": round(len(str(system_context)) * 3 / 4), # Estimate context usage
|
||||
"message_history_length": 5 # Number of messages to supply in context
|
||||
}
|
||||
logging.info(f"{context_id} created and added to sessions.")
|
||||
@ -894,12 +887,11 @@ class WebServer:
|
||||
logging.warning("No context ID provided. Creating a new context.")
|
||||
return self.create_context()
|
||||
if context_id in self.contexts:
|
||||
logging.info(f"Context {context_id} found.")
|
||||
return self.contexts[context_id]
|
||||
logging.info(f"Context {context_id} not found. Creating new context.")
|
||||
return self.load_context(context_id)
|
||||
|
||||
async def chat(self, context, content):
|
||||
async def chat(self, context, type, content):
|
||||
if not self.file_watcher:
|
||||
return
|
||||
|
||||
@ -914,55 +906,172 @@ class WebServer:
|
||||
|
||||
self.processing = True
|
||||
|
||||
llm_history = context["llm_history"]
|
||||
user_history = context["user_history"]
|
||||
try:
|
||||
llm_history = context["sessions"][type]["llm_history"]
|
||||
user_history = context["sessions"][type]["user_history"]
|
||||
metadata = {
|
||||
"rag": {},
|
||||
"type": type,
|
||||
"rag": { "documents": [] },
|
||||
"tools": [],
|
||||
"eval_count": 0,
|
||||
"eval_duration": 0,
|
||||
"prompt_eval_count": 0,
|
||||
"prompt_eval_duration": 0,
|
||||
}
|
||||
rag_docs = []
|
||||
|
||||
# Default to not using tools
|
||||
enable_tools = False
|
||||
|
||||
# Default eo using RAG
|
||||
enable_rag = True
|
||||
|
||||
# The first time a particular session type is used, it is handled differently. After the initial pass (once the
|
||||
# llm_history has more than one entry), the standard 'chat' is used.
|
||||
if len(user_history) >= 1:
|
||||
process_type = "chat"
|
||||
# Do not enable RAG when limiting context to the job description chat
|
||||
if type == "job_description":
|
||||
enable_rag = False
|
||||
else:
|
||||
process_type = type
|
||||
|
||||
if enable_rag:
|
||||
for rag in context["rags"]:
|
||||
if rag["enabled"] and rag["name"] == "JPK": # Only support JPK rag right now...
|
||||
yield {"status": "processing", "message": f"Checking RAG context {rag['name']}..."}
|
||||
chroma_results = self.file_watcher.find_similar(query=content, top_k=10)
|
||||
if chroma_results:
|
||||
rag_docs.extend(chroma_results["documents"])
|
||||
metadata["rag"] = { "name": rag["name"], **chroma_results }
|
||||
preamble = ""
|
||||
if len(rag_docs):
|
||||
preamble = f"""
|
||||
1. Respond to this query: {content}
|
||||
2. If there is information in this context to enhance the answer, do so:
|
||||
[{context_tag}]:\n"""
|
||||
for doc in rag_docs:
|
||||
preamble += doc
|
||||
preamble += f"\n[/{context_tag}]\nUse all of that information to respond to: "
|
||||
chroma_embedding = chroma_results["query_embedding"]
|
||||
metadata["rag"] = {
|
||||
**chroma_results,
|
||||
"name": rag["name"],
|
||||
"umap_embedding_2d": self.file_watcher.umap_model_2d.transform([chroma_embedding])[0].tolist(),
|
||||
"umap_embedding_3d": self.file_watcher.umap_model_3d.transform([chroma_embedding])[0].tolist()
|
||||
}
|
||||
|
||||
|
||||
match process_type:
|
||||
# Normal chat interactions with context history
|
||||
case "chat":
|
||||
enable_tools = True
|
||||
preamble = ""
|
||||
rag_context = ""
|
||||
for doc in metadata["rag"]["documents"]:
|
||||
rag_context += doc
|
||||
if rag_context:
|
||||
preamble = f"""
|
||||
1. Respond to this query: {content}
|
||||
2. If there is information in this context to enhance the answer, do so:
|
||||
[{context_tag}]
|
||||
{rag_context}
|
||||
[/{context_tag}]
|
||||
Use that information to respond to: """
|
||||
|
||||
# Single job_description is provided; generate a resume
|
||||
case "job_description":
|
||||
# Always force the full resume to be in context
|
||||
resume_doc = open(defines.resume_doc, "r").read()
|
||||
work_history = f"{resume_doc}\n"
|
||||
for doc in metadata["rag"]["documents"]:
|
||||
work_history += f"{doc}\n"
|
||||
|
||||
preamble = f"""
|
||||
[INTRO]
|
||||
{resume_intro}
|
||||
[/INTRO]
|
||||
|
||||
[WORK HISTORY]
|
||||
{work_history}
|
||||
[/WORK HISTORY]
|
||||
|
||||
[JOB DESCRIPTION]
|
||||
{content}
|
||||
[/JOB DESCRIPTION]
|
||||
|
||||
1. Use the above [INTRO] and [WORK HISTORY] to create the resume for the [JOB DESCRIPTION].
|
||||
2. Do not use content from the [JOB DESCRIPTION] in the response unless the [WORK HISTORY] mentions them.
|
||||
"""
|
||||
|
||||
# Seed the first context messages with the resume from the 'job_description' session
|
||||
case "resume":
|
||||
raise Exception(f"Invalid chat type: {type}")
|
||||
|
||||
# Fact check the resume created by the 'job_description' using only the RAG and resume
|
||||
case "fact_check":
|
||||
if len(context["sessions"]["resume"]["llm_history"]) < 3: # SYSTEM, USER, **ASSISTANT**
|
||||
yield {"status": "done", "message": "No resume history found." }
|
||||
return
|
||||
|
||||
resume = context["sessions"]["resume"]["llm_history"][2]
|
||||
|
||||
metadata = copy.deepcopy(resume["metadata"])
|
||||
metadata["eval_count"] = 0
|
||||
metadata["eval_duration"] = 0
|
||||
metadata["prompt_eval_count"] = 0
|
||||
metadata["prompt_eval_duration"] = 0
|
||||
|
||||
resume_doc = open(defines.resume_doc, "r").read()
|
||||
work_history = f"{resume_doc}\n"
|
||||
for doc in metadata["rag"]["documents"]:
|
||||
work_history += f"{doc}\n"
|
||||
|
||||
preamble = f"""
|
||||
[WORK HISTORY]
|
||||
{work_history}
|
||||
[/WORK HISTORY]
|
||||
|
||||
[RESUME]
|
||||
{resume['content']}
|
||||
[/RESUME]
|
||||
"""
|
||||
content = resume['content']
|
||||
|
||||
raise Exception(f"Invalid chat type: {type}")
|
||||
|
||||
case _:
|
||||
raise Exception(f"Invalid chat type: {type}")
|
||||
|
||||
# Figure
|
||||
llm_history.append({"role": "user", "content": preamble + content})
|
||||
user_history.append({"role": "user", "content": content})
|
||||
|
||||
if context["message_history_length"]:
|
||||
messages = context["system"] + llm_history[-context["message_history_length"]:]
|
||||
messages = create_system_message(context["sessions"][type]["system_prompt"]) + llm_history[-context["message_history_length"]:]
|
||||
else:
|
||||
messages = context["system"] + llm_history
|
||||
messages = create_system_message(context["sessions"][type]["system_prompt"]) + llm_history
|
||||
|
||||
try:
|
||||
# Estimate token length of new messages
|
||||
ctx_size = self.get_optimal_ctx_size(context["context_tokens"], messages=llm_history[-1]["content"])
|
||||
yield {"status": "processing", "message": "Processing request...", "num_ctx": ctx_size}
|
||||
ctx_size = self.get_optimal_ctx_size(context["sessions"][type]["context_tokens"], messages=llm_history[-1]["content"])
|
||||
|
||||
processing_type = "Processing query..."
|
||||
match type:
|
||||
case "job_description":
|
||||
processing_type = "Generating resume..."
|
||||
case "fact_check":
|
||||
processing_type = "Fact Checking resume..."
|
||||
if len(llm_history) > 1:
|
||||
processing_type = "Processing query..."
|
||||
|
||||
yield {"status": "processing", "message": processing_type, "num_ctx": ctx_size}
|
||||
|
||||
# Use the async generator in an async for loop
|
||||
try:
|
||||
if enable_tools:
|
||||
response = self.client.chat(model=self.model, messages=messages, tools=llm_tools(context["tools"]), options={ "num_ctx": ctx_size })
|
||||
else:
|
||||
response = self.client.chat(model=self.model, messages=messages, options={ "num_ctx": ctx_size })
|
||||
except Exception as e:
|
||||
logging.info(f"1. {messages[0]}")
|
||||
logging.info(f"[LAST]. {messages[-1]}")
|
||||
|
||||
logging.exception({ "model": self.model, "error": str(e) })
|
||||
yield {"status": "error", "message": f"An error occurred communicating with LLM"}
|
||||
return
|
||||
|
||||
metadata["eval_count"] += response["eval_count"]
|
||||
metadata["eval_duration"] += response["eval_duration"]
|
||||
metadata["prompt_eval_count"] += response["prompt_eval_count"]
|
||||
metadata["prompt_eval_duration"] += response["prompt_eval_duration"]
|
||||
context["context_tokens"] = response["prompt_eval_count"] + response["eval_count"]
|
||||
context["sessions"][type]["context_tokens"] = response["prompt_eval_count"] + response["eval_count"]
|
||||
|
||||
tools_used = []
|
||||
|
||||
@ -1005,7 +1114,7 @@ class WebServer:
|
||||
metadata["tools"] = tools_used
|
||||
|
||||
# Estimate token length of new messages
|
||||
ctx_size = self.get_optimal_ctx_size(context["context_tokens"], messages=messages[pre_add_index:])
|
||||
ctx_size = self.get_optimal_ctx_size(context["sessions"][type]["context_tokens"], messages=messages[pre_add_index:])
|
||||
yield {"status": "processing", "message": "Generating final response...", "num_ctx": ctx_size }
|
||||
# Decrease creativity when processing tool call requests
|
||||
response = self.client.chat(model=self.model, messages=messages, stream=False, options={ "num_ctx": ctx_size }) #, "temperature": 0.5 })
|
||||
@ -1013,7 +1122,7 @@ class WebServer:
|
||||
metadata["eval_duration"] += response["eval_duration"]
|
||||
metadata["prompt_eval_count"] += response["prompt_eval_count"]
|
||||
metadata["prompt_eval_duration"] += response["prompt_eval_duration"]
|
||||
context["context_tokens"] = response["prompt_eval_count"] + response["eval_count"]
|
||||
context["sessions"][type]["context_tokens"] = response["prompt_eval_count"] + response["eval_count"]
|
||||
|
||||
reply = response["message"]["content"]
|
||||
final_message = {"role": "assistant", "content": reply }
|
||||
@ -1035,145 +1144,6 @@ class WebServer:
|
||||
finally:
|
||||
self.processing = False
|
||||
|
||||
async def generate_resume(self, context, content):
|
||||
if not self.file_watcher:
|
||||
return
|
||||
|
||||
content = content.strip()
|
||||
if not content:
|
||||
yield {"status": "error", "message": "Invalid request"}
|
||||
return
|
||||
|
||||
if self.processing:
|
||||
yield {"status": "error", "message": "Busy"}
|
||||
return
|
||||
|
||||
self.processing = True
|
||||
resume_history = context["resume_history"]
|
||||
resume = {
|
||||
"job_description": content,
|
||||
"resume": "",
|
||||
"metadata": {},
|
||||
"rag": "",
|
||||
"fact_check": {}
|
||||
}
|
||||
|
||||
metadata = {
|
||||
"rag": {},
|
||||
"tools": [],
|
||||
"eval_count": 0,
|
||||
"eval_duration": 0,
|
||||
"prompt_eval_count": 0,
|
||||
"prompt_eval_duration": 0,
|
||||
}
|
||||
rag_docs = []
|
||||
resume_doc = open(defines.resume_doc, "r").read()
|
||||
rag_docs.append(resume_doc)
|
||||
for rag in context["rags"]:
|
||||
if rag["enabled"] and rag["name"] == "JPK": # Only support JPK rag right now...
|
||||
yield {"status": "processing", "message": f"Checking RAG context {rag['name']}..."}
|
||||
chroma_results = self.file_watcher.find_similar(query=content, top_k=10)
|
||||
if chroma_results:
|
||||
rag_docs.extend(chroma_results["documents"])
|
||||
metadata["rag"] = { "name": rag["name"], **chroma_results }
|
||||
preamble = f"[INTRO]\n{resume_intro}\n[/INTRO]\n"
|
||||
preamble += f"""[WORK HISTORY]:\n"""
|
||||
for doc in rag_docs:
|
||||
preamble += f"{doc}\n"
|
||||
resume["rag"] += f"{doc}\n"
|
||||
preamble += f"\n[/WORK HISTORY]\n"
|
||||
|
||||
content = f"""{preamble}\n
|
||||
Use the above [WORK HISTORY] and [INTRO] to create the resume for this [JOB DESCRIPTION]. Do not use the [JOB DESCRIPTION] in the generated resume unless the [WORK HISTORY] mentions them:\n[JOB DESCRIPTION]\n{content}\n[/JOB DESCRIPTION]\n"""
|
||||
|
||||
try:
|
||||
# Estimate token length of new messages
|
||||
ctx_size = self.get_optimal_ctx_size(context["context_tokens"], messages=[system_generate_resume, content])
|
||||
|
||||
yield {"status": "processing", "message": "Processing request...", "num_ctx": ctx_size}
|
||||
|
||||
# Use the async generator in an async for loop
|
||||
#
|
||||
# To support URL lookup:
|
||||
#
|
||||
# 1. Enable tools in a call to chat() with a simple prompt to invoke the tool to generate the summary if requested.
|
||||
# 2. If not requested (no tool call,) abort the path
|
||||
# 3. Otherwise, we know the URL was good and can use that URLs fetched content as context.
|
||||
#
|
||||
response = self.client.generate(model=self.model, system=system_generate_resume, prompt=content, options={ "num_ctx": ctx_size })
|
||||
metadata["eval_count"] += response["eval_count"]
|
||||
metadata["eval_duration"] += response["eval_duration"]
|
||||
metadata["prompt_eval_count"] += response["prompt_eval_count"]
|
||||
metadata["prompt_eval_duration"] += response["prompt_eval_duration"]
|
||||
context["context_tokens"] = response["prompt_eval_count"] + response["eval_count"]
|
||||
|
||||
reply = response["response"]
|
||||
final_message = {"role": "assistant", "content": reply, "metadata": metadata }
|
||||
|
||||
resume["resume"] = final_message
|
||||
resume_history.append(resume)
|
||||
|
||||
# Return the REST API with metadata
|
||||
yield {"status": "done", "message": final_message }
|
||||
|
||||
except Exception as e:
|
||||
logging.exception({ "model": self.model, "content": content, "error": str(e) })
|
||||
yield {"status": "error", "message": f"An error occurred: {str(e)}"}
|
||||
|
||||
finally:
|
||||
self.processing = False
|
||||
|
||||
async def fact_check(self, context, content):
|
||||
content = content.strip()
|
||||
if not content:
|
||||
yield {"status": "error", "message": "Invalid request"}
|
||||
return
|
||||
|
||||
if self.processing:
|
||||
yield {"status": "error", "message": "Busy"}
|
||||
return
|
||||
|
||||
self.processing = True
|
||||
resume_history = context["resume_history"]
|
||||
if len(resume_history) == 0:
|
||||
yield {"status": "done", "message": "No resume history found." }
|
||||
return
|
||||
|
||||
resume = resume_history[-1]
|
||||
metadata = resume["metadata"]
|
||||
metadata["eval_count"] = 0
|
||||
metadata["eval_duration"] = 0
|
||||
metadata["prompt_eval_count"] = 0
|
||||
metadata["prompt_eval_duration"] = 0
|
||||
|
||||
content = f"[WORK HISTORY]:{resume['rag']}[/WORK HISTORY]\n\n[RESUME]\n{resume['resume']['content']}\n[/RESUME]\n\n"
|
||||
|
||||
try:
|
||||
# Estimate token length of new messages
|
||||
ctx_size = self.get_optimal_ctx_size(context["context_tokens"], messages=[system_fact_check, content])
|
||||
yield {"status": "processing", "message": "Processing request...", "num_ctx": ctx_size}
|
||||
response = self.client.generate(model=self.model, system=system_fact_check, prompt=content, options={ "num_ctx": ctx_size })
|
||||
logging.info(f"Fact checking {ctx_size} tokens.")
|
||||
metadata["eval_count"] += response["eval_count"]
|
||||
metadata["eval_duration"] += response["eval_duration"]
|
||||
metadata["prompt_eval_count"] += response["prompt_eval_count"]
|
||||
metadata["prompt_eval_duration"] += response["prompt_eval_duration"]
|
||||
context["context_tokens"] = response["prompt_eval_count"] + response["eval_count"]
|
||||
reply = response["response"]
|
||||
final_message = {"role": "assistant", "content": reply, "metadata": metadata }
|
||||
resume["fact_check"] = final_message
|
||||
|
||||
# Return the REST API with metadata
|
||||
yield {"status": "done", "message": final_message }
|
||||
|
||||
except Exception as e:
|
||||
logging.exception({ "model": self.model, "content": content, "error": str(e) })
|
||||
yield {"status": "error", "message": f"An error occurred: {str(e)}"}
|
||||
|
||||
finally:
|
||||
self.processing = False
|
||||
|
||||
|
||||
def run(self, host="0.0.0.0", port=WEB_PORT, **kwargs):
|
||||
try:
|
||||
if self.ssl_enabled:
|
||||
|
@ -19,6 +19,7 @@ from langchain.text_splitter import CharacterTextSplitter
|
||||
from langchain.schema import Document
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
import umap
|
||||
|
||||
# Import your existing modules
|
||||
if __name__ == "__main__":
|
||||
@ -52,6 +53,7 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
|
||||
|
||||
# Initialize ChromaDB collection
|
||||
self._collection = self._get_vector_collection(recreate=recreate)
|
||||
self._update_umaps()
|
||||
|
||||
# Setup text splitter
|
||||
self.text_splitter = CharacterTextSplitter(
|
||||
@ -68,6 +70,26 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
|
||||
def collection(self):
|
||||
return self._collection
|
||||
|
||||
@property
|
||||
def umap_collection(self):
|
||||
return self._umap_collection
|
||||
|
||||
@property
|
||||
def umap_embedding_2d(self):
|
||||
return self._umap_embedding_2d
|
||||
|
||||
@property
|
||||
def umap_embedding_3d(self):
|
||||
return self._umap_embedding_3d
|
||||
|
||||
@property
|
||||
def umap_model_2d(self):
|
||||
return self._umap_model_2d
|
||||
|
||||
@property
|
||||
def umap_model_3d(self):
|
||||
return self._umap_model_3d
|
||||
|
||||
def _save_hash_state(self):
|
||||
"""Save the current file hash state to disk."""
|
||||
try:
|
||||
@ -185,6 +207,9 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
|
||||
# Save the hash state after successful update
|
||||
self._save_hash_state()
|
||||
|
||||
# Re-fit the UMAP for the new content
|
||||
self._update_umaps()
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error processing update for {file_path}: {e}")
|
||||
finally:
|
||||
@ -212,6 +237,23 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
|
||||
except Exception as e:
|
||||
logging.error(f"Error removing file from collection: {e}")
|
||||
|
||||
def _update_umaps(self):
|
||||
# Update the UMAP embeddings
|
||||
self._umap_collection = self._collection.get(include=["embeddings", "documents", "metadatas"])
|
||||
if not self._umap_collection or not len(self._umap_collection["embeddings"]):
|
||||
logging.warning("No embeddings found in the collection.")
|
||||
return
|
||||
|
||||
logging.info(f"Updating 2D UMAP for {len(self._umap_collection['embeddings'])} vectors")
|
||||
vectors = np.array(self._umap_collection["embeddings"])
|
||||
self._umap_model_2d = umap.UMAP(n_components=2, random_state=8911, metric="cosine") #, n_neighbors=15, min_dist=0.1)
|
||||
self._umap_embedding_2d = self._umap_model_2d.fit_transform(vectors)
|
||||
|
||||
logging.info(f"Updating 3D UMAP for {len(self._umap_collection['embeddings'])} vectors")
|
||||
vectors = np.array(self._umap_collection["embeddings"])
|
||||
self._umap_model_3d = umap.UMAP(n_components=3, random_state=8911, metric="cosine") #, n_neighbors=15, min_dist=0.1)
|
||||
self._umap_embedding_3d = self._umap_model_3d.fit_transform(vectors)
|
||||
|
||||
def _get_vector_collection(self, recreate=False):
|
||||
"""Get or create a ChromaDB collection."""
|
||||
# Initialize ChromaDB client
|
||||
|
Loading…
x
Reference in New Issue
Block a user