diff --git a/backend/danswer/configs/chat_configs.py b/backend/danswer/configs/chat_configs.py index 6e1874b37..d4f5a5e80 100644 --- a/backend/danswer/configs/chat_configs.py +++ b/backend/danswer/configs/chat_configs.py @@ -44,7 +44,7 @@ DISABLE_LLM_QUERY_REPHRASE = ( QUOTE_ALLOWED_ERROR_PERCENT = 0.05 QA_TIMEOUT = int(os.environ.get("QA_TIMEOUT") or "60") # 60 seconds # Weighting factor between Vector and Keyword Search, 1 for completely vector search -HYBRID_ALPHA = max(0, min(1, float(os.environ.get("HYBRID_ALPHA") or 0.62))) +HYBRID_ALPHA = max(0, min(1, float(os.environ.get("HYBRID_ALPHA") or 0.5))) HYBRID_ALPHA_KEYWORD = max( 0, min(1, float(os.environ.get("HYBRID_ALPHA_KEYWORD") or 0.4)) ) @@ -53,7 +53,7 @@ HYBRID_ALPHA_KEYWORD = max( # Content. This is to avoid cases where the Content is very relevant but it may not be clear # if the title is separated out. Title is most of a "boost" than a separate field. TITLE_CONTENT_RATIO = max( - 0, min(1, float(os.environ.get("TITLE_CONTENT_RATIO") or 0.20)) + 0, min(1, float(os.environ.get("TITLE_CONTENT_RATIO") or 0.10)) ) # A list of languages passed to the LLM to rephase the query diff --git a/backend/danswer/db/swap_index.py b/backend/danswer/db/swap_index.py index f14a45f29..7d55fb961 100644 --- a/backend/danswer/db/swap_index.py +++ b/backend/danswer/db/swap_index.py @@ -1,5 +1,6 @@ from sqlalchemy.orm import Session +from danswer.configs.constants import KV_REINDEX_KEY from danswer.db.connector_credential_pair import get_connector_credential_pairs from danswer.db.connector_credential_pair import resync_cc_pair from danswer.db.embedding_model import get_current_db_embedding_model @@ -10,6 +11,7 @@ from danswer.db.index_attempt import cancel_indexing_attempts_past_model from danswer.db.index_attempt import ( count_unique_cc_pairs_with_successful_index_attempts, ) +from danswer.dynamic_configs.factory import get_dynamic_config_store from danswer.utils.logger import setup_logger logger = setup_logger() @@ -52,6 +54,9 @@ def check_index_swap(db_session: Session) -> None: ) if cc_pair_count > 0: + kv_store = get_dynamic_config_store() + kv_store.store(KV_REINDEX_KEY, False) + # Expire jobs for the now past index/embedding model cancel_indexing_attempts_past_model(db_session) diff --git a/backend/danswer/document_index/vespa/app_config/schemas/danswer_chunk.sd b/backend/danswer/document_index/vespa/app_config/schemas/danswer_chunk.sd index bf7772e15..be279f6a6 100644 --- a/backend/danswer/document_index/vespa/app_config/schemas/danswer_chunk.sd +++ b/backend/danswer/document_index/vespa/app_config/schemas/danswer_chunk.sd @@ -20,18 +20,10 @@ schema DANSWER_CHUNK_NAME { # `semantic_identifier` will be the channel name, but the `title` will be empty field title type string { indexing: summary | index | attribute - match { - gram - gram-size: 3 - } index: enable-bm25 } field content type string { indexing: summary | index - match { - gram - gram-size: 3 - } index: enable-bm25 } # duplication of `content` is far from ideal, but is needed for @@ -157,43 +149,45 @@ schema DANSWER_CHUNK_NAME { query(query_embedding) tensor(x[VARIABLE_DIM]) } - # This must be separate function for normalize_linear to work - function vector_score() { + function title_vector_score() { expression { - # If no title, the full vector score comes from the content embedding - (query(title_content_ratio) * if(attribute(skip_title), closeness(field, embeddings), closeness(field, title_embedding))) + - ((1 - query(title_content_ratio)) * closeness(field, embeddings)) - } - } - - # This must be separate function for normalize_linear to work - function keyword_score() { - expression { - (query(title_content_ratio) * bm25(title)) + - ((1 - query(title_content_ratio)) * bm25(content)) + # If no good matching titles, then it should use the context embeddings rather than having some + # irrelevant title have a vector score of 1. This way at least it will be the doc with the highest + # matching content score getting the full score + max(closeness(field, embeddings), closeness(field, title_embedding)) } } + # First phase must be vector to allow hits that have no keyword matches first-phase { - expression: vector_score + expression: closeness(field, embeddings) } # Weighted average between Vector Search and BM-25 - # Each is a weighted average between the Title and Content fields - # Finally each doc is boosted by it's user feedback based boost and recency - # If any embedding or index field is missing, it just receives a score of 0 - # Assumptions: - # - For a given query + corpus, the BM-25 scores will be relatively similar in distribution - # therefore not normalizing before combining. - # - For documents without title, it gets a score of 0 for that and this is ok as documents - # without any title match should be penalized. global-phase { expression { ( # Weighted Vector Similarity Score - (query(alpha) * normalize_linear(vector_score)) + + ( + query(alpha) * ( + (query(title_content_ratio) * normalize_linear(title_vector_score)) + + + ((1 - query(title_content_ratio)) * normalize_linear(closeness(field, embeddings))) + ) + ) + + + + # Weighted Keyword Similarity Score - ((1 - query(alpha)) * normalize_linear(keyword_score)) + # Note: for the BM25 Title score, it requires decent stopword removal in the query + # This needs to be the case so there aren't irrelevant titles being normalized to a score of 1 + ( + (1 - query(alpha)) * ( + (query(title_content_ratio) * normalize_linear(bm25(title))) + + + ((1 - query(title_content_ratio)) * normalize_linear(bm25(content))) + ) + ) ) # Boost based on user feedback * document_boost @@ -208,8 +202,6 @@ schema DANSWER_CHUNK_NAME { bm25(content) closeness(field, title_embedding) closeness(field, embeddings) - keyword_score - vector_score document_boost recency_bias closest(embeddings) diff --git a/backend/danswer/document_index/vespa/index.py b/backend/danswer/document_index/vespa/index.py index f380d53c9..26c34c499 100644 --- a/backend/danswer/document_index/vespa/index.py +++ b/backend/danswer/document_index/vespa/index.py @@ -1,12 +1,14 @@ import concurrent.futures import io import os +import re import time import zipfile from dataclasses import dataclass from datetime import datetime from datetime import timedelta from typing import BinaryIO +from typing import cast import httpx import requests @@ -14,6 +16,7 @@ import requests from danswer.configs.chat_configs import DOC_TIME_DECAY from danswer.configs.chat_configs import NUM_RETURNED_HITS from danswer.configs.chat_configs import TITLE_CONTENT_RATIO +from danswer.configs.constants import KV_REINDEX_KEY from danswer.document_index.interfaces import DocumentIndex from danswer.document_index.interfaces import DocumentInsertionRecord from danswer.document_index.interfaces import UpdateRequest @@ -53,6 +56,7 @@ from danswer.document_index.vespa_constants import VESPA_APPLICATION_ENDPOINT from danswer.document_index.vespa_constants import VESPA_DIM_REPLACEMENT_PAT from danswer.document_index.vespa_constants import VESPA_TIMEOUT from danswer.document_index.vespa_constants import YQL_BASE +from danswer.dynamic_configs.factory import get_dynamic_config_store from danswer.indexing.models import DocMetadataAwareIndexChunk from danswer.search.models import IndexFilters from danswer.search.models import InferenceChunkUncleaned @@ -88,6 +92,21 @@ def _create_document_xml_lines(doc_names: list[str | None]) -> str: return "\n".join(doc_lines) +def add_ngrams_to_schema(schema_content: str) -> str: + # Add the match blocks containing gram and gram-size to title and content fields + schema_content = re.sub( + r"(field title type string \{[^}]*indexing: summary \| index \| attribute)", + r"\1\n match {\n gram\n gram-size: 3\n }", + schema_content, + ) + schema_content = re.sub( + r"(field content type string \{[^}]*indexing: summary \| index)", + r"\1\n match {\n gram\n gram-size: 3\n }", + schema_content, + ) + return schema_content + + class VespaIndex(DocumentIndex): def __init__(self, index_name: str, secondary_index_name: str | None) -> None: self.index_name = index_name @@ -115,6 +134,13 @@ class VespaIndex(DocumentIndex): doc_lines = _create_document_xml_lines(schema_names) services = services_template.replace(DOCUMENT_REPLACEMENT_PAT, doc_lines) + kv_store = get_dynamic_config_store() + + needs_reindexing = False + try: + needs_reindexing = cast(bool, kv_store.load(KV_REINDEX_KEY)) + except Exception: + logger.debug("Could not load the reindexing flag. Using ngrams") with open(overrides_file, "r") as overrides_f: overrides_template = overrides_f.read() @@ -134,10 +160,10 @@ class VespaIndex(DocumentIndex): with open(schema_file, "r") as schema_f: schema_template = schema_f.read() - schema = schema_template.replace( DANSWER_CHUNK_REPLACEMENT_PAT, self.index_name ).replace(VESPA_DIM_REPLACEMENT_PAT, str(index_embedding_dim)) + schema = add_ngrams_to_schema(schema) if needs_reindexing else schema zip_dict[f"schemas/{schema_names[0]}.sd"] = schema.encode("utf-8") if self.secondary_index_name: diff --git a/backend/danswer/indexing/models.py b/backend/danswer/indexing/models.py index 317a4404a..c70176b8f 100644 --- a/backend/danswer/indexing/models.py +++ b/backend/danswer/indexing/models.py @@ -5,6 +5,7 @@ from pydantic import BaseModel from danswer.access.models import DocumentAccess from danswer.connectors.models import Document from danswer.utils.logger import setup_logger +from shared_configs.configs import ALT_INDEX_SUFFIX from shared_configs.model_server_models import Embedding if TYPE_CHECKING: @@ -108,7 +109,9 @@ class EmbeddingModelDetail(BaseModel): embedding_model: "EmbeddingModel", ) -> "EmbeddingModelDetail": return cls( - model_name=embedding_model.model_name, + # When constructing EmbeddingModel Detail for user-facing flows, strip the + # unneeded additional data after the `_`s + model_name=embedding_model.model_name.removesuffix(ALT_INDEX_SUFFIX), model_dim=embedding_model.model_dim, normalize=embedding_model.normalize, query_prefix=embedding_model.query_prefix, diff --git a/backend/danswer/main.py b/backend/danswer/main.py index 549798adc..7a58722b8 100644 --- a/backend/danswer/main.py +++ b/backend/danswer/main.py @@ -38,11 +38,13 @@ from danswer.configs.chat_configs import NUM_POSTPROCESSED_RESULTS from danswer.configs.constants import AuthType from danswer.configs.constants import KV_REINDEX_KEY from danswer.configs.constants import POSTGRES_WEB_APP_NAME +from danswer.db.connector import check_connectors_exist from danswer.db.connector import create_initial_default_connector from danswer.db.connector_credential_pair import associate_default_cc_pair from danswer.db.connector_credential_pair import get_connector_credential_pairs from danswer.db.connector_credential_pair import resync_cc_pair from danswer.db.credentials import create_initial_public_credential +from danswer.db.document import check_docs_exist from danswer.db.embedding_model import get_current_db_embedding_model from danswer.db.embedding_model import get_secondary_db_embedding_model from danswer.db.engine import get_sqlalchemy_engine @@ -198,21 +200,22 @@ def setup_postgres(db_session: Session) -> None: def mark_reindex_flag(db_session: Session) -> None: kv_store = get_dynamic_config_store() try: - kv_store.load(KV_REINDEX_KEY) + value = kv_store.load(KV_REINDEX_KEY) + logger.debug(f"Re-indexing flag has value {value}") return except ConfigNotFoundError: # Only need to update the flag if it hasn't been set pass # If their first deployment is after the changes, it will - # TODO enable this when the other changes go in, need to avoid + # enable this when the other changes go in, need to avoid # this being set to False, then the user indexes things on the old version - # docs_exist = check_docs_exist(db_session) - # connectors_exist = check_connectors_exist(db_session) - # if docs_exist or connectors_exist: - # kv_store.store(KV_REINDEX_KEY, True) - # else: - # kv_store.store(KV_REINDEX_KEY, False) + docs_exist = check_docs_exist(db_session) + connectors_exist = check_connectors_exist(db_session) + if docs_exist or connectors_exist: + kv_store.store(KV_REINDEX_KEY, True) + else: + kv_store.store(KV_REINDEX_KEY, False) def setup_vespa( diff --git a/backend/danswer/server/manage/search_settings.py b/backend/danswer/server/manage/search_settings.py index b604c0491..39356521d 100644 --- a/backend/danswer/server/manage/search_settings.py +++ b/backend/danswer/server/manage/search_settings.py @@ -26,6 +26,7 @@ from danswer.search.search_settings import update_search_settings from danswer.server.manage.models import FullModelVersionResponse from danswer.server.models import IdReturn from danswer.utils.logger import setup_logger +from shared_configs.configs import ALT_INDEX_SUFFIX router = APIRouter(prefix="/search-settings") logger = setup_logger() @@ -55,11 +56,10 @@ def set_new_embedding_model( embed_model_details.cloud_provider_id = cloud_id + # account for same model name being indexed with two different configurations if embed_model_details.model_name == current_model.model_name: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="New embedding model is the same as the currently active one.", - ) + if not current_model.model_name.endswith(ALT_INDEX_SUFFIX): + embed_model_details.model_name += ALT_INDEX_SUFFIX secondary_model = get_secondary_db_embedding_model(db_session) diff --git a/backend/danswer/server/settings/api.py b/backend/danswer/server/settings/api.py index f75dc38a9..8dfb07869 100644 --- a/backend/danswer/server/settings/api.py +++ b/backend/danswer/server/settings/api.py @@ -3,6 +3,7 @@ from typing import cast from fastapi import APIRouter from fastapi import Depends from fastapi import HTTPException +from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import Session from danswer.auth.users import current_admin_user @@ -55,7 +56,18 @@ def fetch_settings( Postgres calls""" general_settings = load_settings() user_notifications = get_user_notifications(user, db_session) - return UserSettings(**general_settings.dict(), notifications=user_notifications) + + try: + kv_store = get_dynamic_config_store() + needs_reindexing = cast(bool, kv_store.load(KV_REINDEX_KEY)) + except ConfigNotFoundError: + needs_reindexing = False + + return UserSettings( + **general_settings.dict(), + notifications=user_notifications, + needs_reindexing=needs_reindexing + ) @basic_router.post("/notifications/{notification_id}/dismiss") @@ -87,8 +99,8 @@ def get_user_notifications( kv_store = get_dynamic_config_store() try: - need_index = cast(bool, kv_store.load(KV_REINDEX_KEY)) - if not need_index: + needs_index = cast(bool, kv_store.load(KV_REINDEX_KEY)) + if not needs_index: dismiss_all_notifications( notif_type=NotificationType.REINDEX, db_session=db_session ) @@ -99,21 +111,35 @@ def get_user_notifications( logger.warning("Could not find reindex flag") return [] - reindex_notifs = get_notifications( - user=user, notif_type=NotificationType.REINDEX, db_session=db_session - ) + try: + # Need a transaction in order to prevent under-counting current notifications + db_session.begin() - if not reindex_notifs: - notif = create_notification( + reindex_notifs = get_notifications( user=user, notif_type=NotificationType.REINDEX, db_session=db_session ) - return [Notification.from_model(notif)] - if len(reindex_notifs) > 1: - logger.error("User has multiple reindex notifications") + if not reindex_notifs: + notif = create_notification( + user=user, + notif_type=NotificationType.REINDEX, + db_session=db_session, + ) + db_session.flush() + db_session.commit() + return [Notification.from_model(notif)] - reindex_notif = reindex_notifs[0] + if len(reindex_notifs) > 1: + logger.error("User has multiple reindex notifications") - update_notification_last_shown(notification=reindex_notif, db_session=db_session) + reindex_notif = reindex_notifs[0] + update_notification_last_shown( + notification=reindex_notif, db_session=db_session + ) - return [Notification.from_model(reindex_notif)] + db_session.commit() + return [Notification.from_model(reindex_notif)] + except SQLAlchemyError: + logger.exception("Error while processing notifications") + db_session.rollback() + return [] diff --git a/backend/danswer/server/settings/models.py b/backend/danswer/server/settings/models.py index 5a574fb14..e999e7294 100644 --- a/backend/danswer/server/settings/models.py +++ b/backend/danswer/server/settings/models.py @@ -61,3 +61,4 @@ class Settings(BaseModel): class UserSettings(Settings): notifications: list[Notification] + needs_reindexing: bool diff --git a/backend/model_server/encoders.py b/backend/model_server/encoders.py index 46f66fa8e..623782857 100644 --- a/backend/model_server/encoders.py +++ b/backend/model_server/encoders.py @@ -23,6 +23,7 @@ from model_server.constants import DEFAULT_VOYAGE_MODEL from model_server.constants import EmbeddingModelTextType from model_server.constants import EmbeddingProvider from model_server.utils import simple_log_function_time +from shared_configs.configs import ALT_INDEX_SUFFIX from shared_configs.configs import INDEXING_ONLY from shared_configs.enums import EmbedTextType from shared_configs.enums import RerankerProvider @@ -283,8 +284,11 @@ def embed_text( elif model_name is not None: prefixed_texts = [f"{prefix}{text}" for text in texts] if prefix else texts + + # strip additional metadata from model name right before constructing from Huggingface + stripped_model_name = model_name.removesuffix(ALT_INDEX_SUFFIX) local_model = get_embedding_model( - model_name=model_name, max_context_length=max_context_length + model_name=stripped_model_name, max_context_length=max_context_length ) embeddings_vectors = local_model.encode( prefixed_texts, normalize_embeddings=normalize_embeddings diff --git a/backend/shared_configs/configs.py b/backend/shared_configs/configs.py index 236379dd8..2311daca5 100644 --- a/backend/shared_configs/configs.py +++ b/backend/shared_configs/configs.py @@ -20,6 +20,8 @@ INTENT_MODEL_TAG = "v1.0.3" # Bi-Encoder, other details DOC_EMBEDDING_CONTEXT_SIZE = 512 +# Used to distinguish alternative indices +ALT_INDEX_SUFFIX = "__danswer_alt_index" # Used for loading defaults for automatic deployments and dev flows # For local, use: mixedbread-ai/mxbai-rerank-xsmall-v1 diff --git a/web/public/Mixedbread.png b/web/public/Mixedbread.png new file mode 100644 index 000000000..5a2f720ca Binary files /dev/null and b/web/public/Mixedbread.png differ diff --git a/web/public/microsoft.png b/web/public/microsoft.png new file mode 100644 index 000000000..11feffc1d Binary files /dev/null and b/web/public/microsoft.png differ diff --git a/web/public/nomic.svg b/web/public/nomic.svg new file mode 100644 index 000000000..aa32de038 --- /dev/null +++ b/web/public/nomic.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/web/src/app/admin/assistants/AssistantEditor.tsx b/web/src/app/admin/assistants/AssistantEditor.tsx index 3aa73a98b..c9e4e2be2 100644 --- a/web/src/app/admin/assistants/AssistantEditor.tsx +++ b/web/src/app/admin/assistants/AssistantEditor.tsx @@ -42,7 +42,7 @@ import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { FiInfo, FiPlus, FiX } from "react-icons/fi"; import * as Yup from "yup"; -import { FullLLMProvider } from "../models/llm/interfaces"; +import { FullLLMProvider } from "../configuration/llm/interfaces"; import CollapsibleSection from "./CollapsibleSection"; import { SuccessfulPersonaUpdateRedirectType } from "./enums"; import { Persona, StarterMessage } from "./interfaces"; diff --git a/web/src/app/admin/models/llm/ConfiguredLLMProviderDisplay.tsx b/web/src/app/admin/configuration/llm/ConfiguredLLMProviderDisplay.tsx similarity index 100% rename from web/src/app/admin/models/llm/ConfiguredLLMProviderDisplay.tsx rename to web/src/app/admin/configuration/llm/ConfiguredLLMProviderDisplay.tsx diff --git a/web/src/app/admin/models/llm/CustomLLMProviderUpdateForm.tsx b/web/src/app/admin/configuration/llm/CustomLLMProviderUpdateForm.tsx similarity index 100% rename from web/src/app/admin/models/llm/CustomLLMProviderUpdateForm.tsx rename to web/src/app/admin/configuration/llm/CustomLLMProviderUpdateForm.tsx diff --git a/web/src/app/admin/models/llm/LLMConfiguration.tsx b/web/src/app/admin/configuration/llm/LLMConfiguration.tsx similarity index 100% rename from web/src/app/admin/models/llm/LLMConfiguration.tsx rename to web/src/app/admin/configuration/llm/LLMConfiguration.tsx diff --git a/web/src/app/admin/models/llm/LLMProviderUpdateForm.tsx b/web/src/app/admin/configuration/llm/LLMProviderUpdateForm.tsx similarity index 100% rename from web/src/app/admin/models/llm/LLMProviderUpdateForm.tsx rename to web/src/app/admin/configuration/llm/LLMProviderUpdateForm.tsx diff --git a/web/src/app/admin/models/llm/constants.ts b/web/src/app/admin/configuration/llm/constants.ts similarity index 100% rename from web/src/app/admin/models/llm/constants.ts rename to web/src/app/admin/configuration/llm/constants.ts diff --git a/web/src/app/admin/models/llm/interfaces.ts b/web/src/app/admin/configuration/llm/interfaces.ts similarity index 100% rename from web/src/app/admin/models/llm/interfaces.ts rename to web/src/app/admin/configuration/llm/interfaces.ts diff --git a/web/src/app/admin/models/llm/page.tsx b/web/src/app/admin/configuration/llm/page.tsx similarity index 100% rename from web/src/app/admin/models/llm/page.tsx rename to web/src/app/admin/configuration/llm/page.tsx diff --git a/web/src/app/admin/configuration/search/UpgradingPage.tsx b/web/src/app/admin/configuration/search/UpgradingPage.tsx new file mode 100644 index 000000000..270b5ddf2 --- /dev/null +++ b/web/src/app/admin/configuration/search/UpgradingPage.tsx @@ -0,0 +1,117 @@ +import { ThreeDotsLoader } from "@/components/Loading"; +import { Modal } from "@/components/Modal"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ConnectorIndexingStatus } from "@/lib/types"; +import { Button, Text, Title } from "@tremor/react"; +import Link from "next/link"; +import { useState } from "react"; +import useSWR, { mutate } from "swr"; +import { ReindexingProgressTable } from "../../../../components/embedding/ReindexingProgressTable"; +import { ErrorCallout } from "@/components/ErrorCallout"; +import { + CloudEmbeddingModel, + HostedEmbeddingModel, +} from "../../../../components/embedding/interfaces"; +import { Connector } from "@/lib/connectors/connectors"; + +export default function UpgradingPage({ + futureEmbeddingModel, +}: { + futureEmbeddingModel: CloudEmbeddingModel | HostedEmbeddingModel; +}) { + const [isCancelling, setIsCancelling] = useState(false); + + const { data: connectors } = useSWR[]>( + "/api/manage/connector", + errorHandlingFetcher, + { refreshInterval: 5000 } // 5 seconds + ); + + const { + data: ongoingReIndexingStatus, + isLoading: isLoadingOngoingReIndexingStatus, + } = useSWR[]>( + "/api/manage/admin/connector/indexing-status?secondary_index=true", + errorHandlingFetcher, + { refreshInterval: 5000 } // 5 seconds + ); + + const onCancel = async () => { + const response = await fetch("/api/search-settings/cancel-new-embedding", { + method: "POST", + }); + if (response.ok) { + mutate("/api/search-settings/get-secondary-embedding-model"); + } else { + alert( + `Failed to cancel embedding model update - ${await response.text()}` + ); + } + setIsCancelling(false); + }; + + return ( + <> + {isCancelling && ( + setIsCancelling(false)} + title="Cancel Embedding Model Switch" + > +
+
+ Are you sure you want to cancel? +
+
+ Cancelling will revert to the previous model and all progress will + be lost. +
+
+ +
+
+
+ )} + + {futureEmbeddingModel && connectors && connectors.length > 0 && ( +
+ Current Upgrade Status +
+
+ Currently in the process of switching to:{" "} + {futureEmbeddingModel.model_name} +
+ + + + + The table below shows the re-indexing progress of all existing + connectors. Once all connectors have been re-indexed successfully, + the new model will be used for all search queries. Until then, we + will use the old model so that no downtime is necessary during + this transition. + + + {isLoadingOngoingReIndexingStatus ? ( + + ) : ongoingReIndexingStatus ? ( + + ) : ( + + )} +
+
+ )} + + ); +} diff --git a/web/src/app/admin/configuration/search/page.tsx b/web/src/app/admin/configuration/search/page.tsx new file mode 100644 index 000000000..705ddc9f2 --- /dev/null +++ b/web/src/app/admin/configuration/search/page.tsx @@ -0,0 +1,191 @@ +"use client"; + +import { ThreeDotsLoader } from "@/components/Loading"; +import { AdminPageTitle } from "@/components/admin/Title"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { Button, Card, Text, Title } from "@tremor/react"; +import useSWR from "swr"; +import { ModelPreview } from "../../../../components/embedding/ModelSelector"; +import { + AVAILABLE_CLOUD_PROVIDERS, + HostedEmbeddingModel, + CloudEmbeddingModel, + AVAILABLE_MODELS, +} from "@/components/embedding/interfaces"; + +import { ErrorCallout } from "@/components/ErrorCallout"; + +export interface EmbeddingDetails { + api_key: string; + custom_config: any; + default_model_id?: number; + name: string; +} +import { EmbeddingIcon } from "@/components/icons/icons"; + +import Link from "next/link"; +import { SavedSearchSettings } from "../../embeddings/interfaces"; +import UpgradingPage from "./UpgradingPage"; +import { useContext } from "react"; +import { SettingsContext } from "@/components/settings/SettingsProvider"; + +function Main() { + const settings = useContext(SettingsContext); + const { + data: currentEmeddingModel, + isLoading: isLoadingCurrentModel, + error: currentEmeddingModelError, + } = useSWR( + "/api/search-settings/get-current-embedding-model", + errorHandlingFetcher, + { refreshInterval: 5000 } // 5 seconds + ); + + const { data: searchSettings, isLoading: isLoadingSearchSettings } = + useSWR( + "/api/search-settings/get-search-settings", + errorHandlingFetcher, + { refreshInterval: 5000 } // 5 seconds + ); + + const { + data: futureEmbeddingModel, + isLoading: isLoadingFutureModel, + error: futureEmeddingModelError, + } = useSWR( + "/api/search-settings/get-secondary-embedding-model", + errorHandlingFetcher, + { refreshInterval: 5000 } // 5 seconds + ); + + if ( + isLoadingCurrentModel || + isLoadingFutureModel || + isLoadingSearchSettings + ) { + return ; + } + + if ( + currentEmeddingModelError || + !currentEmeddingModel || + futureEmeddingModelError + ) { + return ; + } + + const currentModelName = currentEmeddingModel?.model_name; + const AVAILABLE_CLOUD_PROVIDERS_FLATTENED = AVAILABLE_CLOUD_PROVIDERS.flatMap( + (provider) => + provider.embedding_models.map((model) => ({ + ...model, + cloud_provider_id: provider.id, + model_name: model.model_name, // Ensure model_name is set for consistency + })) + ); + + const currentModel: CloudEmbeddingModel | HostedEmbeddingModel = + AVAILABLE_MODELS.find((model) => model.model_name === currentModelName) || + AVAILABLE_CLOUD_PROVIDERS_FLATTENED.find( + (model) => model.model_name === currentEmeddingModel.model_name + )!; + + return ( +
+ {!futureEmbeddingModel ? ( + <> + {settings?.settings.needs_reindexing && ( +

+ Your search settings are currently out of date! We recommend + updating your search settings and re-indexing. +

+ )} + Embedding Model + + {currentModel ? ( + + ) : ( + Choose your Embedding Model + )} + + Post-processing + + + {searchSettings && ( + <> +
+
+
+ Reranking Model + + {searchSettings.rerank_model_name || "Not set"} + +
+ +
+ Results to Rerank + + {searchSettings.num_rerank} + +
+ +
+ + Multilingual Expansion + + + {searchSettings.multilingual_expansion.length > 0 + ? searchSettings.multilingual_expansion.join(", ") + : "None"} + +
+ +
+ Multipass Indexing + + {searchSettings.multipass_indexing + ? "Enabled" + : "Disabled"} + +
+ +
+ + Disable Reranking for Streaming + + + {searchSettings.disable_rerank_for_streaming + ? "Yes" + : "No"} + +
+
+
+ + )} +
+ + + + + + ) : ( + + )} +
+ ); +} + +function Page() { + return ( +
+ } + /> +
+
+ ); +} + +export default Page; diff --git a/web/src/app/admin/connectors/[connector]/pages/Create.tsx b/web/src/app/admin/connectors/[connector]/pages/Create.tsx index 11170b4cb..2614a3463 100644 --- a/web/src/app/admin/connectors/[connector]/pages/Create.tsx +++ b/web/src/app/admin/connectors/[connector]/pages/Create.tsx @@ -1,4 +1,4 @@ -import React, { Dispatch, SetStateAction, useEffect } from "react"; +import React, { Dispatch, SetStateAction } from "react"; import { Formik, Form, Field, FieldArray } from "formik"; import * as Yup from "yup"; import { FaPlus } from "react-icons/fa"; diff --git a/web/src/app/admin/embeddings/EmbeddingModelSelectionForm.tsx b/web/src/app/admin/embeddings/EmbeddingModelSelectionForm.tsx new file mode 100644 index 000000000..ed83c18fb --- /dev/null +++ b/web/src/app/admin/embeddings/EmbeddingModelSelectionForm.tsx @@ -0,0 +1,306 @@ +"use client"; + +import { errorHandlingFetcher } from "@/lib/fetcher"; +import useSWR, { mutate } from "swr"; +import { Dispatch, SetStateAction, useState } from "react"; +import { + CloudEmbeddingProvider, + CloudEmbeddingModel, + AVAILABLE_CLOUD_PROVIDERS, + AVAILABLE_MODELS, + INVALID_OLD_MODEL, + HostedEmbeddingModel, + EmbeddingModelDescriptor, +} from "../../../components/embedding/interfaces"; +import { Connector } from "@/lib/connectors/connectors"; +import OpenEmbeddingPage from "./pages/OpenEmbeddingPage"; +import CloudEmbeddingPage from "./pages/CloudEmbeddingPage"; +import { ProviderCreationModal } from "./modals/ProviderCreationModal"; + +import { DeleteCredentialsModal } from "./modals/DeleteCredentialsModal"; +import { SelectModelModal } from "./modals/SelectModelModal"; +import { ChangeCredentialsModal } from "./modals/ChangeCredentialsModal"; +import { ModelSelectionConfirmationModal } from "./modals/ModelSelectionModal"; +import { AlreadyPickedModal } from "./modals/AlreadyPickedModal"; +import { ModelOption } from "../../../components/embedding/ModelSelector"; +import { EMBEDDING_PROVIDERS_ADMIN_URL } from "../configuration/llm/constants"; + +export interface EmbeddingDetails { + api_key: string; + custom_config: any; + default_model_id?: number; + name: string; +} + +export function EmbeddingModelSelection({ + selectedProvider, + currentEmbeddingModel, + updateSelectedProvider, + modelTab, + setModelTab, +}: { + modelTab: "open" | "cloud" | null; + setModelTab: Dispatch>; + currentEmbeddingModel: CloudEmbeddingModel | HostedEmbeddingModel; + selectedProvider: CloudEmbeddingModel | HostedEmbeddingModel; + updateSelectedProvider: ( + model: CloudEmbeddingModel | HostedEmbeddingModel + ) => void; +}) { + // Cloud Provider based modals + const [showTentativeProvider, setShowTentativeProvider] = + useState(null); + + const [showUnconfiguredProvider, setShowUnconfiguredProvider] = + useState(null); + const [changeCredentialsProvider, setChangeCredentialsProvider] = + useState(null); + + // Cloud Model based modals + const [alreadySelectedModel, setAlreadySelectedModel] = + useState(null); + const [showTentativeModel, setShowTentativeModel] = + useState(null); + + const [showModelInQueue, setShowModelInQueue] = + useState(null); + + // Open Model based modals + const [showTentativeOpenProvider, setShowTentativeOpenProvider] = + useState(null); + + // Enabled / unenabled providers + const [newEnabledProviders, setNewEnabledProviders] = useState([]); + const [newUnenabledProviders, setNewUnenabledProviders] = useState( + [] + ); + + const [showDeleteCredentialsModal, setShowDeleteCredentialsModal] = + useState(false); + const [showAddConnectorPopup, setShowAddConnectorPopup] = + useState(false); + + const { data: embeddingProviderDetails } = useSWR( + EMBEDDING_PROVIDERS_ADMIN_URL, + errorHandlingFetcher + ); + + const { data: connectors } = useSWR[]>( + "/api/manage/connector", + errorHandlingFetcher, + { refreshInterval: 5000 } // 5 seconds + ); + + const onConfirmSelection = async (model: EmbeddingModelDescriptor) => { + const response = await fetch( + "/api/search-settings/set-new-embedding-model", + { + method: "POST", + body: JSON.stringify(model), + headers: { + "Content-Type": "application/json", + }, + } + ); + if (response.ok) { + setShowTentativeModel(null); + mutate("/api/search-settings/get-secondary-embedding-model"); + if (!connectors || !connectors.length) { + setShowAddConnectorPopup(true); + } + } else { + alert(`Failed to update embedding model - ${await response.text()}`); + } + }; + + const onSelectOpenSource = async (model: HostedEmbeddingModel) => { + if (selectedProvider?.model_name === INVALID_OLD_MODEL) { + await onConfirmSelection(model); + } else { + setShowTentativeOpenProvider(model); + } + }; + + const clientsideAddProvider = (provider: CloudEmbeddingProvider) => { + const providerName = provider.name; + setNewEnabledProviders((newEnabledProviders) => [ + ...newEnabledProviders, + providerName, + ]); + setNewUnenabledProviders((newUnenabledProviders) => + newUnenabledProviders.filter( + (givenProvidername) => givenProvidername != providerName + ) + ); + }; + + const clientsideRemoveProvider = (provider: CloudEmbeddingProvider) => { + const providerName = provider.name; + setNewEnabledProviders((newEnabledProviders) => + newEnabledProviders.filter( + (givenProvidername) => givenProvidername != providerName + ) + ); + setNewUnenabledProviders((newUnenabledProviders) => [ + ...newUnenabledProviders, + providerName, + ]); + }; + + return ( +
+ {alreadySelectedModel && ( + setAlreadySelectedModel(null)} + /> + )} + + {showTentativeOpenProvider && ( + + model.model_name === showTentativeOpenProvider.model_name + ) === undefined + } + onConfirm={() => { + updateSelectedProvider(showTentativeOpenProvider); + setShowTentativeOpenProvider(null); + }} + onCancel={() => setShowTentativeOpenProvider(null)} + /> + )} + + {showTentativeProvider && ( + { + setShowTentativeProvider(showUnconfiguredProvider); + clientsideAddProvider(showTentativeProvider); + if (showModelInQueue) { + setShowTentativeModel(showModelInQueue); + } + }} + onCancel={() => { + setShowModelInQueue(null); + setShowTentativeProvider(null); + }} + /> + )} + {changeCredentialsProvider && ( + { + clientsideRemoveProvider(changeCredentialsProvider); + setChangeCredentialsProvider(null); + }} + provider={changeCredentialsProvider} + onConfirm={() => setChangeCredentialsProvider(null)} + onCancel={() => setChangeCredentialsProvider(null)} + /> + )} + + {showTentativeModel && ( + { + setShowModelInQueue(null); + updateSelectedProvider(showTentativeModel); + setShowTentativeModel(null); + }} + onCancel={() => { + setShowModelInQueue(null); + setShowTentativeModel(null); + }} + /> + )} + + {showDeleteCredentialsModal && ( + { + setShowDeleteCredentialsModal(false); + }} + onCancel={() => setShowDeleteCredentialsModal(false)} + /> + )} + +

+ Select from cloud, self-hosted models, or continue with your current + embedding model. +

+
+ +
+ +
+
+ +
+
+ + {modelTab == "open" && ( + + )} + + {modelTab == "cloud" && ( + + )} + + {!modelTab && ( + <> + + + )} +
+ ); +} diff --git a/web/src/app/admin/embeddings/RerankingFormPage.tsx b/web/src/app/admin/embeddings/RerankingFormPage.tsx new file mode 100644 index 000000000..37bb4c11a --- /dev/null +++ b/web/src/app/admin/embeddings/RerankingFormPage.tsx @@ -0,0 +1,243 @@ +import React, { Dispatch, forwardRef, SetStateAction, useState } from "react"; +import { Formik, Form, FormikProps } from "formik"; +import * as Yup from "yup"; +import { EditingValue } from "@/components/credentials/EditingValue"; +import { + RerankerProvider, + RerankingDetails, + rerankingModels, +} from "./interfaces"; +import { FiExternalLink } from "react-icons/fi"; +import { CohereIcon, MixedBreadIcon } from "@/components/icons/icons"; +import { Modal } from "@/components/Modal"; +import { Button } from "@tremor/react"; + +interface RerankingDetailsFormProps { + setRerankingDetails: Dispatch>; + currentRerankingDetails: RerankingDetails; + originalRerankingDetails: RerankingDetails; + modelTab: "open" | "cloud" | null; + setModelTab: Dispatch>; +} + +const RerankingDetailsForm = forwardRef< + FormikProps, + RerankingDetailsFormProps +>( + ( + { + setRerankingDetails, + originalRerankingDetails, + currentRerankingDetails, + modelTab, + setModelTab, + }, + ref + ) => { + const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false); + + return ( +
+

+ Post-processing +

+
+ {originalRerankingDetails.rerank_model_name && ( + + )} +
+ +
+ +
+ +
+
+ + () + .nullable() + .oneOf(Object.values(RerankerProvider)) + .optional(), + api_key: Yup.string().nullable(), + num_rerank: Yup.number().min(1, "Must be at least 1"), + })} + onSubmit={async (_, { setSubmitting }) => { + setSubmitting(false); + }} + enableReinitialize={true} + > + {({ values, setFieldValue }) => ( +
+
+ {(modelTab + ? rerankingModels.filter( + (model) => model.cloud == (modelTab == "cloud") + ) + : rerankingModels.filter( + (modelCard) => + modelCard.modelName == + originalRerankingDetails.rerank_model_name + ) + ).map((card) => { + const isSelected = + values.provider_type === card.provider && + values.rerank_model_name === card.modelName; + return ( +
{ + if (card.provider) { + setIsApiKeyModalOpen(true); + } + setRerankingDetails({ + ...values, + provider_type: card.provider!, + rerank_model_name: card.modelName, + }); + setFieldValue("provider_type", card.provider); + setFieldValue("rerank_model_name", card.modelName); + }} + > +
+
+ {card.provider === RerankerProvider.COHERE ? ( + + ) : ( + + )} +

+ {card.displayName} +

+
+ {card.link && ( + e.stopPropagation()} + className="text-blue-500 hover:text-blue-700 transition-colors duration-200" + > + + + )} +
+

+ {card.description} +

+
+ {card.cloud ? "Cloud-based" : "Self-hosted"} +
+
+ ); + })} +
+ + {isApiKeyModalOpen && ( + { + Object.keys(originalRerankingDetails).forEach((key) => { + setFieldValue( + key, + originalRerankingDetails[key as keyof RerankingDetails] + ); + }); + + setIsApiKeyModalOpen(false); + }} + width="w-[800px]" + title="API Key Configuration" + > +
+ { + setRerankingDetails({ ...values, api_key: value }); + setFieldValue("api_key", value); + }} + setFieldValue={setFieldValue} + type="password" + label="Cohere API Key" + name="api_key" + /> +
+ + +
+
+
+ )} +
+ )} +
+
+ ); + } +); + +RerankingDetailsForm.displayName = "RerankingDetailsForm"; +export default RerankingDetailsForm; diff --git a/web/src/app/admin/embeddings/interfaces.ts b/web/src/app/admin/embeddings/interfaces.ts new file mode 100644 index 000000000..f42df4e68 --- /dev/null +++ b/web/src/app/admin/embeddings/interfaces.ts @@ -0,0 +1,70 @@ +export interface RerankingDetails { + rerank_model_name: string | null; + provider_type: RerankerProvider | null; + api_key: string | null; + num_rerank: number; +} + +export enum RerankerProvider { + COHERE = "cohere", +} +export interface AdvancedDetails { + multilingual_expansion: string[]; + multipass_indexing: boolean; + disable_rerank_for_streaming: boolean; +} + +export interface SavedSearchSettings extends RerankingDetails { + multilingual_expansion: string[]; + multipass_indexing: boolean; + disable_rerank_for_streaming: boolean; +} + +export interface RerankingModel { + provider?: RerankerProvider; + modelName: string; + displayName: string; + description: string; + link: string; + cloud: boolean; +} + +export const rerankingModels: RerankingModel[] = [ + { + cloud: false, + modelName: "mixedbread-ai/mxbai-rerank-xsmall-v1", + displayName: "MixedBread XSmall", + description: "Fastest, smallest model for basic reranking tasks.", + link: "https://huggingface.co/mixedbread-ai/mxbai-rerank-xsmall-v1", + }, + { + cloud: false, + modelName: "mixedbread-ai/mxbai-rerank-base-v1", + displayName: "MixedBread Base", + description: "Balanced performance for general reranking needs.", + link: "https://huggingface.co/mixedbread-ai/mxbai-rerank-base-v1", + }, + { + cloud: false, + modelName: "mixedbread-ai/mxbai-rerank-large-v1", + displayName: "MixedBread Large", + description: "Most powerful model for complex reranking tasks.", + link: "https://huggingface.co/mixedbread-ai/mxbai-rerank-large-v1", + }, + { + cloud: true, + provider: RerankerProvider.COHERE, + modelName: "rerank-english-v3.0", + displayName: "Cohere English", + description: "High-performance English-focused reranking model.", + link: "https://docs.cohere.com/docs/rerank", + }, + { + cloud: true, + provider: RerankerProvider.COHERE, + modelName: "rerank-multilingual-v3.0", + displayName: "Cohere Multilingual", + description: "Powerful multilingual reranking model.", + link: "https://docs.cohere.com/docs/rerank", + }, +]; diff --git a/web/src/app/admin/models/embedding/modals/AlreadyPickedModal.tsx b/web/src/app/admin/embeddings/modals/AlreadyPickedModal.tsx similarity index 86% rename from web/src/app/admin/models/embedding/modals/AlreadyPickedModal.tsx rename to web/src/app/admin/embeddings/modals/AlreadyPickedModal.tsx index c8d9430c9..d6e294241 100644 --- a/web/src/app/admin/models/embedding/modals/AlreadyPickedModal.tsx +++ b/web/src/app/admin/embeddings/modals/AlreadyPickedModal.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Modal } from "@/components/Modal"; import { Button, Text } from "@tremor/react"; -import { CloudEmbeddingModel } from "../components/types"; +import { CloudEmbeddingModel } from "../../../../components/embedding/interfaces"; export function AlreadyPickedModal({ model, @@ -13,6 +13,7 @@ export function AlreadyPickedModal({ }) { return ( diff --git a/web/src/app/admin/models/embedding/modals/ChangeCredentialsModal.tsx b/web/src/app/admin/embeddings/modals/ChangeCredentialsModal.tsx similarity index 88% rename from web/src/app/admin/models/embedding/modals/ChangeCredentialsModal.tsx rename to web/src/app/admin/embeddings/modals/ChangeCredentialsModal.tsx index fa94ba9cc..aeeea06dc 100644 --- a/web/src/app/admin/models/embedding/modals/ChangeCredentialsModal.tsx +++ b/web/src/app/admin/embeddings/modals/ChangeCredentialsModal.tsx @@ -2,14 +2,12 @@ import React, { useRef, useState } from "react"; import { Modal } from "@/components/Modal"; import { Button, Text, Callout, Subtitle, Divider } from "@tremor/react"; import { Label, TextFormField } from "@/components/admin/connectors/Field"; -import { CloudEmbeddingProvider } from "../components/types"; +import { CloudEmbeddingProvider } from "../../../../components/embedding/interfaces"; import { EMBEDDING_PROVIDERS_ADMIN_URL, LLM_PROVIDERS_ADMIN_URL, -} from "../../llm/constants"; +} from "../../configuration/llm/constants"; import { mutate } from "swr"; -import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup"; -import { Field } from "formik"; export function ChangeCredentialsModal({ provider, @@ -151,16 +149,25 @@ export function ChangeCredentialsModal({ return (
- + Want to swap out your key? + + Visit API + -
+
{useFileUpload ? ( <> @@ -175,9 +182,6 @@ export function ChangeCredentialsModal({ ) : ( <> -
- -
)} - - Visit API -
{testError && ( @@ -211,13 +206,13 @@ export function ChangeCredentialsModal({ )} -
+
diff --git a/web/src/app/admin/models/embedding/modals/DeleteCredentialsModal.tsx b/web/src/app/admin/embeddings/modals/DeleteCredentialsModal.tsx similarity index 90% rename from web/src/app/admin/models/embedding/modals/DeleteCredentialsModal.tsx rename to web/src/app/admin/embeddings/modals/DeleteCredentialsModal.tsx index b13d8051c..335393060 100644 --- a/web/src/app/admin/models/embedding/modals/DeleteCredentialsModal.tsx +++ b/web/src/app/admin/embeddings/modals/DeleteCredentialsModal.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Modal } from "@/components/Modal"; import { Button, Text, Callout } from "@tremor/react"; -import { CloudEmbeddingProvider } from "../components/types"; +import { CloudEmbeddingProvider } from "../../../../components/embedding/interfaces"; export function DeleteCredentialsModal({ modelProvider, @@ -14,6 +14,7 @@ export function DeleteCredentialsModal({ }) { return ( diff --git a/web/src/app/admin/models/embedding/modals/ModelSelectionModal.tsx b/web/src/app/admin/embeddings/modals/ModelSelectionModal.tsx similarity index 91% rename from web/src/app/admin/models/embedding/modals/ModelSelectionModal.tsx rename to web/src/app/admin/embeddings/modals/ModelSelectionModal.tsx index e5918740e..3f59de116 100644 --- a/web/src/app/admin/models/embedding/modals/ModelSelectionModal.tsx +++ b/web/src/app/admin/embeddings/modals/ModelSelectionModal.tsx @@ -3,7 +3,7 @@ import { Button, Text, Callout } from "@tremor/react"; import { EmbeddingModelDescriptor, HostedEmbeddingModel, -} from "../components/types"; +} from "../../../../components/embedding/interfaces"; export function ModelSelectionConfirmationModal({ selectedModel, @@ -17,7 +17,11 @@ export function ModelSelectionConfirmationModal({ onCancel: () => void; }) { return ( - +
@@ -50,7 +54,7 @@ export function ModelSelectionConfirmationModal({
diff --git a/web/src/app/admin/models/embedding/modals/ProviderCreationModal.tsx b/web/src/app/admin/embeddings/modals/ProviderCreationModal.tsx similarity index 97% rename from web/src/app/admin/models/embedding/modals/ProviderCreationModal.tsx rename to web/src/app/admin/embeddings/modals/ProviderCreationModal.tsx index 981dfa0ca..3984709c8 100644 --- a/web/src/app/admin/models/embedding/modals/ProviderCreationModal.tsx +++ b/web/src/app/admin/embeddings/modals/ProviderCreationModal.tsx @@ -4,8 +4,8 @@ import { Formik, Form, Field } from "formik"; import * as Yup from "yup"; import { Label, TextFormField } from "@/components/admin/connectors/Field"; import { LoadingAnimation } from "@/components/Loading"; -import { CloudEmbeddingProvider } from "../components/types"; -import { EMBEDDING_PROVIDERS_ADMIN_URL } from "../../llm/constants"; +import { CloudEmbeddingProvider } from "../../../../components/embedding/interfaces"; +import { EMBEDDING_PROVIDERS_ADMIN_URL } from "../../configuration/llm/constants"; import { Modal } from "@/components/Modal"; export function ProviderCreationModal({ @@ -133,6 +133,7 @@ export function ProviderCreationModal({ return (
- You're about to set your embedding model to {model.model_name}. + You're selecting a new embedding model, {model.model_name}. If + you update to this model, you will need to undergo a complete + re-indexing.
Are you sure?
diff --git a/web/src/app/admin/embeddings/page.tsx b/web/src/app/admin/embeddings/page.tsx new file mode 100644 index 000000000..137602fa1 --- /dev/null +++ b/web/src/app/admin/embeddings/page.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { EmbeddingFormProvider } from "@/components/context/EmbeddingContext"; +import EmbeddingSidebar from "../../../components/embedding/EmbeddingSidebar"; +import EmbeddingForm from "./pages/EmbeddingFormPage"; + +export default function EmbeddingWrapper() { + return ( + +
+ +
+ +
+
+
+ ); +} diff --git a/web/src/app/admin/embeddings/pages/AdvancedEmbeddingFormPage.tsx b/web/src/app/admin/embeddings/pages/AdvancedEmbeddingFormPage.tsx new file mode 100644 index 000000000..c6bd89d11 --- /dev/null +++ b/web/src/app/admin/embeddings/pages/AdvancedEmbeddingFormPage.tsx @@ -0,0 +1,191 @@ +import React, { Dispatch, forwardRef, SetStateAction } from "react"; +import { Formik, Form, FormikProps, FieldArray, Field } from "formik"; +import * as Yup from "yup"; +import { EditingValue } from "@/components/credentials/EditingValue"; +import CredentialSubText from "@/components/credentials/CredentialFields"; +import { TrashIcon } from "@/components/icons/icons"; +import { FaPlus } from "react-icons/fa"; +import { AdvancedDetails, RerankingDetails } from "../interfaces"; + +interface AdvancedEmbeddingFormPageProps { + updateAdvancedEmbeddingDetails: ( + key: keyof AdvancedDetails, + value: any + ) => void; + advancedEmbeddingDetails: AdvancedDetails; + setRerankingDetails: Dispatch>; + numRerank: number; +} + +const AdvancedEmbeddingFormPage = forwardRef< + FormikProps, + AdvancedEmbeddingFormPageProps +>( + ( + { + updateAdvancedEmbeddingDetails, + advancedEmbeddingDetails, + setRerankingDetails, + numRerank, + }, + ref + ) => { + return ( +
+

+ Advanced Configuration +

+ { + setSubmitting(false); + }} + enableReinitialize={true} + > + {({ values, setFieldValue }) => ( +
+ + {({ push, remove }) => ( +
+ + + List of languages for multilingual expansion. Leave empty + for no additional expansion. + + {values.multilingual_expansion.map( + (_: any, index: number) => ( +
+ + ) => { + const newValue = [ + ...values.multilingual_expansion, + ]; + newValue[index] = e.target.value; + setFieldValue("multilingual_expansion", newValue); + updateAdvancedEmbeddingDetails( + "multilingual_expansion", + newValue + ); + }} + value={values.multilingual_expansion[index]} + /> + + +
+ ) + )} + + +
+ )} +
+ +
+ { + updateAdvancedEmbeddingDetails("multipass_indexing", value); + setFieldValue("multipass_indexing", value); + }} + setFieldValue={setFieldValue} + type="checkbox" + label="Multipass Indexing" + name="multipassIndexing" + /> +
+
+ { + updateAdvancedEmbeddingDetails( + "disable_rerank_for_streaming", + value + ); + setFieldValue("disable_rerank_for_streaming", value); + }} + setFieldValue={setFieldValue} + type="checkbox" + label="Disable Rerank for Streaming" + name="disableRerankForStreaming" + /> +
+
+ { + setRerankingDetails({ ...values, num_rerank: value }); + setFieldValue("num_rerank", value); + }} + setFieldValue={setFieldValue} + type="number" + label="Number of Results to Rerank" + name="num_rerank" + /> +
+
+ )} +
+
+ ); + } +); + +AdvancedEmbeddingFormPage.displayName = "AdvancedEmbeddingFormPage"; +export default AdvancedEmbeddingFormPage; diff --git a/web/src/app/admin/embeddings/pages/CloudEmbeddingPage.tsx b/web/src/app/admin/embeddings/pages/CloudEmbeddingPage.tsx new file mode 100644 index 000000000..acf4c8d78 --- /dev/null +++ b/web/src/app/admin/embeddings/pages/CloudEmbeddingPage.tsx @@ -0,0 +1,195 @@ +"use client"; + +import { Text, Title } from "@tremor/react"; + +import { + CloudEmbeddingProvider, + CloudEmbeddingModel, + AVAILABLE_CLOUD_PROVIDERS, + CloudEmbeddingProviderFull, + EmbeddingModelDescriptor, +} from "../../../../components/embedding/interfaces"; +import { EmbeddingDetails } from "../EmbeddingModelSelectionForm"; +import { FiExternalLink, FiInfo } from "react-icons/fi"; +import { HoverPopup } from "@/components/HoverPopup"; +import { Dispatch, SetStateAction } from "react"; + +export default function CloudEmbeddingPage({ + currentModel, + embeddingProviderDetails, + newEnabledProviders, + newUnenabledProviders, + setShowTentativeProvider, + setChangeCredentialsProvider, + setAlreadySelectedModel, + setShowTentativeModel, + setShowModelInQueue, +}: { + setShowModelInQueue: Dispatch>; + setShowTentativeModel: Dispatch>; + currentModel: EmbeddingModelDescriptor | CloudEmbeddingModel; + setAlreadySelectedModel: Dispatch>; + newUnenabledProviders: string[]; + embeddingProviderDetails?: EmbeddingDetails[]; + newEnabledProviders: string[]; + setShowTentativeProvider: React.Dispatch< + React.SetStateAction + >; + setChangeCredentialsProvider: React.Dispatch< + React.SetStateAction + >; +}) { + function hasNameInArray( + arr: Array<{ name: string }>, + searchName: string + ): boolean { + return arr.some( + (item) => item.name.toLowerCase() === searchName.toLowerCase() + ); + } + + let providers: CloudEmbeddingProviderFull[] = AVAILABLE_CLOUD_PROVIDERS.map( + (model) => ({ + ...model, + configured: + !newUnenabledProviders.includes(model.name) && + (newEnabledProviders.includes(model.name) || + (embeddingProviderDetails && + hasNameInArray(embeddingProviderDetails, model.name))!), + }) + ); + + return ( +
+ + Here are some cloud-based models to choose from. + + + These models require API keys and run in the clouds of the respective + providers. + + +
+ {providers.map((provider) => ( +
+
+ {provider.icon({ size: 40 })} +

+ {provider.name} {provider.name == "Cohere" && "(recommended)"} +

+ + } + popupContent={ +
+
{provider.description}
+
+ } + style="dark" + /> +
+ + +
+ {provider.embedding_models.map((model) => ( + + ))} +
+
+ ))} +
+
+ ); +} + +export function CloudModelCard({ + model, + provider, + currentModel, + setAlreadySelectedModel, + setShowTentativeModel, + setShowModelInQueue, + setShowTentativeProvider, +}: { + model: CloudEmbeddingModel; + provider: CloudEmbeddingProviderFull; + currentModel: EmbeddingModelDescriptor | CloudEmbeddingModel; + setAlreadySelectedModel: Dispatch>; + setShowTentativeModel: Dispatch>; + setShowModelInQueue: Dispatch>; + setShowTentativeProvider: React.Dispatch< + React.SetStateAction + >; +}) { + const enabled = model.model_name === currentModel.model_name; + + return ( +
+ +

{model.description}

+
+ ${model.pricePerMillion}/M tokens +
+
+ +
+
+ ); +} diff --git a/web/src/app/admin/embeddings/pages/EmbeddingFormPage.tsx b/web/src/app/admin/embeddings/pages/EmbeddingFormPage.tsx new file mode 100644 index 000000000..c156dad5c --- /dev/null +++ b/web/src/app/admin/embeddings/pages/EmbeddingFormPage.tsx @@ -0,0 +1,431 @@ +"use client"; +import { usePopup } from "@/components/admin/connectors/Popup"; +import { HealthCheckBanner } from "@/components/health/healthcheck"; + +import { EmbeddingModelSelection } from "../EmbeddingModelSelectionForm"; +import { useEffect, useState } from "react"; +import { Button, Card, Text } from "@tremor/react"; +import { ArrowLeft, ArrowRight, WarningCircle } from "@phosphor-icons/react"; +import { + CloudEmbeddingModel, + EmbeddingModelDescriptor, + HostedEmbeddingModel, +} from "../../../../components/embedding/interfaces"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; +import useSWR, { mutate } from "swr"; +import { ThreeDotsLoader } from "@/components/Loading"; +import AdvancedEmbeddingFormPage from "./AdvancedEmbeddingFormPage"; +import { + AdvancedDetails, + RerankingDetails, + SavedSearchSettings, +} from "../interfaces"; +import RerankingDetailsForm from "../RerankingFormPage"; +import { useEmbeddingFormContext } from "@/components/context/EmbeddingContext"; +import { Modal } from "@/components/Modal"; + +export default function EmbeddingForm() { + const { formStep, nextFormStep, prevFormStep } = useEmbeddingFormContext(); + const { popup, setPopup } = usePopup(); + + const [advancedEmbeddingDetails, setAdvancedEmbeddingDetails] = + useState({ + disable_rerank_for_streaming: false, + multilingual_expansion: [], + multipass_indexing: true, + }); + + const [rerankingDetails, setRerankingDetails] = useState({ + api_key: "", + num_rerank: 0, + provider_type: null, + rerank_model_name: "", + }); + + const updateAdvancedEmbeddingDetails = ( + key: keyof AdvancedDetails, + value: any + ) => { + setAdvancedEmbeddingDetails((values) => ({ ...values, [key]: value })); + }; + + async function updateSearchSettings(searchSettings: SavedSearchSettings) { + const response = await fetch( + "/api/search-settings/update-search-settings", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + ...searchSettings, + }), + } + ); + return response; + } + + const updateSelectedProvider = ( + model: CloudEmbeddingModel | HostedEmbeddingModel + ) => { + setSelectedProvider(model); + }; + const [displayPoorModelName, setDisplayPoorModelName] = useState(true); + const [showPoorModel, setShowPoorModel] = useState(false); + const [modelTab, setModelTab] = useState<"open" | "cloud" | null>(null); + + const { + data: currentEmbeddingModel, + isLoading: isLoadingCurrentModel, + error: currentEmbeddingModelError, + } = useSWR( + "/api/search-settings/get-current-embedding-model", + errorHandlingFetcher, + { refreshInterval: 5000 } // 5 seconds + ); + + const [selectedProvider, setSelectedProvider] = useState< + CloudEmbeddingModel | HostedEmbeddingModel | null + >(currentEmbeddingModel!); + + const { data: searchSettings, isLoading: isLoadingSearchSettings } = + useSWR( + "/api/search-settings/get-search-settings", + errorHandlingFetcher, + { refreshInterval: 5000 } // 5 seconds + ); + + useEffect(() => { + if (searchSettings) { + setAdvancedEmbeddingDetails({ + disable_rerank_for_streaming: + searchSettings.disable_rerank_for_streaming, + multilingual_expansion: searchSettings.multilingual_expansion, + multipass_indexing: searchSettings.multipass_indexing, + }); + setRerankingDetails({ + api_key: searchSettings.api_key, + num_rerank: searchSettings.num_rerank, + provider_type: searchSettings.provider_type, + rerank_model_name: searchSettings.rerank_model_name, + }); + } + }, [searchSettings]); + + const originalRerankingDetails = searchSettings + ? { + api_key: searchSettings.api_key, + num_rerank: searchSettings.num_rerank, + provider_type: searchSettings.provider_type, + rerank_model_name: searchSettings.rerank_model_name, + } + : { + api_key: "", + num_rerank: 0, + provider_type: null, + rerank_model_name: "", + }; + + useEffect(() => { + if (currentEmbeddingModel) { + setSelectedProvider(currentEmbeddingModel); + } + }, [currentEmbeddingModel]); + + useEffect(() => { + if (currentEmbeddingModel) { + setSelectedProvider(currentEmbeddingModel); + } + }, [currentEmbeddingModel]); + if (!selectedProvider) { + return ; + } + if (currentEmbeddingModelError || !currentEmbeddingModel) { + return ; + } + + const updateSearch = async () => { + let values: SavedSearchSettings = { + ...rerankingDetails, + ...advancedEmbeddingDetails, + }; + const response = await updateSearchSettings(values); + if (response.ok) { + setPopup({ + message: "Updated search settings succesffuly", + type: "success", + }); + mutate("/api/search-settings/get-search-settings"); + return true; + } else { + setPopup({ message: "Failed to update search settings", type: "error" }); + return false; + } + }; + + const onConfirm = async () => { + let newModel: EmbeddingModelDescriptor; + + if ("cloud_provider_name" in selectedProvider) { + // This is a CloudEmbeddingModel + newModel = { + ...selectedProvider, + model_name: selectedProvider.model_name, + cloud_provider_name: selectedProvider.cloud_provider_name, + }; + } else { + // This is an EmbeddingModelDescriptor + newModel = { + ...selectedProvider, + model_name: selectedProvider.model_name!, + description: "", + cloud_provider_name: null, + }; + } + + const response = await fetch( + "/api/search-settings/set-new-embedding-model", + { + method: "POST", + body: JSON.stringify(newModel), + headers: { + "Content-Type": "application/json", + }, + } + ); + if (response.ok) { + setPopup({ + message: "Changed provider suceessfully. Redirecing to embedding page", + type: "success", + }); + mutate("/api/search-settings/get-secondary-embedding-model"); + setTimeout(() => { + window.open("/admin/configuration/search", "_self"); + }, 2000); + } else { + setPopup({ message: "Failed to update embedding model", type: "error" }); + + alert(`Failed to update embedding model - ${await response.text()}`); + } + }; + + const needsReIndex = + currentEmbeddingModel != selectedProvider || + searchSettings?.multipass_indexing != + advancedEmbeddingDetails.multipass_indexing; + + const ReIndxingButton = () => { + return ( +
+ +
+ +
+

Needs re-indexing due to:

+
    + {currentEmbeddingModel != selectedProvider && ( +
  • Changed embedding provider
  • + )} + {searchSettings?.multipass_indexing != + advancedEmbeddingDetails.multipass_indexing && ( +
  • Multipass indexing modification
  • + )} +
+
+
+
+ ); + }; + + return ( +
+ {popup} + +
+ +
+
+ {formStep == 0 && ( + <> +

+ Select an Embedding Model +

+ + Note that updating the backing model will require a complete + re-indexing of all documents across every connected source. This + is taken care of in the background so that the system can continue + to be used, but depending on the size of the corpus, this could + take hours or days. You can monitor the progress of the + re-indexing on this page while the models are being switched. + + + + +
+ +
+ + )} + {showPoorModel && ( + setShowPoorModel(false)} + width="max-w-3xl" + title={`Are you sure you want to select ${selectedProvider.model_name}?`} + > + <> +
+ {selectedProvider.model_name} is a low-performance model. +
+ We recommend the following alternatives. +
  • OpenAI for cloud-based
  • +
  • Nomic for self-hosted
  • +
    +
    + + +
    + +
    + )} + + {formStep == 1 && ( + <> + + + + +
    + + + {needsReIndex ? ( + + ) : ( + + )} + +
    + +
    +
    + + )} + {formStep == 2 && ( + <> + + + + +
    + + + {needsReIndex ? ( + + ) : ( + + )} +
    + + )} +
    +
    + ); +} diff --git a/web/src/app/admin/embeddings/pages/OpenEmbeddingPage.tsx b/web/src/app/admin/embeddings/pages/OpenEmbeddingPage.tsx new file mode 100644 index 000000000..2e28ce8e4 --- /dev/null +++ b/web/src/app/admin/embeddings/pages/OpenEmbeddingPage.tsx @@ -0,0 +1,69 @@ +"use client"; +import { Button, Card, Text } from "@tremor/react"; +import { ModelSelector } from "../../../../components/embedding/ModelSelector"; +import { + AVAILABLE_MODELS, + CloudEmbeddingModel, + HostedEmbeddingModel, +} from "../../../../components/embedding/interfaces"; +import { CustomModelForm } from "../../../../components/embedding/CustomModelForm"; +import { useState } from "react"; +import { Title } from "@tremor/react"; +export default function OpenEmbeddingPage({ + onSelectOpenSource, + selectedProvider, +}: { + onSelectOpenSource: (model: HostedEmbeddingModel) => Promise; + selectedProvider: HostedEmbeddingModel | CloudEmbeddingModel; +}) { + const [configureModel, setConfigureModel] = useState(false); + return ( +
    + + Here are some locally-hosted models to choose from. + + + These models can be used without any API keys, and can leverage a GPU + for faster inference. + + + + + Alternatively, (if you know what you're doing) you can specify a{" "} + + SentenceTransformers + + -compatible model of your choice below. The rough list of supported + models can be found{" "} + + here + + . +
    + NOTE: not all models listed will work with Danswer, since some + have unique interfaces or special requirements. If in doubt, reach out + to the Danswer team. +
    + {!configureModel && ( + + )} + {configureModel && ( +
    + + + +
    + )} +
    + ); +} diff --git a/web/src/app/admin/models/embedding/CloudEmbeddingPage.tsx b/web/src/app/admin/models/embedding/CloudEmbeddingPage.tsx deleted file mode 100644 index db0388a21..000000000 --- a/web/src/app/admin/models/embedding/CloudEmbeddingPage.tsx +++ /dev/null @@ -1,169 +0,0 @@ -"use client"; - -import { Text, Title } from "@tremor/react"; - -import { - CloudEmbeddingProvider, - CloudEmbeddingModel, - AVAILABLE_CLOUD_PROVIDERS, - CloudEmbeddingProviderFull, - EmbeddingModelDescriptor, -} from "./components/types"; -import { EmbeddingDetails } from "./page"; -import { FiInfo } from "react-icons/fi"; -import { HoverPopup } from "@/components/HoverPopup"; -import { Dispatch, SetStateAction } from "react"; - -export default function CloudEmbeddingPage({ - currentModel, - embeddingProviderDetails, - newEnabledProviders, - newUnenabledProviders, - setShowTentativeProvider, - setChangeCredentialsProvider, - setAlreadySelectedModel, - setShowTentativeModel, - setShowModelInQueue, -}: { - setShowModelInQueue: Dispatch>; - setShowTentativeModel: Dispatch>; - currentModel: EmbeddingModelDescriptor | CloudEmbeddingModel; - setAlreadySelectedModel: Dispatch>; - newUnenabledProviders: string[]; - embeddingProviderDetails?: EmbeddingDetails[]; - newEnabledProviders: string[]; - selectedModel: CloudEmbeddingProvider; - - // create modal functions - - setShowTentativeProvider: React.Dispatch< - React.SetStateAction - >; - setChangeCredentialsProvider: React.Dispatch< - React.SetStateAction - >; -}) { - function hasNameInArray( - arr: Array<{ name: string }>, - searchName: string - ): boolean { - return arr.some( - (item) => item.name.toLowerCase() === searchName.toLowerCase() - ); - } - - let providers: CloudEmbeddingProviderFull[] = []; - AVAILABLE_CLOUD_PROVIDERS.forEach((model, ind) => { - let temporary_model: CloudEmbeddingProviderFull = { - ...model, - configured: - !newUnenabledProviders.includes(model.name) && - (newEnabledProviders.includes(model.name) || - (embeddingProviderDetails && - hasNameInArray(embeddingProviderDetails, model.name))!), - }; - providers.push(temporary_model); - }); - - return ( -
    - - Here are some cloud-based models to choose from. - - - They require API keys and run in the clouds of the respective providers. - - -
    - {providers.map((provider, ind) => ( -
    -
    - {provider.icon({ size: 40 })} -

    {provider.name}

    - -
    - -
    - {provider.embedding_models.map((model, index) => { - const enabled = model.model_name == currentModel.model_name; - - return ( -
    { - if (enabled) { - setAlreadySelectedModel(model); - } else if (provider.configured) { - setShowTentativeModel(model); - } else { - setShowModelInQueue(model); - setShowTentativeProvider(provider); - } - }} - > -
    -
    - {model.model_name} -
    -

    - ${model.pricePerMillion}/M tokens -

    -
    -
    - {model.description} -
    -
    - ); - })} -
    - -
    - ))} -
    -
    - ); -} diff --git a/web/src/app/admin/models/embedding/OpenEmbeddingPage.tsx b/web/src/app/admin/models/embedding/OpenEmbeddingPage.tsx deleted file mode 100644 index 0777a4e38..000000000 --- a/web/src/app/admin/models/embedding/OpenEmbeddingPage.tsx +++ /dev/null @@ -1,51 +0,0 @@ -"use client"; -import { Card, Text } from "@tremor/react"; -import { ModelSelector } from "./components/ModelSelector"; -import { AVAILABLE_MODELS, HostedEmbeddingModel } from "./components/types"; -import { CustomModelForm } from "./components/CustomModelForm"; - -export default function OpenEmbeddingPage({ - onSelectOpenSource, - currentModelName, -}: { - currentModelName: string; - onSelectOpenSource: (model: HostedEmbeddingModel) => Promise; -}) { - return ( -
    - modelOption.model_name !== currentModelName - )} - setSelectedModel={onSelectOpenSource} - /> - - - Alternatively, (if you know what you're doing) you can specify a{" "} - - SentenceTransformers - - -compatible model of your choice below. The rough list of supported - models can be found{" "} - - here - - . -
    - NOTE: not all models listed will work with Danswer, since some - have unique interfaces or special requirements. If in doubt, reach out - to the Danswer team. -
    - -
    - - - -
    -
    - ); -} diff --git a/web/src/app/admin/models/embedding/components/ModelSelector.tsx b/web/src/app/admin/models/embedding/components/ModelSelector.tsx deleted file mode 100644 index 2819fa5ce..000000000 --- a/web/src/app/admin/models/embedding/components/ModelSelector.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { EmbeddingModelDescriptor, HostedEmbeddingModel } from "./types"; -import { FiStar } from "react-icons/fi"; - -export function ModelPreview({ model }: { model: EmbeddingModelDescriptor }) { - return ( -
    -
    {model.model_name}
    -
    - {model.description - ? model.description - : "Custom model—no description is available."} -
    -
    - ); -} - -export function ModelOption({ - model, - onSelect, -}: { - model: HostedEmbeddingModel; - onSelect?: (model: HostedEmbeddingModel) => void; -}) { - return ( -
    -
    - {model.isDefault && } - {model.model_name} -
    -
    - {model.description - ? model.description - : "Custom model—no description is available."} -
    - {model.link && ( - - See More Details - - )} - {onSelect && ( -
    onSelect(model)} - > - Select Model -
    - )} -
    - ); -} - -export function ModelSelector({ - modelOptions, - setSelectedModel, -}: { - modelOptions: HostedEmbeddingModel[]; - setSelectedModel: (model: HostedEmbeddingModel) => void; -}) { - return ( -
    -
    - {modelOptions.map((modelOption) => ( - - ))} -
    -
    - ); -} diff --git a/web/src/app/admin/models/embedding/page.tsx b/web/src/app/admin/models/embedding/page.tsx deleted file mode 100644 index 45ae9d6ce..000000000 --- a/web/src/app/admin/models/embedding/page.tsx +++ /dev/null @@ -1,536 +0,0 @@ -"use client"; - -import { ThreeDotsLoader } from "@/components/Loading"; -import { AdminPageTitle } from "@/components/admin/Title"; -import { errorHandlingFetcher } from "@/lib/fetcher"; -import { Button, Text, Title } from "@tremor/react"; -import useSWR, { mutate } from "swr"; -import { ModelPreview } from "./components/ModelSelector"; -import { useState } from "react"; -import { ReindexingProgressTable } from "./components/ReindexingProgressTable"; -import { Modal } from "@/components/Modal"; -import { - CloudEmbeddingProvider, - CloudEmbeddingModel, - AVAILABLE_CLOUD_PROVIDERS, - AVAILABLE_MODELS, - INVALID_OLD_MODEL, - HostedEmbeddingModel, - EmbeddingModelDescriptor, -} from "./components/types"; -import { ErrorCallout } from "@/components/ErrorCallout"; -import { ConnectorIndexingStatus } from "@/lib/types"; -import { Connector } from "@/lib/connectors/connectors"; -import Link from "next/link"; -import OpenEmbeddingPage from "./OpenEmbeddingPage"; -import CloudEmbeddingPage from "./CloudEmbeddingPage"; -import { ProviderCreationModal } from "./modals/ProviderCreationModal"; - -import { DeleteCredentialsModal } from "./modals/DeleteCredentialsModal"; -import { SelectModelModal } from "./modals/SelectModelModal"; -import { ChangeCredentialsModal } from "./modals/ChangeCredentialsModal"; -import { ModelSelectionConfirmationModal } from "./modals/ModelSelectionModal"; -import { EMBEDDING_PROVIDERS_ADMIN_URL } from "../llm/constants"; -import { AlreadyPickedModal } from "./modals/AlreadyPickedModal"; - -export interface EmbeddingDetails { - api_key: string; - custom_config: any; - default_model_id?: number; - name: string; -} -import { EmbeddingIcon, PackageIcon } from "@/components/icons/icons"; - -function Main() { - const [openToggle, setOpenToggle] = useState(true); - - // Cloud Provider based modals - const [showTentativeProvider, setShowTentativeProvider] = - useState(null); - const [showUnconfiguredProvider, setShowUnconfiguredProvider] = - useState(null); - const [changeCredentialsProvider, setChangeCredentialsProvider] = - useState(null); - - // Cloud Model based modals - const [alreadySelectedModel, setAlreadySelectedModel] = - useState(null); - const [showTentativeModel, setShowTentativeModel] = - useState(null); - - const [showModelInQueue, setShowModelInQueue] = - useState(null); - - // Open Model based modals - const [showTentativeOpenProvider, setShowTentativeOpenProvider] = - useState(null); - - // Enabled / unenabled providers - const [newEnabledProviders, setNewEnabledProviders] = useState([]); - const [newUnenabledProviders, setNewUnenabledProviders] = useState( - [] - ); - - const [showDeleteCredentialsModal, setShowDeleteCredentialsModal] = - useState(false); - const [isCancelling, setIsCancelling] = useState(false); - const [showAddConnectorPopup, setShowAddConnectorPopup] = - useState(false); - - const { - data: currentEmeddingModel, - isLoading: isLoadingCurrentModel, - error: currentEmeddingModelError, - } = useSWR( - "/api/search-settings/get-current-embedding-model", - errorHandlingFetcher, - { refreshInterval: 5000 } // 5 seconds - ); - - const { data: embeddingProviderDetails } = useSWR( - EMBEDDING_PROVIDERS_ADMIN_URL, - errorHandlingFetcher - ); - - const { - data: futureEmbeddingModel, - isLoading: isLoadingFutureModel, - error: futureEmeddingModelError, - } = useSWR( - "/api/search-settings/get-secondary-embedding-model", - errorHandlingFetcher, - { refreshInterval: 5000 } // 5 seconds - ); - const { - data: ongoingReIndexingStatus, - isLoading: isLoadingOngoingReIndexingStatus, - } = useSWR[]>( - "/api/manage/admin/connector/indexing-status?secondary_index=true", - errorHandlingFetcher, - { refreshInterval: 5000 } // 5 seconds - ); - const { data: connectors } = useSWR[]>( - "/api/manage/connector", - errorHandlingFetcher, - { refreshInterval: 5000 } // 5 seconds - ); - - const onConfirm = async ( - model: CloudEmbeddingModel | HostedEmbeddingModel - ) => { - let newModel: EmbeddingModelDescriptor; - - if ("cloud_provider_name" in model) { - // This is a CloudEmbeddingModel - newModel = { - ...model, - model_name: model.model_name, - cloud_provider_name: model.cloud_provider_name, - // cloud_provider_id: model.cloud_provider_id || 0, - }; - } else { - // This is an EmbeddingModelDescriptor - newModel = { - ...model, - model_name: model.model_name!, - description: "", - cloud_provider_name: null, - }; - } - - const response = await fetch( - "/api/search-settings/set-new-embedding-model", - { - method: "POST", - body: JSON.stringify(newModel), - headers: { - "Content-Type": "application/json", - }, - } - ); - if (response.ok) { - setShowTentativeOpenProvider(null); - setShowTentativeModel(null); - mutate("/api/search-settings/get-secondary-embedding-model"); - if (!connectors || !connectors.length) { - setShowAddConnectorPopup(true); - } - } else { - alert(`Failed to update embedding model - ${await response.text()}`); - } - }; - - const onCancel = async () => { - const response = await fetch("/api/search-settings/cancel-new-embedding", { - method: "POST", - }); - if (response.ok) { - setShowTentativeModel(null); - mutate("/api/search-settings/get-secondary-embedding-model"); - } else { - alert( - `Failed to cancel embedding model update - ${await response.text()}` - ); - } - setIsCancelling(false); - }; - - if (isLoadingCurrentModel || isLoadingFutureModel) { - return ; - } - - if ( - currentEmeddingModelError || - !currentEmeddingModel || - futureEmeddingModelError - ) { - return ; - } - - const onConfirmSelection = async (model: EmbeddingModelDescriptor) => { - const response = await fetch( - "/api/search-settings/set-new-embedding-model", - { - method: "POST", - body: JSON.stringify(model), - headers: { - "Content-Type": "application/json", - }, - } - ); - if (response.ok) { - setShowTentativeModel(null); - mutate("/api/search-settings/get-secondary-embedding-model"); - if (!connectors || !connectors.length) { - setShowAddConnectorPopup(true); - } - } else { - alert(`Failed to update embedding model - ${await response.text()}`); - } - }; - - const currentModelName = currentEmeddingModel?.model_name; - const AVAILABLE_CLOUD_PROVIDERS_FLATTENED = AVAILABLE_CLOUD_PROVIDERS.flatMap( - (provider) => - provider.embedding_models.map((model) => ({ - ...model, - cloud_provider_id: provider.id, - model_name: model.model_name, // Ensure model_name is set for consistency - })) - ); - - const currentModel: CloudEmbeddingModel | HostedEmbeddingModel = - AVAILABLE_MODELS.find((model) => model.model_name === currentModelName) || - AVAILABLE_CLOUD_PROVIDERS_FLATTENED.find( - (model) => model.model_name === currentEmeddingModel.model_name - )!; - // || - // fillOutEmeddingModelDescriptor(currentEmeddingModel); - - const onSelectOpenSource = async (model: HostedEmbeddingModel) => { - if (currentEmeddingModel?.model_name === INVALID_OLD_MODEL) { - await onConfirmSelection(model); - } else { - setShowTentativeOpenProvider(model); - } - }; - - const selectedModel = AVAILABLE_CLOUD_PROVIDERS[0]; - const clientsideAddProvider = (provider: CloudEmbeddingProvider) => { - const providerName = provider.name; - setNewEnabledProviders((newEnabledProviders) => [ - ...newEnabledProviders, - providerName, - ]); - setNewUnenabledProviders((newUnenabledProviders) => - newUnenabledProviders.filter( - (givenProvidername) => givenProvidername != providerName - ) - ); - }; - - const clientsideRemoveProvider = (provider: CloudEmbeddingProvider) => { - const providerName = provider.name; - setNewEnabledProviders((newEnabledProviders) => - newEnabledProviders.filter( - (givenProvidername) => givenProvidername != providerName - ) - ); - setNewUnenabledProviders((newUnenabledProviders) => [ - ...newUnenabledProviders, - providerName, - ]); - }; - - return ( -
    - - These deep learning models are used to generate vector representations - of your documents, which then power Danswer's search. - - - {alreadySelectedModel && ( - setAlreadySelectedModel(null)} - /> - )} - - {showTentativeOpenProvider && ( - - model.model_name === showTentativeOpenProvider.model_name - ) === undefined - } - onConfirm={() => onConfirm(showTentativeOpenProvider)} - onCancel={() => setShowTentativeOpenProvider(null)} - /> - )} - - {showTentativeProvider && ( - { - setShowTentativeProvider(showUnconfiguredProvider); - clientsideAddProvider(showTentativeProvider); - if (showModelInQueue) { - setShowTentativeModel(showModelInQueue); - } - }} - onCancel={() => { - setShowModelInQueue(null); - setShowTentativeProvider(null); - }} - /> - )} - {changeCredentialsProvider && ( - { - clientsideRemoveProvider(changeCredentialsProvider); - setChangeCredentialsProvider(null); - }} - provider={changeCredentialsProvider} - onConfirm={() => setChangeCredentialsProvider(null)} - onCancel={() => setChangeCredentialsProvider(null)} - /> - )} - - {showTentativeModel && ( - { - setShowModelInQueue(null); - onConfirm(showTentativeModel); - }} - onCancel={() => { - setShowModelInQueue(null); - setShowTentativeModel(null); - }} - /> - )} - - {showDeleteCredentialsModal && ( - { - setShowDeleteCredentialsModal(false); - }} - onCancel={() => setShowDeleteCredentialsModal(false)} - /> - )} - - {currentModel ? ( - <> - Current Embedding Model - - - ) : ( - Choose your Embedding Model - )} - - {!(futureEmbeddingModel && connectors && connectors.length > 0) && ( - <> - Switch your Embedding Model - - Note that updating the backing model will require a complete - re-indexing of all documents across every connected source. This is - taken care of in the background so that the system can continue to - be used, but depending on the size of the corpus, this could take - hours or days. You can monitor the progress of the re-indexing on - this page while the models are being switched. - - -
    - -
    - -
    -
    - - )} - - {!showAddConnectorPopup && - !futureEmbeddingModel && - (openToggle ? ( - - ) : ( - - ))} - - {openToggle && ( - <> - {showAddConnectorPopup && ( - -
    -
    - - Embedding model successfully selected - {" "} - 🙌 -
    -
    - To complete the initial setup, let's add a connector! -
    -
    - Connectors are the way that Danswer gets data from your - organization's various data sources. Once setup, - we'll automatically sync data from your apps and docs - into Danswer, so you can search all through all of them in one - place. -
    -
    - - - -
    -
    -
    - )} - - {isCancelling && ( - setIsCancelling(false)} - title="Cancel Embedding Model Switch" - > -
    -
    - Are you sure you want to cancel? -
    -
    - Cancelling will revert to the previous model and all progress - will be lost. -
    -
    - -
    -
    -
    - )} - - )} - - {futureEmbeddingModel && connectors && connectors.length > 0 && ( -
    - Current Upgrade Status -
    -
    - Currently in the process of switching to:{" "} - {futureEmbeddingModel.model_name} -
    - {/* */} - - - - - The table below shows the re-indexing progress of all existing - connectors. Once all connectors have been re-indexed successfully, - the new model will be used for all search queries. Until then, we - will use the old model so that no downtime is necessary during - this transition. - - - {isLoadingOngoingReIndexingStatus ? ( - - ) : ongoingReIndexingStatus ? ( - - ) : ( - - )} -
    -
    - )} -
    - ); -} - -function Page() { - return ( -
    - } - /> - -
    -
    - ); -} - -export default Page; diff --git a/web/src/app/admin/settings/interfaces.ts b/web/src/app/admin/settings/interfaces.ts index 910403f1a..247bfd09d 100644 --- a/web/src/app/admin/settings/interfaces.ts +++ b/web/src/app/admin/settings/interfaces.ts @@ -3,6 +3,16 @@ export interface Settings { search_page_enabled: boolean; default_page: "search" | "chat"; maximum_chat_retention_days: number | null; + notifications: Notification[]; + needs_reindexing: boolean; +} + +export interface Notification { + id: number; + notif_type: string; + dismissed: boolean; + last_shown: string; + first_shown: string; } export interface EnterpriseSettings { diff --git a/web/src/app/chat/modal/SetDefaultModelModal.tsx b/web/src/app/chat/modal/SetDefaultModelModal.tsx index b930870b5..d8d75679d 100644 --- a/web/src/app/chat/modal/SetDefaultModelModal.tsx +++ b/web/src/app/chat/modal/SetDefaultModelModal.tsx @@ -1,13 +1,9 @@ -import { Dispatch, SetStateAction, useState } from "react"; +import { Dispatch, SetStateAction } from "react"; import { ModalWrapper } from "./ModalWrapper"; import { Badge, Text } from "@tremor/react"; -import { - getDisplayNameForModel, - LlmOverride, - LlmOverrideManager, - useLlmOverride, -} from "@/lib/hooks"; -import { LLMProviderDescriptor } from "@/app/admin/models/llm/interfaces"; +import { getDisplayNameForModel, LlmOverride } from "@/lib/hooks"; +import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces"; + import { destructureValue, structureValue } from "@/lib/llm/utils"; import { setUserDefaultModel } from "@/lib/users/UserSettings"; import { useRouter } from "next/navigation"; diff --git a/web/src/app/chat/modal/configuration/AssistantsTab.tsx b/web/src/app/chat/modal/configuration/AssistantsTab.tsx index c03796cca..d4933771f 100644 --- a/web/src/app/chat/modal/configuration/AssistantsTab.tsx +++ b/web/src/app/chat/modal/configuration/AssistantsTab.tsx @@ -15,7 +15,7 @@ import { } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { Persona } from "@/app/admin/assistants/interfaces"; -import { LLMProviderDescriptor } from "@/app/admin/models/llm/interfaces"; +import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces"; import { getFinalLLM } from "@/lib/llm/utils"; import React, { useState } from "react"; import { updateUserAssistantList } from "@/lib/assistants/updateAssistantPreferences"; diff --git a/web/src/app/search/page.tsx b/web/src/app/search/page.tsx index 4f0b4e217..2cc53e474 100644 --- a/web/src/app/search/page.tsx +++ b/web/src/app/search/page.tsx @@ -19,7 +19,7 @@ import { import { unstable_noStore as noStore } from "next/cache"; import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh"; import { personaComparator } from "../admin/assistants/lib"; -import { FullEmbeddingModelResponse } from "../admin/models/embedding/components/types"; +import { FullEmbeddingModelResponse } from "@/components/embedding/interfaces"; import { NoSourcesModal } from "@/components/initialSetup/search/NoSourcesModal"; import { NoCompleteSourcesModal } from "@/components/initialSetup/search/NoCompleteSourceModal"; import { ChatPopup } from "../chat/ChatPopup"; @@ -27,10 +27,8 @@ import { FetchAssistantsResponse, fetchAssistantsSS, } from "@/lib/assistants/fetchAssistantsSS"; -import FunctionalWrapper from "../chat/shared_chat_search/FunctionalWrapper"; import { ChatSession } from "../chat/interfaces"; import { SIDEBAR_TOGGLED_COOKIE_NAME } from "@/components/resizable/constants"; -import ToggleSearch from "./WrappedSearch"; import { AGENTIC_SEARCH_TYPE_COOKIE_NAME, NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN, diff --git a/web/src/components/admin/ClientLayout.tsx b/web/src/components/admin/ClientLayout.tsx index cdb6f5aa1..fe9349300 100644 --- a/web/src/components/admin/ClientLayout.tsx +++ b/web/src/components/admin/ClientLayout.tsx @@ -18,15 +18,18 @@ import { ZoomInIconSkeleton, SlackIconSkeleton, DocumentSetIconSkeleton, - EmbeddingIconSkeleton, AssistantsIconSkeleton, ClosedBookIcon, + SearchIcon, } from "@/components/icons/icons"; import { FiActivity, FiBarChart2 } from "react-icons/fi"; import { UserDropdown } from "../UserDropdown"; import { User } from "@/lib/types"; import { usePathname } from "next/navigation"; +import { SettingsContext } from "../settings/SettingsProvider"; +import { useContext } from "react"; +import { CustomTooltip } from "../tooltip/CustomTooltip"; export function ClientLayout({ user, @@ -38,8 +41,12 @@ export function ClientLayout({ enableEnterprise: boolean; }) { const pathname = usePathname(); + const settings = useContext(SettingsContext); - if (pathname.startsWith("/admin/connectors")) { + if ( + pathname.startsWith("/admin/connectors") || + pathname.startsWith("/admin/embeddings") + ) { return <>{children}; } @@ -157,26 +164,28 @@ export function ClientLayout({ ], }, { - name: "Model Configs", + name: "Configuration", items: [ { name: (
    - {/* */}
    LLM
    ), - link: "/admin/models/llm", + link: "/admin/configuration/llm", }, { + error: settings?.settings.needs_reindexing, name: (
    - -
    Embedding
    + + +
    Search Settings
    +
    ), - link: "/admin/models/embedding", + link: "/admin/configuration/search", }, ], }, diff --git a/web/src/components/admin/Layout.tsx b/web/src/components/admin/Layout.tsx index 2450d21a3..11f31a623 100644 --- a/web/src/components/admin/Layout.tsx +++ b/web/src/components/admin/Layout.tsx @@ -7,6 +7,7 @@ import { import { redirect } from "next/navigation"; import { ClientLayout } from "./ClientLayout"; import { SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED } from "@/lib/constants"; +import { AnnouncementBanner } from "../header/AnnouncementBanner"; export async function Layout({ children }: { children: React.ReactNode }) { const tasks = [getAuthTypeMetadataSS(), getCurrentUserSS()]; @@ -44,6 +45,7 @@ export async function Layout({ children }: { children: React.ReactNode }) { enableEnterprise={SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED} user={user} > + {children} ); diff --git a/web/src/components/admin/connectors/AdminSidebar.tsx b/web/src/components/admin/connectors/AdminSidebar.tsx index 8cd26b993..b1679d858 100644 --- a/web/src/components/admin/connectors/AdminSidebar.tsx +++ b/web/src/components/admin/connectors/AdminSidebar.tsx @@ -7,10 +7,18 @@ import { NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED } from "@/lib/constan import { HeaderTitle } from "@/components/header/HeaderTitle"; import { SettingsContext } from "@/components/settings/SettingsProvider"; import { BackIcon } from "@/components/icons/icons"; +import { WarningCircle, WarningDiamond } from "@phosphor-icons/react"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@radix-ui/react-tooltip"; interface Item { name: string | JSX.Element; link: string; + error?: boolean; } interface Collection { @@ -86,8 +94,24 @@ export function AdminSidebar({ collections }: { collections: Collection[] }) { {collection.items.map((item) => ( - ))} diff --git a/web/src/components/context/ChatContext.tsx b/web/src/components/context/ChatContext.tsx index 74772cc29..76cdf8adc 100644 --- a/web/src/components/context/ChatContext.tsx +++ b/web/src/components/context/ChatContext.tsx @@ -10,7 +10,7 @@ import { } from "@/lib/types"; import { ChatSession } from "@/app/chat/interfaces"; import { Persona } from "@/app/admin/assistants/interfaces"; -import { LLMProviderDescriptor } from "@/app/admin/models/llm/interfaces"; +import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces"; import { Folder } from "@/app/chat/folders/interfaces"; import { InputPrompt } from "@/app/admin/prompt-library/interfaces"; diff --git a/web/src/components/context/EmbeddingContext.tsx b/web/src/components/context/EmbeddingContext.tsx new file mode 100644 index 000000000..2ca18dc95 --- /dev/null +++ b/web/src/components/context/EmbeddingContext.tsx @@ -0,0 +1,103 @@ +import React, { + createContext, + useState, + useContext, + ReactNode, + useEffect, +} from "react"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { ValidSources } from "@/lib/types"; + +interface EmbeddingFormContextType { + formStep: number; + formValues: Record; + setFormValues: (values: Record) => void; + nextFormStep: (contract?: string) => void; + prevFormStep: () => void; + formStepToLast: () => void; + setFormStep: React.Dispatch>; + allowAdvanced: boolean; + setAllowAdvanced: React.Dispatch>; + allowCreate: boolean; + setAlowCreate: React.Dispatch>; +} + +const EmbeddingFormContext = createContext< + EmbeddingFormContextType | undefined +>(undefined); + +export const EmbeddingFormProvider: React.FC<{ + children: ReactNode; +}> = ({ children }) => { + const router = useRouter(); + const searchParams = useSearchParams(); + const pathname = usePathname(); + + // Initialize formStep based on the URL parameter + const initialStep = parseInt(searchParams.get("step") || "0", 10); + const [formStep, setFormStep] = useState(initialStep); + const [formValues, setFormValues] = useState>({}); + + const [allowAdvanced, setAllowAdvanced] = useState(false); + const [allowCreate, setAlowCreate] = useState(false); + + const nextFormStep = (values = "") => { + setFormStep((prevStep) => prevStep + 1); + setFormValues((prevValues) => ({ ...prevValues, values })); + }; + + const prevFormStep = () => { + setFormStep((currentStep) => Math.max(currentStep - 1, 0)); + }; + + const formStepToLast = () => { + setFormStep(2); + }; + + useEffect(() => { + // Update URL when formStep changes + const updatedSearchParams = new URLSearchParams(searchParams.toString()); + updatedSearchParams.set("step", formStep.toString()); + const newUrl = `${pathname}?${updatedSearchParams.toString()}`; + router.push(newUrl); + }, [formStep, router, pathname, searchParams]); + + // Update formStep when URL changes + useEffect(() => { + const stepFromUrl = parseInt(searchParams.get("step") || "0", 10); + if (stepFromUrl !== formStep) { + setFormStep(stepFromUrl); + } + }, [searchParams]); + + const contextValue: EmbeddingFormContextType = { + formStep, + formValues, + setFormValues: (values) => + setFormValues((prevValues) => ({ ...prevValues, ...values })), + nextFormStep, + prevFormStep, + formStepToLast, + setFormStep, + allowAdvanced, + setAllowAdvanced, + allowCreate, + setAlowCreate, + }; + + return ( + + {children} + + ); +}; + +export const useEmbeddingFormContext = () => { + const context = useContext(EmbeddingFormContext); + if (context === undefined) { + throw new Error( + "useEmbeddingFormContext must be used within a FormProvider" + ); + } + return context; +}; diff --git a/web/src/components/credentials/EditingValue.tsx b/web/src/components/credentials/EditingValue.tsx index d59be500f..26308f025 100644 --- a/web/src/components/credentials/EditingValue.tsx +++ b/web/src/components/credentials/EditingValue.tsx @@ -18,6 +18,7 @@ export const EditingValue: React.FC<{ // These are escape hatches from the overall // value editing component (when need to modify) + options?: { value: string; label: string }[]; onChange?: (value: string) => void; onChangeBool?: (value: boolean) => void; onChangeNumber?: (value: number) => void; @@ -26,6 +27,7 @@ export const EditingValue: React.FC<{ name, currentValue, label, + options, type, includRevert, className, @@ -38,7 +40,9 @@ export const EditingValue: React.FC<{ onChangeNumber, onChangeDate, }) => { - const [value, setValue] = useState(); + const [value, setValue] = useState( + currentValue + ); const updateValue = (newValue: string | boolean | number | Date) => { setValue(newValue); @@ -148,6 +152,36 @@ export const EditingValue: React.FC<{ focus:invalid:border-pink-500 focus:invalid:ring-pink-500`} /> + ) : type === "select" ? ( +
    + + {description && {description}} + + +
    ) : ( // Default +
    +
    +
    +
    + +
    + +
    + {enterpriseSettings && enterpriseSettings.application_name ? ( + {enterpriseSettings.application_name} + ) : ( + Danswer + )} +
    +
    + +
    + + +

    + Search Settings +

    + +
    + +
    +
    +
    +
    + {settingSteps.map((step, index) => { + return ( +
    { + setFormStep(index); + }} + > +
    +
    + {formStep === index && ( +
    + )} +
    +
    +
    + {step} +
    +
    + ); + })} +
    +
    +
    +
    +
    +
    + ); +} diff --git a/web/src/components/embedding/ModelSelector.tsx b/web/src/components/embedding/ModelSelector.tsx new file mode 100644 index 000000000..93be374d1 --- /dev/null +++ b/web/src/components/embedding/ModelSelector.tsx @@ -0,0 +1,142 @@ +import { + MicrosoftIcon, + NomicIcon, + OpenSourceIcon, +} from "@/components/icons/icons"; +import { + EmbeddingModelDescriptor, + getIconForRerankType, + getTitleForRerankType, + HostedEmbeddingModel, +} from "./interfaces"; +import { FiExternalLink, FiStar } from "react-icons/fi"; + +export function ModelPreview({ + model, + display, +}: { + model: EmbeddingModelDescriptor; + display?: boolean; +}) { + return ( +
    +
    {model.model_name}
    +
    + {model.description + ? model.description + : "Custom model—no description is available."} +
    +
    + ); +} + +export function ModelOption({ + model, + onSelect, + selected, +}: { + model: HostedEmbeddingModel; + onSelect?: (model: HostedEmbeddingModel) => void; + selected: boolean; +}) { + return ( +
    + +

    + {model.description || "Custom model—no description is available."} +

    +
    + {model.isDefault ? "Default" : "Self-hosted"} +
    + {onSelect && ( +
    + +
    + )} +
    + ); +} +export function ModelSelector({ + modelOptions, + setSelectedModel, + currentEmbeddingModel, +}: { + currentEmbeddingModel: HostedEmbeddingModel; + modelOptions: HostedEmbeddingModel[]; + setSelectedModel: (model: HostedEmbeddingModel) => void; +}) { + const groupedModelOptions = modelOptions.reduce( + (acc, model) => { + const [type] = model.model_name.split("/"); + if (!acc[type]) { + acc[type] = []; + } + acc[type].push(model); + return acc; + }, + {} as Record + ); + + return ( +
    +
    + {Object.entries(groupedModelOptions).map(([type, models]) => ( +
    +
    + {getIconForRerankType(type)} +

    + {getTitleForRerankType(type)} +

    +
    + +
    + {models.map((modelOption) => ( + + ))} +
    +
    + ))} +
    +
    + ); +} diff --git a/web/src/app/admin/models/embedding/components/ReindexingProgressTable.tsx b/web/src/components/embedding/ReindexingProgressTable.tsx similarity index 100% rename from web/src/app/admin/models/embedding/components/ReindexingProgressTable.tsx rename to web/src/components/embedding/ReindexingProgressTable.tsx diff --git a/web/src/app/admin/models/embedding/components/types.ts b/web/src/components/embedding/interfaces.tsx similarity index 94% rename from web/src/app/admin/models/embedding/components/types.ts rename to web/src/components/embedding/interfaces.tsx index 3a97ee400..c77e9951e 100644 --- a/web/src/app/admin/models/embedding/components/types.ts +++ b/web/src/components/embedding/interfaces.tsx @@ -2,7 +2,10 @@ import { CohereIcon, GoogleIcon, IconProps, + MicrosoftIcon, + NomicIcon, OpenAIIcon, + OpenSourceIcon, VoyageIcon, } from "@/components/icons/icons"; @@ -120,47 +123,6 @@ export const AVAILABLE_MODELS: HostedEmbeddingModel[] = [ ]; export const AVAILABLE_CLOUD_PROVIDERS: CloudEmbeddingProvider[] = [ - { - id: 0, - name: "OpenAI", - website: "https://openai.com", - icon: OpenAIIcon, - description: "AI industry leader known for ChatGPT and DALL-E", - apiLink: "https://platform.openai.com/api-keys", - docsLink: - "https://docs.danswer.dev/guides/embedding_providers#openai-models", - costslink: "https://openai.com/pricing", - embedding_models: [ - { - model_name: "text-embedding-3-large", - cloud_provider_name: "OpenAI", - description: - "OpenAI's large embedding model. Best performance, but more expensive.", - pricePerMillion: 0.13, - model_dim: 3072, - normalize: false, - query_prefix: "", - passage_prefix: "", - mtebScore: 64.6, - maxContext: 8191, - enabled: false, - }, - { - model_name: "text-embedding-3-small", - cloud_provider_name: "OpenAI", - model_dim: 1536, - normalize: false, - query_prefix: "", - passage_prefix: "", - description: - "OpenAI's newer, more efficient embedding model. Good balance of performance and cost.", - pricePerMillion: 0.02, - enabled: false, - mtebScore: 62.3, - maxContext: 8191, - }, - ], - }, { id: 1, name: "Cohere", @@ -203,6 +165,47 @@ export const AVAILABLE_CLOUD_PROVIDERS: CloudEmbeddingProvider[] = [ }, ], }, + { + id: 0, + name: "OpenAI", + website: "https://openai.com", + icon: OpenAIIcon, + description: "AI industry leader known for ChatGPT and DALL-E", + apiLink: "https://platform.openai.com/api-keys", + docsLink: + "https://docs.danswer.dev/guides/embedding_providers#openai-models", + costslink: "https://openai.com/pricing", + embedding_models: [ + { + model_name: "text-embedding-3-large", + cloud_provider_name: "OpenAI", + description: + "OpenAI's large embedding model. Best performance, but more expensive.", + pricePerMillion: 0.13, + model_dim: 3072, + normalize: false, + query_prefix: "", + passage_prefix: "", + mtebScore: 64.6, + maxContext: 8191, + enabled: false, + }, + { + model_name: "text-embedding-3-small", + cloud_provider_name: "OpenAI", + model_dim: 1536, + normalize: false, + query_prefix: "", + passage_prefix: "", + description: + "OpenAI's newer, more efficient embedding model. Good balance of performance and cost.", + pricePerMillion: 0.02, + enabled: false, + mtebScore: 62.3, + maxContext: 8191, + }, + ], + }, { id: 2, @@ -287,6 +290,28 @@ export const AVAILABLE_CLOUD_PROVIDERS: CloudEmbeddingProvider[] = [ }, ]; +export const getTitleForRerankType = (type: string) => { + switch (type) { + case "nomic-ai": + return "Nomic (recommended)"; + case "intfloat": + return "Microsoft"; + default: + return "Open Source"; + } +}; + +export const getIconForRerankType = (type: string) => { + switch (type) { + case "nomic-ai": + return ; + case "intfloat": + return ; + default: + return ; + } +}; + export const INVALID_OLD_MODEL = "thenlper/gte-small"; export function checkModelNameIsValid( diff --git a/web/src/components/header/AnnouncementBanner.tsx b/web/src/components/header/AnnouncementBanner.tsx new file mode 100644 index 000000000..19b461a1e --- /dev/null +++ b/web/src/components/header/AnnouncementBanner.tsx @@ -0,0 +1,101 @@ +"use client"; +import { useState, useEffect, useContext } from "react"; +import { XIcon } from "../icons/icons"; +import { CustomTooltip } from "../tooltip/CustomTooltip"; +import { SettingsContext } from "../settings/SettingsProvider"; +import Link from "next/link"; +import Cookies from "js-cookie"; + +const DISMISSED_NOTIFICATION_COOKIE_PREFIX = "dismissed_notification_"; +const COOKIE_EXPIRY_DAYS = 1; + +export function AnnouncementBanner() { + const settings = useContext(SettingsContext); + const [localNotifications, setLocalNotifications] = useState( + settings?.settings.notifications || [] + ); + + useEffect(() => { + const filteredNotifications = ( + settings?.settings.notifications || [] + ).filter( + (notification) => + !Cookies.get( + `${DISMISSED_NOTIFICATION_COOKIE_PREFIX}${notification.id}` + ) + ); + setLocalNotifications(filteredNotifications); + }, [settings?.settings.notifications]); + + if (!localNotifications || localNotifications.length === 0) return null; + + const handleDismiss = async (notificationId: number) => { + try { + const response = await fetch( + `/api/settings/notifications/${notificationId}/dismiss`, + { + method: "POST", + } + ); + if (response.ok) { + Cookies.set( + `${DISMISSED_NOTIFICATION_COOKIE_PREFIX}${notificationId}`, + "true", + { expires: COOKIE_EXPIRY_DAYS } + ); + setLocalNotifications((prevNotifications) => + prevNotifications.filter( + (notification) => notification.id !== notificationId + ) + ); + } else { + console.error("Failed to dismiss notification"); + } + } catch (error) { + console.error("Error dismissing notification:", error); + } + }; + + return ( + <> + {localNotifications + .filter((notification) => !notification.dismissed) + .map((notification) => { + if (notification.notif_type == "reindex") { + return ( +
    +

    + Your index is out of date - we strongly recommend updating + your search settings.{" "} + + Update here + +

    + +
    + ); + } + return null; + })} + + ); +} diff --git a/web/src/components/icons/icons.tsx b/web/src/components/icons/icons.tsx index 70fa5bb6a..4a7e30ee4 100644 --- a/web/src/components/icons/icons.tsx +++ b/web/src/components/icons/icons.tsx @@ -53,6 +53,9 @@ import awsWEBP from "../../../public/Amazon.webp"; import azureIcon from "../../../public/Azure.png"; import anthropicSVG from "../../../public/Anthropic.svg"; +import nomicSVG from "../../../public/nomic.svg"; +import microsoftIcon from "../../../public/microsoft.png"; +import mixedBreadSVG from "../../../public/Mixedbread.png"; import OCIStorageSVG from "../../../public/OCI.svg"; import googleCloudStorageIcon from "../../../public/GoogleCloudStorage.png"; @@ -277,6 +280,48 @@ export const OpenSourceIcon = ({
    ); }; +export const MixedBreadIcon = ({ + size = 16, + className = defaultTailwindCSS, +}: IconProps) => { + return ( +
    + Logo +
    + ); +}; + +export const NomicIcon = ({ + size = 16, + className = defaultTailwindCSS, +}: IconProps) => { + return ( +
    + Logo +
    + ); +}; + +export const MicrosoftIcon = ({ + size = 16, + className = defaultTailwindCSS, +}: IconProps) => { + return ( +
    + Logo +
    + ); +}; + export const AnthropicIcon = ({ size = 16, className = defaultTailwindCSS, diff --git a/web/src/components/icons/mixedbread.svg b/web/src/components/icons/mixedbread.svg new file mode 100644 index 000000000..ae32e53a1 --- /dev/null +++ b/web/src/components/icons/mixedbread.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/components/initialSetup/welcome/WelcomeModal.tsx b/web/src/components/initialSetup/welcome/WelcomeModal.tsx index adba63ada..c9472992f 100644 --- a/web/src/components/initialSetup/welcome/WelcomeModal.tsx +++ b/web/src/components/initialSetup/welcome/WelcomeModal.tsx @@ -10,7 +10,7 @@ import { FiCheckCircle, FiMessageSquare, FiShare2 } from "react-icons/fi"; import { useEffect, useState } from "react"; import { BackButton } from "@/components/BackButton"; import { ApiKeyForm } from "@/components/llm/ApiKeyForm"; -import { WellKnownLLMProviderDescriptor } from "@/app/admin/models/llm/interfaces"; +import { WellKnownLLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces"; import { checkLlmProvider } from "./lib"; import { User } from "@/lib/types"; diff --git a/web/src/components/initialSetup/welcome/lib.ts b/web/src/components/initialSetup/welcome/lib.ts index 65e7ace95..5cbe54cc3 100644 --- a/web/src/components/initialSetup/welcome/lib.ts +++ b/web/src/components/initialSetup/welcome/lib.ts @@ -1,7 +1,7 @@ import { FullLLMProvider, WellKnownLLMProviderDescriptor, -} from "@/app/admin/models/llm/interfaces"; +} from "@/app/admin/configuration/llm/interfaces"; import { User } from "@/lib/types"; const DEFAULT_LLM_PROVIDER_TEST_COMPLETE_KEY = "defaultLlmProviderTestComplete"; diff --git a/web/src/components/llm/ApiKeyForm.tsx b/web/src/components/llm/ApiKeyForm.tsx index c46de56a4..0ebe38dc3 100644 --- a/web/src/components/llm/ApiKeyForm.tsx +++ b/web/src/components/llm/ApiKeyForm.tsx @@ -1,9 +1,9 @@ import { Popup } from "../admin/connectors/Popup"; import { useState } from "react"; import { TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react"; -import { WellKnownLLMProviderDescriptor } from "@/app/admin/models/llm/interfaces"; -import { LLMProviderUpdateForm } from "@/app/admin/models/llm/LLMProviderUpdateForm"; -import { CustomLLMProviderUpdateForm } from "@/app/admin/models/llm/CustomLLMProviderUpdateForm"; +import { WellKnownLLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces"; +import { LLMProviderUpdateForm } from "@/app/admin/configuration/llm/LLMProviderUpdateForm"; +import { CustomLLMProviderUpdateForm } from "@/app/admin/configuration/llm/CustomLLMProviderUpdateForm"; export const ApiKeyForm = ({ onSuccess, diff --git a/web/src/components/llm/ApiKeyModal.tsx b/web/src/components/llm/ApiKeyModal.tsx index ec34d29fc..8c522fdc3 100644 --- a/web/src/components/llm/ApiKeyModal.tsx +++ b/web/src/components/llm/ApiKeyModal.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from "react"; import { ApiKeyForm } from "./ApiKeyForm"; import { Modal } from "../Modal"; -import { WellKnownLLMProviderDescriptor } from "@/app/admin/models/llm/interfaces"; +import { WellKnownLLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces"; import { checkLlmProvider } from "../initialSetup/welcome/lib"; import { User } from "@/lib/types"; import { useRouter } from "next/navigation"; diff --git a/web/src/components/tooltip/CustomTooltip.tsx b/web/src/components/tooltip/CustomTooltip.tsx index c8a18795b..6695b99eb 100644 --- a/web/src/components/tooltip/CustomTooltip.tsx +++ b/web/src/components/tooltip/CustomTooltip.tsx @@ -41,11 +41,13 @@ export const CustomTooltip = ({ light, citation, line, + medium, wrap, showTick = false, delay = 500, position = "bottom", }: { + medium?: boolean; content: string | ReactNode; children: JSX.Element; large?: boolean; @@ -120,7 +122,7 @@ export const CustomTooltip = ({ createPortal(