Improve Search (#2105)

This commit is contained in:
pablodanswer 2024-08-16 21:29:15 -07:00 committed by GitHub
parent efae24acd0
commit 22573aba2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
70 changed files with 2670 additions and 1030 deletions

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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:

View File

@ -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,

View File

@ -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(

View File

@ -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)

View File

@ -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 []

View File

@ -61,3 +61,4 @@ class Settings(BaseModel):
class UserSettings(Settings):
notifications: list[Notification]
needs_reindexing: bool

View File

@ -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

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

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
View 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

View File

@ -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";

View 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>
)}
</>
);
}

View 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;

View File

@ -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";

View 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>
);
}

View 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;

View 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",
},
];

View File

@ -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}
>

View File

@ -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 />

View File

@ -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}
>

View File

@ -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>

View File

@ -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}

View File

@ -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&apos;re about to set your embedding model to {model.model_name}.
You&apos;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>

View 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>
);
}

View 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;

View 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>
);
}

View 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>
);
}

View 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&apos;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>
);
}

View File

@ -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>
);
}

View File

@ -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&apos;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>
);
}

View File

@ -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>
);
}

View File

@ -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&apos;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&apos;s add a connector!
<br />
<br />
Connectors are the way that Danswer gets data from your
organization&apos;s various data sources. Once setup,
we&apos;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;

View File

@ -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 {

View File

@ -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";

View File

@ -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";

View File

@ -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,

View File

@ -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",
},
],
},

View File

@ -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>
);

View File

@ -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>
))}

View File

@ -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";

View 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;
};

View File

@ -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

View File

@ -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,

View 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>
);
}

View 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>
);
}

View File

@ -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(

View 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;
})}
</>
);
}

View File

@ -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,

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -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";

View File

@ -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";

View File

@ -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,

View File

@ -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";

View File

@ -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
${

View File

@ -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";

View File

@ -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";

View File

@ -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() {

View File

@ -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(