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',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ "system-prompt": prompt }),
|
||||
body: JSON.stringify({ "system_prompt": prompt }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const newPrompt = data["system-prompt"];
|
||||
const newPrompt = data["system_prompt"];
|
||||
if (newPrompt !== serverSystemPrompt) {
|
||||
setServerSystemPrompt(newPrompt);
|
||||
setSystemPrompt(newPrompt)
|
||||
@ -141,11 +141,11 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ "message-history-length": length }),
|
||||
body: JSON.stringify({ "message_history_length": length }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const newLength = data["message-history-length"];
|
||||
const newLength = data["message_history_length"];
|
||||
if (newLength !== messageHistoryLength) {
|
||||
setMessageHistoryLength(newLength);
|
||||
setSnack("Message history length updated", "success");
|
||||
@ -159,7 +159,7 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
||||
sendMessageHistoryLength(messageHistoryLength);
|
||||
|
||||
}, [messageHistoryLength, setMessageHistoryLength, connectionBase, sessionId, setSnack]);
|
||||
const reset = async (types: ("rags" | "tools" | "history" | "system-prompt" | "message-history-length")[], message: string = "Update successful.") => {
|
||||
const reset = async (types: ("rags" | "tools" | "history" | "system_prompt" | "message_history_length")[], message: string = "Update successful.") => {
|
||||
try {
|
||||
const response = await fetch(connectionBase + `/api/reset/${sessionId}`, {
|
||||
method: 'PUT',
|
||||
@ -183,9 +183,9 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
||||
case "tools":
|
||||
setTools(value as Tool[]);
|
||||
break;
|
||||
case "system-prompt":
|
||||
setServerSystemPrompt((value as any)["system-prompt"].trim());
|
||||
setSystemPrompt((value as any)["system-prompt"].trim());
|
||||
case "system_prompt":
|
||||
setServerSystemPrompt((value as any)["system_prompt"].trim());
|
||||
setSystemPrompt((value as any)["system_prompt"].trim());
|
||||
break;
|
||||
case "history":
|
||||
console.log('TODO: handle history reset');
|
||||
@ -346,10 +346,10 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
const serverSystemPrompt = data["system-prompt"].trim();
|
||||
const serverSystemPrompt = data["system_prompt"].trim();
|
||||
setServerSystemPrompt(serverSystemPrompt);
|
||||
setSystemPrompt(serverSystemPrompt);
|
||||
setMessageHistoryLength(data["message-history-length"]);
|
||||
setMessageHistoryLength(data["message_history_length"]);
|
||||
}
|
||||
|
||||
fetchTunables();
|
||||
@ -402,7 +402,7 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
||||
/>
|
||||
<div style={{ display: "flex", flexDirection: "row", gap: "8px", paddingTop: "8px" }}>
|
||||
<Button variant="contained" disabled={editSystemPrompt === systemPrompt} onClick={() => { setSystemPrompt(editSystemPrompt); }}>Set</Button>
|
||||
<Button variant="outlined" onClick={() => { reset(["system-prompt"], "System prompt reset."); }} color="error">Reset</Button>
|
||||
<Button variant="outlined" onClick={() => { reset(["system_prompt"], "System prompt reset."); }} color="error">Reset</Button>
|
||||
</div>
|
||||
</AccordionActions>
|
||||
</Accordion>
|
||||
@ -481,7 +481,7 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
||||
</AccordionActions>
|
||||
</Accordion>
|
||||
<Button startIcon={<ResetIcon />} onClick={() => { reset(["history"], "History cleared."); }}>Clear Backstory History</Button>
|
||||
<Button onClick={() => { reset(["rags", "tools", "system-prompt", "message-history-length"], "Default settings restored.") }}>Reset system prompt, tunables, and RAG to defaults</Button>
|
||||
<Button onClick={() => { reset(["rags", "tools", "system_prompt", "message_history_length"], "Default settings restored.") }}>Reset system prompt, tunables, and RAG to defaults</Button>
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
@ -3,9 +3,11 @@ import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Box from '@mui/material/Box';
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
|
||||
import ResetIcon from '@mui/icons-material/RestartAlt';
|
||||
import { SxProps, Theme } from '@mui/material';
|
||||
import PropagateLoader from "react-spinners/PropagateLoader";
|
||||
|
||||
import { Message, MessageList, MessageData } from './Message';
|
||||
@ -14,24 +16,31 @@ import { ContextStatus } from './ContextStatus';
|
||||
|
||||
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 {
|
||||
submitQuery: (query: string) => void;
|
||||
}
|
||||
|
||||
interface ConversationProps {
|
||||
className?: string,
|
||||
type: ConversationMode
|
||||
prompt: string,
|
||||
actionLabel?: string,
|
||||
resetAction?: () => void,
|
||||
resetLabel?: string,
|
||||
connectionBase: string,
|
||||
sessionId?: string,
|
||||
setSnack: (message: string, severity: SeverityType) => void,
|
||||
defaultPrompts?: React.ReactElement[],
|
||||
preamble?: MessageList,
|
||||
hideDefaultPrompts?: boolean,
|
||||
messageFilter?: (messages: MessageList) => MessageList,
|
||||
messages?: MessageList,
|
||||
sx?: SxProps<Theme>,
|
||||
};
|
||||
|
||||
const Conversation = forwardRef<ConversationHandle, ConversationProps>(({ prompt, type, preamble, hideDefaultPrompts, defaultPrompts, sessionId, setSnack, connectionBase }: ConversationProps, ref) => {
|
||||
const Conversation = forwardRef<ConversationHandle, ConversationProps>(({ ...props }: ConversationProps, ref) => {
|
||||
const [query, setQuery] = useState<string>("");
|
||||
const [contextUsedPercentage, setContextUsedPercentage] = useState<number>(0);
|
||||
const [processing, setProcessing] = useState<boolean>(false);
|
||||
@ -43,12 +52,13 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({ prompt
|
||||
const [contextStatus, setContextStatus] = useState<ContextStatus>({ context_used: 0, max_context: 0 });
|
||||
const [contextWarningShown, setContextWarningShown] = useState<boolean>(false);
|
||||
const [noInteractions, setNoInteractions] = useState<boolean>(true);
|
||||
const setSnack = props.setSnack;
|
||||
|
||||
// Update the context status
|
||||
const updateContextStatus = useCallback(() => {
|
||||
const fetchContextStatus = async () => {
|
||||
try {
|
||||
const response = await fetch(connectionBase + `/api/context-status/${sessionId}`, {
|
||||
const response = await fetch(props.connectionBase + `/api/context-status/${props.sessionId}/${props.type}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -68,18 +78,18 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({ prompt
|
||||
}
|
||||
};
|
||||
fetchContextStatus();
|
||||
}, [setContextStatus, connectionBase, setSnack, sessionId]);
|
||||
}, [setContextStatus, props.connectionBase, setSnack, props.sessionId, props.type]);
|
||||
|
||||
// Set the initial chat history to "loading" or the welcome message if loaded.
|
||||
useEffect(() => {
|
||||
if (sessionId === undefined) {
|
||||
if (props.sessionId === undefined) {
|
||||
setConversation([loadingMessage]);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchHistory = async () => {
|
||||
try {
|
||||
const response = await fetch(connectionBase + `/api/history/${sessionId}`, {
|
||||
const response = await fetch(props.connectionBase + `/api/history/${props.sessionId}/${props.type}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -89,12 +99,18 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({ prompt
|
||||
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
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) {
|
||||
setConversation(preamble || []);
|
||||
setConversation([
|
||||
...(props.preamble || []),
|
||||
...(props.messages || []),
|
||||
]);
|
||||
setNoInteractions(true);
|
||||
} else {
|
||||
setConversation(data);
|
||||
setConversation([
|
||||
...(props.messages || []),
|
||||
...(props.messageFilter ? props.messageFilter(data) : data)
|
||||
]);
|
||||
setNoInteractions(false);
|
||||
}
|
||||
updateContextStatus();
|
||||
@ -103,10 +119,10 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({ prompt
|
||||
setSnack("Unable to obtain chat history.", "error");
|
||||
}
|
||||
};
|
||||
if (sessionId !== undefined) {
|
||||
if (props.sessionId !== undefined) {
|
||||
fetchHistory();
|
||||
}
|
||||
}, [sessionId, setConversation, updateContextStatus, connectionBase, setSnack, preamble]);
|
||||
}, [props.sessionId, setConversation, updateContextStatus, props.connectionBase, setSnack, props.preamble, props.type]);
|
||||
|
||||
const isScrolledToBottom = useCallback(()=> {
|
||||
// Current vertical scroll position
|
||||
@ -191,6 +207,40 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({ prompt
|
||||
setContextUsedPercentage(context_used_percentage)
|
||||
}, [contextStatus, setContextWarningShown, contextWarningShown, setContextUsedPercentage, setSnack]);
|
||||
|
||||
const reset = async () => {
|
||||
try {
|
||||
const response = await fetch(props.connectionBase + `/api/reset/${props.sessionId}/${props.type}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ reset: 'history' })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('Response body is null');
|
||||
}
|
||||
|
||||
props.messageFilter && props.messageFilter([]);
|
||||
|
||||
setConversation([
|
||||
...(props.preamble || []),
|
||||
...(props.messages || []),
|
||||
]);
|
||||
|
||||
setNoInteractions(true);
|
||||
|
||||
} catch (e) {
|
||||
setSnack("Error resetting history", "error")
|
||||
console.error('Error resetting history:', e);
|
||||
}
|
||||
};
|
||||
|
||||
const sendQuery = async (query: string) => {
|
||||
setNoInteractions(false);
|
||||
|
||||
@ -229,7 +279,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({ prompt
|
||||
}
|
||||
|
||||
// Make the fetch request with proper headers
|
||||
const response = await fetch(connectionBase + `/api/chat/${sessionId}`, {
|
||||
const response = await fetch(props.connectionBase + `/api/chat/${props.sessionId}/${props.type}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -373,8 +423,12 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({ prompt
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="Conversation" sx={{ display: "flex", flexDirection: "column", overflowY: "auto" }}>
|
||||
{conversation.map((message, index) => <Message key={index} {...{ submitQuery, message, connectionBase, sessionId, setSnack }} />)}
|
||||
<Box className={props.className || "Conversation"} sx={{ ...props.sx, display: "flex", flexDirection: "column" }}>
|
||||
{
|
||||
conversation.map((message, index) =>
|
||||
<Message key={index} {...{ submitQuery, message, connectionBase: props.connectionBase, sessionId: props.sessionId, setSnack }} />
|
||||
)
|
||||
}
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
@ -398,26 +452,45 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({ prompt
|
||||
>Estimated response time: {countdown}s</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
|
||||
variant="outlined"
|
||||
disabled={processing}
|
||||
fullWidth
|
||||
multiline={props.type === "job_description"}
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={handleKeyPress}
|
||||
placeholder={prompt}
|
||||
placeholder={props.prompt}
|
||||
id="QueryInput"
|
||||
/>
|
||||
<Tooltip title="Send">
|
||||
<Button sx={{ m: 1 }} variant="contained" onClick={() => { sendQuery(query); }}><SendIcon /></Button>
|
||||
<Box key="jobActions" sx={{ display: "flex", justifyContent: "center", flexDirection: "row" }}>
|
||||
<IconButton
|
||||
sx={{ display: "flex", margin: 'auto 0px' }}
|
||||
size="large"
|
||||
edge="start"
|
||||
color="inherit"
|
||||
onClick={() => { reset(); }}
|
||||
>
|
||||
<Tooltip title={props.resetLabel || "Reset"} >
|
||||
<ResetIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<Tooltip title={props.actionLabel || "Send"}>
|
||||
<Button
|
||||
sx={{ m: 1, gap: 1, flexGrow: 1 }}
|
||||
variant="contained"
|
||||
onClick={() => { sendQuery(query); }}>
|
||||
{props.actionLabel}<SendIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
{(noInteractions || !hideDefaultPrompts) && defaultPrompts !== undefined && defaultPrompts.length &&
|
||||
</Box>
|
||||
{(noInteractions || !props.hideDefaultPrompts) && props.defaultPrompts !== undefined && props.defaultPrompts.length &&
|
||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||
{
|
||||
defaultPrompts.map((element, index) => {
|
||||
props.defaultPrompts.map((element, index) => {
|
||||
return (<Box key={index}>{element}</Box>);
|
||||
})
|
||||
}
|
||||
|
@ -28,33 +28,21 @@ import { SxProps, Theme } from '@mui/material';
|
||||
|
||||
import MuiMarkdown from 'mui-markdown';
|
||||
|
||||
import { Message } from './Message';
|
||||
import { Message, ChatQuery } from './Message';
|
||||
import { Document } from './Document';
|
||||
import { MessageData } from './Message';
|
||||
import { MessageData, MessageList } from './Message';
|
||||
import { SeverityType } from './Snack';
|
||||
import { Conversation } from './Conversation';
|
||||
|
||||
/**
|
||||
* Props for the DocumentViewer component
|
||||
* @interface DocumentViewerProps
|
||||
* @property {function} generateResume - Function to generate a resume based on job description
|
||||
* @property {MessageData | undefined} resume - The generated resume data
|
||||
* @property {function} setResume - Function to set the generated resume
|
||||
* @property {function} factCheck - Function to fact check the generated resume
|
||||
* @property {MessageData | undefined} facts - The fact check results
|
||||
* @property {function} setFacts - Function to set the fact check results
|
||||
* @property {string} jobDescription - The initial job description
|
||||
* @property {function} setJobDescription - Function to set the job description
|
||||
* @property {SxProps<Theme>} [sx] - Optional styling properties
|
||||
* @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 {
|
||||
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>;
|
||||
connectionBase: string;
|
||||
sessionId: string;
|
||||
@ -67,19 +55,16 @@ export interface DocumentViewerProps {
|
||||
* with different layouts for mobile and desktop views.
|
||||
*/
|
||||
const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
||||
generateResume,
|
||||
jobDescription,
|
||||
factCheck,
|
||||
resume,
|
||||
setResume,
|
||||
facts,
|
||||
setFacts,
|
||||
sx,
|
||||
connectionBase,
|
||||
sessionId,
|
||||
setSnack
|
||||
}) => {
|
||||
// State for editing job description
|
||||
const [jobDescription, setJobDescription] = useState<string | undefined>(undefined);
|
||||
const [facts, setFacts] = useState<MessageData | undefined>(undefined);
|
||||
const [resume, setResume] = useState<MessageData | undefined>(undefined);
|
||||
|
||||
const [editJobDescription, setEditJobDescription] = useState<string | undefined>(jobDescription);
|
||||
// Processing state to show loading indicators
|
||||
const [processing, setProcessing] = useState<string | undefined>(undefined);
|
||||
@ -122,8 +107,8 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
||||
}
|
||||
setProcessing("resume");
|
||||
setTimeout(() => { setActiveTab(1); }, 250); // Switch to resume view on mobile
|
||||
generateResume(description);
|
||||
}, [generateResume, setProcessing, setActiveTab, setResume]);
|
||||
console.log('generateResume(description);');
|
||||
}, [/*generateResume*/, setProcessing, setActiveTab, setResume]);
|
||||
|
||||
/**
|
||||
* Trigger fact check and update UI state
|
||||
@ -137,9 +122,9 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
||||
return;
|
||||
}
|
||||
setProcessing("facts");
|
||||
factCheck(resume);
|
||||
console.log('factCheck(resume)');
|
||||
setTimeout(() => { setActiveTab(2); }, 250); // Switch to resume view on mobile
|
||||
}, [factCheck, setResume, setProcessing, setActiveTab, setFacts]);
|
||||
}, [/*factCheck,*/ setResume, setProcessing, setActiveTab, setFacts]);
|
||||
|
||||
useEffect(() => {
|
||||
setEditJobDescription(jobDescription);
|
||||
@ -192,62 +177,73 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
||||
triggerGeneration(editJobDescription || "");
|
||||
}
|
||||
};
|
||||
const handleJobQuery = (query: string) => {
|
||||
triggerGeneration(query);
|
||||
};
|
||||
|
||||
const renderJobDescriptionView = () => {
|
||||
const children = [];
|
||||
const jobDescriptionQuestions = [
|
||||
<Box sx={{ display: "flex", flexDirection: "row" }}>
|
||||
<ChatQuery text="What are the key skills necessary for this position?" submitQuery={handleJobQuery} />
|
||||
<ChatQuery text="How much should this position pay (accounting for inflation)?" submitQuery={handleJobQuery} />
|
||||
</Box>,
|
||||
];
|
||||
|
||||
if (resume === undefined && processing === undefined) {
|
||||
children.push(
|
||||
<Document key="jobDescription" sx={{ display: "flex", flexGrow: 1 }} title="">
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
multiline
|
||||
type="text"
|
||||
sx={{
|
||||
flex: 1,
|
||||
flexGrow: 1,
|
||||
maxHeight: '100%',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
value={editJobDescription}
|
||||
onChange={(e) => setEditJobDescription(e.target.value)}
|
||||
onKeyDown={handleKeyPress}
|
||||
placeholder="Paste a job description, then click Generate..."
|
||||
/>
|
||||
</Document>
|
||||
);
|
||||
} else {
|
||||
children.push(<MuiMarkdown key="jobDescription" >{editJobDescription}</MuiMarkdown>)
|
||||
const filterJobDescriptionMessages = (messages: MessageList): MessageList => {
|
||||
/* The second messages is the RESUME (the LLM response to the JOB-DESCRIPTION) */
|
||||
if (messages.length > 1) {
|
||||
setResume(messages[1]);
|
||||
} else if (resume !== undefined) {
|
||||
setResume(undefined);
|
||||
}
|
||||
|
||||
children.push(
|
||||
<Box key="jobActions" sx={{ display: "flex", justifyContent: "center", flexDirection: "row" }}>
|
||||
<IconButton
|
||||
sx={{ display: "flex", margin: 'auto 0px' }}
|
||||
size="large"
|
||||
edge="start"
|
||||
color="inherit"
|
||||
disabled={processing !== undefined}
|
||||
onClick={() => { setEditJobDescription(""); triggerGeneration(undefined); }}
|
||||
>
|
||||
<Tooltip title="Reset Job Description">
|
||||
<ResetIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<Tooltip title="Generate">
|
||||
<Button
|
||||
sx={{ m: 1, gap: 1, flexGrow: 1 }}
|
||||
variant="contained"
|
||||
onClick={() => { triggerGeneration(editJobDescription); }}
|
||||
>
|
||||
Generate<SendIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
);
|
||||
/* Filter out the RESUME */
|
||||
const reduced = messages.filter((message, index) => index != 1);
|
||||
|
||||
return children;
|
||||
/* Set the first message as coming from the assistant (rendered as markdown) */
|
||||
if (reduced.length > 0) {
|
||||
reduced[0].role = 'assistant';
|
||||
}
|
||||
return reduced;
|
||||
};
|
||||
|
||||
const jobDescriptionMessages: MessageList = [];
|
||||
|
||||
const renderJobDescriptionView = () => {
|
||||
if (resume === undefined) {
|
||||
return <Conversation
|
||||
{...{
|
||||
sx: { display: "flex", flexGrow: 1 },
|
||||
actionLabel: "Generate Resume",
|
||||
multiline: true,
|
||||
type: "job_description",
|
||||
prompt: "Paste a job description, then click Generate...",
|
||||
messageFilter: filterJobDescriptionMessages,
|
||||
messages: jobDescriptionMessages,
|
||||
sessionId,
|
||||
connectionBase,
|
||||
setSnack,
|
||||
defaultPrompts: jobDescriptionQuestions
|
||||
}}
|
||||
/>
|
||||
|
||||
} else {
|
||||
return <Conversation
|
||||
{...{
|
||||
className: "ChatBox",
|
||||
sx: { display: "flex", flexGrow: 1 },
|
||||
type: "job_description",
|
||||
actionLabel: "Send",
|
||||
prompt: "Ask a question about this job description...",
|
||||
messageFilter: filterJobDescriptionMessages,
|
||||
messages: jobDescriptionMessages,
|
||||
sessionId,
|
||||
connectionBase,
|
||||
setSnack,
|
||||
defaultPrompts: jobDescriptionQuestions
|
||||
}}
|
||||
/>
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -363,7 +359,7 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
||||
const otherRatio = showResume ? (100 - splitRatio / 2) : 100;
|
||||
const children = [];
|
||||
children.push(
|
||||
<Box key="JobDescription" sx={{ display: 'flex', flexDirection: 'column', width: `${otherRatio}%`, p: 0, flexGrow: 1, overflow: 'hidden' }}>
|
||||
<Box key="JobDescription" className="ChatBox" sx={{ display: 'flex', flexDirection: 'column', width: `${otherRatio}%`, p: 0, flexGrow: 1, overflowY: 'auto' }}>
|
||||
{renderJobDescriptionView()}
|
||||
</Box>);
|
||||
|
||||
@ -418,7 +414,7 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexGrow: 1, flexDirection: 'column', overflow: 'hidden', p: 0 }}>
|
||||
<Box sx={{ ...sx, display: 'flex', flexGrow: 1, flexDirection: 'column', p: 0 }}>
|
||||
<Box sx={{ display: 'flex', flexGrow: 1, flexDirection: 'row', overflow: 'hidden', p: 0 }}>
|
||||
{children}
|
||||
</Box>
|
||||
@ -428,7 +424,7 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1, ...sx }}>
|
||||
<Box sx={{ ...sx, display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
|
||||
{getActiveDesktopContent()}
|
||||
</Box>
|
||||
);
|
||||
|
@ -233,7 +233,7 @@ const Message = ({ message, submitQuery, isFullWidth, sessionId, setSnack, conne
|
||||
const formattedContent = message.content.trim();
|
||||
|
||||
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" }}>
|
||||
<Tooltip title="Copy to clipboard" placement="top" arrow>
|
||||
<IconButton
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { SeverityType } from './Snack';
|
||||
import { ContextStatus } from './ContextStatus';
|
||||
import { MessageData, MessageMetaProps } from './Message';
|
||||
import { DocumentViewer } from './DocumentViewer';
|
||||
|
||||
@ -25,320 +24,11 @@ type Resume = {
|
||||
};
|
||||
|
||||
const ResumeBuilder = ({ facts, setFacts, resume, setResume, setProcessing, processing, connectionBase, sessionId, setSnack }: ResumeBuilderProps) => {
|
||||
const [lastEvalTPS, setLastEvalTPS] = useState<number>(35);
|
||||
const [lastPromptTPS, setLastPromptTPS] = useState<number>(430);
|
||||
const [contextStatus, setContextStatus] = useState<ContextStatus>({ context_used: 0, max_context: 0 });
|
||||
const [jobDescription, setJobDescription] = useState<string | undefined>(undefined);
|
||||
|
||||
const updateContextStatus = useCallback(() => {
|
||||
fetch(connectionBase + `/api/context-status/${sessionId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
setContextStatus(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error getting context status:', error);
|
||||
setSnack("Unable to obtain context status.", "error");
|
||||
});
|
||||
}, [setContextStatus, connectionBase, setSnack, sessionId]);
|
||||
|
||||
// If the jobDescription and resume have not been set, fetch them from the server
|
||||
useEffect(() => {
|
||||
if (sessionId === undefined) {
|
||||
return;
|
||||
}
|
||||
if (jobDescription !== undefined) {
|
||||
return;
|
||||
}
|
||||
const fetchResume = async () => {
|
||||
try {
|
||||
// Make the fetch request with proper headers
|
||||
const response = await fetch(connectionBase + `/api/resume/${sessionId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw Error();
|
||||
}
|
||||
const data: Resume[] = await response.json();
|
||||
if (data.length) {
|
||||
const lastResume = data[data.length - 1];
|
||||
console.log(lastResume);
|
||||
setJobDescription(lastResume['job_description']);
|
||||
setResume(lastResume.resume);
|
||||
if (lastResume['fact_check'] !== undefined && lastResume['fact_check'] !== null) {
|
||||
lastResume['fact_check'].role = 'info';
|
||||
setFacts(lastResume['fact_check'])
|
||||
} else {
|
||||
setFacts(undefined)
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
setSnack("Unable to fetch resume", "error");
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
fetchResume();
|
||||
}, [sessionId, resume, jobDescription, setResume, setJobDescription, setSnack, setFacts, connectionBase]);
|
||||
|
||||
// const startCountdown = (seconds: number) => {
|
||||
// if (timerRef.current) clearInterval(timerRef.current);
|
||||
// setCountdown(seconds);
|
||||
// timerRef.current = setInterval(() => {
|
||||
// setCountdown((prev) => {
|
||||
// if (prev <= 1) {
|
||||
// clearInterval(timerRef.current);
|
||||
// timerRef.current = null;
|
||||
// if (isScrolledToBottom()) {
|
||||
// setTimeout(() => {
|
||||
// scrollToBottom();
|
||||
// }, 50)
|
||||
// }
|
||||
// return 0;
|
||||
// }
|
||||
// return prev - 1;
|
||||
// });
|
||||
// }, 1000);
|
||||
// };
|
||||
|
||||
// const stopCountdown = () => {
|
||||
// if (timerRef.current) {
|
||||
// clearInterval(timerRef.current);
|
||||
// timerRef.current = null;
|
||||
// setCountdown(0);
|
||||
// }
|
||||
// };
|
||||
|
||||
if (sessionId === undefined) {
|
||||
return (<></>);
|
||||
}
|
||||
|
||||
const generateResume = async (description: string) => {
|
||||
if (!description.trim()) return;
|
||||
setResume(undefined);
|
||||
setFacts(undefined);
|
||||
|
||||
try {
|
||||
setProcessing(true);
|
||||
|
||||
// Add initial processing message
|
||||
//setGenerateStatus({ role: 'assistant', content: 'Processing request...' });
|
||||
|
||||
// Make the fetch request with proper headers
|
||||
const response = await fetch(connectionBase + `/api/generate-resume/${sessionId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ content: description.trim() }),
|
||||
});
|
||||
|
||||
// We'll guess that the response will be around 500 tokens...
|
||||
const token_guess = 500;
|
||||
const estimate = Math.round(token_guess / lastEvalTPS + contextStatus.context_used / lastPromptTPS);
|
||||
|
||||
setSnack(`Job description sent. Response estimated in ${estimate}s.`, "info");
|
||||
//startCountdown(Math.round(estimate));
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('Response body is null');
|
||||
}
|
||||
|
||||
// Set up stream processing with explicit chunking
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
|
||||
// Process each complete line immediately
|
||||
buffer += chunk;
|
||||
let lines = buffer.split('\n');
|
||||
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
|
||||
try {
|
||||
const update = JSON.parse(line);
|
||||
|
||||
// Force an immediate state update based on the message type
|
||||
if (update.status === 'processing') {
|
||||
// Update processing message with immediate re-render
|
||||
//setGenerateStatus({ role: 'info', content: update.message });
|
||||
console.log(update.num_ctx);
|
||||
|
||||
// Add a small delay to ensure React has time to update the UI
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
} else if (update.status === 'done') {
|
||||
// Replace processing message with final result
|
||||
//setGenerateStatus(undefined);
|
||||
setResume(update.message);
|
||||
const metadata = update.message.metadata;
|
||||
const evalTPS = metadata.eval_count * 10 ** 9 / metadata.eval_duration;
|
||||
const promptTPS = metadata.prompt_eval_count * 10 ** 9 / metadata.prompt_eval_duration;
|
||||
setLastEvalTPS(evalTPS ? evalTPS : 35);
|
||||
setLastPromptTPS(promptTPS ? promptTPS : 35);
|
||||
updateContextStatus();
|
||||
} else if (update.status === 'error') {
|
||||
// Show error
|
||||
//setGenerateStatus({ role: 'error', content: update.message });
|
||||
}
|
||||
} catch (e) {
|
||||
setSnack("Error generating resume", "error")
|
||||
console.error('Error parsing JSON:', e, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process any remaining buffer content
|
||||
if (buffer.trim()) {
|
||||
try {
|
||||
const update = JSON.parse(buffer);
|
||||
|
||||
if (update.status === 'done') {
|
||||
//setGenerateStatus(undefined);
|
||||
setResume(update.message);
|
||||
}
|
||||
} catch (e) {
|
||||
setSnack("Error processing job description", "error")
|
||||
}
|
||||
}
|
||||
|
||||
//stopCountdown();
|
||||
setProcessing(false);
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
setSnack("Unable to process job description", "error");
|
||||
//setGenerateStatus({ role: 'error', content: `Error: ${error}` });
|
||||
setProcessing(false);
|
||||
//stopCountdown();
|
||||
}
|
||||
};
|
||||
|
||||
const factCheck = async (resume: string) => {
|
||||
if (!resume.trim()) return;
|
||||
setFacts(undefined);
|
||||
|
||||
try {
|
||||
setProcessing(true);
|
||||
|
||||
const response = await fetch(connectionBase + `/api/fact-check/${sessionId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ content: resume.trim() }),
|
||||
});
|
||||
|
||||
// We'll guess that the response will be around 500 tokens...
|
||||
const token_guess = 500;
|
||||
const estimate = Math.round(token_guess / lastEvalTPS + contextStatus.context_used / lastPromptTPS);
|
||||
|
||||
setSnack(`Resume sent for Fact Check. Response estimated in ${estimate}s.`, "info");
|
||||
//startCountdown(Math.round(estimate));
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('Response body is null');
|
||||
}
|
||||
|
||||
// Set up stream processing with explicit chunking
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
|
||||
// Process each complete line immediately
|
||||
buffer += chunk;
|
||||
let lines = buffer.split('\n');
|
||||
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
|
||||
try {
|
||||
const update = JSON.parse(line);
|
||||
|
||||
// Force an immediate state update based on the message type
|
||||
if (update.status === 'processing') {
|
||||
// Add a small delay to ensure React has time to update the UI
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
} else if (update.status === 'done') {
|
||||
// Replace processing message with final result
|
||||
update.message.role = 'info';
|
||||
setFacts(update.message);
|
||||
const metadata = update.message.metadata;
|
||||
const evalTPS = metadata.eval_count * 10 ** 9 / metadata.eval_duration;
|
||||
const promptTPS = metadata.prompt_eval_count * 10 ** 9 / metadata.prompt_eval_duration;
|
||||
setLastEvalTPS(evalTPS ? evalTPS : 35);
|
||||
setLastPromptTPS(promptTPS ? promptTPS : 35);
|
||||
updateContextStatus();
|
||||
} else if (update.status === 'error') {
|
||||
}
|
||||
} catch (e) {
|
||||
setSnack("Error generating resume", "error")
|
||||
console.error('Error parsing JSON:', e, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process any remaining buffer content
|
||||
if (buffer.trim()) {
|
||||
try {
|
||||
const update = JSON.parse(buffer);
|
||||
|
||||
if (update.status === 'done') {
|
||||
update.message.role = 'info';
|
||||
setFacts(update.message);
|
||||
}
|
||||
} catch (e) {
|
||||
setSnack("Error processing resume", "error")
|
||||
}
|
||||
}
|
||||
|
||||
//stopCountdown();
|
||||
setProcessing(false);
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
setSnack("Unable to process resume", "error");
|
||||
//setGenerateStatus({ role: 'error', content: `Error: ${error}` });
|
||||
setProcessing(false);
|
||||
//stopCountdown();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="DocBox">
|
||||
<Box className="Conversation" sx={{ p: 0, pt: 1 }}>
|
||||
@ -350,7 +40,7 @@ const ResumeBuilder = ({ facts, setFacts, resume, setResume, setProcessing, proc
|
||||
overflowY: "auto",
|
||||
flexDirection: "column",
|
||||
height: "calc(0vh - 0px)", // Hack to make the height work
|
||||
}} {...{ factCheck, facts, jobDescription, generateResume, resume, setFacts, setResume, setSnack, setJobDescription, connectionBase, sessionId }} />
|
||||
}} {...{ setSnack, connectionBase, sessionId }} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
632
src/server.py
632
src/server.py
@ -11,6 +11,7 @@ import uuid
|
||||
import subprocess
|
||||
import re
|
||||
import math
|
||||
import copy
|
||||
|
||||
def try_import(module_name, pip_name=None):
|
||||
try:
|
||||
@ -52,6 +53,8 @@ from tools import (
|
||||
tools
|
||||
)
|
||||
|
||||
CONTEXT_VERSION=2
|
||||
|
||||
rags = [
|
||||
{ "name": "JPK", "enabled": True, "description": "Expert data about James Ketrenos, including work history, personal hobbies, and projects." },
|
||||
# { "name": "LKML", "enabled": False, "description": "Full associative data for entire LKML mailing list archive." },
|
||||
@ -164,6 +167,8 @@ Always use tools and [{context_tag}] when possible. Be concise, and never make u
|
||||
""".strip()
|
||||
|
||||
system_generate_resume = f"""
|
||||
Launched on {DateTime()}.
|
||||
|
||||
You are a professional resume writer. Your task is to write a polished, tailored resume for a specific job based only on the individual's [WORK HISTORY].
|
||||
|
||||
When answering queries, follow these steps:
|
||||
@ -188,9 +193,11 @@ Structure the resume professionally with the following sections where applicable
|
||||
|
||||
Do not include any information unless it is provided in [WORK HISTORY] or [INTRO].
|
||||
Ensure the langauge is clear, concise, and aligned with industry standards for professional resumes.
|
||||
"""
|
||||
""".strip()
|
||||
|
||||
system_fact_check = f"""
|
||||
Launched on {DateTime()}.
|
||||
|
||||
You are a professional resume fact checker. Your task is to identify any inaccuracies in the [RESUME] based on the individual's [WORK HISTORY].
|
||||
|
||||
If there are inaccuracies, list them in a bullet point format.
|
||||
@ -198,7 +205,20 @@ If there are inaccuracies, list them in a bullet point format.
|
||||
When answering queries, follow these steps:
|
||||
1. You must not invent or assume any information not explicitly present in the [WORK HISTORY].
|
||||
2. Analyze the [RESUME] to identify any discrepancies or inaccuracies based on the [WORK HISTORY].
|
||||
"""
|
||||
""".strip()
|
||||
|
||||
system_job_description = f"""
|
||||
Launched on {DateTime()}.
|
||||
|
||||
You are a hiring and job placing specialist. Your task is to answers about a job description.
|
||||
|
||||
When answering queries, follow these steps:
|
||||
1. Analyze the [JOB DESCRIPTION] to provide insights for the asked question.
|
||||
2. If any financial information is requested, be sure to account for inflation.
|
||||
""".strip()
|
||||
|
||||
def create_system_message(prompt):
|
||||
return [{"role": "system", "content": prompt}]
|
||||
|
||||
tool_log = []
|
||||
command_log = []
|
||||
@ -374,6 +394,9 @@ async def handle_tool_calls(message):
|
||||
final_result = all_responses[0] if len(all_responses) == 1 else all_responses
|
||||
yield (final_result, tools_used)
|
||||
|
||||
|
||||
|
||||
|
||||
# %%
|
||||
class WebServer:
|
||||
def __init__(self, logging, client, model=MODEL_NAME):
|
||||
@ -431,71 +454,6 @@ class WebServer:
|
||||
return RedirectResponse(url=f"/{context['id']}", status_code=307)
|
||||
#return JSONResponse({"redirect": f"/{context['id']}"})
|
||||
|
||||
@self.app.get("/api/query")
|
||||
async def query_documents(query: str, top_k: int = 3):
|
||||
if not self.file_watcher:
|
||||
return
|
||||
|
||||
"""Query the RAG system with the given prompt."""
|
||||
results = self.file_watcher.find_similar(query, top_k=top_k)
|
||||
return {
|
||||
"query": query,
|
||||
"results": [
|
||||
{
|
||||
"content": doc,
|
||||
"metadata": meta,
|
||||
"distance": dist
|
||||
}
|
||||
for doc, meta, dist in zip(
|
||||
results["documents"],
|
||||
results["metadatas"],
|
||||
results["distances"]
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
@self.app.post("/api/refresh/{file_path:path}")
|
||||
async def refresh_document(file_path: str, background_tasks: BackgroundTasks):
|
||||
if not self.file_watcher:
|
||||
return
|
||||
|
||||
"""Manually refresh a specific document in the collection."""
|
||||
full_path = os.path.join(defines.doc_dir, file_path)
|
||||
|
||||
if not os.path.exists(full_path):
|
||||
return {"status": "error", "message": "File not found"}
|
||||
|
||||
# Schedule the update in the background
|
||||
background_tasks.add_task(
|
||||
self.file_watcher.process_file_update, full_path
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Document refresh scheduled for {file_path}"
|
||||
}
|
||||
|
||||
# @self.app.post("/api/refresh-all")
|
||||
# async def refresh_all_documents():
|
||||
# if not self.file_watcher:
|
||||
# return
|
||||
|
||||
# """Refresh all documents in the collection."""
|
||||
# # Re-initialize file hashes and process all files
|
||||
# self.file_watcher._initialize_file_hashes()
|
||||
|
||||
# # Schedule updates for all files
|
||||
# file_paths = self.file_watcher.file_hashes.keys()
|
||||
# tasks = [self.file_watcher.process_file_update(path) for path in file_paths]
|
||||
|
||||
# # Wait for all updates to complete
|
||||
# await asyncio.gather(*tasks)
|
||||
|
||||
# return {
|
||||
# "status": "success",
|
||||
# "message": f"Refreshed {len(file_paths)} documents",
|
||||
# "document_count": file_watcher.collection.count()
|
||||
# }
|
||||
|
||||
@self.app.put("/api/umap/{context_id}")
|
||||
async def put_umap(context_id: str, request: Request):
|
||||
@ -566,20 +524,23 @@ class WebServer:
|
||||
logging.error(e)
|
||||
#return JSONResponse({"error": str(e)}, 500)
|
||||
|
||||
@self.app.put("/api/reset/{context_id}")
|
||||
async def put_reset(context_id: str, request: Request):
|
||||
@self.app.put("/api/reset/{context_id}/{type}")
|
||||
async def put_reset(context_id: str, type: str, request: Request):
|
||||
if not is_valid_uuid(context_id):
|
||||
logging.warning(f"Invalid context_id: {context_id}")
|
||||
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
|
||||
context = self.upsert_context(context_id)
|
||||
if type not in context["sessions"]:
|
||||
return JSONResponse({ "error": f"{type} is not recognized", "context": context }, status_code=404)
|
||||
|
||||
data = await request.json()
|
||||
try:
|
||||
response = {}
|
||||
for reset in data["reset"]:
|
||||
match reset:
|
||||
case "system-prompt":
|
||||
context["system"] = [{"role": "system", "content": system_message}]
|
||||
response["system-prompt"] = { "system-prompt": system_message }
|
||||
case "system_prompt":
|
||||
context["sessions"][type]["system_prompt"] = system_message
|
||||
response["system_prompt"] = { "system_prompt": system_message }
|
||||
case "rags":
|
||||
context["rags"] = rags.copy()
|
||||
response["rags"] = context["rags"]
|
||||
@ -587,23 +548,23 @@ class WebServer:
|
||||
context["tools"] = default_tools(tools)
|
||||
response["tools"] = context["tools"]
|
||||
case "history":
|
||||
context["llm_history"] = []
|
||||
context["user_history"] = []
|
||||
context["sessions"][type]["llm_history"] = []
|
||||
context["sessions"][type]["user_history"] = []
|
||||
context["sessions"][type]["context_tokens"] = round(len(str(context["system"])) * 3 / 4) # Estimate context usage
|
||||
response["history"] = []
|
||||
context["context_tokens"] = round(len(str(context["system"])) * 3 / 4) # Estimate context usage
|
||||
response["context_used"] = context["context_tokens"]
|
||||
case "message-history-length":
|
||||
response["context_used"] = context["sessions"][type]["context_tokens"]
|
||||
case "message_history_length":
|
||||
context["message_history_length"] = DEFAULT_HISTORY_LENGTH
|
||||
response["message-history-length"] = DEFAULT_HISTORY_LENGTH
|
||||
response["message_history_length"] = DEFAULT_HISTORY_LENGTH
|
||||
|
||||
if not response:
|
||||
return JSONResponse({ "error": "Usage: { reset: rags|tools|history|system-prompt}"})
|
||||
return JSONResponse({ "error": "Usage: { reset: rags|tools|history|system_prompt}"})
|
||||
else:
|
||||
self.save_context(context_id)
|
||||
return JSONResponse(response)
|
||||
|
||||
except:
|
||||
return JSONResponse({ "error": "Usage: { reset: rags|tools|history|system-prompt}"})
|
||||
return JSONResponse({ "error": "Usage: { reset: rags|tools|history|system_prompt}"})
|
||||
|
||||
@self.app.put("/api/tunables/{context_id}")
|
||||
async def put_tunables(context_id: str, request: Request):
|
||||
@ -614,20 +575,20 @@ class WebServer:
|
||||
data = await request.json()
|
||||
for k in data.keys():
|
||||
match k:
|
||||
case "system-prompt":
|
||||
case "system_prompt":
|
||||
system_prompt = data[k].strip()
|
||||
if not system_prompt:
|
||||
return JSONResponse({ "status": "error", "message": "System prompt can not be empty." })
|
||||
context["system"] = [{"role": "system", "content": system_prompt}]
|
||||
self.save_context(context_id)
|
||||
return JSONResponse({ "system-prompt": system_prompt })
|
||||
case "message-history-length":
|
||||
return JSONResponse({ "system_prompt": system_prompt })
|
||||
case "message_history_length":
|
||||
value = max(0, int(data[k]))
|
||||
context["message_history_length"] = value
|
||||
self.save_context(context_id)
|
||||
return JSONResponse({ "message-history-length": value })
|
||||
return JSONResponse({ "message_history_length": value })
|
||||
case _:
|
||||
return JSONResponse({ "error": f"Unrecognized tunable {k}"}, 404)
|
||||
return JSONResponse({ "error": f"Unrecognized tunable {k}"}, status_code=404)
|
||||
|
||||
@self.app.get("/api/tunables/{context_id}")
|
||||
async def get_tunables(context_id: str):
|
||||
@ -636,33 +597,29 @@ class WebServer:
|
||||
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
|
||||
context = self.upsert_context(context_id)
|
||||
return JSONResponse({
|
||||
"system-prompt": context["system"][0]["content"],
|
||||
"message-history-length": context["message_history_length"]
|
||||
"system_prompt": context["system"][0]["content"],
|
||||
"message_history_length": context["message_history_length"]
|
||||
})
|
||||
|
||||
@self.app.get("/api/resume/{context_id}")
|
||||
async def get_resume(context_id: str):
|
||||
if not is_valid_uuid(context_id):
|
||||
logging.warning(f"Invalid context_id: {context_id}")
|
||||
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
|
||||
context = self.upsert_context(context_id)
|
||||
return JSONResponse(context["resume_history"])
|
||||
|
||||
@self.app.get("/api/system-info/{context_id}")
|
||||
async def get_system_info(context_id: str):
|
||||
return JSONResponse(system_info(self.model))
|
||||
|
||||
@self.app.post("/api/chat/{context_id}")
|
||||
async def chat_endpoint(context_id: str, request: Request):
|
||||
@self.app.post("/api/chat/{context_id}/{type}")
|
||||
async def chat_endpoint(context_id: str, type: str, request: Request):
|
||||
if not is_valid_uuid(context_id):
|
||||
logging.warning(f"Invalid context_id: {context_id}")
|
||||
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
|
||||
context = self.upsert_context(context_id)
|
||||
|
||||
if type not in context["sessions"]:
|
||||
return JSONResponse({ "error": f"{type} is not recognized", "context": context }, status_code=404)
|
||||
|
||||
data = await request.json()
|
||||
|
||||
# Create a custom generator that ensures flushing
|
||||
async def flush_generator():
|
||||
async for message in self.chat(context=context, content=data["content"]):
|
||||
async for message in self.chat(context=context, type=type, content=data["content"]):
|
||||
# Convert to JSON and add newline
|
||||
yield json.dumps(message) + "\n"
|
||||
# Save the history as its generated
|
||||
@ -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")
|
||||
async def create_context():
|
||||
context = self.create_context()
|
||||
self.logging.info(f"Generated new session as {context['id']}")
|
||||
return JSONResponse(context)
|
||||
|
||||
@self.app.get("/api/history/{context_id}")
|
||||
async def get_history(context_id: str):
|
||||
@self.app.get("/api/history/{context_id}/{type}")
|
||||
async def get_history(context_id: str, type: str):
|
||||
context = self.upsert_context(context_id)
|
||||
return JSONResponse(context["user_history"])
|
||||
if type not in context["sessions"]:
|
||||
return JSONResponse({ "error": f"{type} is not recognized", "context": context }, status_code=404)
|
||||
return JSONResponse(context["sessions"][type]["user_history"])
|
||||
|
||||
@self.app.get("/api/tools/{context_id}")
|
||||
async def get_tools(context_id: str):
|
||||
@ -770,7 +671,7 @@ class WebServer:
|
||||
tool["enabled"] = enabled
|
||||
self.save_context(context_id)
|
||||
return JSONResponse(context["tools"])
|
||||
return JSONResponse({ "status": f"{modify} not found in tools." }), 404
|
||||
return JSONResponse({ "status": f"{modify} not found in tools." }, status_code=404)
|
||||
except:
|
||||
return JSONResponse({ "status": "error" }), 405
|
||||
|
||||
@ -794,17 +695,19 @@ class WebServer:
|
||||
tool["enabled"] = enabled
|
||||
self.save_context(context_id)
|
||||
return JSONResponse(context["rags"])
|
||||
return JSONResponse({ "status": f"{modify} not found in tools." }), 404
|
||||
return JSONResponse({ "status": f"{modify} not found in tools." }, status_code=404)
|
||||
except:
|
||||
return JSONResponse({ "status": "error" }), 405
|
||||
|
||||
@self.app.get("/api/context-status/{context_id}")
|
||||
async def get_context_status(context_id):
|
||||
@self.app.get("/api/context-status/{context_id}/{type}")
|
||||
async def get_context_status(context_id, type: str):
|
||||
if not is_valid_uuid(context_id):
|
||||
logging.warning(f"Invalid context_id: {context_id}")
|
||||
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
|
||||
context = self.upsert_context(context_id)
|
||||
return JSONResponse({"context_used": context["context_tokens"], "max_context": defines.max_context})
|
||||
if type not in context["sessions"]:
|
||||
return JSONResponse({ "error": f"{type} is not recognized", "context": context }, status_code=404)
|
||||
return JSONResponse({"context_used": context["sessions"][type]["context_tokens"], "max_context": defines.max_context})
|
||||
|
||||
@self.app.get("/api/health")
|
||||
async def health_check():
|
||||
@ -839,15 +742,80 @@ class WebServer:
|
||||
# Create the full file path
|
||||
file_path = os.path.join(defines.session_dir, session_id)
|
||||
|
||||
umap_model = context.get("umap_model")
|
||||
if umap_model:
|
||||
del context["umap_model"]
|
||||
# Serialize the data to JSON and write to file
|
||||
with open(file_path, "w") as f:
|
||||
json.dump(context, f)
|
||||
|
||||
return session_id
|
||||
|
||||
|
||||
def migrate_context(self, context):
|
||||
# No version
|
||||
# context = {
|
||||
# "id": context_id,
|
||||
# "tools": default_tools(tools),
|
||||
# "rags": rags.copy(),
|
||||
# "context_tokens": round(len(str(system_context)) * 3 / 4), # Estimate context usage
|
||||
# "message_history_length": 5, # Number of messages to supply in context
|
||||
# "system": system_context,
|
||||
# "system_generate_resume": system_generate_resume,
|
||||
# "llm_history": [],
|
||||
# "user_history": [],
|
||||
# "resume_history": [],
|
||||
# }
|
||||
# Version 2:
|
||||
# context = {
|
||||
# "version": 2,
|
||||
# "id": context_id,
|
||||
# "sessions": {
|
||||
# **TYPE**: { # chat, job-description, resume, fact-check
|
||||
# "system_prompt": **SYSTEM_MESSAGE**,
|
||||
# "llm_history": [],
|
||||
# "user_history": [],
|
||||
# "context_tokens": round(len(str(**SYSTEM_MESSAGE**)) * 3 / 4),
|
||||
# }
|
||||
# },
|
||||
# "tools": default_tools(tools),
|
||||
# "rags": rags.copy(),
|
||||
# "message_history_length": 5 # Number of messages to supply in context
|
||||
# }
|
||||
if "version" not in context:
|
||||
logging.info(f"Migrating {context['id']}")
|
||||
context["version"] = CONTEXT_VERSION
|
||||
context["sessions"] = {
|
||||
"chat": {
|
||||
"system_prompt": system_message,
|
||||
"llm_history": context["llm_history"],
|
||||
"user_history": context["user_history"],
|
||||
"context_tokens": round(len(str(create_system_message(system_message))))
|
||||
},
|
||||
"job_description": {
|
||||
"system_prompt": system_job_description,
|
||||
"llm_history": [],
|
||||
"user_history": [],
|
||||
"context_tokens": round(len(str(create_system_message(system_job_description))))
|
||||
},
|
||||
"resume": {
|
||||
"system_prompt": system_generate_resume,
|
||||
"llm_history": [],
|
||||
"user_history": [],
|
||||
"context_tokens": round(len(str(create_system_message(system_generate_resume))))
|
||||
},
|
||||
"fact_check": {
|
||||
"system_prompt": system_fact_check,
|
||||
"llm_history": [],
|
||||
"user_history": [],
|
||||
"context_tokens": round(len(str(create_system_message(system_fact_check))))
|
||||
},
|
||||
}
|
||||
del context["system"]
|
||||
del context["system_generate_resume"]
|
||||
del context["llm_history"]
|
||||
del context["user_history"]
|
||||
del context["resume_history"]
|
||||
|
||||
return context
|
||||
|
||||
def load_context(self, session_id):
|
||||
"""
|
||||
Load a serialized Python dictionary from a file in the sessions directory.
|
||||
@ -868,22 +836,42 @@ class WebServer:
|
||||
with open(file_path, "r") as f:
|
||||
self.contexts[session_id] = json.load(f)
|
||||
|
||||
return self.contexts[session_id]
|
||||
return self.migrate_context(self.contexts[session_id])
|
||||
|
||||
def create_context(self, context_id = None):
|
||||
if not context_id:
|
||||
context_id = str(uuid.uuid4())
|
||||
system_context = [{"role": "system", "content": system_message}];
|
||||
context = {
|
||||
"id": context_id,
|
||||
"system": system_context,
|
||||
"system_generate_resume": system_generate_resume,
|
||||
"version": CONTEXT_VERSION,
|
||||
"sessions": {
|
||||
"chat": {
|
||||
"system_prompt": system_message,
|
||||
"llm_history": [],
|
||||
"user_history": [],
|
||||
"context_tokens": round(len(str(system_message)) * 3 / 4), # Estimate context usage
|
||||
},
|
||||
"job_description": {
|
||||
"system_prompt": system_job_description,
|
||||
"llm_history": [],
|
||||
"user_history": [],
|
||||
"context_tokens": round(len(str(system_job_description)) * 3 / 4), # Estimate context usage
|
||||
},
|
||||
"resume": {
|
||||
"system_prompt": system_generate_resume,
|
||||
"llm_history": [],
|
||||
"user_history": [],
|
||||
"context_tokens": round(len(str(system_generate_resume)) * 3 / 4), # Estimate context usage
|
||||
},
|
||||
"fact_check": {
|
||||
"system_prompt": system_fact_check,
|
||||
"llm_history": [],
|
||||
"user_history": [],
|
||||
"context_tokens": round(len(str(system_fact_check)) * 3 / 4), # Estimate context usage
|
||||
},
|
||||
},
|
||||
"tools": default_tools(tools),
|
||||
"resume_history": [],
|
||||
"rags": rags.copy(),
|
||||
"context_tokens": round(len(str(system_context)) * 3 / 4), # Estimate context usage
|
||||
"message_history_length": 5 # Number of messages to supply in context
|
||||
}
|
||||
logging.info(f"{context_id} created and added to sessions.")
|
||||
@ -903,7 +891,7 @@ class WebServer:
|
||||
logging.info(f"Context {context_id} not found. Creating new context.")
|
||||
return self.load_context(context_id)
|
||||
|
||||
async def chat(self, context, content):
|
||||
async def chat(self, context, type, content):
|
||||
if not self.file_watcher:
|
||||
return
|
||||
|
||||
@ -918,23 +906,41 @@ class WebServer:
|
||||
|
||||
self.processing = True
|
||||
|
||||
llm_history = context["llm_history"]
|
||||
user_history = context["user_history"]
|
||||
try:
|
||||
llm_history = context["sessions"][type]["llm_history"]
|
||||
user_history = context["sessions"][type]["user_history"]
|
||||
metadata = {
|
||||
"rag": {},
|
||||
"type": type,
|
||||
"rag": { "documents": [] },
|
||||
"tools": [],
|
||||
"eval_count": 0,
|
||||
"eval_duration": 0,
|
||||
"prompt_eval_count": 0,
|
||||
"prompt_eval_duration": 0,
|
||||
}
|
||||
rag_docs = []
|
||||
|
||||
# Default to not using tools
|
||||
enable_tools = False
|
||||
|
||||
# Default eo using RAG
|
||||
enable_rag = True
|
||||
|
||||
# The first time a particular session type is used, it is handled differently. After the initial pass (once the
|
||||
# llm_history has more than one entry), the standard 'chat' is used.
|
||||
if len(user_history) >= 1:
|
||||
process_type = "chat"
|
||||
# Do not enable RAG when limiting context to the job description chat
|
||||
if type == "job_description":
|
||||
enable_rag = False
|
||||
else:
|
||||
process_type = type
|
||||
|
||||
if enable_rag:
|
||||
for rag in context["rags"]:
|
||||
if rag["enabled"] and rag["name"] == "JPK": # Only support JPK rag right now...
|
||||
yield {"status": "processing", "message": f"Checking RAG context {rag['name']}..."}
|
||||
chroma_results = self.file_watcher.find_similar(query=content, top_k=10)
|
||||
if chroma_results:
|
||||
rag_docs.extend(chroma_results["documents"])
|
||||
chroma_embedding = chroma_results["query_embedding"]
|
||||
metadata["rag"] = {
|
||||
**chroma_results,
|
||||
@ -942,37 +948,130 @@ class WebServer:
|
||||
"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()
|
||||
}
|
||||
preamble = ""
|
||||
if len(rag_docs):
|
||||
preamble = f"""
|
||||
1. Respond to this query: {content}
|
||||
2. If there is information in this context to enhance the answer, do so:
|
||||
[{context_tag}]:\n"""
|
||||
for doc in rag_docs:
|
||||
preamble += doc
|
||||
preamble += f"\n[/{context_tag}]\nUse all of that information to respond to: "
|
||||
|
||||
# Figure
|
||||
|
||||
match process_type:
|
||||
# Normal chat interactions with context history
|
||||
case "chat":
|
||||
enable_tools = True
|
||||
preamble = ""
|
||||
rag_context = ""
|
||||
for doc in metadata["rag"]["documents"]:
|
||||
rag_context += doc
|
||||
if rag_context:
|
||||
preamble = f"""
|
||||
1. Respond to this query: {content}
|
||||
2. If there is information in this context to enhance the answer, do so:
|
||||
[{context_tag}]
|
||||
{rag_context}
|
||||
[/{context_tag}]
|
||||
Use that information to respond to: """
|
||||
|
||||
# Single job_description is provided; generate a resume
|
||||
case "job_description":
|
||||
# Always force the full resume to be in context
|
||||
resume_doc = open(defines.resume_doc, "r").read()
|
||||
work_history = f"{resume_doc}\n"
|
||||
for doc in metadata["rag"]["documents"]:
|
||||
work_history += f"{doc}\n"
|
||||
|
||||
preamble = f"""
|
||||
[INTRO]
|
||||
{resume_intro}
|
||||
[/INTRO]
|
||||
|
||||
[WORK HISTORY]
|
||||
{work_history}
|
||||
[/WORK HISTORY]
|
||||
|
||||
[JOB DESCRIPTION]
|
||||
{content}
|
||||
[/JOB DESCRIPTION]
|
||||
|
||||
1. Use the above [INTRO] and [WORK HISTORY] to create the resume for the [JOB DESCRIPTION].
|
||||
2. Do not use content from the [JOB DESCRIPTION] in the response unless the [WORK HISTORY] mentions them.
|
||||
"""
|
||||
|
||||
# Seed the first context messages with the resume from the 'job_description' session
|
||||
case "resume":
|
||||
raise Exception(f"Invalid chat type: {type}")
|
||||
|
||||
# Fact check the resume created by the 'job_description' using only the RAG and resume
|
||||
case "fact_check":
|
||||
if len(context["sessions"]["resume"]["llm_history"]) < 3: # SYSTEM, USER, **ASSISTANT**
|
||||
yield {"status": "done", "message": "No resume history found." }
|
||||
return
|
||||
|
||||
resume = context["sessions"]["resume"]["llm_history"][2]
|
||||
|
||||
metadata = copy.deepcopy(resume["metadata"])
|
||||
metadata["eval_count"] = 0
|
||||
metadata["eval_duration"] = 0
|
||||
metadata["prompt_eval_count"] = 0
|
||||
metadata["prompt_eval_duration"] = 0
|
||||
|
||||
resume_doc = open(defines.resume_doc, "r").read()
|
||||
work_history = f"{resume_doc}\n"
|
||||
for doc in metadata["rag"]["documents"]:
|
||||
work_history += f"{doc}\n"
|
||||
|
||||
preamble = f"""
|
||||
[WORK HISTORY]
|
||||
{work_history}
|
||||
[/WORK HISTORY]
|
||||
|
||||
[RESUME]
|
||||
{resume['content']}
|
||||
[/RESUME]
|
||||
"""
|
||||
content = resume['content']
|
||||
|
||||
raise Exception(f"Invalid chat type: {type}")
|
||||
|
||||
case _:
|
||||
raise Exception(f"Invalid chat type: {type}")
|
||||
|
||||
llm_history.append({"role": "user", "content": preamble + content})
|
||||
user_history.append({"role": "user", "content": content})
|
||||
|
||||
if context["message_history_length"]:
|
||||
messages = context["system"] + llm_history[-context["message_history_length"]:]
|
||||
messages = create_system_message(context["sessions"][type]["system_prompt"]) + llm_history[-context["message_history_length"]:]
|
||||
else:
|
||||
messages = context["system"] + llm_history
|
||||
messages = create_system_message(context["sessions"][type]["system_prompt"]) + llm_history
|
||||
|
||||
try:
|
||||
# Estimate token length of new messages
|
||||
ctx_size = self.get_optimal_ctx_size(context["context_tokens"], messages=llm_history[-1]["content"])
|
||||
yield {"status": "processing", "message": "Processing request...", "num_ctx": ctx_size}
|
||||
ctx_size = self.get_optimal_ctx_size(context["sessions"][type]["context_tokens"], messages=llm_history[-1]["content"])
|
||||
|
||||
processing_type = "Processing query..."
|
||||
match type:
|
||||
case "job_description":
|
||||
processing_type = "Generating resume..."
|
||||
case "fact_check":
|
||||
processing_type = "Fact Checking resume..."
|
||||
if len(llm_history) > 1:
|
||||
processing_type = "Processing query..."
|
||||
|
||||
yield {"status": "processing", "message": processing_type, "num_ctx": ctx_size}
|
||||
|
||||
# Use the async generator in an async for loop
|
||||
try:
|
||||
if enable_tools:
|
||||
response = self.client.chat(model=self.model, messages=messages, tools=llm_tools(context["tools"]), options={ "num_ctx": ctx_size })
|
||||
else:
|
||||
response = self.client.chat(model=self.model, messages=messages, options={ "num_ctx": ctx_size })
|
||||
except Exception as e:
|
||||
logging.info(f"1. {messages[0]}")
|
||||
logging.info(f"[LAST]. {messages[-1]}")
|
||||
|
||||
logging.exception({ "model": self.model, "error": str(e) })
|
||||
yield {"status": "error", "message": f"An error occurred communicating with LLM"}
|
||||
return
|
||||
|
||||
metadata["eval_count"] += response["eval_count"]
|
||||
metadata["eval_duration"] += response["eval_duration"]
|
||||
metadata["prompt_eval_count"] += response["prompt_eval_count"]
|
||||
metadata["prompt_eval_duration"] += response["prompt_eval_duration"]
|
||||
context["context_tokens"] = response["prompt_eval_count"] + response["eval_count"]
|
||||
context["sessions"][type]["context_tokens"] = response["prompt_eval_count"] + response["eval_count"]
|
||||
|
||||
tools_used = []
|
||||
|
||||
@ -1015,7 +1114,7 @@ class WebServer:
|
||||
metadata["tools"] = tools_used
|
||||
|
||||
# Estimate token length of new messages
|
||||
ctx_size = self.get_optimal_ctx_size(context["context_tokens"], messages=messages[pre_add_index:])
|
||||
ctx_size = self.get_optimal_ctx_size(context["sessions"][type]["context_tokens"], messages=messages[pre_add_index:])
|
||||
yield {"status": "processing", "message": "Generating final response...", "num_ctx": ctx_size }
|
||||
# Decrease creativity when processing tool call requests
|
||||
response = self.client.chat(model=self.model, messages=messages, stream=False, options={ "num_ctx": ctx_size }) #, "temperature": 0.5 })
|
||||
@ -1023,7 +1122,7 @@ class WebServer:
|
||||
metadata["eval_duration"] += response["eval_duration"]
|
||||
metadata["prompt_eval_count"] += response["prompt_eval_count"]
|
||||
metadata["prompt_eval_duration"] += response["prompt_eval_duration"]
|
||||
context["context_tokens"] = response["prompt_eval_count"] + response["eval_count"]
|
||||
context["sessions"][type]["context_tokens"] = response["prompt_eval_count"] + response["eval_count"]
|
||||
|
||||
reply = response["message"]["content"]
|
||||
final_message = {"role": "assistant", "content": reply }
|
||||
@ -1045,145 +1144,6 @@ class WebServer:
|
||||
finally:
|
||||
self.processing = False
|
||||
|
||||
async def generate_resume(self, context, content):
|
||||
if not self.file_watcher:
|
||||
return
|
||||
|
||||
content = content.strip()
|
||||
if not content:
|
||||
yield {"status": "error", "message": "Invalid request"}
|
||||
return
|
||||
|
||||
if self.processing:
|
||||
yield {"status": "error", "message": "Busy"}
|
||||
return
|
||||
|
||||
self.processing = True
|
||||
resume_history = context["resume_history"]
|
||||
resume = {
|
||||
"job_description": content,
|
||||
"resume": "",
|
||||
"metadata": {},
|
||||
"rag": "",
|
||||
"fact_check": {}
|
||||
}
|
||||
|
||||
metadata = {
|
||||
"rag": {},
|
||||
"tools": [],
|
||||
"eval_count": 0,
|
||||
"eval_duration": 0,
|
||||
"prompt_eval_count": 0,
|
||||
"prompt_eval_duration": 0,
|
||||
}
|
||||
rag_docs = []
|
||||
resume_doc = open(defines.resume_doc, "r").read()
|
||||
rag_docs.append(resume_doc)
|
||||
for rag in context["rags"]:
|
||||
if rag["enabled"] and rag["name"] == "JPK": # Only support JPK rag right now...
|
||||
yield {"status": "processing", "message": f"Checking RAG context {rag['name']}..."}
|
||||
chroma_results = self.file_watcher.find_similar(query=content, top_k=10)
|
||||
if chroma_results:
|
||||
rag_docs.extend(chroma_results["documents"])
|
||||
metadata["rag"] = { "name": rag["name"], **chroma_results }
|
||||
preamble = f"[INTRO]\n{resume_intro}\n[/INTRO]\n"
|
||||
preamble += f"""[WORK HISTORY]:\n"""
|
||||
for doc in rag_docs:
|
||||
preamble += f"{doc}\n"
|
||||
resume["rag"] += f"{doc}\n"
|
||||
preamble += f"\n[/WORK HISTORY]\n"
|
||||
|
||||
content = f"""{preamble}\n
|
||||
Use the above [WORK HISTORY] and [INTRO] to create the resume for this [JOB DESCRIPTION]. Do not use the [JOB DESCRIPTION] in the generated resume unless the [WORK HISTORY] mentions them:\n[JOB DESCRIPTION]\n{content}\n[/JOB DESCRIPTION]\n"""
|
||||
|
||||
try:
|
||||
# Estimate token length of new messages
|
||||
ctx_size = self.get_optimal_ctx_size(context["context_tokens"], messages=[system_generate_resume, content])
|
||||
|
||||
yield {"status": "processing", "message": "Processing request...", "num_ctx": ctx_size}
|
||||
|
||||
# Use the async generator in an async for loop
|
||||
#
|
||||
# To support URL lookup:
|
||||
#
|
||||
# 1. Enable tools in a call to chat() with a simple prompt to invoke the tool to generate the summary if requested.
|
||||
# 2. If not requested (no tool call,) abort the path
|
||||
# 3. Otherwise, we know the URL was good and can use that URLs fetched content as context.
|
||||
#
|
||||
response = self.client.generate(model=self.model, system=system_generate_resume, prompt=content, options={ "num_ctx": ctx_size })
|
||||
metadata["eval_count"] += response["eval_count"]
|
||||
metadata["eval_duration"] += response["eval_duration"]
|
||||
metadata["prompt_eval_count"] += response["prompt_eval_count"]
|
||||
metadata["prompt_eval_duration"] += response["prompt_eval_duration"]
|
||||
context["context_tokens"] = response["prompt_eval_count"] + response["eval_count"]
|
||||
|
||||
reply = response["response"]
|
||||
final_message = {"role": "assistant", "content": reply, "metadata": metadata }
|
||||
|
||||
resume["resume"] = final_message
|
||||
resume_history.append(resume)
|
||||
|
||||
# Return the REST API with metadata
|
||||
yield {"status": "done", "message": final_message }
|
||||
|
||||
except Exception as e:
|
||||
logging.exception({ "model": self.model, "content": content, "error": str(e) })
|
||||
yield {"status": "error", "message": f"An error occurred: {str(e)}"}
|
||||
|
||||
finally:
|
||||
self.processing = False
|
||||
|
||||
async def fact_check(self, context, content):
|
||||
content = content.strip()
|
||||
if not content:
|
||||
yield {"status": "error", "message": "Invalid request"}
|
||||
return
|
||||
|
||||
if self.processing:
|
||||
yield {"status": "error", "message": "Busy"}
|
||||
return
|
||||
|
||||
self.processing = True
|
||||
resume_history = context["resume_history"]
|
||||
if len(resume_history) == 0:
|
||||
yield {"status": "done", "message": "No resume history found." }
|
||||
return
|
||||
|
||||
resume = resume_history[-1]
|
||||
metadata = resume["metadata"]
|
||||
metadata["eval_count"] = 0
|
||||
metadata["eval_duration"] = 0
|
||||
metadata["prompt_eval_count"] = 0
|
||||
metadata["prompt_eval_duration"] = 0
|
||||
|
||||
content = f"[WORK HISTORY]:{resume['rag']}[/WORK HISTORY]\n\n[RESUME]\n{resume['resume']['content']}\n[/RESUME]\n\n"
|
||||
|
||||
try:
|
||||
# Estimate token length of new messages
|
||||
ctx_size = self.get_optimal_ctx_size(context["context_tokens"], messages=[system_fact_check, content])
|
||||
yield {"status": "processing", "message": "Processing request...", "num_ctx": ctx_size}
|
||||
response = self.client.generate(model=self.model, system=system_fact_check, prompt=content, options={ "num_ctx": ctx_size })
|
||||
logging.info(f"Fact checking {ctx_size} tokens.")
|
||||
metadata["eval_count"] += response["eval_count"]
|
||||
metadata["eval_duration"] += response["eval_duration"]
|
||||
metadata["prompt_eval_count"] += response["prompt_eval_count"]
|
||||
metadata["prompt_eval_duration"] += response["prompt_eval_duration"]
|
||||
context["context_tokens"] = response["prompt_eval_count"] + response["eval_count"]
|
||||
reply = response["response"]
|
||||
final_message = {"role": "assistant", "content": reply, "metadata": metadata }
|
||||
resume["fact_check"] = final_message
|
||||
|
||||
# Return the REST API with metadata
|
||||
yield {"status": "done", "message": final_message }
|
||||
|
||||
except Exception as e:
|
||||
logging.exception({ "model": self.model, "content": content, "error": str(e) })
|
||||
yield {"status": "error", "message": f"An error occurred: {str(e)}"}
|
||||
|
||||
finally:
|
||||
self.processing = False
|
||||
|
||||
|
||||
def run(self, host="0.0.0.0", port=WEB_PORT, **kwargs):
|
||||
try:
|
||||
if self.ssl_enabled:
|
||||
|
Loading…
x
Reference in New Issue
Block a user