From 80b63fe0e1bacda408746a89f96e792174a8e23f Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Thu, 10 Jul 2025 15:04:34 -0700 Subject: [PATCH] Plumbed cache through agents Removed some dead frontend code --- frontend/src/components/Conversation.tsx | 455 +---------------------- frontend/src/pages/OldChatPage.tsx | 67 ---- frontend/src/types/types.ts | 2 +- src/backend/agents/base.py | 69 ++-- src/backend/agents/candidate_chat.py | 29 +- src/backend/agents/general.py | 86 ----- src/backend/agents/generate_image.py | 12 +- src/backend/agents/generate_persona.py | 11 +- src/backend/agents/job_requirements.py | 11 +- src/backend/agents/rag_search.py | 20 +- src/backend/agents/skill_match.py | 11 +- src/backend/models.py | 6 - src/backend/rag/rag.py | 4 +- src/backend/routes/candidates.py | 7 +- src/backend/routes/chat.py | 12 +- src/backend/routes/jobs.py | 1 + src/backend/utils/helpers.py | 17 +- 17 files changed, 145 insertions(+), 675 deletions(-) delete mode 100644 frontend/src/pages/OldChatPage.tsx delete mode 100644 src/backend/agents/general.py diff --git a/frontend/src/components/Conversation.tsx b/frontend/src/components/Conversation.tsx index dff9971..beaa80e 100644 --- a/frontend/src/components/Conversation.tsx +++ b/frontend/src/components/Conversation.tsx @@ -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( - (props: ConversationProps, ref) => { - const { - actionLabel, - defaultPrompts, - hideDefaultPrompts, - hidePreamble, - messageFilter, - messages, - onResponse, - placeholder, - preamble, - resetLabel, - sx, - } = props; - const { apiClient } = useAuth(); - const [processing, setProcessing] = useState(false); - const [conversation, setConversation] = useState([]); - const conversationRef = useRef([]); - const [filteredConversation, setFilteredConversation] = useState([]); - const [processingMessage, setProcessingMessage] = useState(undefined); - const [streamingMessage, setStreamingMessage] = useState(undefined); - const [noInteractions, setNoInteractions] = useState(true); - const viewableElementRef = useRef(null); - const backstoryTextRef = useRef(null); - const stopRef = useRef(false); - const controllerRef = useRef(null); - const [chatSession, setChatSession] = useState(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 => { - 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 = 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 ( - // - - - {filteredConversation.map((message, index) => ( - - ))} - {processingMessage !== undefined && ( - - )} - {streamingMessage !== undefined && ( - - )} - - - - - {placeholder && ( - - - - )} - - - { - /*reset(); resetAction && resetAction(); */ - }} - /> - - - - - - - - {' '} - {/* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */} - { - cancelQuery(); - }} - sx={{ display: 'flex', margin: 'auto 0px' }} - size="large" - edge="start" - disabled={stopRef.current || !chatSession || processing === false} - > - - - - - - - {(noInteractions || !hideDefaultPrompts) && - defaultPrompts !== undefined && - defaultPrompts.length !== 0 && ( - - {defaultPrompts.map((element, index) => { - return {element}; - })} - - )} - - - - ); - } -); -Conversation.displayName = 'Conversation'; export type { ConversationProps, ConversationHandle }; - -export { Conversation }; diff --git a/frontend/src/pages/OldChatPage.tsx b/frontend/src/pages/OldChatPage.tsx deleted file mode 100644 index 0df389e..0000000 --- a/frontend/src/pages/OldChatPage.tsx +++ /dev/null @@ -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( - (props: BackstoryPageProps, ref) => { - const { setSnack } = useAppState(); - const { user } = useAuth(); - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); - const [questions, setQuestions] = useState([]); - const candidate: Candidate | null = - user?.userType === 'candidate' ? (user as Types.Candidate) : null; - - // console.log("ChatPage candidate =>", candidate); - useEffect(() => { - if (!candidate) { - return; - } - - setQuestions([ - - {candidate.questions?.map((q, i: number) => ( - - ))} - , - - - {`As with all LLM interactions, the results may not be 100% accurate. Please contact **${candidate.fullName}** if you have any questions.`} - - , - ]); - }, [candidate, isMobile]); - - if (!candidate) { - return <>; - } - return ( - - - - - ); - } -); - -export { ChatPage }; diff --git a/frontend/src/types/types.ts b/frontend/src/types/types.ts index 9e1bf76..6ed432c 100644 --- a/frontend/src/types/types.ts +++ b/frontend/src/types/types.ts @@ -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 // ============================ diff --git a/src/backend/agents/base.py b/src/backend/agents/base.py index e3c3f73..d3b4094 100644 --- a/src/backend/agents/base.py +++ b/src/backend/agents/base.py @@ -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 diff --git a/src/backend/agents/candidate_chat.py b/src/backend/agents/candidate_chat.py index 27ee876..9cebf7f 100644 --- a/src/backend/agents/candidate_chat.py +++ b/src/backend/agents/candidate_chat.py @@ -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 diff --git a/src/backend/agents/general.py b/src/backend/agents/general.py deleted file mode 100644 index 6670635..0000000 --- a/src/backend/agents/general.py +++ /dev/null @@ -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: . Do this when users request images, drawings, or visual content. -3. MANDATORY: You must respond with EXACTLY this format: -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: "" - -WRONG EXAMPLES (DO NOT DO THIS): -- ![](https://example.com/...) -- ![Cat image](any_url) -- - -The 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 , 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) diff --git a/src/backend/agents/generate_image.py b/src/backend/agents/generate_image.py index d509bdc..965ee7f 100644 --- a/src/backend/agents/generate_image.py +++ b/src/backend/agents/generate_image.py @@ -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.") diff --git a/src/backend/agents/generate_persona.py b/src/backend/agents/generate_persona.py index 6609c1e..312264a 100644 --- a/src/backend/agents/generate_persona.py +++ b/src/backend/agents/generate_persona.py @@ -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() diff --git a/src/backend/agents/job_requirements.py b/src/backend/agents/job_requirements.py index 102f944..0985296 100644 --- a/src/backend/agents/job_requirements.py +++ b/src/backend/agents/job_requirements.py @@ -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.") diff --git a/src/backend/agents/rag_search.py b/src/backend/agents/rag_search.py index 838e4e0..deec2c6 100644 --- a/src/backend/agents/rag_search.py +++ b/src/backend/agents/rag_search.py @@ -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) diff --git a/src/backend/agents/skill_match.py b/src/backend/agents/skill_match.py index f0d3250..9c17cc8 100644 --- a/src/backend/agents/skill_match.py +++ b/src/backend/agents/skill_match.py @@ -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( diff --git a/src/backend/models.py b/src/backend/models.py index 073ae84..c297217 100644 --- a/src/backend/models.py +++ b/src/backend/models.py @@ -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) diff --git a/src/backend/rag/rag.py b/src/backend/rag/rag.py index f48a19e..f9b20d4 100644 --- a/src/backend/rag/rag.py +++ b/src/backend/rag/rag.py @@ -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 diff --git a/src/backend/routes/candidates.py b/src/backend/routes/candidates.py index dcc589e..75628ff 100644 --- a/src/backend/routes/candidates.py +++ b/src/backend/routes/candidates.py @@ -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 diff --git a/src/backend/routes/chat.py b/src/backend/routes/chat.py index 42f9978..ef50055 100644 --- a/src/backend/routes/chat.py +++ b/src/backend/routes/chat.py @@ -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: diff --git a/src/backend/routes/jobs.py b/src/backend/routes/jobs.py index 5e50959..10dd98f 100644 --- a/src/backend/routes/jobs.py +++ b/src/backend/routes/jobs.py @@ -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 diff --git a/src/backend/utils/helpers.py b/src/backend/utils/helpers.py index 7507e8f..3c82393 100644 --- a/src/backend/utils/helpers.py +++ b/src/backend/utils/helpers.py @@ -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