diff --git a/backend/ee/onyx/server/query_and_chat/query_backend.py b/backend/ee/onyx/server/query_and_chat/query_backend.py index 18bffcd8d..c8c0acfc4 100644 --- a/backend/ee/onyx/server/query_and_chat/query_backend.py +++ b/backend/ee/onyx/server/query_and_chat/query_backend.py @@ -24,10 +24,10 @@ from onyx.chat.chat_utils import prepare_chat_message_request from onyx.chat.models import PersonaOverrideConfig from onyx.chat.process_message import ChatPacketStream from onyx.chat.process_message import stream_chat_message_objects +from onyx.configs.app_configs import FAST_SEARCH_MAX_HITS from onyx.configs.onyxbot_configs import MAX_THREAD_CONTEXT_PERCENTAGE -from onyx.context.search.fast_search import FAST_SEARCH_MAX_HITS -from onyx.context.search.fast_search import run_fast_search -from onyx.context.search.models import RetrievalOptions +from onyx.context.search.enums import LLMEvaluationType +from onyx.context.search.models import BaseFilters from onyx.context.search.models import SavedSearchDocWithContent from onyx.context.search.models import SearchRequest from onyx.context.search.pipeline import SearchPipeline @@ -35,19 +35,16 @@ from onyx.context.search.utils import dedupe_documents from onyx.context.search.utils import drop_llm_indices from onyx.context.search.utils import relevant_sections_to_indices from onyx.db.chat import get_prompt_by_id -from onyx.db.dependencies import get_session +from onyx.db.engine import get_session from onyx.db.models import Persona from onyx.db.models import User from onyx.db.persona import get_persona_by_id -from onyx.llm.factory import AllLLMs -from onyx.llm.factory import AllModelProviders from onyx.llm.factory import get_default_llms from onyx.llm.factory import get_llms_for_persona from onyx.llm.factory import get_main_llm_from_tuple from onyx.llm.utils import get_max_input_tokens from onyx.natural_language_processing.utils import get_tokenizer from onyx.server.utils import get_json_line -from onyx.utils.license import check_user_license_if_ee_feature from onyx.utils.logger import setup_logger @@ -297,7 +294,9 @@ class FastSearchRequest(BaseModel): """Request for fast search endpoint that returns raw search results without section merging.""" query: str - retrieval_options: Optional[RetrievalOptions] = None + filters: BaseFilters | None = ( + None # Direct filter options instead of retrieval_options + ) max_results: Optional[ int ] = None # If not provided, defaults to FAST_SEARCH_MAX_HITS @@ -309,7 +308,7 @@ class FastSearchResult(BaseModel): document_id: str chunk_id: int content: str - source_links: list[str] = [] + source_links: dict[int, str] | None = None score: Optional[float] = None metadata: Optional[dict] = None @@ -333,54 +332,57 @@ def get_fast_search_response( of section expansion, reranking, relevance evaluation, and merging. """ try: - # Set up the search request + # Set up the search request with optimized settings + max_results = request.max_results or FAST_SEARCH_MAX_HITS + + # Create a search request with optimized settings search_request = SearchRequest( query=request.query, - retrieval_options=request.retrieval_options, + human_selected_filters=request.filters, + # Skip section expansion + chunks_above=0, + chunks_below=0, + # Skip LLM evaluation + evaluation_type=LLMEvaluationType.SKIP, + # Limit the number of results + limit=max_results, ) # Set up the LLM instances - with AllModelProviders() as all_model_providers: - with AllLLMs( - model_providers=all_model_providers, - persona=Persona( - id="default", - name="Default", - llm_relevance_filter=False, - ), - db_session=db_session, - ) as llm_instances: - # Get user's license status - check_user_license_if_ee_feature(user, db_session, "fast_search") - # Run the fast search - max_results = request.max_results or FAST_SEARCH_MAX_HITS - chunks = run_fast_search( - search_request=search_request, - user=user, - llm=llm_instances.llm, - fast_llm=llm_instances.fast_llm, - db_session=db_session, - max_results=max_results, - ) + llm, fast_llm = get_default_llms() - # Convert chunks to response format - results = [ - FastSearchResult( - document_id=chunk.document_id, - chunk_id=chunk.chunk_id, - content=chunk.content, - source_links=chunk.source_links, - score=chunk.score, - metadata=chunk.metadata, - ) - for chunk in chunks - ] + # Create the search pipeline with optimized settings + search_pipeline = SearchPipeline( + search_request=search_request, + user=user, + llm=llm, + fast_llm=fast_llm, + skip_query_analysis=True, # Skip expensive query analysis + db_session=db_session, + bypass_acl=False, + ) - return FastSearchResponse( - results=results, - total_found=len(results), - ) + # Only retrieve chunks without further processing + chunks = search_pipeline._get_chunks() + + # Convert chunks to response format + results = [ + FastSearchResult( + document_id=chunk.document_id, + chunk_id=chunk.chunk_id, + content=chunk.content, + source_links=chunk.source_links, + score=chunk.score, + metadata=chunk.metadata, + ) + for chunk in chunks + ] + + return FastSearchResponse( + results=results, + total_found=len(results), + ) except Exception as e: logger.exception("Error in fast search") raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/onyx/configs/app_configs.py b/backend/onyx/configs/app_configs.py index 74be29c6d..8ffdff838 100644 --- a/backend/onyx/configs/app_configs.py +++ b/backend/onyx/configs/app_configs.py @@ -667,3 +667,5 @@ IMAGE_ANALYSIS_SYSTEM_PROMPT = os.environ.get( "IMAGE_ANALYSIS_SYSTEM_PROMPT", DEFAULT_IMAGE_ANALYSIS_SYSTEM_PROMPT, ) + +FAST_SEARCH_MAX_HITS = 300 diff --git a/backend/onyx/context/search/fast_search.py b/backend/onyx/context/search/fast_search.py deleted file mode 100644 index 4136df8e3..000000000 --- a/backend/onyx/context/search/fast_search.py +++ /dev/null @@ -1,182 +0,0 @@ -from collections.abc import Callable -from typing import cast -from typing import Optional - -from sqlalchemy.orm import Session - -from onyx.context.search.enums import QueryFlow -from onyx.context.search.enums import SearchType -from onyx.context.search.models import IndexFilters -from onyx.context.search.models import InferenceChunk -from onyx.context.search.models import RetrievalMetricsContainer -from onyx.context.search.models import SearchQuery -from onyx.context.search.models import SearchRequest -from onyx.context.search.retrieval.search_runner import retrieve_chunks -from onyx.db.models import User -from onyx.db.search_settings import get_current_search_settings -from onyx.document_index.factory import get_default_document_index -from onyx.llm.interfaces import LLM -from onyx.utils.logger import setup_logger - -logger = setup_logger() - -# Constant for the maximum number of search results to return in fast search -FAST_SEARCH_MAX_HITS = 300 - - -class FastSearchPipeline: - """A streamlined version of SearchPipeline that only retrieves chunks without section expansion or merging. - - This is optimized for quickly returning a large number of search results without the overhead - of section expansion, reranking, and relevance evaluation. - """ - - def __init__( - self, - search_request: SearchRequest, - user: User | None, - llm: LLM, - fast_llm: LLM, - skip_query_analysis: bool, - db_session: Session, - bypass_acl: bool = False, - retrieval_metrics_callback: Optional[ - Callable[[RetrievalMetricsContainer], None] - ] = None, - max_results: int = FAST_SEARCH_MAX_HITS, - ): - self.search_request = search_request - self.user = user - self.llm = llm - self.fast_llm = fast_llm - self.skip_query_analysis = skip_query_analysis - self.db_session = db_session - self.bypass_acl = bypass_acl - self.retrieval_metrics_callback = retrieval_metrics_callback - self.max_results = max_results - - self.search_settings = get_current_search_settings(db_session) - self.document_index = get_default_document_index(self.search_settings, None) - - # Preprocessing steps generate this - self._search_query: Optional[SearchQuery] = None - self._predicted_search_type: Optional[SearchType] = None - - # Initial document index retrieval chunks - self._retrieved_chunks: Optional[list[InferenceChunk]] = None - - # Default flow type - self._predicted_flow: Optional[QueryFlow] = QueryFlow.QUESTION_ANSWER - - def _run_preprocessing(self) -> None: - """Run a simplified version of preprocessing that only prepares the search query. - - This skips complex query analysis and just focuses on preparing the basic search parameters. - """ - # Create a simplified search query with the necessary parameters - self._search_query = SearchQuery( - query=self.search_request.query, - search_type=self.search_request.search_type, - filters=self.search_request.human_selected_filters - or IndexFilters(access_control_list=None), - hybrid_alpha=0.5, # Default hybrid search balance - recency_bias_multiplier=self.search_request.recency_bias_multiplier or 1.0, - num_hits=self.max_results, # Use the higher limit here - offset=self.search_request.offset or 0, - chunks_above=0, # Skip section expansion - chunks_below=0, # Skip section expansion - precomputed_query_embedding=self.search_request.precomputed_query_embedding, - precomputed_is_keyword=self.search_request.precomputed_is_keyword, - processed_keywords=self.search_request.precomputed_keywords, - ) - self._predicted_search_type = self._search_query.search_type - - @property - def search_query(self) -> SearchQuery: - """Get the search query, running preprocessing if necessary.""" - if self._search_query is not None: - return self._search_query - - self._run_preprocessing() - return cast(SearchQuery, self._search_query) - - @property - def predicted_search_type(self) -> SearchType: - """Get the predicted search type.""" - if self._predicted_search_type is not None: - return self._predicted_search_type - - self._run_preprocessing() - return cast(SearchType, self._predicted_search_type) - - @property - def predicted_flow(self) -> QueryFlow: - """Get the predicted query flow.""" - if self._predicted_flow is not None: - return self._predicted_flow - - self._run_preprocessing() - return cast(QueryFlow, self._predicted_flow) - - @property - def retrieved_chunks(self) -> list[InferenceChunk]: - """Get the retrieved chunks from the document index.""" - if self._retrieved_chunks is not None: - return self._retrieved_chunks - - # Use the existing retrieve_chunks function with our search query - self._retrieved_chunks = retrieve_chunks( - query=self.search_query, - document_index=self.document_index, - db_session=self.db_session, - retrieval_metrics_callback=self.retrieval_metrics_callback, - ) - - return self._retrieved_chunks - - -def run_fast_search( - search_request: SearchRequest, - user: User | None, - llm: LLM, - fast_llm: LLM, - db_session: Session, - max_results: int = FAST_SEARCH_MAX_HITS, -) -> list[InferenceChunk]: - """Run a fast search that returns up to 300 results without section expansion or merging. - - Args: - search_request: The search request containing the query and filters - user: The current user - llm: The main LLM instance - fast_llm: The faster LLM instance for some operations - db_session: The database session - max_results: Maximum number of results to return (default: 300) - - Returns: - A list of InferenceChunk objects representing the search results - """ - # Create a modified search request with optimized parameters - # Skip unnecessary processing by setting these properties - modified_request = search_request.model_copy( - update={ - "chunks_above": 0, # Skip section expansion - "chunks_below": 0, # Skip section expansion - "evaluation_type": None, # Skip LLM evaluation - "limit": max_results, # Use higher limit - } - ) - - # Create and run the fast search pipeline - pipeline = FastSearchPipeline( - search_request=modified_request, - user=user, - llm=llm, - fast_llm=fast_llm, - skip_query_analysis=True, # Skip complex query analysis - db_session=db_session, - max_results=max_results, - ) - - # Just get the retrieved chunks without further processing - return pipeline.retrieved_chunks diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx index 77f77f8ba..48d9c299e 100644 --- a/web/src/app/chat/ChatPage.tsx +++ b/web/src/app/chat/ChatPage.tsx @@ -317,14 +317,10 @@ export function ChatPage({ (assistant) => assistant.id === existingChatSessionAssistantId ) : defaultAssistantId !== undefined - ? availableAssistants.find( - (assistant) => assistant.id === defaultAssistantId - ) - : undefined - ); - // Gather default temperature settings - const search_param_temperature = searchParams.get( - SEARCH_PARAM_NAMES.TEMPERATURE + ? availableAssistants.find( + (assistant) => assistant.id === defaultAssistantId + ) + : undefined ); const setSelectedAssistantFromId = (assistantId: number) => { diff --git a/web/src/app/search/SearchPage.tsx b/web/src/app/search/SearchPage.tsx index fe835b57c..62e018214 100644 --- a/web/src/app/search/SearchPage.tsx +++ b/web/src/app/search/SearchPage.tsx @@ -32,24 +32,12 @@ import { HistorySidebar } from "@/app/chat/sessionSidebar/HistorySidebar"; import { Persona } from "@/app/admin/assistants/interfaces"; import { HealthCheckBanner } from "@/components/health/healthcheck"; import { - buildChatUrl, buildLatestMessageChain, - createChatSession, - deleteAllChatSessions, - getCitedDocumentsFromMessage, getHumanAndAIMessageFromMessageNumber, - getLastSuccessfulMessageId, - handleChatFeedback, - nameChatSession, PacketType, personaIncludesRetrieval, - processRawChatHistory, removeMessage, - sendMessage, - setMessageAsLatest, - updateLlmOverrideForChatSession, updateParentChildren, - uploadFilesForChat, useScrollonStream, } from "@/app/chat/lib"; import { @@ -71,25 +59,8 @@ import { } from "@/app/chat/searchParams"; import { LlmDescriptor, useFilters, useLlmManager } from "@/lib/hooks"; import { ChatState, FeedbackType, RegenerationState } from "@/app/chat/types"; -import { - AnswerPiecePacket, - OnyxDocument, - DocumentInfoPacket, - StreamStopInfo, - StreamStopReason, - SubQueryPiece, - SubQuestionPiece, - AgentAnswerPiece, - RefinedAnswerImprovement, -} from "@/lib/search/interfaces"; -import { buildFilters } from "@/lib/search/utils"; +import { OnyxDocument } from "@/lib/search/interfaces"; import { SettingsContext } from "@/components/settings/SettingsProvider"; -import Dropzone from "react-dropzone"; -import { - checkLLMSupportsImageInput, - getFinalLLM, - structureValue, -} from "@/lib/llm/utils"; import { ChatInputBar } from "@/app/chat/input/ChatInputBar"; import { useChatContext } from "@/components/context/ChatContext"; import { v4 as uuidv4 } from "uuid"; @@ -102,9 +73,6 @@ import { } from "@/components/resizable/constants"; import FixedLogo from "@/components/logo/FixedLogo"; -import { MinimalMarkdown } from "@/components/chat/MinimalMarkdown"; -import ExceptionTraceModal from "@/components/modals/ExceptionTraceModal"; - import { INTERNET_SEARCH_TOOL_ID, SEARCH_TOOL_ID, @@ -257,33 +225,6 @@ export default function SearchPage({ } }, [user]); - const processSearchParamsAndSubmitMessage = (searchParamsString: string) => { - const newSearchParams = new URLSearchParams(searchParamsString); - const message = newSearchParams.get("user-prompt"); - - filterManager.buildFiltersFromQueryString( - newSearchParams.toString(), - availableSources, - documentSets.map((ds) => ds.name), - tags - ); - - const fileDescriptorString = newSearchParams.get(SEARCH_PARAM_NAMES.FILES); - const overrideFileDescriptors: FileDescriptor[] = fileDescriptorString - ? JSON.parse(decodeURIComponent(fileDescriptorString)) - : []; - - newSearchParams.delete(SEARCH_PARAM_NAMES.SEND_ON_LOAD); - - router.replace(`?${newSearchParams.toString()}`, { scroll: false }); - - // If there's a message, submit it - if (message) { - setSubmittedMessage(message); - onSubmit({ messageOverride: message, overrideFileDescriptors }); - } - }; - const chatSessionIdRef = useRef(existingChatSessionId); // Only updates on session load (ie. rename / switching chat session) @@ -422,141 +363,6 @@ export default function SearchPage({ const { popup, setPopup } = usePopup(); - // fetch messages for the chat session - const [isFetchingChatMessages, setIsFetchingChatMessages] = useState( - existingChatSessionId !== null - ); - - useEffect(() => { - const priorChatSessionId = chatSessionIdRef.current; - const loadedSessionId = loadedIdSessionRef.current; - chatSessionIdRef.current = existingChatSessionId; - loadedIdSessionRef.current = existingChatSessionId; - - textAreaRef.current?.focus(); - - // only clear things if we're going from one chat session to another - const isChatSessionSwitch = existingChatSessionId !== priorChatSessionId; - if (isChatSessionSwitch) { - // de-select documents - - // reset all filters - filterManager.setSelectedDocumentSets([]); - filterManager.setSelectedSources([]); - filterManager.setSelectedTags([]); - filterManager.setTimeRange(null); - - // if switching from one chat to another, then need to scroll again - // if we're creating a brand new chat, then don't need to scroll - if (chatSessionIdRef.current !== null) { - setHasPerformedInitialScroll(false); - } - } - - async function initialSessionFetch() { - if (existingChatSessionId === null) { - setIsFetchingChatMessages(false); - if (defaultAssistantId !== undefined) { - setSelectedAssistantFromId(defaultAssistantId); - } else { - setSelectedAssistant(undefined); - } - updateCompleteMessageDetail(null, new Map()); - setChatSessionSharedStatus(ChatSessionSharedStatus.Private); - - // if we're supposed to submit on initial load, then do that here - if ( - shouldSubmitOnLoad(searchParams) && - !submitOnLoadPerformed.current - ) { - submitOnLoadPerformed.current = true; - await onSubmit(); - } - return; - } - - setIsFetchingChatMessages(true); - const response = await fetch( - `/api/chat/get-chat-session/${existingChatSessionId}` - ); - - const session = await response.json(); - const chatSession = session as BackendChatSession; - setSelectedAssistantFromId(chatSession.persona_id); - - const newMessageMap = processRawChatHistory(chatSession.messages); - const newMessageHistory = buildLatestMessageChain(newMessageMap); - - // Update message history except for edge where where - // last message is an error and we're on a new chat. - // This corresponds to a "renaming" of chat, which occurs after first message - // stream - if ( - (messageHistory[messageHistory.length - 1]?.type !== "error" || - loadedSessionId != null) && - !currentChatAnswering() - ) { - const latestMessageId = - newMessageHistory[newMessageHistory.length - 1]?.messageId; - - setSelectedMessageForDocDisplay( - latestMessageId !== undefined ? latestMessageId : null - ); - - updateCompleteMessageDetail(chatSession.chat_session_id, newMessageMap); - } - - setChatSessionSharedStatus(chatSession.shared_status); - - // go to bottom. If initial load, then do a scroll, - // otherwise just appear at the bottom - - scrollInitialized.current = false; - - if (!hasPerformedInitialScroll) { - if (isInitialLoad.current) { - setHasPerformedInitialScroll(true); - isInitialLoad.current = false; - } - clientScrollToBottom(); - - setTimeout(() => { - setHasPerformedInitialScroll(true); - }, 100); - } else if (isChatSessionSwitch) { - setHasPerformedInitialScroll(true); - clientScrollToBottom(true); - } - - setIsFetchingChatMessages(false); - - // if this is a seeded chat, then kick off the AI message generation - if ( - newMessageHistory.length === 1 && - !submitOnLoadPerformed.current && - searchParams.get(SEARCH_PARAM_NAMES.SEEDED) === "true" - ) { - submitOnLoadPerformed.current = true; - const seededMessage = newMessageHistory[0].message; - await onSubmit({ - isSeededChat: true, - messageOverride: seededMessage, - }); - // force re-name if the chat session doesn't have one - if (!chatSession.description) { - await nameChatSession(existingChatSessionId); - refreshChatSessions(); - } - } else if (newMessageHistory.length === 2 && !chatSession.description) { - await nameChatSession(existingChatSessionId); - refreshChatSessions(); - } - } - - initialSessionFetch(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [existingChatSessionId, searchParams.get(SEARCH_PARAM_NAMES.PERSONA_ID)]); - const [message, setMessage] = useState( searchParams.get(SEARCH_PARAM_NAMES.USER_PROMPT) || "" ); @@ -987,35 +793,6 @@ export default function SearchPage({ clientScrollToBottom(); }, [chatSessionIdRef.current]); - const loadNewPageLogic = (event: MessageEvent) => { - if (event.data.type === SUBMIT_MESSAGE_TYPES.PAGE_CHANGE) { - try { - const url = new URL(event.data.href); - processSearchParamsAndSubmitMessage(url.searchParams.toString()); - } catch (error) { - console.error("Error parsing URL:", error); - } - } - }; - - // Equivalent to `loadNewPageLogic` - useEffect(() => { - if (searchParams.get(SEARCH_PARAM_NAMES.SEND_ON_LOAD)) { - processSearchParamsAndSubmitMessage(searchParams.toString()); - } - }, [searchParams, router]); - - useEffect(() => { - adjustDocumentSidebarWidth(); - window.addEventListener("resize", adjustDocumentSidebarWidth); - window.addEventListener("message", loadNewPageLogic); - - return () => { - window.removeEventListener("message", loadNewPageLogic); - window.removeEventListener("resize", adjustDocumentSidebarWidth); - }; - }, []); - if (!documentSidebarInitialWidth && maxDocumentSidebarWidth) { documentSidebarInitialWidth = Math.min(700, maxDocumentSidebarWidth); } @@ -1037,45 +814,6 @@ export default function SearchPage({ } } - async function updateCurrentMessageFIFO( - stack: CurrentMessageFIFO, - params: any - ) { - try { - for await (const packet of sendMessage(params)) { - if (params.signal?.aborted) { - throw new Error("AbortError"); - } - stack.push(packet); - } - } catch (error: unknown) { - if (error instanceof Error) { - if (error.name === "AbortError") { - console.debug("Stream aborted"); - } else { - stack.error = error.message; - } - } else { - stack.error = String(error); - } - } finally { - stack.isComplete = true; - } - } - - const resetInputBar = () => { - setMessage(""); - if (endPaddingRef.current) { - endPaddingRef.current.style.height = `95px`; - } - }; - - const continueGenerating = () => { - onSubmit({ - messageOverride: - "Continue Generating (pick up exactly where you left off)", - }); - }; const [uncaughtError, setUncaughtError] = useState(null); const [agenticGenerating, setAgenticGenerating] = useState(false); @@ -1128,739 +866,8 @@ export default function SearchPage({ }; }, [autoScrollEnabled, screenHeight, currentSessionHasSentLocalUserMessage]); - const onSubmit = async ({ - messageIdToResend, - messageOverride, - queryOverride, - forceSearch, - isSeededChat, - alternativeAssistantOverride = null, - modelOverride, - regenerationRequest, - overrideFileDescriptors, - }: { - messageIdToResend?: number; - messageOverride?: string; - queryOverride?: string; - forceSearch?: boolean; - isSeededChat?: boolean; - alternativeAssistantOverride?: Persona | null; - modelOverride?: LlmDescriptor; - regenerationRequest?: RegenerationRequest | null; - overrideFileDescriptors?: FileDescriptor[]; - } = {}) => { - navigatingAway.current = false; - let frozenSessionId = currentSessionId(); - updateCanContinue(false, frozenSessionId); - setUncaughtError(null); - - // Mark that we've sent a message for this session in the current page load - markSessionMessageSent(frozenSessionId); - - if (currentChatState() != "input") { - if (currentChatState() == "uploading") { - setPopup({ - message: "Please wait for the content to upload", - type: "error", - }); - } else { - setPopup({ - message: "Please wait for the response to complete", - type: "error", - }); - } - - return; - } - - setAlternativeGeneratingAssistant(alternativeAssistantOverride); - - clientScrollToBottom(); - - let currChatSessionId: string; - const isNewSession = chatSessionIdRef.current === null; - - const searchParamBasedChatSessionName = - searchParams.get(SEARCH_PARAM_NAMES.TITLE) || null; - - if (isNewSession) { - currChatSessionId = await createChatSession( - liveAssistant?.id || 0, - searchParamBasedChatSessionName - ); - } else { - currChatSessionId = chatSessionIdRef.current as string; - } - frozenSessionId = currChatSessionId; - // update the selected model for the chat session if one is specified so that - // it persists across page reloads. Do not `await` here so that the message - // request can continue and this will just happen in the background. - // NOTE: only set the model override for the chat session once we send a - // message with it. If the user switches models and then starts a new - // chat session, it is unexpected for that model to be used when they - // return to this session the next day. - let finalLLM = modelOverride || llmManager.currentLlm; - updateLlmOverrideForChatSession( - currChatSessionId, - structureValue( - finalLLM.name || "", - finalLLM.provider || "", - finalLLM.modelName || "" - ) - ); - - updateStatesWithNewSessionId(currChatSessionId); - - const controller = new AbortController(); - - setAbortControllers((prev) => - new Map(prev).set(currChatSessionId, controller) - ); - - const messageToResend = messageHistory.find( - (message) => message.messageId === messageIdToResend - ); - if (messageIdToResend) { - updateRegenerationState( - { regenerating: true, finalMessageIndex: messageIdToResend }, - currentSessionId() - ); - } - const messageMap = currentMessageMap(completeMessageDetail); - const messageToResendParent = - messageToResend?.parentMessageId !== null && - messageToResend?.parentMessageId !== undefined - ? messageMap.get(messageToResend.parentMessageId) - : null; - const messageToResendIndex = messageToResend - ? messageHistory.indexOf(messageToResend) - : null; - - if (!messageToResend && messageIdToResend !== undefined) { - setPopup({ - message: - "Failed to re-send message - please refresh the page and try again.", - type: "error", - }); - resetRegenerationState(currentSessionId()); - updateChatState("input", frozenSessionId); - return; - } - let currMessage = messageToResend ? messageToResend.message : message; - if (messageOverride) { - currMessage = messageOverride; - } - - setSubmittedMessage(currMessage); - - updateChatState("loading"); - - const currMessageHistory = - messageToResendIndex !== null - ? messageHistory.slice(0, messageToResendIndex) - : messageHistory; - - let parentMessage = - messageToResendParent || - (currMessageHistory.length > 0 - ? currMessageHistory[currMessageHistory.length - 1] - : null) || - (messageMap.size === 1 ? Array.from(messageMap.values())[0] : null); - - let currentAssistantId; - if (alternativeAssistantOverride) { - currentAssistantId = alternativeAssistantOverride.id; - } else if (alternativeAssistant) { - currentAssistantId = alternativeAssistant.id; - } else { - currentAssistantId = liveAssistant.id; - } - - resetInputBar(); - let messageUpdates: Message[] | null = null; - - let answer = ""; - let second_level_answer = ""; - - const stopReason: StreamStopReason | null = null; - let query: string | null = null; - let retrievalType: RetrievalType = - selectedDocuments.length > 0 - ? RetrievalType.SelectedDocs - : RetrievalType.None; - let documents: OnyxDocument[] = selectedDocuments; - let aiMessageImages: FileDescriptor[] | null = null; - let agenticDocs: OnyxDocument[] | null = null; - let error: string | null = null; - let stackTrace: string | null = null; - - let sub_questions: SubQuestionDetail[] = []; - let is_generating: boolean = false; - let second_level_generating: boolean = false; - let finalMessage: BackendMessage | null = null; - let toolCall: ToolCallMetadata | null = null; - let isImprovement: boolean | undefined = undefined; - let isStreamingQuestions = true; - let includeAgentic = false; - let secondLevelMessageId: number | null = null; - let isAgentic: boolean = false; - let files: FileDescriptor[] = []; - - let initialFetchDetails: null | { - user_message_id: number; - assistant_message_id: number; - frozenMessageMap: Map; - } = null; - try { - const mapKeys = Array.from( - currentMessageMap(completeMessageDetail).keys() - ); - const systemMessage = Math.min(...mapKeys); - - const lastSuccessfulMessageId = - getLastSuccessfulMessageId(currMessageHistory) || systemMessage; - - const stack = new CurrentMessageFIFO(); - - updateCurrentMessageFIFO(stack, { - signal: controller.signal, - message: currMessage, - alternateAssistantId: currentAssistantId, - fileDescriptors: overrideFileDescriptors, - parentMessageId: - regenerationRequest?.parentMessage.messageId || - lastSuccessfulMessageId, - chatSessionId: currChatSessionId, - promptId: liveAssistant?.prompts[0]?.id || 0, - filters: buildFilters( - filterManager.selectedSources, - filterManager.selectedDocumentSets, - filterManager.timeRange, - filterManager.selectedTags - ), - selectedDocumentIds: selectedDocuments - .filter( - (document) => - document.db_doc_id !== undefined && document.db_doc_id !== null - ) - .map((document) => document.db_doc_id as number), - queryOverride, - forceSearch, - regenerate: regenerationRequest !== undefined, - modelProvider: - modelOverride?.name || llmManager.currentLlm.name || undefined, - modelVersion: - modelOverride?.modelName || - llmManager.currentLlm.modelName || - searchParams.get(SEARCH_PARAM_NAMES.MODEL_VERSION) || - undefined, - temperature: llmManager.temperature || undefined, - systemPromptOverride: - searchParams.get(SEARCH_PARAM_NAMES.SYSTEM_PROMPT) || undefined, - useExistingUserMessage: isSeededChat, - useLanggraph: - settings?.settings.pro_search_enabled && - proSearchEnabled && - retrievalEnabled, - }); - - const delay = (ms: number) => { - return new Promise((resolve) => setTimeout(resolve, ms)); - }; - - await delay(50); - while (!stack.isComplete || !stack.isEmpty()) { - if (stack.isEmpty()) { - await delay(0.5); - } - - if (!stack.isEmpty() && !controller.signal.aborted) { - const packet = stack.nextPacket(); - if (!packet) { - continue; - } - - if (!initialFetchDetails) { - if (!Object.hasOwn(packet, "user_message_id")) { - console.error( - "First packet should contain message response info " - ); - if (Object.hasOwn(packet, "error")) { - const error = (packet as StreamingError).error; - setLoadingError(error); - updateChatState("input"); - return; - } - continue; - } - - const messageResponseIDInfo = packet as MessageResponseIDInfo; - - const user_message_id = messageResponseIDInfo.user_message_id!; - const assistant_message_id = - messageResponseIDInfo.reserved_assistant_message_id; - - // we will use tempMessages until the regenerated message is complete - messageUpdates = [ - { - messageId: regenerationRequest - ? regenerationRequest?.parentMessage?.messageId! - : user_message_id, - message: currMessage, - type: "user", - files: files, - toolCall: null, - parentMessageId: parentMessage?.messageId || SYSTEM_MESSAGE_ID, - }, - ]; - - if (parentMessage && !regenerationRequest) { - messageUpdates.push({ - ...parentMessage, - childrenMessageIds: ( - parentMessage.childrenMessageIds || [] - ).concat([user_message_id]), - latestChildMessageId: user_message_id, - }); - } - - const { messageMap: currentFrozenMessageMap } = - upsertToCompleteMessageMap({ - messages: messageUpdates, - chatSessionId: currChatSessionId, - }); - - const frozenMessageMap = currentFrozenMessageMap; - initialFetchDetails = { - frozenMessageMap, - assistant_message_id, - user_message_id, - }; - - resetRegenerationState(); - } else { - const { user_message_id, frozenMessageMap } = initialFetchDetails; - if (Object.hasOwn(packet, "agentic_message_ids")) { - const agenticMessageIds = (packet as AgenticMessageResponseIDInfo) - .agentic_message_ids; - const level1MessageId = agenticMessageIds.find( - (item) => item.level === 1 - )?.message_id; - if (level1MessageId) { - secondLevelMessageId = level1MessageId; - includeAgentic = true; - } - } - - setChatState((prevState) => { - if (prevState.get(chatSessionIdRef.current!) === "loading") { - return new Map(prevState).set( - chatSessionIdRef.current!, - "streaming" - ); - } - return prevState; - }); - - if (Object.hasOwn(packet, "level")) { - if ((packet as any).level === 1) { - second_level_generating = true; - } - } - - if (Object.hasOwn(packet, "is_agentic")) { - isAgentic = (packet as any).is_agentic; - } - - if (Object.hasOwn(packet, "refined_answer_improvement")) { - isImprovement = (packet as RefinedAnswerImprovement) - .refined_answer_improvement; - } - - if (Object.hasOwn(packet, "stream_type")) { - if ((packet as any).stream_type == "main_answer") { - is_generating = false; - second_level_generating = true; - } - } - - // // Continuously refine the sub_questions based on the packets that we receive - if ( - Object.hasOwn(packet, "stop_reason") && - Object.hasOwn(packet, "level_question_num") - ) { - if ((packet as StreamStopInfo).stream_type == "main_answer") { - updateChatState("streaming", frozenSessionId); - } - if ( - (packet as StreamStopInfo).stream_type == "sub_questions" && - (packet as StreamStopInfo).level_question_num == undefined - ) { - isStreamingQuestions = false; - } - sub_questions = constructSubQuestions( - sub_questions, - packet as StreamStopInfo - ); - } else if (Object.hasOwn(packet, "sub_question")) { - updateChatState("toolBuilding", frozenSessionId); - isAgentic = true; - is_generating = true; - sub_questions = constructSubQuestions( - sub_questions, - packet as SubQuestionPiece - ); - setAgenticGenerating(true); - } else if (Object.hasOwn(packet, "sub_query")) { - sub_questions = constructSubQuestions( - sub_questions, - packet as SubQueryPiece - ); - } else if ( - Object.hasOwn(packet, "answer_piece") && - Object.hasOwn(packet, "answer_type") && - (packet as AgentAnswerPiece).answer_type === "agent_sub_answer" - ) { - sub_questions = constructSubQuestions( - sub_questions, - packet as AgentAnswerPiece - ); - } else if (Object.hasOwn(packet, "answer_piece")) { - // Mark every sub_question's is_generating as false - sub_questions = sub_questions.map((subQ) => ({ - ...subQ, - is_generating: false, - })); - - if ( - Object.hasOwn(packet, "level") && - (packet as any).level === 1 - ) { - second_level_answer += (packet as AnswerPiecePacket) - .answer_piece; - } else { - answer += (packet as AnswerPiecePacket).answer_piece; - } - } else if ( - Object.hasOwn(packet, "top_documents") && - Object.hasOwn(packet, "level_question_num") && - (packet as DocumentsResponse).level_question_num != undefined - ) { - const documentsResponse = packet as DocumentsResponse; - sub_questions = constructSubQuestions( - sub_questions, - documentsResponse - ); - - if ( - documentsResponse.level_question_num === 0 && - documentsResponse.level == 0 - ) { - documents = (packet as DocumentsResponse).top_documents; - } else if ( - documentsResponse.level_question_num === 0 && - documentsResponse.level == 1 - ) { - agenticDocs = (packet as DocumentsResponse).top_documents; - } - } else if (Object.hasOwn(packet, "top_documents")) { - documents = (packet as DocumentInfoPacket).top_documents; - retrievalType = RetrievalType.Search; - - if (documents && documents.length > 0) { - // point to the latest message (we don't know the messageId yet, which is why - // we have to use -1) - setSelectedMessageForDocDisplay(user_message_id); - } - } else if (Object.hasOwn(packet, "tool_name")) { - // Will only ever be one tool call per message - toolCall = { - tool_name: (packet as ToolCallMetadata).tool_name, - tool_args: (packet as ToolCallMetadata).tool_args, - tool_result: (packet as ToolCallMetadata).tool_result, - }; - - if (!toolCall.tool_name.includes("agent")) { - if ( - !toolCall.tool_result || - toolCall.tool_result == undefined - ) { - updateChatState("toolBuilding", frozenSessionId); - } else { - updateChatState("streaming", frozenSessionId); - } - - // This will be consolidated in upcoming tool calls udpate, - // but for now, we need to set query as early as possible - if (toolCall.tool_name == SEARCH_TOOL_NAME) { - query = toolCall.tool_args["query"]; - } - } else { - toolCall = null; - } - } else if (Object.hasOwn(packet, "file_ids")) { - aiMessageImages = (packet as FileChatDisplay).file_ids.map( - (fileId) => { - return { - id: fileId, - type: ChatFileType.IMAGE, - }; - } - ); - } else if ( - Object.hasOwn(packet, "error") && - (packet as any).error != null - ) { - if ( - sub_questions.length > 0 && - sub_questions - .filter((q) => q.level === 0) - .every((q) => q.is_stopped === true) - ) { - setUncaughtError((packet as StreamingError).error); - updateChatState("input"); - setAgenticGenerating(false); - setAlternativeGeneratingAssistant(null); - setSubmittedMessage(""); - - throw new Error((packet as StreamingError).error); - } else { - error = (packet as StreamingError).error; - stackTrace = (packet as StreamingError).stack_trace; - } - } else if (Object.hasOwn(packet, "message_id")) { - finalMessage = packet as BackendMessage; - } else if (Object.hasOwn(packet, "stop_reason")) { - const stop_reason = (packet as StreamStopInfo).stop_reason; - if (stop_reason === StreamStopReason.CONTEXT_LENGTH) { - updateCanContinue(true, frozenSessionId); - } - } - - // on initial message send, we insert a dummy system message - // set this as the parent here if no parent is set - parentMessage = - parentMessage || frozenMessageMap?.get(SYSTEM_MESSAGE_ID)!; - - const updateFn = (messages: Message[]) => { - const replacementsMap = regenerationRequest - ? new Map([ - [ - regenerationRequest?.parentMessage?.messageId, - regenerationRequest?.parentMessage?.messageId, - ], - [ - regenerationRequest?.messageId, - initialFetchDetails?.assistant_message_id, - ], - ] as [number, number][]) - : null; - - return upsertToCompleteMessageMap({ - messages: messages, - replacementsMap: replacementsMap, - completeMessageMapOverride: frozenMessageMap, - chatSessionId: frozenSessionId!, - }); - }; - - updateFn([ - { - messageId: regenerationRequest - ? regenerationRequest?.parentMessage?.messageId! - : initialFetchDetails.user_message_id!, - message: currMessage, - type: "user", - files: files, - toolCall: null, - parentMessageId: error ? null : lastSuccessfulMessageId, - childrenMessageIds: [ - ...(regenerationRequest?.parentMessage?.childrenMessageIds || - []), - initialFetchDetails.assistant_message_id!, - ], - latestChildMessageId: initialFetchDetails.assistant_message_id, - }, - { - isStreamingQuestions: isStreamingQuestions, - is_generating: is_generating, - isImprovement: isImprovement, - messageId: initialFetchDetails.assistant_message_id!, - message: error || answer, - second_level_message: second_level_answer, - type: error ? "error" : "assistant", - retrievalType, - query: finalMessage?.rephrased_query || query, - documents: documents, - citations: finalMessage?.citations || {}, - files: finalMessage?.files || aiMessageImages || [], - toolCall: finalMessage?.tool_call || toolCall, - parentMessageId: regenerationRequest - ? regenerationRequest?.parentMessage?.messageId! - : initialFetchDetails.user_message_id, - alternateAssistantID: alternativeAssistant?.id, - stackTrace: stackTrace, - overridden_model: finalMessage?.overridden_model, - stopReason: stopReason, - sub_questions: sub_questions, - second_level_generating: second_level_generating, - agentic_docs: agenticDocs, - is_agentic: isAgentic, - }, - ...(includeAgentic - ? [ - { - messageId: secondLevelMessageId!, - message: second_level_answer, - type: "assistant" as const, - files: [], - toolCall: null, - parentMessageId: - initialFetchDetails.assistant_message_id!, - }, - ] - : []), - ]); - } - } - } - } catch (e: any) { - const errorMsg = e.message; - upsertToCompleteMessageMap({ - messages: [ - { - messageId: - initialFetchDetails?.user_message_id || TEMP_USER_MESSAGE_ID, - message: currMessage, - type: "user", - files: [], - toolCall: null, - parentMessageId: parentMessage?.messageId || SYSTEM_MESSAGE_ID, - }, - { - messageId: - initialFetchDetails?.assistant_message_id || - TEMP_ASSISTANT_MESSAGE_ID, - message: errorMsg, - type: "error", - files: aiMessageImages || [], - toolCall: null, - parentMessageId: - initialFetchDetails?.user_message_id || TEMP_USER_MESSAGE_ID, - }, - ], - completeMessageMapOverride: currentMessageMap(completeMessageDetail), - }); - } - setAgenticGenerating(false); - resetRegenerationState(currentSessionId()); - - updateChatState("input"); - if (isNewSession) { - if (finalMessage) { - setSelectedMessageForDocDisplay(finalMessage.message_id); - } - - if (!searchParamBasedChatSessionName) { - await new Promise((resolve) => setTimeout(resolve, 200)); - await nameChatSession(currChatSessionId); - refreshChatSessions(); - } - - // NOTE: don't switch pages if the user has navigated away from the chat - if ( - currChatSessionId === chatSessionIdRef.current || - chatSessionIdRef.current === null - ) { - const newUrl = buildChatUrl(searchParams, currChatSessionId, null); - // newUrl is like /chat?chatId=10 - // current page is like /chat - - if (pathname == "/chat" && !navigatingAway.current) { - router.push(newUrl, { scroll: false }); - } - } - } - if ( - finalMessage?.context_docs && - finalMessage.context_docs.top_documents.length > 0 && - retrievalType === RetrievalType.Search - ) { - setSelectedMessageForDocDisplay(finalMessage.message_id); - } - setAlternativeGeneratingAssistant(null); - setSubmittedMessage(""); - }; - - const onFeedback = async ( - messageId: number, - feedbackType: FeedbackType, - feedbackDetails: string, - predefinedFeedback: string | undefined - ) => { - if (chatSessionIdRef.current === null) { - return; - } - - const response = await handleChatFeedback( - messageId, - feedbackType, - feedbackDetails, - predefinedFeedback - ); - - if (response.ok) { - setPopup({ - message: "Thanks for your feedback!", - type: "success", - }); - } else { - const responseJson = await response.json(); - const errorMsg = responseJson.detail || responseJson.message; - setPopup({ - message: `Failed to submit feedback - ${errorMsg}`, - type: "error", - }); - } - }; - - const handleImageUpload = async (acceptedFiles: File[]) => { - const [_, llmModel] = getFinalLLM( - llmProviders, - liveAssistant, - llmManager.currentLlm - ); - const llmAcceptsImages = checkLLMSupportsImageInput(llmModel); - - const imageFiles = acceptedFiles.filter((file) => - file.type.startsWith("image/") - ); - - if (imageFiles.length > 0 && !llmAcceptsImages) { - setPopup({ - type: "error", - message: - "The current model does not support image input. Please select a model with Vision support.", - }); - return; - } - - updateChatState("uploading", currentSessionId()); - - const [uploadedFiles, error] = await uploadFilesForChat(acceptedFiles); - if (error) { - setPopup({ - type: "error", - message: error, - }); - } - - updateChatState("input", currentSessionId()); - }; - // Used to maintain a "time out" for history sidebar so our existing refs can have time to process change const [untoggled, setUntoggled] = useState(false); - const [loadingError, setLoadingError] = useState(null); const explicitlyUntoggle = () => { setShowHistorySidebar(false); @@ -1978,48 +985,6 @@ export default function SearchPage({ useSidebarShortcut(router, toggleSidebar); - const [sharedChatSession, setSharedChatSession] = - useState(); - - const handleResubmitLastMessage = () => { - // Grab the last user-type message - const lastUserMsg = messageHistory - .slice() - .reverse() - .find((m) => m.type === "user"); - if (!lastUserMsg) { - setPopup({ - message: "No previously-submitted user message found.", - type: "error", - }); - return; - } - // We call onSubmit, passing a `messageOverride` - onSubmit({ - messageIdToResend: lastUserMsg.messageId, - messageOverride: lastUserMsg.message, - }); - }; - - const showShareModal = (chatSession: ChatSession) => { - setSharedChatSession(chatSession); - }; - const [showAssistantsModal, setShowAssistantsModal] = useState(false); - - const toggleDocumentSidebar = () => { - if (!documentSidebarVisible) { - setDocumentSidebarVisible(true); - } else { - setDocumentSidebarVisible(false); - } - }; - - interface RegenerationRequest { - messageId: number; - parentMessage: Message; - forceSearch?: boolean; - } - if (!user) { redirect("/auth/login"); } @@ -2103,6 +1068,7 @@ export default function SearchPage({ setPresentingDocument(document); setDocumentSidebarVisible(true); }; + const [showAssistantsModal, setShowAssistantsModal] = useState(false); // Add state for pagination const [currentPage, setCurrentPage] = useState(1); @@ -2143,6 +1109,11 @@ export default function SearchPage({ + {/* Assistants modal- browse, creat, etc. */} + {showAssistantsModal && ( + setShowAssistantsModal(false)} /> + )} + setIsChatSearchModalOpen(false)} @@ -2194,7 +1165,7 @@ export default function SearchPage({ currentChatSession={selectedChatSession} folders={folders} removeToggle={removeToggle} - showShareModal={showShareModal} + // showShareModal={showShareModal} /> @@ -2212,7 +1183,7 @@ export default function SearchPage({
{/* Header with search inputrflow */} {!firstSearch && ( -
+
- {documents.map((doc) => ( - + {documents.map((doc, ind) => ( + ))}
); diff --git a/web/src/app/search/searchUtils.ts b/web/src/app/search/searchUtils.ts index 1a1905b81..7d610a936 100644 --- a/web/src/app/search/searchUtils.ts +++ b/web/src/app/search/searchUtils.ts @@ -17,6 +17,25 @@ export interface SearchStreamResponse { error: string | null; } +// Define interface matching FastSearchResult +interface FastSearchResult { + document_id: string; + chunk_id: number; + content: string; + source_links: string[]; + score?: number; + metadata?: { + source_type?: string; + semantic_identifier?: string; + boost?: number; + hidden?: boolean; + updated_at?: string; + primary_owners?: string[]; + secondary_owners?: string[]; + [key: string]: any; + }; +} + export async function* streamSearchWithCitation({ query, persona, @@ -34,24 +53,16 @@ export async function* streamSearchWithCitation({ }): AsyncGenerator { const filters = buildFilters(sources, documentSets, timeRange, tags); - const response = await fetch("/api/query/search", { + // Use the fast-search endpoint instead + const response = await fetch("/api/query/fast-search", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ - persona_id: persona.id, - messages: [ - { - role: "user", - message: query, - }, - ], - retrieval_options: { - filters: filters, - favor_recent: true, - }, - skip_gen_ai_answer_generation: false, + query: query, + filters: filters, + max_results: 300, // Use the default max results for fast search }), }); @@ -65,43 +76,59 @@ export async function* streamSearchWithCitation({ return; } - let currentAnswer = ""; - let documents: OnyxDocument[] = []; - let error: string | null = null; + // Since fast-search is not streaming, we need to process the complete response + const searchResults = await response.json(); - for await (const packet of handleSSEStream(response)) { - if ("error" in packet && packet.error) { - error = (packet as StreamingError).error; - yield { - answer: currentAnswer, - documents, - error, - }; - continue; - } + // Convert results to OnyxDocument format + const documents: OnyxDocument[] = searchResults.results.map( + (result: FastSearchResult) => { + // Create a blurb from the content (first 200 chars) + const blurb = + result.content.substring(0, 200) + + (result.content.length > 200 ? "..." : ""); - if ("answer_piece" in packet && packet.answer_piece) { - currentAnswer += (packet as AnswerPiecePacket).answer_piece; - yield { - answer: currentAnswer, - documents, - error, + // Get the source link if available + const link = + result.source_links && result.source_links.length > 0 + ? result.source_links[0] + : null; + + // Convert to OnyxDocument format + return { + document_id: result.document_id, + chunk_ind: result.chunk_id, + content: result.content, + source_type: result.metadata?.source_type || "unknown", + semantic_identifier: result.metadata?.semantic_identifier || "Unknown", + score: result.score || 0, + metadata: result.metadata || {}, + match_highlights: [], + is_internet: false, + link: link, + updated_at: result.metadata?.updated_at + ? new Date(result.metadata.updated_at).toISOString() + : null, + blurb: blurb, + primary_owners: result.metadata?.primary_owners || [], + secondary_owners: result.metadata?.secondary_owners || [], + boost: result.metadata?.boost || 0, + hidden: result.metadata?.hidden || false, + validationState: null, }; } + ); - if ("top_documents" in packet && packet.top_documents) { - documents = (packet as DocumentInfoPacket).top_documents; - yield { - answer: currentAnswer, - documents, - error, - }; - } - } - + // First yield just the documents to maintain similar streaming behavior yield { - answer: currentAnswer, + answer: null, documents, - error, + error: null, + }; + + // Final yield with completed results + yield { + answer: null, + documents, + error: null, }; } diff --git a/web/src/components/SourceIcon.tsx b/web/src/components/SourceIcon.tsx index 359360a73..5369393fc 100644 --- a/web/src/components/SourceIcon.tsx +++ b/web/src/components/SourceIcon.tsx @@ -10,7 +10,12 @@ export function SourceIcon({ sourceType: ValidSources; iconSize: number; }) { - return getSourceMetadata(sourceType).icon({ - size: iconSize, - }); + try { + return getSourceMetadata(sourceType).icon({ + size: iconSize, + }); + } catch (error) { + console.error("Error getting source icon:", error); + return null; + } }