mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-04-09 20:39:29 +02:00
Improve Search (#2105)
This commit is contained in:
parent
efae24acd0
commit
22573aba2a
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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<float>(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)
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 []
|
||||
|
@ -61,3 +61,4 @@ class Settings(BaseModel):
|
||||
|
||||
class UserSettings(Settings):
|
||||
notifications: list[Notification]
|
||||
needs_reindexing: bool
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
BIN
web/public/Mixedbread.png
Normal file
BIN
web/public/Mixedbread.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 134 KiB |
BIN
web/public/microsoft.png
Normal file
BIN
web/public/microsoft.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
8
web/public/nomic.svg
Normal file
8
web/public/nomic.svg
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
<svg width="207" height="144" viewBox="0 0 207 144" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M25.9375 107.812V0H32.1875V143.75H25.9375L7.1875 35.9375V143.75H0.9375V0H7.1875L25.9375 107.812Z" fill="#3C593D"/>
|
||||
<path d="M64.0234 137.5C65.7161 137.5 67.1484 136.914 68.3203 135.742C69.6224 134.57 70.2734 133.073 70.2734 131.25V12.5C70.2734 10.8073 69.6224 9.375 68.3203 8.20312C67.1484 6.90104 65.7161 6.25 64.0234 6.25C62.2005 6.25 60.7031 6.90104 59.5312 8.20312C58.3594 9.375 57.7734 10.8073 57.7734 12.5V131.25C57.7734 133.073 58.3594 134.57 59.5312 135.742C60.7031 136.914 62.2005 137.5 64.0234 137.5ZM64.0234 143.75C60.5078 143.75 57.513 142.578 55.0391 140.234C52.6953 137.76 51.5234 134.766 51.5234 131.25V12.5C51.5234 9.11458 52.6953 6.1849 55.0391 3.71094C57.513 1.23698 60.5078 0 64.0234 0C67.4089 0 70.3385 1.23698 72.8125 3.71094C75.2865 6.1849 76.5234 9.11458 76.5234 12.5V131.25C76.5234 134.766 75.2865 137.76 72.8125 140.234C70.3385 142.578 67.4089 143.75 64.0234 143.75Z" fill="#3C593D"/>
|
||||
<path d="M131.797 0H138.047V143.75H131.797V25L116.172 87.5L100.547 25V143.75H94.2969V0H100.547L116.172 62.5L131.797 0Z" fill="#3C593D"/>
|
||||
<path d="M156.602 143.75V0H162.852V143.75H156.602Z" fill="#3C593D"/>
|
||||
<path d="M193.906 143.75C190.391 143.75 187.396 142.578 184.922 140.234C182.578 137.76 181.406 134.766 181.406 131.25V12.5C181.406 9.11458 182.578 6.1849 184.922 3.71094C187.396 1.23698 190.391 0 193.906 0C197.292 0 200.221 1.23698 202.695 3.71094C205.169 6.1849 206.406 9.11458 206.406 12.5V25H200.156V12.5C200.156 10.8073 199.505 9.375 198.203 8.20312C197.031 6.90104 195.599 6.25 193.906 6.25C192.083 6.25 190.586 6.90104 189.414 8.20312C188.242 9.375 187.656 10.8073 187.656 12.5V131.25C187.656 133.073 188.242 134.57 189.414 135.742C190.586 136.914 192.083 137.5 193.906 137.5C195.599 137.5 197.031 136.914 198.203 135.742C199.505 134.57 200.156 133.073 200.156 131.25V118.75H206.406V131.25C206.406 134.766 205.169 137.76 202.695 140.234C200.221 142.578 197.292 143.75 193.906 143.75Z" fill="#3C593D"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
@ -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";
|
||||
|
117
web/src/app/admin/configuration/search/UpgradingPage.tsx
Normal file
117
web/src/app/admin/configuration/search/UpgradingPage.tsx
Normal file
@ -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<boolean>(false);
|
||||
|
||||
const { data: connectors } = useSWR<Connector<any>[]>(
|
||||
"/api/manage/connector",
|
||||
errorHandlingFetcher,
|
||||
{ refreshInterval: 5000 } // 5 seconds
|
||||
);
|
||||
|
||||
const {
|
||||
data: ongoingReIndexingStatus,
|
||||
isLoading: isLoadingOngoingReIndexingStatus,
|
||||
} = useSWR<ConnectorIndexingStatus<any, any>[]>(
|
||||
"/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 && (
|
||||
<Modal
|
||||
onOutsideClick={() => setIsCancelling(false)}
|
||||
title="Cancel Embedding Model Switch"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
Are you sure you want to cancel?
|
||||
<br />
|
||||
<br />
|
||||
Cancelling will revert to the previous model and all progress will
|
||||
be lost.
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Button onClick={onCancel} className="mt-3 mx-auto" color="green">
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{futureEmbeddingModel && connectors && connectors.length > 0 && (
|
||||
<div>
|
||||
<Title className="mt-8">Current Upgrade Status</Title>
|
||||
<div className="mt-4">
|
||||
<div className="italic text-lg mb-2">
|
||||
Currently in the process of switching to:{" "}
|
||||
{futureEmbeddingModel.model_name}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
color="red"
|
||||
size="xs"
|
||||
className="mt-4"
|
||||
onClick={() => setIsCancelling(true)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Text className="my-4">
|
||||
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.
|
||||
</Text>
|
||||
|
||||
{isLoadingOngoingReIndexingStatus ? (
|
||||
<ThreeDotsLoader />
|
||||
) : ongoingReIndexingStatus ? (
|
||||
<ReindexingProgressTable
|
||||
reindexingProgress={ongoingReIndexingStatus}
|
||||
/>
|
||||
) : (
|
||||
<ErrorCallout errorTitle="Failed to fetch re-indexing progress" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
191
web/src/app/admin/configuration/search/page.tsx
Normal file
191
web/src/app/admin/configuration/search/page.tsx
Normal file
@ -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<CloudEmbeddingModel | HostedEmbeddingModel | null>(
|
||||
"/api/search-settings/get-current-embedding-model",
|
||||
errorHandlingFetcher,
|
||||
{ refreshInterval: 5000 } // 5 seconds
|
||||
);
|
||||
|
||||
const { data: searchSettings, isLoading: isLoadingSearchSettings } =
|
||||
useSWR<SavedSearchSettings | null>(
|
||||
"/api/search-settings/get-search-settings",
|
||||
errorHandlingFetcher,
|
||||
{ refreshInterval: 5000 } // 5 seconds
|
||||
);
|
||||
|
||||
const {
|
||||
data: futureEmbeddingModel,
|
||||
isLoading: isLoadingFutureModel,
|
||||
error: futureEmeddingModelError,
|
||||
} = useSWR<CloudEmbeddingModel | HostedEmbeddingModel | null>(
|
||||
"/api/search-settings/get-secondary-embedding-model",
|
||||
errorHandlingFetcher,
|
||||
{ refreshInterval: 5000 } // 5 seconds
|
||||
);
|
||||
|
||||
if (
|
||||
isLoadingCurrentModel ||
|
||||
isLoadingFutureModel ||
|
||||
isLoadingSearchSettings
|
||||
) {
|
||||
return <ThreeDotsLoader />;
|
||||
}
|
||||
|
||||
if (
|
||||
currentEmeddingModelError ||
|
||||
!currentEmeddingModel ||
|
||||
futureEmeddingModelError
|
||||
) {
|
||||
return <ErrorCallout errorTitle="Failed to fetch embedding model status" />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="h-screen">
|
||||
{!futureEmbeddingModel ? (
|
||||
<>
|
||||
{settings?.settings.needs_reindexing && (
|
||||
<p className="max-w-3xl">
|
||||
Your search settings are currently out of date! We recommend
|
||||
updating your search settings and re-indexing.
|
||||
</p>
|
||||
)}
|
||||
<Title className="mb-6 mt-8 !text-2xl">Embedding Model</Title>
|
||||
|
||||
{currentModel ? (
|
||||
<ModelPreview model={currentModel} display />
|
||||
) : (
|
||||
<Title className="mt-8 mb-4">Choose your Embedding Model</Title>
|
||||
)}
|
||||
|
||||
<Title className="mb-2 mt-8 !text-2xl">Post-processing</Title>
|
||||
|
||||
<Card className="!mr-auto mt-8 !w-96">
|
||||
{searchSettings && (
|
||||
<>
|
||||
<div className="px-1 w-full rounded-lg">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Text className="font-semibold">Reranking Model</Text>
|
||||
<Text className="text-gray-700">
|
||||
{searchSettings.rerank_model_name || "Not set"}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-semibold">Results to Rerank</Text>
|
||||
<Text className="text-gray-700">
|
||||
{searchSettings.num_rerank}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-semibold">
|
||||
Multilingual Expansion
|
||||
</Text>
|
||||
<Text className="text-gray-700">
|
||||
{searchSettings.multilingual_expansion.length > 0
|
||||
? searchSettings.multilingual_expansion.join(", ")
|
||||
: "None"}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-semibold">Multipass Indexing</Text>
|
||||
<Text className="text-gray-700">
|
||||
{searchSettings.multipass_indexing
|
||||
? "Enabled"
|
||||
: "Disabled"}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-semibold">
|
||||
Disable Reranking for Streaming
|
||||
</Text>
|
||||
<Text className="text-gray-700">
|
||||
{searchSettings.disable_rerank_for_streaming
|
||||
? "Yes"
|
||||
: "No"}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Link href="/admin/embeddings">
|
||||
<Button className="mt-8">Update Search Settings</Button>
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<UpgradingPage futureEmbeddingModel={futureEmbeddingModel} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Page() {
|
||||
return (
|
||||
<div className="mx-auto container">
|
||||
<AdminPageTitle
|
||||
title="Search Settings"
|
||||
icon={<EmbeddingIcon size={32} className="my-auto" />}
|
||||
/>
|
||||
<Main />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
@ -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";
|
||||
|
306
web/src/app/admin/embeddings/EmbeddingModelSelectionForm.tsx
Normal file
306
web/src/app/admin/embeddings/EmbeddingModelSelectionForm.tsx
Normal file
@ -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<SetStateAction<"open" | "cloud" | null>>;
|
||||
currentEmbeddingModel: CloudEmbeddingModel | HostedEmbeddingModel;
|
||||
selectedProvider: CloudEmbeddingModel | HostedEmbeddingModel;
|
||||
updateSelectedProvider: (
|
||||
model: CloudEmbeddingModel | HostedEmbeddingModel
|
||||
) => void;
|
||||
}) {
|
||||
// Cloud Provider based modals
|
||||
const [showTentativeProvider, setShowTentativeProvider] =
|
||||
useState<CloudEmbeddingProvider | null>(null);
|
||||
|
||||
const [showUnconfiguredProvider, setShowUnconfiguredProvider] =
|
||||
useState<CloudEmbeddingProvider | null>(null);
|
||||
const [changeCredentialsProvider, setChangeCredentialsProvider] =
|
||||
useState<CloudEmbeddingProvider | null>(null);
|
||||
|
||||
// Cloud Model based modals
|
||||
const [alreadySelectedModel, setAlreadySelectedModel] =
|
||||
useState<CloudEmbeddingModel | null>(null);
|
||||
const [showTentativeModel, setShowTentativeModel] =
|
||||
useState<CloudEmbeddingModel | null>(null);
|
||||
|
||||
const [showModelInQueue, setShowModelInQueue] =
|
||||
useState<CloudEmbeddingModel | null>(null);
|
||||
|
||||
// Open Model based modals
|
||||
const [showTentativeOpenProvider, setShowTentativeOpenProvider] =
|
||||
useState<HostedEmbeddingModel | null>(null);
|
||||
|
||||
// Enabled / unenabled providers
|
||||
const [newEnabledProviders, setNewEnabledProviders] = useState<string[]>([]);
|
||||
const [newUnenabledProviders, setNewUnenabledProviders] = useState<string[]>(
|
||||
[]
|
||||
);
|
||||
|
||||
const [showDeleteCredentialsModal, setShowDeleteCredentialsModal] =
|
||||
useState<boolean>(false);
|
||||
const [showAddConnectorPopup, setShowAddConnectorPopup] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const { data: embeddingProviderDetails } = useSWR<EmbeddingDetails[]>(
|
||||
EMBEDDING_PROVIDERS_ADMIN_URL,
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
const { data: connectors } = useSWR<Connector<any>[]>(
|
||||
"/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 (
|
||||
<div className="p-2">
|
||||
{alreadySelectedModel && (
|
||||
<AlreadyPickedModal
|
||||
model={alreadySelectedModel}
|
||||
onClose={() => setAlreadySelectedModel(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showTentativeOpenProvider && (
|
||||
<ModelSelectionConfirmationModal
|
||||
selectedModel={showTentativeOpenProvider}
|
||||
isCustom={
|
||||
AVAILABLE_MODELS.find(
|
||||
(model) =>
|
||||
model.model_name === showTentativeOpenProvider.model_name
|
||||
) === undefined
|
||||
}
|
||||
onConfirm={() => {
|
||||
updateSelectedProvider(showTentativeOpenProvider);
|
||||
setShowTentativeOpenProvider(null);
|
||||
}}
|
||||
onCancel={() => setShowTentativeOpenProvider(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showTentativeProvider && (
|
||||
<ProviderCreationModal
|
||||
selectedProvider={showTentativeProvider}
|
||||
onConfirm={() => {
|
||||
setShowTentativeProvider(showUnconfiguredProvider);
|
||||
clientsideAddProvider(showTentativeProvider);
|
||||
if (showModelInQueue) {
|
||||
setShowTentativeModel(showModelInQueue);
|
||||
}
|
||||
}}
|
||||
onCancel={() => {
|
||||
setShowModelInQueue(null);
|
||||
setShowTentativeProvider(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{changeCredentialsProvider && (
|
||||
<ChangeCredentialsModal
|
||||
useFileUpload={changeCredentialsProvider.name == "Google"}
|
||||
onDeleted={() => {
|
||||
clientsideRemoveProvider(changeCredentialsProvider);
|
||||
setChangeCredentialsProvider(null);
|
||||
}}
|
||||
provider={changeCredentialsProvider}
|
||||
onConfirm={() => setChangeCredentialsProvider(null)}
|
||||
onCancel={() => setChangeCredentialsProvider(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showTentativeModel && (
|
||||
<SelectModelModal
|
||||
model={showTentativeModel}
|
||||
onConfirm={() => {
|
||||
setShowModelInQueue(null);
|
||||
updateSelectedProvider(showTentativeModel);
|
||||
setShowTentativeModel(null);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setShowModelInQueue(null);
|
||||
setShowTentativeModel(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showDeleteCredentialsModal && (
|
||||
<DeleteCredentialsModal
|
||||
modelProvider={showTentativeProvider!}
|
||||
onConfirm={() => {
|
||||
setShowDeleteCredentialsModal(false);
|
||||
}}
|
||||
onCancel={() => setShowDeleteCredentialsModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<p className=" t mb-4">
|
||||
Select from cloud, self-hosted models, or continue with your current
|
||||
embedding model.
|
||||
</p>
|
||||
<div className="text-sm mr-auto mb-6 divide-x-2 flex">
|
||||
<button
|
||||
onClick={() => setModelTab(null)}
|
||||
className={`mr-4 p-2 font-bold ${
|
||||
!modelTab
|
||||
? "rounded bg-background-900 text-text-100 underline"
|
||||
: " hover:underline bg-background-100"
|
||||
}`}
|
||||
>
|
||||
Current
|
||||
</button>
|
||||
<div className="px-2 ">
|
||||
<button
|
||||
onClick={() => setModelTab("cloud")}
|
||||
className={`mx-2 p-2 font-bold ${
|
||||
modelTab == "cloud"
|
||||
? "rounded bg-background-900 text-text-100 underline"
|
||||
: " hover:underline bg-background-100"
|
||||
}`}
|
||||
>
|
||||
Cloud-based
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-2 ">
|
||||
<button
|
||||
onClick={() => setModelTab("open")}
|
||||
className={` mx-2 p-2 font-bold ${
|
||||
modelTab == "open"
|
||||
? "rounded bg-background-900 text-text-100 underline"
|
||||
: "hover:underline bg-background-100"
|
||||
}`}
|
||||
>
|
||||
Self-hosted
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{modelTab == "open" && (
|
||||
<OpenEmbeddingPage
|
||||
selectedProvider={selectedProvider}
|
||||
onSelectOpenSource={onSelectOpenSource}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modelTab == "cloud" && (
|
||||
<CloudEmbeddingPage
|
||||
setShowModelInQueue={setShowModelInQueue}
|
||||
setShowTentativeModel={setShowTentativeModel}
|
||||
currentModel={selectedProvider}
|
||||
setAlreadySelectedModel={setAlreadySelectedModel}
|
||||
embeddingProviderDetails={embeddingProviderDetails}
|
||||
newEnabledProviders={newEnabledProviders}
|
||||
newUnenabledProviders={newUnenabledProviders}
|
||||
setShowTentativeProvider={setShowTentativeProvider}
|
||||
setChangeCredentialsProvider={setChangeCredentialsProvider}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!modelTab && (
|
||||
<>
|
||||
<button onClick={() => updateSelectedProvider(currentEmbeddingModel)}>
|
||||
<ModelOption
|
||||
model={currentEmbeddingModel}
|
||||
selected={
|
||||
selectedProvider.model_name == currentEmbeddingModel.model_name
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
243
web/src/app/admin/embeddings/RerankingFormPage.tsx
Normal file
243
web/src/app/admin/embeddings/RerankingFormPage.tsx
Normal file
@ -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<SetStateAction<RerankingDetails>>;
|
||||
currentRerankingDetails: RerankingDetails;
|
||||
originalRerankingDetails: RerankingDetails;
|
||||
modelTab: "open" | "cloud" | null;
|
||||
setModelTab: Dispatch<SetStateAction<"open" | "cloud" | null>>;
|
||||
}
|
||||
|
||||
const RerankingDetailsForm = forwardRef<
|
||||
FormikProps<RerankingDetails>,
|
||||
RerankingDetailsFormProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
setRerankingDetails,
|
||||
originalRerankingDetails,
|
||||
currentRerankingDetails,
|
||||
modelTab,
|
||||
setModelTab,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="p-2 rounded-lg max-w-4xl mx-auto">
|
||||
<h2 className="text-2xl font-bold mb-4 text-text-800">
|
||||
Post-processing
|
||||
</h2>
|
||||
<div className="text-sm mr-auto mb-6 divide-x-2 flex">
|
||||
{originalRerankingDetails.rerank_model_name && (
|
||||
<button
|
||||
onClick={() => setModelTab(null)}
|
||||
className={`mx-2 p-2 font-bold ${
|
||||
!modelTab
|
||||
? "rounded bg-background-900 text-text-100 underline"
|
||||
: " hover:underline bg-background-100"
|
||||
}`}
|
||||
>
|
||||
Current
|
||||
</button>
|
||||
)}
|
||||
<div
|
||||
className={`${originalRerankingDetails.rerank_model_name && "px-2 ml-2"}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => setModelTab("cloud")}
|
||||
className={`mr-2 p-2 font-bold ${
|
||||
modelTab == "cloud"
|
||||
? "rounded bg-background-900 text-text-100 underline"
|
||||
: " hover:underline bg-background-100"
|
||||
}`}
|
||||
>
|
||||
Cloud-based
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="px-2 ">
|
||||
<button
|
||||
onClick={() => setModelTab("open")}
|
||||
className={` mx-2 p-2 font-bold ${
|
||||
modelTab == "open"
|
||||
? "rounded bg-background-900 text-text-100 underline"
|
||||
: "hover:underline bg-background-100"
|
||||
}`}
|
||||
>
|
||||
Self-hosted
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Formik
|
||||
innerRef={ref}
|
||||
initialValues={currentRerankingDetails}
|
||||
validationSchema={Yup.object().shape({
|
||||
rerank_model_name: Yup.string().nullable(),
|
||||
provider_type: Yup.mixed<RerankerProvider>()
|
||||
.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 }) => (
|
||||
<Form className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{(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 (
|
||||
<div
|
||||
key={`${card.provider}-${card.modelName}`}
|
||||
className={`p-4 border rounded-lg cursor-pointer transition-all duration-200 ${
|
||||
isSelected
|
||||
? "border-blue-500 bg-blue-50 shadow-md"
|
||||
: "border-gray-200 hover:border-blue-300 hover:shadow-sm"
|
||||
}`}
|
||||
onClick={() => {
|
||||
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);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center">
|
||||
{card.provider === RerankerProvider.COHERE ? (
|
||||
<CohereIcon size={24} className="mr-2" />
|
||||
) : (
|
||||
<MixedBreadIcon size={24} className="mr-2" />
|
||||
)}
|
||||
<h3 className="font-bold text-lg">
|
||||
{card.displayName}
|
||||
</h3>
|
||||
</div>
|
||||
{card.link && (
|
||||
<a
|
||||
href={card.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="text-blue-500 hover:text-blue-700 transition-colors duration-200"
|
||||
>
|
||||
<FiExternalLink size={18} />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mb-2">
|
||||
{card.description}
|
||||
</p>
|
||||
<div className="text-xs text-gray-500">
|
||||
{card.cloud ? "Cloud-based" : "Self-hosted"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{isApiKeyModalOpen && (
|
||||
<Modal
|
||||
onOutsideClick={() => {
|
||||
Object.keys(originalRerankingDetails).forEach((key) => {
|
||||
setFieldValue(
|
||||
key,
|
||||
originalRerankingDetails[key as keyof RerankingDetails]
|
||||
);
|
||||
});
|
||||
|
||||
setIsApiKeyModalOpen(false);
|
||||
}}
|
||||
width="w-[800px]"
|
||||
title="API Key Configuration"
|
||||
>
|
||||
<div className="w-full px-4">
|
||||
<EditingValue
|
||||
optional={false}
|
||||
currentValue={values.api_key}
|
||||
onChange={(value: string | null) => {
|
||||
setRerankingDetails({ ...values, api_key: value });
|
||||
setFieldValue("api_key", value);
|
||||
}}
|
||||
setFieldValue={setFieldValue}
|
||||
type="password"
|
||||
label="Cohere API Key"
|
||||
name="api_key"
|
||||
/>
|
||||
<div className="mt-4 flex justify-between">
|
||||
<Button
|
||||
onClick={() => {
|
||||
Object.keys(originalRerankingDetails).forEach(
|
||||
(key) => {
|
||||
setFieldValue(
|
||||
key,
|
||||
originalRerankingDetails[
|
||||
key as keyof RerankingDetails
|
||||
]
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
setIsApiKeyModalOpen(false);
|
||||
}}
|
||||
color="red"
|
||||
size="xs"
|
||||
>
|
||||
Abandon
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setIsApiKeyModalOpen(false)}
|
||||
color="blue"
|
||||
size="xs"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
RerankingDetailsForm.displayName = "RerankingDetailsForm";
|
||||
export default RerankingDetailsForm;
|
70
web/src/app/admin/embeddings/interfaces.ts
Normal file
70
web/src/app/admin/embeddings/interfaces.ts
Normal file
@ -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",
|
||||
},
|
||||
];
|
@ -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 (
|
||||
<Modal
|
||||
width="max-w-3xl"
|
||||
title={`${model.model_name} already chosen`}
|
||||
onOutsideClick={onClose}
|
||||
>
|
@ -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 (
|
||||
<Modal
|
||||
width="max-w-3xl"
|
||||
icon={provider.icon}
|
||||
title={`Modify your ${provider.name} key`}
|
||||
onOutsideClick={onCancel}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<Subtitle className="mt-4 font-bold text-lg mb-2">
|
||||
<Subtitle className="font-bold text-lg">
|
||||
Want to swap out your key?
|
||||
</Subtitle>
|
||||
<a
|
||||
href={provider.apiLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline cursor-pointer mt-2 mb-4"
|
||||
>
|
||||
Visit API
|
||||
</a>
|
||||
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<div className="flex flex-col mt-4 gap-y-2">
|
||||
{useFileUpload ? (
|
||||
<>
|
||||
<Label>Upload JSON File</Label>
|
||||
@ -175,9 +182,6 @@ export function ChangeCredentialsModal({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex gap-x-2 items-center">
|
||||
<Label>New API Key</Label>
|
||||
</div>
|
||||
<input
|
||||
className={`
|
||||
border
|
||||
@ -186,7 +190,6 @@ export function ChangeCredentialsModal({
|
||||
w-full
|
||||
py-2
|
||||
px-3
|
||||
mt-1
|
||||
bg-background-emphasis
|
||||
`}
|
||||
value={apiKey}
|
||||
@ -195,14 +198,6 @@ export function ChangeCredentialsModal({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<a
|
||||
href={provider.apiLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline cursor-pointer"
|
||||
>
|
||||
Visit API
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{testError && (
|
||||
@ -211,13 +206,13 @@ export function ChangeCredentialsModal({
|
||||
</Callout>
|
||||
)}
|
||||
|
||||
<div className="flex mt-8 justify-between">
|
||||
<div className="flex mt-4 justify-between">
|
||||
<Button
|
||||
color="blue"
|
||||
onClick={() => handleSubmit()}
|
||||
disabled={!apiKey}
|
||||
>
|
||||
Execute Key Swap
|
||||
Swap Key
|
||||
</Button>
|
||||
</div>
|
||||
<Divider />
|
@ -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 (
|
||||
<Modal
|
||||
width="max-w-3xl"
|
||||
title={`Nuke ${modelProvider.name} Credentials?`}
|
||||
onOutsideClick={onCancel}
|
||||
>
|
@ -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 (
|
||||
<Modal title="Update Embedding Model" onOutsideClick={onCancel}>
|
||||
<Modal
|
||||
width="max-w-3xl"
|
||||
title="Update Embedding Model"
|
||||
onOutsideClick={onCancel}
|
||||
>
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<Text className="text-lg mb-4">
|
||||
@ -50,7 +54,7 @@ export function ModelSelectionConfirmationModal({
|
||||
|
||||
<div className="flex mt-8">
|
||||
<Button className="mx-auto" color="green" onClick={onConfirm}>
|
||||
Confirm
|
||||
Yes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
@ -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 (
|
||||
<Modal
|
||||
width="max-w-3xl"
|
||||
title={`Configure ${selectedProvider.name}`}
|
||||
onOutsideClick={onCancel}
|
||||
icon={selectedProvider.icon}
|
@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { Modal } from "@/components/Modal";
|
||||
import { Button, Text, Callout } from "@tremor/react";
|
||||
import { CloudEmbeddingModel } from "../components/types";
|
||||
import { Button, Text } from "@tremor/react";
|
||||
import { CloudEmbeddingModel } from "../../../../components/embedding/interfaces";
|
||||
|
||||
export function SelectModelModal({
|
||||
model,
|
||||
@ -14,18 +14,21 @@ export function SelectModelModal({
|
||||
}) {
|
||||
return (
|
||||
<Modal
|
||||
width="max-w-3xl"
|
||||
onOutsideClick={onCancel}
|
||||
title={`Update model to ${model.model_name}`}
|
||||
title={`Select ${model.model_name}`}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<Text className="text-lg mb-2">
|
||||
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.
|
||||
<br />
|
||||
Are you sure?
|
||||
</Text>
|
||||
<div className="flex mt-8 justify-end">
|
||||
<Button color="green" onClick={onConfirm}>
|
||||
Continue
|
||||
Yes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
18
web/src/app/admin/embeddings/page.tsx
Normal file
18
web/src/app/admin/embeddings/page.tsx
Normal file
@ -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 (
|
||||
<EmbeddingFormProvider>
|
||||
<div className="flex justify-center w-full h-full">
|
||||
<EmbeddingSidebar />
|
||||
<div className="mt-12 w-full max-w-5xl mx-auto">
|
||||
<EmbeddingForm />
|
||||
</div>
|
||||
</div>
|
||||
</EmbeddingFormProvider>
|
||||
);
|
||||
}
|
191
web/src/app/admin/embeddings/pages/AdvancedEmbeddingFormPage.tsx
Normal file
191
web/src/app/admin/embeddings/pages/AdvancedEmbeddingFormPage.tsx
Normal file
@ -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<SetStateAction<RerankingDetails>>;
|
||||
numRerank: number;
|
||||
}
|
||||
|
||||
const AdvancedEmbeddingFormPage = forwardRef<
|
||||
FormikProps<any>,
|
||||
AdvancedEmbeddingFormPageProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
updateAdvancedEmbeddingDetails,
|
||||
advancedEmbeddingDetails,
|
||||
setRerankingDetails,
|
||||
numRerank,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<div className="py-4 rounded-lg max-w-4xl px-4 mx-auto">
|
||||
<h2 className="text-2xl font-bold mb-4 text-text-800">
|
||||
Advanced Configuration
|
||||
</h2>
|
||||
<Formik
|
||||
innerRef={ref}
|
||||
initialValues={{
|
||||
multilingual_expansion:
|
||||
advancedEmbeddingDetails.multilingual_expansion,
|
||||
multipass_indexing: advancedEmbeddingDetails.multipass_indexing,
|
||||
disable_rerank_for_streaming:
|
||||
advancedEmbeddingDetails.disable_rerank_for_streaming,
|
||||
num_rerank: numRerank,
|
||||
}}
|
||||
validationSchema={Yup.object().shape({
|
||||
multilingual_expansion: Yup.array().of(Yup.string()),
|
||||
multipass_indexing: Yup.boolean(),
|
||||
disable_rerank_for_streaming: Yup.boolean(),
|
||||
})}
|
||||
onSubmit={async (_, { setSubmitting }) => {
|
||||
setSubmitting(false);
|
||||
}}
|
||||
enableReinitialize={true}
|
||||
>
|
||||
{({ values, setFieldValue }) => (
|
||||
<Form className="space-y-6">
|
||||
<FieldArray name="multilingual_expansion">
|
||||
{({ push, remove }) => (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="multilingual_expansion"
|
||||
className="block text-sm font-medium text-text-700 mb-1"
|
||||
>
|
||||
Multilingual Expansion
|
||||
<span className="text-text-500 ml-1">(optional)</span>
|
||||
</label>
|
||||
<CredentialSubText>
|
||||
List of languages for multilingual expansion. Leave empty
|
||||
for no additional expansion.
|
||||
</CredentialSubText>
|
||||
{values.multilingual_expansion.map(
|
||||
(_: any, index: number) => (
|
||||
<div key={index} className="w-full flex mb-4">
|
||||
<Field
|
||||
name={`multilingual_expansion.${index}`}
|
||||
className={`w-full bg-input text-sm p-2 border border-border-medium rounded-md
|
||||
focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 mr-2`}
|
||||
onChange={(
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const newValue = [
|
||||
...values.multilingual_expansion,
|
||||
];
|
||||
newValue[index] = e.target.value;
|
||||
setFieldValue("multilingual_expansion", newValue);
|
||||
updateAdvancedEmbeddingDetails(
|
||||
"multilingual_expansion",
|
||||
newValue
|
||||
);
|
||||
}}
|
||||
value={values.multilingual_expansion[index]}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
remove(index);
|
||||
const newValue =
|
||||
values.multilingual_expansion.filter(
|
||||
(_: any, i: number) => i !== index
|
||||
);
|
||||
setFieldValue("multilingual_expansion", newValue);
|
||||
updateAdvancedEmbeddingDetails(
|
||||
"multilingual_expansion",
|
||||
newValue
|
||||
);
|
||||
}}
|
||||
className={`p-2 my-auto bg-input flex-none rounded-md
|
||||
bg-red-500 text-white hover:bg-red-600
|
||||
focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50`}
|
||||
>
|
||||
<TrashIcon className="text-white my-auto" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => push("")}
|
||||
className={`mt-2 p-2 bg-rose-500 text-xs text-white rounded-md flex items-center
|
||||
hover:bg-rose-600 focus:outline-none focus:ring-2 focus:ring-rose-500 focus:ring-opacity-50`}
|
||||
>
|
||||
<FaPlus className="mr-2" />
|
||||
Add Language
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</FieldArray>
|
||||
|
||||
<div key="multipass_indexing">
|
||||
<EditingValue
|
||||
description="Enable multipass indexing for both mini and large chunks."
|
||||
optional
|
||||
currentValue={values.multipass_indexing}
|
||||
onChangeBool={(value: boolean) => {
|
||||
updateAdvancedEmbeddingDetails("multipass_indexing", value);
|
||||
setFieldValue("multipass_indexing", value);
|
||||
}}
|
||||
setFieldValue={setFieldValue}
|
||||
type="checkbox"
|
||||
label="Multipass Indexing"
|
||||
name="multipassIndexing"
|
||||
/>
|
||||
</div>
|
||||
<div key="disable_rerank_for_streaming">
|
||||
<EditingValue
|
||||
description="Disable reranking for streaming to improve response time."
|
||||
optional
|
||||
currentValue={values.disable_rerank_for_streaming}
|
||||
onChangeBool={(value: boolean) => {
|
||||
updateAdvancedEmbeddingDetails(
|
||||
"disable_rerank_for_streaming",
|
||||
value
|
||||
);
|
||||
setFieldValue("disable_rerank_for_streaming", value);
|
||||
}}
|
||||
setFieldValue={setFieldValue}
|
||||
type="checkbox"
|
||||
label="Disable Rerank for Streaming"
|
||||
name="disableRerankForStreaming"
|
||||
/>
|
||||
</div>
|
||||
<div key="num_rerank">
|
||||
<EditingValue
|
||||
description="Number of results to rerank"
|
||||
optional={false}
|
||||
currentValue={values.num_rerank}
|
||||
onChangeNumber={(value: number) => {
|
||||
setRerankingDetails({ ...values, num_rerank: value });
|
||||
setFieldValue("num_rerank", value);
|
||||
}}
|
||||
setFieldValue={setFieldValue}
|
||||
type="number"
|
||||
label="Number of Results to Rerank"
|
||||
name="num_rerank"
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
AdvancedEmbeddingFormPage.displayName = "AdvancedEmbeddingFormPage";
|
||||
export default AdvancedEmbeddingFormPage;
|
195
web/src/app/admin/embeddings/pages/CloudEmbeddingPage.tsx
Normal file
195
web/src/app/admin/embeddings/pages/CloudEmbeddingPage.tsx
Normal file
@ -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<SetStateAction<CloudEmbeddingModel | null>>;
|
||||
setShowTentativeModel: Dispatch<SetStateAction<CloudEmbeddingModel | null>>;
|
||||
currentModel: EmbeddingModelDescriptor | CloudEmbeddingModel;
|
||||
setAlreadySelectedModel: Dispatch<SetStateAction<CloudEmbeddingModel | null>>;
|
||||
newUnenabledProviders: string[];
|
||||
embeddingProviderDetails?: EmbeddingDetails[];
|
||||
newEnabledProviders: string[];
|
||||
setShowTentativeProvider: React.Dispatch<
|
||||
React.SetStateAction<CloudEmbeddingProvider | null>
|
||||
>;
|
||||
setChangeCredentialsProvider: React.Dispatch<
|
||||
React.SetStateAction<CloudEmbeddingProvider | null>
|
||||
>;
|
||||
}) {
|
||||
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 (
|
||||
<div>
|
||||
<Title className="mt-8">
|
||||
Here are some cloud-based models to choose from.
|
||||
</Title>
|
||||
<Text className="mb-4">
|
||||
These models require API keys and run in the clouds of the respective
|
||||
providers.
|
||||
</Text>
|
||||
|
||||
<div className="gap-4 mt-2 pb-10 flex content-start flex-wrap">
|
||||
{providers.map((provider) => (
|
||||
<div key={provider.name} className="mt-4 w-full">
|
||||
<div className="flex items-center mb-2">
|
||||
{provider.icon({ size: 40 })}
|
||||
<h2 className="ml-2 mt-2 text-xl font-bold">
|
||||
{provider.name} {provider.name == "Cohere" && "(recommended)"}
|
||||
</h2>
|
||||
<HoverPopup
|
||||
mainContent={
|
||||
<FiInfo className="ml-2 mt-2 cursor-pointer" size={18} />
|
||||
}
|
||||
popupContent={
|
||||
<div className="text-sm text-text-800 w-52">
|
||||
<div className="my-auto">{provider.description}</div>
|
||||
</div>
|
||||
}
|
||||
style="dark"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!provider.configured) {
|
||||
setShowTentativeProvider(provider);
|
||||
} else {
|
||||
setChangeCredentialsProvider(provider);
|
||||
}
|
||||
}}
|
||||
className="mb-2 hover:underline text-sm cursor-pointer"
|
||||
>
|
||||
{provider.configured ? "Modify API key" : "Provide API key"}
|
||||
</button>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{provider.embedding_models.map((model) => (
|
||||
<CloudModelCard
|
||||
key={model.model_name}
|
||||
model={model}
|
||||
provider={provider}
|
||||
currentModel={currentModel}
|
||||
setAlreadySelectedModel={setAlreadySelectedModel}
|
||||
setShowTentativeModel={setShowTentativeModel}
|
||||
setShowModelInQueue={setShowModelInQueue}
|
||||
setShowTentativeProvider={setShowTentativeProvider}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CloudModelCard({
|
||||
model,
|
||||
provider,
|
||||
currentModel,
|
||||
setAlreadySelectedModel,
|
||||
setShowTentativeModel,
|
||||
setShowModelInQueue,
|
||||
setShowTentativeProvider,
|
||||
}: {
|
||||
model: CloudEmbeddingModel;
|
||||
provider: CloudEmbeddingProviderFull;
|
||||
currentModel: EmbeddingModelDescriptor | CloudEmbeddingModel;
|
||||
setAlreadySelectedModel: Dispatch<SetStateAction<CloudEmbeddingModel | null>>;
|
||||
setShowTentativeModel: Dispatch<SetStateAction<CloudEmbeddingModel | null>>;
|
||||
setShowModelInQueue: Dispatch<SetStateAction<CloudEmbeddingModel | null>>;
|
||||
setShowTentativeProvider: React.Dispatch<
|
||||
React.SetStateAction<CloudEmbeddingProvider | null>
|
||||
>;
|
||||
}) {
|
||||
const enabled = model.model_name === currentModel.model_name;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`p-4 w-96 border rounded-lg transition-all duration-200 ${
|
||||
enabled
|
||||
? "border-blue-500 bg-blue-50 shadow-md"
|
||||
: "border-gray-300 hover:border-blue-300 hover:shadow-sm"
|
||||
} ${!provider.configured && "opacity-80 hover:opacity-100"}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-bold text-lg">{model.model_name}</h3>
|
||||
<a
|
||||
href={provider.website}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="text-blue-500 hover:text-blue-700 transition-colors duration-200"
|
||||
>
|
||||
<FiExternalLink size={18} />
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mb-2">{model.description}</p>
|
||||
<div className="text-xs text-gray-500 mb-2">
|
||||
${model.pricePerMillion}/M tokens
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<button
|
||||
className={`w-full p-2 rounded-lg text-sm ${
|
||||
enabled
|
||||
? "bg-background-125 border border-border cursor-not-allowed"
|
||||
: "bg-background border border-border hover:bg-hover cursor-pointer"
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (enabled) {
|
||||
setAlreadySelectedModel(model);
|
||||
} else if (provider.configured) {
|
||||
setShowTentativeModel(model);
|
||||
} else {
|
||||
setShowModelInQueue(model);
|
||||
setShowTentativeProvider(provider);
|
||||
}
|
||||
}}
|
||||
disabled={enabled}
|
||||
>
|
||||
{enabled ? "Selected Model" : "Select Model"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
431
web/src/app/admin/embeddings/pages/EmbeddingFormPage.tsx
Normal file
431
web/src/app/admin/embeddings/pages/EmbeddingFormPage.tsx
Normal file
@ -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<AdvancedDetails>({
|
||||
disable_rerank_for_streaming: false,
|
||||
multilingual_expansion: [],
|
||||
multipass_indexing: true,
|
||||
});
|
||||
|
||||
const [rerankingDetails, setRerankingDetails] = useState<RerankingDetails>({
|
||||
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<CloudEmbeddingModel | HostedEmbeddingModel | null>(
|
||||
"/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<SavedSearchSettings | null>(
|
||||
"/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 <ThreeDotsLoader />;
|
||||
}
|
||||
if (currentEmbeddingModelError || !currentEmbeddingModel) {
|
||||
return <ErrorCallout errorTitle="Failed to fetch embedding model status" />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="flex mx-auto gap-x-1 ml-auto items-center">
|
||||
<button
|
||||
className="enabled:cursor-pointer disabled:bg-accent/50 disabled:cursor-not-allowed bg-accent flex gap-x-1 items-center text-white py-2.5 px-3.5 text-sm font-regular rounded-sm"
|
||||
onClick={async () => {
|
||||
const updated = await updateSearch();
|
||||
if (updated) {
|
||||
await onConfirm();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Re-index
|
||||
</button>
|
||||
<div className="relative group">
|
||||
<WarningCircle
|
||||
className="text-text-800 cursor-help"
|
||||
size={20}
|
||||
weight="fill"
|
||||
/>
|
||||
<div className="absolute z-10 invisible group-hover:visible bg-background-800 text-text-200 text-sm rounded-md shadow-md p-2 right-0 mt-1 w-64">
|
||||
<p className="font-semibold mb-2">Needs re-indexing due to:</p>
|
||||
<ul className="list-disc pl-5">
|
||||
{currentEmbeddingModel != selectedProvider && (
|
||||
<li>Changed embedding provider</li>
|
||||
)}
|
||||
{searchSettings?.multipass_indexing !=
|
||||
advancedEmbeddingDetails.multipass_indexing && (
|
||||
<li>Multipass indexing modification</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto mb-8 w-full">
|
||||
{popup}
|
||||
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="mx-auto max-w-4xl">
|
||||
{formStep == 0 && (
|
||||
<>
|
||||
<h2 className="text-2xl font-bold mb-4 text-text-800">
|
||||
Select an Embedding Model
|
||||
</h2>
|
||||
<Text className="mb-4">
|
||||
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.
|
||||
</Text>
|
||||
<Card>
|
||||
<EmbeddingModelSelection
|
||||
setModelTab={setModelTab}
|
||||
modelTab={modelTab}
|
||||
selectedProvider={selectedProvider}
|
||||
currentEmbeddingModel={currentEmbeddingModel}
|
||||
updateSelectedProvider={updateSelectedProvider}
|
||||
/>
|
||||
</Card>
|
||||
<div className="mt-4 flex w-full justify-end">
|
||||
<button
|
||||
className="enabled:cursor-pointer disabled:cursor-not-allowed disabled:bg-blue-200 bg-blue-400 flex gap-x-1 items-center text-white py-2.5 px-3.5 text-sm font-regular rounded-sm"
|
||||
onClick={() => {
|
||||
if (
|
||||
selectedProvider.model_name.includes("e5") &&
|
||||
displayPoorModelName
|
||||
) {
|
||||
setDisplayPoorModelName(false);
|
||||
setShowPoorModel(true);
|
||||
} else {
|
||||
nextFormStep();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
<ArrowRight />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{showPoorModel && (
|
||||
<Modal
|
||||
onOutsideClick={() => setShowPoorModel(false)}
|
||||
width="max-w-3xl"
|
||||
title={`Are you sure you want to select ${selectedProvider.model_name}?`}
|
||||
>
|
||||
<>
|
||||
<div className="text-lg">
|
||||
{selectedProvider.model_name} is a low-performance model.
|
||||
<br />
|
||||
We recommend the following alternatives.
|
||||
<li>OpenAI for cloud-based</li>
|
||||
<li>Nomic for self-hosted</li>
|
||||
</div>
|
||||
<div className="flex mt-4 justify-between">
|
||||
<Button color="green" onClick={() => setShowPoorModel(false)}>
|
||||
Cancel update
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowPoorModel(false);
|
||||
nextFormStep();
|
||||
}}
|
||||
>
|
||||
Continue with {selectedProvider.model_name}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{formStep == 1 && (
|
||||
<>
|
||||
<Card>
|
||||
<RerankingDetailsForm
|
||||
setModelTab={setModelTab}
|
||||
modelTab={
|
||||
originalRerankingDetails.rerank_model_name
|
||||
? modelTab
|
||||
: modelTab || "cloud"
|
||||
}
|
||||
currentRerankingDetails={rerankingDetails}
|
||||
originalRerankingDetails={originalRerankingDetails}
|
||||
setRerankingDetails={setRerankingDetails}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<div className={` mt-4 w-full grid grid-cols-3`}>
|
||||
<button
|
||||
className="border-border-dark mr-auto border flex gap-x-1 items-center text-text p-2.5 text-sm font-regular rounded-sm "
|
||||
onClick={() => prevFormStep()}
|
||||
>
|
||||
<ArrowLeft />
|
||||
Previous
|
||||
</button>
|
||||
|
||||
{needsReIndex ? (
|
||||
<ReIndxingButton />
|
||||
) : (
|
||||
<button
|
||||
className="enabled:cursor-pointer ml-auto disabled:bg-accent/50 disabled:cursor-not-allowed bg-accent flex mx-auto gap-x-1 items-center text-white py-2.5 px-3.5 text-sm font-regular rounded-sm"
|
||||
onClick={async () => {
|
||||
updateSearch();
|
||||
}}
|
||||
>
|
||||
Update Search
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="flex w-full justify-end">
|
||||
<button
|
||||
className={`enabled:cursor-pointer enabled:hover:underline disabled:cursor-not-allowed mt-auto enabled:text-text-600 disabled:text-text-400 ml-auto flex gap-x-1 items-center py-2.5 px-3.5 text-sm font-regular rounded-sm`}
|
||||
// disabled={!isFormValid}
|
||||
onClick={() => {
|
||||
nextFormStep();
|
||||
}}
|
||||
>
|
||||
Advanced
|
||||
<ArrowRight />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{formStep == 2 && (
|
||||
<>
|
||||
<Card>
|
||||
<AdvancedEmbeddingFormPage
|
||||
numRerank={rerankingDetails.num_rerank}
|
||||
setRerankingDetails={setRerankingDetails}
|
||||
advancedEmbeddingDetails={advancedEmbeddingDetails}
|
||||
updateAdvancedEmbeddingDetails={updateAdvancedEmbeddingDetails}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<div className={`mt-4 grid grid-cols-3 w-full `}>
|
||||
<button
|
||||
className={`border-border-dark border mr-auto flex gap-x-1
|
||||
items-center text-text py-2.5 px-3.5 text-sm font-regular rounded-sm`}
|
||||
onClick={() => prevFormStep()}
|
||||
>
|
||||
<ArrowLeft />
|
||||
Previous
|
||||
</button>
|
||||
|
||||
{needsReIndex ? (
|
||||
<ReIndxingButton />
|
||||
) : (
|
||||
<button
|
||||
className="enabled:cursor-pointer ml-auto disabled:bg-accent/50
|
||||
disabled:cursor-not-allowed bg-accent flex mx-auto gap-x-1 items-center
|
||||
text-white py-2.5 px-3.5 text-sm font-regular rounded-sm"
|
||||
onClick={async () => {
|
||||
updateSearch();
|
||||
}}
|
||||
>
|
||||
Update Search
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
69
web/src/app/admin/embeddings/pages/OpenEmbeddingPage.tsx
Normal file
69
web/src/app/admin/embeddings/pages/OpenEmbeddingPage.tsx
Normal file
@ -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<void>;
|
||||
selectedProvider: HostedEmbeddingModel | CloudEmbeddingModel;
|
||||
}) {
|
||||
const [configureModel, setConfigureModel] = useState(false);
|
||||
return (
|
||||
<div>
|
||||
<Title className="mt-8">
|
||||
Here are some locally-hosted models to choose from.
|
||||
</Title>
|
||||
<Text className="mb-4">
|
||||
These models can be used without any API keys, and can leverage a GPU
|
||||
for faster inference.
|
||||
</Text>
|
||||
<ModelSelector
|
||||
modelOptions={AVAILABLE_MODELS}
|
||||
setSelectedModel={onSelectOpenSource}
|
||||
currentEmbeddingModel={selectedProvider}
|
||||
/>
|
||||
|
||||
<Text className="mt-6">
|
||||
Alternatively, (if you know what you're doing) you can specify a{" "}
|
||||
<a target="_blank" href="https://www.sbert.net/" className="text-link">
|
||||
SentenceTransformers
|
||||
</a>
|
||||
-compatible model of your choice below. The rough list of supported
|
||||
models can be found{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://huggingface.co/models?library=sentence-transformers&sort=trending"
|
||||
className="text-link"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
<br />
|
||||
<b>NOTE:</b> 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.
|
||||
</Text>
|
||||
{!configureModel && (
|
||||
<Button onClick={() => setConfigureModel(true)} className="mt-4">
|
||||
Configure custom model
|
||||
</Button>
|
||||
)}
|
||||
{configureModel && (
|
||||
<div className="w-full flex">
|
||||
<Card className="mt-4 2xl:w-4/6 mx-auto">
|
||||
<CustomModelForm onSubmit={onSelectOpenSource} />
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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<SetStateAction<CloudEmbeddingModel | null>>;
|
||||
setShowTentativeModel: Dispatch<SetStateAction<CloudEmbeddingModel | null>>;
|
||||
currentModel: EmbeddingModelDescriptor | CloudEmbeddingModel;
|
||||
setAlreadySelectedModel: Dispatch<SetStateAction<CloudEmbeddingModel | null>>;
|
||||
newUnenabledProviders: string[];
|
||||
embeddingProviderDetails?: EmbeddingDetails[];
|
||||
newEnabledProviders: string[];
|
||||
selectedModel: CloudEmbeddingProvider;
|
||||
|
||||
// create modal functions
|
||||
|
||||
setShowTentativeProvider: React.Dispatch<
|
||||
React.SetStateAction<CloudEmbeddingProvider | null>
|
||||
>;
|
||||
setChangeCredentialsProvider: React.Dispatch<
|
||||
React.SetStateAction<CloudEmbeddingProvider | null>
|
||||
>;
|
||||
}) {
|
||||
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 (
|
||||
<div>
|
||||
<Title className="mt-8">
|
||||
Here are some cloud-based models to choose from.
|
||||
</Title>
|
||||
<Text className="mb-4">
|
||||
They require API keys and run in the clouds of the respective providers.
|
||||
</Text>
|
||||
|
||||
<div className="gap-4 mt-2 pb-10 flex content-start flex-wrap">
|
||||
{providers.map((provider, ind) => (
|
||||
<div
|
||||
key={ind}
|
||||
className="p-4 border border-border rounded-lg shadow-md bg-hover-light w-96 flex flex-col"
|
||||
>
|
||||
<div className="font-bold text-text-900 text-lg items-center py-1 gap-x-2 flex">
|
||||
{provider.icon({ size: 40 })}
|
||||
<p className="my-auto">{provider.name}</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowTentativeProvider(provider);
|
||||
}}
|
||||
className="cursor-pointer ml-auto"
|
||||
>
|
||||
<a className="my-auto hover:underline cursor-pointer">
|
||||
<HoverPopup
|
||||
mainContent={
|
||||
<FiInfo className="cusror-pointer" size={20} />
|
||||
}
|
||||
popupContent={
|
||||
<div className="text-sm text-text-800 w-52 flex">
|
||||
<div className="flex mx-auto">
|
||||
<div className="my-auto">{provider.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
direction="left-top"
|
||||
style="dark"
|
||||
/>
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{provider.embedding_models.map((model, index) => {
|
||||
const enabled = model.model_name == currentModel.model_name;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`p-3 my-2 border-2 border-border-medium border-opacity-40 rounded-md rounded cursor-pointer
|
||||
${
|
||||
!provider.configured
|
||||
? "opacity-80 hover:opacity-100"
|
||||
: enabled
|
||||
? "bg-background-stronger"
|
||||
: "hover:bg-background-strong"
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (enabled) {
|
||||
setAlreadySelectedModel(model);
|
||||
} else if (provider.configured) {
|
||||
setShowTentativeModel(model);
|
||||
} else {
|
||||
setShowModelInQueue(model);
|
||||
setShowTentativeProvider(provider);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<div className="font-medium text-sm">
|
||||
{model.model_name}
|
||||
</div>
|
||||
<p className="text-sm flex-none">
|
||||
${model.pricePerMillion}/M tokens
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
{model.description}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!provider.configured) {
|
||||
setShowTentativeProvider(provider);
|
||||
} else {
|
||||
setChangeCredentialsProvider(provider);
|
||||
}
|
||||
}}
|
||||
className="hover:underline mb-1 text-sm mr-auto cursor-pointer"
|
||||
>
|
||||
{provider.configured && "Modify credentials"}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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<void>;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<ModelSelector
|
||||
modelOptions={AVAILABLE_MODELS.filter(
|
||||
(modelOption) => modelOption.model_name !== currentModelName
|
||||
)}
|
||||
setSelectedModel={onSelectOpenSource}
|
||||
/>
|
||||
|
||||
<Text className="mt-6">
|
||||
Alternatively, (if you know what you're doing) you can specify a{" "}
|
||||
<a target="_blank" href="https://www.sbert.net/" className="text-link">
|
||||
SentenceTransformers
|
||||
</a>
|
||||
-compatible model of your choice below. The rough list of supported
|
||||
models can be found{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://huggingface.co/models?library=sentence-transformers&sort=trending"
|
||||
className="text-link"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
<br />
|
||||
<b>NOTE:</b> 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.
|
||||
</Text>
|
||||
|
||||
<div className="w-full flex">
|
||||
<Card className="mt-4 2xl:w-4/6 mx-auto">
|
||||
<CustomModelForm onSubmit={onSelectOpenSource} />
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
import { EmbeddingModelDescriptor, HostedEmbeddingModel } from "./types";
|
||||
import { FiStar } from "react-icons/fi";
|
||||
|
||||
export function ModelPreview({ model }: { model: EmbeddingModelDescriptor }) {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"p-2 border border-border rounded shadow-md bg-hover-light w-96 flex flex-col"
|
||||
}
|
||||
>
|
||||
<div className="font-bold text-lg flex">{model.model_name}</div>
|
||||
<div className="text-sm mt-1 mx-1">
|
||||
{model.description
|
||||
? model.description
|
||||
: "Custom model—no description is available."}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ModelOption({
|
||||
model,
|
||||
onSelect,
|
||||
}: {
|
||||
model: HostedEmbeddingModel;
|
||||
onSelect?: (model: HostedEmbeddingModel) => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"p-2 border border-border rounded shadow-md bg-hover-light w-96 flex flex-col"
|
||||
}
|
||||
>
|
||||
<div className="font-bold text-lg flex">
|
||||
{model.isDefault && <FiStar className="my-auto mr-1 text-accent" />}
|
||||
{model.model_name}
|
||||
</div>
|
||||
<div className="text-sm mt-1 mx-1">
|
||||
{model.description
|
||||
? model.description
|
||||
: "Custom model—no description is available."}
|
||||
</div>
|
||||
{model.link && (
|
||||
<a
|
||||
target="_blank"
|
||||
href={model.link}
|
||||
className="text-xs text-link mx-1 mt-1"
|
||||
>
|
||||
See More Details
|
||||
</a>
|
||||
)}
|
||||
{onSelect && (
|
||||
<div
|
||||
className={`
|
||||
m-auto
|
||||
flex
|
||||
mt-3
|
||||
mb-1
|
||||
w-fit
|
||||
p-2
|
||||
rounded-lg
|
||||
bg-background
|
||||
border
|
||||
border-border
|
||||
cursor-pointer
|
||||
hover:bg-hover
|
||||
text-sm
|
||||
mt-auto`}
|
||||
onClick={() => onSelect(model)}
|
||||
>
|
||||
Select Model
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ModelSelector({
|
||||
modelOptions,
|
||||
setSelectedModel,
|
||||
}: {
|
||||
modelOptions: HostedEmbeddingModel[];
|
||||
setSelectedModel: (model: HostedEmbeddingModel) => void;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{modelOptions.map((modelOption) => (
|
||||
<ModelOption
|
||||
key={modelOption.model_name}
|
||||
model={modelOption}
|
||||
onSelect={setSelectedModel}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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<CloudEmbeddingProvider | null>(null);
|
||||
const [showUnconfiguredProvider, setShowUnconfiguredProvider] =
|
||||
useState<CloudEmbeddingProvider | null>(null);
|
||||
const [changeCredentialsProvider, setChangeCredentialsProvider] =
|
||||
useState<CloudEmbeddingProvider | null>(null);
|
||||
|
||||
// Cloud Model based modals
|
||||
const [alreadySelectedModel, setAlreadySelectedModel] =
|
||||
useState<CloudEmbeddingModel | null>(null);
|
||||
const [showTentativeModel, setShowTentativeModel] =
|
||||
useState<CloudEmbeddingModel | null>(null);
|
||||
|
||||
const [showModelInQueue, setShowModelInQueue] =
|
||||
useState<CloudEmbeddingModel | null>(null);
|
||||
|
||||
// Open Model based modals
|
||||
const [showTentativeOpenProvider, setShowTentativeOpenProvider] =
|
||||
useState<HostedEmbeddingModel | null>(null);
|
||||
|
||||
// Enabled / unenabled providers
|
||||
const [newEnabledProviders, setNewEnabledProviders] = useState<string[]>([]);
|
||||
const [newUnenabledProviders, setNewUnenabledProviders] = useState<string[]>(
|
||||
[]
|
||||
);
|
||||
|
||||
const [showDeleteCredentialsModal, setShowDeleteCredentialsModal] =
|
||||
useState<boolean>(false);
|
||||
const [isCancelling, setIsCancelling] = useState<boolean>(false);
|
||||
const [showAddConnectorPopup, setShowAddConnectorPopup] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const {
|
||||
data: currentEmeddingModel,
|
||||
isLoading: isLoadingCurrentModel,
|
||||
error: currentEmeddingModelError,
|
||||
} = useSWR<CloudEmbeddingModel | HostedEmbeddingModel | null>(
|
||||
"/api/search-settings/get-current-embedding-model",
|
||||
errorHandlingFetcher,
|
||||
{ refreshInterval: 5000 } // 5 seconds
|
||||
);
|
||||
|
||||
const { data: embeddingProviderDetails } = useSWR<EmbeddingDetails[]>(
|
||||
EMBEDDING_PROVIDERS_ADMIN_URL,
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
const {
|
||||
data: futureEmbeddingModel,
|
||||
isLoading: isLoadingFutureModel,
|
||||
error: futureEmeddingModelError,
|
||||
} = useSWR<CloudEmbeddingModel | HostedEmbeddingModel | null>(
|
||||
"/api/search-settings/get-secondary-embedding-model",
|
||||
errorHandlingFetcher,
|
||||
{ refreshInterval: 5000 } // 5 seconds
|
||||
);
|
||||
const {
|
||||
data: ongoingReIndexingStatus,
|
||||
isLoading: isLoadingOngoingReIndexingStatus,
|
||||
} = useSWR<ConnectorIndexingStatus<any, any>[]>(
|
||||
"/api/manage/admin/connector/indexing-status?secondary_index=true",
|
||||
errorHandlingFetcher,
|
||||
{ refreshInterval: 5000 } // 5 seconds
|
||||
);
|
||||
const { data: connectors } = useSWR<Connector<any>[]>(
|
||||
"/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 <ThreeDotsLoader />;
|
||||
}
|
||||
|
||||
if (
|
||||
currentEmeddingModelError ||
|
||||
!currentEmeddingModel ||
|
||||
futureEmeddingModelError
|
||||
) {
|
||||
return <ErrorCallout errorTitle="Failed to fetch embedding model status" />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="h-screen">
|
||||
<Text>
|
||||
These deep learning models are used to generate vector representations
|
||||
of your documents, which then power Danswer's search.
|
||||
</Text>
|
||||
|
||||
{alreadySelectedModel && (
|
||||
<AlreadyPickedModal
|
||||
model={alreadySelectedModel}
|
||||
onClose={() => setAlreadySelectedModel(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showTentativeOpenProvider && (
|
||||
<ModelSelectionConfirmationModal
|
||||
selectedModel={showTentativeOpenProvider}
|
||||
isCustom={
|
||||
AVAILABLE_MODELS.find(
|
||||
(model) =>
|
||||
model.model_name === showTentativeOpenProvider.model_name
|
||||
) === undefined
|
||||
}
|
||||
onConfirm={() => onConfirm(showTentativeOpenProvider)}
|
||||
onCancel={() => setShowTentativeOpenProvider(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showTentativeProvider && (
|
||||
<ProviderCreationModal
|
||||
selectedProvider={showTentativeProvider}
|
||||
onConfirm={() => {
|
||||
setShowTentativeProvider(showUnconfiguredProvider);
|
||||
clientsideAddProvider(showTentativeProvider);
|
||||
if (showModelInQueue) {
|
||||
setShowTentativeModel(showModelInQueue);
|
||||
}
|
||||
}}
|
||||
onCancel={() => {
|
||||
setShowModelInQueue(null);
|
||||
setShowTentativeProvider(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{changeCredentialsProvider && (
|
||||
<ChangeCredentialsModal
|
||||
// setPopup={setPopup}
|
||||
useFileUpload={changeCredentialsProvider.name == "Google"}
|
||||
onDeleted={() => {
|
||||
clientsideRemoveProvider(changeCredentialsProvider);
|
||||
setChangeCredentialsProvider(null);
|
||||
}}
|
||||
provider={changeCredentialsProvider}
|
||||
onConfirm={() => setChangeCredentialsProvider(null)}
|
||||
onCancel={() => setChangeCredentialsProvider(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showTentativeModel && (
|
||||
<SelectModelModal
|
||||
model={showTentativeModel}
|
||||
onConfirm={() => {
|
||||
setShowModelInQueue(null);
|
||||
onConfirm(showTentativeModel);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setShowModelInQueue(null);
|
||||
setShowTentativeModel(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showDeleteCredentialsModal && (
|
||||
<DeleteCredentialsModal
|
||||
modelProvider={showTentativeProvider!}
|
||||
onConfirm={() => {
|
||||
setShowDeleteCredentialsModal(false);
|
||||
}}
|
||||
onCancel={() => setShowDeleteCredentialsModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentModel ? (
|
||||
<>
|
||||
<Title className="mt-8 mb-2">Current Embedding Model</Title>
|
||||
<ModelPreview model={currentModel} />
|
||||
</>
|
||||
) : (
|
||||
<Title className="mt-8 mb-4">Choose your Embedding Model</Title>
|
||||
)}
|
||||
|
||||
{!(futureEmbeddingModel && connectors && connectors.length > 0) && (
|
||||
<>
|
||||
<Title className="mt-8">Switch your Embedding Model</Title>
|
||||
<Text className="mb-4">
|
||||
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.
|
||||
</Text>
|
||||
|
||||
<div className="mt-8 text-sm mr-auto mb-12 divide-x-2 flex">
|
||||
<button
|
||||
onClick={() => setOpenToggle(true)}
|
||||
className={` mx-2 p-2 font-bold ${
|
||||
openToggle
|
||||
? "rounded bg-background-900 text-text-100 underline"
|
||||
: "hover:underline"
|
||||
}`}
|
||||
>
|
||||
Self-hosted
|
||||
</button>
|
||||
<div className="px-2 ">
|
||||
<button
|
||||
onClick={() => setOpenToggle(false)}
|
||||
className={`mx-2 p-2 font-bold ${
|
||||
!openToggle
|
||||
? "rounded bg-background-900 text-text-100 underline"
|
||||
: " hover:underline"
|
||||
}`}
|
||||
>
|
||||
Cloud-based
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!showAddConnectorPopup &&
|
||||
!futureEmbeddingModel &&
|
||||
(openToggle ? (
|
||||
<OpenEmbeddingPage
|
||||
onSelectOpenSource={onSelectOpenSource}
|
||||
currentModelName={currentModelName!}
|
||||
/>
|
||||
) : (
|
||||
<CloudEmbeddingPage
|
||||
setShowModelInQueue={setShowModelInQueue}
|
||||
setShowTentativeModel={setShowTentativeModel}
|
||||
currentModel={currentModel}
|
||||
setAlreadySelectedModel={setAlreadySelectedModel}
|
||||
embeddingProviderDetails={embeddingProviderDetails}
|
||||
newEnabledProviders={newEnabledProviders}
|
||||
newUnenabledProviders={newUnenabledProviders}
|
||||
setShowTentativeProvider={setShowTentativeProvider}
|
||||
selectedModel={selectedModel}
|
||||
setChangeCredentialsProvider={setChangeCredentialsProvider}
|
||||
/>
|
||||
))}
|
||||
|
||||
{openToggle && (
|
||||
<>
|
||||
{showAddConnectorPopup && (
|
||||
<Modal>
|
||||
<div>
|
||||
<div>
|
||||
<b className="text-base">
|
||||
Embedding model successfully selected
|
||||
</b>{" "}
|
||||
🙌
|
||||
<br />
|
||||
<br />
|
||||
To complete the initial setup, let's add a connector!
|
||||
<br />
|
||||
<br />
|
||||
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.
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Link
|
||||
className="mx-auto mt-2 w-fit"
|
||||
href="/admin/add-connector"
|
||||
>
|
||||
<Button className="mt-3 mx-auto" size="xs">
|
||||
Add Connector
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{isCancelling && (
|
||||
<Modal
|
||||
onOutsideClick={() => setIsCancelling(false)}
|
||||
title="Cancel Embedding Model Switch"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
Are you sure you want to cancel?
|
||||
<br />
|
||||
<br />
|
||||
Cancelling will revert to the previous model and all progress
|
||||
will be lost.
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
className="mt-3 mx-auto"
|
||||
color="green"
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{futureEmbeddingModel && connectors && connectors.length > 0 && (
|
||||
<div>
|
||||
<Title className="mt-8">Current Upgrade Status</Title>
|
||||
<div className="mt-4">
|
||||
<div className="italic text-lg mb-2">
|
||||
Currently in the process of switching to:{" "}
|
||||
{futureEmbeddingModel.model_name}
|
||||
</div>
|
||||
{/* <ModelOption model={futureEmbeddingModel} /> */}
|
||||
|
||||
<Button
|
||||
color="red"
|
||||
size="xs"
|
||||
className="mt-4"
|
||||
onClick={() => setIsCancelling(true)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Text className="my-4">
|
||||
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.
|
||||
</Text>
|
||||
|
||||
{isLoadingOngoingReIndexingStatus ? (
|
||||
<ThreeDotsLoader />
|
||||
) : ongoingReIndexingStatus ? (
|
||||
<ReindexingProgressTable
|
||||
reindexingProgress={ongoingReIndexingStatus}
|
||||
/>
|
||||
) : (
|
||||
<ErrorCallout errorTitle="Failed to fetch re-indexing progress" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Page() {
|
||||
return (
|
||||
<div className="mx-auto container">
|
||||
<AdminPageTitle
|
||||
title="Embedding"
|
||||
icon={<EmbeddingIcon size={32} className="my-auto" />}
|
||||
/>
|
||||
|
||||
<Main />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
@ -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 {
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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,
|
||||
|
@ -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: (
|
||||
<div className="flex">
|
||||
{/* <FiCpu size={18} /> */}
|
||||
<CpuIconSkeleton size={18} />
|
||||
<div className="ml-1">LLM</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/models/llm",
|
||||
link: "/admin/configuration/llm",
|
||||
},
|
||||
{
|
||||
error: settings?.settings.needs_reindexing,
|
||||
name: (
|
||||
<div className="flex">
|
||||
<EmbeddingIconSkeleton />
|
||||
<div className="ml-1">Embedding</div>
|
||||
<SearchIcon />
|
||||
<CustomTooltip content="Navigate here to update your search settings">
|
||||
<div className="ml-1">Search Settings</div>
|
||||
</CustomTooltip>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/models/embedding",
|
||||
link: "/admin/configuration/search",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -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}
|
||||
>
|
||||
<AnnouncementBanner />
|
||||
{children}
|
||||
</ClientLayout>
|
||||
);
|
||||
|
@ -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[] }) {
|
||||
</h2>
|
||||
{collection.items.map((item) => (
|
||||
<Link key={item.link} href={item.link}>
|
||||
<button className="text-sm block w-52 py-2.5 px-2 text-left hover:bg-hover rounded">
|
||||
<button
|
||||
className={` text-sm block flex gap-x-2 items-center w-52 py-2.5 px-2 text-left hover:bg-hover rounded`}
|
||||
>
|
||||
{item.name}
|
||||
{item.error && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<WarningCircle size={18} className="text-error" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="max-w-xs text-text-100 mb-1 p-2 rounded-lg bg-background-900">
|
||||
Navigate here to update your search settings
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</button>
|
||||
</Link>
|
||||
))}
|
||||
|
@ -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";
|
||||
|
||||
|
103
web/src/components/context/EmbeddingContext.tsx
Normal file
103
web/src/components/context/EmbeddingContext.tsx
Normal file
@ -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<string, any>;
|
||||
setFormValues: (values: Record<string, any>) => void;
|
||||
nextFormStep: (contract?: string) => void;
|
||||
prevFormStep: () => void;
|
||||
formStepToLast: () => void;
|
||||
setFormStep: React.Dispatch<React.SetStateAction<number>>;
|
||||
allowAdvanced: boolean;
|
||||
setAllowAdvanced: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
allowCreate: boolean;
|
||||
setAlowCreate: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
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<Record<string, any>>({});
|
||||
|
||||
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 (
|
||||
<EmbeddingFormContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</EmbeddingFormContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useEmbeddingFormContext = () => {
|
||||
const context = useContext(EmbeddingFormContext);
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
"useEmbeddingFormContext must be used within a FormProvider"
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
@ -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<boolean | string | number | Date>();
|
||||
const [value, setValue] = useState<boolean | string | number | Date>(
|
||||
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" ? (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-700 mb-1">
|
||||
{label}
|
||||
{optional && (
|
||||
<span className="text-text-500 ml-1">(optional)</span>
|
||||
)}
|
||||
</label>
|
||||
{description && <SubLabel>{description}</SubLabel>}
|
||||
|
||||
<select
|
||||
name={name}
|
||||
value={value as string}
|
||||
onChange={(e) => {
|
||||
updateValue(e.target.value);
|
||||
if (onChange) {
|
||||
onChange(e.target.value);
|
||||
}
|
||||
}}
|
||||
className="mt-2 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md text-sm shadow-sm
|
||||
focus:outline-none focus:border-sky-500 focus:ring-1 focus:ring-sky-500"
|
||||
>
|
||||
<option value="">Select an option</option>
|
||||
{options?.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
) : (
|
||||
// Default
|
||||
<AdminTextField
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
import { Button } from "@tremor/react";
|
||||
import { Form, Formik } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import { HostedEmbeddingModel } from "./types";
|
||||
import { HostedEmbeddingModel } from "./interfaces";
|
||||
|
||||
export function CustomModelForm({
|
||||
onSubmit,
|
96
web/src/components/embedding/EmbeddingSidebar.tsx
Normal file
96
web/src/components/embedding/EmbeddingSidebar.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import { useEmbeddingFormContext } from "@/components/context/EmbeddingContext";
|
||||
import { HeaderTitle } from "@/components/header/HeaderTitle";
|
||||
|
||||
import { SettingsIcon } from "@/components/icons/icons";
|
||||
import { Logo } from "@/components/Logo";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import Link from "next/link";
|
||||
import { useContext } from "react";
|
||||
|
||||
export default function EmbeddingSidebar() {
|
||||
const { formStep, setFormStep, allowAdvanced, allowCreate } =
|
||||
useEmbeddingFormContext();
|
||||
const combinedSettings = useContext(SettingsContext);
|
||||
if (!combinedSettings) {
|
||||
return null;
|
||||
}
|
||||
const enterpriseSettings = combinedSettings.enterpriseSettings;
|
||||
|
||||
const settingSteps = ["Embedding Model", "Reranking Model", "Advanced"];
|
||||
|
||||
return (
|
||||
<div className="flex bg-background text-default ">
|
||||
<div
|
||||
className={`flex-none
|
||||
bg-background-100
|
||||
h-screen
|
||||
transition-all
|
||||
bg-opacity-80
|
||||
duration-300
|
||||
ease-in-out
|
||||
w-[250px]
|
||||
`}
|
||||
>
|
||||
<div className="fixed h-full left-0 top-0 w-[250px]">
|
||||
<div className="ml-4 mr-3 flex flex gap-x-1 items-center mt-2 my-auto text-text-700 text-xl">
|
||||
<div className="mr-1 my-auto h-6 w-6">
|
||||
<Logo height={24} width={24} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{enterpriseSettings && enterpriseSettings.application_name ? (
|
||||
<HeaderTitle>{enterpriseSettings.application_name}</HeaderTitle>
|
||||
) : (
|
||||
<HeaderTitle>Danswer</HeaderTitle>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-3 mt-6 gap-y-1 flex-col flex gap-x-1.5 items-center items-center">
|
||||
<Link
|
||||
href={"/admin/configuration/search"}
|
||||
className="w-full p-2 bg-white border-border border rounded items-center hover:bg-background-200 cursor-pointer transition-all duration-150 flex gap-x-2"
|
||||
>
|
||||
<SettingsIcon className="flex-none " />
|
||||
<p className="my-auto flex items-center text-sm">
|
||||
Search Settings
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="h-full flex">
|
||||
<div className="mx-auto w-full max-w-2xl px-4 py-8">
|
||||
<div className="relative">
|
||||
<div className="absolute h-[85%] left-[6px] top-[8px] bottom-0 w-0.5 bg-gray-300"></div>
|
||||
{settingSteps.map((step, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center mb-6 relative cursor-pointer"
|
||||
onClick={() => {
|
||||
setFormStep(index);
|
||||
}}
|
||||
>
|
||||
<div className="flex-shrink-0 mr-4 z-10">
|
||||
<div className="rounded-full h-3.5 w-3.5 flex items-center justify-center bg-blue-500">
|
||||
{formStep === index && (
|
||||
<div className="h-2 w-2 rounded-full bg-white"></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`${index <= formStep ? "text-gray-800" : "text-gray-500"}`}
|
||||
>
|
||||
{step}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
142
web/src/components/embedding/ModelSelector.tsx
Normal file
142
web/src/components/embedding/ModelSelector.tsx
Normal file
@ -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 (
|
||||
<div
|
||||
className={`border border-border rounded shadow-md ${display ? "bg-inverted rounded-lg p-4" : "bg-hover-light p-2"} w-96 flex flex-col`}
|
||||
>
|
||||
<div className="font-bold text-lg flex">{model.model_name}</div>
|
||||
<div className="text-sm mt-1 mx-1">
|
||||
{model.description
|
||||
? model.description
|
||||
: "Custom model—no description is available."}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ModelOption({
|
||||
model,
|
||||
onSelect,
|
||||
selected,
|
||||
}: {
|
||||
model: HostedEmbeddingModel;
|
||||
onSelect?: (model: HostedEmbeddingModel) => void;
|
||||
selected: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`p-4 w-96 border rounded-lg transition-all duration-200 ${
|
||||
selected
|
||||
? "border-blue-500 bg-blue-50 shadow-md"
|
||||
: "border-gray-200 hover:border-blue-300 hover:shadow-sm"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-bold text-lg">{model.model_name}</h3>
|
||||
|
||||
{model.link && (
|
||||
<a
|
||||
href={model.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="text-blue-500 hover:text-blue-700 transition-colors duration-200"
|
||||
>
|
||||
<FiExternalLink size={18} />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm k text-gray-600 text-left mb-2">
|
||||
{model.description || "Custom model—no description is available."}
|
||||
</p>
|
||||
<div className="text-xs text-gray-500">
|
||||
{model.isDefault ? "Default" : "Self-hosted"}
|
||||
</div>
|
||||
{onSelect && (
|
||||
<div className="mt-3">
|
||||
<button
|
||||
className={`w-full p-2 rounded-lg text-sm ${
|
||||
selected
|
||||
? "bg-background-125 border border-border cursor-not-allowed"
|
||||
: "bg-background border border-border hover:bg-hover cursor-pointer"
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!selected) onSelect(model);
|
||||
}}
|
||||
disabled={selected}
|
||||
>
|
||||
{selected ? "Selected Model" : "Select Model"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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<string, HostedEmbeddingModel[]>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col gap-y-6 gap-6">
|
||||
{Object.entries(groupedModelOptions).map(([type, models]) => (
|
||||
<div key={type}>
|
||||
<div className="flex items-center mb-2">
|
||||
{getIconForRerankType(type)}
|
||||
<h2 className="ml-2 mt-2 text-xl font-bold">
|
||||
{getTitleForRerankType(type)}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex mt-4 flex-wrap gap-4">
|
||||
{models.map((modelOption) => (
|
||||
<ModelOption
|
||||
key={modelOption.model_name}
|
||||
model={modelOption}
|
||||
onSelect={setSelectedModel}
|
||||
selected={currentEmbeddingModel === modelOption}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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 <NomicIcon size={40} />;
|
||||
case "intfloat":
|
||||
return <MicrosoftIcon size={40} />;
|
||||
default:
|
||||
return <OpenSourceIcon size={40} />;
|
||||
}
|
||||
};
|
||||
|
||||
export const INVALID_OLD_MODEL = "thenlper/gte-small";
|
||||
|
||||
export function checkModelNameIsValid(
|
101
web/src/components/header/AnnouncementBanner.tsx
Normal file
101
web/src/components/header/AnnouncementBanner.tsx
Normal file
@ -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 (
|
||||
<div
|
||||
key={notification.id}
|
||||
className="absolute top-0 left-1/2 transform -translate-x-1/2 bg-blue-600 rounded-sm text-white px-4 pr-8 py-3 mx-auto"
|
||||
>
|
||||
<p className="text-center">
|
||||
Your index is out of date - we strongly recommend updating
|
||||
your search settings.{" "}
|
||||
<Link
|
||||
href={"/admin/configuration/search"}
|
||||
className="ml-2 underline cursor-pointer"
|
||||
>
|
||||
Update here
|
||||
</Link>
|
||||
</p>
|
||||
<button
|
||||
onClick={() => handleDismiss(notification.id)}
|
||||
className="absolute top-0 right-0 mt-2 mr-2"
|
||||
aria-label="Dismiss"
|
||||
>
|
||||
<CustomTooltip
|
||||
showTick
|
||||
citation
|
||||
delay={100}
|
||||
content="Dismiss"
|
||||
>
|
||||
<XIcon className="h-5 w-5" />
|
||||
</CustomTooltip>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
@ -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 = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const MixedBreadIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
}: IconProps) => {
|
||||
return (
|
||||
<div
|
||||
style={{ width: `${size + 4}px`, height: `${size + 4}px` }}
|
||||
className={`w-[${size + 4}px] h-[${size + 4}px] -m-0.5 ` + className}
|
||||
>
|
||||
<Image src={mixedBreadSVG} alt="Logo" width="96" height="96" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const NomicIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
}: IconProps) => {
|
||||
return (
|
||||
<div
|
||||
style={{ width: `${size + 4}px`, height: `${size + 4}px` }}
|
||||
className={`w-[${size + 4}px] h-[${size + 4}px] -m-0.5 ` + className}
|
||||
>
|
||||
<Image src={nomicSVG} alt="Logo" width="96" height="96" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const MicrosoftIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
}: IconProps) => {
|
||||
return (
|
||||
<div
|
||||
style={{ width: `${size + 4}px`, height: `${size + 4}px` }}
|
||||
className={`w-[${size + 4}px] h-[${size + 4}px] -m-0.5 ` + className}
|
||||
>
|
||||
<Image src={microsoftIcon} alt="Logo" width="96" height="96" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AnthropicIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
|
1
web/src/components/icons/mixedbread.svg
Normal file
1
web/src/components/icons/mixedbread.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 39 KiB |
@ -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";
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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,
|
||||
|
@ -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";
|
||||
|
@ -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(
|
||||
<div
|
||||
className={`fixed z-[1000] ${citation ? "max-w-[350px]" : "w-40"} ${
|
||||
large ? "w-96" : line && "max-w-64 w-auto"
|
||||
large ? (medium ? "w-88" : "w-96") : line && "max-w-64 w-auto"
|
||||
}
|
||||
transform -translate-x-1/2 text-sm
|
||||
${
|
||||
|
@ -2,7 +2,7 @@ import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { CCPairBasicInfo, DocumentSet, User } from "../types";
|
||||
import { getCurrentUserSS } from "../userSS";
|
||||
import { fetchSS } from "../utilsSS";
|
||||
import { FullLLMProvider } from "@/app/admin/models/llm/interfaces";
|
||||
import { FullLLMProvider } from "@/app/admin/configuration/llm/interfaces";
|
||||
import { ToolSnapshot } from "../tools/interfaces";
|
||||
import { fetchToolsSS } from "../tools/fetchTools";
|
||||
import { IconManifestType } from "react-icons/lib";
|
||||
|
@ -14,10 +14,10 @@ import {
|
||||
import { ChatSession } from "@/app/chat/interfaces";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { InputPrompt } from "@/app/admin/prompt-library/interfaces";
|
||||
import { FullEmbeddingModelResponse } from "@/app/admin/models/embedding/components/types";
|
||||
import { FullEmbeddingModelResponse } from "@/components/embedding/interfaces";
|
||||
import { Settings } from "@/app/admin/settings/interfaces";
|
||||
import { fetchLLMProvidersSS } from "@/lib/llm/fetchLLMs";
|
||||
import { LLMProviderDescriptor } from "@/app/admin/models/llm/interfaces";
|
||||
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
|
||||
import { Folder } from "@/app/chat/folders/interfaces";
|
||||
import { personaComparator } from "@/app/admin/assistants/lib";
|
||||
import { cookies } from "next/headers";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { LLMProviderDescriptor } from "@/app/admin/models/llm/interfaces";
|
||||
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
|
||||
import { fetchSS } from "../utilsSS";
|
||||
|
||||
export async function fetchLLMProvidersSS() {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { LLMProviderDescriptor } from "@/app/admin/models/llm/interfaces";
|
||||
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
|
||||
import { LlmOverride } from "@/lib/hooks";
|
||||
|
||||
export function getFinalLLM(
|
||||
|
Loading…
x
Reference in New Issue
Block a user