Compare commits
2 Commits
02915b9a23
...
ca9dd950b3
Author | SHA1 | Date | |
---|---|---|---|
ca9dd950b3 | |||
4ce616b64b |
@ -4,6 +4,11 @@ div {
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gl-container #scene {
|
||||||
|
top: 0px !important;
|
||||||
|
left: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
|
@ -19,8 +19,7 @@ import MenuIcon from '@mui/icons-material/Menu';
|
|||||||
|
|
||||||
|
|
||||||
import { ResumeBuilder } from './ResumeBuilder';
|
import { ResumeBuilder } from './ResumeBuilder';
|
||||||
import { Message } from './Message';
|
import { Message, ChatQuery, MessageList, MessageData } from './Message';
|
||||||
import { MessageData } from './MessageMeta';
|
|
||||||
import { SeverityType } from './Snack';
|
import { SeverityType } from './Snack';
|
||||||
import { VectorVisualizer } from './VectorVisualizer';
|
import { VectorVisualizer } from './VectorVisualizer';
|
||||||
import { Controls } from './Controls';
|
import { Controls } from './Controls';
|
||||||
@ -33,6 +32,8 @@ import '@fontsource/roboto/400.css';
|
|||||||
import '@fontsource/roboto/500.css';
|
import '@fontsource/roboto/500.css';
|
||||||
import '@fontsource/roboto/700.css';
|
import '@fontsource/roboto/700.css';
|
||||||
|
|
||||||
|
import MuiMarkdown from 'mui-markdown';
|
||||||
|
|
||||||
|
|
||||||
const getConnectionBase = (loc: any): string => {
|
const getConnectionBase = (loc: any): string => {
|
||||||
if (!loc.host.match(/.*battle-linux.*/)) {
|
if (!loc.host.match(/.*battle-linux.*/)) {
|
||||||
@ -130,10 +131,41 @@ const App = () => {
|
|||||||
}, [about, setAbout])
|
}, [about, setAbout])
|
||||||
|
|
||||||
|
|
||||||
const handleSubmitChatQuery = () => {
|
const handleSubmitChatQuery = (query: string) => {
|
||||||
chatRef.current?.submitQuery();
|
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
|
// Extract the sessionId from the URL if present, otherwise
|
||||||
// request a sessionId from the server.
|
// request a sessionId from the server.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -368,11 +400,13 @@ const App = () => {
|
|||||||
ref={chatRef}
|
ref={chatRef}
|
||||||
{...{
|
{...{
|
||||||
type: "chat",
|
type: "chat",
|
||||||
prompt: "Enter your question...",
|
prompt: "What would you like to know about James?",
|
||||||
sessionId,
|
sessionId,
|
||||||
connectionBase,
|
connectionBase,
|
||||||
setSnack
|
setSnack,
|
||||||
}}
|
preamble: chatPreamble,
|
||||||
|
defaultPrompts: chatQuestions
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</CustomTabPanel>
|
</CustomTabPanel>
|
||||||
@ -392,7 +426,7 @@ const App = () => {
|
|||||||
<CustomTabPanel tab={tab} index={3}>
|
<CustomTabPanel tab={tab} index={3}>
|
||||||
<Box className="ChatBox">
|
<Box className="ChatBox">
|
||||||
<Box className="Conversation">
|
<Box className="Conversation">
|
||||||
<Message {...{ message: { role: 'assistant', content: about }, submitQuery: handleSubmitChatQuery }} />
|
<Message {...{ message: { role: 'assistant', content: about }, submitQuery: handleSubmitChatQuery, connectionBase, sessionId, setSnack }} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</CustomTabPanel>
|
</CustomTabPanel>
|
||||||
|
@ -2,7 +2,7 @@ import { Box } from '@mui/material';
|
|||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import { SxProps, Theme } from '@mui/material';
|
import { SxProps, Theme } from '@mui/material';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MessageRoles } from './MessageMeta';
|
import { MessageRoles } from './Message';
|
||||||
|
|
||||||
interface ChatBubbleProps {
|
interface ChatBubbleProps {
|
||||||
role: MessageRoles,
|
role: MessageRoles,
|
||||||
@ -16,64 +16,52 @@ interface ChatBubbleProps {
|
|||||||
function ChatBubble({ role, isFullWidth, children, sx, className }: ChatBubbleProps) {
|
function ChatBubble({ role, isFullWidth, children, sx, className }: ChatBubbleProps) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
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', // Children inherit 'color' from parent
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
'user': {
|
'user': {
|
||||||
|
...defaultStyle,
|
||||||
backgroundColor: theme.palette.background.default, // Warm Gray (#D3CDBF)
|
backgroundColor: theme.palette.background.default, // Warm Gray (#D3CDBF)
|
||||||
border: `1px solid ${theme.palette.custom.highlight}`, // Golden Ochre (#D4A017)
|
border: `1px solid ${theme.palette.custom.highlight}`, // Golden Ochre (#D4A017)
|
||||||
borderRadius: '16px 16px 0 16px', // Rounded, flat bottom-right for user
|
borderRadius: `${defaultRadius} ${defaultRadius} 0 ${defaultRadius}`, // Rounded, flat bottom-right for user
|
||||||
padding: theme.spacing(1, 2),
|
|
||||||
maxWidth: isFullWidth ? '100%' : '100%',
|
|
||||||
minWidth: '80%',
|
|
||||||
alignSelf: 'flex-end', // Right-aligned for user
|
alignSelf: 'flex-end', // Right-aligned for user
|
||||||
color: theme.palette.primary.main, // Midnight Blue (#1A2536) for text
|
color: theme.palette.primary.main, // Midnight Blue (#1A2536) for text
|
||||||
'& > *': {
|
|
||||||
color: 'inherit', // Children inherit Midnight Blue unless overridden
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
'assistant': {
|
'assistant': {
|
||||||
|
...defaultStyle,
|
||||||
backgroundColor: theme.palette.primary.main, // Midnight Blue (#1A2536)
|
backgroundColor: theme.palette.primary.main, // Midnight Blue (#1A2536)
|
||||||
border: `1px solid ${theme.palette.secondary.main}`, // Dusty Teal (#4A7A7D)
|
border: `1px solid ${theme.palette.secondary.main}`, // Dusty Teal (#4A7A7D)
|
||||||
borderRadius: '16px 16px 16px 0', // Rounded, flat bottom-left for assistant
|
borderRadius: `${defaultRadius} ${defaultRadius} ${defaultRadius} 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: theme.palette.primary.contrastText, // Warm Gray (#D3CDBF) for text
|
||||||
'& > *': {
|
|
||||||
color: 'inherit', // Children inherit Warm Gray unless overridden
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
'system': {
|
'system': {
|
||||||
|
...defaultStyle,
|
||||||
backgroundColor: '#EDEAE0', // Soft warm gray that plays nice with #D3CDBF
|
backgroundColor: '#EDEAE0', // Soft warm gray that plays nice with #D3CDBF
|
||||||
border: `1px dashed ${theme.palette.custom.highlight}`, // Golden Ochre
|
border: `1px dashed ${theme.palette.custom.highlight}`, // Golden Ochre
|
||||||
borderRadius: '12px',
|
borderRadius: defaultRadius,
|
||||||
padding: theme.spacing(1, 2),
|
|
||||||
maxWidth: isFullWidth ? '100%' : '90%',
|
maxWidth: isFullWidth ? '100%' : '90%',
|
||||||
minWidth: '60%',
|
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
color: theme.palette.text.primary, // Charcoal Black
|
color: theme.palette.text.primary, // Charcoal Black
|
||||||
fontStyle: 'italic',
|
fontStyle: 'italic',
|
||||||
fontSize: '0.95rem',
|
|
||||||
'& > *': {
|
|
||||||
color: 'inherit',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
'info': {
|
'info': {
|
||||||
|
...defaultStyle,
|
||||||
backgroundColor: '#BFD8D8', // Softened Dusty Teal
|
backgroundColor: '#BFD8D8', // Softened Dusty Teal
|
||||||
border: `1px solid ${theme.palette.secondary.main}`, // Dusty Teal
|
border: `1px solid ${theme.palette.secondary.main}`, // Dusty Teal
|
||||||
borderRadius: '16px',
|
borderRadius: defaultRadius,
|
||||||
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
|
color: theme.palette.text.primary, // Charcoal Black (#2E2E2E) — much better contrast
|
||||||
opacity: 0.95,
|
opacity: 0.95,
|
||||||
fontSize: '0.875rem',
|
|
||||||
'& > *': {
|
|
||||||
color: 'inherit',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -109,11 +109,11 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ "system-prompt": prompt }),
|
body: JSON.stringify({ "system_prompt": prompt }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const newPrompt = data["system-prompt"];
|
const newPrompt = data["system_prompt"];
|
||||||
if (newPrompt !== serverSystemPrompt) {
|
if (newPrompt !== serverSystemPrompt) {
|
||||||
setServerSystemPrompt(newPrompt);
|
setServerSystemPrompt(newPrompt);
|
||||||
setSystemPrompt(newPrompt)
|
setSystemPrompt(newPrompt)
|
||||||
@ -141,11 +141,11 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Accept': '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 data = await response.json();
|
||||||
const newLength = data["message-history-length"];
|
const newLength = data["message_history_length"];
|
||||||
if (newLength !== messageHistoryLength) {
|
if (newLength !== messageHistoryLength) {
|
||||||
setMessageHistoryLength(newLength);
|
setMessageHistoryLength(newLength);
|
||||||
setSnack("Message history length updated", "success");
|
setSnack("Message history length updated", "success");
|
||||||
@ -159,7 +159,7 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
|||||||
sendMessageHistoryLength(messageHistoryLength);
|
sendMessageHistoryLength(messageHistoryLength);
|
||||||
|
|
||||||
}, [messageHistoryLength, setMessageHistoryLength, connectionBase, sessionId, setSnack]);
|
}, [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 {
|
try {
|
||||||
const response = await fetch(connectionBase + `/api/reset/${sessionId}`, {
|
const response = await fetch(connectionBase + `/api/reset/${sessionId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@ -183,9 +183,9 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
|||||||
case "tools":
|
case "tools":
|
||||||
setTools(value as Tool[]);
|
setTools(value as Tool[]);
|
||||||
break;
|
break;
|
||||||
case "system-prompt":
|
case "system_prompt":
|
||||||
setServerSystemPrompt((value as any)["system-prompt"].trim());
|
setServerSystemPrompt((value as any)["system_prompt"].trim());
|
||||||
setSystemPrompt((value as any)["system-prompt"].trim());
|
setSystemPrompt((value as any)["system_prompt"].trim());
|
||||||
break;
|
break;
|
||||||
case "history":
|
case "history":
|
||||||
console.log('TODO: handle history reset');
|
console.log('TODO: handle history reset');
|
||||||
@ -346,10 +346,10 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const serverSystemPrompt = data["system-prompt"].trim();
|
const serverSystemPrompt = data["system_prompt"].trim();
|
||||||
setServerSystemPrompt(serverSystemPrompt);
|
setServerSystemPrompt(serverSystemPrompt);
|
||||||
setSystemPrompt(serverSystemPrompt);
|
setSystemPrompt(serverSystemPrompt);
|
||||||
setMessageHistoryLength(data["message-history-length"]);
|
setMessageHistoryLength(data["message_history_length"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchTunables();
|
fetchTunables();
|
||||||
@ -402,7 +402,7 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
|||||||
/>
|
/>
|
||||||
<div style={{ display: "flex", flexDirection: "row", gap: "8px", paddingTop: "8px" }}>
|
<div style={{ display: "flex", flexDirection: "row", gap: "8px", paddingTop: "8px" }}>
|
||||||
<Button variant="contained" disabled={editSystemPrompt === systemPrompt} onClick={() => { setSystemPrompt(editSystemPrompt); }}>Set</Button>
|
<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>
|
</div>
|
||||||
</AccordionActions>
|
</AccordionActions>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
@ -481,7 +481,7 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
|||||||
</AccordionActions>
|
</AccordionActions>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Button startIcon={<ResetIcon />} onClick={() => { reset(["history"], "History cleared."); }}>Clear Backstory History</Button>
|
<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>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,55 +3,44 @@ import TextField from '@mui/material/TextField';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import SendIcon from '@mui/icons-material/Send';
|
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 PropagateLoader from "react-spinners/PropagateLoader";
|
||||||
|
|
||||||
import { Message, MessageList } from './Message';
|
import { Message, MessageList, MessageData } from './Message';
|
||||||
import { SeverityType } from './Snack';
|
import { SeverityType } from './Snack';
|
||||||
import { ContextStatus } from './ContextStatus';
|
import { ContextStatus } from './ContextStatus';
|
||||||
import { MessageData } from './MessageMeta';
|
|
||||||
|
|
||||||
const welcomeMarkdown = `
|
const loadingMessage: MessageData = { "role": "assistant", "content": "Establishing connection with server..." };
|
||||||
# Welcome to Backstory
|
|
||||||
|
|
||||||
Backstory was written by James Ketrenos in order to provide answers to
|
type ConversationMode = 'chat' | 'job_description' | 'resume' | 'fact_check';
|
||||||
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';
|
|
||||||
|
|
||||||
interface ConversationHandle {
|
interface ConversationHandle {
|
||||||
submitQuery: () => void;
|
submitQuery: (query: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConversationProps {
|
interface ConversationProps {
|
||||||
|
className?: string,
|
||||||
type: ConversationMode
|
type: ConversationMode
|
||||||
prompt: string,
|
prompt: string,
|
||||||
|
actionLabel?: string,
|
||||||
|
resetAction?: () => void,
|
||||||
|
resetLabel?: string,
|
||||||
connectionBase: string,
|
connectionBase: string,
|
||||||
sessionId: string | undefined,
|
sessionId?: string,
|
||||||
setSnack: (message: string, severity: SeverityType) => void,
|
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 [query, setQuery] = useState<string>("");
|
||||||
const [contextUsedPercentage, setContextUsedPercentage] = useState<number>(0);
|
const [contextUsedPercentage, setContextUsedPercentage] = useState<number>(0);
|
||||||
const [processing, setProcessing] = useState<boolean>(false);
|
const [processing, setProcessing] = useState<boolean>(false);
|
||||||
@ -62,12 +51,14 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({prompt,
|
|||||||
const [lastPromptTPS, setLastPromptTPS] = useState<number>(430);
|
const [lastPromptTPS, setLastPromptTPS] = useState<number>(430);
|
||||||
const [contextStatus, setContextStatus] = useState<ContextStatus>({ context_used: 0, max_context: 0 });
|
const [contextStatus, setContextStatus] = useState<ContextStatus>({ context_used: 0, max_context: 0 });
|
||||||
const [contextWarningShown, setContextWarningShown] = useState<boolean>(false);
|
const [contextWarningShown, setContextWarningShown] = useState<boolean>(false);
|
||||||
|
const [noInteractions, setNoInteractions] = useState<boolean>(true);
|
||||||
|
const setSnack = props.setSnack;
|
||||||
|
|
||||||
// Update the context status
|
// Update the context status
|
||||||
const updateContextStatus = useCallback(() => {
|
const updateContextStatus = useCallback(() => {
|
||||||
const fetchContextStatus = async () => {
|
const fetchContextStatus = async () => {
|
||||||
try {
|
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',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -87,36 +78,52 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({prompt,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchContextStatus();
|
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.
|
// Set the initial chat history to "loading" or the welcome message if loaded.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sessionId === undefined) {
|
if (props.sessionId === undefined) {
|
||||||
setConversation([loadingMessage]);
|
setConversation([loadingMessage]);
|
||||||
} else {
|
return;
|
||||||
fetch(connectionBase + `/api/history/${sessionId}`, {
|
}
|
||||||
|
|
||||||
|
const fetchHistory = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(props.connectionBase + `/api/history/${props.sessionId}/${props.type}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
.then(response => response.json())
|
if (!response.ok) {
|
||||||
.then(data => {
|
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
|
||||||
console.log(`Session id: ${sessionId} -- history returned from server with ${data.length} entries`)
|
}
|
||||||
setConversation([
|
const data = await response.json();
|
||||||
welcomeMessage,
|
console.log(`History returned from server with ${data.length} entries`)
|
||||||
...data
|
if (data.length === 0) {
|
||||||
]);
|
setConversation([
|
||||||
})
|
...(props.preamble || []),
|
||||||
.catch(error => {
|
...(props.messages || []),
|
||||||
console.error('Error generating session ID:', error);
|
]);
|
||||||
setSnack("Unable to obtain chat history.", "error");
|
setNoInteractions(true);
|
||||||
});
|
} else {
|
||||||
|
setConversation([
|
||||||
|
...(props.messages || []),
|
||||||
|
...(props.messageFilter ? props.messageFilter(data) : data)
|
||||||
|
]);
|
||||||
|
setNoInteractions(false);
|
||||||
|
}
|
||||||
updateContextStatus();
|
updateContextStatus();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating session ID:', error);
|
||||||
|
setSnack("Unable to obtain chat history.", "error");
|
||||||
}
|
}
|
||||||
}, [sessionId, setConversation, updateContextStatus, connectionBase, setSnack]);
|
};
|
||||||
|
if (props.sessionId !== undefined) {
|
||||||
|
fetchHistory();
|
||||||
|
}
|
||||||
|
}, [props.sessionId, setConversation, updateContextStatus, props.connectionBase, setSnack, props.preamble, props.type]);
|
||||||
|
|
||||||
const isScrolledToBottom = useCallback(()=> {
|
const isScrolledToBottom = useCallback(()=> {
|
||||||
// Current vertical scroll position
|
// Current vertical scroll position
|
||||||
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
||||||
@ -138,7 +145,6 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({prompt,
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const startCountdown = (seconds: number) => {
|
const startCountdown = (seconds: number) => {
|
||||||
if (timerRef.current) clearInterval(timerRef.current);
|
if (timerRef.current) clearInterval(timerRef.current);
|
||||||
setCountdown(seconds);
|
setCountdown(seconds);
|
||||||
@ -159,10 +165,6 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({prompt,
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitQuery = (text: string) => {
|
|
||||||
sendQuery(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopCountdown = () => {
|
const stopCountdown = () => {
|
||||||
if (timerRef.current) {
|
if (timerRef.current) {
|
||||||
clearInterval(timerRef.current);
|
clearInterval(timerRef.current);
|
||||||
@ -182,12 +184,16 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({prompt,
|
|||||||
};
|
};
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
submitQuery: () => {
|
submitQuery: (query: string) => {
|
||||||
sendQuery(query);
|
sendQuery(query);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// If context status changes, show a warning if necessary. If it drops
|
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
|
// back below the threshold, clear the warning trigger
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const context_used_percentage = Math.round(100 * contextStatus.context_used / contextStatus.max_context);
|
const context_used_percentage = Math.round(100 * contextStatus.context_used / contextStatus.max_context);
|
||||||
@ -201,7 +207,43 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({prompt,
|
|||||||
setContextUsedPercentage(context_used_percentage)
|
setContextUsedPercentage(context_used_percentage)
|
||||||
}, [contextStatus, setContextWarningShown, contextWarningShown, setContextUsedPercentage, setSnack]);
|
}, [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) => {
|
const sendQuery = async (query: string) => {
|
||||||
|
setNoInteractions(false);
|
||||||
|
|
||||||
if (!query.trim()) return;
|
if (!query.trim()) return;
|
||||||
|
|
||||||
//setTab(0);
|
//setTab(0);
|
||||||
@ -237,7 +279,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({prompt,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Make the fetch request with proper headers
|
// 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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -381,9 +423,12 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({prompt,
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="ConversationContainer" sx={{ display: "flex", flexDirection: "column", height: "100%", overflowY: "auto" }}>
|
<Box className={props.className || "Conversation"} sx={{ ...props.sx, display: "flex", flexDirection: "column" }}>
|
||||||
<Box className="Conversation" sx={{ flexGrow: 2, p: 1 }}>
|
{
|
||||||
{conversation.map((message, index) => <Message key={index} submitQuery={submitQuery} message={message} />)}
|
conversation.map((message, index) =>
|
||||||
|
<Message key={index} {...{ submitQuery, message, connectionBase: props.connectionBase, sessionId: props.sessionId, setSnack }} />
|
||||||
|
)
|
||||||
|
}
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
@ -407,7 +452,51 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({prompt,
|
|||||||
>Estimated response time: {countdown}s</Box>
|
>Estimated response time: {countdown}s</Box>
|
||||||
)}
|
)}
|
||||||
</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}
|
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>
|
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>
|
<Box sx={{ display: "flex", flexGrow: 1 }}></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>
|
</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,
|
RestartAlt as ResetIcon,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import PropagateLoader from "react-spinners/PropagateLoader";
|
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 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
|
* DocumentViewer component
|
||||||
*
|
*
|
||||||
@ -37,16 +55,16 @@ import MuiMarkdown from 'mui-markdown';
|
|||||||
* with different layouts for mobile and desktop views.
|
* with different layouts for mobile and desktop views.
|
||||||
*/
|
*/
|
||||||
const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
||||||
generateResume,
|
sx,
|
||||||
jobDescription,
|
connectionBase,
|
||||||
factCheck,
|
sessionId,
|
||||||
resume,
|
setSnack
|
||||||
setResume,
|
|
||||||
facts,
|
|
||||||
setFacts,
|
|
||||||
sx
|
|
||||||
}) => {
|
}) => {
|
||||||
// State for editing job description
|
// 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);
|
const [editJobDescription, setEditJobDescription] = useState<string | undefined>(jobDescription);
|
||||||
// Processing state to show loading indicators
|
// Processing state to show loading indicators
|
||||||
const [processing, setProcessing] = useState<string | undefined>(undefined);
|
const [processing, setProcessing] = useState<string | undefined>(undefined);
|
||||||
@ -89,8 +107,8 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
|||||||
}
|
}
|
||||||
setProcessing("resume");
|
setProcessing("resume");
|
||||||
setTimeout(() => { setActiveTab(1); }, 250); // Switch to resume view on mobile
|
setTimeout(() => { setActiveTab(1); }, 250); // Switch to resume view on mobile
|
||||||
generateResume(description);
|
console.log('generateResume(description);');
|
||||||
}, [generateResume, setProcessing, setActiveTab, setResume]);
|
}, [/*generateResume*/, setProcessing, setActiveTab, setResume]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger fact check and update UI state
|
* Trigger fact check and update UI state
|
||||||
@ -104,9 +122,9 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setProcessing("facts");
|
setProcessing("facts");
|
||||||
factCheck(resume);
|
console.log('factCheck(resume)');
|
||||||
setTimeout(() => { setActiveTab(2); }, 250); // Switch to resume view on mobile
|
setTimeout(() => { setActiveTab(2); }, 250); // Switch to resume view on mobile
|
||||||
}, [factCheck, setResume, setProcessing, setActiveTab, setFacts]);
|
}, [/*factCheck,*/ setResume, setProcessing, setActiveTab, setFacts]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setEditJobDescription(jobDescription);
|
setEditJobDescription(jobDescription);
|
||||||
@ -159,62 +177,73 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
|||||||
triggerGeneration(editJobDescription || "");
|
triggerGeneration(editJobDescription || "");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const handleJobQuery = (query: string) => {
|
||||||
|
triggerGeneration(query);
|
||||||
|
};
|
||||||
|
|
||||||
const renderJobDescriptionView = () => {
|
const jobDescriptionQuestions = [
|
||||||
const children = [];
|
<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) {
|
const filterJobDescriptionMessages = (messages: MessageList): MessageList => {
|
||||||
children.push(
|
/* The second messages is the RESUME (the LLM response to the JOB-DESCRIPTION) */
|
||||||
<Document key="jobDescription" sx={{ display: "flex", flexGrow: 1 }} title="">
|
if (messages.length > 1) {
|
||||||
<TextField
|
setResume(messages[1]);
|
||||||
variant="outlined"
|
} else if (resume !== undefined) {
|
||||||
fullWidth
|
setResume(undefined);
|
||||||
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>)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
children.push(
|
/* Filter out the RESUME */
|
||||||
<Box key="jobActions" sx={{ display: "flex", justifyContent: "center", flexDirection: "row" }}>
|
const reduced = messages.filter((message, index) => index != 1);
|
||||||
<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>
|
|
||||||
);
|
|
||||||
|
|
||||||
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 = () => (
|
const renderResumeView = () => (
|
||||||
<Box key="ResumeView" sx={{ display: "flex", flexDirection: "column", overflow: "auto", flexGrow: 1, flexBasis: 0 }}>
|
<Box key="ResumeView" sx={{ display: "flex", flexDirection: "column", overflow: "auto", flexGrow: 1, flexBasis: 0 }}>
|
||||||
<Document sx={{ display: "flex", flexGrow: 1 }} title="">
|
<Document sx={{ display: "flex", flexGrow: 1 }} title="">
|
||||||
{resume !== undefined && <Message message={resume} />}
|
{resume !== undefined && <Message {...{ message: resume, connectionBase, sessionId, setSnack }} />}
|
||||||
</Document>
|
</Document>
|
||||||
{processing === "resume" && (
|
{processing === "resume" && (
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
@ -257,13 +286,7 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
|||||||
const renderFactCheckView = () => (
|
const renderFactCheckView = () => (
|
||||||
<Box key="FactView" sx={{ display: "flex", flexDirection: "column", overflow: "auto", flexGrow: 1, flexBasis: 0, p: 0 }}>
|
<Box key="FactView" sx={{ display: "flex", flexDirection: "column", overflow: "auto", flexGrow: 1, flexBasis: 0, p: 0 }}>
|
||||||
<Document sx={{ display: "flex", flexGrow: 1 }} title="">
|
<Document sx={{ display: "flex", flexGrow: 1 }} title="">
|
||||||
{facts !== undefined && <Message message={facts} />}
|
{facts !== undefined && <Message {...{ message: facts, connectionBase, sessionId, setSnack }} />}
|
||||||
{/* <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> */}
|
|
||||||
</Document>
|
</Document>
|
||||||
{processing === "facts" && (
|
{processing === "facts" && (
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
@ -336,7 +359,7 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
|||||||
const otherRatio = showResume ? (100 - splitRatio / 2) : 100;
|
const otherRatio = showResume ? (100 - splitRatio / 2) : 100;
|
||||||
const children = [];
|
const children = [];
|
||||||
children.push(
|
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()}
|
{renderJobDescriptionView()}
|
||||||
</Box>);
|
</Box>);
|
||||||
|
|
||||||
@ -391,7 +414,7 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 }}>
|
<Box sx={{ display: 'flex', flexGrow: 1, flexDirection: 'row', overflow: 'hidden', p: 0 }}>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
@ -401,7 +424,7 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1, ...sx }}>
|
<Box sx={{ ...sx, display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
|
||||||
{getActiveDesktopContent()}
|
{getActiveDesktopContent()}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,15 @@
|
|||||||
import { useState, useRef } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import Accordion from '@mui/material/Accordion';
|
||||||
|
import AccordionSummary from '@mui/material/AccordionSummary';
|
||||||
|
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 Box from '@mui/material/Box';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
@ -11,17 +22,50 @@ import { ExpandMore } from './ExpandMore';
|
|||||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||||
import CheckIcon from '@mui/icons-material/Check';
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
|
|
||||||
import { MessageData, MessageMeta } from './MessageMeta';
|
|
||||||
import { ChatBubble } from './ChatBubble';
|
import { ChatBubble } from './ChatBubble';
|
||||||
import { StyledMarkdown } from './StyledMarkdown';
|
import { StyledMarkdown } from './StyledMarkdown';
|
||||||
import { Tooltip } from '@mui/material';
|
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[];
|
type MessageList = MessageData[];
|
||||||
|
|
||||||
interface MessageInterface {
|
interface MessageProps {
|
||||||
message?: MessageData,
|
message?: MessageData,
|
||||||
isFullWidth?: boolean,
|
isFullWidth?: boolean,
|
||||||
submitQuery?: (text: string) => void
|
submitQuery?: (text: string) => void,
|
||||||
|
sessionId?: string,
|
||||||
|
connectionBase: string,
|
||||||
|
setSnack: (message: string, severity: SeverityType) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ChatQueryInterface {
|
interface ChatQueryInterface {
|
||||||
@ -29,18 +73,135 @@ interface ChatQueryInterface {
|
|||||||
submitQuery?: (text: string) => void
|
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) => {
|
const ChatQuery = ({ text, submitQuery }: ChatQueryInterface) => {
|
||||||
return (submitQuery
|
if (submitQuery === undefined) {
|
||||||
? <Button variant="outlined" sx={{
|
return (<Box>{text}</Box>);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button variant="outlined" sx={{
|
||||||
color: theme => theme.palette.custom.highlight, // Golden Ochre (#D4A017)
|
color: theme => theme.palette.custom.highlight, // Golden Ochre (#D4A017)
|
||||||
borderColor: theme => theme.palette.custom.highlight,
|
borderColor: theme => theme.palette.custom.highlight,
|
||||||
m: 1
|
m: 1
|
||||||
}}
|
}}
|
||||||
size="small" onClick={(e: any) => { console.log(text); submitQuery(text); }}>{text}</Button>
|
size="small" onClick={(e: any) => { console.log(text); submitQuery(text); }}>
|
||||||
: <Box>{text}</Box>);
|
{text}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Message = ({ message, submitQuery, isFullWidth }: MessageInterface) => {
|
const Message = ({ message, submitQuery, isFullWidth, sessionId, setSnack, connectionBase }: MessageProps) => {
|
||||||
const [expanded, setExpanded] = useState<boolean>(false);
|
const [expanded, setExpanded] = useState<boolean>(false);
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const textFieldRef = useRef(null);
|
const textFieldRef = useRef(null);
|
||||||
@ -72,32 +233,31 @@ const Message = ({ message, submitQuery, isFullWidth }: MessageInterface) => {
|
|||||||
const formattedContent = message.content.trim();
|
const formattedContent = message.content.trim();
|
||||||
|
|
||||||
return (
|
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" }}>
|
<CardContent ref={textFieldRef} sx={{ position: "relative", display: "flex", flexDirection: "column", overflowX: "auto" }}>
|
||||||
<Tooltip title="Copy to clipboard" placement="top" arrow>
|
<Tooltip title="Copy to clipboard" placement="top" arrow>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 8,
|
top: 0,
|
||||||
right: 8,
|
right: 0,
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
bgcolor: 'background.paper',
|
bgcolor: 'background.paper',
|
||||||
'&:hover': { bgcolor: 'action.hover' },
|
'&:hover': { bgcolor: 'action.hover' },
|
||||||
}}
|
}}
|
||||||
size="small"
|
size="small"
|
||||||
color={copied ? "success" : "default"}
|
color={copied ? "success" : "default"}
|
||||||
>
|
>
|
||||||
{copied ? <CheckIcon sx={{ width: 16, height: 16 }} /> : <ContentCopyIcon sx={{ width: 16, height: 16 }} />}
|
{copied ? <CheckIcon sx={{ width: 16, height: 16 }} /> : <ContentCopyIcon sx={{ width: 16, height: 16 }} />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{message.role !== 'user' ?
|
{message.role !== 'user' ?
|
||||||
<StyledMarkdown
|
<StyledMarkdown
|
||||||
className="MessageContent"
|
className="MessageContent"
|
||||||
sx={{ display: "flex", color: 'text.secondary' }}
|
sx={{ display: "flex", color: 'text.secondary' }}
|
||||||
|
|
||||||
{...{ content: formattedContent, submitQuery }} />
|
{...{ content: formattedContent, submitQuery }} />
|
||||||
:
|
:
|
||||||
<Typography
|
<Typography
|
||||||
@ -110,8 +270,8 @@ const Message = ({ message, submitQuery, isFullWidth }: MessageInterface) => {
|
|||||||
}
|
}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
{message.metadata && <>
|
{message.metadata && <>
|
||||||
<CardActions disableSpacing>
|
<CardActions disableSpacing sx={{ justifySelf: "flex-end" }}>
|
||||||
<Typography sx={{ color: "darkgrey", p: 1, textAlign: "end", flexGrow: 1 }}>LLM information for this query</Typography>
|
<Button variant="text" onClick={handleExpandClick} sx={{ color: "darkgrey", p: 1, flexGrow: 0 }}>LLM information for this query</Button>
|
||||||
<ExpandMore
|
<ExpandMore
|
||||||
expand={expanded}
|
expand={expanded}
|
||||||
onClick={handleExpandClick}
|
onClick={handleExpandClick}
|
||||||
@ -123,7 +283,7 @@ const Message = ({ message, submitQuery, isFullWidth }: MessageInterface) => {
|
|||||||
</CardActions>
|
</CardActions>
|
||||||
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<MessageMeta metadata={message.metadata} />
|
<MessageMeta {...{ ...message.metadata, sessionId, connectionBase, setSnack }} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</>}
|
</>}
|
||||||
@ -132,12 +292,17 @@ const Message = ({ message, submitQuery, isFullWidth }: MessageInterface) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
MessageInterface,
|
MessageProps,
|
||||||
MessageList,
|
MessageList,
|
||||||
|
ChatQueryInterface,
|
||||||
|
MessageMetaProps,
|
||||||
|
MessageData,
|
||||||
|
MessageRoles
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Message,
|
Message,
|
||||||
ChatQuery,
|
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 { useState, useCallback, useEffect } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import { SeverityType } from './Snack';
|
import { SeverityType } from './Snack';
|
||||||
import { ContextStatus } from './ContextStatus';
|
import { MessageData, MessageMetaProps } from './Message';
|
||||||
import { MessageData, MessageMetadata } from './MessageMeta';
|
|
||||||
import { DocumentViewer } from './DocumentViewer';
|
import { DocumentViewer } from './DocumentViewer';
|
||||||
|
|
||||||
interface ResumeBuilderProps {
|
interface ResumeBuilderProps {
|
||||||
@ -21,324 +20,15 @@ type Resume = {
|
|||||||
resume: MessageData | undefined,
|
resume: MessageData | undefined,
|
||||||
fact_check: MessageData | undefined,
|
fact_check: MessageData | undefined,
|
||||||
job_description: string,
|
job_description: string,
|
||||||
metadata: MessageMetadata
|
metadata: MessageMetaProps
|
||||||
};
|
};
|
||||||
|
|
||||||
const ResumeBuilder = ({ facts, setFacts, resume, setResume, setProcessing, processing, connectionBase, sessionId, setSnack }: ResumeBuilderProps) => {
|
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 [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) {
|
if (sessionId === undefined) {
|
||||||
return (<></>);
|
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 (
|
return (
|
||||||
<Box className="DocBox">
|
<Box className="DocBox">
|
||||||
<Box className="Conversation" sx={{ p: 0, pt: 1 }}>
|
<Box className="Conversation" sx={{ p: 0, pt: 1 }}>
|
||||||
@ -350,7 +40,7 @@ const ResumeBuilder = ({ facts, setFacts, resume, setResume, setProcessing, proc
|
|||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
height: "calc(0vh - 0px)", // Hack to make the height work
|
height: "calc(0vh - 0px)", // Hack to make the height work
|
||||||
}} {...{ factCheck, facts, jobDescription, generateResume, resume, setFacts, setResume, setJobDescription }} />
|
}} {...{ setSnack, connectionBase, sessionId }} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -7,6 +7,8 @@ import TextField from '@mui/material/TextField';
|
|||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import SendIcon from '@mui/icons-material/Send';
|
import SendIcon from '@mui/icons-material/Send';
|
||||||
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
import Switch from '@mui/material/Switch';
|
||||||
|
|
||||||
import { SeverityType } from './Snack';
|
import { SeverityType } from './Snack';
|
||||||
|
|
||||||
@ -19,6 +21,8 @@ interface ResultData {
|
|||||||
embeddings: number[][] | number[][][];
|
embeddings: number[][] | number[][][];
|
||||||
documents: string[];
|
documents: string[];
|
||||||
metadatas: Metadata[];
|
metadatas: Metadata[];
|
||||||
|
ids: string[];
|
||||||
|
dimensions: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PlotData {
|
interface PlotData {
|
||||||
@ -39,6 +43,8 @@ interface VectorVisualizerProps {
|
|||||||
connectionBase: string;
|
connectionBase: string;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
setSnack: (message: string, severity: SeverityType) => void;
|
setSnack: (message: string, severity: SeverityType) => void;
|
||||||
|
inline?: boolean;
|
||||||
|
rag?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChromaResult {
|
interface ChromaResult {
|
||||||
@ -48,7 +54,8 @@ interface ChromaResult {
|
|||||||
metadatas: Metadata[];
|
metadatas: Metadata[];
|
||||||
query_embedding: number[];
|
query_embedding: number[];
|
||||||
query?: string;
|
query?: string;
|
||||||
vector_embedding?: number[];
|
umap_embedding_2d?: number[];
|
||||||
|
umap_embedding_3d?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeDimension = (arr: number[]): number[] => {
|
const normalizeDimension = (arr: number[]): number[] => {
|
||||||
@ -89,11 +96,12 @@ const symbolMap: Record<string, string> = {
|
|||||||
'query': 'circle',
|
'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 [plotData, setPlotData] = useState<PlotData | null>(null);
|
||||||
const [query, setQuery] = useState<string>('');
|
const [newQuery, setNewQuery] = useState<string>('');
|
||||||
const [queryEmbedding, setQueryEmbedding] = useState<ChromaResult | undefined>(undefined);
|
const [newQueryEmbedding, setNewQueryEmbedding] = useState<ChromaResult | undefined>(undefined);
|
||||||
const [result, setResult] = useState<ResultData | undefined>(undefined);
|
const [result, setResult] = useState<ResultData | undefined>(undefined);
|
||||||
|
const [view2D, setView2D] = useState<boolean>(true);
|
||||||
const [tooltip, setTooltip] = useState<{
|
const [tooltip, setTooltip] = useState<{
|
||||||
visible: boolean,
|
visible: boolean,
|
||||||
// x: number,
|
// x: number,
|
||||||
@ -105,7 +113,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, connectio
|
|||||||
|
|
||||||
// Get the collection to visualize
|
// Get the collection to visualize
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (result !== undefined || sessionId === undefined) {
|
if ((result !== undefined && result.dimensions !== (view2D ? 3 : 2)) || sessionId === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const fetchCollection = async () => {
|
const fetchCollection = async () => {
|
||||||
@ -115,9 +123,10 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, connectio
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'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);
|
setResult(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error obtaining collection information:', error);
|
console.error('Error obtaining collection information:', error);
|
||||||
@ -126,7 +135,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, connectio
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchCollection();
|
fetchCollection();
|
||||||
}, [result, setResult, connectionBase, setSnack, sessionId])
|
}, [result, setResult, connectionBase, setSnack, sessionId, view2D])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!result || !result.embeddings) return;
|
if (!result || !result.embeddings) return;
|
||||||
@ -135,12 +144,31 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, connectio
|
|||||||
const vectors: number[][] = [...result.embeddings as number[][]];
|
const vectors: number[][] = [...result.embeddings as number[][]];
|
||||||
const documents = [...result.documents || []];
|
const documents = [...result.documents || []];
|
||||||
const metadatas = [...result.metadatas || []];
|
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' });
|
metadatas.unshift({ doc_type: 'query' });
|
||||||
documents.unshift(queryEmbedding.query || '');
|
documents.unshift('Query');
|
||||||
vectors.unshift(queryEmbedding.vector_embedding);
|
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 is2D = vectors.every((v: number[]) => v.length === 2);
|
||||||
const is3D = vectors.every((v: number[]) => v.length === 3);
|
const is3D = vectors.every((v: number[]) => v.length === 3);
|
||||||
if (!is2D && !is3D) {
|
if (!is2D && !is3D) {
|
||||||
@ -148,11 +176,19 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, connectio
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const doc_types = metadatas.map(m => m.doc_type || 'unknown');
|
const doc_types = metadatas.map(m => m.doc_type || 'unknown')
|
||||||
const sizes = doc_types.map(type => {
|
|
||||||
|
const sizes = doc_types.map((type, index) => {
|
||||||
if (!sizeMap[type]) {
|
if (!sizeMap[type]) {
|
||||||
sizeMap[type] = 5;
|
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];
|
return sizeMap[type];
|
||||||
});
|
});
|
||||||
const symbols = doc_types.map(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' },
|
xaxis: { title: 'X', gridcolor: '#cccccc', zerolinecolor: '#aaaaaa' },
|
||||||
yaxis: { title: 'Y', 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 = {
|
const data: any = {
|
||||||
@ -212,17 +248,23 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, connectio
|
|||||||
|
|
||||||
setPlotData({ data, layout });
|
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) => {
|
const handleKeyPress = (event: any) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
sendQuery(query);
|
sendQuery(newQuery);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendQuery = async (query: string) => {
|
const sendQuery = async (query: string) => {
|
||||||
if (!query.trim()) return;
|
if (!query.trim()) return;
|
||||||
setQuery('');
|
setNewQuery('');
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${connectionBase}/api/similarity/${sessionId}`, {
|
const response = await fetch(`${connectionBase}/api/similarity/${sessionId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@ -231,11 +273,11 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, connectio
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
query: query,
|
query: query,
|
||||||
|
dimensions: view2D ? 2 : 3,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
const chroma: ChromaResult = await response.json();
|
const chroma: ChromaResult = await response.json();
|
||||||
console.log('Chroma:', chroma);
|
setNewQueryEmbedding(chroma);
|
||||||
setQueryEmbedding(chroma);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error obtaining query similarity information:', error);
|
console.error('Error obtaining query similarity information:', error);
|
||||||
setSnack("Unable to obtain query similarity information.", "error");
|
setSnack("Unable to obtain query similarity information.", "error");
|
||||||
@ -249,86 +291,93 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, connectio
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Box className="VectorVisualizer" sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1, overflow: 'hidden' }}>
|
||||||
<Card sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', mb: 1, pt: 0 }}>
|
{
|
||||||
<Typography variant="h6" sx={{ p: 1, pt: 0 }}>
|
!inline &&
|
||||||
Similarity Visualization via Uniform Manifold Approximation and Projection (UMAP)
|
<Card sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', mb: 1, pt: 0 }}>
|
||||||
</Typography>
|
<Typography variant="h6" sx={{ p: 1, pt: 0 }}>
|
||||||
</Card>
|
Similarity Visualization via Uniform Manifold Approximation and Projection (UMAP)
|
||||||
<Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'center', alignItems: 'center' }}>
|
</Typography>
|
||||||
<Plot
|
</Card>
|
||||||
onClick={(event: any) => {
|
}
|
||||||
const point = event.points[0];
|
<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" />
|
||||||
console.log('Point:', point);
|
<Plot
|
||||||
const type = point.customdata.type;
|
onClick={(event: any) => {
|
||||||
const text = point.customdata.doc;
|
const point = event.points[0];
|
||||||
const emoji = emojiMap[type] || '❓';
|
console.log('Point:', point);
|
||||||
setTooltip({
|
const type = point.customdata.type;
|
||||||
visible: true,
|
const text = point.customdata.doc;
|
||||||
background: point['marker.color'],
|
const emoji = emojiMap[type] || '❓';
|
||||||
color: getTextColorForBackground(point['marker.color']),
|
setTooltip({
|
||||||
content: `${emoji} ${type.toUpperCase()}\n${text}`,
|
visible: true,
|
||||||
});
|
background: point['marker.color'],
|
||||||
}}
|
color: getTextColorForBackground(point['marker.color']),
|
||||||
|
content: `${emoji} ${type.toUpperCase()}\n${text}`,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
|
||||||
data={[plotData.data]}
|
data={[plotData.data]}
|
||||||
useResizeHandler={true}
|
useResizeHandler={true}
|
||||||
config={{
|
config={{
|
||||||
responsive: true,
|
responsive: true,
|
||||||
displayModeBar: false,
|
// displayModeBar: false,
|
||||||
displaylogo: false,
|
displaylogo: false,
|
||||||
showSendToCloud: false,
|
showSendToCloud: false,
|
||||||
staticPlot: 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}
|
layout={plotData.layout}
|
||||||
/>
|
/>
|
||||||
</Box>
|
{!inline &&
|
||||||
<Card sx={{
|
<Card sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
mt: 1,
|
mt: 1,
|
||||||
p: 0.5,
|
p: 0.5,
|
||||||
color: tooltip?.color || '#2E2E2E',
|
color: tooltip?.color || '#2E2E2E',
|
||||||
background: tooltip?.background || '#FFFFFF',
|
background: tooltip?.background || '#FFFFFF',
|
||||||
whiteSpace: 'pre-line',
|
whiteSpace: 'pre-line',
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
maxHeight: '20vh',
|
maxHeight: '20vh',
|
||||||
minHeight: '20vh',
|
minHeight: '20vh',
|
||||||
overflowWrap: 'break-all',
|
overflowWrap: 'break-all',
|
||||||
wordBreak: 'break-all',
|
wordBreak: 'break-all',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="body2" sx={{ p: 1, pt: 0 }}>
|
<Typography variant="body2" sx={{ p: 1, pt: 0 }}>
|
||||||
{tooltip?.content}
|
{tooltip?.content}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Card>
|
</Card>
|
||||||
{ queryEmbedding !== undefined &&
|
}
|
||||||
|
{!inline && newQueryEmbedding !== undefined &&
|
||||||
<Card sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', mt: 1, pb: 0 }}>
|
<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' }}>
|
<Typography variant="h6" sx={{ p: 1, pt: 0, maxHeight: '5rem', overflow: 'auto' }}>
|
||||||
Query: {queryEmbedding.query}
|
Query: {newQueryEmbedding.query}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Card>
|
</Card>
|
||||||
}
|
}
|
||||||
|
|
||||||
<Box className="Query" sx={{ display: "flex", flexDirection: "row", p: 1 }}>
|
{
|
||||||
<TextField
|
!inline &&
|
||||||
variant="outlined"
|
<Box className="Query" sx={{ display: "flex", flexDirection: "row", p: 1 }}>
|
||||||
fullWidth
|
<TextField
|
||||||
type="text"
|
variant="outlined"
|
||||||
value={query}
|
fullWidth
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
type="text"
|
||||||
onKeyDown={handleKeyPress}
|
value={newQuery}
|
||||||
placeholder="Enter query to find related documents..."
|
onChange={(e) => setNewQuery(e.target.value)}
|
||||||
id="QueryInput"
|
onKeyDown={handleKeyPress}
|
||||||
/>
|
placeholder="Enter query to find related documents..."
|
||||||
<Tooltip title="Send">
|
id="QueryInput"
|
||||||
<Button sx={{ m: 1 }} variant="contained" onClick={() => { sendQuery(query); }}><SendIcon /></Button>
|
/>
|
||||||
</Tooltip>
|
<Tooltip title="Send">
|
||||||
</Box>
|
<Button sx={{ m: 1 }} variant="contained" onClick={() => { sendQuery(newQuery); }}><SendIcon /></Button>
|
||||||
</>
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
716
src/server.py
716
src/server.py
@ -11,6 +11,7 @@ import uuid
|
|||||||
import subprocess
|
import subprocess
|
||||||
import re
|
import re
|
||||||
import math
|
import math
|
||||||
|
import copy
|
||||||
|
|
||||||
def try_import(module_name, pip_name=None):
|
def try_import(module_name, pip_name=None):
|
||||||
try:
|
try:
|
||||||
@ -52,6 +53,8 @@ from tools import (
|
|||||||
tools
|
tools
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CONTEXT_VERSION=2
|
||||||
|
|
||||||
rags = [
|
rags = [
|
||||||
{ "name": "JPK", "enabled": True, "description": "Expert data about James Ketrenos, including work history, personal hobbies, and projects." },
|
{ "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." },
|
# { "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()
|
""".strip()
|
||||||
|
|
||||||
system_generate_resume = f"""
|
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].
|
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:
|
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].
|
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.
|
Ensure the langauge is clear, concise, and aligned with industry standards for professional resumes.
|
||||||
"""
|
""".strip()
|
||||||
|
|
||||||
system_fact_check = f"""
|
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].
|
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.
|
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:
|
When answering queries, follow these steps:
|
||||||
1. You must not invent or assume any information not explicitly present in the [WORK HISTORY].
|
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].
|
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 = []
|
tool_log = []
|
||||||
command_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
|
final_result = all_responses[0] if len(all_responses) == 1 else all_responses
|
||||||
yield (final_result, tools_used)
|
yield (final_result, tools_used)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# %%
|
# %%
|
||||||
class WebServer:
|
class WebServer:
|
||||||
def __init__(self, logging, client, model=MODEL_NAME):
|
def __init__(self, logging, client, model=MODEL_NAME):
|
||||||
@ -431,71 +454,6 @@ class WebServer:
|
|||||||
return RedirectResponse(url=f"/{context['id']}", status_code=307)
|
return RedirectResponse(url=f"/{context['id']}", status_code=307)
|
||||||
#return JSONResponse({"redirect": f"/{context['id']}"})
|
#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}")
|
@self.app.put("/api/umap/{context_id}")
|
||||||
async def put_umap(context_id: str, request: Request):
|
async def put_umap(context_id: str, request: Request):
|
||||||
@ -515,12 +473,16 @@ class WebServer:
|
|||||||
dimensions = 2
|
dimensions = 2
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self.file_watcher.collection.get(include=["embeddings", "documents", "metadatas"])
|
result = self.file_watcher.umap_collection
|
||||||
vectors = np.array(result["embeddings"])
|
if dimensions == 2:
|
||||||
umap_model = umap.UMAP(n_components=dimensions, random_state=42) #, n_neighbors=15, min_dist=0.1)
|
logging.info("Returning 2D UMAP")
|
||||||
embedding = umap_model.fit_transform(vectors)
|
umap_embedding = self.file_watcher.umap_embedding_2d
|
||||||
context["umap_model"] = umap_model
|
else:
|
||||||
result["embeddings"] = embedding.tolist()
|
logging.info("Returning 3D UMAP")
|
||||||
|
umap_embedding = self.file_watcher.umap_embedding_3d
|
||||||
|
|
||||||
|
result["embeddings"] = umap_embedding.tolist()
|
||||||
|
|
||||||
return JSONResponse(result)
|
return JSONResponse(result)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -536,10 +498,6 @@ class WebServer:
|
|||||||
logging.warning(f"Invalid context_id: {context_id}")
|
logging.warning(f"Invalid context_id: {context_id}")
|
||||||
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
|
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:
|
try:
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
query = data.get("query", "")
|
query = data.get("query", "")
|
||||||
@ -552,28 +510,37 @@ class WebServer:
|
|||||||
chroma_results = self.file_watcher.find_similar(query=query, top_k=10)
|
chroma_results = self.file_watcher.find_similar(query=query, top_k=10)
|
||||||
if not chroma_results:
|
if not chroma_results:
|
||||||
return JSONResponse({"error": "No results found"}, status_code=404)
|
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()
|
chroma_embedding = chroma_results["query_embedding"]
|
||||||
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:
|
except Exception as e:
|
||||||
logging.error(e)
|
logging.error(e)
|
||||||
#return JSONResponse({"error": str(e)}, 500)
|
#return JSONResponse({"error": str(e)}, 500)
|
||||||
|
|
||||||
@self.app.put("/api/reset/{context_id}")
|
@self.app.put("/api/reset/{context_id}/{type}")
|
||||||
async def put_reset(context_id: str, request: Request):
|
async def put_reset(context_id: str, type: str, request: Request):
|
||||||
if not is_valid_uuid(context_id):
|
if not is_valid_uuid(context_id):
|
||||||
logging.warning(f"Invalid context_id: {context_id}")
|
logging.warning(f"Invalid context_id: {context_id}")
|
||||||
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
|
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
|
||||||
context = self.upsert_context(context_id)
|
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()
|
data = await request.json()
|
||||||
try:
|
try:
|
||||||
response = {}
|
response = {}
|
||||||
for reset in data["reset"]:
|
for reset in data["reset"]:
|
||||||
match reset:
|
match reset:
|
||||||
case "system-prompt":
|
case "system_prompt":
|
||||||
context["system"] = [{"role": "system", "content": system_message}]
|
context["sessions"][type]["system_prompt"] = system_message
|
||||||
response["system-prompt"] = { "system-prompt": system_message }
|
response["system_prompt"] = { "system_prompt": system_message }
|
||||||
case "rags":
|
case "rags":
|
||||||
context["rags"] = rags.copy()
|
context["rags"] = rags.copy()
|
||||||
response["rags"] = context["rags"]
|
response["rags"] = context["rags"]
|
||||||
@ -581,23 +548,23 @@ class WebServer:
|
|||||||
context["tools"] = default_tools(tools)
|
context["tools"] = default_tools(tools)
|
||||||
response["tools"] = context["tools"]
|
response["tools"] = context["tools"]
|
||||||
case "history":
|
case "history":
|
||||||
context["llm_history"] = []
|
context["sessions"][type]["llm_history"] = []
|
||||||
context["user_history"] = []
|
context["sessions"][type]["user_history"] = []
|
||||||
|
context["sessions"][type]["context_tokens"] = round(len(str(context["system"])) * 3 / 4) # Estimate context usage
|
||||||
response["history"] = []
|
response["history"] = []
|
||||||
context["context_tokens"] = round(len(str(context["system"])) * 3 / 4) # Estimate context usage
|
response["context_used"] = context["sessions"][type]["context_tokens"]
|
||||||
response["context_used"] = context["context_tokens"]
|
case "message_history_length":
|
||||||
case "message-history-length":
|
|
||||||
context["message_history_length"] = DEFAULT_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:
|
if not response:
|
||||||
return JSONResponse({ "error": "Usage: { reset: rags|tools|history|system-prompt}"})
|
return JSONResponse({ "error": "Usage: { reset: rags|tools|history|system_prompt}"})
|
||||||
else:
|
else:
|
||||||
self.save_context(context_id)
|
self.save_context(context_id)
|
||||||
return JSONResponse(response)
|
return JSONResponse(response)
|
||||||
|
|
||||||
except:
|
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}")
|
@self.app.put("/api/tunables/{context_id}")
|
||||||
async def put_tunables(context_id: str, request: Request):
|
async def put_tunables(context_id: str, request: Request):
|
||||||
@ -608,20 +575,20 @@ class WebServer:
|
|||||||
data = await request.json()
|
data = await request.json()
|
||||||
for k in data.keys():
|
for k in data.keys():
|
||||||
match k:
|
match k:
|
||||||
case "system-prompt":
|
case "system_prompt":
|
||||||
system_prompt = data[k].strip()
|
system_prompt = data[k].strip()
|
||||||
if not system_prompt:
|
if not system_prompt:
|
||||||
return JSONResponse({ "status": "error", "message": "System prompt can not be empty." })
|
return JSONResponse({ "status": "error", "message": "System prompt can not be empty." })
|
||||||
context["system"] = [{"role": "system", "content": system_prompt}]
|
context["system"] = [{"role": "system", "content": system_prompt}]
|
||||||
self.save_context(context_id)
|
self.save_context(context_id)
|
||||||
return JSONResponse({ "system-prompt": system_prompt })
|
return JSONResponse({ "system_prompt": system_prompt })
|
||||||
case "message-history-length":
|
case "message_history_length":
|
||||||
value = max(0, int(data[k]))
|
value = max(0, int(data[k]))
|
||||||
context["message_history_length"] = value
|
context["message_history_length"] = value
|
||||||
self.save_context(context_id)
|
self.save_context(context_id)
|
||||||
return JSONResponse({ "message-history-length": value })
|
return JSONResponse({ "message_history_length": value })
|
||||||
case _:
|
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}")
|
@self.app.get("/api/tunables/{context_id}")
|
||||||
async def get_tunables(context_id: str):
|
async def get_tunables(context_id: str):
|
||||||
@ -630,33 +597,29 @@ class WebServer:
|
|||||||
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
|
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
|
||||||
context = self.upsert_context(context_id)
|
context = self.upsert_context(context_id)
|
||||||
return JSONResponse({
|
return JSONResponse({
|
||||||
"system-prompt": context["system"][0]["content"],
|
"system_prompt": context["system"][0]["content"],
|
||||||
"message-history-length": context["message_history_length"]
|
"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}")
|
@self.app.get("/api/system-info/{context_id}")
|
||||||
async def get_system_info(context_id: str):
|
async def get_system_info(context_id: str):
|
||||||
return JSONResponse(system_info(self.model))
|
return JSONResponse(system_info(self.model))
|
||||||
|
|
||||||
@self.app.post("/api/chat/{context_id}")
|
@self.app.post("/api/chat/{context_id}/{type}")
|
||||||
async def chat_endpoint(context_id: str, request: Request):
|
async def chat_endpoint(context_id: str, type: str, request: Request):
|
||||||
if not is_valid_uuid(context_id):
|
if not is_valid_uuid(context_id):
|
||||||
logging.warning(f"Invalid context_id: {context_id}")
|
logging.warning(f"Invalid context_id: {context_id}")
|
||||||
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
|
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
|
||||||
context = self.upsert_context(context_id)
|
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()
|
data = await request.json()
|
||||||
|
|
||||||
# Create a custom generator that ensures flushing
|
# Create a custom generator that ensures flushing
|
||||||
async def flush_generator():
|
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
|
# Convert to JSON and add newline
|
||||||
yield json.dumps(message) + "\n"
|
yield json.dumps(message) + "\n"
|
||||||
# Save the history as its generated
|
# 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")
|
@self.app.post("/api/context")
|
||||||
async def create_context():
|
async def create_context():
|
||||||
context = self.create_context()
|
context = self.create_context()
|
||||||
self.logging.info(f"Generated new session as {context['id']}")
|
self.logging.info(f"Generated new session as {context['id']}")
|
||||||
return JSONResponse(context)
|
return JSONResponse(context)
|
||||||
|
|
||||||
@self.app.get("/api/history/{context_id}")
|
@self.app.get("/api/history/{context_id}/{type}")
|
||||||
async def get_history(context_id: str):
|
async def get_history(context_id: str, type: str):
|
||||||
context = self.upsert_context(context_id)
|
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}")
|
@self.app.get("/api/tools/{context_id}")
|
||||||
async def get_tools(context_id: str):
|
async def get_tools(context_id: str):
|
||||||
@ -764,7 +671,7 @@ class WebServer:
|
|||||||
tool["enabled"] = enabled
|
tool["enabled"] = enabled
|
||||||
self.save_context(context_id)
|
self.save_context(context_id)
|
||||||
return JSONResponse(context["tools"])
|
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:
|
except:
|
||||||
return JSONResponse({ "status": "error" }), 405
|
return JSONResponse({ "status": "error" }), 405
|
||||||
|
|
||||||
@ -788,17 +695,19 @@ class WebServer:
|
|||||||
tool["enabled"] = enabled
|
tool["enabled"] = enabled
|
||||||
self.save_context(context_id)
|
self.save_context(context_id)
|
||||||
return JSONResponse(context["rags"])
|
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:
|
except:
|
||||||
return JSONResponse({ "status": "error" }), 405
|
return JSONResponse({ "status": "error" }), 405
|
||||||
|
|
||||||
@self.app.get("/api/context-status/{context_id}")
|
@self.app.get("/api/context-status/{context_id}/{type}")
|
||||||
async def get_context_status(context_id):
|
async def get_context_status(context_id, type: str):
|
||||||
if not is_valid_uuid(context_id):
|
if not is_valid_uuid(context_id):
|
||||||
logging.warning(f"Invalid context_id: {context_id}")
|
logging.warning(f"Invalid context_id: {context_id}")
|
||||||
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
|
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
|
||||||
context = self.upsert_context(context_id)
|
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")
|
@self.app.get("/api/health")
|
||||||
async def health_check():
|
async def health_check():
|
||||||
@ -833,16 +742,80 @@ class WebServer:
|
|||||||
# Create the full file path
|
# Create the full file path
|
||||||
file_path = os.path.join(defines.session_dir, session_id)
|
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
|
# Serialize the data to JSON and write to file
|
||||||
with open(file_path, "w") as f:
|
with open(file_path, "w") as f:
|
||||||
json.dump(context, f)
|
json.dump(context, f)
|
||||||
if umap_model:
|
|
||||||
context["umap_model"] = umap_model
|
|
||||||
return session_id
|
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):
|
def load_context(self, session_id):
|
||||||
"""
|
"""
|
||||||
Load a serialized Python dictionary from a file in the sessions directory.
|
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:
|
with open(file_path, "r") as f:
|
||||||
self.contexts[session_id] = json.load(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):
|
def create_context(self, context_id = None):
|
||||||
if not context_id:
|
if not context_id:
|
||||||
context_id = str(uuid.uuid4())
|
context_id = str(uuid.uuid4())
|
||||||
system_context = [{"role": "system", "content": system_message}];
|
|
||||||
context = {
|
context = {
|
||||||
"id": context_id,
|
"id": context_id,
|
||||||
"system": system_context,
|
"version": CONTEXT_VERSION,
|
||||||
"system_generate_resume": system_generate_resume,
|
"sessions": {
|
||||||
"llm_history": [],
|
"chat": {
|
||||||
"user_history": [],
|
"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),
|
"tools": default_tools(tools),
|
||||||
"resume_history": [],
|
|
||||||
"rags": rags.copy(),
|
"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
|
"message_history_length": 5 # Number of messages to supply in context
|
||||||
}
|
}
|
||||||
logging.info(f"{context_id} created and added to sessions.")
|
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.")
|
logging.warning("No context ID provided. Creating a new context.")
|
||||||
return self.create_context()
|
return self.create_context()
|
||||||
if context_id in self.contexts:
|
if context_id in self.contexts:
|
||||||
logging.info(f"Context {context_id} found.")
|
|
||||||
return self.contexts[context_id]
|
return self.contexts[context_id]
|
||||||
logging.info(f"Context {context_id} not found. Creating new context.")
|
logging.info(f"Context {context_id} not found. Creating new context.")
|
||||||
return self.load_context(context_id)
|
return self.load_context(context_id)
|
||||||
|
|
||||||
async def chat(self, context, content):
|
async def chat(self, context, type, content):
|
||||||
if not self.file_watcher:
|
if not self.file_watcher:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -913,56 +905,173 @@ class WebServer:
|
|||||||
return
|
return
|
||||||
|
|
||||||
self.processing = True
|
self.processing = True
|
||||||
|
|
||||||
llm_history = context["llm_history"]
|
|
||||||
user_history = context["user_history"]
|
|
||||||
metadata = {
|
|
||||||
"rag": {},
|
|
||||||
"tools": [],
|
|
||||||
"eval_count": 0,
|
|
||||||
"eval_duration": 0,
|
|
||||||
"prompt_eval_count": 0,
|
|
||||||
"prompt_eval_duration": 0,
|
|
||||||
}
|
|
||||||
rag_docs = []
|
|
||||||
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: "
|
|
||||||
|
|
||||||
# Figure
|
try:
|
||||||
llm_history.append({"role": "user", "content": preamble + content})
|
llm_history = context["sessions"][type]["llm_history"]
|
||||||
user_history.append({"role": "user", "content": content})
|
user_history = context["sessions"][type]["user_history"]
|
||||||
|
metadata = {
|
||||||
|
"type": type,
|
||||||
|
"rag": { "documents": [] },
|
||||||
|
"tools": [],
|
||||||
|
"eval_count": 0,
|
||||||
|
"eval_duration": 0,
|
||||||
|
"prompt_eval_count": 0,
|
||||||
|
"prompt_eval_duration": 0,
|
||||||
|
}
|
||||||
|
|
||||||
if context["message_history_length"]:
|
# Default to not using tools
|
||||||
messages = context["system"] + llm_history[-context["message_history_length"]:]
|
enable_tools = False
|
||||||
else:
|
|
||||||
messages = context["system"] + llm_history
|
# 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:
|
||||||
|
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}")
|
||||||
|
|
||||||
|
llm_history.append({"role": "user", "content": preamble + content})
|
||||||
|
user_history.append({"role": "user", "content": content})
|
||||||
|
|
||||||
|
if context["message_history_length"]:
|
||||||
|
messages = create_system_message(context["sessions"][type]["system_prompt"]) + llm_history[-context["message_history_length"]:]
|
||||||
|
else:
|
||||||
|
messages = create_system_message(context["sessions"][type]["system_prompt"]) + llm_history
|
||||||
|
|
||||||
try:
|
|
||||||
# Estimate token length of new messages
|
# Estimate token length of new messages
|
||||||
ctx_size = self.get_optimal_ctx_size(context["context_tokens"], messages=llm_history[-1]["content"])
|
ctx_size = self.get_optimal_ctx_size(context["sessions"][type]["context_tokens"], messages=llm_history[-1]["content"])
|
||||||
yield {"status": "processing", "message": "Processing request...", "num_ctx": ctx_size}
|
|
||||||
|
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
|
# Use the async generator in an async for loop
|
||||||
response = self.client.chat(model=self.model, messages=messages, tools=llm_tools(context["tools"]), options={ "num_ctx": ctx_size })
|
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_count"] += response["eval_count"]
|
||||||
metadata["eval_duration"] += response["eval_duration"]
|
metadata["eval_duration"] += response["eval_duration"]
|
||||||
metadata["prompt_eval_count"] += response["prompt_eval_count"]
|
metadata["prompt_eval_count"] += response["prompt_eval_count"]
|
||||||
metadata["prompt_eval_duration"] += response["prompt_eval_duration"]
|
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 = []
|
tools_used = []
|
||||||
|
|
||||||
@ -1005,7 +1114,7 @@ class WebServer:
|
|||||||
metadata["tools"] = tools_used
|
metadata["tools"] = tools_used
|
||||||
|
|
||||||
# Estimate token length of new messages
|
# 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 }
|
yield {"status": "processing", "message": "Generating final response...", "num_ctx": ctx_size }
|
||||||
# Decrease creativity when processing tool call requests
|
# 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 })
|
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["eval_duration"] += response["eval_duration"]
|
||||||
metadata["prompt_eval_count"] += response["prompt_eval_count"]
|
metadata["prompt_eval_count"] += response["prompt_eval_count"]
|
||||||
metadata["prompt_eval_duration"] += response["prompt_eval_duration"]
|
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"]
|
reply = response["message"]["content"]
|
||||||
final_message = {"role": "assistant", "content": reply }
|
final_message = {"role": "assistant", "content": reply }
|
||||||
@ -1035,145 +1144,6 @@ class WebServer:
|
|||||||
finally:
|
finally:
|
||||||
self.processing = False
|
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):
|
def run(self, host="0.0.0.0", port=WEB_PORT, **kwargs):
|
||||||
try:
|
try:
|
||||||
if self.ssl_enabled:
|
if self.ssl_enabled:
|
||||||
|
@ -19,6 +19,7 @@ from langchain.text_splitter import CharacterTextSplitter
|
|||||||
from langchain.schema import Document
|
from langchain.schema import Document
|
||||||
from watchdog.observers import Observer
|
from watchdog.observers import Observer
|
||||||
from watchdog.events import FileSystemEventHandler
|
from watchdog.events import FileSystemEventHandler
|
||||||
|
import umap
|
||||||
|
|
||||||
# Import your existing modules
|
# Import your existing modules
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@ -52,7 +53,8 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
|
|||||||
|
|
||||||
# Initialize ChromaDB collection
|
# Initialize ChromaDB collection
|
||||||
self._collection = self._get_vector_collection(recreate=recreate)
|
self._collection = self._get_vector_collection(recreate=recreate)
|
||||||
|
self._update_umaps()
|
||||||
|
|
||||||
# Setup text splitter
|
# Setup text splitter
|
||||||
self.text_splitter = CharacterTextSplitter(
|
self.text_splitter = CharacterTextSplitter(
|
||||||
chunk_size=chunk_size,
|
chunk_size=chunk_size,
|
||||||
@ -67,7 +69,27 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
|
|||||||
@property
|
@property
|
||||||
def collection(self):
|
def collection(self):
|
||||||
return self._collection
|
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):
|
def _save_hash_state(self):
|
||||||
"""Save the current file hash state to disk."""
|
"""Save the current file hash state to disk."""
|
||||||
try:
|
try:
|
||||||
@ -184,7 +206,10 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
|
|||||||
|
|
||||||
# Save the hash state after successful update
|
# Save the hash state after successful update
|
||||||
self._save_hash_state()
|
self._save_hash_state()
|
||||||
|
|
||||||
|
# Re-fit the UMAP for the new content
|
||||||
|
self._update_umaps()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error processing update for {file_path}: {e}")
|
logging.error(f"Error processing update for {file_path}: {e}")
|
||||||
finally:
|
finally:
|
||||||
@ -212,6 +237,23 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error removing file from collection: {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):
|
def _get_vector_collection(self, recreate=False):
|
||||||
"""Get or create a ChromaDB collection."""
|
"""Get or create a ChromaDB collection."""
|
||||||
# Initialize ChromaDB client
|
# Initialize ChromaDB client
|
||||||
|
Loading…
x
Reference in New Issue
Block a user