Chat is working again, just not saving
This commit is contained in:
parent
02a278736e
commit
11447b68aa
@ -44,6 +44,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
webpack: {
|
webpack: {
|
||||||
configure: (webpackConfig) => {
|
configure: (webpackConfig) => {
|
||||||
|
webpackConfig.devtool = 'source-map';
|
||||||
// Add .ts and .tsx to resolve.extensions
|
// Add .ts and .tsx to resolve.extensions
|
||||||
webpackConfig.resolve.extensions = [
|
webpackConfig.resolve.extensions = [
|
||||||
...webpackConfig.resolve.extensions,
|
...webpackConfig.resolve.extensions,
|
||||||
|
@ -8,20 +8,20 @@ import CancelIcon from '@mui/icons-material/Cancel';
|
|||||||
import { SxProps, Theme } from '@mui/material';
|
import { SxProps, Theme } from '@mui/material';
|
||||||
import PropagateLoader from "react-spinners/PropagateLoader";
|
import PropagateLoader from "react-spinners/PropagateLoader";
|
||||||
|
|
||||||
import { Message, MessageRoles } from './Message';
|
import { Message } from './Message';
|
||||||
import { DeleteConfirmation } from 'components/DeleteConfirmation';
|
import { DeleteConfirmation } from 'components/DeleteConfirmation';
|
||||||
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
|
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
|
||||||
import { BackstoryElementProps } from './BackstoryTab';
|
import { BackstoryElementProps } from './BackstoryTab';
|
||||||
import { connectionBase } from 'utils/Global';
|
import { connectionBase } from 'utils/Global';
|
||||||
import { useUser } from "hooks/useUser";
|
import { useUser } from "hooks/useUser";
|
||||||
import { StreamingResponse } from 'types/api-client';
|
import { StreamingResponse } from 'types/api-client';
|
||||||
import { ChatMessage, ChatContext, ChatSession, ChatQuery } from 'types/types';
|
import { ChatMessage, ChatMessageBase, ChatContext, ChatSession, ChatQuery } from 'types/types';
|
||||||
import { PaginatedResponse } from 'types/conversion';
|
import { PaginatedResponse } from 'types/conversion';
|
||||||
|
|
||||||
import './Conversation.css';
|
import './Conversation.css';
|
||||||
|
|
||||||
const defaultMessage: ChatMessage = {
|
const defaultMessage: ChatMessage = {
|
||||||
status: "thinking", sender: "system", sessionId: "", timestamp: new Date(), content: ""
|
type: "preparing", status: "done", sender: "system", sessionId: "", timestamp: new Date(), content: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadingMessage: ChatMessage = { ...defaultMessage, content: "Establishing connection with server..." };
|
const loadingMessage: ChatMessage = { ...defaultMessage, content: "Establishing connection with server..." };
|
||||||
@ -249,6 +249,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
|
|||||||
...conversationRef.current,
|
...conversationRef.current,
|
||||||
{
|
{
|
||||||
...defaultMessage,
|
...defaultMessage,
|
||||||
|
type: 'user',
|
||||||
sender: 'user',
|
sender: 'user',
|
||||||
content: query.prompt,
|
content: query.prompt,
|
||||||
}
|
}
|
||||||
@ -259,44 +260,44 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
|
|||||||
);
|
);
|
||||||
|
|
||||||
controllerRef.current = apiClient.sendMessageStream(sessionId, query, {
|
controllerRef.current = apiClient.sendMessageStream(sessionId, query, {
|
||||||
onComplete: (msg) => {
|
onMessage: (msg) => {
|
||||||
console.log(msg);
|
console.log("onMessage:", msg);
|
||||||
switch (msg.status) {
|
if (msg.type === "response") {
|
||||||
case "done":
|
setConversation([
|
||||||
case "partial":
|
...conversationRef.current,
|
||||||
setConversation([
|
msg
|
||||||
...conversationRef.current, {
|
]);
|
||||||
...msg,
|
setStreamingMessage(undefined);
|
||||||
role: 'assistant',
|
setProcessingMessage(undefined);
|
||||||
origin: type,
|
setProcessing(false);
|
||||||
}] as ChatMessage[]);
|
} else {
|
||||||
if (msg.status === "done") {
|
setProcessingMessage(msg);
|
||||||
setStreamingMessage(undefined);
|
}
|
||||||
setProcessingMessage(undefined);
|
if (onResponse) {
|
||||||
setProcessing(false);
|
onResponse(msg);
|
||||||
controllerRef.current = null;
|
|
||||||
}
|
|
||||||
if (onResponse) {
|
|
||||||
onResponse(msg);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "error":
|
|
||||||
// Show error
|
|
||||||
setConversation([
|
|
||||||
...conversationRef.current,
|
|
||||||
msg
|
|
||||||
]);
|
|
||||||
setProcessingMessage(msg);
|
|
||||||
setProcessing(false);
|
|
||||||
controllerRef.current = null;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
setProcessingMessage(msg);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPartialMessage: (chunk) => {
|
onError: (error: string | ChatMessageBase) => {
|
||||||
setStreamingMessage({ ...defaultMessage, status: "streaming", content: chunk });
|
console.log("onError:", error);
|
||||||
|
// Type-guard to determine if this is a ChatMessageBase or a string
|
||||||
|
if (typeof error === "object" && error !== null && "content" in error) {
|
||||||
|
setProcessingMessage(error as ChatMessage);
|
||||||
|
setProcessing(false);
|
||||||
|
controllerRef.current = null;
|
||||||
|
} else {
|
||||||
|
setProcessingMessage({ ...defaultMessage, content: error as string });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onStreaming: (chunk) => {
|
||||||
|
console.log("onStreaming:", chunk);
|
||||||
|
setStreamingMessage({ ...defaultMessage, ...chunk });
|
||||||
|
},
|
||||||
|
onStatusChange: (status) => {
|
||||||
|
console.log("onStatusChange:", status);
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
console.log("onComplete");
|
||||||
|
controllerRef.current = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -32,68 +32,9 @@ import { SetSnackType } from './Snack';
|
|||||||
import { CopyBubble } from './CopyBubble';
|
import { CopyBubble } from './CopyBubble';
|
||||||
import { Scrollable } from './Scrollable';
|
import { Scrollable } from './Scrollable';
|
||||||
import { BackstoryElementProps } from './BackstoryTab';
|
import { BackstoryElementProps } from './BackstoryTab';
|
||||||
import { ChatMessage, ChatSession } from 'types/types';
|
import { ChatMessage, ChatSession, ChatMessageType } from 'types/types';
|
||||||
|
|
||||||
type MessageRoles =
|
|
||||||
'assistant' |
|
|
||||||
'content' |
|
|
||||||
'error' |
|
|
||||||
'fact-check' |
|
|
||||||
'info' |
|
|
||||||
'job-description' |
|
|
||||||
'job-requirements' |
|
|
||||||
'processing' |
|
|
||||||
'qualifications' |
|
|
||||||
'resume' |
|
|
||||||
'status' |
|
|
||||||
'streaming' |
|
|
||||||
'system' |
|
|
||||||
'thinking' |
|
|
||||||
'user';
|
|
||||||
|
|
||||||
type BackstoryMessage = {
|
|
||||||
// Only two required fields
|
|
||||||
role: MessageRoles,
|
|
||||||
content: string,
|
|
||||||
// Rest are optional
|
|
||||||
prompt?: string;
|
|
||||||
preamble?: {};
|
|
||||||
status?: string;
|
|
||||||
remaining_time?: number;
|
|
||||||
full_content?: string;
|
|
||||||
response?: string; // Set when status === 'done', 'partial', or 'error'
|
|
||||||
chunk?: string; // Used when status === 'streaming'
|
|
||||||
timestamp?: number;
|
|
||||||
disableCopy?: boolean,
|
|
||||||
user?: string,
|
|
||||||
title?: string,
|
|
||||||
origin?: string,
|
|
||||||
display?: string, /* Messages generated on the server for filler should not be shown */
|
|
||||||
id?: string,
|
|
||||||
isProcessing?: boolean,
|
|
||||||
actions?: string[],
|
|
||||||
metadata?: MessageMetaData,
|
|
||||||
expanded?: boolean,
|
|
||||||
expandable?: boolean,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ChatBubbleProps {
|
|
||||||
role: MessageRoles,
|
|
||||||
isInfo?: boolean;
|
|
||||||
children: React.ReactNode;
|
|
||||||
sx?: SxProps<Theme>;
|
|
||||||
className?: string;
|
|
||||||
title?: string;
|
|
||||||
expanded?: boolean;
|
|
||||||
expandable?: boolean;
|
|
||||||
onExpand?: (open: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ChatBubble(props: ChatBubbleProps) {
|
|
||||||
const { role, children, sx, className, title, onExpand, expandable, expanded } = props;
|
|
||||||
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
|
const getStyle = (theme: Theme, type: ChatMessageType): any => {
|
||||||
const defaultRadius = '16px';
|
const defaultRadius = '16px';
|
||||||
const defaultStyle = {
|
const defaultStyle = {
|
||||||
padding: theme.spacing(1, 2),
|
padding: theme.spacing(1, 2),
|
||||||
@ -115,7 +56,7 @@ function ChatBubble(props: ChatBubbleProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const styles: any = {
|
const styles: any = {
|
||||||
assistant: {
|
response: {
|
||||||
...defaultStyle,
|
...defaultStyle,
|
||||||
backgroundColor: theme.palette.primary.main,
|
backgroundColor: theme.palette.primary.main,
|
||||||
border: `1px solid ${theme.palette.secondary.main}`,
|
border: `1px solid ${theme.palette.secondary.main}`,
|
||||||
@ -184,7 +125,7 @@ function ChatBubble(props: ChatBubbleProps) {
|
|||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
transition: 'opacity 0.3s ease-in-out',
|
transition: 'opacity 0.3s ease-in-out',
|
||||||
},
|
},
|
||||||
streaming: 'assistant',
|
streaming: 'response',
|
||||||
system: {
|
system: {
|
||||||
...defaultStyle,
|
...defaultStyle,
|
||||||
backgroundColor: '#EDEAE0',
|
backgroundColor: '#EDEAE0',
|
||||||
@ -214,102 +155,32 @@ function ChatBubble(props: ChatBubbleProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return styles[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIcon = (messageType: string): React.ReactNode | null => {
|
||||||
const icons: any = {
|
const icons: any = {
|
||||||
error: <ErrorOutline color="error" />,
|
error: <ErrorOutline color="error" />,
|
||||||
|
generating: <LocationSearchingIcon />,
|
||||||
info: <InfoOutline color="info" />,
|
info: <InfoOutline color="info" />,
|
||||||
|
preparing: <LocationSearchingIcon />,
|
||||||
processing: <LocationSearchingIcon />,
|
processing: <LocationSearchingIcon />,
|
||||||
searching: <Memory />,
|
system: <Memory />,
|
||||||
thinking: <Psychology />,
|
thinking: <Psychology />,
|
||||||
tooling: <LocationSearchingIcon />,
|
tooling: <LocationSearchingIcon />,
|
||||||
};
|
};
|
||||||
|
return icons[messageType] || null;
|
||||||
// Render Accordion for expandable content
|
|
||||||
if (expandable || title) {
|
|
||||||
// Determine if Accordion is controlled
|
|
||||||
const isControlled = typeof expanded === 'boolean' && typeof onExpand === 'function';
|
|
||||||
return (
|
|
||||||
<Accordion
|
|
||||||
expanded={isControlled ? expanded : undefined} // Omit expanded prop for uncontrolled
|
|
||||||
defaultExpanded={expanded} // Default to collapsed for uncontrolled Accordion
|
|
||||||
className={className}
|
|
||||||
onChange={(_event, newExpanded) => {
|
|
||||||
if (isControlled && onExpand) {
|
|
||||||
onExpand(newExpanded); // Call onExpand with new state
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
sx={{ ...styles[role], ...sx }}
|
|
||||||
>
|
|
||||||
<AccordionSummary
|
|
||||||
expandIcon={<ExpandMoreIcon />}
|
|
||||||
slotProps={{
|
|
||||||
content: {
|
|
||||||
sx: {
|
|
||||||
fontWeight: 'bold',
|
|
||||||
fontSize: '1.1rem',
|
|
||||||
m: 0,
|
|
||||||
p: 0,
|
|
||||||
display: 'flex',
|
|
||||||
justifyItems: 'center',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{title || ''}
|
|
||||||
</AccordionSummary>
|
|
||||||
<AccordionDetails sx={{ mt: 0, mb: 0, p: 0, pl: 2, pr: 2 }}>
|
|
||||||
{children}
|
|
||||||
</AccordionDetails>
|
|
||||||
</Accordion>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render non-expandable content
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
className={className}
|
|
||||||
sx={{
|
|
||||||
...(role in styles ? styles[role] : styles['status']),
|
|
||||||
gap: 1,
|
|
||||||
display: 'flex',
|
|
||||||
...sx,
|
|
||||||
flexDirection: 'row',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{icons[role] !== undefined && icons[role]}
|
|
||||||
<Box sx={{ p: 0, m: 0, gap: 0, display: 'flex', flexGrow: 1, flexDirection: 'column' }}>
|
|
||||||
{children}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MessageMetaData {
|
|
||||||
query?: {
|
|
||||||
query_embedding: number[];
|
|
||||||
vector_embedding: number[];
|
|
||||||
},
|
|
||||||
origin: string,
|
|
||||||
rag: any[],
|
|
||||||
tools?: {
|
|
||||||
tool_calls: any[],
|
|
||||||
},
|
|
||||||
eval_count: number,
|
|
||||||
eval_duration: number,
|
|
||||||
prompt_eval_count: number,
|
|
||||||
prompt_eval_duration: number,
|
|
||||||
connectionBase: string,
|
|
||||||
setSnack: SetSnackType,
|
|
||||||
}
|
|
||||||
|
|
||||||
type MessageList = BackstoryMessage[];
|
|
||||||
|
|
||||||
interface MessageProps extends BackstoryElementProps {
|
interface MessageProps extends BackstoryElementProps {
|
||||||
sx?: SxProps<Theme>,
|
|
||||||
message: ChatMessage,
|
message: ChatMessage,
|
||||||
|
title?: string,
|
||||||
|
chatSession?: ChatSession,
|
||||||
|
className?: string,
|
||||||
|
sx?: SxProps<Theme>,
|
||||||
|
expandable?: boolean,
|
||||||
expanded?: boolean,
|
expanded?: boolean,
|
||||||
onExpand?: (open: boolean) => void,
|
onExpand?: (open: boolean) => void,
|
||||||
className?: string,
|
|
||||||
chatSession?: ChatSession,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface MessageMetaProps {
|
interface MessageMetaProps {
|
||||||
@ -317,7 +188,6 @@ interface MessageMetaProps {
|
|||||||
messageProps: MessageProps
|
messageProps: MessageProps
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const MessageMeta = (props: MessageMetaProps) => {
|
const MessageMeta = (props: MessageMetaProps) => {
|
||||||
const {
|
const {
|
||||||
/* MessageData */
|
/* MessageData */
|
||||||
@ -447,102 +317,124 @@ const MessageMeta = (props: MessageMetaProps) => {
|
|||||||
</>);
|
</>);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface MessageContainerProps {
|
||||||
|
type: ChatMessageType,
|
||||||
|
metadataView?: React.ReactNode | null,
|
||||||
|
messageView?: React.ReactNode | null,
|
||||||
|
sx?: SxProps<Theme>,
|
||||||
|
copyContent?: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MessageContainer = (props: MessageContainerProps) => {
|
||||||
|
const { type, sx, messageView, metadataView, copyContent } = props;
|
||||||
|
const icon = getIcon(type);
|
||||||
|
|
||||||
|
return <Box
|
||||||
|
className={`Message Message-${type}`}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
m: 0,
|
||||||
|
mt: 1,
|
||||||
|
marginBottom: "0px !important", // Remove whitespace from expanded Accordion
|
||||||
|
gap: 1,
|
||||||
|
...sx,
|
||||||
|
}}>
|
||||||
|
<Box sx={{ display: "flex", flexDirection: 'row' }}>
|
||||||
|
{icon !== null && icon}
|
||||||
|
{messageView}
|
||||||
|
</Box>
|
||||||
|
{metadataView}
|
||||||
|
{copyContent && <CopyBubble content={copyContent} />}
|
||||||
|
</Box>;
|
||||||
|
};
|
||||||
|
|
||||||
const Message = (props: MessageProps) => {
|
const Message = (props: MessageProps) => {
|
||||||
const { message, submitQuery, sx, className, chatSession, onExpand, setSnack, expanded } = props;
|
const { message, title, submitQuery, sx, className, chatSession, onExpand, setSnack, expanded, expandable } = props;
|
||||||
const [metaExpanded, setMetaExpanded] = useState<boolean>(false);
|
const [metaExpanded, setMetaExpanded] = useState<boolean>(false);
|
||||||
const textFieldRef = useRef(null);
|
|
||||||
const backstoryProps = {
|
const backstoryProps = {
|
||||||
submitQuery,
|
submitQuery,
|
||||||
setSnack
|
setSnack
|
||||||
};
|
};
|
||||||
|
const theme = useTheme();
|
||||||
|
const style: any = getStyle(theme, message.type);
|
||||||
|
|
||||||
const handleMetaExpandClick = () => {
|
const handleMetaExpandClick = () => {
|
||||||
setMetaExpanded(!metaExpanded);
|
setMetaExpanded(!metaExpanded);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (message === undefined) {
|
const content = message.content?.trim();
|
||||||
return (<></>);
|
if (!content) {
|
||||||
}
|
return (<></>)
|
||||||
|
};
|
||||||
|
|
||||||
if (message.content === undefined) {
|
const messageView = (
|
||||||
console.info("Message content is undefined");
|
<StyledMarkdown chatSession={chatSession} streaming={message.status === "streaming"} content={content} {...backstoryProps} />
|
||||||
return (<></>);
|
|
||||||
}
|
|
||||||
|
|
||||||
const formattedContent = message.content.trim();
|
|
||||||
if (formattedContent === "") {
|
|
||||||
return (<></>);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ChatBubble
|
|
||||||
role='assistant'
|
|
||||||
className={`${className || ""} Message Message-${message.sender}`}
|
|
||||||
{...message}
|
|
||||||
expanded={expanded}
|
|
||||||
onExpand={onExpand}
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
pb: message.metadata ? 0 : "8px",
|
|
||||||
m: 0,
|
|
||||||
mt: 1,
|
|
||||||
marginBottom: "0px !important", // Remove whitespace from expanded Accordion
|
|
||||||
// overflowX: "auto"
|
|
||||||
...sx,
|
|
||||||
}}>
|
|
||||||
<CardContent ref={textFieldRef} sx={{ position: "relative", display: "flex", flexDirection: "column", overflowX: "auto", m: 0, p: 0, paddingBottom: '0px !important' }}>
|
|
||||||
<Scrollable
|
|
||||||
className="MessageContent"
|
|
||||||
autoscroll
|
|
||||||
fallbackThreshold={0.5}
|
|
||||||
sx={{
|
|
||||||
p: 0,
|
|
||||||
m: 0,
|
|
||||||
// maxHeight: (message.role === "streaming") ? "20rem" : "unset",
|
|
||||||
display: "flex",
|
|
||||||
flexGrow: 1,
|
|
||||||
overflow: "auto", /* Handles scrolling for the div */
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StyledMarkdown chatSession={chatSession} streaming={message.status === "streaming"} content={formattedContent} {...backstoryProps} />
|
|
||||||
</Scrollable>
|
|
||||||
</CardContent>
|
|
||||||
<CardActions disableSpacing sx={{ display: "flex", flexDirection: "row", justifyContent: "space-between", alignItems: "center", width: "100%", p: 0, m: 0 }}>
|
|
||||||
{/*(message.disableCopy === undefined || message.disableCopy === false) &&*/ <CopyBubble content={message.content} />}
|
|
||||||
{message.metadata && (
|
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
|
||||||
<Button variant="text" onClick={handleMetaExpandClick} sx={{ color: "darkgrey", p: 0 }}>
|
|
||||||
LLM information for this query
|
|
||||||
</Button>
|
|
||||||
<ExpandMore
|
|
||||||
expand={metaExpanded}
|
|
||||||
onClick={handleMetaExpandClick}
|
|
||||||
aria-expanded={true /*message.expanded*/}
|
|
||||||
aria-label="show more"
|
|
||||||
>
|
|
||||||
<ExpandMoreIcon />
|
|
||||||
</ExpandMore>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</CardActions>
|
|
||||||
{message.metadata && <>
|
|
||||||
<Collapse in={metaExpanded} timeout="auto" unmountOnExit>
|
|
||||||
<CardContent>
|
|
||||||
<MessageMeta messageProps={props} metadata={message.metadata} />
|
|
||||||
</CardContent>
|
|
||||||
</Collapse>
|
|
||||||
</>}
|
|
||||||
</ChatBubble>
|
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
let metadataView = (<></>);
|
||||||
|
if (message.metadata) {
|
||||||
|
metadataView = (<>
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
|
<Button variant="text" onClick={handleMetaExpandClick} sx={{ color: "darkgrey", p: 0 }}>
|
||||||
|
LLM information for this query
|
||||||
|
</Button>
|
||||||
|
<ExpandMore
|
||||||
|
expand={metaExpanded}
|
||||||
|
onClick={handleMetaExpandClick}
|
||||||
|
aria-expanded={true /*message.expanded*/}
|
||||||
|
aria-label="show more">
|
||||||
|
<ExpandMoreIcon />
|
||||||
|
</ExpandMore>
|
||||||
|
</Box>
|
||||||
|
<Collapse in={metaExpanded} timeout="auto" unmountOnExit>
|
||||||
|
<CardContent>
|
||||||
|
<MessageMeta messageProps={props} metadata={message.metadata} />
|
||||||
|
</CardContent>
|
||||||
|
</Collapse>
|
||||||
|
</>);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!expandable) {
|
||||||
|
/* When not expandable, the styles are applied directly to MessageContainer */
|
||||||
|
return (<>
|
||||||
|
{messageView && <MessageContainer type={message.type} {...{ messageView, metadataView }} sx={{ ...style, ...sx }} />}
|
||||||
|
</>);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if Accordion is controlled
|
||||||
|
const isControlled = typeof expanded === 'boolean' && typeof onExpand === 'function';
|
||||||
|
return (
|
||||||
|
<Accordion
|
||||||
|
expanded={isControlled ? expanded : undefined} // Omit expanded prop for uncontrolled
|
||||||
|
defaultExpanded={expanded} // Default to collapsed for uncontrolled Accordion
|
||||||
|
className={className}
|
||||||
|
onChange={(_event, newExpanded) => { isControlled && onExpand && onExpand(newExpanded) }}
|
||||||
|
sx={{ ...sx, ...style }}>
|
||||||
|
<AccordionSummary
|
||||||
|
expandIcon={<ExpandMoreIcon />}
|
||||||
|
slotProps={{
|
||||||
|
content: {
|
||||||
|
sx: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyItems: 'center',
|
||||||
|
m: 0, p: 0,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: '1.1rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}>
|
||||||
|
{title || ''}
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails sx={{ mt: 0, mb: 0, p: 0, pl: 2, pr: 2 }}>
|
||||||
|
<MessageContainer type={message.type} {...{ messageView, metadataView }} />
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
MessageProps,
|
MessageProps,
|
||||||
MessageList,
|
|
||||||
BackstoryMessage,
|
|
||||||
MessageMetaData,
|
|
||||||
MessageRoles,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -17,7 +17,7 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
|
|||||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
const [questions, setQuestions] = useState<React.ReactElement[]>([]);
|
const [questions, setQuestions] = useState<React.ReactElement[]>([]);
|
||||||
|
|
||||||
console.log("ChatPage candidate =>", candidate);
|
// console.log("ChatPage candidate =>", candidate);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!candidate) {
|
if (!candidate) {
|
||||||
return;
|
return;
|
||||||
|
@ -19,7 +19,7 @@ import { StyledMarkdown } from 'components/StyledMarkdown';
|
|||||||
import { Scrollable } from '../components/Scrollable';
|
import { Scrollable } from '../components/Scrollable';
|
||||||
import { Pulse } from 'components/Pulse';
|
import { Pulse } from 'components/Pulse';
|
||||||
import { StreamingResponse } from 'types/api-client';
|
import { StreamingResponse } from 'types/api-client';
|
||||||
import { ChatContext, ChatSession, ChatQuery } from 'types/types';
|
import { ChatContext, ChatMessage, ChatMessageBase, ChatSession, ChatQuery } from 'types/types';
|
||||||
import { useUser } from 'hooks/useUser';
|
import { useUser } from 'hooks/useUser';
|
||||||
|
|
||||||
const emptyUser: Candidate = {
|
const emptyUser: Candidate = {
|
||||||
@ -102,21 +102,30 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
|
|||||||
setShouldGenerateProfile(false); // Reset the flag
|
setShouldGenerateProfile(false); // Reset the flag
|
||||||
|
|
||||||
const streamResponse = apiClient.sendMessageStream(sessionId, query, {
|
const streamResponse = apiClient.sendMessageStream(sessionId, query, {
|
||||||
onPartialMessage: (content, messageId) => {
|
onMessage: (chatMessage: ChatMessage) => {
|
||||||
console.log('Partial content:', content);
|
console.log('Message:', chatMessage);
|
||||||
// Update UI with partial content
|
// Update UI with partial content
|
||||||
},
|
},
|
||||||
onStatusChange: (status) => {
|
onStatusChange: (status) => {
|
||||||
console.log('Status changed:', status);
|
console.log('Status changed:', status);
|
||||||
// Update UI status indicator
|
// Update UI status indicator
|
||||||
},
|
},
|
||||||
onComplete: (finalMessage) => {
|
onComplete: () => {
|
||||||
console.log('Final message:', finalMessage.content);
|
console.log('Content complete');
|
||||||
// Handle completed message
|
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onWarn: (warning) => {
|
||||||
console.error('Streaming error:', error);
|
console.log("Warning:", warning);
|
||||||
// Handle error
|
},
|
||||||
|
onError: (error: string | ChatMessageBase) => {
|
||||||
|
// Type-guard to determine if this is a ChatMessageBase or a string
|
||||||
|
if (typeof error === "object" && error !== null && "content" in error) {
|
||||||
|
console.log("Error message:", error);
|
||||||
|
} else {
|
||||||
|
console.log("Error string:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onStreaming: (chunk) => {
|
||||||
|
console.log("Streaming: ", chunk);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// controllerRef.current = streamQueryResponse({
|
// controllerRef.current = streamQueryResponse({
|
||||||
|
@ -6,6 +6,7 @@ import { ChatMessage } from 'types/types';
|
|||||||
const LoadingPage = (props: BackstoryPageProps) => {
|
const LoadingPage = (props: BackstoryPageProps) => {
|
||||||
const preamble: ChatMessage = {
|
const preamble: ChatMessage = {
|
||||||
sender: 'system',
|
sender: 'system',
|
||||||
|
type: 'preparing',
|
||||||
status: 'done',
|
status: 'done',
|
||||||
sessionId: '',
|
sessionId: '',
|
||||||
content: 'Please wait while connecting to Backstory...',
|
content: 'Please wait while connecting to Backstory...',
|
||||||
|
@ -7,11 +7,9 @@ import {
|
|||||||
import { SxProps } from '@mui/material';
|
import { SxProps } from '@mui/material';
|
||||||
|
|
||||||
import { BackstoryQuery } from 'components/BackstoryQuery';
|
import { BackstoryQuery } from 'components/BackstoryQuery';
|
||||||
import { MessageList, BackstoryMessage } from 'components/Message';
|
|
||||||
import { Conversation } from 'components/Conversation';
|
import { Conversation } from 'components/Conversation';
|
||||||
import { BackstoryPageProps } from 'components/BackstoryTab';
|
import { BackstoryPageProps } from 'components/BackstoryTab';
|
||||||
import { ChatQuery } from "types/types";
|
import { ChatQuery, ChatMessage } from "types/types";
|
||||||
|
|
||||||
import './ResumeBuilderPage.css';
|
import './ResumeBuilderPage.css';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -58,19 +56,19 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPagePro
|
|||||||
factsConversationRef.current?.submitQuery(query);
|
factsConversationRef.current?.submitQuery(query);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterJobDescriptionMessages = useCallback((messages: MessageList): MessageList => {
|
const filterJobDescriptionMessages = useCallback((messages: ChatMessage[]): ChatMessage[] => {
|
||||||
if (messages === undefined || messages.length === 0) {
|
if (messages === undefined || messages.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messages.length > 0) {
|
if (messages.length > 0) {
|
||||||
messages[0].role = 'content';
|
// messages[0].role = 'content';
|
||||||
messages[0].title = 'Job Description';
|
// messages[0].title = 'Job Description';
|
||||||
messages[0].disableCopy = false;
|
// messages[0].disableCopy = false;
|
||||||
messages[0].expandable = true;
|
// messages[0].expandable = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-1 !== messages.findIndex(m => m.status === 'done' || (m.actions && m.actions.includes("resume_generated")))) {
|
if (-1 !== messages.findIndex(m => m.status === 'done')) { // || (m.actions && m.actions.includes("resume_generated")))) {
|
||||||
setHasResume(true);
|
setHasResume(true);
|
||||||
setHasFacts(true);
|
setHasFacts(true);
|
||||||
}
|
}
|
||||||
@ -85,11 +83,11 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPagePro
|
|||||||
|
|
||||||
if (messages.length > 3) {
|
if (messages.length > 3) {
|
||||||
// messages[2] is Show job requirements
|
// messages[2] is Show job requirements
|
||||||
messages[3].role = 'job-requirements';
|
// messages[3].role = 'job-requirements';
|
||||||
messages[3].title = 'Job Requirements';
|
// messages[3].title = 'Job Requirements';
|
||||||
messages[3].disableCopy = false;
|
// messages[3].disableCopy = false;
|
||||||
messages[3].expanded = false;
|
// messages[3].expanded = false;
|
||||||
messages[3].expandable = true;
|
// messages[3].expandable = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Filter out the 2nd and 3rd (0-based) */
|
/* Filter out the 2nd and 3rd (0-based) */
|
||||||
@ -99,7 +97,7 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPagePro
|
|||||||
return filtered;
|
return filtered;
|
||||||
}, [setHasResume, setHasFacts]);
|
}, [setHasResume, setHasFacts]);
|
||||||
|
|
||||||
const filterResumeMessages = useCallback((messages: MessageList): MessageList => {
|
const filterResumeMessages = useCallback((messages: ChatMessage[]): ChatMessage[] => {
|
||||||
if (messages === undefined || messages.length === 0) {
|
if (messages === undefined || messages.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -108,20 +106,20 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPagePro
|
|||||||
|
|
||||||
if (messages.length > 1) {
|
if (messages.length > 1) {
|
||||||
// messages[0] is Show Qualifications
|
// messages[0] is Show Qualifications
|
||||||
messages[1].role = 'qualifications';
|
// messages[1].role = 'qualifications';
|
||||||
messages[1].title = 'Candidate qualifications';
|
// messages[1].title = 'Candidate qualifications';
|
||||||
messages[1].disableCopy = false;
|
// messages[1].disableCopy = false;
|
||||||
messages[1].expanded = false;
|
// messages[1].expanded = false;
|
||||||
messages[1].expandable = true;
|
// messages[1].expandable = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messages.length > 3) {
|
if (messages.length > 3) {
|
||||||
// messages[2] is Show Resume
|
// messages[2] is Show Resume
|
||||||
messages[3].role = 'resume';
|
// messages[3].role = 'resume';
|
||||||
messages[3].title = 'Generated Resume';
|
// messages[3].title = 'Generated Resume';
|
||||||
messages[3].disableCopy = false;
|
// messages[3].disableCopy = false;
|
||||||
messages[3].expanded = true;
|
// messages[3].expanded = true;
|
||||||
messages[3].expandable = true;
|
// messages[3].expandable = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Filter out the 1st and 3rd messages (0-based) */
|
/* Filter out the 1st and 3rd messages (0-based) */
|
||||||
@ -130,18 +128,18 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPagePro
|
|||||||
return filtered;
|
return filtered;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const filterFactsMessages = useCallback((messages: MessageList): MessageList => {
|
const filterFactsMessages = useCallback((messages: ChatMessage[]): ChatMessage[] => {
|
||||||
if (messages === undefined || messages.length === 0) {
|
if (messages === undefined || messages.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messages.length > 1) {
|
if (messages.length > 1) {
|
||||||
// messages[0] is Show verification
|
// messages[0] is Show verification
|
||||||
messages[1].role = 'fact-check';
|
// messages[1].role = 'fact-check';
|
||||||
messages[1].title = 'Fact Check';
|
// messages[1].title = 'Fact Check';
|
||||||
messages[1].disableCopy = false;
|
// messages[1].disableCopy = false;
|
||||||
messages[1].expanded = true;
|
// messages[1].expanded = true;
|
||||||
messages[1].expandable = true;
|
// messages[1].expandable = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Filter out the 1st (0-based) */
|
/* Filter out the 1st (0-based) */
|
||||||
@ -150,33 +148,33 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPagePro
|
|||||||
return filtered;
|
return filtered;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const jobResponse = useCallback(async (message: BackstoryMessage) => {
|
const jobResponse = useCallback(async (message: ChatMessage) => {
|
||||||
if (message.actions && message.actions.includes("job_description")) {
|
// if (message.actions && message.actions.includes("job_description")) {
|
||||||
if (jobConversationRef.current) {
|
// if (jobConversationRef.current) {
|
||||||
await jobConversationRef.current.fetchHistory();
|
// await jobConversationRef.current.fetchHistory();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
if (message.actions && message.actions.includes("resume_generated")) {
|
// if (message.actions && message.actions.includes("resume_generated")) {
|
||||||
if (resumeConversationRef.current) {
|
// if (resumeConversationRef.current) {
|
||||||
await resumeConversationRef.current.fetchHistory();
|
// await resumeConversationRef.current.fetchHistory();
|
||||||
}
|
// }
|
||||||
setHasResume(true);
|
// setHasResume(true);
|
||||||
setActiveTab(1); // Switch to Resume tab
|
// setActiveTab(1); // Switch to Resume tab
|
||||||
}
|
// }
|
||||||
if (message.actions && message.actions.includes("facts_checked")) {
|
// if (message.actions && message.actions.includes("facts_checked")) {
|
||||||
if (factsConversationRef.current) {
|
// if (factsConversationRef.current) {
|
||||||
await factsConversationRef.current.fetchHistory();
|
// await factsConversationRef.current.fetchHistory();
|
||||||
}
|
// }
|
||||||
setHasFacts(true);
|
// setHasFacts(true);
|
||||||
}
|
// }
|
||||||
}, [setHasFacts, setHasResume, setActiveTab]);
|
}, [setHasFacts, setHasResume, setActiveTab]);
|
||||||
|
|
||||||
const resumeResponse = useCallback((message: BackstoryMessage): void => {
|
const resumeResponse = useCallback((message: ChatMessage): void => {
|
||||||
console.log('onResumeResponse', message);
|
console.log('onResumeResponse', message);
|
||||||
setHasFacts(true);
|
setHasFacts(true);
|
||||||
}, [setHasFacts]);
|
}, [setHasFacts]);
|
||||||
|
|
||||||
const factsResponse = useCallback((message: BackstoryMessage): void => {
|
const factsResponse = useCallback((message: ChatMessage): void => {
|
||||||
console.log('onFactsResponse', message);
|
console.log('onFactsResponse', message);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -207,7 +205,7 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPagePro
|
|||||||
// </Box>,
|
// </Box>,
|
||||||
// ];
|
// ];
|
||||||
|
|
||||||
// const jobDescriptionPreamble: MessageList = [{
|
// const jobDescriptionPreamble: ChatMessage[] = [{
|
||||||
// role: 'info',
|
// role: 'info',
|
||||||
// content: `Once you paste a job description and press **Generate Resume**, Backstory will perform the following actions:
|
// content: `Once you paste a job description and press **Generate Resume**, Backstory will perform the following actions:
|
||||||
|
|
||||||
|
@ -26,29 +26,19 @@ import {
|
|||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
interface StreamingOptions {
|
interface StreamingOptions {
|
||||||
onMessage?: (message: Types.ChatMessage) => void;
|
|
||||||
onPartialMessage?: (partialContent: string, messageId?: string) => void;
|
|
||||||
onComplete?: (finalMessage: Types.ChatMessage) => void;
|
|
||||||
onError?: (error: Error) => void;
|
|
||||||
onStatusChange?: (status: Types.ChatStatusType) => void;
|
onStatusChange?: (status: Types.ChatStatusType) => void;
|
||||||
|
onMessage?: (message: Types.ChatMessage) => void;
|
||||||
|
onStreaming?: (chunk: Types.ChatMessageBase) => void;
|
||||||
|
onComplete?: () => void;
|
||||||
|
onError?: (error: string | Types.ChatMessageBase) => void;
|
||||||
|
onWarn?: (warning: string) => void;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StreamingResponse {
|
interface StreamingResponse {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
cancel: () => void;
|
cancel: () => void;
|
||||||
promise: Promise<Types.ChatMessage>;
|
promise: Promise<Types.ChatMessage[]>;
|
||||||
}
|
|
||||||
|
|
||||||
interface ChatMessageChunk {
|
|
||||||
id?: string;
|
|
||||||
sessionId: string;
|
|
||||||
status: Types.ChatStatusType;
|
|
||||||
sender: Types.ChatSenderType;
|
|
||||||
content: string;
|
|
||||||
isPartial?: boolean;
|
|
||||||
timestamp: Date;
|
|
||||||
metadata?: Record<string, any>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
@ -87,7 +77,6 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async logout(accessToken: string, refreshToken: string): Promise<Types.ApiResponse> {
|
async logout(accessToken: string, refreshToken: string): Promise<Types.ApiResponse> {
|
||||||
console.log(this.defaultHeaders);
|
|
||||||
const response = await fetch(`${this.baseUrl}/auth/logout`, {
|
const response = await fetch(`${this.baseUrl}/auth/logout`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: this.defaultHeaders,
|
headers: this.defaultHeaders,
|
||||||
@ -348,10 +337,8 @@ class ApiClient {
|
|||||||
const signal = options.signal || abortController.signal;
|
const signal = options.signal || abortController.signal;
|
||||||
|
|
||||||
let messageId = '';
|
let messageId = '';
|
||||||
let accumulatedContent = '';
|
|
||||||
let currentMessage: Partial<Types.ChatMessage> = {};
|
|
||||||
|
|
||||||
const promise = new Promise<Types.ChatMessage>(async (resolve, reject) => {
|
const promise = new Promise<Types.ChatMessage[]>(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.baseUrl}/chat/sessions/${sessionId}/messages/stream`, {
|
const response = await fetch(`${this.baseUrl}/chat/sessions/${sessionId}/messages/stream`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -374,78 +361,65 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = '';
|
||||||
|
let chatMessage: Types.ChatMessage | null = null;
|
||||||
|
const chatMessageList : Types.ChatMessage[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read();
|
||||||
|
|
||||||
if (done) break;
|
if (done) {
|
||||||
|
// Stream ended naturally - create final message
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
const chunk = decoder.decode(value, { stream: true });
|
buffer += decoder.decode(value, { stream: true });
|
||||||
const lines = chunk.split('\n');
|
|
||||||
|
// Process complete lines
|
||||||
|
const lines = buffer.split('\n');
|
||||||
|
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.trim() === '') continue;
|
if (line.trim() === '') continue; // Skip blank lines between SSEs
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Handle Server-Sent Events format
|
|
||||||
if (line.startsWith('data: ')) {
|
if (line.startsWith('data: ')) {
|
||||||
const data = line.slice(6);
|
const data = line.slice(5).trim();
|
||||||
|
const incoming: Types.ChatMessageBase = JSON.parse(data);
|
||||||
|
|
||||||
if (data === '[DONE]') {
|
// Trigger callbacks based on status
|
||||||
// Stream completed
|
if (incoming.status !== chatMessage?.status) {
|
||||||
const finalMessage: Types.ChatMessage = {
|
options.onStatusChange?.(incoming.status);
|
||||||
id: messageId,
|
}
|
||||||
sessionId,
|
|
||||||
status: 'done',
|
|
||||||
sender: currentMessage.sender || 'ai',
|
|
||||||
content: accumulatedContent,
|
|
||||||
timestamp: currentMessage.timestamp || new Date(),
|
|
||||||
...currentMessage
|
|
||||||
};
|
|
||||||
|
|
||||||
options.onComplete?.(finalMessage);
|
// Handle different status types
|
||||||
resolve(finalMessage);
|
switch (incoming.status) {
|
||||||
return;
|
case 'streaming':
|
||||||
|
if (chatMessage === null) {
|
||||||
|
chatMessage = {...incoming};
|
||||||
|
} else {
|
||||||
|
// Can't do a simple += as typescript thinks .content might not be there
|
||||||
|
chatMessage.content = (chatMessage?.content || '') + incoming.content;
|
||||||
|
}
|
||||||
|
options.onStreaming?.(incoming);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'error':
|
||||||
|
options.onError?.(incoming);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
chatMessageList.push(incoming);
|
||||||
|
options.onMessage?.(incoming);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageChunk: ChatMessageChunk = JSON.parse(data);
|
|
||||||
|
|
||||||
// Update accumulated state
|
|
||||||
if (messageChunk.id) messageId = messageChunk.id;
|
|
||||||
if (messageChunk.content) {
|
|
||||||
accumulatedContent += messageChunk.content;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update current message properties
|
|
||||||
Object.assign(currentMessage, {
|
|
||||||
...messageChunk,
|
|
||||||
content: accumulatedContent
|
|
||||||
});
|
|
||||||
|
|
||||||
// Trigger callbacks
|
|
||||||
if (messageChunk.status) {
|
|
||||||
options.onStatusChange?.(messageChunk.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (messageChunk.isPartial) {
|
|
||||||
options.onPartialMessage?.(messageChunk.content, messageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentCompleteMessage: Types.ChatMessage = {
|
|
||||||
id: messageId,
|
|
||||||
sessionId,
|
|
||||||
status: messageChunk.status,
|
|
||||||
sender: messageChunk.sender,
|
|
||||||
content: accumulatedContent,
|
|
||||||
timestamp: messageChunk.timestamp,
|
|
||||||
...currentMessage
|
|
||||||
};
|
|
||||||
|
|
||||||
options.onMessage?.(currentCompleteMessage);
|
|
||||||
}
|
}
|
||||||
} catch (parseError) {
|
} catch (error) {
|
||||||
console.warn('Failed to parse SSE chunk:', parseError);
|
console.warn('Failed to process SSE:', error);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
options.onWarn?.(error.message);
|
||||||
|
}
|
||||||
// Continue processing other lines
|
// Continue processing other lines
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -454,25 +428,15 @@ class ApiClient {
|
|||||||
reader.releaseLock();
|
reader.releaseLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we get here without a [DONE] signal, create final message
|
options.onComplete?.();
|
||||||
const finalMessage: Types.ChatMessage = {
|
resolve(chatMessageList);
|
||||||
id: messageId || `msg_${Date.now()}`,
|
|
||||||
sessionId,
|
|
||||||
status: 'done',
|
|
||||||
sender: currentMessage.sender || 'ai',
|
|
||||||
content: accumulatedContent,
|
|
||||||
timestamp: currentMessage.timestamp || new Date(),
|
|
||||||
...currentMessage
|
|
||||||
};
|
|
||||||
|
|
||||||
options.onComplete?.(finalMessage);
|
|
||||||
resolve(finalMessage);
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (signal.aborted) {
|
if (signal.aborted) {
|
||||||
|
options.onComplete?.();
|
||||||
reject(new Error('Request was aborted'));
|
reject(new Error('Request was aborted'));
|
||||||
} else {
|
} else {
|
||||||
options.onError?.(error as Error);
|
options.onError?.((error as Error).message);
|
||||||
|
options.onComplete?.();
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -492,15 +456,15 @@ class ApiClient {
|
|||||||
sessionId: string,
|
sessionId: string,
|
||||||
query: Types.ChatQuery,
|
query: Types.ChatQuery,
|
||||||
options?: StreamingOptions
|
options?: StreamingOptions
|
||||||
): Promise<Types.ChatMessage> {
|
): Promise<Types.ChatMessage[]> {
|
||||||
// If streaming options are provided, use streaming
|
// If streaming options are provided, use streaming
|
||||||
if (options && (options.onMessage || options.onPartialMessage || options.onStatusChange)) {
|
if (options && (options.onMessage || options.onStreaming || options.onStatusChange)) {
|
||||||
const streamResponse = this.sendMessageStream(sessionId, query, options);
|
const streamResponse = this.sendMessageStream(sessionId, query, options);
|
||||||
return streamResponse.promise;
|
return streamResponse.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, use standard response
|
// Otherwise, use standard response
|
||||||
return this.sendMessage(sessionId, query);
|
return [await this.sendMessage(sessionId, query)];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChatMessages(sessionId: string, request: Partial<PaginatedRequest> = {}): Promise<PaginatedResponse<Types.ChatMessage>> {
|
async getChatMessages(sessionId: string, request: Partial<PaginatedRequest> = {}): Promise<PaginatedResponse<Types.ChatMessage>> {
|
||||||
@ -737,4 +701,4 @@ await apiClient.sendMessageAuto(sessionId, 'Quick question'); // Will use standa
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export { ApiClient }
|
export { ApiClient }
|
||||||
export type { StreamingOptions, StreamingResponse, ChatMessageChunk };
|
export type { StreamingOptions, StreamingResponse };
|
@ -1,6 +1,6 @@
|
|||||||
// Generated TypeScript types from Pydantic models
|
// Generated TypeScript types from Pydantic models
|
||||||
// Source: src/backend/models.py
|
// Source: src/backend/models.py
|
||||||
// Generated on: 2025-05-29T05:47:25.809967
|
// Generated on: 2025-05-29T21:15:06.572082
|
||||||
// DO NOT EDIT MANUALLY - This file is auto-generated
|
// DO NOT EDIT MANUALLY - This file is auto-generated
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
@ -15,9 +15,11 @@ export type ApplicationStatus = "applied" | "reviewing" | "interview" | "offer"
|
|||||||
|
|
||||||
export type ChatContextType = "job_search" | "candidate_screening" | "interview_prep" | "resume_review" | "general" | "generate_persona" | "generate_profile";
|
export type ChatContextType = "job_search" | "candidate_screening" | "interview_prep" | "resume_review" | "general" | "generate_persona" | "generate_profile";
|
||||||
|
|
||||||
export type ChatSenderType = "user" | "ai" | "system";
|
export type ChatMessageType = "error" | "generating" | "info" | "preparing" | "processing" | "response" | "searching" | "system" | "thinking" | "tooling" | "user";
|
||||||
|
|
||||||
export type ChatStatusType = "preparing" | "thinking" | "partial" | "streaming" | "done" | "error";
|
export type ChatSenderType = "user" | "assistant" | "system";
|
||||||
|
|
||||||
|
export type ChatStatusType = "initializing" | "streaming" | "done" | "error";
|
||||||
|
|
||||||
export type ColorBlindMode = "protanopia" | "deuteranopia" | "tritanopia" | "none";
|
export type ColorBlindMode = "protanopia" | "deuteranopia" | "tritanopia" | "none";
|
||||||
|
|
||||||
@ -231,17 +233,26 @@ export interface ChatContext {
|
|||||||
export interface ChatMessage {
|
export interface ChatMessage {
|
||||||
id?: string;
|
id?: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
status: "preparing" | "thinking" | "partial" | "streaming" | "done" | "error";
|
|
||||||
sender: "user" | "ai" | "system";
|
|
||||||
senderId?: string;
|
senderId?: string;
|
||||||
prompt?: string;
|
status: "initializing" | "streaming" | "done" | "error";
|
||||||
content?: string;
|
type: "error" | "generating" | "info" | "preparing" | "processing" | "response" | "searching" | "system" | "thinking" | "tooling" | "user";
|
||||||
chunk?: string;
|
sender: "user" | "assistant" | "system";
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
isEdited?: boolean;
|
content?: string;
|
||||||
metadata?: ChatMessageMetaData;
|
metadata?: ChatMessageMetaData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChatMessageBase {
|
||||||
|
id?: string;
|
||||||
|
sessionId: string;
|
||||||
|
senderId?: string;
|
||||||
|
status: "initializing" | "streaming" | "done" | "error";
|
||||||
|
type: "error" | "generating" | "info" | "preparing" | "processing" | "response" | "searching" | "system" | "thinking" | "tooling" | "user";
|
||||||
|
sender: "user" | "assistant" | "system";
|
||||||
|
timestamp: Date;
|
||||||
|
content?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ChatMessageMetaData {
|
export interface ChatMessageMetaData {
|
||||||
model?: "qwen2.5" | "flux-schnell";
|
model?: "qwen2.5" | "flux-schnell";
|
||||||
temperature?: number;
|
temperature?: number;
|
||||||
@ -261,6 +272,17 @@ export interface ChatMessageMetaData {
|
|||||||
timers?: Record<string, number>;
|
timers?: Record<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChatMessageUser {
|
||||||
|
id?: string;
|
||||||
|
sessionId: string;
|
||||||
|
senderId?: string;
|
||||||
|
status: "initializing" | "streaming" | "done" | "error";
|
||||||
|
type?: "error" | "generating" | "info" | "preparing" | "processing" | "response" | "searching" | "system" | "thinking" | "tooling" | "user";
|
||||||
|
sender: "user" | "assistant" | "system";
|
||||||
|
timestamp: Date;
|
||||||
|
content?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ChatOptions {
|
export interface ChatOptions {
|
||||||
seed?: number;
|
seed?: number;
|
||||||
numCtx?: number;
|
numCtx?: number;
|
||||||
|
@ -20,8 +20,9 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"baseUrl": "src",
|
"baseUrl": "src",
|
||||||
|
"sourceMap": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*"
|
"src/**/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -21,12 +21,13 @@ import asyncio
|
|||||||
from datetime import datetime, UTC
|
from datetime import datetime, UTC
|
||||||
from prometheus_client import Counter, Summary, CollectorRegistry # type: ignore
|
from prometheus_client import Counter, Summary, CollectorRegistry # type: ignore
|
||||||
|
|
||||||
from models import ( ChatQuery, ChatMessage, Tunables, ChatStatusType, ChatMessageMetaData)
|
from models import ( ChatQuery, ChatMessage, ChatOptions, ChatMessageBase, ChatMessageUser, Tunables, ChatMessageType, ChatSenderType, ChatStatusType, ChatMessageMetaData)
|
||||||
from logger import logger
|
from logger import logger
|
||||||
import defines
|
import defines
|
||||||
from .registry import agent_registry
|
from .registry import agent_registry
|
||||||
from metrics import Metrics
|
from metrics import Metrics
|
||||||
from database import RedisDatabase # type: ignore
|
from database import RedisDatabase # type: ignore
|
||||||
|
import model_cast
|
||||||
|
|
||||||
class LLMMessage(BaseModel):
|
class LLMMessage(BaseModel):
|
||||||
role: str = Field(default="")
|
role: str = Field(default="")
|
||||||
@ -342,15 +343,25 @@ class Agent(BaseModel, ABC):
|
|||||||
|
|
||||||
async def generate(
|
async def generate(
|
||||||
self, llm: Any, model: str, query: ChatQuery, session_id: str, user_id: str, temperature=0.7
|
self, llm: Any, model: str, query: ChatQuery, session_id: str, user_id: str, temperature=0.7
|
||||||
) -> AsyncGenerator[ChatMessage, None]:
|
) -> AsyncGenerator[ChatMessage | ChatMessageBase, None]:
|
||||||
logger.info(f"{self.agent_type} - {inspect.stack()[0].function}")
|
logger.info(f"{self.agent_type} - {inspect.stack()[0].function}")
|
||||||
|
|
||||||
|
user_message = ChatMessageUser(
|
||||||
|
session_id=session_id,
|
||||||
|
tunables=query.tunables,
|
||||||
|
type=ChatMessageType.USER,
|
||||||
|
status=ChatStatusType.DONE,
|
||||||
|
sender=ChatSenderType.USER,
|
||||||
|
content=query.prompt.strip(),
|
||||||
|
timestamp=datetime.now(UTC)
|
||||||
|
)
|
||||||
|
|
||||||
chat_message = ChatMessage(
|
chat_message = ChatMessage(
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
prompt=query.prompt,
|
|
||||||
tunables=query.tunables,
|
tunables=query.tunables,
|
||||||
status=ChatStatusType.PREPARING,
|
status=ChatStatusType.INITIALIZING,
|
||||||
sender="user",
|
type=ChatMessageType.PREPARING,
|
||||||
|
sender=ChatSenderType.ASSISTANT,
|
||||||
content="",
|
content="",
|
||||||
timestamp=datetime.now(UTC)
|
timestamp=datetime.now(UTC)
|
||||||
)
|
)
|
||||||
@ -361,28 +372,22 @@ class Agent(BaseModel, ABC):
|
|||||||
messages: List[LLMMessage] = [
|
messages: List[LLMMessage] = [
|
||||||
LLMMessage(role="system", content=self.system_prompt)
|
LLMMessage(role="system", content=self.system_prompt)
|
||||||
]
|
]
|
||||||
messages.extend(
|
messages.extend([
|
||||||
[
|
LLMMessage(role=m.sender, content=m.content.strip())
|
||||||
item
|
for m in self.conversation
|
||||||
for m in self.conversation
|
])
|
||||||
for item in [
|
|
||||||
LLMMessage(role="user", content=m.prompt.strip() if m.prompt else ""),
|
|
||||||
LLMMessage(role="assistant", content=m.response.strip()),
|
|
||||||
]
|
|
||||||
]
|
|
||||||
)
|
|
||||||
# Only the actual user query is provided with the full context message
|
# Only the actual user query is provided with the full context message
|
||||||
messages.append(
|
messages.append(
|
||||||
LLMMessage(role="user", content=query.prompt.strip())
|
LLMMessage(role=user_message.sender, content=user_message.content.strip())
|
||||||
)
|
)
|
||||||
|
|
||||||
# message.messages = messages
|
# message.messages = messages
|
||||||
chat_message.metadata = ChatMessageMetaData()
|
chat_message.metadata = ChatMessageMetaData()
|
||||||
chat_message.metadata.options = {
|
chat_message.metadata.options = ChatOptions(
|
||||||
"seed": 8911,
|
seed=8911,
|
||||||
"num_ctx": self.context_size,
|
num_ctx=self.context_size,
|
||||||
"temperature": temperature, # Higher temperature to encourage tool usage
|
temperature=temperature, # Higher temperature to encourage tool usage
|
||||||
}
|
)
|
||||||
|
|
||||||
# Create a dict for storing various timing stats
|
# Create a dict for storing various timing stats
|
||||||
chat_message.metadata.timers = {}
|
chat_message.metadata.timers = {}
|
||||||
@ -488,17 +493,21 @@ class Agent(BaseModel, ABC):
|
|||||||
# return
|
# return
|
||||||
|
|
||||||
# not use_tools
|
# not use_tools
|
||||||
chat_message.status = ChatStatusType.THINKING
|
chat_message.type = ChatMessageType.THINKING
|
||||||
chat_message.content = f"Generating response..."
|
chat_message.content = f"Generating response..."
|
||||||
yield chat_message
|
yield chat_message
|
||||||
|
|
||||||
# Reset the response for streaming
|
# Reset the response for streaming
|
||||||
chat_message.content = ""
|
chat_message.content = ""
|
||||||
start_time = time.perf_counter()
|
start_time = time.perf_counter()
|
||||||
|
chat_message.type = ChatMessageType.GENERATING
|
||||||
|
chat_message.status = ChatStatusType.STREAMING
|
||||||
|
|
||||||
for response in llm.chat(
|
for response in llm.chat(
|
||||||
model=model,
|
model=model,
|
||||||
messages=messages,
|
messages=messages,
|
||||||
options={
|
options={
|
||||||
**chat_message.metadata.options,
|
**chat_message.metadata.model_dump(exclude_unset=True),
|
||||||
},
|
},
|
||||||
stream=True,
|
stream=True,
|
||||||
):
|
):
|
||||||
@ -508,12 +517,13 @@ class Agent(BaseModel, ABC):
|
|||||||
yield chat_message
|
yield chat_message
|
||||||
return
|
return
|
||||||
|
|
||||||
chat_message.status = ChatStatusType.STREAMING
|
chat_message.content += response.message.content
|
||||||
chat_message.chunk = response.message.content
|
|
||||||
chat_message.content += chat_message.chunk
|
|
||||||
|
|
||||||
if not response.done:
|
if not response.done:
|
||||||
|
chat_chunk = model_cast.cast_to_model(ChatMessageBase, chat_message)
|
||||||
|
chat_chunk.content = response.message.content
|
||||||
yield chat_message
|
yield chat_message
|
||||||
|
continue
|
||||||
|
|
||||||
if response.done:
|
if response.done:
|
||||||
self.collect_metrics(response)
|
self.collect_metrics(response)
|
||||||
@ -524,12 +534,15 @@ class Agent(BaseModel, ABC):
|
|||||||
self.context_tokens = (
|
self.context_tokens = (
|
||||||
response.prompt_eval_count + response.eval_count
|
response.prompt_eval_count + response.eval_count
|
||||||
)
|
)
|
||||||
|
chat_message.type = ChatMessageType.RESPONSE
|
||||||
chat_message.status = ChatStatusType.DONE
|
chat_message.status = ChatStatusType.DONE
|
||||||
yield chat_message
|
yield chat_message
|
||||||
|
|
||||||
end_time = time.perf_counter()
|
end_time = time.perf_counter()
|
||||||
chat_message.metadata.timers["streamed"] = end_time - start_time
|
chat_message.metadata.timers["streamed"] = end_time - start_time
|
||||||
chat_message.status = ChatStatusType.DONE
|
|
||||||
|
# Add the user and chat messages to the conversation
|
||||||
|
self.conversation.append(user_message)
|
||||||
self.conversation.append(chat_message)
|
self.conversation.append(chat_message)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -31,12 +31,13 @@ from models import (
|
|||||||
Job, JobApplication, ApplicationStatus,
|
Job, JobApplication, ApplicationStatus,
|
||||||
|
|
||||||
# Chat models
|
# Chat models
|
||||||
ChatSession, ChatMessage, ChatContext, ChatQuery,
|
ChatSession, ChatMessage, ChatContext, ChatQuery, ChatStatusType, ChatMessageBase,
|
||||||
|
|
||||||
# Supporting models
|
# Supporting models
|
||||||
Location, Skill, WorkExperience, Education
|
Location, Skill, WorkExperience, Education
|
||||||
)
|
)
|
||||||
|
|
||||||
|
import model_cast
|
||||||
import defines
|
import defines
|
||||||
import agents
|
import agents
|
||||||
from logger import logger
|
from logger import logger
|
||||||
@ -343,7 +344,7 @@ async def login(
|
|||||||
expiresAt=int((datetime.now(UTC) + timedelta(hours=24)).timestamp())
|
expiresAt=int((datetime.now(UTC) + timedelta(hours=24)).timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return create_success_response(auth_response.model_dump(by_alias=True))
|
return create_success_response(auth_response.model_dump(by_alias=True, exclude_unset=True))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"⚠️ Login error: {e}")
|
logger.error(f"⚠️ Login error: {e}")
|
||||||
@ -531,7 +532,7 @@ async def refresh_token_endpoint(
|
|||||||
expiresAt=int((datetime.now(UTC) + timedelta(hours=24)).timestamp())
|
expiresAt=int((datetime.now(UTC) + timedelta(hours=24)).timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return create_success_response(auth_response.model_dump(by_alias=True))
|
return create_success_response(auth_response.model_dump(by_alias=True, exclude_unset=True))
|
||||||
|
|
||||||
except jwt.PyJWTError:
|
except jwt.PyJWTError:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@ -578,7 +579,7 @@ async def create_candidate(
|
|||||||
"type": "candidate"
|
"type": "candidate"
|
||||||
})
|
})
|
||||||
|
|
||||||
return create_success_response(candidate.model_dump(by_alias=True))
|
return create_success_response(candidate.model_dump(by_alias=True, exclude_unset=True))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Candidate creation error: {e}")
|
logger.error(f"Candidate creation error: {e}")
|
||||||
@ -614,7 +615,7 @@ async def get_candidate(
|
|||||||
)
|
)
|
||||||
|
|
||||||
candidate = Candidate.model_validate(candidates_list[0])
|
candidate = Candidate.model_validate(candidates_list[0])
|
||||||
return create_success_response(candidate.model_dump(by_alias=True))
|
return create_success_response(candidate.model_dump(by_alias=True, exclude_unset=True))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Get candidate error: {e}")
|
logger.error(f"Get candidate error: {e}")
|
||||||
@ -656,7 +657,7 @@ async def update_candidate(
|
|||||||
updated_candidate = Candidate.model_validate(candidate_dict)
|
updated_candidate = Candidate.model_validate(candidate_dict)
|
||||||
await database.set_candidate(candidate_id, updated_candidate.model_dump())
|
await database.set_candidate(candidate_id, updated_candidate.model_dump())
|
||||||
|
|
||||||
return create_success_response(updated_candidate.model_dump(by_alias=True))
|
return create_success_response(updated_candidate.model_dump(by_alias=True, exclude_unset=True))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Update candidate error: {e}")
|
logger.error(f"Update candidate error: {e}")
|
||||||
@ -690,7 +691,7 @@ async def get_candidates(
|
|||||||
)
|
)
|
||||||
|
|
||||||
paginated_response = create_paginated_response(
|
paginated_response = create_paginated_response(
|
||||||
[c.model_dump(by_alias=True) for c in paginated_candidates],
|
[c.model_dump(by_alias=True, exclude_unset=True) for c in paginated_candidates],
|
||||||
page, limit, total
|
page, limit, total
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -739,7 +740,7 @@ async def search_candidates(
|
|||||||
)
|
)
|
||||||
|
|
||||||
paginated_response = create_paginated_response(
|
paginated_response = create_paginated_response(
|
||||||
[c.model_dump(by_alias=True) for c in paginated_candidates],
|
[c.model_dump(by_alias=True, exclude_unset=True) for c in paginated_candidates],
|
||||||
page, limit, total
|
page, limit, total
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -781,7 +782,7 @@ async def create_job(
|
|||||||
job = Job.model_validate(job_data)
|
job = Job.model_validate(job_data)
|
||||||
await database.set_job(job.id, job.model_dump())
|
await database.set_job(job.id, job.model_dump())
|
||||||
|
|
||||||
return create_success_response(job.model_dump(by_alias=True))
|
return create_success_response(job.model_dump(by_alias=True, exclude_unset=True))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Job creation error: {e}")
|
logger.error(f"Job creation error: {e}")
|
||||||
@ -809,7 +810,7 @@ async def get_job(
|
|||||||
await database.set_job(job_id, job_data)
|
await database.set_job(job_id, job_data)
|
||||||
|
|
||||||
job = Job.model_validate(job_data)
|
job = Job.model_validate(job_data)
|
||||||
return create_success_response(job.model_dump(by_alias=True))
|
return create_success_response(job.model_dump(by_alias=True, exclude_unset=True))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Get job error: {e}")
|
logger.error(f"Get job error: {e}")
|
||||||
@ -842,7 +843,7 @@ async def get_jobs(
|
|||||||
)
|
)
|
||||||
|
|
||||||
paginated_response = create_paginated_response(
|
paginated_response = create_paginated_response(
|
||||||
[j.model_dump(by_alias=True) for j in paginated_jobs],
|
[j.model_dump(by_alias=True, exclude_unset=True) for j in paginated_jobs],
|
||||||
page, limit, total
|
page, limit, total
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -887,7 +888,7 @@ async def search_jobs(
|
|||||||
)
|
)
|
||||||
|
|
||||||
paginated_response = create_paginated_response(
|
paginated_response = create_paginated_response(
|
||||||
[j.model_dump(by_alias=True) for j in paginated_jobs],
|
[j.model_dump(by_alias=True, exclude_unset=True) for j in paginated_jobs],
|
||||||
page, limit, total
|
page, limit, total
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -921,7 +922,7 @@ async def create_chat_session(
|
|||||||
await database.set_chat_session(chat_session.id, chat_session.model_dump())
|
await database.set_chat_session(chat_session.id, chat_session.model_dump())
|
||||||
|
|
||||||
logger.info(f"✅ Chat session created: {chat_session.id} for user {current_user.id}")
|
logger.info(f"✅ Chat session created: {chat_session.id} for user {current_user.id}")
|
||||||
return create_success_response(chat_session.model_dump(by_alias=True))
|
return create_success_response(chat_session.model_dump(by_alias=True, exclude_unset=True))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Chat session creation error: {e}")
|
logger.error(f"Chat session creation error: {e}")
|
||||||
@ -946,7 +947,7 @@ async def get_chat_session(
|
|||||||
)
|
)
|
||||||
|
|
||||||
chat_session = ChatSession.model_validate(chat_session_data)
|
chat_session = ChatSession.model_validate(chat_session_data)
|
||||||
return create_success_response(chat_session.model_dump(by_alias=True))
|
return create_success_response(chat_session.model_dump(by_alias=True, exclude_unset=True))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Get chat session error: {e}")
|
logger.error(f"Get chat session error: {e}")
|
||||||
@ -986,7 +987,7 @@ async def get_chat_session_messages(
|
|||||||
messages_list, page, limit, sortBy, sortOrder, filter_dict
|
messages_list, page, limit, sortBy, sortOrder, filter_dict
|
||||||
)
|
)
|
||||||
paginated_response = create_paginated_response(
|
paginated_response = create_paginated_response(
|
||||||
[m.model_dump(by_alias=True) for m in paginated_messages],
|
[m.model_dump(by_alias=True, exclude_unset=True) for m in paginated_messages],
|
||||||
page, limit, total
|
page, limit, total
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1034,27 +1035,37 @@ async def post_chat_session_message_stream(
|
|||||||
)
|
)
|
||||||
async def message_stream_generator():
|
async def message_stream_generator():
|
||||||
"""Generator to stream messages"""
|
"""Generator to stream messages"""
|
||||||
async for message in chat_agent.generate(
|
last_log = None
|
||||||
|
async for chat_message in chat_agent.generate(
|
||||||
llm=llm_manager.get_llm(),
|
llm=llm_manager.get_llm(),
|
||||||
model=defines.model,
|
model=defines.model,
|
||||||
query=chat_query,
|
query=chat_query,
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
):
|
):
|
||||||
json_data = message.model_dump(mode='json', by_alias=True)
|
# If the message is not done, convert it to a ChatMessageBase to remove
|
||||||
|
# metadata and other unnecessary fields
|
||||||
|
if chat_message.status != ChatStatusType.DONE:
|
||||||
|
chat_message = model_cast.cast_to_model(ChatMessageBase, chat_message)
|
||||||
|
|
||||||
|
json_data = chat_message.model_dump(mode='json', by_alias=True, exclude_unset=True)
|
||||||
json_str = json.dumps(json_data)
|
json_str = json.dumps(json_data)
|
||||||
logger.info(f"🔗 Streaming message for session {session_id}: {json_str}")
|
log = f"🔗 Message status={chat_message.status}, type={chat_message.type}"
|
||||||
yield json_str + "\n"
|
if last_log != log:
|
||||||
|
last_log = log
|
||||||
|
logger.info(log)
|
||||||
|
yield f"data: {json_str}\n\n"
|
||||||
|
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
message_stream_generator(),
|
message_stream_generator(),
|
||||||
media_type="application/json",
|
media_type="text/event-stream",
|
||||||
headers={
|
headers={
|
||||||
"Cache-Control": "no-cache",
|
"Cache-Control": "no-cache",
|
||||||
"Connection": "keep-alive",
|
"Connection": "keep-alive",
|
||||||
"X-Accel-Buffering": "no", # Prevents Nginx buffering if you're using it
|
#"Access-Control-Allow-Origin": "*", # CORS
|
||||||
},
|
"X-Accel-Buffering": "no", # Prevents Nginx buffering if you're using it
|
||||||
)
|
},
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
@ -1090,7 +1101,7 @@ async def get_chat_sessions(
|
|||||||
)
|
)
|
||||||
|
|
||||||
paginated_response = create_paginated_response(
|
paginated_response = create_paginated_response(
|
||||||
[s.model_dump(by_alias=True) for s in paginated_sessions],
|
[s.model_dump(by_alias=True, exclude_unset=True) for s in paginated_sessions],
|
||||||
page, limit, total
|
page, limit, total
|
||||||
)
|
)
|
||||||
|
|
||||||
|
14
src/backend/model_cast.py
Normal file
14
src/backend/model_cast.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from typing import Type, TypeVar
|
||||||
|
from pydantic import BaseModel # type: ignore
|
||||||
|
import copy
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar('T', bound=BaseModel)
|
||||||
|
|
||||||
|
def cast_to_model(model_cls: Type[T], source: BaseModel) -> T:
|
||||||
|
data = {field: getattr(source, field) for field in model_cls.__fields__}
|
||||||
|
return model_cls(**data)
|
||||||
|
|
||||||
|
def cast_to_model_safe(model_cls: Type[T], source: BaseModel) -> T:
|
||||||
|
data = {field: copy.deepcopy(getattr(source, field)) for field in model_cls.__fields__}
|
||||||
|
return model_cls(**data)
|
@ -64,13 +64,24 @@ class InterviewRecommendation(str, Enum):
|
|||||||
|
|
||||||
class ChatSenderType(str, Enum):
|
class ChatSenderType(str, Enum):
|
||||||
USER = "user"
|
USER = "user"
|
||||||
AI = "ai"
|
ASSISTANT = "assistant"
|
||||||
SYSTEM = "system"
|
SYSTEM = "system"
|
||||||
|
|
||||||
class ChatStatusType(str, Enum):
|
class ChatMessageType(str, Enum):
|
||||||
|
ERROR = "error"
|
||||||
|
GENERATING = "generating"
|
||||||
|
INFO = "info"
|
||||||
PREPARING = "preparing"
|
PREPARING = "preparing"
|
||||||
|
PROCESSING = "processing"
|
||||||
|
RESPONSE = "response"
|
||||||
|
SEARCHING = "searching"
|
||||||
|
SYSTEM = "system"
|
||||||
THINKING = "thinking"
|
THINKING = "thinking"
|
||||||
PARTIAL = "partial"
|
TOOLING = "tooling"
|
||||||
|
USER = "user"
|
||||||
|
|
||||||
|
class ChatStatusType(str, Enum):
|
||||||
|
INITIALIZING = "initializing"
|
||||||
STREAMING = "streaming"
|
STREAMING = "streaming"
|
||||||
DONE = "done"
|
DONE = "done"
|
||||||
ERROR = "error"
|
ERROR = "error"
|
||||||
@ -572,24 +583,28 @@ class ChatMessageMetaData(BaseModel):
|
|||||||
class Config:
|
class Config:
|
||||||
populate_by_name = True # Allow both field names and aliases
|
populate_by_name = True # Allow both field names and aliases
|
||||||
|
|
||||||
class ChatMessage(BaseModel):
|
class ChatMessageBase(BaseModel):
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
||||||
session_id: str = Field(..., alias="sessionId")
|
session_id: str = Field(..., alias="sessionId")
|
||||||
status: ChatStatusType
|
|
||||||
sender: ChatSenderType
|
|
||||||
sender_id: Optional[str] = Field(None, alias="senderId")
|
sender_id: Optional[str] = Field(None, alias="senderId")
|
||||||
prompt: str = ""
|
status: ChatStatusType
|
||||||
content: str = ""
|
type: ChatMessageType
|
||||||
chunk: str = ""
|
sender: ChatSenderType
|
||||||
timestamp: datetime
|
timestamp: datetime
|
||||||
#attachments: Optional[List[Attachment]] = None
|
content: str = ""
|
||||||
#reactions: Optional[List[MessageReaction]] = None
|
|
||||||
is_edited: bool = Field(False, alias="isEdited")
|
|
||||||
#edit_history: Optional[List[EditHistory]] = Field(None, alias="editHistory")
|
|
||||||
metadata: ChatMessageMetaData = Field(None)
|
|
||||||
class Config:
|
class Config:
|
||||||
populate_by_name = True # Allow both field names and aliases
|
populate_by_name = True # Allow both field names and aliases
|
||||||
|
|
||||||
|
class ChatMessageUser(ChatMessageBase):
|
||||||
|
type: ChatMessageType = ChatMessageType.USER
|
||||||
|
|
||||||
|
class ChatMessage(ChatMessageBase):
|
||||||
|
#attachments: Optional[List[Attachment]] = None
|
||||||
|
#reactions: Optional[List[MessageReaction]] = None
|
||||||
|
#is_edited: bool = Field(False, alias="isEdited")
|
||||||
|
#edit_history: Optional[List[EditHistory]] = Field(None, alias="editHistory")
|
||||||
|
metadata: ChatMessageMetaData = Field(None)
|
||||||
|
|
||||||
class ChatSession(BaseModel):
|
class ChatSession(BaseModel):
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
||||||
user_id: Optional[str] = Field(None, alias="userId")
|
user_id: Optional[str] = Field(None, alias="userId")
|
||||||
|
@ -1,19 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Ensure input was provided
|
get_pid() {
|
||||||
if [[ -z "$1" ]]; then
|
|
||||||
TARGET=$(readlink -f "src/server.py")
|
|
||||||
else
|
|
||||||
TARGET=$(readlink -f "$1")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Resolve user-supplied path to absolute path
|
|
||||||
|
|
||||||
if [[ ! -f "$TARGET" ]]; then
|
|
||||||
echo "Target file '$TARGET' not found."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Loop through python processes and resolve each script path
|
# Loop through python processes and resolve each script path
|
||||||
PID=""
|
PID=""
|
||||||
for pid in $(pgrep -f python); do
|
for pid in $(pgrep -f python); do
|
||||||
@ -32,9 +19,33 @@ for pid in $(pgrep -f python); do
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -z "$1" ]]; then
|
||||||
|
for file in "src/server.py" "src/backend/main.py"; do
|
||||||
|
echo "Checking ${file}"
|
||||||
|
# Ensure input was provided
|
||||||
|
TARGET=$(readlink -f "$file")
|
||||||
|
if [[ ! -f "$TARGET" ]]; then
|
||||||
|
echo "Target file '$TARGET' not found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
get_pid
|
||||||
|
if [[ "${PID}" != "" ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
TARGET=$(readlink -f "$1")
|
||||||
|
if [[ ! -f "$TARGET" ]]; then
|
||||||
|
echo "Target file '$TARGET' not found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
get_pid
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -z "$PID" ]]; then
|
if [[ -z "$PID" ]]; then
|
||||||
echo "No Python process found running '$TARGET'."
|
echo "No Python process found running."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user