Compare commits

..

3 Commits

Author SHA1 Message Date
pablonyx
75103d2f8b quick nit 2025-02-28 15:05:39 -08:00
pablonyx
e8f7c34a72 k 2025-02-28 14:18:52 -08:00
pablonyx
1a378448f4 k 2025-02-28 14:10:33 -08:00
80 changed files with 531 additions and 1790 deletions

View File

@@ -1,55 +0,0 @@
"""add background_reindex_enabled field
Revision ID: b7c2b63c4a03
Revises: f11b408e39d3
Create Date: 2024-03-26 12:34:56.789012
"""
from alembic import op
import sqlalchemy as sa
from onyx.db.enums import EmbeddingPrecision
# revision identifiers, used by Alembic.
revision = "b7c2b63c4a03"
down_revision = "f11b408e39d3"
branch_labels = None
depends_on = None
def upgrade() -> None:
# Add background_reindex_enabled column with default value of True
op.add_column(
"search_settings",
sa.Column(
"background_reindex_enabled",
sa.Boolean(),
nullable=False,
server_default="true",
),
)
# Add embedding_precision column with default value of FLOAT
op.add_column(
"search_settings",
sa.Column(
"embedding_precision",
sa.Enum(EmbeddingPrecision, native_enum=False),
nullable=False,
server_default=EmbeddingPrecision.FLOAT.name,
),
)
# Add reduced_dimension column with default value of None
op.add_column(
"search_settings",
sa.Column("reduced_dimension", sa.Integer(), nullable=True),
)
def downgrade() -> None:
# Remove the background_reindex_enabled column
op.drop_column("search_settings", "background_reindex_enabled")
op.drop_column("search_settings", "embedding_precision")
op.drop_column("search_settings", "reduced_dimension")

View File

@@ -424,7 +424,7 @@ def _validate_curator_status__no_commit(
)
# if the user is a curator in any of their groups, set their role to CURATOR
# otherwise, set their role to BASIC only if they were previously a CURATOR
# otherwise, set their role to BASIC
if curator_relationships:
user.role = UserRole.CURATOR
elif user.role == UserRole.CURATOR:
@@ -631,16 +631,7 @@ def update_user_group(
removed_users = db_session.scalars(
select(User).where(User.id.in_(removed_user_ids)) # type: ignore
).unique()
# Filter out admin and global curator users before validating curator status
users_to_validate = [
user
for user in removed_users
if user.role not in [UserRole.ADMIN, UserRole.GLOBAL_CURATOR]
]
if users_to_validate:
_validate_curator_status__no_commit(db_session, users_to_validate)
_validate_curator_status__no_commit(db_session, list(removed_users))
# update "time_updated" to now
db_user_group.time_last_modified_by_user = func.now()

View File

@@ -22,7 +22,7 @@ from onyx.onyxbot.slack.blocks import get_restate_blocks
from onyx.onyxbot.slack.constants import GENERATE_ANSWER_BUTTON_ACTION_ID
from onyx.onyxbot.slack.handlers.utils import send_team_member_message
from onyx.onyxbot.slack.models import SlackMessageInfo
from onyx.onyxbot.slack.utils import respond_in_thread_or_channel
from onyx.onyxbot.slack.utils import respond_in_thread
from onyx.onyxbot.slack.utils import update_emote_react
from onyx.utils.logger import OnyxLoggingAdapter
from onyx.utils.logger import setup_logger
@@ -216,7 +216,7 @@ def _handle_standard_answers(
all_blocks = restate_question_blocks + answer_blocks
try:
respond_in_thread_or_channel(
respond_in_thread(
client=client,
channel=message_info.channel_to_respond,
receiver_ids=receiver_ids,
@@ -231,7 +231,6 @@ def _handle_standard_answers(
client=client,
channel=message_info.channel_to_respond,
thread_ts=slack_thread_id,
receiver_ids=receiver_ids,
)
return True

View File

@@ -78,7 +78,7 @@ class CloudEmbedding:
self._closed = False
async def _embed_openai(
self, texts: list[str], model: str | None, reduced_dimension: int | None
self, texts: list[str], model: str | None
) -> list[Embedding]:
if not model:
model = DEFAULT_OPENAI_MODEL
@@ -91,11 +91,7 @@ class CloudEmbedding:
final_embeddings: list[Embedding] = []
try:
for text_batch in batch_list(texts, _OPENAI_MAX_INPUT_LEN):
response = await client.embeddings.create(
input=text_batch,
model=model,
dimensions=reduced_dimension or openai.NOT_GIVEN,
)
response = await client.embeddings.create(input=text_batch, model=model)
final_embeddings.extend(
[embedding.embedding for embedding in response.data]
)
@@ -227,10 +223,9 @@ class CloudEmbedding:
text_type: EmbedTextType,
model_name: str | None = None,
deployment_name: str | None = None,
reduced_dimension: int | None = None,
) -> list[Embedding]:
if self.provider == EmbeddingProvider.OPENAI:
return await self._embed_openai(texts, model_name, reduced_dimension)
return await self._embed_openai(texts, model_name)
elif self.provider == EmbeddingProvider.AZURE:
return await self._embed_azure(texts, f"azure/{deployment_name}")
elif self.provider == EmbeddingProvider.LITELLM:
@@ -331,7 +326,6 @@ async def embed_text(
prefix: str | None,
api_url: str | None,
api_version: str | None,
reduced_dimension: int | None,
gpu_type: str = "UNKNOWN",
) -> list[Embedding]:
if not all(texts):
@@ -375,7 +369,6 @@ async def embed_text(
model_name=model_name,
deployment_name=deployment_name,
text_type=text_type,
reduced_dimension=reduced_dimension,
)
if any(embedding is None for embedding in embeddings):
@@ -515,7 +508,6 @@ async def process_embed_request(
text_type=embed_request.text_type,
api_url=embed_request.api_url,
api_version=embed_request.api_version,
reduced_dimension=embed_request.reduced_dimension,
prefix=prefix,
gpu_type=gpu_type,
)

View File

@@ -23,9 +23,9 @@ from sqlalchemy.orm import Session
from onyx.background.celery.apps.app_base import task_logger
from onyx.background.celery.celery_utils import httpx_init_vespa_pool
from onyx.background.celery.tasks.indexing.utils import _should_index
from onyx.background.celery.tasks.indexing.utils import get_unfenced_index_attempt_ids
from onyx.background.celery.tasks.indexing.utils import IndexingCallback
from onyx.background.celery.tasks.indexing.utils import should_index
from onyx.background.celery.tasks.indexing.utils import try_creating_indexing_task
from onyx.background.celery.tasks.indexing.utils import validate_indexing_fences
from onyx.background.indexing.checkpointing_utils import cleanup_checkpoint
@@ -61,7 +61,7 @@ from onyx.db.index_attempt import mark_attempt_canceled
from onyx.db.index_attempt import mark_attempt_failed
from onyx.db.search_settings import get_active_search_settings_list
from onyx.db.search_settings import get_current_search_settings
from onyx.db.swap_index import check_and_perform_index_swap
from onyx.db.swap_index import check_index_swap
from onyx.natural_language_processing.search_nlp_models import EmbeddingModel
from onyx.natural_language_processing.search_nlp_models import warm_up_bi_encoder
from onyx.redis.redis_connector import RedisConnector
@@ -406,7 +406,7 @@ def check_for_indexing(self: Task, *, tenant_id: str) -> int | None:
# check for search settings swap
with get_session_with_current_tenant() as db_session:
old_search_settings = check_and_perform_index_swap(db_session=db_session)
old_search_settings = check_index_swap(db_session=db_session)
current_search_settings = get_current_search_settings(db_session)
# So that the first time users aren't surprised by really slow speed of first
# batch of documents indexed
@@ -439,15 +439,6 @@ def check_for_indexing(self: Task, *, tenant_id: str) -> int | None:
with get_session_with_current_tenant() as db_session:
search_settings_list = get_active_search_settings_list(db_session)
for search_settings_instance in search_settings_list:
# skip non-live search settings that don't have background reindex enabled
# those should just auto-change to live shortly after creation without
# requiring any indexing till that point
if (
not search_settings_instance.status.is_current()
and not search_settings_instance.background_reindex_enabled
):
continue
redis_connector_index = redis_connector.new_index(
search_settings_instance.id
)
@@ -465,18 +456,23 @@ def check_for_indexing(self: Task, *, tenant_id: str) -> int | None:
cc_pair.id, search_settings_instance.id, db_session
)
if not should_index(
search_settings_primary = False
if search_settings_instance.id == search_settings_list[0].id:
search_settings_primary = True
if not _should_index(
cc_pair=cc_pair,
last_index=last_attempt,
search_settings_instance=search_settings_instance,
search_settings_primary=search_settings_primary,
secondary_index_building=len(search_settings_list) > 1,
db_session=db_session,
):
continue
reindex = False
if search_settings_instance.status.is_current():
# the indexing trigger is only checked and cleared with the current search settings
if search_settings_instance.id == search_settings_list[0].id:
# the indexing trigger is only checked and cleared with the primary search settings
if cc_pair.indexing_trigger is not None:
if cc_pair.indexing_trigger == IndexingMode.REINDEX:
reindex = True

View File

@@ -346,10 +346,11 @@ def validate_indexing_fences(
return
def should_index(
def _should_index(
cc_pair: ConnectorCredentialPair,
last_index: IndexAttempt | None,
search_settings_instance: SearchSettings,
search_settings_primary: bool,
secondary_index_building: bool,
db_session: Session,
) -> bool:
@@ -414,9 +415,9 @@ def should_index(
):
return False
if search_settings_instance.status.is_current():
if search_settings_primary:
if cc_pair.indexing_trigger is not None:
# if a manual indexing trigger is on the cc pair, honor it for live search settings
# if a manual indexing trigger is on the cc pair, honor it for primary search settings
return True
# if no attempt has ever occurred, we should index regardless of refresh_freq

View File

@@ -22,7 +22,6 @@ from onyx.configs.constants import DocumentSource
from onyx.configs.constants import MilestoneRecordType
from onyx.connectors.connector_runner import ConnectorRunner
from onyx.connectors.exceptions import ConnectorValidationError
from onyx.connectors.exceptions import UnexpectedValidationError
from onyx.connectors.factory import instantiate_connector
from onyx.connectors.models import ConnectorCheckpoint
from onyx.connectors.models import ConnectorFailure
@@ -93,13 +92,8 @@ def _get_connector_runner(
if not INTEGRATION_TESTS_MODE:
runnable_connector.validate_connector_settings()
except UnexpectedValidationError as e:
logger.exception(
"Unable to instantiate connector due to an unexpected temporary issue."
)
raise e
except Exception as e:
logger.exception("Unable to instantiate connector. Pausing until fixed.")
logger.exception("Unable to instantiate connector.")
# since we failed to even instantiate the connector, we pause the CCPair since
# it will never succeed

View File

@@ -18,7 +18,7 @@ from onyx.configs.constants import DocumentSource
from onyx.connectors.exceptions import ConnectorValidationError
from onyx.connectors.exceptions import CredentialExpiredError
from onyx.connectors.exceptions import InsufficientPermissionsError
from onyx.connectors.exceptions import UnexpectedValidationError
from onyx.connectors.exceptions import UnexpectedError
from onyx.connectors.interfaces import GenerateDocumentsOutput
from onyx.connectors.interfaces import LoadConnector
from onyx.connectors.interfaces import PollConnector
@@ -310,7 +310,7 @@ class BlobStorageConnector(LoadConnector, PollConnector):
# Catch-all for anything not captured by the above
# Since we are unsure of the error and it may not disable the connector,
# raise an unexpected error (does not disable connector)
raise UnexpectedValidationError(
raise UnexpectedError(
f"Unexpected error during blob storage settings validation: {e}"
)

View File

@@ -22,7 +22,7 @@ from onyx.connectors.confluence.utils import validate_attachment_filetype
from onyx.connectors.exceptions import ConnectorValidationError
from onyx.connectors.exceptions import CredentialExpiredError
from onyx.connectors.exceptions import InsufficientPermissionsError
from onyx.connectors.exceptions import UnexpectedValidationError
from onyx.connectors.exceptions import UnexpectedError
from onyx.connectors.interfaces import CredentialsConnector
from onyx.connectors.interfaces import CredentialsProviderInterface
from onyx.connectors.interfaces import GenerateDocumentsOutput
@@ -451,11 +451,11 @@ class ConfluenceConnector(
raise InsufficientPermissionsError(
"Insufficient permissions to access Confluence resources (HTTP 403)."
)
raise UnexpectedValidationError(
raise UnexpectedError(
f"Unexpected Confluence error (status={status_code}): {e}"
)
except Exception as e:
raise UnexpectedValidationError(
raise UnexpectedError(
f"Unexpected error while validating Confluence settings: {e}"
)

View File

@@ -14,15 +14,12 @@ class ConnectorValidationError(ValidationError):
super().__init__(self.message)
class UnexpectedValidationError(ValidationError):
class UnexpectedError(ValidationError):
"""Raised when an unexpected error occurs during connector validation.
Unexpected errors don't necessarily mean the credential is invalid,
but rather that there was an error during the validation process
or we encountered a currently unhandled error case.
Currently, unexpected validation errors are defined as transient and should not be
used to disable the connector.
"""
def __init__(self, message: str = "Unexpected error during connector validation"):

View File

@@ -20,7 +20,7 @@ from onyx.configs.constants import DocumentSource
from onyx.connectors.exceptions import ConnectorValidationError
from onyx.connectors.exceptions import CredentialExpiredError
from onyx.connectors.exceptions import InsufficientPermissionsError
from onyx.connectors.exceptions import UnexpectedValidationError
from onyx.connectors.exceptions import UnexpectedError
from onyx.connectors.interfaces import GenerateDocumentsOutput
from onyx.connectors.interfaces import LoadConnector
from onyx.connectors.interfaces import PollConnector
@@ -284,7 +284,7 @@ class GithubConnector(LoadConnector, PollConnector):
user.get_repos().totalCount # Just check if we can access repos
except RateLimitExceededException:
raise UnexpectedValidationError(
raise UnexpectedError(
"Validation failed due to GitHub rate-limits being exceeded. Please try again later."
)

View File

@@ -19,7 +19,7 @@ from onyx.connectors.cross_connector_utils.rate_limit_wrapper import (
from onyx.connectors.exceptions import ConnectorValidationError
from onyx.connectors.exceptions import CredentialExpiredError
from onyx.connectors.exceptions import InsufficientPermissionsError
from onyx.connectors.exceptions import UnexpectedValidationError
from onyx.connectors.exceptions import UnexpectedError
from onyx.connectors.interfaces import GenerateDocumentsOutput
from onyx.connectors.interfaces import LoadConnector
from onyx.connectors.interfaces import PollConnector
@@ -671,12 +671,12 @@ class NotionConnector(LoadConnector, PollConnector):
"Please try again later."
)
else:
raise UnexpectedValidationError(
raise UnexpectedError(
f"Unexpected Notion HTTP error (status={status_code}): {http_err}"
) from http_err
except Exception as exc:
raise UnexpectedValidationError(
raise UnexpectedError(
f"Unexpected error during Notion settings validation: {exc}"
)

View File

@@ -21,7 +21,7 @@ from onyx.configs.constants import DocumentSource
from onyx.connectors.exceptions import ConnectorValidationError
from onyx.connectors.exceptions import CredentialExpiredError
from onyx.connectors.exceptions import InsufficientPermissionsError
from onyx.connectors.exceptions import UnexpectedValidationError
from onyx.connectors.exceptions import UnexpectedError
from onyx.connectors.interfaces import CheckpointConnector
from onyx.connectors.interfaces import CheckpointOutput
from onyx.connectors.interfaces import GenerateSlimDocumentOutput
@@ -702,9 +702,7 @@ class SlackConnector(SlimConnector, CheckpointConnector):
raise CredentialExpiredError(
f"Invalid or expired Slack bot token ({error_msg})."
)
raise UnexpectedValidationError(
f"Slack API returned a failure: {error_msg}"
)
raise UnexpectedError(f"Slack API returned a failure: {error_msg}")
# 3) If channels are specified, verify each is accessible
if self.channels:
@@ -742,13 +740,13 @@ class SlackConnector(SlimConnector, CheckpointConnector):
raise CredentialExpiredError(
f"Invalid or expired Slack bot token ({slack_error})."
)
raise UnexpectedValidationError(
raise UnexpectedError(
f"Unexpected Slack error '{slack_error}' during settings validation."
)
except ConnectorValidationError as e:
raise e
except Exception as e:
raise UnexpectedValidationError(
raise UnexpectedError(
f"Unexpected error during Slack settings validation: {e}"
)

View File

@@ -72,7 +72,6 @@ def make_slack_api_rate_limited(
@wraps(call)
def rate_limited_call(**kwargs: Any) -> SlackResponse:
last_exception = None
for _ in range(max_retries):
try:
# Make the API call

View File

@@ -16,7 +16,7 @@ from onyx.connectors.cross_connector_utils.miscellaneous_utils import time_str_t
from onyx.connectors.exceptions import ConnectorValidationError
from onyx.connectors.exceptions import CredentialExpiredError
from onyx.connectors.exceptions import InsufficientPermissionsError
from onyx.connectors.exceptions import UnexpectedValidationError
from onyx.connectors.exceptions import UnexpectedError
from onyx.connectors.interfaces import GenerateDocumentsOutput
from onyx.connectors.interfaces import LoadConnector
from onyx.connectors.interfaces import PollConnector
@@ -302,7 +302,7 @@ class TeamsConnector(LoadConnector, PollConnector):
raise InsufficientPermissionsError(
"Your app lacks sufficient permissions to read Teams (403 Forbidden)."
)
raise UnexpectedValidationError(f"Unexpected error retrieving teams: {e}")
raise UnexpectedError(f"Unexpected error retrieving teams: {e}")
except Exception as e:
error_str = str(e).lower()

View File

@@ -28,7 +28,7 @@ from onyx.configs.constants import DocumentSource
from onyx.connectors.exceptions import ConnectorValidationError
from onyx.connectors.exceptions import CredentialExpiredError
from onyx.connectors.exceptions import InsufficientPermissionsError
from onyx.connectors.exceptions import UnexpectedValidationError
from onyx.connectors.exceptions import UnexpectedError
from onyx.connectors.interfaces import GenerateDocumentsOutput
from onyx.connectors.interfaces import LoadConnector
from onyx.connectors.models import Document
@@ -42,10 +42,6 @@ from shared_configs.configs import MULTI_TENANT
logger = setup_logger()
WEB_CONNECTOR_MAX_SCROLL_ATTEMPTS = 20
# Threshold for determining when to replace vs append iframe content
IFRAME_TEXT_LENGTH_THRESHOLD = 700
# Message indicating JavaScript is disabled, which often appears when scraping fails
JAVASCRIPT_DISABLED_MESSAGE = "You have JavaScript disabled in your browser"
class WEB_CONNECTOR_VALID_SETTINGS(str, Enum):
@@ -142,8 +138,7 @@ def get_internal_links(
# Account for malformed backslashes in URLs
href = href.replace("\\", "/")
# "#!" indicates the page is using a hashbang URL, which is a client-side routing technique
if should_ignore_pound and "#" in href and "#!" not in href:
if should_ignore_pound and "#" in href:
href = href.split("#")[0]
if not is_valid_url(href):
@@ -293,7 +288,6 @@ class WebConnector(LoadConnector):
and converts them into documents"""
visited_links: set[str] = set()
to_visit: list[str] = self.to_visit_list
content_hashes = set()
if not to_visit:
raise ValueError("No URLs to visit")
@@ -320,8 +314,7 @@ class WebConnector(LoadConnector):
logger.warning(last_error)
continue
index = len(visited_links)
logger.info(f"{index}: Visiting {initial_url}")
logger.info(f"{len(visited_links)}: Visiting {initial_url}")
try:
check_internet_connection(initial_url)
@@ -354,13 +347,7 @@ class WebConnector(LoadConnector):
continue
page = context.new_page()
# Can't use wait_until="networkidle" because it interferes with the scrolling behavior
page_response = page.goto(
initial_url,
timeout=30000, # 30 seconds
)
page_response = page.goto(initial_url)
last_modified = (
page_response.header_value("Last-Modified")
if page_response
@@ -372,10 +359,12 @@ class WebConnector(LoadConnector):
initial_url = final_url
if initial_url in visited_links:
logger.info(
f"{index}: {initial_url} redirected to {final_url} - already indexed"
f"{len(visited_links)}: {initial_url} redirected to {final_url} - already indexed"
)
continue
logger.info(f"{index}: {initial_url} redirected to {final_url}")
logger.info(
f"{len(visited_links)}: {initial_url} redirected to {final_url}"
)
visited_links.add(initial_url)
if self.scroll_before_scraping:
@@ -406,38 +395,6 @@ class WebConnector(LoadConnector):
parsed_html = web_html_cleanup(soup, self.mintlify_cleanup)
"""For websites containing iframes that need to be scraped,
the code below can extract text from within these iframes.
"""
logger.debug(
f"{index}: Length of cleaned text {len(parsed_html.cleaned_text)}"
)
if JAVASCRIPT_DISABLED_MESSAGE in parsed_html.cleaned_text:
iframe_count = page.frame_locator("iframe").locator("html").count()
if iframe_count > 0:
iframe_texts = (
page.frame_locator("iframe")
.locator("html")
.all_inner_texts()
)
document_text = "\n".join(iframe_texts)
""" 700 is the threshold value for the length of the text extracted
from the iframe based on the issue faced """
if len(parsed_html.cleaned_text) < IFRAME_TEXT_LENGTH_THRESHOLD:
parsed_html.cleaned_text = document_text
else:
parsed_html.cleaned_text += "\n" + document_text
# Sometimes pages with #! will serve duplicate content
# There are also just other ways this can happen
hashed_text = hash((parsed_html.title, parsed_html.cleaned_text))
if hashed_text in content_hashes:
logger.info(
f"{index}: Skipping duplicate title + content for {initial_url}"
)
continue
content_hashes.add(hashed_text)
doc_batch.append(
Document(
id=initial_url,
@@ -528,9 +485,7 @@ class WebConnector(LoadConnector):
)
else:
# Could be a 5xx or another error, treat as unexpected
raise UnexpectedValidationError(
f"Unexpected error validating '{test_url}': {e}"
)
raise UnexpectedError(f"Unexpected error validating '{test_url}': {e}")
if __name__ == "__main__":

View File

@@ -76,10 +76,6 @@ class SavedSearchSettings(InferenceSettings, IndexingSetting):
provider_type=search_settings.provider_type,
index_name=search_settings.index_name,
multipass_indexing=search_settings.multipass_indexing,
embedding_precision=search_settings.embedding_precision,
reduced_dimension=search_settings.reduced_dimension,
# Whether switching to this model requires re-indexing
background_reindex_enabled=search_settings.background_reindex_enabled,
# Reranking Details
rerank_model_name=search_settings.rerank_model_name,
rerank_provider_type=search_settings.rerank_provider_type,

View File

@@ -63,9 +63,6 @@ class IndexModelStatus(str, PyEnum):
PRESENT = "PRESENT"
FUTURE = "FUTURE"
def is_current(self) -> bool:
return self == IndexModelStatus.PRESENT
class ChatSessionSharedStatus(str, PyEnum):
PUBLIC = "public"
@@ -86,11 +83,3 @@ class AccessType(str, PyEnum):
PUBLIC = "public"
PRIVATE = "private"
SYNC = "sync"
class EmbeddingPrecision(str, PyEnum):
# matches vespa tensor type
# only support float / bfloat16 for now, since there's not a
# good reason to specify anything else
BFLOAT16 = "bfloat16"
FLOAT = "float"

View File

@@ -46,13 +46,7 @@ from onyx.configs.constants import DEFAULT_BOOST, MilestoneRecordType
from onyx.configs.constants import DocumentSource
from onyx.configs.constants import FileOrigin
from onyx.configs.constants import MessageType
from onyx.db.enums import (
AccessType,
EmbeddingPrecision,
IndexingMode,
SyncType,
SyncStatus,
)
from onyx.db.enums import AccessType, IndexingMode, SyncType, SyncStatus
from onyx.configs.constants import NotificationType
from onyx.configs.constants import SearchFeedbackType
from onyx.configs.constants import TokenRateLimitScope
@@ -722,23 +716,6 @@ class SearchSettings(Base):
ForeignKey("embedding_provider.provider_type"), nullable=True
)
# Whether switching to this model should re-index all connectors in the background
# if no re-index is needed, will be ignored. Only used during the switch-over process.
background_reindex_enabled: Mapped[bool] = mapped_column(Boolean, default=True)
# allows for quantization -> less memory usage for a small performance hit
embedding_precision: Mapped[EmbeddingPrecision] = mapped_column(
Enum(EmbeddingPrecision, native_enum=False)
)
# can be used to reduce dimensionality of vectors and save memory with
# a small performance hit. More details in the `Reducing embedding dimensions`
# section here:
# https://platform.openai.com/docs/guides/embeddings#embedding-models
# If not specified, will just use the model_dim without any reduction.
# NOTE: this is only currently available for OpenAI models
reduced_dimension: Mapped[int | None] = mapped_column(Integer, nullable=True)
# Mini and Large Chunks (large chunk also checks for model max context)
multipass_indexing: Mapped[bool] = mapped_column(Boolean, default=True)
@@ -820,12 +797,6 @@ class SearchSettings(Base):
self.multipass_indexing, self.model_name, self.provider_type
)
@property
def final_embedding_dim(self) -> int:
if self.reduced_dimension:
return self.reduced_dimension
return self.model_dim
@staticmethod
def can_use_large_chunks(
multipass: bool, model_name: str, provider_type: EmbeddingProvider | None
@@ -1790,7 +1761,6 @@ class ChannelConfig(TypedDict):
channel_name: str | None # None for default channel config
respond_tag_only: NotRequired[bool] # defaults to False
respond_to_bots: NotRequired[bool] # defaults to False
is_ephemeral: NotRequired[bool] # defaults to False
respond_member_group_list: NotRequired[list[str]]
answer_filters: NotRequired[list[AllowedAnswerFilters]]
# If None then no follow up

View File

@@ -209,21 +209,13 @@ def create_update_persona(
if not all_prompt_ids:
raise ValueError("No prompt IDs provided")
is_default_persona: bool | None = create_persona_request.is_default_persona
# Default persona validation
if create_persona_request.is_default_persona:
if not create_persona_request.is_public:
raise ValueError("Cannot make a default persona non public")
if user:
# Curators can edit default personas, but not make them
if (
user.role == UserRole.CURATOR
or user.role == UserRole.GLOBAL_CURATOR
):
is_default_persona = None
elif user.role != UserRole.ADMIN:
raise ValueError("Only admins can make a default persona")
if user and user.role != UserRole.ADMIN:
raise ValueError("Only admins can make a default persona")
persona = upsert_persona(
persona_id=persona_id,
@@ -249,7 +241,7 @@ def create_update_persona(
num_chunks=create_persona_request.num_chunks,
llm_relevance_filter=create_persona_request.llm_relevance_filter,
llm_filter_extraction=create_persona_request.llm_filter_extraction,
is_default_persona=is_default_persona,
is_default_persona=create_persona_request.is_default_persona,
)
versioned_make_persona_private = fetch_versioned_implementation(
@@ -436,7 +428,7 @@ def upsert_persona(
remove_image: bool | None = None,
search_start_date: datetime | None = None,
builtin_persona: bool = False,
is_default_persona: bool | None = None,
is_default_persona: bool = False,
label_ids: list[int] | None = None,
chunks_above: int = CONTEXT_CHUNKS_ABOVE,
chunks_below: int = CONTEXT_CHUNKS_BELOW,
@@ -531,11 +523,7 @@ def upsert_persona(
existing_persona.is_visible = is_visible
existing_persona.search_start_date = search_start_date
existing_persona.labels = labels or []
existing_persona.is_default_persona = (
is_default_persona
if is_default_persona is not None
else existing_persona.is_default_persona
)
existing_persona.is_default_persona = is_default_persona
# Do not delete any associations manually added unless
# a new updated list is provided
if document_sets is not None:
@@ -587,9 +575,7 @@ def upsert_persona(
display_priority=display_priority,
is_visible=is_visible,
search_start_date=search_start_date,
is_default_persona=is_default_persona
if is_default_persona is not None
else False,
is_default_persona=is_default_persona,
labels=labels or [],
)
db_session.add(new_persona)

View File

@@ -14,7 +14,6 @@ from onyx.configs.model_configs import OLD_DEFAULT_MODEL_DOC_EMBEDDING_DIM
from onyx.configs.model_configs import OLD_DEFAULT_MODEL_NORMALIZE_EMBEDDINGS
from onyx.context.search.models import SavedSearchSettings
from onyx.db.engine import get_session_with_current_tenant
from onyx.db.enums import EmbeddingPrecision
from onyx.db.llm import fetch_embedding_provider
from onyx.db.models import CloudEmbeddingProvider
from onyx.db.models import IndexAttempt
@@ -60,15 +59,12 @@ def create_search_settings(
index_name=search_settings.index_name,
provider_type=search_settings.provider_type,
multipass_indexing=search_settings.multipass_indexing,
embedding_precision=search_settings.embedding_precision,
reduced_dimension=search_settings.reduced_dimension,
multilingual_expansion=search_settings.multilingual_expansion,
disable_rerank_for_streaming=search_settings.disable_rerank_for_streaming,
rerank_model_name=search_settings.rerank_model_name,
rerank_provider_type=search_settings.rerank_provider_type,
rerank_api_key=search_settings.rerank_api_key,
num_rerank=search_settings.num_rerank,
background_reindex_enabled=search_settings.background_reindex_enabled,
)
db_session.add(embedding_model)
@@ -309,7 +305,6 @@ def get_old_default_embedding_model() -> IndexingSetting:
model_dim=(
DOC_EMBEDDING_DIM if is_overridden else OLD_DEFAULT_MODEL_DOC_EMBEDDING_DIM
),
embedding_precision=(EmbeddingPrecision.FLOAT),
normalize=(
NORMALIZE_EMBEDDINGS
if is_overridden
@@ -327,7 +322,6 @@ def get_new_default_embedding_model() -> IndexingSetting:
return IndexingSetting(
model_name=DOCUMENT_ENCODER_MODEL,
model_dim=DOC_EMBEDDING_DIM,
embedding_precision=(EmbeddingPrecision.FLOAT),
normalize=NORMALIZE_EMBEDDINGS,
query_prefix=ASYM_QUERY_PREFIX,
passage_prefix=ASYM_PASSAGE_PREFIX,

View File

@@ -8,12 +8,10 @@ from onyx.db.index_attempt import cancel_indexing_attempts_past_model
from onyx.db.index_attempt import (
count_unique_cc_pairs_with_successful_index_attempts,
)
from onyx.db.models import ConnectorCredentialPair
from onyx.db.models import SearchSettings
from onyx.db.search_settings import get_current_search_settings
from onyx.db.search_settings import get_secondary_search_settings
from onyx.db.search_settings import update_search_settings_status
from onyx.document_index.factory import get_default_document_index
from onyx.key_value_store.factory import get_kv_store
from onyx.utils.logger import setup_logger
@@ -21,49 +19,7 @@ from onyx.utils.logger import setup_logger
logger = setup_logger()
def _perform_index_swap(
db_session: Session,
current_search_settings: SearchSettings,
secondary_search_settings: SearchSettings,
all_cc_pairs: list[ConnectorCredentialPair],
) -> None:
"""Swap the indices and expire the old one."""
current_search_settings = get_current_search_settings(db_session)
update_search_settings_status(
search_settings=current_search_settings,
new_status=IndexModelStatus.PAST,
db_session=db_session,
)
update_search_settings_status(
search_settings=secondary_search_settings,
new_status=IndexModelStatus.PRESENT,
db_session=db_session,
)
if len(all_cc_pairs) > 0:
kv_store = get_kv_store()
kv_store.store(KV_REINDEX_KEY, False)
# Expire jobs for the now past index/embedding model
cancel_indexing_attempts_past_model(db_session)
# Recount aggregates
for cc_pair in all_cc_pairs:
resync_cc_pair(cc_pair, db_session=db_session)
# remove the old index from the vector db
document_index = get_default_document_index(secondary_search_settings, None)
document_index.ensure_indices_exist(
primary_embedding_dim=secondary_search_settings.final_embedding_dim,
primary_embedding_precision=secondary_search_settings.embedding_precision,
# just finished swap, no more secondary index
secondary_index_embedding_dim=None,
secondary_index_embedding_precision=None,
)
def check_and_perform_index_swap(db_session: Session) -> SearchSettings | None:
def check_index_swap(db_session: Session) -> SearchSettings | None:
"""Get count of cc-pairs and count of successful index_attempts for the
new model grouped by connector + credential, if it's the same, then assume
new index is done building. If so, swap the indices and expire the old one.
@@ -71,45 +27,52 @@ def check_and_perform_index_swap(db_session: Session) -> SearchSettings | None:
Returns None if search settings did not change, or the old search settings if they
did change.
"""
old_search_settings = None
# Default CC-pair created for Ingestion API unused here
all_cc_pairs = get_connector_credential_pairs(db_session)
cc_pair_count = max(len(all_cc_pairs) - 1, 0)
secondary_search_settings = get_secondary_search_settings(db_session)
search_settings = get_secondary_search_settings(db_session)
if not secondary_search_settings:
if not search_settings:
return None
# If the secondary search settings are not configured to reindex in the background,
# we can just swap over instantly
if not secondary_search_settings.background_reindex_enabled:
current_search_settings = get_current_search_settings(db_session)
_perform_index_swap(
db_session=db_session,
current_search_settings=current_search_settings,
secondary_search_settings=secondary_search_settings,
all_cc_pairs=all_cc_pairs,
)
return current_search_settings
unique_cc_indexings = count_unique_cc_pairs_with_successful_index_attempts(
search_settings_id=secondary_search_settings.id, db_session=db_session
search_settings_id=search_settings.id, db_session=db_session
)
# Index Attempts are cleaned up as well when the cc-pair is deleted so the logic in this
# function is correct. The unique_cc_indexings are specifically for the existing cc-pairs
old_search_settings = None
if unique_cc_indexings > cc_pair_count:
logger.error("More unique indexings than cc pairs, should not occur")
if cc_pair_count == 0 or cc_pair_count == unique_cc_indexings:
# Swap indices
current_search_settings = get_current_search_settings(db_session)
_perform_index_swap(
update_search_settings_status(
search_settings=current_search_settings,
new_status=IndexModelStatus.PAST,
db_session=db_session,
current_search_settings=current_search_settings,
secondary_search_settings=secondary_search_settings,
all_cc_pairs=all_cc_pairs,
)
old_search_settings = current_search_settings
update_search_settings_status(
search_settings=search_settings,
new_status=IndexModelStatus.PRESENT,
db_session=db_session,
)
if cc_pair_count > 0:
kv_store = get_kv_store()
kv_store.store(KV_REINDEX_KEY, False)
# Expire jobs for the now past index/embedding model
cancel_indexing_attempts_past_model(db_session)
# Recount aggregates
for cc_pair in all_cc_pairs:
resync_cc_pair(cc_pair, db_session=db_session)
old_search_settings = current_search_settings
return old_search_settings

View File

@@ -6,7 +6,6 @@ from typing import Any
from onyx.access.models import DocumentAccess
from onyx.context.search.models import IndexFilters
from onyx.context.search.models import InferenceChunkUncleaned
from onyx.db.enums import EmbeddingPrecision
from onyx.indexing.models import DocMetadataAwareIndexChunk
from shared_configs.model_server_models import Embedding
@@ -146,21 +145,17 @@ class Verifiable(abc.ABC):
@abc.abstractmethod
def ensure_indices_exist(
self,
primary_embedding_dim: int,
primary_embedding_precision: EmbeddingPrecision,
index_embedding_dim: int,
secondary_index_embedding_dim: int | None,
secondary_index_embedding_precision: EmbeddingPrecision | None,
) -> None:
"""
Verify that the document index exists and is consistent with the expectations in the code.
Parameters:
- primary_embedding_dim: Vector dimensionality for the vector similarity part of the search
- primary_embedding_precision: Precision of the vector similarity part of the search
- index_embedding_dim: Vector dimensionality for the vector similarity part of the search
- secondary_index_embedding_dim: Vector dimensionality of the secondary index being built
behind the scenes. The secondary index should only be built when switching
embedding models therefore this dim should be different from the primary index.
- secondary_index_embedding_precision: Precision of the vector similarity part of the secondary index
"""
raise NotImplementedError
@@ -169,7 +164,6 @@ class Verifiable(abc.ABC):
def register_multitenant_indices(
indices: list[str],
embedding_dims: list[int],
embedding_precisions: list[EmbeddingPrecision],
) -> None:
"""
Register multitenant indices with the document index.

View File

@@ -37,7 +37,7 @@ schema DANSWER_CHUNK_NAME {
summary: dynamic
}
# Title embedding (x1)
field title_embedding type tensor<EMBEDDING_PRECISION>(x[VARIABLE_DIM]) {
field title_embedding type tensor<float>(x[VARIABLE_DIM]) {
indexing: attribute | index
attribute {
distance-metric: angular
@@ -45,7 +45,7 @@ schema DANSWER_CHUNK_NAME {
}
# Content embeddings (chunk + optional mini chunks embeddings)
# "t" and "x" are arbitrary names, not special keywords
field embeddings type tensor<EMBEDDING_PRECISION>(t{},x[VARIABLE_DIM]) {
field embeddings type tensor<float>(t{},x[VARIABLE_DIM]) {
indexing: attribute | index
attribute {
distance-metric: angular

View File

@@ -5,7 +5,4 @@
<allow
until="DATE_REPLACEMENT"
comment="We need to be able to update the schema for updates to the Onyx schema">indexing-change</allow>
<allow
until='DATE_REPLACEMENT'
comment="Prevents old alt indices from interfering with changes">field-type-change</allow>
</validation-overrides>

View File

@@ -310,11 +310,6 @@ def query_vespa(
f"Request Headers: {e.request.headers}\n"
f"Request Payload: {params}\n"
f"Exception: {str(e)}"
+ (
f"\nResponse: {e.response.text}"
if isinstance(e, httpx.HTTPStatusError)
else ""
)
)
raise httpx.HTTPError(error_base) from e

View File

@@ -26,7 +26,6 @@ from onyx.configs.chat_configs import VESPA_SEARCHER_THREADS
from onyx.configs.constants import KV_REINDEX_KEY
from onyx.context.search.models import IndexFilters
from onyx.context.search.models import InferenceChunkUncleaned
from onyx.db.enums import EmbeddingPrecision
from onyx.document_index.document_index_utils import get_document_chunk_ids
from onyx.document_index.interfaces import DocumentIndex
from onyx.document_index.interfaces import DocumentInsertionRecord
@@ -64,7 +63,6 @@ from onyx.document_index.vespa_constants import DATE_REPLACEMENT
from onyx.document_index.vespa_constants import DOCUMENT_ID_ENDPOINT
from onyx.document_index.vespa_constants import DOCUMENT_REPLACEMENT_PAT
from onyx.document_index.vespa_constants import DOCUMENT_SETS
from onyx.document_index.vespa_constants import EMBEDDING_PRECISION_REPLACEMENT_PAT
from onyx.document_index.vespa_constants import HIDDEN
from onyx.document_index.vespa_constants import NUM_THREADS
from onyx.document_index.vespa_constants import SEARCH_THREAD_NUMBER_PAT
@@ -114,21 +112,6 @@ def _create_document_xml_lines(doc_names: list[str | None] | list[str]) -> str:
return "\n".join(doc_lines)
def _replace_template_values_in_schema(
schema_template: str,
index_name: str,
embedding_dim: int,
embedding_precision: EmbeddingPrecision,
) -> str:
return (
schema_template.replace(
EMBEDDING_PRECISION_REPLACEMENT_PAT, embedding_precision.value
)
.replace(DANSWER_CHUNK_REPLACEMENT_PAT, index_name)
.replace(VESPA_DIM_REPLACEMENT_PAT, str(embedding_dim))
)
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(
@@ -180,10 +163,8 @@ class VespaIndex(DocumentIndex):
def ensure_indices_exist(
self,
primary_embedding_dim: int,
primary_embedding_precision: EmbeddingPrecision,
index_embedding_dim: int,
secondary_index_embedding_dim: int | None,
secondary_index_embedding_precision: EmbeddingPrecision | None,
) -> None:
if MULTI_TENANT:
logger.info(
@@ -240,29 +221,18 @@ class VespaIndex(DocumentIndex):
schema_template = schema_f.read()
schema_template = schema_template.replace(TENANT_ID_PAT, "")
schema = _replace_template_values_in_schema(
schema_template,
self.index_name,
primary_embedding_dim,
primary_embedding_precision,
)
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
schema = schema.replace(TENANT_ID_PAT, "")
zip_dict[f"schemas/{schema_names[0]}.sd"] = schema.encode("utf-8")
if self.secondary_index_name:
if secondary_index_embedding_dim is None:
raise ValueError("Secondary index embedding dimension is required")
if secondary_index_embedding_precision is None:
raise ValueError("Secondary index embedding precision is required")
upcoming_schema = _replace_template_values_in_schema(
schema_template,
self.secondary_index_name,
secondary_index_embedding_dim,
secondary_index_embedding_precision,
)
upcoming_schema = schema_template.replace(
DANSWER_CHUNK_REPLACEMENT_PAT, self.secondary_index_name
).replace(VESPA_DIM_REPLACEMENT_PAT, str(secondary_index_embedding_dim))
zip_dict[f"schemas/{schema_names[1]}.sd"] = upcoming_schema.encode("utf-8")
zip_file = in_memory_zip_from_file_bytes(zip_dict)
@@ -281,7 +251,6 @@ class VespaIndex(DocumentIndex):
def register_multitenant_indices(
indices: list[str],
embedding_dims: list[int],
embedding_precisions: list[EmbeddingPrecision],
) -> None:
if not MULTI_TENANT:
raise ValueError("Multi-tenant is not enabled")
@@ -340,14 +309,13 @@ class VespaIndex(DocumentIndex):
for i, index_name in enumerate(indices):
embedding_dim = embedding_dims[i]
embedding_precision = embedding_precisions[i]
logger.info(
f"Creating index: {index_name} with embedding dimension: {embedding_dim}"
)
schema = _replace_template_values_in_schema(
schema_template, index_name, embedding_dim, embedding_precision
)
schema = schema_template.replace(
DANSWER_CHUNK_REPLACEMENT_PAT, index_name
).replace(VESPA_DIM_REPLACEMENT_PAT, str(embedding_dim))
schema = schema.replace(
TENANT_ID_PAT, TENANT_ID_REPLACEMENT if MULTI_TENANT else ""
)

View File

@@ -6,7 +6,6 @@ from onyx.configs.app_configs import VESPA_TENANT_PORT
from onyx.configs.constants import SOURCE_TYPE
VESPA_DIM_REPLACEMENT_PAT = "VARIABLE_DIM"
EMBEDDING_PRECISION_REPLACEMENT_PAT = "EMBEDDING_PRECISION"
DANSWER_CHUNK_REPLACEMENT_PAT = "DANSWER_CHUNK_NAME"
DOCUMENT_REPLACEMENT_PAT = "DOCUMENT_REPLACEMENT"
SEARCH_THREAD_NUMBER_PAT = "SEARCH_THREAD_NUMBER"

View File

@@ -38,7 +38,6 @@ class IndexingEmbedder(ABC):
api_url: str | None,
api_version: str | None,
deployment_name: str | None,
reduced_dimension: int | None,
callback: IndexingHeartbeatInterface | None,
):
self.model_name = model_name
@@ -61,7 +60,6 @@ class IndexingEmbedder(ABC):
api_url=api_url,
api_version=api_version,
deployment_name=deployment_name,
reduced_dimension=reduced_dimension,
# The below are globally set, this flow always uses the indexing one
server_host=INDEXING_MODEL_SERVER_HOST,
server_port=INDEXING_MODEL_SERVER_PORT,
@@ -89,7 +87,6 @@ class DefaultIndexingEmbedder(IndexingEmbedder):
api_url: str | None = None,
api_version: str | None = None,
deployment_name: str | None = None,
reduced_dimension: int | None = None,
callback: IndexingHeartbeatInterface | None = None,
):
super().__init__(
@@ -102,7 +99,6 @@ class DefaultIndexingEmbedder(IndexingEmbedder):
api_url,
api_version,
deployment_name,
reduced_dimension,
callback,
)
@@ -223,7 +219,6 @@ class DefaultIndexingEmbedder(IndexingEmbedder):
api_url=search_settings.api_url,
api_version=search_settings.api_version,
deployment_name=search_settings.deployment_name,
reduced_dimension=search_settings.reduced_dimension,
callback=callback,
)

View File

@@ -5,7 +5,6 @@ from pydantic import Field
from onyx.access.models import DocumentAccess
from onyx.connectors.models import Document
from onyx.db.enums import EmbeddingPrecision
from onyx.utils.logger import setup_logger
from shared_configs.enums import EmbeddingProvider
from shared_configs.model_server_models import Embedding
@@ -144,20 +143,10 @@ class IndexingSetting(EmbeddingModelDetail):
model_dim: int
index_name: str | None
multipass_indexing: bool
embedding_precision: EmbeddingPrecision
reduced_dimension: int | None = None
background_reindex_enabled: bool = True
# This disables the "model_" protected namespace for pydantic
model_config = {"protected_namespaces": ()}
@property
def final_embedding_dim(self) -> int:
if self.reduced_dimension:
return self.reduced_dimension
return self.model_dim
@classmethod
def from_db_model(cls, search_settings: "SearchSettings") -> "IndexingSetting":
return cls(
@@ -169,9 +158,6 @@ class IndexingSetting(EmbeddingModelDetail):
provider_type=search_settings.provider_type,
index_name=search_settings.index_name,
multipass_indexing=search_settings.multipass_indexing,
embedding_precision=search_settings.embedding_precision,
reduced_dimension=search_settings.reduced_dimension,
background_reindex_enabled=search_settings.background_reindex_enabled,
)

View File

@@ -14,14 +14,12 @@ from onyx.db.models import KVStore
from onyx.key_value_store.interface import KeyValueStore
from onyx.key_value_store.interface import KvKeyNotFoundError
from onyx.redis.redis_pool import get_redis_client
from onyx.server.utils import BasicAuthenticationError
from onyx.utils.logger import setup_logger
from onyx.utils.special_types import JSON_ro
from shared_configs.configs import MULTI_TENANT
from shared_configs.configs import POSTGRES_DEFAULT_SCHEMA
from shared_configs.contextvars import get_current_tenant_id
logger = setup_logger()
@@ -45,7 +43,9 @@ class PgRedisKVStore(KeyValueStore):
with Session(engine, expire_on_commit=False) as session:
if MULTI_TENANT:
if self.tenant_id == POSTGRES_DEFAULT_SCHEMA:
raise BasicAuthenticationError(detail="User must authenticate")
raise HTTPException(
status_code=401, detail="User must authenticate"
)
if not is_valid_schema_name(self.tenant_id):
raise HTTPException(status_code=400, detail="Invalid tenant ID")
# Set the search_path to the tenant's schema

View File

@@ -89,7 +89,6 @@ class EmbeddingModel:
callback: IndexingHeartbeatInterface | None = None,
api_version: str | None = None,
deployment_name: str | None = None,
reduced_dimension: int | None = None,
) -> None:
self.api_key = api_key
self.provider_type = provider_type
@@ -101,7 +100,6 @@ class EmbeddingModel:
self.api_url = api_url
self.api_version = api_version
self.deployment_name = deployment_name
self.reduced_dimension = reduced_dimension
self.tokenizer = get_tokenizer(
model_name=model_name, provider_type=provider_type
)
@@ -190,7 +188,6 @@ class EmbeddingModel:
manual_query_prefix=self.query_prefix,
manual_passage_prefix=self.passage_prefix,
api_url=self.api_url,
reduced_dimension=self.reduced_dimension,
)
start_time = time.time()
@@ -303,7 +300,6 @@ class EmbeddingModel:
retrim_content=retrim_content,
api_version=search_settings.api_version,
deployment_name=search_settings.deployment_name,
reduced_dimension=search_settings.reduced_dimension,
)

View File

@@ -31,18 +31,12 @@ from onyx.onyxbot.slack.constants import FEEDBACK_DOC_BUTTON_BLOCK_ACTION_ID
from onyx.onyxbot.slack.constants import FOLLOWUP_BUTTON_ACTION_ID
from onyx.onyxbot.slack.constants import FOLLOWUP_BUTTON_RESOLVED_ACTION_ID
from onyx.onyxbot.slack.constants import IMMEDIATE_RESOLVED_BUTTON_ACTION_ID
from onyx.onyxbot.slack.constants import KEEP_TO_YOURSELF_ACTION_ID
from onyx.onyxbot.slack.constants import LIKE_BLOCK_ACTION_ID
from onyx.onyxbot.slack.constants import SHOW_EVERYONE_ACTION_ID
from onyx.onyxbot.slack.formatting import format_slack_message
from onyx.onyxbot.slack.icons import source_to_github_img_link
from onyx.onyxbot.slack.models import ActionValuesEphemeralMessage
from onyx.onyxbot.slack.models import ActionValuesEphemeralMessageChannelConfig
from onyx.onyxbot.slack.models import ActionValuesEphemeralMessageMessageInfo
from onyx.onyxbot.slack.models import SlackMessageInfo
from onyx.onyxbot.slack.utils import build_continue_in_web_ui_id
from onyx.onyxbot.slack.utils import build_feedback_id
from onyx.onyxbot.slack.utils import build_publish_ephemeral_message_id
from onyx.onyxbot.slack.utils import remove_slack_text_interactions
from onyx.onyxbot.slack.utils import translate_vespa_highlight_to_slack
from onyx.utils.text_processing import decode_escapes
@@ -111,77 +105,6 @@ def _build_qa_feedback_block(
)
def _build_ephemeral_publication_block(
channel_id: str,
chat_message_id: int,
message_info: SlackMessageInfo,
original_question_ts: str,
channel_conf: ChannelConfig,
feedback_reminder_id: str | None = None,
) -> Block:
# check whether the message is in a thread
if (
message_info is not None
and message_info.msg_to_respond is not None
and message_info.thread_to_respond is not None
and (message_info.msg_to_respond == message_info.thread_to_respond)
):
respond_ts = None
else:
respond_ts = original_question_ts
action_values_ephemeral_message_channel_config = (
ActionValuesEphemeralMessageChannelConfig(
channel_name=channel_conf.get("channel_name"),
respond_tag_only=channel_conf.get("respond_tag_only"),
respond_to_bots=channel_conf.get("respond_to_bots"),
is_ephemeral=channel_conf.get("is_ephemeral", False),
respond_member_group_list=channel_conf.get("respond_member_group_list"),
answer_filters=channel_conf.get("answer_filters"),
follow_up_tags=channel_conf.get("follow_up_tags"),
show_continue_in_web_ui=channel_conf.get("show_continue_in_web_ui", False),
)
)
action_values_ephemeral_message_message_info = (
ActionValuesEphemeralMessageMessageInfo(
bypass_filters=message_info.bypass_filters,
channel_to_respond=message_info.channel_to_respond,
msg_to_respond=message_info.msg_to_respond,
email=message_info.email,
sender_id=message_info.sender_id,
thread_messages=[],
is_bot_msg=message_info.is_bot_msg,
is_bot_dm=message_info.is_bot_dm,
thread_to_respond=respond_ts,
)
)
action_values_ephemeral_message = ActionValuesEphemeralMessage(
original_question_ts=original_question_ts,
feedback_reminder_id=feedback_reminder_id,
chat_message_id=chat_message_id,
message_info=action_values_ephemeral_message_message_info,
channel_conf=action_values_ephemeral_message_channel_config,
)
return ActionsBlock(
block_id=build_publish_ephemeral_message_id(original_question_ts),
elements=[
ButtonElement(
action_id=SHOW_EVERYONE_ACTION_ID,
text="📢 Share with Everyone",
value=action_values_ephemeral_message.model_dump_json(),
),
ButtonElement(
action_id=KEEP_TO_YOURSELF_ACTION_ID,
text="🤫 Keep to Yourself",
value=action_values_ephemeral_message.model_dump_json(),
),
],
)
def get_document_feedback_blocks() -> Block:
return SectionBlock(
text=(
@@ -563,21 +486,16 @@ def build_slack_response_blocks(
use_citations: bool,
feedback_reminder_id: str | None,
skip_ai_feedback: bool = False,
offer_ephemeral_publication: bool = False,
expecting_search_result: bool = False,
skip_restated_question: bool = False,
) -> list[Block]:
"""
This function is a top level function that builds all the blocks for the Slack response.
It also handles combining all the blocks together.
"""
# If called with the OnyxBot slash command, the question is lost so we have to reshow it
if not skip_restated_question:
restate_question_block = get_restate_blocks(
message_info.thread_messages[-1].message, message_info.is_bot_msg
)
else:
restate_question_block = []
restate_question_block = get_restate_blocks(
message_info.thread_messages[-1].message, message_info.is_bot_msg
)
if expecting_search_result:
answer_blocks = _build_qa_response_blocks(
@@ -602,36 +520,12 @@ def build_slack_response_blocks(
)
follow_up_block = []
if (
channel_conf
and channel_conf.get("follow_up_tags") is not None
and not channel_conf.get("is_ephemeral", False)
):
if channel_conf and channel_conf.get("follow_up_tags") is not None:
follow_up_block.append(
_build_follow_up_block(message_id=answer.chat_message_id)
)
publish_ephemeral_message_block = []
if (
offer_ephemeral_publication
and answer.chat_message_id is not None
and message_info.msg_to_respond is not None
and channel_conf is not None
):
publish_ephemeral_message_block.append(
_build_ephemeral_publication_block(
channel_id=message_info.channel_to_respond,
chat_message_id=answer.chat_message_id,
original_question_ts=message_info.msg_to_respond,
message_info=message_info,
channel_conf=channel_conf,
feedback_reminder_id=feedback_reminder_id,
)
)
ai_feedback_block: list[Block] = []
ai_feedback_block = []
if answer.chat_message_id is not None and not skip_ai_feedback:
ai_feedback_block.append(
_build_qa_feedback_block(
@@ -653,7 +547,6 @@ def build_slack_response_blocks(
all_blocks = (
restate_question_block
+ answer_blocks
+ publish_ephemeral_message_block
+ ai_feedback_block
+ citations_divider
+ citations_blocks

View File

@@ -2,8 +2,6 @@ from enum import Enum
LIKE_BLOCK_ACTION_ID = "feedback-like"
DISLIKE_BLOCK_ACTION_ID = "feedback-dislike"
SHOW_EVERYONE_ACTION_ID = "show-everyone"
KEEP_TO_YOURSELF_ACTION_ID = "keep-to-yourself"
CONTINUE_IN_WEB_UI_ACTION_ID = "continue-in-web-ui"
FEEDBACK_DOC_BUTTON_BLOCK_ACTION_ID = "feedback-doc-button"
IMMEDIATE_RESOLVED_BUTTON_ACTION_ID = "immediate-resolved-button"

View File

@@ -1,4 +1,3 @@
import json
from typing import Any
from typing import cast
@@ -6,32 +5,21 @@ from slack_sdk import WebClient
from slack_sdk.models.blocks import SectionBlock
from slack_sdk.models.views import View
from slack_sdk.socket_mode.request import SocketModeRequest
from slack_sdk.webhook import WebhookClient
from onyx.chat.models import ChatOnyxBotResponse
from onyx.chat.models import CitationInfo
from onyx.chat.models import QADocsResponse
from onyx.configs.constants import MessageType
from onyx.configs.constants import SearchFeedbackType
from onyx.configs.onyxbot_configs import DANSWER_FOLLOWUP_EMOJI
from onyx.connectors.slack.utils import expert_info_from_slack_id
from onyx.connectors.slack.utils import make_slack_api_rate_limited
from onyx.context.search.models import SavedSearchDoc
from onyx.db.chat import get_chat_message
from onyx.db.chat import translate_db_message_to_chat_message_detail
from onyx.db.engine import get_session_with_current_tenant
from onyx.db.feedback import create_chat_message_feedback
from onyx.db.feedback import create_doc_retrieval_feedback
from onyx.db.users import get_user_by_email
from onyx.onyxbot.slack.blocks import build_follow_up_resolved_blocks
from onyx.onyxbot.slack.blocks import build_slack_response_blocks
from onyx.onyxbot.slack.blocks import get_document_feedback_blocks
from onyx.onyxbot.slack.config import get_slack_channel_config_for_bot_and_channel
from onyx.onyxbot.slack.constants import DISLIKE_BLOCK_ACTION_ID
from onyx.onyxbot.slack.constants import FeedbackVisibility
from onyx.onyxbot.slack.constants import KEEP_TO_YOURSELF_ACTION_ID
from onyx.onyxbot.slack.constants import LIKE_BLOCK_ACTION_ID
from onyx.onyxbot.slack.constants import SHOW_EVERYONE_ACTION_ID
from onyx.onyxbot.slack.constants import VIEW_DOC_FEEDBACK_ID
from onyx.onyxbot.slack.handlers.handle_message import (
remove_scheduled_feedback_reminder,
@@ -47,48 +35,15 @@ from onyx.onyxbot.slack.utils import fetch_slack_user_ids_from_emails
from onyx.onyxbot.slack.utils import get_channel_name_from_id
from onyx.onyxbot.slack.utils import get_feedback_visibility
from onyx.onyxbot.slack.utils import read_slack_thread
from onyx.onyxbot.slack.utils import respond_in_thread_or_channel
from onyx.onyxbot.slack.utils import respond_in_thread
from onyx.onyxbot.slack.utils import TenantSocketModeClient
from onyx.onyxbot.slack.utils import update_emote_react
from onyx.server.query_and_chat.models import ChatMessageDetail
from onyx.utils.logger import setup_logger
logger = setup_logger()
def _convert_db_doc_id_to_document_ids(
citation_dict: dict[int, int], top_documents: list[SavedSearchDoc]
) -> list[CitationInfo]:
citation_list_with_document_id = []
for citation_num, db_doc_id in citation_dict.items():
if db_doc_id is not None:
matching_doc = next(
(d for d in top_documents if d.db_doc_id == db_doc_id), None
)
if matching_doc:
citation_list_with_document_id.append(
CitationInfo(
citation_num=citation_num, document_id=matching_doc.document_id
)
)
return citation_list_with_document_id
def _build_citation_list(chat_message_detail: ChatMessageDetail) -> list[CitationInfo]:
citation_dict = chat_message_detail.citations
if citation_dict is None:
return []
else:
top_documents = (
chat_message_detail.context_docs.top_documents
if chat_message_detail.context_docs
else []
)
citation_list = _convert_db_doc_id_to_document_ids(citation_dict, top_documents)
return citation_list
def handle_doc_feedback_button(
req: SocketModeRequest,
client: TenantSocketModeClient,
@@ -103,7 +58,7 @@ def handle_doc_feedback_button(
external_id = build_feedback_id(query_event_id, doc_id, doc_rank)
channel_id = req.payload["container"]["channel_id"]
thread_ts = req.payload["container"].get("thread_ts", None)
thread_ts = req.payload["container"]["thread_ts"]
data = View(
type="modal",
@@ -129,7 +84,7 @@ def handle_generate_answer_button(
channel_id = req.payload["channel"]["id"]
channel_name = req.payload["channel"]["name"]
message_ts = req.payload["message"]["ts"]
thread_ts = req.payload["container"].get("thread_ts", None)
thread_ts = req.payload["container"]["thread_ts"]
user_id = req.payload["user"]["id"]
expert_info = expert_info_from_slack_id(user_id, client.web_client, user_cache={})
email = expert_info.email if expert_info else None
@@ -151,7 +106,7 @@ def handle_generate_answer_button(
# tell the user that we're working on it
# Send an ephemeral message to the user that we're generating the answer
respond_in_thread_or_channel(
respond_in_thread(
client=client.web_client,
channel=channel_id,
receiver_ids=[user_id],
@@ -187,178 +142,6 @@ def handle_generate_answer_button(
)
def handle_publish_ephemeral_message_button(
req: SocketModeRequest,
client: TenantSocketModeClient,
action_id: str,
) -> None:
"""
This function handles the Share with Everyone/Keep for Yourself buttons
for ephemeral messages.
"""
channel_id = req.payload["channel"]["id"]
ephemeral_message_ts = req.payload["container"]["message_ts"]
slack_sender_id = req.payload["user"]["id"]
response_url = req.payload["response_url"]
webhook = WebhookClient(url=response_url)
# The additional data required that was added to buttons.
# Specifically, this contains the message_info, channel_conf information
# and some additional attributes.
value_dict = json.loads(req.payload["actions"][0]["value"])
original_question_ts = value_dict.get("original_question_ts")
if not original_question_ts:
raise ValueError("Missing original_question_ts in the payload")
if not ephemeral_message_ts:
raise ValueError("Missing ephemeral_message_ts in the payload")
feedback_reminder_id = value_dict.get("feedback_reminder_id")
slack_message_info = SlackMessageInfo(**value_dict["message_info"])
channel_conf = value_dict.get("channel_conf")
user_email = value_dict.get("message_info", {}).get("email")
chat_message_id = value_dict.get("chat_message_id")
# Obtain onyx_user and chat_message information
if not chat_message_id:
raise ValueError("Missing chat_message_id in the payload")
with get_session_with_current_tenant() as db_session:
onyx_user = get_user_by_email(user_email, db_session)
if not onyx_user:
raise ValueError("Cannot determine onyx_user_id from email in payload")
try:
chat_message = get_chat_message(chat_message_id, onyx_user.id, db_session)
except ValueError:
chat_message = get_chat_message(
chat_message_id, None, db_session
) # is this good idea?
except Exception as e:
logger.error(f"Failed to get chat message: {e}")
raise e
chat_message_detail = translate_db_message_to_chat_message_detail(chat_message)
# construct the proper citation format and then the answer in the suitable format
# we need to construct the blocks.
citation_list = _build_citation_list(chat_message_detail)
onyx_bot_answer = ChatOnyxBotResponse(
answer=chat_message_detail.message,
citations=citation_list,
chat_message_id=chat_message_id,
docs=QADocsResponse(
top_documents=chat_message_detail.context_docs.top_documents
if chat_message_detail.context_docs
else [],
predicted_flow=None,
predicted_search=None,
applied_source_filters=None,
applied_time_cutoff=None,
recency_bias_multiplier=1.0,
),
llm_selected_doc_indices=None,
error_msg=None,
)
# Note: we need to use the webhook and the respond_url to update/delete ephemeral messages
if action_id == SHOW_EVERYONE_ACTION_ID:
# Convert to non-ephemeral message in thread
try:
webhook.send(
response_type="ephemeral",
text="",
blocks=[],
replace_original=True,
delete_original=True,
)
except Exception as e:
logger.error(f"Failed to send webhook: {e}")
# remove handling of empheremal block and add AI feedback.
all_blocks = build_slack_response_blocks(
answer=onyx_bot_answer,
message_info=slack_message_info,
channel_conf=channel_conf,
use_citations=True,
feedback_reminder_id=feedback_reminder_id,
skip_ai_feedback=False,
offer_ephemeral_publication=False,
skip_restated_question=True,
)
try:
# Post in thread as non-ephemeral message
respond_in_thread_or_channel(
client=client.web_client,
channel=channel_id,
receiver_ids=None, # If respond_member_group_list is set, send to them. TODO: check!
text="Hello! Onyx has some results for you!",
blocks=all_blocks,
thread_ts=original_question_ts,
# don't unfurl, since otherwise we will have 5+ previews which makes the message very long
unfurl=False,
send_as_ephemeral=False,
)
except Exception as e:
logger.error(f"Failed to publish ephemeral message: {e}")
raise e
elif action_id == KEEP_TO_YOURSELF_ACTION_ID:
# Keep as ephemeral message in channel or thread, but remove the publish button and add feedback button
changed_blocks = build_slack_response_blocks(
answer=onyx_bot_answer,
message_info=slack_message_info,
channel_conf=channel_conf,
use_citations=True,
feedback_reminder_id=feedback_reminder_id,
skip_ai_feedback=False,
offer_ephemeral_publication=False,
skip_restated_question=True,
)
try:
if slack_message_info.thread_to_respond is not None:
# There seems to be a bug in slack where an update within the thread
# actually leads to the update to be posted in the channel. Therefore,
# for now we delete the original ephemeral message and post a new one
# if the ephemeral message is in a thread.
webhook.send(
response_type="ephemeral",
text="",
blocks=[],
replace_original=True,
delete_original=True,
)
respond_in_thread_or_channel(
client=client.web_client,
channel=channel_id,
receiver_ids=[slack_sender_id],
text="Your personal response, sent as an ephemeral message.",
blocks=changed_blocks,
thread_ts=original_question_ts,
# don't unfurl, since otherwise we will have 5+ previews which makes the message very long
unfurl=False,
send_as_ephemeral=True,
)
else:
# This works fine if the ephemeral message is in the channel
webhook.send(
response_type="ephemeral",
text="Your personal response, sent as an ephemeral message.",
blocks=changed_blocks,
replace_original=True,
delete_original=False,
)
except Exception as e:
logger.error(f"Failed to send webhook: {e}")
def handle_slack_feedback(
feedback_id: str,
feedback_type: str,
@@ -370,20 +153,13 @@ def handle_slack_feedback(
) -> None:
message_id, doc_id, doc_rank = decompose_action_id(feedback_id)
# Get Onyx user from Slack ID
expert_info = expert_info_from_slack_id(
user_id_to_post_confirmation, client, user_cache={}
)
email = expert_info.email if expert_info else None
with get_session_with_current_tenant() as db_session:
onyx_user = get_user_by_email(email, db_session) if email else None
if feedback_type in [LIKE_BLOCK_ACTION_ID, DISLIKE_BLOCK_ACTION_ID]:
create_chat_message_feedback(
is_positive=feedback_type == LIKE_BLOCK_ACTION_ID,
feedback_text="",
chat_message_id=message_id,
user_id=onyx_user.id if onyx_user else None,
user_id=None, # no "user" for Slack bot for now
db_session=db_session,
)
remove_scheduled_feedback_reminder(
@@ -437,7 +213,7 @@ def handle_slack_feedback(
else:
msg = f"<@{user_id_to_post_confirmation}> has {feedback_response_txt} the AI Answer"
respond_in_thread_or_channel(
respond_in_thread(
client=client,
channel=channel_id_to_post_confirmation,
text=msg,
@@ -456,7 +232,7 @@ def handle_followup_button(
action_id = cast(str, action.get("block_id"))
channel_id = req.payload["container"]["channel_id"]
thread_ts = req.payload["container"].get("thread_ts", None)
thread_ts = req.payload["container"]["thread_ts"]
update_emote_react(
emoji=DANSWER_FOLLOWUP_EMOJI,
@@ -489,7 +265,7 @@ def handle_followup_button(
blocks = build_follow_up_resolved_blocks(tag_ids=tag_ids, group_ids=group_ids)
respond_in_thread_or_channel(
respond_in_thread(
client=client.web_client,
channel=channel_id,
text="Received your request for more help",
@@ -539,7 +315,7 @@ def handle_followup_resolved_button(
) -> None:
channel_id = req.payload["container"]["channel_id"]
message_ts = req.payload["container"]["message_ts"]
thread_ts = req.payload["container"].get("thread_ts", None)
thread_ts = req.payload["container"]["thread_ts"]
clicker_name = get_clicker_name(req, client)
@@ -573,7 +349,7 @@ def handle_followup_resolved_button(
resolved_block = SectionBlock(text=msg_text)
respond_in_thread_or_channel(
respond_in_thread(
client=client.web_client,
channel=channel_id,
text="Your request for help as been addressed!",

View File

@@ -18,7 +18,7 @@ from onyx.onyxbot.slack.handlers.handle_standard_answers import (
from onyx.onyxbot.slack.models import SlackMessageInfo
from onyx.onyxbot.slack.utils import fetch_slack_user_ids_from_emails
from onyx.onyxbot.slack.utils import fetch_user_ids_from_groups
from onyx.onyxbot.slack.utils import respond_in_thread_or_channel
from onyx.onyxbot.slack.utils import respond_in_thread
from onyx.onyxbot.slack.utils import slack_usage_report
from onyx.onyxbot.slack.utils import update_emote_react
from onyx.utils.logger import setup_logger
@@ -29,7 +29,7 @@ logger_base = setup_logger()
def send_msg_ack_to_user(details: SlackMessageInfo, client: WebClient) -> None:
if details.is_bot_msg and details.sender_id:
respond_in_thread_or_channel(
respond_in_thread(
client=client,
channel=details.channel_to_respond,
thread_ts=details.msg_to_respond,
@@ -202,7 +202,7 @@ def handle_message(
# which would just respond to the sender
if send_to and is_bot_msg:
if sender_id:
respond_in_thread_or_channel(
respond_in_thread(
client=client,
channel=channel,
receiver_ids=[sender_id],
@@ -220,7 +220,6 @@ def handle_message(
add_slack_user_if_not_exists(db_session, message_info.email)
# first check if we need to respond with a standard answer
# standard answers should be published in a thread
used_standard_answer = handle_standard_answers(
message_info=message_info,
receiver_ids=send_to,

View File

@@ -33,7 +33,7 @@ from onyx.onyxbot.slack.blocks import build_slack_response_blocks
from onyx.onyxbot.slack.handlers.utils import send_team_member_message
from onyx.onyxbot.slack.handlers.utils import slackify_message_thread
from onyx.onyxbot.slack.models import SlackMessageInfo
from onyx.onyxbot.slack.utils import respond_in_thread_or_channel
from onyx.onyxbot.slack.utils import respond_in_thread
from onyx.onyxbot.slack.utils import SlackRateLimiter
from onyx.onyxbot.slack.utils import update_emote_react
from onyx.server.query_and_chat.models import CreateChatMessageRequest
@@ -82,38 +82,12 @@ def handle_regular_answer(
message_ts_to_respond_to = message_info.msg_to_respond
is_bot_msg = message_info.is_bot_msg
# Capture whether response mode for channel is ephemeral. Even if the channel is set
# to respond with an ephemeral message, we still send as non-ephemeral if
# the message is a dm with the Onyx bot.
send_as_ephemeral = (
slack_channel_config.channel_config.get("is_ephemeral", False)
and not message_info.is_bot_dm
)
# If the channel mis configured to respond with an ephemeral message,
# or the message is a dm to the Onyx bot, we should use the proper onyx user from the email.
# This will make documents privately accessible to the user available to Onyx Bot answers.
# Otherwise - if not ephemeral or DM to Onyx Bot - we must use None as the user to restrict
# to public docs.
user = None
if message_info.is_bot_dm or send_as_ephemeral:
if message_info.is_bot_dm:
if message_info.email:
with get_session_with_current_tenant() as db_session:
user = get_user_by_email(message_info.email, db_session)
target_thread_ts = (
None
if send_as_ephemeral and len(message_info.thread_messages) < 2
else message_ts_to_respond_to
)
target_receiver_ids = (
[message_info.sender_id]
if message_info.sender_id and send_as_ephemeral
else receiver_ids
)
document_set_names: list[str] | None = None
prompt = None
# If no persona is specified, use the default search based persona
@@ -160,10 +134,11 @@ def handle_regular_answer(
history_messages = messages[:-1]
single_message_history = slackify_message_thread(history_messages) or None
# Always check for ACL permissions, also for documnt sets that were explicitly added
# to the Bot by the Administrator. (Change relative to earlier behavior where all documents
# in an attached document set were available to all users in the channel.)
bypass_acl = False
if slack_channel_config.persona and slack_channel_config.persona.document_sets:
# For Slack channels, use the full document set, admin will be warned when configuring it
# with non-public document sets
bypass_acl = True
if not message_ts_to_respond_to and not is_bot_msg:
# if the message is not "/onyx" command, then it should have a message ts to respond to
@@ -244,13 +219,12 @@ def handle_regular_answer(
# Optionally, respond in thread with the error message, Used primarily
# for debugging purposes
if should_respond_with_error_msgs:
respond_in_thread_or_channel(
respond_in_thread(
client=client,
channel=channel,
receiver_ids=target_receiver_ids,
receiver_ids=None,
text=f"Encountered exception when trying to answer: \n\n```{e}```",
thread_ts=target_thread_ts,
send_as_ephemeral=send_as_ephemeral,
thread_ts=message_ts_to_respond_to,
)
# In case of failures, don't keep the reaction there permanently
@@ -268,36 +242,32 @@ def handle_regular_answer(
if answer is None:
assert DISABLE_GENERATIVE_AI is True
try:
respond_in_thread_or_channel(
respond_in_thread(
client=client,
channel=channel,
receiver_ids=target_receiver_ids,
receiver_ids=receiver_ids,
text="Hello! Onyx has some results for you!",
blocks=[
SectionBlock(
text="Onyx is down for maintenance.\nWe're working hard on recharging the AI!"
)
],
thread_ts=target_thread_ts,
send_as_ephemeral=send_as_ephemeral,
thread_ts=message_ts_to_respond_to,
# don't unfurl, since otherwise we will have 5+ previews which makes the message very long
unfurl=False,
)
# For DM (ephemeral message), we need to create a thread via a normal message so the user can see
# the ephemeral message. This also will give the user a notification which ephemeral message does not.
# If the channel is ephemeral, we don't need to send a message to the user since they will already see the message
if target_receiver_ids and not send_as_ephemeral:
respond_in_thread_or_channel(
if receiver_ids:
respond_in_thread(
client=client,
channel=channel,
text=(
"👋 Hi, we've just gathered and forwarded the relevant "
+ "information to the team. They'll get back to you shortly!"
),
thread_ts=target_thread_ts,
send_as_ephemeral=send_as_ephemeral,
thread_ts=message_ts_to_respond_to,
)
return False
@@ -346,13 +316,12 @@ def handle_regular_answer(
# Optionally, respond in thread with the error message
# Used primarily for debugging purposes
if should_respond_with_error_msgs:
respond_in_thread_or_channel(
respond_in_thread(
client=client,
channel=channel,
receiver_ids=target_receiver_ids,
receiver_ids=None,
text="Found no documents when trying to answer. Did you index any documents?",
thread_ts=target_thread_ts,
send_as_ephemeral=send_as_ephemeral,
thread_ts=message_ts_to_respond_to,
)
return True
@@ -380,27 +349,15 @@ def handle_regular_answer(
# Optionally, respond in thread with the error message
# Used primarily for debugging purposes
if should_respond_with_error_msgs:
respond_in_thread_or_channel(
respond_in_thread(
client=client,
channel=channel,
receiver_ids=target_receiver_ids,
receiver_ids=None,
text="Found no citations or quotes when trying to answer.",
thread_ts=target_thread_ts,
send_as_ephemeral=send_as_ephemeral,
thread_ts=message_ts_to_respond_to,
)
return True
if (
send_as_ephemeral
and target_receiver_ids is not None
and len(target_receiver_ids) == 1
):
offer_ephemeral_publication = True
skip_ai_feedback = True
else:
offer_ephemeral_publication = False
skip_ai_feedback = False if feedback_reminder_id else True
all_blocks = build_slack_response_blocks(
message_info=message_info,
answer=answer,
@@ -408,39 +365,31 @@ def handle_regular_answer(
use_citations=True, # No longer supporting quotes
feedback_reminder_id=feedback_reminder_id,
expecting_search_result=expecting_search_result,
offer_ephemeral_publication=offer_ephemeral_publication,
skip_ai_feedback=skip_ai_feedback,
)
try:
respond_in_thread_or_channel(
respond_in_thread(
client=client,
channel=channel,
receiver_ids=target_receiver_ids,
receiver_ids=[message_info.sender_id]
if message_info.is_bot_msg and message_info.sender_id
else receiver_ids,
text="Hello! Onyx has some results for you!",
blocks=all_blocks,
thread_ts=target_thread_ts,
thread_ts=message_ts_to_respond_to,
# don't unfurl, since otherwise we will have 5+ previews which makes the message very long
unfurl=False,
send_as_ephemeral=send_as_ephemeral,
)
# For DM (ephemeral message), we need to create a thread via a normal message so the user can see
# the ephemeral message. This also will give the user a notification which ephemeral message does not.
# if there is no message_ts_to_respond_to, and we have made it this far, then this is a /onyx message
# so we shouldn't send_team_member_message
if (
target_receiver_ids
and message_ts_to_respond_to is not None
and not send_as_ephemeral
and target_thread_ts is not None
):
if receiver_ids and message_ts_to_respond_to is not None:
send_team_member_message(
client=client,
channel=channel,
thread_ts=target_thread_ts,
receiver_ids=target_receiver_ids,
send_as_ephemeral=send_as_ephemeral,
thread_ts=message_ts_to_respond_to,
)
return False

View File

@@ -2,7 +2,7 @@ from slack_sdk import WebClient
from onyx.chat.models import ThreadMessage
from onyx.configs.constants import MessageType
from onyx.onyxbot.slack.utils import respond_in_thread_or_channel
from onyx.onyxbot.slack.utils import respond_in_thread
def slackify_message_thread(messages: list[ThreadMessage]) -> str:
@@ -32,10 +32,8 @@ def send_team_member_message(
client: WebClient,
channel: str,
thread_ts: str,
receiver_ids: list[str] | None = None,
send_as_ephemeral: bool = False,
) -> None:
respond_in_thread_or_channel(
respond_in_thread(
client=client,
channel=channel,
text=(
@@ -43,6 +41,4 @@ def send_team_member_message(
+ "information to the team. They'll get back to you shortly!"
),
thread_ts=thread_ts,
receiver_ids=None,
send_as_ephemeral=send_as_ephemeral,
)

View File

@@ -57,9 +57,7 @@ from onyx.onyxbot.slack.constants import FOLLOWUP_BUTTON_ACTION_ID
from onyx.onyxbot.slack.constants import FOLLOWUP_BUTTON_RESOLVED_ACTION_ID
from onyx.onyxbot.slack.constants import GENERATE_ANSWER_BUTTON_ACTION_ID
from onyx.onyxbot.slack.constants import IMMEDIATE_RESOLVED_BUTTON_ACTION_ID
from onyx.onyxbot.slack.constants import KEEP_TO_YOURSELF_ACTION_ID
from onyx.onyxbot.slack.constants import LIKE_BLOCK_ACTION_ID
from onyx.onyxbot.slack.constants import SHOW_EVERYONE_ACTION_ID
from onyx.onyxbot.slack.constants import VIEW_DOC_FEEDBACK_ID
from onyx.onyxbot.slack.handlers.handle_buttons import handle_doc_feedback_button
from onyx.onyxbot.slack.handlers.handle_buttons import handle_followup_button
@@ -69,9 +67,6 @@ from onyx.onyxbot.slack.handlers.handle_buttons import (
from onyx.onyxbot.slack.handlers.handle_buttons import (
handle_generate_answer_button,
)
from onyx.onyxbot.slack.handlers.handle_buttons import (
handle_publish_ephemeral_message_button,
)
from onyx.onyxbot.slack.handlers.handle_buttons import handle_slack_feedback
from onyx.onyxbot.slack.handlers.handle_message import handle_message
from onyx.onyxbot.slack.handlers.handle_message import (
@@ -86,7 +81,7 @@ from onyx.onyxbot.slack.utils import get_onyx_bot_slack_bot_id
from onyx.onyxbot.slack.utils import read_slack_thread
from onyx.onyxbot.slack.utils import remove_onyx_bot_tag
from onyx.onyxbot.slack.utils import rephrase_slack_message
from onyx.onyxbot.slack.utils import respond_in_thread_or_channel
from onyx.onyxbot.slack.utils import respond_in_thread
from onyx.onyxbot.slack.utils import TenantSocketModeClient
from onyx.redis.redis_pool import get_redis_client
from onyx.server.manage.models import SlackBotTokens
@@ -672,11 +667,7 @@ def process_feedback(req: SocketModeRequest, client: TenantSocketModeClient) ->
feedback_msg_reminder = cast(str, action.get("value"))
feedback_id = cast(str, action.get("block_id"))
channel_id = cast(str, req.payload["container"]["channel_id"])
thread_ts = cast(
str,
req.payload["container"].get("thread_ts")
or req.payload["container"].get("message_ts"),
)
thread_ts = cast(str, req.payload["container"]["thread_ts"])
else:
logger.error("Unable to process feedback. Action not found")
return
@@ -792,7 +783,7 @@ def apologize_for_fail(
details: SlackMessageInfo,
client: TenantSocketModeClient,
) -> None:
respond_in_thread_or_channel(
respond_in_thread(
client=client.web_client,
channel=details.channel_to_respond,
thread_ts=details.msg_to_respond,
@@ -868,14 +859,6 @@ def action_routing(req: SocketModeRequest, client: TenantSocketModeClient) -> No
if action["action_id"] in [DISLIKE_BLOCK_ACTION_ID, LIKE_BLOCK_ACTION_ID]:
# AI Answer feedback
return process_feedback(req, client)
elif action["action_id"] in [
SHOW_EVERYONE_ACTION_ID,
KEEP_TO_YOURSELF_ACTION_ID,
]:
# Publish ephemeral message or keep hidden in main channel
return handle_publish_ephemeral_message_button(
req, client, action["action_id"]
)
elif action["action_id"] == FEEDBACK_DOC_BUTTON_BLOCK_ACTION_ID:
# Activation of the "source feedback" button
return handle_doc_feedback_button(req, client)

View File

@@ -1,5 +1,3 @@
from typing import Literal
from pydantic import BaseModel
from onyx.chat.models import ThreadMessage
@@ -15,37 +13,3 @@ class SlackMessageInfo(BaseModel):
bypass_filters: bool # User has tagged @OnyxBot
is_bot_msg: bool # User is using /OnyxBot
is_bot_dm: bool # User is direct messaging to OnyxBot
# Models used to encode the relevant data for the ephemeral message actions
class ActionValuesEphemeralMessageMessageInfo(BaseModel):
bypass_filters: bool | None
channel_to_respond: str | None
msg_to_respond: str | None
email: str | None
sender_id: str | None
thread_messages: list[ThreadMessage] | None
is_bot_msg: bool | None
is_bot_dm: bool | None
thread_to_respond: str | None
class ActionValuesEphemeralMessageChannelConfig(BaseModel):
channel_name: str | None
respond_tag_only: bool | None
respond_to_bots: bool | None
is_ephemeral: bool
respond_member_group_list: list[str] | None
answer_filters: list[
Literal["well_answered_postfilter", "questionmark_prefilter"]
] | None
follow_up_tags: list[str] | None
show_continue_in_web_ui: bool
class ActionValuesEphemeralMessage(BaseModel):
original_question_ts: str | None
feedback_reminder_id: str | None
chat_message_id: int
message_info: ActionValuesEphemeralMessageMessageInfo
channel_conf: ActionValuesEphemeralMessageChannelConfig

View File

@@ -184,7 +184,7 @@ def _build_error_block(error_message: str) -> Block:
backoff=2,
logger=cast(logging.Logger, logger),
)
def respond_in_thread_or_channel(
def respond_in_thread(
client: WebClient,
channel: str,
thread_ts: str | None,
@@ -193,7 +193,6 @@ def respond_in_thread_or_channel(
receiver_ids: list[str] | None = None,
metadata: Metadata | None = None,
unfurl: bool = True,
send_as_ephemeral: bool | None = True,
) -> list[str]:
if not text and not blocks:
raise ValueError("One of `text` or `blocks` must be provided")
@@ -237,7 +236,6 @@ def respond_in_thread_or_channel(
message_ids.append(response["message_ts"])
else:
slack_call = make_slack_api_rate_limited(client.chat_postEphemeral)
for receiver in receiver_ids:
try:
response = slack_call(
@@ -301,12 +299,6 @@ def build_feedback_id(
return unique_prefix + ID_SEPARATOR + feedback_id
def build_publish_ephemeral_message_id(
original_question_ts: str,
) -> str:
return "publish_ephemeral_message__" + original_question_ts
def build_continue_in_web_ui_id(
message_id: int,
) -> str:
@@ -547,7 +539,7 @@ def read_slack_thread(
# If auto-detected filters are on, use the second block for the actual answer
# The first block is the auto-detected filters
if message is not None and message.startswith("_Filters"):
if message.startswith("_Filters"):
if len(blocks) < 2:
logger.warning(f"Only filter blocks found: {reply}")
continue
@@ -619,7 +611,7 @@ class SlackRateLimiter:
def notify(
self, client: WebClient, channel: str, position: int, thread_ts: str | None
) -> None:
respond_in_thread_or_channel(
respond_in_thread(
client=client,
channel=channel,
receiver_ids=None,

View File

@@ -181,7 +181,6 @@ class SlackChannelConfigCreationRequest(BaseModel):
channel_name: str
respond_tag_only: bool = False
respond_to_bots: bool = False
is_ephemeral: bool = False
show_continue_in_web_ui: bool = False
enable_auto_filters: bool = False
# If no team members, assume respond in the channel to everyone

View File

@@ -72,13 +72,11 @@ def set_new_search_settings(
and not search_settings.index_name.endswith(ALT_INDEX_SUFFIX)
):
index_name += ALT_INDEX_SUFFIX
search_values = search_settings_new.model_dump()
search_values = search_settings_new.dict()
search_values["index_name"] = index_name
new_search_settings_request = SavedSearchSettings(**search_values)
else:
new_search_settings_request = SavedSearchSettings(
**search_settings_new.model_dump()
)
new_search_settings_request = SavedSearchSettings(**search_settings_new.dict())
secondary_search_settings = get_secondary_search_settings(db_session)
@@ -105,10 +103,8 @@ def set_new_search_settings(
document_index = get_default_document_index(search_settings, new_search_settings)
document_index.ensure_indices_exist(
primary_embedding_dim=search_settings.final_embedding_dim,
primary_embedding_precision=search_settings.embedding_precision,
secondary_index_embedding_dim=new_search_settings.final_embedding_dim,
secondary_index_embedding_precision=new_search_settings.embedding_precision,
index_embedding_dim=search_settings.model_dim,
secondary_index_embedding_dim=new_search_settings.model_dim,
)
# Pause index attempts for the currently in use index to preserve resources
@@ -141,17 +137,6 @@ def cancel_new_embedding(
db_session=db_session,
)
# remove the old index from the vector db
primary_search_settings = get_current_search_settings(db_session)
document_index = get_default_document_index(primary_search_settings, None)
document_index.ensure_indices_exist(
primary_embedding_dim=primary_search_settings.final_embedding_dim,
primary_embedding_precision=primary_search_settings.embedding_precision,
# just finished swap, no more secondary index
secondary_index_embedding_dim=None,
secondary_index_embedding_precision=None,
)
@router.delete("/delete-search-settings")
def delete_search_settings_endpoint(

View File

@@ -71,15 +71,6 @@ def _form_channel_config(
"also respond to a predetermined set of users."
)
if (
slack_channel_config_creation_request.is_ephemeral
and slack_channel_config_creation_request.respond_member_group_list
):
raise ValueError(
"Cannot set OnyxBot to respond to users in a private (ephemeral) message "
"and also respond to a selected list of users."
)
channel_config: ChannelConfig = {
"channel_name": cleaned_channel_name,
}
@@ -100,8 +91,6 @@ def _form_channel_config(
"respond_to_bots"
] = slack_channel_config_creation_request.respond_to_bots
channel_config["is_ephemeral"] = slack_channel_config_creation_request.is_ephemeral
channel_config["disabled"] = slack_channel_config_creation_request.disabled
return channel_config

View File

@@ -21,7 +21,6 @@ from onyx.db.connector_credential_pair import get_connector_credential_pairs
from onyx.db.connector_credential_pair import resync_cc_pair
from onyx.db.credentials import create_initial_public_credential
from onyx.db.document import check_docs_exist
from onyx.db.enums import EmbeddingPrecision
from onyx.db.index_attempt import cancel_indexing_attempts_past_model
from onyx.db.index_attempt import expire_index_attempts
from onyx.db.llm import fetch_default_provider
@@ -33,7 +32,7 @@ from onyx.db.search_settings import get_current_search_settings
from onyx.db.search_settings import get_secondary_search_settings
from onyx.db.search_settings import update_current_search_settings
from onyx.db.search_settings import update_secondary_search_settings
from onyx.db.swap_index import check_and_perform_index_swap
from onyx.db.swap_index import check_index_swap
from onyx.document_index.factory import get_default_document_index
from onyx.document_index.interfaces import DocumentIndex
from onyx.document_index.vespa.index import VespaIndex
@@ -74,7 +73,7 @@ def setup_onyx(
The Tenant Service calls the tenants/create endpoint which runs this.
"""
check_and_perform_index_swap(db_session=db_session)
check_index_swap(db_session=db_session)
active_search_settings = get_active_search_settings(db_session)
search_settings = active_search_settings.primary
@@ -244,18 +243,10 @@ def setup_vespa(
try:
logger.notice(f"Setting up Vespa (attempt {x+1}/{num_attempts})...")
document_index.ensure_indices_exist(
primary_embedding_dim=index_setting.final_embedding_dim,
primary_embedding_precision=index_setting.embedding_precision,
secondary_index_embedding_dim=(
secondary_index_setting.final_embedding_dim
if secondary_index_setting
else None
),
secondary_index_embedding_precision=(
secondary_index_setting.embedding_precision
if secondary_index_setting
else None
),
index_embedding_dim=index_setting.model_dim,
secondary_index_embedding_dim=secondary_index_setting.model_dim
if secondary_index_setting
else None,
)
logger.notice("Vespa setup complete.")
@@ -369,11 +360,6 @@ def setup_vespa_multitenant(supported_indices: list[SupportedEmbeddingModel]) ->
],
embedding_dims=[index.dim for index in supported_indices]
+ [index.dim for index in supported_indices],
# on the cloud, just use float for all indices, the option to change this
# is not exposed to the user
embedding_precisions=[
EmbeddingPrecision.FLOAT for _ in range(len(supported_indices) * 2)
],
)
logger.notice("Vespa setup complete.")

View File

@@ -136,7 +136,7 @@ def seed_dummy_docs(
search_settings = get_current_search_settings(db_session)
multipass_config = get_multipass_config(search_settings)
index_name = search_settings.index_name
embedding_dim = search_settings.final_embedding_dim
embedding_dim = search_settings.model_dim
vespa_index = VespaIndex(
index_name=index_name,

View File

@@ -30,12 +30,6 @@ class EmbedRequest(BaseModel):
manual_passage_prefix: str | None = None
api_url: str | None = None
api_version: str | None = None
# allows for the truncation of the vector to a lower dimension
# to reduce memory usage. Currently only supported for OpenAI models.
# will be ignored for other providers.
reduced_dimension: int | None = None
# This disables the "model_" protected namespace for pydantic
model_config = {"protected_namespaces": ()}

View File

@@ -17,7 +17,7 @@ from onyx.db.engine import get_session_context_manager
from onyx.db.engine import get_session_with_tenant
from onyx.db.engine import SYNC_DB_API
from onyx.db.search_settings import get_current_search_settings
from onyx.db.swap_index import check_and_perform_index_swap
from onyx.db.swap_index import check_index_swap
from onyx.document_index.document_index_utils import get_multipass_config
from onyx.document_index.vespa.index import DOCUMENT_ID_ENDPOINT
from onyx.document_index.vespa.index import VespaIndex
@@ -194,7 +194,7 @@ def reset_vespa() -> None:
with get_session_context_manager() as db_session:
# swap to the correct default model
check_and_perform_index_swap(db_session)
check_index_swap(db_session)
search_settings = get_current_search_settings(db_session)
multipass_config = get_multipass_config(search_settings)
@@ -289,7 +289,7 @@ def reset_vespa_multitenant() -> None:
for tenant_id in get_all_tenant_ids():
with get_session_with_tenant(tenant_id=tenant_id) as db_session:
# swap to the correct default model for each tenant
check_and_perform_index_swap(db_session)
check_index_swap(db_session)
search_settings = get_current_search_settings(db_session)
multipass_config = get_multipass_config(search_settings)

View File

@@ -142,12 +142,8 @@ def test_web_pruning(reset: None, vespa_client: vespa_fixture) -> None:
selected_cc_pair = CCPairManager.get_indexing_status_by_id(
cc_pair_1.id, user_performing_action=admin_user
)
assert selected_cc_pair is not None, "cc_pair not found after indexing!"
# used to be 15, but now
# localhost:8889/ and localhost:8889/index.html are deduped
assert selected_cc_pair.docs_indexed == 14
assert selected_cc_pair.docs_indexed == 15
logger.info("Removing about.html.")
os.remove(os.path.join(website_tgt, "about.html"))
@@ -164,28 +160,23 @@ def test_web_pruning(reset: None, vespa_client: vespa_fixture) -> None:
cc_pair_1.id, user_performing_action=admin_user
)
assert selected_cc_pair is not None, "cc_pair not found after pruning!"
assert selected_cc_pair.docs_indexed == 12
assert selected_cc_pair.docs_indexed == 13
# check vespa
root_id = f"http://{hostname}:{port}/"
index_id = f"http://{hostname}:{port}/index.html"
about_id = f"http://{hostname}:{port}/about.html"
courses_id = f"http://{hostname}:{port}/courses.html"
doc_ids = [root_id, index_id, about_id, courses_id]
doc_ids = [index_id, about_id, courses_id]
retrieved_docs_dict = vespa_client.get_documents_by_id(doc_ids)["documents"]
retrieved_docs = {
doc["fields"]["document_id"]: doc["fields"]
for doc in retrieved_docs_dict
}
# verify root exists in Vespa
retrieved_doc = retrieved_docs.get(root_id)
assert retrieved_doc
# verify index.html does not exist in Vespa since it is a duplicate of root
# verify index.html exists in Vespa
retrieved_doc = retrieved_docs.get(index_id)
assert not retrieved_doc
assert retrieved_doc
# verify about and courses do not exist
retrieved_doc = retrieved_docs.get(about_id)

View File

@@ -64,7 +64,7 @@ async def test_openai_embedding(
embedding = CloudEmbedding("fake-key", EmbeddingProvider.OPENAI)
result = await embedding._embed_openai(
["test1", "test2"], "text-embedding-ada-002", None
["test1", "test2"], "text-embedding-ada-002"
)
assert result == sample_embeddings
@@ -89,7 +89,6 @@ async def test_embed_text_cloud_provider() -> None:
prefix=None,
api_url=None,
api_version=None,
reduced_dimension=None,
)
assert result == [[0.1, 0.2], [0.3, 0.4]]
@@ -115,7 +114,6 @@ async def test_embed_text_local_model() -> None:
prefix=None,
api_url=None,
api_version=None,
reduced_dimension=None,
)
assert result == [[0.1, 0.2], [0.3, 0.4]]
@@ -159,7 +157,6 @@ async def test_rate_limit_handling() -> None:
prefix=None,
api_url=None,
api_version=None,
reduced_dimension=None,
)
@@ -182,7 +179,6 @@ async def test_concurrent_embeddings() -> None:
manual_passage_prefix=None,
api_url=None,
api_version=None,
reduced_dimension=None,
)
with patch("model_server.encoders.get_embedding_model") as mock_get_model:

114
web/package-lock.json generated
View File

@@ -52,7 +52,7 @@
"lodash": "^4.17.21",
"lucide-react": "^0.454.0",
"mdast-util-find-and-replace": "^3.0.1",
"next": "^15.2.0",
"next": "^15.0.2",
"next-themes": "^0.4.4",
"npm": "^10.8.0",
"postcss": "^8.4.31",
@@ -2631,10 +2631,9 @@
}
},
"node_modules/@next/env": {
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.0.tgz",
"integrity": "sha512-eMgJu1RBXxxqqnuRJQh5RozhskoNUDHBFybvi+Z+yK9qzKeG7dadhv/Vp1YooSZmCnegf7JxWuapV77necLZNA==",
"license": "MIT"
"version": "15.0.2",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.0.2.tgz",
"integrity": "sha512-c0Zr0ModK5OX7D4ZV8Jt/wqoXtitLNPwUfG9zElCZztdaZyNVnN40rDXVZ/+FGuR4CcNV5AEfM6N8f+Ener7Dg=="
},
"node_modules/@next/eslint-plugin-next": {
"version": "14.2.3",
@@ -2646,13 +2645,12 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.0.tgz",
"integrity": "sha512-rlp22GZwNJjFCyL7h5wz9vtpBVuCt3ZYjFWpEPBGzG712/uL1bbSkS675rVAUCRZ4hjoTJ26Q7IKhr5DfJrHDA==",
"version": "15.0.2",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.0.2.tgz",
"integrity": "sha512-GK+8w88z+AFlmt+ondytZo2xpwlfAR8U6CRwXancHImh6EdGfHMIrTSCcx5sOSBei00GyLVL0ioo1JLKTfprgg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -2662,13 +2660,12 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.0.tgz",
"integrity": "sha512-DiU85EqSHogCz80+sgsx90/ecygfCSGl5P3b4XDRVZpgujBm5lp4ts7YaHru7eVTyZMjHInzKr+w0/7+qDrvMA==",
"version": "15.0.2",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.0.2.tgz",
"integrity": "sha512-KUpBVxIbjzFiUZhiLIpJiBoelqzQtVZbdNNsehhUn36e2YzKHphnK8eTUW1s/4aPy5kH/UTid8IuVbaOpedhpw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -2678,13 +2675,12 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.0.tgz",
"integrity": "sha512-VnpoMaGukiNWVxeqKHwi8MN47yKGyki5q+7ql/7p/3ifuU2341i/gDwGK1rivk0pVYbdv5D8z63uu9yMw0QhpQ==",
"version": "15.0.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.0.2.tgz",
"integrity": "sha512-9J7TPEcHNAZvwxXRzOtiUvwtTD+fmuY0l7RErf8Yyc7kMpE47MIQakl+3jecmkhOoIyi/Rp+ddq7j4wG6JDskQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
@@ -2694,13 +2690,12 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.0.tgz",
"integrity": "sha512-ka97/ssYE5nPH4Qs+8bd8RlYeNeUVBhcnsNUmFM6VWEob4jfN9FTr0NBhXVi1XEJpj3cMfgSRW+LdE3SUZbPrw==",
"version": "15.0.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.0.2.tgz",
"integrity": "sha512-BjH4ZSzJIoTTZRh6rG+a/Ry4SW0HlizcPorqNBixBWc3wtQtj4Sn9FnRZe22QqrPnzoaW0ctvSz4FaH4eGKMww==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
@@ -2710,13 +2705,12 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.0.tgz",
"integrity": "sha512-zY1JduE4B3q0k2ZCE+DAF/1efjTXUsKP+VXRtrt/rJCTgDlUyyryx7aOgYXNc1d8gobys/Lof9P9ze8IyRDn7Q==",
"version": "15.0.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.0.2.tgz",
"integrity": "sha512-i3U2TcHgo26sIhcwX/Rshz6avM6nizrZPvrDVDY1bXcLH1ndjbO8zuC7RoHp0NSK7wjJMPYzm7NYL1ksSKFreA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
@@ -2726,13 +2720,12 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.0.tgz",
"integrity": "sha512-QqvLZpurBD46RhaVaVBepkVQzh8xtlUN00RlG4Iq1sBheNugamUNPuZEH1r9X1YGQo1KqAe1iiShF0acva3jHQ==",
"version": "15.0.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.0.2.tgz",
"integrity": "sha512-AMfZfSVOIR8fa+TXlAooByEF4OB00wqnms1sJ1v+iu8ivwvtPvnkwdzzFMpsK5jA2S9oNeeQ04egIWVb4QWmtQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
@@ -2742,13 +2735,12 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.0.tgz",
"integrity": "sha512-ODZ0r9WMyylTHAN6pLtvUtQlGXBL9voljv6ujSlcsjOxhtXPI1Ag6AhZK0SE8hEpR1374WZZ5w33ChpJd5fsjw==",
"version": "15.0.2",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.0.2.tgz",
"integrity": "sha512-JkXysDT0/hEY47O+Hvs8PbZAeiCQVxKfGtr4GUpNAhlG2E0Mkjibuo8ryGD29Qb5a3IOnKYNoZlh/MyKd2Nbww==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
@@ -2758,13 +2750,12 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.0.tgz",
"integrity": "sha512-8+4Z3Z7xa13NdUuUAcpVNA6o76lNPniBd9Xbo02bwXQXnZgFvEopwY2at5+z7yHl47X9qbZpvwatZ2BRo3EdZw==",
"version": "15.0.2",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.0.2.tgz",
"integrity": "sha512-foaUL0NqJY/dX0Pi/UcZm5zsmSk5MtP/gxx3xOPyREkMFN+CTjctPfu3QaqrQHinaKdPnMWPJDKt4VjDfTBe/Q==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
@@ -7398,12 +7389,11 @@
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
"license": "Apache-2.0",
"version": "0.5.13",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz",
"integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==",
"dependencies": {
"tslib": "^2.8.0"
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/typography": {
@@ -14976,14 +14966,13 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
},
"node_modules/next": {
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/next/-/next-15.2.0.tgz",
"integrity": "sha512-VaiM7sZYX8KIAHBrRGSFytKknkrexNfGb8GlG6e93JqueCspuGte8i4ybn8z4ww1x3f2uzY4YpTaBEW4/hvsoQ==",
"license": "MIT",
"version": "15.0.2",
"resolved": "https://registry.npmjs.org/next/-/next-15.0.2.tgz",
"integrity": "sha512-rxIWHcAu4gGSDmwsELXacqAPUk+j8dV/A9cDF5fsiCMpkBDYkO2AEaL1dfD+nNmDiU6QMCFN8Q30VEKapT9UHQ==",
"dependencies": {
"@next/env": "15.2.0",
"@next/env": "15.0.2",
"@swc/counter": "0.1.3",
"@swc/helpers": "0.5.15",
"@swc/helpers": "0.5.13",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
@@ -14993,25 +14982,25 @@
"next": "dist/bin/next"
},
"engines": {
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
"node": ">=18.18.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "15.2.0",
"@next/swc-darwin-x64": "15.2.0",
"@next/swc-linux-arm64-gnu": "15.2.0",
"@next/swc-linux-arm64-musl": "15.2.0",
"@next/swc-linux-x64-gnu": "15.2.0",
"@next/swc-linux-x64-musl": "15.2.0",
"@next/swc-win32-arm64-msvc": "15.2.0",
"@next/swc-win32-x64-msvc": "15.2.0",
"@next/swc-darwin-arm64": "15.0.2",
"@next/swc-darwin-x64": "15.0.2",
"@next/swc-linux-arm64-gnu": "15.0.2",
"@next/swc-linux-arm64-musl": "15.0.2",
"@next/swc-linux-x64-gnu": "15.0.2",
"@next/swc-linux-x64-musl": "15.0.2",
"@next/swc-win32-arm64-msvc": "15.0.2",
"@next/swc-win32-x64-msvc": "15.0.2",
"sharp": "^0.33.5"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
"@playwright/test": "^1.41.2",
"babel-plugin-react-compiler": "*",
"react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
"react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
"react": "^18.2.0 || 19.0.0-rc-02c0e824-20241028",
"react-dom": "^18.2.0 || 19.0.0-rc-02c0e824-20241028",
"sass": "^1.3.0"
},
"peerDependenciesMeta": {
@@ -20601,10 +20590,9 @@
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/type-check": {
"version": "0.4.0",

View File

@@ -55,7 +55,7 @@
"lodash": "^4.17.21",
"lucide-react": "^0.454.0",
"mdast-util-find-and-replace": "^3.0.1",
"next": "^15.2.0",
"next": "^15.0.2",
"next-themes": "^0.4.4",
"npm": "^10.8.0",
"postcss": "^8.4.31",

View File

@@ -163,7 +163,7 @@ export function PersonasTable() {
{popup}
{deleteModalOpen && personaToDelete && (
<ConfirmEntityModal
entityType="Assistant"
entityType="Persona"
entityName={personaToDelete.name}
onClose={closeDeleteModal}
onSubmit={handleDeletePersona}

View File

@@ -83,8 +83,6 @@ export const SlackChannelConfigCreationForm = ({
respond_tag_only:
existingSlackChannelConfig?.channel_config?.respond_tag_only ||
false,
is_ephemeral:
existingSlackChannelConfig?.channel_config?.is_ephemeral || false,
respond_to_bots:
existingSlackChannelConfig?.channel_config?.respond_to_bots ||
false,
@@ -137,7 +135,6 @@ export const SlackChannelConfigCreationForm = ({
questionmark_prefilter_enabled: Yup.boolean().required(),
respond_tag_only: Yup.boolean().required(),
respond_to_bots: Yup.boolean().required(),
is_ephemeral: Yup.boolean().required(),
show_continue_in_web_ui: Yup.boolean().required(),
enable_auto_filters: Yup.boolean().required(),
respond_member_group_list: Yup.array().of(Yup.string()).required(),

View File

@@ -597,13 +597,6 @@ export function SlackChannelConfigFormFields({
label="Respond to Bot messages"
tooltip="If not set, OnyxBot will always ignore messages from Bots"
/>
<CheckFormField
name="is_ephemeral"
label="Respond to user in a private (ephemeral) message"
tooltip="If set, OnyxBot will respond only to the user in a private (ephemeral) message. If you also
chose 'Search' Assistant above, selecting this option will make documents that are private to the user
available for their queries."
/>
<TextArrayField
name="respond_member_group_list"
@@ -642,14 +635,11 @@ export function SlackChannelConfigFormFields({
Privacy Alert
</Label>
<p className="text-sm text-text-darker mb-4">
Please note that if the private (ephemeral) response is *not
selected*, only public documents within the selected document
sets will be accessible for user queries. If the private
(ephemeral) response *is selected*, user quries can also
leverage documents that the user has already been granted
access to. Note that users will be able to share the response
with others in the channel, so please ensure that this is
aligned with your company sharing policies.
Please note that at least one of the documents accessible by
your OnyxBot is marked as private and may contain sensitive
information. These documents will be accessible to all users
of this OnyxBot. Ensure this aligns with your intended
document sharing policy.
</p>
<div className="space-y-2">
<h4 className="text-sm text-text font-medium">

View File

@@ -14,7 +14,6 @@ interface SlackChannelConfigCreationRequest {
answer_validity_check_enabled: boolean;
questionmark_prefilter_enabled: boolean;
respond_tag_only: boolean;
is_ephemeral: boolean;
respond_to_bots: boolean;
show_continue_in_web_ui: boolean;
respond_member_group_list: string[];
@@ -46,7 +45,6 @@ const buildRequestBodyFromCreationRequest = (
channel_name: creationRequest.channel_name,
respond_tag_only: creationRequest.respond_tag_only,
respond_to_bots: creationRequest.respond_to_bots,
is_ephemeral: creationRequest.is_ephemeral,
show_continue_in_web_ui: creationRequest.show_continue_in_web_ui,
enable_auto_filters: creationRequest.enable_auto_filters,
respond_member_group_list: creationRequest.respond_member_group_list,

View File

@@ -71,7 +71,7 @@ function Main() {
<p className="text-text-600">
Learn more about Unstructured{" "}
<a
href="https://docs.unstructured.io/welcome"
href="https://unstructured.io/docs"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline font-medium"

View File

@@ -108,13 +108,15 @@ export default function UpgradingPage({
>
<div>
<div>
Are you sure you want to cancel? Cancelling will revert to the
previous model and all progress will be lost.
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="mt-12 gap-x-2 w-full justify-end flex">
<Button onClick={onCancel}>Confirm</Button>
<Button onClick={() => setIsCancelling(false)} variant="outline">
Cancel
<div className="flex">
<Button onClick={onCancel} variant="submit">
Confirm
</Button>
</div>
</div>
@@ -139,46 +141,30 @@ export default function UpgradingPage({
</Button>
{connectors && connectors.length > 0 ? (
futureEmbeddingModel.background_reindex_enabled ? (
<>
{failedIndexingStatus && failedIndexingStatus.length > 0 && (
<FailedReIndexAttempts
failedIndexingStatuses={failedIndexingStatus}
setPopup={setPopup}
/>
)}
<>
{failedIndexingStatus && failedIndexingStatus.length > 0 && (
<FailedReIndexAttempts
failedIndexingStatuses={failedIndexingStatus}
setPopup={setPopup}
/>
)}
<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>
<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>
{sortedReindexingProgress ? (
<ReindexingProgressTable
reindexingProgress={sortedReindexingProgress}
/>
) : (
<ErrorCallout errorTitle="Failed to fetch re-indexing progress" />
)}
</>
) : (
<div className="mt-8">
<h3 className="text-lg font-semibold mb-2">
Switching Embedding Models
</h3>
<p className="mb-4 text-text-800">
You&apos;re currently switching embedding models, and
you&apos;ve selected the instant switch option. The
transition will complete shortly.
</p>
<p className="text-text-600">
The new model will be active soon.
</p>
</div>
)
{sortedReindexingProgress ? (
<ReindexingProgressTable
reindexingProgress={sortedReindexingProgress}
/>
) : (
<ErrorCallout errorTitle="Failed to fetch reindexing progress" />
)}
</>
) : (
<div className="mt-8 p-6 bg-background-100 border border-border-strong rounded-lg max-w-2xl">
<h3 className="text-lg font-semibold mb-2">

View File

@@ -455,15 +455,15 @@ function Main({ ccPairId }: { ccPairId: number }) {
<Title>Indexing Attempts</Title>
</div>
{indexAttemptErrors && indexAttemptErrors.total_items > 0 && (
<Alert className="border-alert bg-yellow-50 dark:bg-yellow-800 my-2">
<AlertCircle className="h-4 w-4 text-yellow-700 dark:text-yellow-500" />
<AlertTitle className="text-yellow-950 dark:text-yellow-200 font-semibold">
<Alert className="border-alert bg-yellow-50 my-2">
<AlertCircle className="h-4 w-4 text-yellow-700" />
<AlertTitle className="text-yellow-950 font-semibold">
Some documents failed to index
</AlertTitle>
<AlertDescription className="text-yellow-900 dark:text-yellow-300">
<AlertDescription className="text-yellow-900">
{isResolvingErrors ? (
<span>
<span className="text-sm text-yellow-700 dark:text-yellow-400 da animate-pulse">
<span className="text-sm text-yellow-700 animate-pulse">
Resolving failures
</span>
</span>
@@ -471,7 +471,7 @@ function Main({ ccPairId }: { ccPairId: number }) {
<>
We ran into some issues while processing some documents.{" "}
<b
className="text-link cursor-pointer dark:text-blue-300"
className="text-link cursor-pointer"
onClick={() => setShowIndexAttemptErrors(true)}
>
View details.

View File

@@ -1,5 +1,5 @@
import { Label, SubLabel } from "@/components/admin/connectors/Field";
import { ErrorMessage, useField } from "formik";
import { SubLabel } from "@/components/admin/connectors/Field";
import { Field } from "formik";
export default function NumberInput({
label,
@@ -14,36 +14,18 @@ export default function NumberInput({
description?: string;
showNeverIfZero?: boolean;
}) {
const [field, meta, helpers] = useField(name);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// If the input is empty, set the value to undefined or null
// This prevents the "NaN from empty string" error
if (e.target.value === "") {
helpers.setValue(undefined);
} else {
helpers.setValue(Number(e.target.value));
}
};
return (
<div className="w-full flex flex-col">
<Label>
<>
{label}
{optional && <span className="text-text-500 ml-1">(optional)</span>}
</>
</Label>
<label className="block text-base font-medium text-text-700 dark:text-neutral-100 mb-1">
{label}
{optional && <span className="text-text-500 ml-1">(optional)</span>}
</label>
{description && <SubLabel>{description}</SubLabel>}
<input
{...field}
<Field
type="number"
name={name}
min="-1"
onChange={handleChange}
value={
field.value === undefined || field.value === null ? "" : field.value
}
className={`mt-2 block w-full px-3 py-2
bg-[#fff] dark:bg-transparent border border-background-300 rounded-md
text-sm shadow-sm placeholder-text-400
@@ -52,11 +34,6 @@ export default function NumberInput({
invalid:border-pink-500 invalid:text-pink-600
focus:invalid:border-pink-500 focus:invalid:ring-pink-500`}
/>
<ErrorMessage
name={name}
component="div"
className="text-error text-sm mt-1"
/>
</div>
);
}

View File

@@ -0,0 +1,42 @@
import { SubLabel } from "@/components/admin/connectors/Field";
import { Field } from "formik";
export default function NumberInput({
label,
value,
optional,
description,
name,
showNeverIfZero,
}: {
value?: number;
label: string;
name: string;
optional?: boolean;
description?: string;
showNeverIfZero?: boolean;
}) {
return (
<div className="w-full flex flex-col">
<label className="block text-base font-medium text-text-700 mb-1">
{label}
{optional && <span className="text-text-500 ml-1">(optional)</span>}
</label>
{description && <SubLabel>{description}</SubLabel>}
<Field
type="number"
name={name}
min="-1"
value={value === 0 && showNeverIfZero ? "Never" : value}
className={`mt-2 block w-full px-3 py-2
bg-white border border-background-300 rounded-md
text-sm shadow-sm placeholder-text-400
focus:outline-none focus:border-sky-500 focus:ring-1 focus:ring-sky-500
disabled:bg-background-50 disabled:text-text-500 disabled:border-background-200 disabled:shadow-none
invalid:border-pink-500 invalid:text-pink-600
focus:invalid:border-pink-500 focus:invalid:ring-pink-500`}
/>
</div>
);
}

View File

@@ -103,6 +103,42 @@ export function EmbeddingModelSelection({
{ refreshInterval: 5000 } // 5 seconds
);
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-search-settings",
{
method: "POST",
body: JSON.stringify({ ...model, index_name: null }),
headers: {
"Content-Type": "application/json",
},
}
);
if (response.ok) {
setShowTentativeModel(null);
mutate("/api/search-settings/get-secondary-search-settings");
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);
}
};
return (
<div className="p-2">
{alreadySelectedModel && (
@@ -234,9 +270,7 @@ export function EmbeddingModelSelection({
{modelTab == "open" && (
<OpenEmbeddingPage
selectedProvider={selectedProvider}
onSelectOpenSource={(model: HostedEmbeddingModel) => {
setShowTentativeOpenProvider(model);
}}
onSelectOpenSource={onSelectOpenSource}
/>
)}

View File

@@ -30,10 +30,6 @@ interface RerankingDetailsFormProps {
originalRerankingDetails: RerankingDetails;
modelTab: "open" | "cloud" | null;
setModelTab: Dispatch<SetStateAction<"open" | "cloud" | null>>;
onValidationChange?: (
isValid: boolean,
errors: Record<string, string>
) => void;
}
const RerankingDetailsForm = forwardRef<
@@ -47,7 +43,6 @@ const RerankingDetailsForm = forwardRef<
currentRerankingDetails,
modelTab,
setModelTab,
onValidationChange,
},
ref
) => {
@@ -60,78 +55,26 @@ const RerankingDetailsForm = forwardRef<
const combinedSettings = useContext(SettingsContext);
const gpuEnabled = combinedSettings?.settings.gpu_enabled;
// Define the validation schema
const validationSchema = Yup.object().shape({
rerank_model_name: Yup.string().nullable(),
rerank_provider_type: Yup.mixed<RerankerProvider>()
.nullable()
.oneOf(Object.values(RerankerProvider))
.optional(),
rerank_api_key: Yup.string()
.nullable()
.test(
"required-if-cohere",
"API Key is required for Cohere reranking",
function (value) {
const { rerank_provider_type } = this.parent;
return (
rerank_provider_type !== RerankerProvider.COHERE ||
(value !== null && value !== "")
);
}
),
rerank_api_url: Yup.string()
.url("Must be a valid URL")
.matches(/^https?:\/\//, "URL must start with http:// or https://")
.nullable()
.test(
"required-if-litellm",
"API URL is required for LiteLLM reranking",
function (value) {
const { rerank_provider_type } = this.parent;
return (
rerank_provider_type !== RerankerProvider.LITELLM ||
(value !== null && value !== "")
);
}
),
});
return (
<Formik
innerRef={ref}
initialValues={currentRerankingDetails}
validationSchema={validationSchema}
validationSchema={Yup.object().shape({
rerank_model_name: Yup.string().nullable(),
rerank_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"),
rerank_api_url: Yup.string()
.url("Must be a valid URL")
.matches(/^https?:\/\//, "URL must start with http:// or https://")
.nullable(),
})}
onSubmit={async (_, { setSubmitting }) => {
setSubmitting(false);
}}
validate={(values) => {
// Update parent component with values
setRerankingDetails(values);
// Run validation and report errors
if (onValidationChange) {
// We'll return an empty object here since Yup will handle the actual validation
// But we need to check if there are any validation errors
const errors: Record<string, string> = {};
try {
// Manually validate against the schema
validationSchema.validateSync(values, { abortEarly: false });
onValidationChange(true, {});
} catch (validationError) {
if (validationError instanceof Yup.ValidationError) {
validationError.inner.forEach((err) => {
if (err.path) {
errors[err.path] = err.message;
}
});
onValidationChange(false, errors);
}
}
}
return {}; // Return empty object as Formik will handle the errors
}}
enableReinitialize={true}
>
{({ values, setFieldValue, resetForm }) => {

View File

@@ -20,11 +20,6 @@ export enum RerankerProvider {
LITELLM = "litellm",
}
export enum EmbeddingPrecision {
FLOAT = "float",
BFLOAT16 = "bfloat16",
}
export interface AdvancedSearchConfiguration {
index_name: string | null;
multipass_indexing: boolean;
@@ -32,15 +27,12 @@ export interface AdvancedSearchConfiguration {
disable_rerank_for_streaming: boolean;
api_url: string | null;
num_rerank: number;
embedding_precision: EmbeddingPrecision;
reduced_dimension: number | null;
}
export interface SavedSearchSettings
extends RerankingDetails,
AdvancedSearchConfiguration {
provider_type: EmbeddingProvider | null;
background_reindex_enabled: boolean;
}
export interface RerankingModel {

View File

@@ -1,37 +0,0 @@
import { Modal } from "@/components/Modal";
import { Button } from "@/components/ui/button";
interface InstantSwitchConfirmModalProps {
onClose: () => void;
onConfirm: () => void;
}
export const InstantSwitchConfirmModal = ({
onClose,
onConfirm,
}: InstantSwitchConfirmModalProps) => {
return (
<Modal
onOutsideClick={onClose}
width="max-w-3xl"
title="Are you sure you want to do an instant switch?"
>
<>
<div>
Instant switching will immediately change the embedding model without
re-indexing. Searches will be over a partial set of documents
(starting with 0 documents) until re-indexing is complete.
<br />
<br />
<b>This is not reversible.</b>
</div>
<div className="flex mt-4 gap-x-2 justify-end">
<Button onClick={onConfirm}>Confirm</Button>
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
</div>
</>
</Modal>
);
};

View File

@@ -51,10 +51,9 @@ export function ModelSelectionConfirmationModal({
</Callout>
)}
<div className="flex mt-8 gap-x-2 justify-end">
<Button onClick={onConfirm}>Confirm</Button>
<Button variant="outline" onClick={onCancel}>
Cancel
<div className="flex mt-8">
<Button className="mx-auto" variant="submit" onClick={onConfirm}>
Yes
</Button>
</div>
</div>

View File

@@ -21,14 +21,15 @@ export function SelectModelModal({
>
<div className="mb-4">
<Text className="text-lg mb-2">
You&apos;re selecting a new embedding model, <b>{model.model_name}</b>
. If you update to this model, you will need to undergo a complete
re-indexing. Are you sure?
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 gap-x-2">
<Button onClick={onConfirm}>Confirm</Button>
<Button variant="outline" onClick={onCancel}>
Cancel
<div className="flex mt-8 justify-end">
<Button variant="submit" onClick={onConfirm}>
Yes
</Button>
</div>
</div>

View File

@@ -3,15 +3,13 @@ import { Formik, Form, FormikProps, FieldArray, Field } from "formik";
import * as Yup from "yup";
import { TrashIcon } from "@/components/icons/icons";
import { FaPlus } from "react-icons/fa";
import { AdvancedSearchConfiguration, EmbeddingPrecision } from "../interfaces";
import { AdvancedSearchConfiguration } from "../interfaces";
import {
BooleanFormField,
Label,
SubLabel,
SelectorFormField,
} from "@/components/admin/connectors/Field";
import NumberInput from "../../connectors/[connector]/pages/ConnectorInput/NumberInput";
import { StringOrNumberOption } from "@/components/Dropdown";
interface AdvancedEmbeddingFormPageProps {
updateAdvancedEmbeddingDetails: (
@@ -19,207 +17,102 @@ interface AdvancedEmbeddingFormPageProps {
value: any
) => void;
advancedEmbeddingDetails: AdvancedSearchConfiguration;
embeddingProviderType: string | null;
onValidationChange?: (
isValid: boolean,
errors: Record<string, string>
) => void;
}
// Options for embedding precision based on EmbeddingPrecision enum
const embeddingPrecisionOptions: StringOrNumberOption[] = [
{ name: EmbeddingPrecision.BFLOAT16, value: EmbeddingPrecision.BFLOAT16 },
{ name: EmbeddingPrecision.FLOAT, value: EmbeddingPrecision.FLOAT },
];
const AdvancedEmbeddingFormPage = forwardRef<
FormikProps<any>,
AdvancedEmbeddingFormPageProps
>(
(
{
updateAdvancedEmbeddingDetails,
advancedEmbeddingDetails,
embeddingProviderType,
onValidationChange,
},
ref
) => {
return (
<div className="py-4 rounded-lg max-w-4xl px-4 mx-auto">
<Formik
innerRef={ref}
initialValues={advancedEmbeddingDetails}
validationSchema={Yup.object().shape({
multilingual_expansion: Yup.array().of(Yup.string()),
multipass_indexing: Yup.boolean(),
disable_rerank_for_streaming: Yup.boolean(),
num_rerank: Yup.number()
.required("Number of results to rerank is required")
.min(1, "Must be at least 1"),
embedding_precision: Yup.string().nullable(),
reduced_dimension: Yup.number()
.nullable()
.test(
"positive",
"Must be larger than or equal to 256",
(value) => value === null || value === undefined || value >= 256
)
.test(
"openai",
"Reduced Dimensions is only supported for OpenAI embedding models",
(value) => {
return embeddingProviderType === "openai" || value === null;
}
),
})}
onSubmit={async (_, { setSubmitting }) => {
setSubmitting(false);
}}
validate={(values) => {
// Call updateAdvancedEmbeddingDetails for each changed field
Object.entries(values).forEach(([key, value]) => {
updateAdvancedEmbeddingDetails(
key as keyof AdvancedSearchConfiguration,
value
);
});
>(({ updateAdvancedEmbeddingDetails, advancedEmbeddingDetails }, ref) => {
return (
<div className="py-4 rounded-lg max-w-4xl px-4 mx-auto">
<Formik
innerRef={ref}
initialValues={advancedEmbeddingDetails}
validationSchema={Yup.object().shape({
multilingual_expansion: Yup.array().of(Yup.string()),
multipass_indexing: Yup.boolean(),
disable_rerank_for_streaming: Yup.boolean(),
num_rerank: Yup.number(),
})}
onSubmit={async (_, { setSubmitting }) => {
setSubmitting(false);
}}
validate={(values) => {
// Call updateAdvancedEmbeddingDetails for each changed field
Object.entries(values).forEach(([key, value]) => {
updateAdvancedEmbeddingDetails(
key as keyof AdvancedSearchConfiguration,
value
);
});
}}
enableReinitialize={true}
>
{({ values }) => (
<Form>
<FieldArray name="multilingual_expansion">
{({ push, remove }) => (
<div className="w-full">
<Label>Multi-lingual Expansion</Label>
// Run validation and report errors
if (onValidationChange) {
// We'll return an empty object here since Yup will handle the actual validation
// But we need to check if there are any validation errors
const errors: Record<string, string> = {};
try {
// Manually validate against the schema
Yup.object()
.shape({
multilingual_expansion: Yup.array().of(Yup.string()),
multipass_indexing: Yup.boolean(),
disable_rerank_for_streaming: Yup.boolean(),
num_rerank: Yup.number()
.required("Number of results to rerank is required")
.min(1, "Must be at least 1"),
embedding_precision: Yup.string().nullable(),
reduced_dimension: Yup.number()
.nullable()
.test(
"positive",
"Must be larger than or equal to 256",
(value) =>
value === null || value === undefined || value >= 256
)
.test(
"openai",
"Reduced Dimensions is only supported for OpenAI embedding models",
(value) => {
return (
embeddingProviderType === "openai" || value === null
);
}
),
})
.validateSync(values, { abortEarly: false });
onValidationChange(true, {});
} catch (validationError) {
if (validationError instanceof Yup.ValidationError) {
validationError.inner.forEach((err) => {
if (err.path) {
errors[err.path] = err.message;
}
});
onValidationChange(false, errors);
}
}
}
return {}; // Return empty object as Formik will handle the errors
}}
enableReinitialize={true}
>
{({ values }) => (
<Form>
<FieldArray name="multilingual_expansion">
{({ push, remove }) => (
<div className="w-full">
<Label>Multi-lingual Expansion</Label>
<SubLabel>Add additional languages to the search.</SubLabel>
{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
<SubLabel>Add additional languages to the search.</SubLabel>
{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`}
/>
<button
type="button"
onClick={() => remove(index)}
className={`p-2 my-auto bg-input flex-none rounded-md
/>
<button
type="button"
onClick={() => remove(index)}
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
>
<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>
>
<FaPlus className="mr-2" />
Add Language
</button>
</div>
)}
</FieldArray>
<BooleanFormField
subtext="Enable multipass indexing for both mini and large chunks."
optional
label="Multipass Indexing"
name="multipass_indexing"
/>
<BooleanFormField
subtext="Disable reranking for streaming to improve response time."
optional
label="Disable Rerank for Streaming"
name="disable_rerank_for_streaming"
/>
<NumberInput
description="Number of results to rerank"
optional={false}
label="Number of Results to Rerank"
name="num_rerank"
/>
<SelectorFormField
name="embedding_precision"
label="Embedding Precision"
options={embeddingPrecisionOptions}
subtext="Select the precision for embedding vectors. Lower precision uses less storage but may reduce accuracy."
/>
<NumberInput
description="Number of dimensions to reduce the embedding to.
Will reduce memory usage but may reduce accuracy.
If not specified, will just use the selected model's default dimensionality without any reduction.
Currently only supported for OpenAI embedding models"
optional={true}
label="Reduced Dimension"
name="reduced_dimension"
/>
</Form>
)}
</Formik>
</div>
);
}
);
<BooleanFormField
subtext="Enable multipass indexing for both mini and large chunks."
optional
label="Multipass Indexing"
name="multipass_indexing"
/>
<BooleanFormField
subtext="Disable reranking for streaming to improve response time."
optional
label="Disable Rerank for Streaming"
name="disable_rerank_for_streaming"
/>
<NumberInput
description="Number of results to rerank"
optional={false}
label="Number of Results to Rerank"
name="num_rerank"
/>
</Form>
)}
</Formik>
</div>
);
});
export default AdvancedEmbeddingFormPage;
AdvancedEmbeddingFormPage.displayName = "AdvancedEmbeddingFormPage";

View File

@@ -3,16 +3,10 @@ import { usePopup } from "@/components/admin/connectors/Popup";
import { HealthCheckBanner } from "@/components/health/healthcheck";
import { EmbeddingModelSelection } from "../EmbeddingModelSelectionForm";
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import Text from "@/components/ui/text";
import { Button } from "@/components/ui/button";
import {
ArrowLeft,
ArrowRight,
WarningCircle,
CaretDown,
Warning,
} from "@phosphor-icons/react";
import { ArrowLeft, ArrowRight, WarningCircle } from "@phosphor-icons/react";
import {
CloudEmbeddingModel,
EmbeddingProvider,
@@ -25,35 +19,16 @@ import { ThreeDotsLoader } from "@/components/Loading";
import AdvancedEmbeddingFormPage from "./AdvancedEmbeddingFormPage";
import {
AdvancedSearchConfiguration,
EmbeddingPrecision,
RerankingDetails,
SavedSearchSettings,
} from "../interfaces";
import RerankingDetailsForm from "../RerankingFormPage";
import { useEmbeddingFormContext } from "@/components/context/EmbeddingContext";
import { Modal } from "@/components/Modal";
import { InstantSwitchConfirmModal } from "../modals/InstantSwitchConfirmModal";
import { useRouter } from "next/navigation";
import CardSection from "@/components/admin/CardSection";
import { combineSearchSettings } from "./utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
enum ReindexType {
REINDEX = "reindex",
INSTANT = "instant",
}
export default function EmbeddingForm() {
const { formStep, nextFormStep, prevFormStep } = useEmbeddingFormContext();
@@ -68,8 +43,6 @@ export default function EmbeddingForm() {
disable_rerank_for_streaming: false,
api_url: null,
num_rerank: 0,
embedding_precision: EmbeddingPrecision.FLOAT,
reduced_dimension: null,
});
const [rerankingDetails, setRerankingDetails] = useState<RerankingDetails>({
@@ -79,19 +52,6 @@ export default function EmbeddingForm() {
rerank_api_url: null,
});
const [reindexType, setReindexType] = useState<ReindexType>(
ReindexType.REINDEX
);
const [formErrors, setFormErrors] = useState<Record<string, string>>({});
const [isFormValid, setIsFormValid] = useState(true);
const [rerankFormErrors, setRerankFormErrors] = useState<
Record<string, string>
>({});
const [isRerankFormValid, setIsRerankFormValid] = useState(true);
const advancedFormRef = useRef(null);
const rerankFormRef = useRef(null);
const updateAdvancedEmbeddingDetails = (
key: keyof AdvancedSearchConfiguration,
value: any
@@ -122,8 +82,6 @@ export default function EmbeddingForm() {
};
const [displayPoorModelName, setDisplayPoorModelName] = useState(true);
const [showPoorModel, setShowPoorModel] = useState(false);
const [showInstantSwitchConfirm, setShowInstantSwitchConfirm] =
useState(false);
const [modelTab, setModelTab] = useState<"open" | "cloud" | null>(null);
const {
@@ -157,8 +115,6 @@ export default function EmbeddingForm() {
searchSettings.disable_rerank_for_streaming,
num_rerank: searchSettings.num_rerank,
api_url: null,
embedding_precision: searchSettings.embedding_precision,
reduced_dimension: searchSettings.reduced_dimension,
});
setRerankingDetails({
@@ -190,14 +146,17 @@ export default function EmbeddingForm() {
}
}, [currentEmbeddingModel]);
const handleReindex = async () => {
const update = await updateSearch();
if (update) {
await onConfirm();
}
};
const needsReIndex =
currentEmbeddingModel != selectedProvider ||
searchSettings?.multipass_indexing !=
advancedEmbeddingDetails.multipass_indexing ||
searchSettings?.embedding_precision !=
advancedEmbeddingDetails.embedding_precision ||
searchSettings?.reduced_dimension !=
advancedEmbeddingDetails.reduced_dimension;
advancedEmbeddingDetails.multipass_indexing;
const updateSearch = useCallback(async () => {
if (!selectedProvider) {
@@ -207,44 +166,18 @@ export default function EmbeddingForm() {
selectedProvider,
advancedEmbeddingDetails,
rerankingDetails,
selectedProvider.provider_type?.toLowerCase() as EmbeddingProvider | null,
reindexType === ReindexType.REINDEX
selectedProvider.provider_type?.toLowerCase() as EmbeddingProvider | null
);
const response = await updateSearchSettings(searchSettings);
if (response.ok) {
return true;
} else {
setPopup({
message: "Failed to update search settings",
type: "error",
});
setPopup({ message: "Failed to update search settings", type: "error" });
return false;
}
}, [selectedProvider, advancedEmbeddingDetails, rerankingDetails, setPopup]);
const handleValidationChange = useCallback(
(isValid: boolean, errors: Record<string, string>) => {
setIsFormValid(isValid);
setFormErrors(errors);
},
[]
);
const handleRerankValidationChange = useCallback(
(isValid: boolean, errors: Record<string, string>) => {
setIsRerankFormValid(isValid);
setRerankFormErrors(errors);
},
[]
);
// Combine validation states for both forms
const isOverallFormValid = isFormValid && isRerankFormValid;
const combinedFormErrors = useMemo(() => {
return { ...formErrors, ...rerankFormErrors };
}, [formErrors, rerankFormErrors]);
const ReIndexingButton = useMemo(() => {
const ReIndexingButtonComponent = ({
needsReIndex,
@@ -253,204 +186,47 @@ export default function EmbeddingForm() {
}) => {
return needsReIndex ? (
<div className="flex mx-auto gap-x-1 ml-auto items-center">
<div className="flex items-center">
<button
onClick={() => {
if (reindexType == ReindexType.INSTANT) {
setShowInstantSwitchConfirm(true);
} else {
handleReIndex();
navigateToEmbeddingPage("search settings");
}
}}
disabled={!isOverallFormValid}
className="
enabled:cursor-pointer
disabled:bg-accent/50
disabled:cursor-not-allowed
bg-agent
flex
items-center
justify-center
text-white
text-sm
font-regular
rounded-l-sm
py-2.5
px-3.5
transition-colors
hover:bg-white/10
text-center
w-32"
>
{reindexType == ReindexType.REINDEX
? "Re-index"
: "Instant Switch"}
</button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
disabled={!isOverallFormValid}
className="
enabled:cursor-pointer
disabled:bg-accent/50
disabled:cursor-not-allowed
bg-agent
flex
items-center
justify-center
text-white
text-sm
font-regular
rounded-r-sm
border-l
border-white/20
py-2.5
px-2
h-[40px]
w-[34px]
transition-colors
hover:bg-white/10"
>
<CaretDown className="h-4 w-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
onClick={() => {
setReindexType(ReindexType.REINDEX);
}}
>
<TooltipProvider>
<Tooltip>
<TooltipTrigger className="w-full text-left">
(Recommended) Re-index
</TooltipTrigger>
<TooltipContent>
<p>
Re-runs all connectors in the background before
switching over. Takes longer but ensures no
degredation of search during the switch.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setReindexType(ReindexType.INSTANT);
}}
>
<TooltipProvider>
<Tooltip>
<TooltipTrigger className="w-full text-left">
Instant Switch
</TooltipTrigger>
<TooltipContent>
<p>
Immediately switches to new settings without
re-indexing. Searches will be degraded until the
re-indexing is complete.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{isOverallFormValid && (
<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>
)}
{searchSettings?.embedding_precision !=
advancedEmbeddingDetails.embedding_precision && (
<li>Embedding precision modification</li>
)}
{searchSettings?.reduced_dimension !=
advancedEmbeddingDetails.reduced_dimension && (
<li>Reduced dimension modification</li>
)}
</ul>
</div>
<button
className="enabled:cursor-pointer disabled:bg-accent/50 disabled:cursor-not-allowed bg-agent flex gap-x-1 items-center text-white py-2.5 px-3.5 text-sm font-regular rounded-sm"
onClick={handleReindex}
>
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>
)}
{!isOverallFormValid &&
Object.keys(combinedFormErrors).length > 0 && (
<div className="relative group">
<Warning
className="text-red-500 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">Validation Errors:</p>
<ul className="list-disc pl-5">
{Object.entries(combinedFormErrors).map(
([field, error]) => (
<li key={field}>
{field}: {error}
</li>
)
)}
</ul>
</div>
</div>
)}
</div>
</div>
) : (
<div className="flex mx-auto gap-x-1 ml-auto items-center">
<button
className="enabled:cursor-pointer ml-auto disabled:bg-accent/50 disabled:cursor-not-allowed bg-agent flex mx-auto gap-x-1 items-center text-white py-2.5 px-3.5 text-sm font-regular rounded-sm"
onClick={() => {
updateSearch();
navigateToEmbeddingPage("search settings");
}}
disabled={!isOverallFormValid}
>
Update Search
</button>
{!isOverallFormValid &&
Object.keys(combinedFormErrors).length > 0 && (
<div className="relative group">
<Warning
className="text-red-500 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 text-red-400">
Validation Errors:
</p>
<ul className="list-disc pl-5">
{Object.entries(combinedFormErrors).map(
([field, error]) => (
<li key={field}>{error}</li>
)
)}
</ul>
</div>
</div>
)}
</div>
<button
className="enabled:cursor-pointer ml-auto disabled:bg-accent/50 disabled:cursor-not-allowed bg-agent 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();
navigateToEmbeddingPage("search settings");
}}
>
Update Search
</button>
);
};
ReIndexingButtonComponent.displayName = "ReIndexingButton";
return ReIndexingButtonComponent;
}, [needsReIndex, reindexType, isOverallFormValid, combinedFormErrors]);
}, [needsReIndex, updateSearch]);
if (!selectedProvider) {
return <ThreeDotsLoader />;
@@ -470,7 +246,7 @@ export default function EmbeddingForm() {
router.push("/admin/configuration/search?message=search-settings");
};
const handleReIndex = async () => {
const onConfirm = async () => {
if (!selectedProvider) {
return;
}
@@ -484,8 +260,7 @@ export default function EmbeddingForm() {
rerankingDetails,
selectedProvider.provider_type
?.toLowerCase()
.split(" ")[0] as EmbeddingProvider | null,
reindexType === ReindexType.REINDEX
.split(" ")[0] as EmbeddingProvider | null
);
} else {
// This is a locally hosted model
@@ -493,8 +268,7 @@ export default function EmbeddingForm() {
selectedProvider,
advancedEmbeddingDetails,
rerankingDetails,
null,
reindexType === ReindexType.REINDEX
null
);
}
@@ -607,17 +381,6 @@ export default function EmbeddingForm() {
</Modal>
)}
{showInstantSwitchConfirm && (
<InstantSwitchConfirmModal
onClose={() => setShowInstantSwitchConfirm(false)}
onConfirm={() => {
setShowInstantSwitchConfirm(false);
handleReIndex();
navigateToEmbeddingPage("search settings");
}}
/>
)}
{formStep == 1 && (
<>
<h2 className="text-2xl font-bold mb-4 text-text-800">
@@ -632,7 +395,6 @@ export default function EmbeddingForm() {
<CardSection>
<RerankingDetailsForm
ref={rerankFormRef}
setModelTab={setModelTab}
modelTab={
originalRerankingDetails.rerank_model_name
@@ -642,7 +404,6 @@ export default function EmbeddingForm() {
currentRerankingDetails={rerankingDetails}
originalRerankingDetails={originalRerankingDetails}
setRerankingDetails={setRerankingDetails}
onValidationChange={handleRerankValidationChange}
/>
</CardSection>
@@ -683,11 +444,8 @@ export default function EmbeddingForm() {
<CardSection>
<AdvancedEmbeddingFormPage
ref={advancedFormRef}
advancedEmbeddingDetails={advancedEmbeddingDetails}
updateAdvancedEmbeddingDetails={updateAdvancedEmbeddingDetails}
embeddingProviderType={selectedProvider.provider_type}
onValidationChange={handleValidationChange}
/>
</CardSection>

View File

@@ -16,7 +16,7 @@ export default function OpenEmbeddingPage({
onSelectOpenSource,
selectedProvider,
}: {
onSelectOpenSource: (model: HostedEmbeddingModel) => void;
onSelectOpenSource: (model: HostedEmbeddingModel) => Promise<void>;
selectedProvider: HostedEmbeddingModel | CloudEmbeddingModel;
}) {
const [configureModel, setConfigureModel] = useState(false);

View File

@@ -63,14 +63,12 @@ export const combineSearchSettings = (
selectedProvider: CloudEmbeddingProvider | HostedEmbeddingModel,
advancedEmbeddingDetails: AdvancedSearchConfiguration,
rerankingDetails: RerankingDetails,
provider_type: EmbeddingProvider | null,
background_reindex_enabled: boolean
provider_type: EmbeddingProvider | null
): SavedSearchSettings => {
return {
...selectedProvider,
...advancedEmbeddingDetails,
...rerankingDetails,
provider_type: provider_type,
background_reindex_enabled,
};
};

View File

@@ -206,7 +206,7 @@ export function SharedChatDisplay({
{chatSession.description || `Unnamed Chat`}
</h1>
<p className=" text-text-darker">
{humanReadableFormat(chatSession.time_created)}
{humanReadableFormat(chatSession.time_updated)}
</p>
<div
className={`

View File

@@ -51,13 +51,13 @@ export function Label({
className?: string;
}) {
return (
<label
className={`block font-medium text-text-700 dark:text-neutral-100 ${className} ${
small ? "text-sm" : "text-base"
<div
className={`block text-text-darker font-medium base ${className} ${
small ? "text-xs" : "text-sm"
}`}
>
{children}
</label>
</div>
);
}
@@ -686,7 +686,7 @@ export function SelectorFormField({
defaultValue,
tooltip,
includeReset = false,
fontSize = "md",
fontSize = "sm",
small = false,
}: SelectorFormFieldProps) {
const [field] = useField<string>(name);

View File

@@ -181,7 +181,7 @@ const SignedUpUserTable = ({
: "All Roles"}
</SelectValue>
</SelectTrigger>
<SelectContent className="bg-background">
<SelectContent className="bg-background-50">
{Object.entries(USER_ROLE_LABELS)
.filter(([role]) => role !== UserRole.EXT_PERM_USER)
.map(([role, label]) => (

View File

@@ -29,7 +29,6 @@ export function ReindexingProgressTable({
<TableHead className="w-1/7 sm:w-1/5">Connector Name</TableHead>
<TableHead className="w-3/7 sm:w-1/5">Status</TableHead>
<TableHead className="w-3/7 sm:w-1/5">Docs Re-Indexed</TableHead>
<TableHead className="w-3/7 sm:w-1/5"></TableHead>
</TableRow>
</TableHeader>
<TableBody>

View File

@@ -55,7 +55,6 @@ export interface EmbeddingModelDescriptor {
api_version?: string | null;
deployment_name?: string | null;
index_name: string | null;
background_reindex_enabled?: boolean;
}
export interface CloudEmbeddingModel extends EmbeddingModelDescriptor {

View File

@@ -25,8 +25,8 @@ export function mockedRefreshToken(): CustomRefreshTokenResponse {
*/
const mockExp = Date.now() + 3600000; // 1 hour from now in milliseconds
const data: CustomRefreshTokenResponse = {
access_token: "Mock access token",
refresh_token: "Mock refresh token",
access_token: "asdf Mock access token",
refresh_token: "asdf Mock refresh token",
session: { exp: mockExp },
userinfo: {
sub: "Mock email",

View File

@@ -85,7 +85,7 @@ export const LLMSelector: React.FC<LLMSelectorProps> = ({
<span>{userSettings ? "System Default" : "User Default"}</span>
{userSettings && (
<span className=" my-auto font-normal ml-1">
({defaultModelDisplayName})
({defaultModelDisplayName}) asdf
</span>
)}
</SelectItem>

View File

@@ -10,8 +10,8 @@ const alertVariants = cva(
variant: {
broken:
"border-red-500/50 text-red-500 dark:border-red-500 [&>svg]:text-red-500 dark:border-red-900/50 dark:text-red-100 dark:dark:border-red-900 dark:[&>svg]:text-red-700 bg-red-50 dark:bg-red-950",
ark: "border-amber-500/50 text-amber-500 dark:border-amber-500 [&>svg]:text-amber-500 dark:border-amber-900/50 dark:text-amber-900 dark:dark:border-amber-900 dark:[&>svg]:text-amber-900 bg-amber-50 dark:bg-amber-950",
info: "border-[#fff]/50 dark:border-[#fff] dark:border-[#fff]/50 dark:dark:border-[#fff]",
ark: "border-amber-400/50 text-amber-400 dark:border-amber-400 [&>svg]:text-amber-400 dark:border-amber-800/50 dark:text-amber-800 dark:dark:border-amber-800 dark:[&>svg]:text-amber-800 bg-amber-50 dark:bg-amber-950",
info: "border-black/50 dark:border-black dark:border-black/50 dark:dark:border-black",
default:
"bg-neutral-50 text-neutral-darker dark:bg-neutral-950 dark:text-text",
destructive:

View File

@@ -273,7 +273,6 @@ export interface ChannelConfig {
channel_name: string;
respond_tag_only?: boolean;
respond_to_bots?: boolean;
is_ephemeral?: boolean;
show_continue_in_web_ui?: boolean;
respond_member_group_list?: string[];
answer_filters?: AnswerFilterOption[];