mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-02-21 09:45:46 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75103d2f8b | ||
|
|
e8f7c34a72 | ||
|
|
1a378448f4 |
@@ -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")
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 ""
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": ()}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
114
web/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -163,7 +163,7 @@ export function PersonasTable() {
|
||||
{popup}
|
||||
{deleteModalOpen && personaToDelete && (
|
||||
<ConfirmEntityModal
|
||||
entityType="Assistant"
|
||||
entityType="Persona"
|
||||
entityName={personaToDelete.name}
|
||||
onClose={closeDeleteModal}
|
||||
onSubmit={handleDeletePersona}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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're currently switching embedding models, and
|
||||
you'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">
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -21,14 +21,15 @@ export function SelectModelModal({
|
||||
>
|
||||
<div className="mb-4">
|
||||
<Text className="text-lg mb-2">
|
||||
You'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'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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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={`
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]) => (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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[];
|
||||
|
||||
Reference in New Issue
Block a user