Refactoring continues; working on styles
This commit is contained in:
parent
4ce616b64b
commit
ca9dd950b3
@ -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,9 +3,11 @@ 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, MessageData } from './Message';
|
import { Message, MessageList, MessageData } from './Message';
|
||||||
@ -14,24 +16,31 @@ import { ContextStatus } from './ContextStatus';
|
|||||||
|
|
||||||
const loadingMessage: MessageData = { "role": "assistant", "content": "Establishing connection with server..." };
|
const loadingMessage: MessageData = { "role": "assistant", "content": "Establishing connection with server..." };
|
||||||
|
|
||||||
type ConversationMode = 'chat' | 'fact-check' | 'system';
|
type ConversationMode = 'chat' | 'job_description' | 'resume' | 'fact_check';
|
||||||
|
|
||||||
interface ConversationHandle {
|
interface ConversationHandle {
|
||||||
submitQuery: (query: string) => 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,
|
sessionId?: string,
|
||||||
setSnack: (message: string, severity: SeverityType) => void,
|
setSnack: (message: string, severity: SeverityType) => void,
|
||||||
defaultPrompts?: React.ReactElement[],
|
defaultPrompts?: React.ReactElement[],
|
||||||
preamble?: MessageList,
|
preamble?: MessageList,
|
||||||
hideDefaultPrompts?: boolean,
|
hideDefaultPrompts?: boolean,
|
||||||
|
messageFilter?: (messages: MessageList) => MessageList,
|
||||||
|
messages?: MessageList,
|
||||||
|
sx?: SxProps<Theme>,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Conversation = forwardRef<ConversationHandle, ConversationProps>(({ prompt, type, preamble, hideDefaultPrompts, defaultPrompts, 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);
|
||||||
@ -43,12 +52,13 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({ prompt
|
|||||||
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 [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',
|
||||||
@ -68,18 +78,18 @@ 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]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchHistory = async () => {
|
const fetchHistory = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(connectionBase + `/api/history/${sessionId}`, {
|
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',
|
||||||
@ -89,12 +99,18 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({ prompt
|
|||||||
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
|
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log(`Session id: ${sessionId} -- history returned from server with ${data.length} entries`)
|
console.log(`History returned from server with ${data.length} entries`)
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
setConversation(preamble || []);
|
setConversation([
|
||||||
|
...(props.preamble || []),
|
||||||
|
...(props.messages || []),
|
||||||
|
]);
|
||||||
setNoInteractions(true);
|
setNoInteractions(true);
|
||||||
} else {
|
} else {
|
||||||
setConversation(data);
|
setConversation([
|
||||||
|
...(props.messages || []),
|
||||||
|
...(props.messageFilter ? props.messageFilter(data) : data)
|
||||||
|
]);
|
||||||
setNoInteractions(false);
|
setNoInteractions(false);
|
||||||
}
|
}
|
||||||
updateContextStatus();
|
updateContextStatus();
|
||||||
@ -103,10 +119,10 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({ prompt
|
|||||||
setSnack("Unable to obtain chat history.", "error");
|
setSnack("Unable to obtain chat history.", "error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (sessionId !== undefined) {
|
if (props.sessionId !== undefined) {
|
||||||
fetchHistory();
|
fetchHistory();
|
||||||
}
|
}
|
||||||
}, [sessionId, setConversation, updateContextStatus, connectionBase, setSnack, preamble]);
|
}, [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
|
||||||
@ -191,6 +207,40 @@ 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);
|
setNoInteractions(false);
|
||||||
|
|
||||||
@ -229,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',
|
||||||
@ -373,8 +423,12 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({ prompt
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="Conversation" sx={{ display: "flex", flexDirection: "column", overflowY: "auto" }}>
|
<Box className={props.className || "Conversation"} sx={{ ...props.sx, display: "flex", flexDirection: "column" }}>
|
||||||
{conversation.map((message, index) => <Message key={index} {...{ submitQuery, message, connectionBase, sessionId, setSnack }} />)}
|
{
|
||||||
|
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",
|
||||||
@ -398,26 +452,45 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({ prompt
|
|||||||
>Estimated response time: {countdown}s</Box>
|
>Estimated response time: {countdown}s</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box className="Query" sx={{ display: "flex", flexDirection: "row", p: 1 }}>
|
<Box className="Query" sx={{ display: "flex", flexDirection: props.type === "job_description" ? "column" : "row", p: 1 }}>
|
||||||
<TextField
|
<TextField
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
multiline={props.type === "job_description"}
|
||||||
type="text"
|
type="text"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
onKeyDown={handleKeyPress}
|
onKeyDown={handleKeyPress}
|
||||||
placeholder={prompt}
|
placeholder={props.prompt}
|
||||||
id="QueryInput"
|
id="QueryInput"
|
||||||
/>
|
/>
|
||||||
<Tooltip title="Send">
|
<Box key="jobActions" sx={{ display: "flex", justifyContent: "center", flexDirection: "row" }}>
|
||||||
<Button sx={{ m: 1 }} variant="contained" onClick={() => { sendQuery(query); }}><SendIcon /></Button>
|
<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>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
{(noInteractions || !hideDefaultPrompts) && defaultPrompts !== undefined && defaultPrompts.length &&
|
</Box>
|
||||||
|
{(noInteractions || !props.hideDefaultPrompts) && props.defaultPrompts !== undefined && props.defaultPrompts.length &&
|
||||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||||
{
|
{
|
||||||
defaultPrompts.map((element, index) => {
|
props.defaultPrompts.map((element, index) => {
|
||||||
return (<Box key={index}>{element}</Box>);
|
return (<Box key={index}>{element}</Box>);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -28,33 +28,21 @@ import { SxProps, Theme } from '@mui/material';
|
|||||||
|
|
||||||
import MuiMarkdown from 'mui-markdown';
|
import MuiMarkdown from 'mui-markdown';
|
||||||
|
|
||||||
import { Message } from './Message';
|
import { Message, ChatQuery } from './Message';
|
||||||
import { Document } from './Document';
|
import { Document } from './Document';
|
||||||
import { MessageData } from './Message';
|
import { MessageData, MessageList } from './Message';
|
||||||
import { SeverityType } from './Snack';
|
import { SeverityType } from './Snack';
|
||||||
|
import { Conversation } from './Conversation';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props for the DocumentViewer component
|
* Props for the DocumentViewer component
|
||||||
* @interface DocumentViewerProps
|
* @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
|
* @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 {
|
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>;
|
sx?: SxProps<Theme>;
|
||||||
connectionBase: string;
|
connectionBase: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@ -67,19 +55,16 @@ export interface DocumentViewerProps {
|
|||||||
* 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,
|
|
||||||
jobDescription,
|
|
||||||
factCheck,
|
|
||||||
resume,
|
|
||||||
setResume,
|
|
||||||
facts,
|
|
||||||
setFacts,
|
|
||||||
sx,
|
sx,
|
||||||
connectionBase,
|
connectionBase,
|
||||||
sessionId,
|
sessionId,
|
||||||
setSnack
|
setSnack
|
||||||
}) => {
|
}) => {
|
||||||
// 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);
|
||||||
@ -122,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
|
||||||
@ -137,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);
|
||||||
@ -192,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
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -363,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>);
|
||||||
|
|
||||||
@ -418,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>
|
||||||
@ -428,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>
|
||||||
);
|
);
|
||||||
|
@ -233,7 +233,7 @@ const Message = ({ message, submitQuery, isFullWidth, sessionId, setSnack, conne
|
|||||||
const formattedContent = message.content.trim();
|
const formattedContent = message.content.trim();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChatBubble className="Message" isFullWidth={isFullWidth} role={message.role} sx={{ 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
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
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, MessageMetaProps } from './Message';
|
||||||
import { DocumentViewer } from './DocumentViewer';
|
import { DocumentViewer } from './DocumentViewer';
|
||||||
|
|
||||||
@ -25,320 +24,11 @@ type Resume = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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, setSnack, setJobDescription, connectionBase, sessionId }} />
|
}} {...{ setSnack, connectionBase, sessionId }} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
624
src/server.py
624
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):
|
||||||
@ -566,20 +524,23 @@ class WebServer:
|
|||||||
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"]
|
||||||
@ -587,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):
|
||||||
@ -614,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):
|
||||||
@ -636,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
|
||||||
@ -681,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):
|
||||||
@ -770,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
|
||||||
|
|
||||||
@ -794,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():
|
||||||
@ -839,15 +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)
|
||||||
|
|
||||||
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.
|
||||||
@ -868,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": {
|
||||||
|
"chat": {
|
||||||
|
"system_prompt": system_message,
|
||||||
"llm_history": [],
|
"llm_history": [],
|
||||||
"user_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.")
|
||||||
@ -903,7 +891,7 @@ class WebServer:
|
|||||||
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
|
||||||
|
|
||||||
@ -918,23 +906,41 @@ class WebServer:
|
|||||||
|
|
||||||
self.processing = True
|
self.processing = True
|
||||||
|
|
||||||
llm_history = context["llm_history"]
|
try:
|
||||||
user_history = context["user_history"]
|
llm_history = context["sessions"][type]["llm_history"]
|
||||||
|
user_history = context["sessions"][type]["user_history"]
|
||||||
metadata = {
|
metadata = {
|
||||||
"rag": {},
|
"type": type,
|
||||||
|
"rag": { "documents": [] },
|
||||||
"tools": [],
|
"tools": [],
|
||||||
"eval_count": 0,
|
"eval_count": 0,
|
||||||
"eval_duration": 0,
|
"eval_duration": 0,
|
||||||
"prompt_eval_count": 0,
|
"prompt_eval_count": 0,
|
||||||
"prompt_eval_duration": 0,
|
"prompt_eval_duration": 0,
|
||||||
}
|
}
|
||||||
rag_docs = []
|
|
||||||
|
# Default to not using tools
|
||||||
|
enable_tools = False
|
||||||
|
|
||||||
|
# Default eo using RAG
|
||||||
|
enable_rag = True
|
||||||
|
|
||||||
|
# The first time a particular session type is used, it is handled differently. After the initial pass (once the
|
||||||
|
# llm_history has more than one entry), the standard 'chat' is used.
|
||||||
|
if len(user_history) >= 1:
|
||||||
|
process_type = "chat"
|
||||||
|
# Do not enable RAG when limiting context to the job description chat
|
||||||
|
if type == "job_description":
|
||||||
|
enable_rag = False
|
||||||
|
else:
|
||||||
|
process_type = type
|
||||||
|
|
||||||
|
if enable_rag:
|
||||||
for rag in context["rags"]:
|
for rag in context["rags"]:
|
||||||
if rag["enabled"] and rag["name"] == "JPK": # Only support JPK rag right now...
|
if rag["enabled"] and rag["name"] == "JPK": # Only support JPK rag right now...
|
||||||
yield {"status": "processing", "message": f"Checking RAG context {rag['name']}..."}
|
yield {"status": "processing", "message": f"Checking RAG context {rag['name']}..."}
|
||||||
chroma_results = self.file_watcher.find_similar(query=content, top_k=10)
|
chroma_results = self.file_watcher.find_similar(query=content, top_k=10)
|
||||||
if chroma_results:
|
if chroma_results:
|
||||||
rag_docs.extend(chroma_results["documents"])
|
|
||||||
chroma_embedding = chroma_results["query_embedding"]
|
chroma_embedding = chroma_results["query_embedding"]
|
||||||
metadata["rag"] = {
|
metadata["rag"] = {
|
||||||
**chroma_results,
|
**chroma_results,
|
||||||
@ -942,37 +948,130 @@ class WebServer:
|
|||||||
"umap_embedding_2d": self.file_watcher.umap_model_2d.transform([chroma_embedding])[0].tolist(),
|
"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()
|
"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 = ""
|
preamble = ""
|
||||||
if len(rag_docs):
|
rag_context = ""
|
||||||
|
for doc in metadata["rag"]["documents"]:
|
||||||
|
rag_context += doc
|
||||||
|
if rag_context:
|
||||||
preamble = f"""
|
preamble = f"""
|
||||||
1. Respond to this query: {content}
|
1. Respond to this query: {content}
|
||||||
2. If there is information in this context to enhance the answer, do so:
|
2. If there is information in this context to enhance the answer, do so:
|
||||||
[{context_tag}]:\n"""
|
[{context_tag}]
|
||||||
for doc in rag_docs:
|
{rag_context}
|
||||||
preamble += doc
|
[/{context_tag}]
|
||||||
preamble += f"\n[/{context_tag}]\nUse all of that information to respond to: "
|
Use that information to respond to: """
|
||||||
|
|
||||||
|
# Single job_description is provided; generate a resume
|
||||||
|
case "job_description":
|
||||||
|
# Always force the full resume to be in context
|
||||||
|
resume_doc = open(defines.resume_doc, "r").read()
|
||||||
|
work_history = f"{resume_doc}\n"
|
||||||
|
for doc in metadata["rag"]["documents"]:
|
||||||
|
work_history += f"{doc}\n"
|
||||||
|
|
||||||
|
preamble = f"""
|
||||||
|
[INTRO]
|
||||||
|
{resume_intro}
|
||||||
|
[/INTRO]
|
||||||
|
|
||||||
|
[WORK HISTORY]
|
||||||
|
{work_history}
|
||||||
|
[/WORK HISTORY]
|
||||||
|
|
||||||
|
[JOB DESCRIPTION]
|
||||||
|
{content}
|
||||||
|
[/JOB DESCRIPTION]
|
||||||
|
|
||||||
|
1. Use the above [INTRO] and [WORK HISTORY] to create the resume for the [JOB DESCRIPTION].
|
||||||
|
2. Do not use content from the [JOB DESCRIPTION] in the response unless the [WORK HISTORY] mentions them.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Seed the first context messages with the resume from the 'job_description' session
|
||||||
|
case "resume":
|
||||||
|
raise Exception(f"Invalid chat type: {type}")
|
||||||
|
|
||||||
|
# Fact check the resume created by the 'job_description' using only the RAG and resume
|
||||||
|
case "fact_check":
|
||||||
|
if len(context["sessions"]["resume"]["llm_history"]) < 3: # SYSTEM, USER, **ASSISTANT**
|
||||||
|
yield {"status": "done", "message": "No resume history found." }
|
||||||
|
return
|
||||||
|
|
||||||
|
resume = context["sessions"]["resume"]["llm_history"][2]
|
||||||
|
|
||||||
|
metadata = copy.deepcopy(resume["metadata"])
|
||||||
|
metadata["eval_count"] = 0
|
||||||
|
metadata["eval_duration"] = 0
|
||||||
|
metadata["prompt_eval_count"] = 0
|
||||||
|
metadata["prompt_eval_duration"] = 0
|
||||||
|
|
||||||
|
resume_doc = open(defines.resume_doc, "r").read()
|
||||||
|
work_history = f"{resume_doc}\n"
|
||||||
|
for doc in metadata["rag"]["documents"]:
|
||||||
|
work_history += f"{doc}\n"
|
||||||
|
|
||||||
|
preamble = f"""
|
||||||
|
[WORK HISTORY]
|
||||||
|
{work_history}
|
||||||
|
[/WORK HISTORY]
|
||||||
|
|
||||||
|
[RESUME]
|
||||||
|
{resume['content']}
|
||||||
|
[/RESUME]
|
||||||
|
"""
|
||||||
|
content = resume['content']
|
||||||
|
|
||||||
|
raise Exception(f"Invalid chat type: {type}")
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise Exception(f"Invalid chat type: {type}")
|
||||||
|
|
||||||
# Figure
|
|
||||||
llm_history.append({"role": "user", "content": preamble + content})
|
llm_history.append({"role": "user", "content": preamble + content})
|
||||||
user_history.append({"role": "user", "content": content})
|
user_history.append({"role": "user", "content": content})
|
||||||
|
|
||||||
if context["message_history_length"]:
|
if context["message_history_length"]:
|
||||||
messages = context["system"] + llm_history[-context["message_history_length"]:]
|
messages = create_system_message(context["sessions"][type]["system_prompt"]) + llm_history[-context["message_history_length"]:]
|
||||||
else:
|
else:
|
||||||
messages = context["system"] + llm_history
|
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
|
||||||
|
try:
|
||||||
|
if enable_tools:
|
||||||
response = self.client.chat(model=self.model, messages=messages, tools=llm_tools(context["tools"]), options={ "num_ctx": ctx_size })
|
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 = []
|
||||||
|
|
||||||
@ -1015,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 })
|
||||||
@ -1023,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 }
|
||||||
@ -1045,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:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user