Plumbed cache through agents

Removed some dead frontend code
This commit is contained in:
James Ketr 2025-07-10 15:04:34 -07:00
parent 2fe1f5b181
commit 80b63fe0e1
17 changed files with 145 additions and 675 deletions

View File

@ -1,41 +1,8 @@
import React, {
useState,
useImperativeHandle,
forwardRef,
useEffect,
useRef,
useCallback,
} from 'react';
import Tooltip from '@mui/material/Tooltip';
import IconButton from '@mui/material/IconButton';
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';
import SendIcon from '@mui/icons-material/Send';
import CancelIcon from '@mui/icons-material/Cancel';
import React from 'react';
import { SxProps, Theme } from '@mui/material';
import PropagateLoader from 'react-spinners/PropagateLoader';
import { Message } from './Message';
import { DeleteConfirmation } from 'components/DeleteConfirmation';
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
import { BackstoryElementProps } from './BackstoryTab';
import { useAuth } from 'hooks/AuthContext';
import { StreamingResponse } from 'services/api-client';
import {
ChatMessage,
ChatContext,
ChatSession,
ChatQuery,
ChatMessageUser,
ChatMessageError,
ChatMessageStreaming,
ChatMessageStatus,
ChatMessageMetaData,
} from 'types/types';
import { PaginatedResponse } from 'types/conversion';
import './Conversation.css';
import { useAppState } from 'hooks/GlobalContext';
import { ChatMessage, ChatQuery, ChatMessageMetaData } from 'types/types';
const defaultMessage: ChatMessage = {
status: 'done',
@ -47,11 +14,6 @@ const defaultMessage: ChatMessage = {
metadata: null as unknown as ChatMessageMetaData,
};
const loadingMessage: ChatMessage = {
...defaultMessage,
content: 'Establishing connection with server...',
};
type ConversationMode = 'chat' | 'job_description' | 'resume' | 'fact_check' | 'persona';
interface ConversationHandle {
@ -77,417 +39,4 @@ interface ConversationProps extends BackstoryElementProps {
onResponse?: ((message: ChatMessage) => void) | undefined; // Event called when a query completes (provides messages)
}
const Conversation = forwardRef<ConversationHandle, ConversationProps>(
(props: ConversationProps, ref) => {
const {
actionLabel,
defaultPrompts,
hideDefaultPrompts,
hidePreamble,
messageFilter,
messages,
onResponse,
placeholder,
preamble,
resetLabel,
sx,
} = props;
const { apiClient } = useAuth();
const [processing, setProcessing] = useState<boolean>(false);
const [conversation, setConversation] = useState<ChatMessage[]>([]);
const conversationRef = useRef<ChatMessage[]>([]);
const [filteredConversation, setFilteredConversation] = useState<ChatMessage[]>([]);
const [processingMessage, setProcessingMessage] = useState<ChatMessage | undefined>(undefined);
const [streamingMessage, setStreamingMessage] = useState<ChatMessage | undefined>(undefined);
const [noInteractions, setNoInteractions] = useState<boolean>(true);
const viewableElementRef = useRef<HTMLDivElement>(null);
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
const stopRef = useRef(false);
const controllerRef = useRef<StreamingResponse>(null);
const [chatSession, setChatSession] = useState<ChatSession | null>(null);
const { setSnack } = useAppState();
// Keep the ref updated whenever items changes
useEffect(() => {
conversationRef.current = conversation;
}, [conversation]);
// Update the context status
/* Transform the 'Conversation' by filtering via callback, then adding
* preamble and messages based on whether the conversation
* has any elements yet */
useEffect(() => {
let filtered = [];
if (messageFilter === undefined) {
filtered = conversation;
// console.log('No message filter provided. Using all messages.', filtered);
} else {
//console.log('Filtering conversation...')
filtered =
messageFilter(conversation); /* Do not copy conversation or useEffect will loop forever */
//console.log(`${conversation.length - filtered.length} messages filtered out.`);
}
if (filtered.length === 0) {
setFilteredConversation([...(preamble || []), ...(messages || [])]);
} else {
setFilteredConversation([
...(hidePreamble ? [] : preamble || []),
...(messages || []),
...filtered,
]);
}
}, [conversation, setFilteredConversation, messageFilter, preamble, messages, hidePreamble]);
useEffect(() => {
if (chatSession) {
return;
}
const createChatSession = async (): Promise<void> => {
try {
const chatContext: ChatContext = { type: 'general' };
const response: ChatSession = await apiClient.createChatSession(chatContext);
setChatSession(response);
} catch (e) {
console.error(e);
setSnack('Unable to create chat session.', 'error');
}
};
createChatSession();
}, [chatSession, setChatSession, apiClient, setSnack]);
const getChatMessages = useCallback(async () => {
if (!chatSession || !chatSession.id) {
return;
}
try {
const response: PaginatedResponse<ChatMessage> = await apiClient.getChatMessages(
chatSession.id
);
const messages: ChatMessage[] = response.data;
setProcessingMessage(undefined);
setStreamingMessage(undefined);
if (messages.length === 0) {
console.log(`History returned with 0 entries`);
setConversation([]);
setNoInteractions(true);
} else {
console.log(`History returned with ${messages.length} entries:`, messages);
setConversation(messages);
setNoInteractions(false);
}
} catch (error) {
console.error('Unable to obtain chat history', error);
setProcessingMessage({
...defaultMessage,
status: 'error',
content: `Unable to obtain history from server.`,
});
setTimeout(() => {
setProcessingMessage(undefined);
setNoInteractions(true);
}, 3000);
setSnack('Unable to obtain chat history.', 'error');
}
}, [chatSession, apiClient, setSnack]);
// Set the initial chat history to "loading" or the welcome message if loaded.
useEffect(() => {
if (!chatSession) {
setProcessingMessage(loadingMessage);
return;
}
setProcessingMessage(undefined);
setStreamingMessage(undefined);
setConversation([]);
setNoInteractions(true);
getChatMessages();
}, [chatSession, getChatMessages]);
const handleEnter = (value: string): void => {
const query: ChatQuery = {
prompt: value,
};
processQuery(query);
};
useImperativeHandle(ref, () => ({
submitQuery: (query: ChatQuery): void => {
processQuery(query);
},
fetchHistory: (): void => {
getChatMessages();
},
}));
// const reset = async () => {
// try {
// const response = await fetch(connectionBase + `/api/reset/${sessionId}/${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');
// }
// setProcessingMessage(undefined);
// setStreamingMessage(undefined);
// setConversation([]);
// setNoInteractions(true);
// } catch (e) {
// setSnack("Error resetting history", "error")
// console.error('Error resetting history:', e);
// }
// };
const cancelQuery = (): void => {
console.log('Stop query');
if (controllerRef.current) {
controllerRef.current.cancel();
}
controllerRef.current = null;
};
const processQuery = (query: ChatQuery): void => {
if (controllerRef.current || !chatSession || !chatSession.id) {
return;
}
setNoInteractions(false);
setConversation([
...conversationRef.current,
{
...defaultMessage,
type: 'text',
content: query.prompt,
},
]);
setProcessing(true);
setProcessingMessage({
...defaultMessage,
content: 'Submitting request...',
});
const chatMessage: ChatMessageUser = {
role: 'user',
sessionId: chatSession.id,
content: query.prompt,
tunables: query.tunables,
status: 'done',
type: 'text',
timestamp: new Date(),
};
controllerRef.current = apiClient.sendMessageStream(chatMessage, {
onMessage: (msg: ChatMessage) => {
console.log('onMessage:', msg);
setConversation([...conversationRef.current, msg]);
setStreamingMessage(undefined);
setProcessingMessage(undefined);
setProcessing(false);
if (onResponse) {
onResponse(msg);
}
},
onError: (error: string | ChatMessageError) => {
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: ChatMessageStreaming) => {
console.log('onStreaming:', chunk);
setStreamingMessage({ ...defaultMessage, ...chunk });
},
onStatus: (status: ChatMessageStatus) => {
console.log('onStatus:', status);
},
onComplete: () => {
console.log('onComplete');
controllerRef.current = null;
},
});
};
if (!chatSession) {
return <></>;
}
return (
// <Scrollable
// className={`${className || ""} Conversation`}
// autoscroll
// textFieldRef={viewableElementRef}
// fallbackThreshold={0.5}
// sx={{
// p: 1,
// mt: 0,
// ...sx
// }}
// >
<Box
className="Conversation"
sx={{
flexGrow: 1,
minHeight: 'max-content',
height: 'max-content',
maxHeight: 'max-content',
overflow: 'hidden',
}}
>
<Box sx={{ p: 1, mt: 0, ...sx }}>
{filteredConversation.map((message, index) => (
<Message key={index} {...{ chatSession, sendQuery: processQuery, message }} />
))}
{processingMessage !== undefined && (
<Message
{...{
chatSession,
sendQuery: processQuery,
message: processingMessage,
}}
/>
)}
{streamingMessage !== undefined && (
<Message
{...{
chatSession,
sendQuery: processQuery,
message: streamingMessage,
}}
/>
)}
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
m: 1,
}}
>
<PropagateLoader
size="10px"
loading={processing}
aria-label="Loading Spinner"
data-testid="loader"
/>
</Box>
<Box
className="Query"
sx={{ display: 'flex', flexDirection: 'column', p: 1, flexGrow: 1 }}
>
{placeholder && (
<Box
sx={{
display: 'flex',
flexGrow: 1,
p: 0,
m: 0,
flexDirection: 'column',
}}
ref={viewableElementRef}
>
<BackstoryTextField
ref={backstoryTextRef}
disabled={processing}
onEnter={handleEnter}
placeholder={placeholder}
/>
</Box>
)}
<Box
key="jobActions"
sx={{
display: 'flex',
justifyContent: 'center',
flexDirection: 'row',
}}
>
<DeleteConfirmation
label={resetLabel || 'all data'}
disabled={!chatSession || processingMessage !== undefined || noInteractions}
onDelete={(): void => {
/*reset(); resetAction && resetAction(); */
}}
/>
<Tooltip title={actionLabel || 'Send'}>
<span style={{ display: 'flex', flexGrow: 1 }}>
<Button
sx={{ m: 1, gap: 1, flexGrow: 1 }}
variant="contained"
disabled={!chatSession || processingMessage !== undefined}
onClick={(): void => {
processQuery({
prompt:
(backstoryTextRef.current &&
backstoryTextRef.current.getAndResetValue()) ||
'',
});
}}
>
{actionLabel}
<SendIcon />
</Button>
</span>
</Tooltip>
<Tooltip title="Cancel">
<span style={{ display: 'flex' }}>
{' '}
{/* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */}
<IconButton
aria-label="cancel"
onClick={(): void => {
cancelQuery();
}}
sx={{ display: 'flex', margin: 'auto 0px' }}
size="large"
edge="start"
disabled={stopRef.current || !chatSession || processing === false}
>
<CancelIcon />
</IconButton>
</span>
</Tooltip>
</Box>
</Box>
{(noInteractions || !hideDefaultPrompts) &&
defaultPrompts !== undefined &&
defaultPrompts.length !== 0 && (
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
{defaultPrompts.map((element, index) => {
return <Box key={index}>{element}</Box>;
})}
</Box>
)}
<Box sx={{ display: 'flex', flexGrow: 1 }}></Box>
</Box>
</Box>
);
}
);
Conversation.displayName = 'Conversation';
export type { ConversationProps, ConversationHandle };
export { Conversation };

View File

@ -1,67 +0,0 @@
import React, { forwardRef, useEffect, useState } from 'react';
import useMediaQuery from '@mui/material/useMediaQuery';
import Box from '@mui/material/Box';
import { useTheme } from '@mui/material/styles';
import MuiMarkdown from 'mui-markdown';
import { BackstoryPageProps } from '../components/BackstoryTab';
import { Conversation, ConversationHandle } from '../components/Conversation';
import { BackstoryQuery } from '../components/BackstoryQuery';
import { CandidateInfo } from 'components/ui/CandidateInfo';
import { useAuth } from 'hooks/AuthContext';
import { Candidate } from 'types/types';
import { useAppState } from 'hooks/GlobalContext';
import * as Types from 'types/types';
const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
(props: BackstoryPageProps, ref) => {
const { setSnack } = useAppState();
const { user } = useAuth();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [questions, setQuestions] = useState<React.ReactElement[]>([]);
const candidate: Candidate | null =
user?.userType === 'candidate' ? (user as Types.Candidate) : null;
// console.log("ChatPage candidate =>", candidate);
useEffect(() => {
if (!candidate) {
return;
}
setQuestions([
<Box sx={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row' }}>
{candidate.questions?.map((q, i: number) => (
<BackstoryQuery key={i} question={q} />
))}
</Box>,
<Box sx={{ p: 1 }}>
<MuiMarkdown>
{`As with all LLM interactions, the results may not be 100% accurate. Please contact **${candidate.fullName}** if you have any questions.`}
</MuiMarkdown>
</Box>,
]);
}, [candidate, isMobile]);
if (!candidate) {
return <></>;
}
return (
<Box>
<CandidateInfo candidate={candidate} action="Chat with Backstory AI about " />
<Conversation
ref={ref}
{...{
multiline: true,
type: 'chat',
placeholder: `What would you like to know about ${candidate?.firstName}?`,
resetLabel: 'chat',
defaultPrompts: questions,
}}
/>
</Box>
);
}
);
export { ChatPage };

View File

@ -1,6 +1,6 @@
// Generated TypeScript types from Pydantic models
// Source: src/backend/models.py
// Generated on: 2025-07-01T22:28:07.615325
// Generated on: 2025-07-09T20:57:47.233946
// DO NOT EDIT MANUALLY - This file is auto-generated
// ============================

View File

@ -236,11 +236,6 @@ class Agent(BaseModel, ABC):
# context_size is shared across all subclasses
_context_size: ClassVar[int] = int(defines.max_context * 0.5)
conversation: List[ChatMessageUser] = Field(
default_factory=list,
description="Conversation history for this agent, used to maintain context across messages.",
)
@property
def context_size(self) -> int:
return Agent._context_size
@ -519,22 +514,30 @@ Content: {content}
chroma_results = await user.file_watcher.find_similar(query=prompt, top_k=top_k, threshold=threshold)
if not chroma_results:
continue
query_embedding = np.array(chroma_results["query_embedding"]).flatten() # type: ignore
umap_2d = user.file_watcher.umap_model_2d.transform([query_embedding])[0] # type: ignore
umap_3d = user.file_watcher.umap_model_3d.transform([query_embedding])[0] # type: ignore
query_embedding = np.array(chroma_results["query_embedding"]).flatten()
umap_2d = []
umap_3d = []
if user.file_watcher.umap_model_2d and user.file_watcher.umap_model_3d:
umap_2d = user.file_watcher.umap_model_2d.transform([query_embedding])[0].tolist() # type: ignore
umap_3d = user.file_watcher.umap_model_3d.transform([query_embedding])[0].tolist() # type: ignore
rag_metadata = ChromaDBGetResponse(
name=rag.name,
query=prompt,
query_embedding=query_embedding.tolist(),
size=user.file_watcher.collection.count(),
ids=chroma_results.get("ids", []),
distances=chroma_results.get("distances", []),
embeddings=chroma_results.get("embeddings", []),
documents=chroma_results.get("documents", []),
metadatas=chroma_results.get("metadatas", []),
umap_embedding_2d=umap_2d.tolist(),
umap_embedding_3d=umap_3d.tolist(),
umap_embedding_2d=umap_2d,
umap_embedding_3d=umap_3d,
)
if rag_metadata.query_embedding:
logger.info(f"Len of embedding: {len(rag_metadata.query_embedding)}")
else:
logger.warning(f"No query embedding found in RAG results; {query_embedding}")
results.append(rag_metadata)
except Exception as e:
continue_message = ChatMessageStatus(
@ -552,6 +555,8 @@ Content: {content}
yield final_message
return
# Send a single message to the LLM and return the response
# This is a one-shot generation method that does not maintain conversation history.
async def llm_one_shot(
self,
llm: Any,
@ -636,7 +641,14 @@ Content: {content}
return
async def generate(
self, llm: Any, model: str, session_id: str, prompt: str, tunables: Optional[Tunables] = None, temperature=0.7
self,
llm: Any,
model: str,
session_id: str,
prompt: str,
database: RedisDatabase,
tunables: Optional[Tunables] = None,
temperature=0.7,
) -> AsyncGenerator[ApiMessage, None]:
if not self.user:
error_message = ChatMessageError(session_id=session_id, content="No user set for chat generation.")
@ -648,11 +660,26 @@ Content: {content}
content=prompt,
)
await database.add_chat_message(session_id, user_message.model_dump())
logger.info(f"💬 User message saved to database for session {session_id}")
# Create a pruned down message list based purely on the prompt and responses,
# discarding the full preamble generated by prepare_message
messages: List[LLMMessage] = [LLMMessage(role="system", content=self.system_prompt)]
# Add the conversation history to the messages
messages.extend(
[
LLMMessage(role=m["role"], content=m["content"])
for m in await database.get_recent_chat_messages(session_id=session_id)
]
)
self.user.metrics.generate_count.labels(agent=self.agent_type).inc()
with self.user.metrics.generate_duration.labels(agent=self.agent_type).time():
context = None
rag_message: ChatMessageRagSearch | None = None
if self.user:
logger.info("Generating RAG results")
message = None
async for message in self.generate_rag_results(session_id=session_id, prompt=prompt):
if message.status == ApiStatusType.ERROR:
@ -668,16 +695,6 @@ Content: {content}
rag_message = message
context = self.get_rag_context(rag_message)
# Create a pruned down message list based purely on the prompt and responses,
# discarding the full preamble generated by prepare_message
messages: List[LLMMessage] = [LLMMessage(role="system", content=self.system_prompt)]
# Add the conversation history to the messages
messages.extend(
[
LLMMessage(role="user" if isinstance(m, ChatMessageUser) else "assistant", content=m.content)
for m in self.conversation
]
)
# Add the RAG context to the messages if available
if context:
messages.append(
@ -804,7 +821,7 @@ Content: {content}
num_ctx=self.context_size,
temperature=temperature,
)
logger.info(f"Message options: {options.model_dump(exclude_unset=True)}")
logger.info(f"Message options: {options.model_dump(exclude_unset=True)} with {len(messages)} messages")
content = ""
start_time = time.perf_counter()
response = None
@ -851,6 +868,7 @@ Content: {content}
prompt_eval_count=response.usage.prompt_eval_count,
prompt_eval_duration=response.usage.prompt_eval_duration,
rag_results=rag_message.content if rag_message else [],
llm_history=messages,
timers={
"llm_streamed": end_time - start_time,
"llm_with_tools": 0, # Placeholder for tool processing time
@ -858,9 +876,10 @@ Content: {content}
),
)
await database.add_chat_message(session_id, chat_message.model_dump())
logger.info(f"🤖 Assistent response saved to database for session {session_id}")
# Add the user and chat messages to the conversation
self.conversation.append(user_message)
self.conversation.append(chat_message)
yield chat_message
return

View File

@ -1,11 +1,14 @@
from __future__ import annotations
from typing import Literal, AsyncGenerator, ClassVar, Optional, Any
from pydantic import Field
from database.core import RedisDatabase
from .base import Agent, agent_registry
from logger import logger
from .registry import agent_registry
from models import ApiMessage, Tunables, ApiStatusType
from models import ApiMessage, Tunables, ApiStatusType, LLMMessage
system_message = """
@ -16,7 +19,7 @@ When answering queries, follow these steps:
- If there is information in the <|context|> section to enhance the answer, incorporate it seamlessly and refer to it as 'the latest information' or 'recent data' instead of mentioning '<|context|>' (etc.) or quoting it directly.
- Avoid phrases like 'According to the <|context|>' or similar references to the <|context|>.
Always <|context|> when possible. Be concise, and never make up information. If you do not know the answer, say so.
Always use <|context|> when possible. Be concise, and never make up information. If you do not know the answer, say so.
Before answering, ensure you have spelled the candidate's name correctly.
"""
@ -31,9 +34,17 @@ class CandidateChat(Agent):
_agent_type: ClassVar[str] = agent_type # Add this for registration
system_prompt: str = system_message
sessions: dict[str, list[LLMMessage]] = Field(default_factory=dict)
async def generate(
self, llm: Any, model: str, session_id: str, prompt: str, tunables: Optional[Tunables] = None, temperature=0.7
self,
llm: Any,
model: str,
session_id: str,
prompt: str,
database: RedisDatabase,
tunables: Optional[Tunables] = None,
temperature=0.7,
) -> AsyncGenerator[ApiMessage, None]:
user = self.user
if not user:
@ -51,9 +62,17 @@ Use that spelling instead of any spelling you may find in the <|context|>.
{system_message}
"""
if session_id not in self.sessions:
self.sessions[session_id] = [LLMMessage(role="user", content=prompt)]
async for message in super().generate(
llm=llm, model=model, session_id=session_id, prompt=prompt, temperature=temperature, tunables=tunables
llm=llm,
model=model,
session_id=session_id,
prompt=prompt,
database=database,
temperature=temperature,
tunables=tunables,
):
if message.status == ApiStatusType.ERROR:
yield message

View File

@ -1,86 +0,0 @@
from __future__ import annotations
from typing import Literal, ClassVar
from datetime import datetime
from .base import Agent, agent_registry
from .registry import agent_registry
system_message = f"""
Launched on {datetime.now().isoformat()}.
When answering queries, follow these steps:
- First analyze the query to determine if real-time information from the tools might be helpful
- Even when <|context|> or <|resume|> is provided, consider whether the tools would provide more current or comprehensive information
- Use the provided tools whenever they would enhance your response, regardless of whether context is also available
- When presenting weather forecasts, include relevant emojis immediately before the corresponding text. For example, for a sunny day, say \"☀️ Sunny\" or if the forecast says there will be \"rain showers, say \"🌧️ Rain showers\". Use this mapping for weather emojis: Sunny: ☀️, Cloudy: ☁️, Rainy: 🌧️, Snowy: ❄️
- When any combination of <|context|>, <|resume|> and tool outputs are relevant, synthesize information from all sources to provide the most complete answer
- Always prioritize the most up-to-date and relevant information, whether it comes from <|context|>, <|resume|> or tools
- If <|context|> and tool outputs contain conflicting information, prefer the tool outputs as they likely represent more current data
- If there is information in the <|context|> or <|resume|> sections to enhance the answer, incorporate it seamlessly and refer to it as 'the latest information' or 'recent data' instead of mentioning '<|context|>' (etc.) or quoting it directly.
- Avoid phrases like 'According to the <|context|>' or similar references to the <|context|> or <|resume|>.
CRITICAL INSTRUCTIONS FOR IMAGE GENERATION:
1. When the user requests to generate an image, inject the following into the response: <GenerateImage prompt="USER-PROMPT"/>. Do this when users request images, drawings, or visual content.
3. MANDATORY: You must respond with EXACTLY this format: <GenerateImage prompt="{{USER-PROMPT}}"/>
4. FORBIDDEN: DO NOT use markdown image syntax ![](url)
5. FORBIDDEN: DO NOT create fake URLs or file paths
6. FORBIDDEN: DO NOT use any other image embedding format
CORRECT EXAMPLE:
User: "Draw a cat"
Your response: "<GenerateImage prompt='Draw a cat'/>"
WRONG EXAMPLES (DO NOT DO THIS):
- ![](https://example.com/...)
- ![Cat image](any_url)
- <img src="...">
The <GenerateImage prompt="{{USER-PROMPT}}"/> format is the ONLY way to display images in this system.
DO NOT make up a URL for an image or provide markdown syntax for embedding an image. Only use <GenerateImage prompt="{{USER-PROMPT}}".
Always use tools, <|resume|>, and <|context|> when possible. Be concise, and never make up information. If you do not know the answer, say so.
"""
class Chat(Agent):
"""
Chat Agent
"""
agent_type: Literal["general"] = "general" # type: ignore
_agent_type: ClassVar[str] = agent_type # Add this for registration
system_prompt: str = system_message
# async def prepare_message(self, message: Message) -> AsyncGenerator[Message, None]:
# logger.info(f"{self.agent_type} - {inspect.stack()[0].function}")
# if not self.context:
# raise ValueError("Context is not set for this agent.")
# async for message in super().prepare_message(message):
# if message.status != "done":
# yield message
# if message.preamble:
# excluded = {}
# preamble_types = [
# f"<|{p}|>" for p in message.preamble.keys() if p not in excluded
# ]
# preamble_types_AND = " and ".join(preamble_types)
# preamble_types_OR = " or ".join(preamble_types)
# message.preamble[
# "rules"
# ] = f"""\
# - Answer the question based on the information provided in the {preamble_types_AND} sections by incorporate it seamlessly and refer to it using natural language instead of mentioning {preamble_types_OR} or quoting it directly.
# - If there is no information in these sections, answer based on your knowledge, or use any available tools.
# - Avoid phrases like 'According to the {preamble_types[0]}' or similar references to the {preamble_types_OR}.
# """
# message.preamble["question"] = "Respond to:"
# Register the base agent
agent_registry.register(Chat._agent_type, Chat)

View File

@ -9,9 +9,10 @@ from typing import (
) # NOTE: You must import Optional for late binding to work
import random
import time
import time
import os
from database.core import RedisDatabase
from .base import Agent, agent_registry
from models import (
ApiActivityType,
@ -40,7 +41,14 @@ class ImageGenerator(Agent):
system_prompt: str = "" # No system prompt is used
async def generate(
self, llm: Any, model: str, session_id: str, prompt: str, tunables: Optional[Tunables] = None, temperature=0.7
self,
llm: Any,
model: str,
session_id: str,
prompt: str,
database: RedisDatabase,
tunables: Optional[Tunables] = None,
temperature=0.7,
) -> AsyncGenerator[ChatMessage | ChatMessageStatus | ChatMessageError | ChatMessageStreaming, None]:
if not self.user:
logger.error("User is not set for ImageGenerator agent.")

View File

@ -17,6 +17,8 @@ import json
import time
import os
from database.core import RedisDatabase
from .base import Agent, agent_registry
from models import (
ApiActivityType,
@ -303,7 +305,14 @@ class GeneratePersona(Agent):
self.full_name = f"{self.first_name} {self.last_name}"
async def generate(
self, llm: Any, model: str, session_id: str, prompt: str, tunables: Optional[Tunables] = None, temperature=0.7
self,
llm: Any,
model: str,
session_id: str,
prompt: str,
database: RedisDatabase,
tunables: Optional[Tunables] = None,
temperature=0.7,
) -> AsyncGenerator[ChatMessage | ChatMessageStatus | ChatMessageError | ChatMessageStreaming, None]:
self.randomize()

View File

@ -11,6 +11,8 @@ from typing import (
import inspect
import json
from database.core import RedisDatabase
from .base import Agent, agent_registry
from models import (
ApiActivityType,
@ -153,7 +155,14 @@ Avoid vague categorizations and be precise about whether skills are explicitly r
return display
async def generate(
self, llm: Any, model: str, session_id: str, prompt: str, tunables: Optional[Tunables] = None, temperature=0.7
self,
llm: Any,
model: str,
session_id: str,
prompt: str,
database: RedisDatabase,
tunables: Optional[Tunables] = None,
temperature=0.7,
) -> AsyncGenerator[ApiMessage, None]:
if not self.user:
error_message = ChatMessageError(session_id=session_id, content="User is not set for this agent.")

View File

@ -1,23 +1,31 @@
from __future__ import annotations
from typing import Literal, AsyncGenerator, ClassVar, Optional, Any
from database.core import RedisDatabase
from .base import Agent, agent_registry
from logger import logger
from .registry import agent_registry
from models import ApiMessage, ApiStatusType, ChatMessageError, ChatMessageRagSearch, ApiStatusType, Tunables
from models import ApiMessage, ApiStatusType, ChatMessageError, ChatMessageRagSearch, Tunables
class Chat(Agent):
class RagSearchChat(Agent):
"""
Chat Agent
RagSearchChat Agent
"""
agent_type: Literal["rag_search"] = "rag_search" # type: ignore
_agent_type: ClassVar[str] = agent_type # Add this for registration
async def generate(
self, llm: Any, model: str, session_id: str, prompt: str, tunables: Optional[Tunables] = None, temperature=0.7
self,
llm: Any,
model: str,
session_id: str,
prompt: str,
database: RedisDatabase,
tunables: Optional[Tunables] = None,
temperature=0.7,
) -> AsyncGenerator[ApiMessage, None]:
"""
Generate a response based on the user message and the provided LLM.
@ -53,4 +61,4 @@ class Chat(Agent):
# Register the base agent
agent_registry.register(Chat._agent_type, Chat)
agent_registry.register(RagSearchChat._agent_type, RagSearchChat)

View File

@ -9,6 +9,8 @@ from typing import (
) # NOTE: You must import Optional for late binding to work
import json
from database.core import RedisDatabase
from .base import Agent, agent_registry
from models import (
ApiMessage,
@ -106,7 +108,14 @@ JSON RESPONSE:"""
return system_prompt, prompt
async def generate(
self, llm: Any, model: str, session_id: str, prompt: str, tunables: Optional[Tunables] = None, temperature=0.7
self,
llm: Any,
model: str,
session_id: str,
prompt: str,
database: RedisDatabase,
tunables: Optional[Tunables] = None,
temperature=0.7,
) -> AsyncGenerator[ApiMessage, None]:
if not self.user:
error_message = ChatMessageError(

View File

@ -1084,11 +1084,6 @@ class ChatMessageUser(ApiMessage):
class ChatMessage(ChatMessageUser):
role: ChatSenderType = ChatSenderType.ASSISTANT
metadata: ChatMessageMetaData = Field(default=ChatMessageMetaData())
# attachments: Optional[List[Attachment]] = None
# reactions: Optional[List[MessageReaction]] = None
# is_edited: bool = Field(False, alias=str("isEdited"))
# edit_history: Optional[List[EditHistory]] = Field(default=None, alias=str("editHistory"))
class ChatMessageSkillAssessment(ChatMessageUser):
role: ChatSenderType = ChatSenderType.ASSISTANT
@ -1145,7 +1140,6 @@ class ChatSession(BaseModel):
last_activity: datetime = Field(default_factory=lambda: datetime.now(UTC), alias=str("lastActivity"))
title: Optional[str] = None
context: ChatContext
# messages: Optional[List[ChatMessage]] = None
is_archived: bool = Field(False, alias=str("isArchived"))
system_prompt: Optional[str] = Field(default=None, alias=str("systemPrompt"))
model_config = ConfigDict(populate_by_name=True)

View File

@ -299,7 +299,7 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
n_components=2,
random_state=8911,
metric="cosine",
n_neighbors=30,
n_neighbors=round(min(30, len(self._umap_collection.embeddings) * 0.5)),
min_dist=0.1,
)
self._umap_embedding_2d = self._umap_model_2d.fit_transform(vectors) # type: ignore
@ -312,7 +312,7 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
n_components=3,
random_state=8911,
metric="cosine",
n_neighbors=30,
n_neighbors=round(min(30, len(self._umap_collection.embeddings) * 0.5)),
min_dist=0.01,
)
self._umap_embedding_3d = self._umap_model_3d.fit_transform(vectors) # type: ignore

View File

@ -113,6 +113,7 @@ async def create_candidate_ai(
model=defines.model,
session_id=user_message.session_id,
prompt=user_message.content,
database=database,
):
if isinstance(generated_message, ChatMessageError):
error_message: ChatMessageError = generated_message
@ -1375,6 +1376,7 @@ async def post_candidate_rag_search(
model=defines.model,
session_id=user_message.session_id,
prompt=user_message.content,
database=database,
):
rag_message = generated_message
@ -1387,6 +1389,7 @@ async def post_candidate_rag_search(
return create_success_response(final_message.content[0].model_dump(by_alias=True))
except Exception as e:
logger.error(backstory_traceback.format_exc())
logger.error(f"❌ Get candidate chat summary error: {e}")
return JSONResponse(status_code=500, content=create_error_response("SUMMARY_ERROR", str(e)))
@ -1622,6 +1625,7 @@ async def get_candidate_skill_match(
model=defines.model,
session_id=MOCK_UUID,
prompt=skill,
database=database,
):
if generated_message.status == ApiStatusType.ERROR:
if isinstance(generated_message, ChatMessageError):
@ -2077,9 +2081,6 @@ async def get_candidate_chat_sessions(
session = ChatSession.model_validate(session_data)
if session.user_id != current_user.id:
# User can only access their own sessions
logger.info(
f"🔗 Skipping session {session.id} - not owned by user {current_user.id} (created by {session.user_id})"
)
continue
# Check if this session is related to the candidate
context = session.context

View File

@ -171,7 +171,7 @@ async def post_chat_session_message_stream(
# Get candidate info if this chat is about a specific candidate
if candidate_info:
logger.info(
f"🔗 Chat session {user_message.session_id} about candidate {candidate_info['name']} accessed by user {current_user.id}"
f"🔗 Chat session {user_message.session_id} about {candidate_info['name']} accessed by user {current_user.id}"
)
else:
logger.info(
@ -208,19 +208,15 @@ async def post_chat_session_message_stream(
content=create_error_response("AGENT_NOT_FOUND", "No agent found for this chat type"),
)
# Persist user message to database
await database.add_chat_message(user_message.session_id, user_message.model_dump())
logger.info(f"💬 User message saved to database for session {user_message.session_id}")
# Update session last activity
chat_session_data["lastActivity"] = datetime.now(UTC).isoformat()
await database.set_chat_session(user_message.session_id, chat_session_data)
chat_session.last_activity = datetime.now(UTC)
await database.set_chat_session(user_message.session_id, chat_session.model_dump())
return await stream_agent_response(
chat_agent=chat_agent,
user_message=user_message,
database=database,
chat_session_data=chat_session_data,
chat_session=chat_session,
)
except Exception:

View File

@ -136,6 +136,7 @@ async def create_job_from_content(database: RedisDatabase, current_user: Candida
model=defines.model,
session_id=MOCK_UUID,
prompt=markdown_message.content,
database=database,
):
if message.status != ApiStatusType.DONE:
yield message

View File

@ -12,7 +12,7 @@ from fastapi.responses import StreamingResponse
import defines
from logger import logger
from models import DocumentType
from models import ChatSession, DocumentType
from models import Job, ChatMessage, ApiStatusType
import utils.llm_proxy as llm_manager
@ -65,7 +65,9 @@ def filter_and_paginate(
return paginated_items, total
async def stream_agent_response(chat_agent, user_message, database, chat_session_data=None) -> StreamingResponse:
async def stream_agent_response(
chat_agent, user_message, database, chat_session: Optional[ChatSession] = None
) -> StreamingResponse:
"""Stream agent response with proper formatting"""
async def message_stream_generator():
@ -79,6 +81,7 @@ async def stream_agent_response(chat_agent, user_message, database, chat_session
model=defines.model,
session_id=user_message.session_id,
prompt=user_message.content,
database=database,
):
if generated_message.status == ApiStatusType.ERROR:
logger.error(f"❌ AI generation error: {generated_message.content}")
@ -109,13 +112,10 @@ async def stream_agent_response(chat_agent, user_message, database, chat_session
# After streaming is complete, persist the final AI message to database
if final_message and final_message.status == ApiStatusType.DONE:
try:
if database and chat_session_data:
await database.add_chat_message(final_message.session_id, final_message.model_dump())
logger.info(f"🤖 Message saved to database for session {final_message.session_id}")
if database and chat_session:
# Update session last activity again
chat_session_data["lastActivity"] = datetime.now(UTC).isoformat()
await database.set_chat_session(final_message.session_id, chat_session_data)
chat_session.last_activity = datetime.now(UTC)
await database.set_chat_session(final_message.session_id, chat_session.model_dump())
except Exception as e:
logger.error(f"❌ Failed to save message to database: {e}")
@ -271,6 +271,7 @@ async def create_job_from_content(database, current_user, content: str):
model=defines.model,
session_id=MOCK_UUID,
prompt=markdown_message.content,
database=database,
):
if message.status != ApiStatusType.DONE:
yield message